typst/tests/typeset.rs

240 lines
6.5 KiB
Rust
Raw Normal View History

use std::cell::RefCell;
use std::env;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::BufWriter;
use std::path::Path;
use std::rc::Rc;
2020-10-12 17:59:21 +03:00
use fontdock::fs::{FsIndex, FsSource};
use raqote::{DrawTarget, PathBuilder, SolidSource, Source, Transform, Vector};
use ttf_parser::OutlineBuilder;
use typstc::diag::{Feedback, Pass};
2020-10-04 20:06:20 +03:00
use typstc::eval::State;
use typstc::export::pdf;
2020-09-30 14:18:42 +03:00
use typstc::font::{FontLoader, SharedFontLoader};
use typstc::geom::{Length, Point};
use typstc::layout::{BoxLayout, LayoutElement};
use typstc::parse::LineMap;
use typstc::shaping::Shaped;
use typstc::typeset;
const TEST_DIR: &str = "tests";
const OUT_DIR: &str = "tests/out";
const FONT_DIR: &str = "fonts";
const BLACK: SolidSource = SolidSource { r: 0, g: 0, b: 0, a: 255 };
const WHITE: SolidSource = SolidSource { r: 255, g: 255, b: 255, a: 255 };
fn main() {
let filter = TestFilter::new(env::args().skip(1));
let mut filtered = Vec::new();
for entry in fs::read_dir(TEST_DIR).unwrap() {
let path = entry.unwrap().path();
if path.extension() != Some(OsStr::new("typ")) {
continue;
}
let name = path.file_stem().unwrap().to_string_lossy().to_string();
if filter.matches(&name) {
let src = fs::read_to_string(&path).unwrap();
filtered.push((name, path, src));
}
}
let len = filtered.len();
if len == 0 {
return;
} else if len == 1 {
println!("Running test ...");
} else {
println!("Running {} tests", len);
}
fs::create_dir_all(OUT_DIR).unwrap();
let mut index = FsIndex::new();
index.search_dir(FONT_DIR);
2020-10-12 17:59:21 +03:00
let (files, descriptors) = index.into_vecs();
let loader = Rc::new(RefCell::new(FontLoader::new(
Box::new(FsSource::new(files)),
descriptors,
)));
for (name, path, src) in filtered {
test(&name, &src, &path, &loader)
}
}
fn test(name: &str, src: &str, src_path: &Path, loader: &SharedFontLoader) {
println!("Testing {}.", name);
2020-10-04 20:06:20 +03:00
let state = State::default();
let Pass {
output: layouts,
2020-10-04 21:22:11 +03:00
feedback: Feedback { mut diags, .. },
2020-10-12 18:10:01 +03:00
} = typeset(&src, state, Rc::clone(loader));
2020-10-04 21:22:11 +03:00
if !diags.is_empty() {
diags.sort();
let map = LineMap::new(&src);
2020-10-04 21:22:11 +03:00
for diag in diags {
let span = diag.span;
let start = map.location(span.start);
let end = map.location(span.end);
println!(
" {}: {}:{}-{}: {}",
2020-10-04 21:22:11 +03:00
diag.v.level,
src_path.display(),
start,
end,
2020-10-04 21:22:11 +03:00
diag.v.message,
);
}
}
2020-09-30 14:18:42 +03:00
let loader = loader.borrow();
let png_path = format!("{}/{}.png", OUT_DIR, name);
2020-10-12 22:26:58 +03:00
let surface = render(&layouts, &loader, 3.0);
surface.write_png(png_path).unwrap();
let pdf_path = format!("{}/{}.pdf", OUT_DIR, name);
let file = BufWriter::new(File::create(pdf_path).unwrap());
pdf::export(&layouts, &loader, file).unwrap();
}
struct TestFilter {
filter: Vec<String>,
perfect: bool,
}
impl TestFilter {
fn new(args: impl Iterator<Item = String>) -> Self {
let mut filter = Vec::new();
let mut perfect = false;
for arg in args {
match arg.as_str() {
"--nocapture" => {}
"=" => perfect = true,
_ => filter.push(arg),
}
}
Self { filter, 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 render(layouts: &[BoxLayout], loader: &FontLoader, scale: f64) -> DrawTarget {
let pad = Length::pt(scale * 10.0);
2020-08-30 23:18:55 +03:00
let width = 2.0 * pad
+ layouts
.iter()
2020-10-03 14:23:59 +03:00
.map(|l| scale * l.size.width)
2020-08-30 23:18:55 +03:00
.max_by(|a, b| a.partial_cmp(&b).unwrap())
.unwrap();
2020-08-30 23:18:55 +03:00
let height =
pad + layouts.iter().map(|l| scale * l.size.height + pad).sum::<Length>();
let int_width = width.to_pt().round() as i32;
let int_height = height.to_pt().round() as i32;
let mut surface = DrawTarget::new(int_width, int_height);
surface.clear(BLACK);
let mut offset = Point::new(pad, pad);
for layout in layouts {
surface.fill_rect(
offset.x.to_pt() as f32,
offset.y.to_pt() as f32,
(scale * layout.size.width).to_pt() as f32,
(scale * layout.size.height).to_pt() as f32,
&Source::Solid(WHITE),
&Default::default(),
);
for &(pos, ref element) in &layout.elements {
match element {
LayoutElement::Text(shaped) => render_shaped(
&mut surface,
loader,
shaped,
scale * pos + offset,
scale,
),
}
}
2020-10-03 14:23:59 +03:00
offset.y += scale * layout.size.height + pad;
}
surface
}
fn render_shaped(
surface: &mut DrawTarget,
2020-09-30 14:18:42 +03:00
loader: &FontLoader,
shaped: &Shaped,
2020-10-03 14:23:59 +03:00
pos: Point,
scale: f64,
) {
2020-09-30 14:18:42 +03:00
let face = loader.get_loaded(shaped.face).get();
for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) {
let mut builder = WrappedPathBuilder(PathBuilder::new());
face.outline_glyph(glyph, &mut builder);
let path = builder.0.finish();
let units_per_em = face.units_per_em().unwrap_or(1000);
2020-10-11 23:38:30 +03:00
let s = scale * (shaped.font_size / units_per_em as f64);
let x = pos.x + scale * offset;
2020-10-11 23:38:30 +03:00
let y = pos.y + scale * shaped.font_size;
let t = Transform::create_scale(s.to_pt() as f32, -s.to_pt() as f32)
.post_translate(Vector::new(x.to_pt() as f32, y.to_pt() as f32));
surface.fill(
&path.transform(&t),
&Source::Solid(SolidSource { r: 0, g: 0, b: 0, a: 255 }),
&Default::default(),
)
}
}
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();
}
}