2020-08-02 22:17:42 +03:00
use std ::cell ::RefCell ;
use std ::env ;
use std ::ffi ::OsStr ;
2020-12-11 00:44:35 +03:00
use std ::fs ;
2020-08-17 17:18:55 +03:00
use std ::path ::Path ;
2020-08-02 22:17:42 +03:00
use std ::rc ::Rc ;
2020-10-12 17:59:21 +03:00
use fontdock ::fs ::{ FsIndex , FsSource } ;
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 ,
} ;
2020-08-02 22:17:42 +03:00
use ttf_parser ::OutlineBuilder ;
2021-01-14 18:47:29 +03:00
use walkdir ::WalkDir ;
2020-08-02 22:17:42 +03:00
2020-12-11 00:44:35 +03:00
use typst ::diag ::{ Diag , Feedback , Level , Pass } ;
2020-12-01 00:07:08 +03:00
use typst ::env ::{ Env , ImageResource , ResourceLoader , SharedEnv } ;
2021-01-13 17:44:41 +03:00
use typst ::eval ::{ Args , EvalContext , State , Value , ValueFunc } ;
2020-10-13 12:47:29 +03:00
use typst ::export ::pdf ;
2020-11-28 00:35:42 +03:00
use typst ::font ::FontLoader ;
2021-01-14 01:19:44 +03:00
use typst ::geom ::{ Length , Point , Sides , Size , Spec } ;
use typst ::layout ::{ Element , Expansion , Frame , Image } ;
2020-12-11 00:44:35 +03:00
use typst ::parse ::{ LineMap , Scanner } ;
2020-10-13 12:47:29 +03:00
use typst ::shaping ::Shaped ;
2021-01-01 19:54:31 +03:00
use typst ::syntax ::{ Location , Pos , SpanVec , Spanned , WithSpan } ;
2020-10-13 12:47:29 +03:00
use typst ::typeset ;
2020-08-02 22:17:42 +03:00
2020-11-20 18:36:22 +03:00
const TYP_DIR : & str = " typ " ;
2020-12-11 00:44:35 +03:00
const REF_DIR : & str = " ref " ;
2021-01-13 16:07:38 +03:00
const PNG_DIR : & str = " png " ;
const PDF_DIR : & str = " pdf " ;
2020-12-11 00:44:35 +03:00
const FONT_DIR : & str = " ../fonts " ;
2020-08-02 22:17:42 +03:00
fn main ( ) {
2020-11-20 18:36:22 +03:00
env ::set_current_dir ( env ::current_dir ( ) . unwrap ( ) . join ( " tests " ) ) . unwrap ( ) ;
2020-08-02 22:17:42 +03:00
let filter = TestFilter ::new ( env ::args ( ) . skip ( 1 ) ) ;
let mut filtered = Vec ::new ( ) ;
2021-01-14 18:47:29 +03:00
for entry in WalkDir ::new ( TYP_DIR ) . into_iter ( ) {
let entry = entry . unwrap ( ) ;
let src_path = entry . into_path ( ) ;
2020-10-13 13:34:11 +03:00
if src_path . extension ( ) ! = Some ( OsStr ::new ( " typ " ) ) {
2020-08-02 22:17:42 +03:00
continue ;
}
2021-01-14 18:47:29 +03:00
if filter . matches ( & src_path . to_string_lossy ( ) . to_string ( ) ) {
filtered . push ( src_path ) ;
2020-08-02 22:17:42 +03:00
}
}
let len = filtered . len ( ) ;
2021-01-13 23:33:22 +03:00
if len = = 1 {
2020-08-02 22:17:42 +03:00
println! ( " Running test ... " ) ;
2021-01-13 23:33:22 +03:00
} else if len > 1 {
2020-08-02 22:17:42 +03:00
println! ( " Running {} tests " , len ) ;
}
let mut index = FsIndex ::new ( ) ;
index . search_dir ( FONT_DIR ) ;
2020-10-12 17:59:21 +03:00
let ( files , descriptors ) = index . into_vecs ( ) ;
2020-11-28 00:35:42 +03:00
let env = Rc ::new ( RefCell ::new ( Env {
fonts : FontLoader ::new ( Box ::new ( FsSource ::new ( files ) ) , descriptors ) ,
resources : ResourceLoader ::new ( ) ,
} ) ) ;
2020-08-02 22:17:42 +03:00
2021-01-13 23:33:22 +03:00
let playground = Path ::new ( " playground.typ " ) ;
2021-01-14 18:47:29 +03:00
if playground . exists ( ) & & filtered . is_empty ( ) {
2021-01-13 23:33:22 +03:00
test (
playground ,
Path ::new ( " playground.png " ) ,
Path ::new ( " playground.pdf " ) ,
None ,
& env ,
) ;
}
2020-10-13 13:34:11 +03:00
2021-01-13 23:33:22 +03:00
let mut ok = true ;
2021-01-14 18:47:29 +03:00
for src_path in filtered {
let relative = src_path . strip_prefix ( TYP_DIR ) . unwrap ( ) ;
let png_path = Path ::new ( PNG_DIR ) . join ( & relative ) . with_extension ( " png " ) ;
let pdf_path = Path ::new ( PDF_DIR ) . join ( & relative ) . with_extension ( " pdf " ) ;
let ref_path = Path ::new ( REF_DIR ) . join ( & relative ) . with_extension ( " png " ) ;
ok & = test ( & src_path , & png_path , & pdf_path , Some ( & ref_path ) , & env ) ;
2021-01-13 16:07:38 +03:00
}
2020-10-13 13:34:11 +03:00
if ! ok {
std ::process ::exit ( 1 ) ;
}
}
2020-08-02 22:17:42 +03:00
struct TestFilter {
filter : Vec < String > ,
perfect : bool ,
}
impl TestFilter {
2020-08-03 17:01:23 +03:00
fn new ( args : impl Iterator < Item = String > ) -> Self {
2020-08-02 22:17:42 +03:00
let mut filter = Vec ::new ( ) ;
let mut perfect = false ;
for arg in args {
match arg . as_str ( ) {
2020-08-03 17:01:23 +03:00
" --nocapture " = > { }
2020-08-02 22:17:42 +03:00
" = " = > perfect = true ,
_ = > filter . push ( arg ) ,
}
}
2020-08-03 17:01:23 +03:00
Self { filter , perfect }
2020-08-02 22:17:42 +03:00
}
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 ) )
2020-08-02 22:17:42 +03:00
}
}
}
2020-12-11 00:44:35 +03:00
fn test (
src_path : & Path ,
png_path : & Path ,
2021-01-13 16:07:38 +03:00
pdf_path : & Path ,
ref_path : Option < & Path > ,
2020-12-11 00:44:35 +03:00
env : & SharedEnv ,
) -> bool {
2021-01-14 18:47:29 +03:00
let name = src_path . strip_prefix ( TYP_DIR ) . unwrap_or ( src_path ) ;
println! ( " Testing {} " , name . display ( ) ) ;
2020-12-11 00:44:35 +03:00
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! [ ] ;
for ( i , part ) in src . split ( " --- " ) . enumerate ( ) {
let ( part_ok , part_frames ) = test_part ( i , part , env ) ;
ok & = part_ok ;
frames . extend ( part_frames ) ;
}
let env = env . borrow ( ) ;
if ! frames . is_empty ( ) {
let pdf_data = pdf ::export ( & frames , & env ) ;
2021-01-14 18:47:29 +03:00
fs ::create_dir_all ( & pdf_path . parent ( ) . unwrap ( ) ) . unwrap ( ) ;
2021-01-13 16:07:38 +03:00
fs ::write ( pdf_path , pdf_data ) . unwrap ( ) ;
2021-01-14 01:19:44 +03:00
let canvas = draw ( & frames , & env , 2.0 ) ;
2021-01-14 18:47:29 +03:00
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-01-13 16:07:38 +03:00
if let Some ( ref_path ) = ref_path {
if let Ok ( ref_pixmap ) = Pixmap ::load_png ( ref_path ) {
if canvas . pixmap ! = ref_pixmap {
println! ( " Does not match reference image. ❌ " ) ;
ok = false ;
}
} else {
println! ( " Failed to open reference image. ❌ " ) ;
ok = false ;
}
}
}
if ok {
2021-01-14 18:47:29 +03:00
println! ( " \x1b [1ATesting {} ✔ " , name . display ( ) ) ;
2021-01-13 16:07:38 +03:00
}
ok
}
fn test_part ( i : usize , src : & str , env : & SharedEnv ) -> ( bool , Vec < Frame > ) {
2021-01-13 19:22:33 +03:00
let map = LineMap ::new ( src ) ;
let ( compare_ref , ref_diags ) = parse_metadata ( src , & map ) ;
2020-12-11 00:44:35 +03:00
let mut state = State ::default ( ) ;
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.
state . page . size = Size ::new ( Length ::pt ( 120.0 ) , Length ::raw ( f64 ::INFINITY ) ) ;
state . page . expand = Spec ::new ( Expansion ::Fill , Expansion ::Fit ) ;
2020-12-11 00:44:35 +03:00
state . page . margins = Sides ::uniform ( Some ( Length ::pt ( 10.0 ) . into ( ) ) ) ;
2021-01-13 17:44:41 +03:00
pub fn dump ( _ : & mut EvalContext , args : & mut Args ) -> Value {
let ( array , dict ) = args . drain ( ) ;
Value ::Array ( vec! [ Value ::Array ( array ) , Value ::Dict ( dict ) ] )
}
Rc ::make_mut ( & mut state . scope ) . set ( " dump " , ValueFunc ::new ( " dump " , dump ) ) ;
2020-12-11 00:44:35 +03:00
let Pass {
2021-01-13 16:07:38 +03:00
output : mut frames ,
2020-12-11 00:44:35 +03:00
feedback : Feedback { mut diags , .. } ,
} = typeset ( & src , Rc ::clone ( env ) , state ) ;
2021-01-13 16:07:38 +03:00
if ! compare_ref {
frames . clear ( ) ;
}
2020-12-11 00:44:35 +03:00
2021-01-13 16:07:38 +03:00
diags . sort_by_key ( | d | d . span ) ;
2020-12-11 00:44:35 +03:00
let mut ok = true ;
if diags ! = ref_diags {
2021-01-13 16:07:38 +03:00
println! ( " Subtest {} does not match expected diagnostics. ❌ " , i ) ;
2020-12-11 00:44:35 +03:00
ok = false ;
for diag in & diags {
2021-01-13 18:37:18 +03:00
if ! ref_diags . contains ( diag ) {
2020-12-11 00:44:35 +03:00
print! ( " Unexpected | " ) ;
print_diag ( diag , & map ) ;
}
}
for diag in & ref_diags {
2021-01-13 18:37:18 +03:00
if ! diags . contains ( diag ) {
2020-12-11 00:44:35 +03:00
print! ( " Missing | " ) ;
print_diag ( diag , & map ) ;
}
}
}
2021-01-13 16:07:38 +03:00
( ok , frames )
2020-12-11 00:44:35 +03:00
}
2021-01-13 19:22:33 +03:00
fn parse_metadata ( src : & str , map : & LineMap ) -> ( bool , SpanVec < Diag > ) {
2020-12-11 00:44:35 +03:00
let mut diags = vec! [ ] ;
2020-12-17 02:20:27 +03:00
let mut compare_ref = true ;
2020-12-11 00:44:35 +03:00
2021-01-13 19:22:33 +03:00
for ( i , line ) in src . lines ( ) . enumerate ( ) {
compare_ref & = ! line . starts_with ( " // Ref: false " ) ;
2021-01-13 16:07:38 +03:00
2021-01-13 19:22:33 +03:00
let ( level , rest ) = if let Some ( rest ) = line . strip_prefix ( " // Warning: " ) {
2020-12-11 00:44:35 +03:00
( Level ::Warning , rest )
2021-01-13 19:22:33 +03:00
} else if let Some ( rest ) = line . strip_prefix ( " // Error: " ) {
2021-01-13 16:07:38 +03:00
( Level ::Error , rest )
2020-12-11 00:44:35 +03:00
} else {
continue ;
} ;
2021-01-13 19:22:33 +03:00
fn num ( s : & mut Scanner ) -> u32 {
s . eat_while ( | c | c . is_numeric ( ) ) . parse ( ) . unwrap ( )
}
2020-12-11 00:44:35 +03:00
2021-01-13 19:22:33 +03:00
let pos = | s : & mut Scanner | -> Pos {
let ( delta , _ , column ) = ( num ( s ) , s . eat_assert ( ':' ) , num ( s ) ) ;
let line = i as u32 + 1 + delta ;
map . pos ( Location { line , column } ) . unwrap ( )
} ;
2021-01-13 16:07:38 +03:00
2021-01-13 19:22:33 +03:00
let mut s = Scanner ::new ( rest ) ;
let ( start , _ , end ) = ( pos ( & mut s ) , s . eat_assert ( '-' ) , pos ( & mut s ) ) ;
2021-01-13 16:07:38 +03:00
2021-01-13 19:22:33 +03:00
diags . push ( Diag ::new ( level , s . rest ( ) . trim ( ) ) . with_span ( start .. end ) ) ;
}
2020-12-11 00:44:35 +03:00
2021-01-13 13:54:50 +03:00
diags . sort_by_key ( | d | d . span ) ;
2020-12-17 02:20:27 +03:00
2021-01-13 19:22:33 +03:00
( compare_ref , diags )
2020-12-11 00:44:35 +03:00
}
fn print_diag ( diag : & Spanned < Diag > , map : & LineMap ) {
let start = map . location ( diag . span . start ) . unwrap ( ) ;
let end = map . location ( diag . span . end ) . unwrap ( ) ;
2020-12-11 14:37:20 +03:00
println! ( " {} : {} - {} : {} " , diag . v . level , start , end , diag . v . message ) ;
2020-12-11 00:44:35 +03:00
}
2020-08-02 22:17:42 +03:00
2021-01-03 02:12:09 +03:00
fn draw ( frames : & [ Frame ] , env : & Env , 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 ( ) )
2020-10-10 23:19:36 +03:00
. unwrap ( ) ;
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 {} " , pixel_width , pixel_height ) ;
}
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-08-02 22:17:42 +03:00
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 ,
2020-08-02 22:17:42 +03:00
) ;
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 ;
2020-08-02 22:17:42 +03:00
match element {
2021-01-03 02:12:09 +03:00
Element ::Text ( shaped ) = > {
2020-11-28 00:35:42 +03:00
draw_text ( & mut canvas , pos , env , shaped ) ;
}
2021-01-03 02:12:09 +03:00
Element ::Image ( image ) = > {
2020-11-28 00:35:42 +03:00
draw_image ( & mut canvas , pos , env , image ) ;
2020-11-20 18:36:22 +03:00
}
2020-08-02 22:17:42 +03:00
}
}
2021-01-03 02:12:09 +03:00
origin . y + = frame . size . height + pad ;
2020-08-02 22:17:42 +03:00
}
2020-11-25 20:46:47 +03:00
canvas
2020-08-02 22:17:42 +03:00
}
2020-11-28 00:35:42 +03:00
fn draw_text ( canvas : & mut Canvas , pos : Point , env : & Env , shaped : & Shaped ) {
2020-12-17 17:43:30 +03:00
let face = env . fonts . face ( shaped . face ) . get ( ) ;
2020-08-02 22:17:42 +03:00
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 + shaped . font_size ) . to_pt ( ) as f32 ;
let scale = ( shaped . font_size / units_per_em as f64 ) . to_pt ( ) as f32 ;
2020-08-02 22:17:42 +03:00
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
2020-12-17 14:16:17 +03:00
let mut paint = Paint ::default ( ) ;
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 ( ) ) ;
}
2020-08-02 22:17:42 +03:00
}
}
2021-01-03 02:12:09 +03:00
fn draw_image ( canvas : & mut Canvas , pos : Point , env : & Env , element : & Image ) {
2021-01-01 19:54:31 +03:00
let img = & env . resources . loaded ::< ImageResource > ( element . res ) ;
2020-11-28 00:35:42 +03:00
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 ( ) ) {
2020-11-28 00:35:42 +03:00
let Rgba ( [ r , g , b , a ] ) = src ;
* dest = ColorU8 ::from_rgba ( r , g , b , a ) . premultiply ( ) ;
2020-11-20 18:36:22 +03:00
}
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 ,
2020-11-20 18:36:22 +03:00
) ;
}
2020-08-02 22:17:42 +03:00
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 ( ) ;
}
}