typst/tests/typeset.rs

518 lines
15 KiB
Rust
Raw Normal View History

use std::cell::RefCell;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
use std::rc::Rc;
use fontdock::fs::FsIndex;
2020-12-01 00:07:08 +03:00
use image::{GenericImageView, Rgba};
2020-11-25 20:46:47 +03:00
use tiny_skia::{
Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, PathBuilder, Pattern, Pixmap,
Rect, SpreadMode, Transform,
};
use ttf_parser::OutlineBuilder;
use walkdir::WalkDir;
use typst::diag::{Diag, DiagSet, Level, Pass};
use typst::env::{Env, FsIndexExt, ImageResource, ResourceLoader};
use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
2021-02-09 21:46:57 +03:00
use typst::exec::State;
2020-10-13 12:47:29 +03:00
use typst::export::pdf;
use typst::geom::{Length, Point, Sides, Size};
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Shaped};
use typst::library;
use typst::parse::{LineMap, Scanner};
use typst::pretty::pretty;
use typst::syntax::{Location, Pos};
2020-10-13 12:47:29 +03:00
use typst::typeset;
2021-02-20 19:53:40 +03:00
const TYP_DIR: &str = "./typ";
const REF_DIR: &str = "./ref";
const PNG_DIR: &str = "./png";
const PDF_DIR: &str = "./pdf";
const FONT_DIR: &str = "../fonts";
fn main() {
env::set_current_dir(env::current_dir().unwrap().join("tests")).unwrap();
let args = Args::new(env::args().skip(1));
let mut filtered = Vec::new();
for entry in WalkDir::new(".").into_iter() {
let entry = entry.unwrap();
if entry.depth() <= 1 {
continue;
}
let src_path = entry.into_path();
if src_path.extension() != Some(OsStr::new("typ")) {
continue;
}
if args.matches(&src_path.to_string_lossy()) {
filtered.push(src_path);
}
}
let len = filtered.len();
2021-01-13 23:33:22 +03:00
if len == 1 {
println!("Running test ...");
2021-01-13 23:33:22 +03:00
} else if len > 1 {
println!("Running {} tests", len);
}
let mut index = FsIndex::new();
index.search_dir(FONT_DIR);
let mut env = Env {
fonts: index.into_dynamic_loader(),
resources: ResourceLoader::new(),
};
2021-01-13 23:33:22 +03:00
let mut ok = true;
for src_path in filtered {
let path = src_path.strip_prefix(TYP_DIR).unwrap();
let png_path = Path::new(PNG_DIR).join(path).with_extension("png");
let ref_path = Path::new(REF_DIR).join(path).with_extension("png");
let pdf_path =
args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));
ok &= test(
&mut env,
&src_path,
&png_path,
&ref_path,
pdf_path.as_deref(),
);
2021-01-13 16:07:38 +03:00
}
if !ok {
std::process::exit(1);
}
}
struct Args {
filter: Vec<String>,
pdf: bool,
perfect: bool,
}
impl Args {
fn new(args: impl Iterator<Item = String>) -> Self {
let mut filter = Vec::new();
let mut perfect = false;
let mut pdf = false;
for arg in args {
match arg.as_str() {
"--nocapture" => {}
"--pdf" => pdf = true,
"=" => perfect = true,
_ => filter.push(arg),
}
}
Self { filter, pdf, perfect }
}
fn matches(&self, name: &str) -> bool {
if self.perfect {
self.filter.iter().any(|p| name == p)
} else {
2020-08-30 23:18:55 +03:00
self.filter.is_empty() || self.filter.iter().any(|p| name.contains(p))
}
}
}
fn test(
env: &mut Env,
src_path: &Path,
png_path: &Path,
2021-02-20 19:53:40 +03:00
ref_path: &Path,
pdf_path: Option<&Path>,
) -> bool {
let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path);
println!("Testing {}", name.display());
let src = fs::read_to_string(src_path).unwrap();
2021-01-13 16:07:38 +03:00
let mut ok = true;
let mut frames = vec![];
let mut lines = 0;
2021-02-01 00:43:11 +03:00
let mut compare_ref = true;
let mut compare_ever = false;
2021-02-01 00:43:11 +03:00
let parts: Vec<_> = src.split("---").collect();
for (i, part) in parts.iter().enumerate() {
let is_header = i == 0
&& parts.len() > 1
&& part
.lines()
.all(|s| s.starts_with("//") || s.chars().all(|c| c.is_whitespace()));
if is_header {
for line in part.lines() {
if line.starts_with("// Ref: false") {
compare_ref = false;
}
}
} else {
let (part_ok, compare_here, part_frames) =
test_part(env, part, i, compare_ref, lines);
2021-02-01 00:43:11 +03:00
ok &= part_ok;
compare_ever |= compare_here;
2021-02-01 00:43:11 +03:00
frames.extend(part_frames);
}
lines += part.lines().count() as u32;
2021-01-13 16:07:38 +03:00
}
if compare_ever {
if let Some(pdf_path) = pdf_path {
let pdf_data = pdf::export(&env, &frames);
fs::create_dir_all(&pdf_path.parent().unwrap()).unwrap();
fs::write(pdf_path, pdf_data).unwrap();
}
2021-01-13 16:07:38 +03:00
2021-02-11 23:22:06 +03:00
let canvas = draw(&env, &frames, 2.0);
fs::create_dir_all(&png_path.parent().unwrap()).unwrap();
2021-01-14 01:19:44 +03:00
canvas.pixmap.save_png(png_path).unwrap();
2021-02-20 19:53:40 +03:00
if let Ok(ref_pixmap) = Pixmap::load_png(ref_path) {
if canvas.pixmap != ref_pixmap {
println!(" Does not match reference image. ❌");
2021-01-13 16:07:38 +03:00
ok = false;
}
2021-02-20 19:53:40 +03:00
} else {
println!(" Failed to open reference image. ❌");
ok = false;
2021-01-13 16:07:38 +03:00
}
}
if ok {
println!("\x1b[1ATesting {}", name.display());
2021-01-13 16:07:38 +03:00
}
ok
}
2021-02-01 00:43:11 +03:00
fn test_part(
env: &mut Env,
2021-02-01 00:43:11 +03:00
src: &str,
i: usize,
compare_ref: bool,
lines: u32,
) -> (bool, bool, Vec<Frame>) {
let map = LineMap::new(src);
2021-02-01 00:43:11 +03:00
let (local_compare_ref, ref_diags) = parse_metadata(src, &map);
let compare_ref = local_compare_ref.unwrap_or(compare_ref);
let mut scope = library::new();
let panics = Rc::new(RefCell::new(vec![]));
register_helpers(&mut scope, Rc::clone(&panics));
2021-01-14 01:19:44 +03:00
// We want to have "unbounded" pages, so we allow them to be infinitely
// large and fit them to match their content.
let mut state = State::default();
2021-01-14 01:19:44 +03:00
state.page.size = Size::new(Length::pt(120.0), Length::raw(f64::INFINITY));
state.page.margins = Sides::uniform(Some(Length::pt(10.0).into()));
2021-01-13 17:44:41 +03:00
let Pass { output: mut frames, diags } = typeset(env, &src, &scope, state);
2021-01-13 16:07:38 +03:00
if !compare_ref {
frames.clear();
}
let mut ok = true;
for panic in &*panics.borrow() {
let line = map.location(panic.pos).unwrap().line;
println!(" Assertion failed in line {}", lines + line);
if let (Some(lhs), Some(rhs)) = (&panic.lhs, &panic.rhs) {
println!(" Left: {:?}", lhs);
println!(" Right: {:?}", rhs);
} else {
println!(" Missing argument.");
}
ok = false;
}
if diags != ref_diags {
2021-01-13 16:07:38 +03:00
println!(" Subtest {} does not match expected diagnostics. ❌", i);
ok = false;
for diag in &diags {
if !ref_diags.contains(diag) {
print!(" Not annotated | ");
print_diag(diag, &map, lines);
}
}
for diag in &ref_diags {
if !diags.contains(diag) {
print!(" Not emitted | ");
print_diag(diag, &map, lines);
}
}
}
(ok, compare_ref, frames)
}
fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) {
let mut diags = DiagSet::new();
2021-02-01 00:43:11 +03:00
let mut compare_ref = None;
for (i, line) in src.lines().enumerate() {
2021-02-01 00:43:11 +03:00
let line = line.trim();
if line.starts_with("// Ref: false") {
compare_ref = Some(false);
}
if line.starts_with("// Ref: true") {
compare_ref = Some(true);
}
2021-01-13 16:07:38 +03:00
let (level, rest) = if let Some(rest) = line.strip_prefix("// Warning: ") {
(Level::Warning, rest)
} else if let Some(rest) = line.strip_prefix("// Error: ") {
2021-01-13 16:07:38 +03:00
(Level::Error, rest)
} else {
continue;
};
fn num(s: &mut Scanner) -> u32 {
s.eat_while(|c| c.is_numeric()).parse().unwrap()
}
let pos = |s: &mut Scanner| -> Pos {
let first = num(s);
let (delta, column) =
if s.eat_if(':') { (first, num(s)) } else { (1, first) };
let line = i as u32 + 1 + delta;
2021-01-26 15:49:04 +03:00
map.pos(Location::new(line, column)).unwrap()
};
2021-01-13 16:07:38 +03:00
let mut s = Scanner::new(rest);
2021-02-17 23:30:20 +03:00
let start = pos(&mut s);
let end = if s.eat_if('-') { pos(&mut s) } else { start };
diags.insert(Diag::new(start .. end, level, s.rest().trim()));
}
(compare_ref, diags)
}
struct Panic {
pos: Pos,
lhs: Option<Value>,
rhs: Option<Value>,
}
fn register_helpers(scope: &mut Scope, panics: Rc<RefCell<Vec<Panic>>>) {
pub fn args(_: &mut EvalContext, args: &mut FuncArgs) -> Value {
let repr = pretty(args);
2021-02-10 00:56:44 +03:00
args.items.clear();
Value::template("args", move |ctx| {
let snapshot = ctx.state.clone();
ctx.set_monospace();
ctx.push_text(&repr);
ctx.state = snapshot;
})
}
let test = move |ctx: &mut EvalContext, args: &mut FuncArgs| -> Value {
let lhs = args.require::<Value>(ctx, "left-hand side");
let rhs = args.require::<Value>(ctx, "right-hand side");
if lhs != rhs {
panics.borrow_mut().push(Panic { pos: args.span.start, lhs, rhs });
Value::Str(format!("(panic)"))
} else {
Value::None
}
};
scope.def_const("error", Value::Error);
scope.def_const("args", FuncValue::new(Some("args".into()), args));
scope.def_const("test", FuncValue::new(Some("test".into()), test));
}
fn print_diag(diag: &Diag, map: &LineMap, lines: u32) {
let mut start = map.location(diag.span.start).unwrap();
let mut end = map.location(diag.span.end).unwrap();
start.line += lines;
end.line += lines;
println!("{}: {}-{}: {}", diag.level, start, end, diag.message);
}
2021-02-11 23:22:06 +03:00
fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Canvas {
2020-11-25 20:46:47 +03:00
let pad = Length::pt(5.0);
2021-01-03 02:12:09 +03:00
let height = pad + frames.iter().map(|l| l.size.height + pad).sum::<Length>();
2020-08-30 23:18:55 +03:00
let width = 2.0 * pad
2021-01-03 02:12:09 +03:00
+ frames
2020-08-30 23:18:55 +03:00
.iter()
2020-11-25 20:46:47 +03:00
.map(|l| l.size.width)
2020-08-30 23:18:55 +03:00
.max_by(|a, b| a.partial_cmp(&b).unwrap())
.unwrap_or_default();
2020-08-30 23:18:55 +03:00
2020-11-25 20:46:47 +03:00
let pixel_width = (pixel_per_pt * width.to_pt() as f32) as u32;
let pixel_height = (pixel_per_pt * height.to_pt() as f32) as u32;
2021-01-14 01:19:44 +03:00
if pixel_width > 4000 || pixel_height > 4000 {
panic!(
"overlarge image: {} by {} ({} x {})",
pixel_width, pixel_height, width, height,
);
2021-01-14 01:19:44 +03:00
}
2020-11-25 20:46:47 +03:00
let mut canvas = Canvas::new(pixel_width, pixel_height).unwrap();
canvas.scale(pixel_per_pt, pixel_per_pt);
canvas.pixmap.fill(Color::BLACK);
2020-11-25 20:46:47 +03:00
let mut origin = Point::new(pad, pad);
2021-01-03 02:12:09 +03:00
for frame in frames {
2020-11-25 20:46:47 +03:00
let mut paint = Paint::default();
paint.set_color(Color::WHITE);
canvas.fill_rect(
Rect::from_xywh(
origin.x.to_pt() as f32,
origin.y.to_pt() as f32,
2021-01-03 02:12:09 +03:00
frame.size.width.to_pt() as f32,
frame.size.height.to_pt() as f32,
2020-11-25 20:46:47 +03:00
)
.unwrap(),
&paint,
);
2021-01-03 02:12:09 +03:00
for &(pos, ref element) in &frame.elements {
2020-11-25 20:46:47 +03:00
let pos = origin + pos;
match element {
2021-01-03 02:12:09 +03:00
Element::Text(shaped) => {
2021-02-11 23:22:06 +03:00
draw_text(env, &mut canvas, pos, shaped);
}
2021-01-03 02:12:09 +03:00
Element::Image(image) => {
2021-02-11 23:22:06 +03:00
draw_image(env, &mut canvas, pos, image);
}
2021-02-04 23:30:18 +03:00
Element::Geometry(geom) => {
2021-02-11 23:22:06 +03:00
draw_geometry(env, &mut canvas, pos, geom);
2021-02-04 23:30:18 +03:00
}
}
}
2021-01-03 02:12:09 +03:00
origin.y += frame.size.height + pad;
}
2020-11-25 20:46:47 +03:00
canvas
}
2021-02-11 23:22:06 +03:00
fn draw_text(env: &Env, canvas: &mut Canvas, pos: Point, shaped: &Shaped) {
2020-12-17 17:43:30 +03:00
let face = env.fonts.face(shaped.face).get();
for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) {
2020-11-25 20:46:47 +03:00
let units_per_em = face.units_per_em().unwrap_or(1000);
let x = (pos.x + offset).to_pt() as f32;
let y = pos.y.to_pt() as f32;
2020-11-25 20:46:47 +03:00
let scale = (shaped.font_size / units_per_em as f64).to_pt() as f32;
let mut builder = WrappedPathBuilder(PathBuilder::new());
face.outline_glyph(glyph, &mut builder);
2020-12-17 14:16:17 +03:00
if let Some(path) = builder.0.finish() {
let placed = path
.transform(&Transform::from_row(scale, 0.0, 0.0, -scale, x, y).unwrap())
.unwrap();
2020-11-25 20:46:47 +03:00
2021-03-20 00:36:13 +03:00
let mut paint = paint_from_fill(shaped.color);
2020-12-17 14:16:17 +03:00
paint.anti_alias = true;
2020-11-25 20:46:47 +03:00
2020-12-17 14:16:17 +03:00
canvas.fill_path(&placed, &paint, FillRule::default());
}
}
}
2021-02-11 23:22:06 +03:00
fn draw_geometry(_: &Env, canvas: &mut Canvas, pos: Point, element: &Geometry) {
2021-02-04 23:30:18 +03:00
let x = pos.x.to_pt() as f32;
let y = pos.y.to_pt() as f32;
2021-03-20 00:36:13 +03:00
let paint = paint_from_fill(element.fill);
2021-02-04 23:30:18 +03:00
2021-02-06 14:30:44 +03:00
match &element.shape {
Shape::Rect(s) => {
let (w, h) = (s.width.to_pt() as f32, s.height.to_pt() as f32);
canvas.fill_rect(Rect::from_xywh(x, y, w, h).unwrap(), &paint);
}
2021-02-06 14:30:44 +03:00
};
2021-02-04 23:30:18 +03:00
}
2021-03-20 00:36:13 +03:00
fn paint_from_fill(fill: Fill) -> Paint<'static> {
let mut paint = Paint::default();
match fill {
Fill::Color(c) => match c {
typst::color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a),
},
Fill::Image(_) => todo!(),
}
paint
}
2021-02-11 23:22:06 +03:00
fn draw_image(env: &Env, canvas: &mut Canvas, pos: Point, element: &Image) {
2021-01-01 19:54:31 +03:00
let img = &env.resources.loaded::<ImageResource>(element.res);
2021-01-01 19:54:31 +03:00
let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap();
for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {
let Rgba([r, g, b, a]) = src;
*dest = ColorU8::from_rgba(r, g, b, a).premultiply();
}
2021-01-01 19:54:31 +03:00
let view_width = element.size.width.to_pt() as f32;
let view_height = element.size.height.to_pt() as f32;
2020-11-25 20:46:47 +03:00
let x = pos.x.to_pt() as f32;
let y = pos.y.to_pt() as f32;
let scale_x = view_width as f32 / pixmap.width() as f32;
let scale_y = view_height as f32 / pixmap.height() as f32;
let mut paint = Paint::default();
paint.shader = Pattern::new(
&pixmap,
SpreadMode::Pad,
FilterQuality::Bilinear,
1.0,
Transform::from_row(scale_x, 0.0, 0.0, scale_y, x, y).unwrap(),
);
canvas.fill_rect(
Rect::from_xywh(x, y, view_width, view_height).unwrap(),
&paint,
);
}
struct WrappedPathBuilder(PathBuilder);
impl OutlineBuilder for WrappedPathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.0.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.0.line_to(x, y);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.0.quad_to(x1, y1, x, y);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.0.cubic_to(x1, y1, x2, y2, x, y);
}
fn close(&mut self) {
self.0.close();
}
}