package pdf import "core:fmt" import "core:log" import "core:math" import "core:mem/virtual" import "core:os" import "core:path/filepath" import "core:slice" import "core:strings" import "core:testing" import "core:time" // Content-stream interpreter tests. // // Everything here runs against `Recorder`, a Device that keeps every call it // is handed. That is deliberately the whole apparatus: the interpreter's // output IS its sequence of device calls, so a test that pins the calls pins // the behaviour, and the same recorder is what the three real devices will be // checked against once they exist. // // The fixture document comes from function_test.odin (`cf_open`, `cf_parse`, // `cf_stream`, `cf_close`). It is opened by scanning and so already carries // repairs; every assertion about a repair counts the GROWTH of doc.repairs // rather than its length. // --------------------------------------------------------------------------- // The recording device // --------------------------------------------------------------------------- @(private = "file") Rec_Kind :: enum u8 { Fill_Path, Stroke_Path, Clip_Path, Clip_Stroke_Path, Clip_Text, Clip_Image_Mask, Pop_Clip, Fill_Text, Stroke_Text, Ignore_Text, Fill_Image, Fill_Image_Mask, Fill_Shade, Begin_Mask, End_Mask, Begin_Group, End_Group, Begin_Tile, End_Tile, Begin_Layer, End_Layer, Close, } @(private = "file") Rec :: struct { kind: Rec_Kind, even_odd: bool, ctm: Matrix, paint: Paint, stroke: Stroke_State, verbs: []Path_Verb, points: []Point, items: []Text_Item, font: ^Font, wmode: int, mode: Text_Render_Mode, name: string, area: Rect, view: Rect, xstep: f64, ystep: f64, isolated: bool, knockout: bool, blend: Blend_Mode, alpha: f64, luminosity: bool, img: ^Image, shade: ^Shading, transfer: ^Function, // A Soft_Mask lives in the run's scratch and does not outlive it, so a // device that records has to copy it -- exactly as device_list.odin does. smask: Soft_Mask, has_smask: bool, } @(private = "file") Recorder :: struct { dev: Device, log: [dynamic]Rec, // What begin_tile answers: 0 means "the device tiles, run the cell once". tiles: int, } @(private = "file") rec_add :: proc(dev: ^Device, rec: Rec) { r := (^Recorder)(dev.user) e := rec if e.paint.soft_mask != nil { e.smask = e.paint.soft_mask^ e.has_smask = true } append(&r.log, e) } @(private = "file") rec_path :: proc(rec: ^Rec, p: ^Path) { if p == nil { return } rec.verbs = slice.clone(p.verbs[:]) rec.points = slice.clone(p.points[:]) } @(private = "file") rec_text :: proc(rec: ^Rec, txt: ^Text) { if txt == nil { return } rec.items = slice.clone(txt.items[:]) rec.font = txt.font rec.wmode = txt.wmode rec.mode = txt.render_mode } @(private = "file") rec_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { r := Rec{kind = .Fill_Path, even_odd = even_odd, ctm = ctm, paint = paint} rec_path(&r, path) rec_add(dev, r) } @(private = "file") rec_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { r := Rec{kind = .Stroke_Path, ctm = ctm, paint = paint} if stroke != nil { r.stroke = stroke^ } rec_path(&r, path) rec_add(dev, r) } @(private = "file") rec_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) { r := Rec{kind = .Clip_Path, even_odd = even_odd, ctm = ctm} rec_path(&r, path) rec_add(dev, r) } @(private = "file") rec_clip_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) { r := Rec{kind = .Clip_Stroke_Path, ctm = ctm} if stroke != nil { r.stroke = stroke^ } rec_path(&r, path) rec_add(dev, r) } @(private = "file") rec_clip_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { r := Rec{kind = .Clip_Text, ctm = ctm} rec_text(&r, txt) rec_add(dev, r) } @(private = "file") rec_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) { rec_add(dev, Rec{kind = .Clip_Image_Mask, ctm = ctm, img = img}) } @(private = "file") rec_pop_clip :: proc(dev: ^Device) { rec_add(dev, Rec{kind = .Pop_Clip}) } @(private = "file") rec_fill_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix, paint: Paint) { r := Rec{kind = .Fill_Text, ctm = ctm, paint = paint} rec_text(&r, txt) rec_add(dev, r) } @(private = "file") rec_stroke_text :: proc(dev: ^Device, txt: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { r := Rec{kind = .Stroke_Text, ctm = ctm, paint = paint} if stroke != nil { r.stroke = stroke^ } rec_text(&r, txt) rec_add(dev, r) } @(private = "file") rec_ignore_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { r := Rec{kind = .Ignore_Text, ctm = ctm} rec_text(&r, txt) rec_add(dev, r) } @(private = "file") rec_fill_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { rec_add(dev, Rec{kind = .Fill_Image, ctm = ctm, paint = paint, img = img}) } @(private = "file") rec_fill_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { rec_add(dev, Rec{kind = .Fill_Image_Mask, ctm = ctm, paint = paint, img = img}) } @(private = "file") rec_fill_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) { rec_add(dev, Rec{kind = .Fill_Shade, ctm = ctm, paint = paint, shade = shade}) } @(private = "file") rec_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) { rec_add(dev, Rec{kind = .Begin_Mask, area = area, luminosity = luminosity, paint = backdrop}) } @(private = "file") rec_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) { rec_add(dev, Rec{kind = .End_Mask, transfer = transfer}) } @(private = "file") rec_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { rec_add(dev, Rec{ kind = .Begin_Group, area = area, isolated = isolated, knockout = knockout, blend = blend, alpha = alpha, }) } @(private = "file") rec_end_group :: proc(dev: ^Device) { rec_add(dev, Rec{kind = .End_Group}) } @(private = "file") rec_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int { r := (^Recorder)(dev.user) append(&r.log, Rec{ kind = .Begin_Tile, area = area, view = view, xstep = xstep, ystep = ystep, ctm = ctm, }) return r.tiles } @(private = "file") rec_end_tile :: proc(dev: ^Device) { rec_add(dev, Rec{kind = .End_Tile}) } @(private = "file") rec_begin_layer :: proc(dev: ^Device, name: string) { // The name is scratch memory the run reclaims, so it has to be copied. rec_add(dev, Rec{kind = .Begin_Layer, name = strings.clone(name)}) } @(private = "file") rec_end_layer :: proc(dev: ^Device) { rec_add(dev, Rec{kind = .End_Layer}) } @(private = "file") rec_close :: proc(dev: ^Device) { rec_add(dev, Rec{kind = .Close}) } @(private = "file") rec_init :: proc(rec: ^Recorder) { rec.log = make([dynamic]Rec, 0, 32) rec.dev = Device{ user = rec, fill_path = rec_fill_path, stroke_path = rec_stroke_path, clip_path = rec_clip_path, clip_stroke_path = rec_clip_stroke_path, clip_text = rec_clip_text, clip_image_mask = rec_clip_image_mask, pop_clip = rec_pop_clip, fill_text = rec_fill_text, stroke_text = rec_stroke_text, ignore_text = rec_ignore_text, fill_image = rec_fill_image, fill_image_mask = rec_fill_image_mask, fill_shade = rec_fill_shade, begin_mask = rec_begin_mask, end_mask = rec_end_mask, begin_group = rec_begin_group, end_group = rec_end_group, begin_tile = rec_begin_tile, end_tile = rec_end_tile, begin_layer = rec_begin_layer, end_layer = rec_end_layer, close = rec_close, } } @(private = "file") rec_destroy :: proc(rec: ^Recorder) { for &e in rec.log { delete(e.verbs) delete(e.points) delete(e.items) delete(e.name) } delete(rec.log) rec.log = nil } @(private = "file") rec_count :: proc(rec: ^Recorder, kind: Rec_Kind) -> (n: int) { for &e in rec.log { if e.kind == kind { n += 1 } } return } // rec_nth is the (0-based) n'th call of a kind, or nil. @(private = "file") rec_nth :: proc(rec: ^Recorder, kind: Rec_Kind, n := 0) -> ^Rec { seen := 0 for &e in rec.log { if e.kind != kind { continue } if seen == n { return &e } seen += 1 } return nil } // rec_kinds is the whole call sequence, for order assertions. @(private = "file") rec_kinds :: proc(rec: ^Recorder, allocator := context.allocator) -> []Rec_Kind { out := make([]Rec_Kind, len(rec.log), allocator) for e, i in rec.log { out[i] = e.kind } return out } // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- @(private = "file") CT_EPS :: 1e-9 @(private = "file") ct_near :: proc(t: ^testing.T, got, want: f64, what: string, tol := CT_EPS, loc := #caller_location) { if !(abs(got - want) <= tol) { testing.expectf(t, false, "%s: got %v, want %v", what, got, want, loc = loc) } } @(private = "file") ct_point :: proc(t: ^testing.T, p: Point, x, y: f64, what: string, tol := CT_EPS, loc := #caller_location) { if !(abs(p.x - x) <= tol && abs(p.y - y) <= tol) { testing.expectf(t, false, "%s: got (%v, %v), want (%v, %v)", what, p.x, p.y, x, y, loc = loc) } } // ct_run executes a content stream against a fresh recorder. @(private = "file") ct_run :: proc(doc: ^Document, rec: ^Recorder, src: string, res: ^Dict = nil, ctm := MATRIX_IDENTITY, opts := Content_Options{}) -> Error { return run_content(doc, &rec.dev, transmute([]byte)src, res, ctm, opts) } // A font every code of which is exactly half an em wide. /BaseFont is not one // of the standard 14, so no AFM metric can slip in ahead of /MissingWidth and // every advance in these tests is 0.5 * Tfs exactly. @(private = "file") CT_FONT :: "<< /Type /Font /Subtype /Type1 /BaseFont /Halfwidth " + "/FontDescriptor << /Flags 32 /MissingWidth 500 >> >>" // The same width through a two-byte composite font, so that a code 32 that is // half of a two-byte code can be told from a single-byte one. @(private = "file") CT_FONT0 :: "<< /Type /Font /Subtype /Type0 /BaseFont /Halfwide /Encoding /Identity-H " + "/DescendantFonts [ << /Type /Font /Subtype /CIDFontType2 /BaseFont /Halfwide " + "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> " + "/DW 500 >> ] >>" @(private = "file") CT_TYPE3 :: "<< /Type /Font /Subtype /Type3 /FontMatrix [0.01 0 0 0.01 0 0] " + "/FontBBox [0 0 100 100] /FirstChar 65 /LastChar 66 /Widths [100 100] " + "/Encoding << /Type /Encoding /Differences [65 /square 66 /blank] >> >>" // ct_res builds the resource dictionary the tests share. Everything in it is // Document memory, so it lives until cf_close. @(private = "file") ct_res :: proc(doc: ^Document) -> ^Dict { res := new_dict(doc.allocator) fonts := new_dict(doc.allocator) dict_set(fonts, "F1", cf_parse(doc, CT_FONT)) dict_set(fonts, "F0", cf_parse(doc, CT_FONT0)) t3 := cf_parse(doc, CT_TYPE3) if t3d, ok := to_dict(t3); ok { procs := new_dict(doc.allocator) // A square that fills the whole glyph box, and a glyph that draws // nothing at all. dict_set(procs, "square", cf_stream(doc, "<< >>", transmute([]byte)string("100 0 d0 0 0 100 100 re f"))) dict_set(procs, "blank", cf_stream(doc, "<< >>", transmute([]byte)string("100 0 d0"))) dict_set(t3d, "CharProcs", procs) } dict_set(fonts, "T3", t3) dict_set(res, "Font", fonts) xobj := new_dict(doc.allocator) // A form that invokes itself. Its /Resources are inherited from the page, // which is what makes /Self resolve inside it. dict_set(xobj, "Self", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] >>", transmute([]byte)string("0 0 10 10 re f /Self Do"))) dict_set(xobj, "Box", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Matrix [2 0 0 2 5 5] >>", transmute([]byte)string("0 0 4 4 re f"))) dict_set(xobj, "Grp", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] " + "/Group << /S /Transparency /I true /K true /CS /DeviceRGB >> >>", transmute([]byte)string("0 0 4 4 re f"))) // A 2x2 greyscale image and a 2x2 stencil. dict_set(xobj, "Im", cf_stream(doc, "<< /Type /XObject /Subtype /Image /Width 2 /Height 2 " + "/ColorSpace /DeviceGray /BitsPerComponent 8 >>", transmute([]byte)string("\x00\x40\x80\xff"))) dict_set(xobj, "Stencil", cf_stream(doc, "<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ImageMask true >>", transmute([]byte)string("\x40\x80"))) dict_set(res, "XObject", xobj) shadings := new_dict(doc.allocator) dict_set(shadings, "Sh", cf_parse(doc, "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] " + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >>")) dict_set(res, "Shading", shadings) pats := new_dict(doc.allocator) dict_set(pats, "Tile", cf_stream(doc, "<< /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] " + "/XStep 10 /YStep 10 >>", transmute([]byte)string("0 0 5 5 re f"))) dict_set(pats, "Uncol", cf_stream(doc, "<< /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 10 10] " + "/XStep 10 /YStep 10 >>", transmute([]byte)string("1 0 0 rg 0 0 5 5 re f"))) dict_set(pats, "ShadeGS", cf_parse(doc, "<< /PatternType 2 /ExtGState << /ca 0.25 /BM /Screen >> /Shading " + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] " + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >> >>")) dict_set(pats, "Shade", cf_parse(doc, "<< /PatternType 2 /Matrix [1 0 0 1 3 4] /Shading " + "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] " + "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >> >>")) dict_set(res, "Pattern", pats) egs := new_dict(doc.allocator) dict_set(egs, "G1", cf_parse(doc, "<< /Type /ExtGState /LW 4 /LC 1 /LJ 2 /ML 3 /CA 0.25 /ca 0.5 " + "/BM /Multiply /D [[3 2] 1] >>")) dict_set(egs, "GNone", cf_parse(doc, "<< /Type /ExtGState /SMask /None >>")) smask_form := cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] " + "/Group << /S /Transparency /CS /DeviceGray >> >>", transmute([]byte)string("0 0 20 20 re f")) smd := new_dict(doc.allocator) dict_set(smd, "S", Name("Luminosity")) dict_set(smd, "G", smask_form) gsm := new_dict(doc.allocator) dict_set(gsm, "SMask", smd) dict_set(egs, "GMask", gsm) dict_set(res, "ExtGState", egs) cspaces := new_dict(doc.allocator) dict_set(cspaces, "Sep", cf_parse(doc, "[/Separation /Spot /DeviceRGB << /FunctionType 2 /Domain [0 1] " + "/C0 [0 0 0] /C1 [1 0 0] /N 1 >>]")) dict_set(cspaces, "PatRGB", cf_parse(doc, "[/Pattern /DeviceRGB]")) dict_set(res, "ColorSpace", cspaces) return res } // --------------------------------------------------------------------------- // Graphics state: q, Q, cm // --------------------------------------------------------------------------- @(test) test_content_q_restores_ctm :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "q 2 0 0 2 0 0 cm 0 0 1 1 re f Q 0 0 1 1 re f"), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) inner := rec_nth(&rec, .Fill_Path, 0) outer := rec_nth(&rec, .Fill_Path, 1) if inner == nil || outer == nil { return } ct_near(t, inner.ctm.a, 2, "inside q the CTM is scaled") ct_near(t, outer.ctm.a, 1, "after Q the CTM is restored") } @(test) test_content_cm_composes_in_order :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // CTM = operand * CTM, so the second `cm` is applied FIRST to user space: // a point at (1,0) lands at 2*1 + 10 = 12 under `2 0 0 2 0 0` then // `1 0 0 1 10 0`... which is 2*(1+10) = 22 the other way round. testing.expect_value(t, ct_run(doc, &rec, "2 0 0 2 0 0 cm 1 0 0 1 10 0 cm 0 0 1 1 re f"), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil { return } ct_point(t, transform_point(e.ctm, Point{1, 0}), 22, 0, "cm order") } @(test) test_content_Q_underflow_is_ignored :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "Q Q 1 0 0 1 5 0 cm 0 0 1 1 re f"), Error.None) testing.expect(t, len(doc.repairs) > before, "a stray Q must be recorded as a repair") // And the page still draws, in the state the strays did not damage. testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) e := rec_nth(&rec, .Fill_Path) if e == nil { return } ct_near(t, e.ctm.e, 5, "the cm after the stray Qs still applies") } // A `q` past the depth cap saves nothing, so its `Q` must restore nothing. If // the overflow were not counted, the first `Q` would pop an outer scope's // state and everything after it would draw in the wrong one. @(test) test_content_q_overflow_does_not_pop :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) over :: 6 b := strings.builder_make() defer strings.builder_destroy(&b) strings.write_string(&b, "1 0 0 1 5 0 cm ") for _ in 0 ..< MAX_GSTATE_DEPTH + over { strings.write_string(&b, "q ") } strings.write_string(&b, "1 0 0 1 7 0 cm ") for _ in 0 ..< over { strings.write_string(&b, "Q ") } strings.write_string(&b, "0 0 1 1 re f ") for _ in 0 ..< MAX_GSTATE_DEPTH { strings.write_string(&b, "Q ") } strings.write_string(&b, "0 0 1 1 re f") testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) first := rec_nth(&rec, .Fill_Path, 0) second := rec_nth(&rec, .Fill_Path, 1) if first == nil || second == nil { return } ct_near(t, first.ctm.e, 12, "the Qs matching the overflowed qs restore nothing") ct_near(t, second.ctm.e, 5, "the real Qs restore the state their qs saved") } @(test) test_content_stroke_state_operators :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "5 w 1 J 2 j 7 M [3 2] 1 d 0 0 10 10 re S"), Error.None) e := rec_nth(&rec, .Stroke_Path) if e == nil { testing.expect(t, false, "S did not reach the device") return } ct_near(t, e.stroke.line_width, 5, "w") testing.expect_value(t, e.stroke.line_cap, Line_Cap.Round) testing.expect_value(t, e.stroke.line_join, Line_Join.Bevel) ct_near(t, e.stroke.miter_limit, 7, "M") testing.expect_value(t, e.stroke.dash_count, 2) ct_near(t, e.stroke.dash[0], 3, "dash[0]") ct_near(t, e.stroke.dash[1], 2, "dash[1]") ct_near(t, e.stroke.dash_phase, 1, "dash phase") } @(test) test_content_non_finite_cm_is_rejected :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // The product overflows to infinity, which must never reach the geometry. before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "1e300 0 0 1e300 0 0 cm 1e300 0 0 1e300 0 0 cm 0 0 1 1 re f"), Error.None) testing.expect(t, len(doc.repairs) > before, "an unusable cm must be recorded") e := rec_nth(&rec, .Fill_Path) if e == nil { return } testing.expect(t, matrix_is_finite(e.ctm), "the CTM must stay finite") ct_near(t, e.ctm.a, 1e300, "an unusable cm leaves the CTM as it was", 1e290) } // --------------------------------------------------------------------------- // Path construction and painting // --------------------------------------------------------------------------- @(test) test_content_path_operators :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "1 2 m 3 4 l 5 6 7 8 9 10 c 11 12 13 14 v 15 16 17 18 y h f"), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil { testing.expect(t, false, "f did not reach the device") return } want := []Path_Verb{.Move, .Line, .Curve, .Curve, .Curve, .Close} testing.expect(t, slice.equal(e.verbs, want), "verb sequence") if len(e.points) < 11 { return } ct_point(t, e.points[0], 1, 2, "m") ct_point(t, e.points[1], 3, 4, "l") ct_point(t, e.points[4], 9, 10, "c end point") // `v` takes the current point as its first control point. ct_point(t, e.points[5], 9, 10, "v implied control point") ct_point(t, e.points[7], 13, 14, "v end point") // `y` repeats its end point as the second control point. ct_point(t, e.points[9], 17, 18, "y second control point") ct_point(t, e.points[10], 17, 18, "y end point") } @(test) test_content_re_winds_counter_clockwise :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "10 20 30 40 re f"), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil || len(e.points) < 4 { return } ct_point(t, e.points[0], 10, 20, "re origin") ct_point(t, e.points[1], 40, 20, "re second corner") ct_point(t, e.points[2], 40, 60, "re third corner") ct_point(t, e.points[3], 10, 60, "re fourth corner") } @(test) test_content_painting_operators :: proc(t: ^testing.T) { Case :: struct { src: string, fills: int, strokes: int, even_odd: bool, closed: bool, } cases := []Case{ {"0 0 1 1 re S", 0, 1, false, false}, {"0 0 m 1 1 l s", 0, 1, false, true}, {"0 0 1 1 re f", 1, 0, false, false}, {"0 0 1 1 re F", 1, 0, false, false}, {"0 0 1 1 re f*", 1, 0, true, false}, {"0 0 1 1 re B", 1, 1, false, false}, {"0 0 1 1 re B*", 1, 1, true, false}, {"0 0 m 1 1 l b", 1, 1, false, true}, {"0 0 m 1 1 l b*",1, 1, true, true}, {"0 0 1 1 re n", 0, 0, false, false}, } for c in cases { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, c.src), Error.None) testing.expectf(t, rec_count(&rec, .Fill_Path) == c.fills, "%s: %d fills, want %d", c.src, rec_count(&rec, .Fill_Path), c.fills) testing.expectf(t, rec_count(&rec, .Stroke_Path) == c.strokes, "%s: %d strokes, want %d", c.src, rec_count(&rec, .Stroke_Path), c.strokes) if c.fills > 0 { e := rec_nth(&rec, .Fill_Path) if e != nil { testing.expectf(t, e.even_odd == c.even_odd, "%s: fill rule", c.src) if c.closed && len(e.verbs) > 0 { testing.expectf(t, e.verbs[len(e.verbs) - 1] == .Close, "%s: the path must be closed before painting", c.src) } } } } } // --------------------------------------------------------------------------- // Clipping: W and W* arm a clip that the NEXT painting operator installs // --------------------------------------------------------------------------- @(test) test_content_clip_takes_effect_after_the_paint :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // `f` paints first and only then installs the clip. testing.expect_value(t, ct_run(doc, &rec, "0 0 10 10 re W f"), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) // The trailing pop is the interpreter balancing the device's clip stack // at the end of the stream, which it owes whether or not a Q ever came. testing.expect(t, slice.equal(kinds, []Rec_Kind{.Fill_Path, .Clip_Path, .Pop_Clip}), "the fill must reach the device before the clip it arms") } @(test) test_content_clip_is_not_installed_at_W :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // `sh` is not a path-painting operator, so the armed clip is still // pending when it runs: the shading must be painted unclipped and the // clip must arrive only with the `n`. testing.expect_value(t, ct_run(doc, &rec, "0 0 10 10 re W /Sh sh n", ct_res(doc)), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{.Fill_Shade, .Clip_Path, .Pop_Clip}), "W must not clip until a painting operator runs") } @(test) test_content_clip_rule_and_pop_on_Q :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "q 0 0 10 10 re W* n Q 0 0 1 1 re f"), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{.Clip_Path, .Pop_Clip, .Fill_Path}), "Q must pop exactly the clips its q was holding") e := rec_nth(&rec, .Clip_Path) if e == nil { return } testing.expect(t, e.even_odd, "W* clips with the even-odd rule") } @(test) test_content_clip_survives_to_end_of_stream :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // No Q at all: the interpreter still has to leave the device's clip stack // as it found it. testing.expect_value(t, ct_run(doc, &rec, "0 0 10 10 re W n 0 0 1 1 re f"), Error.None) testing.expect_value(t, rec_count(&rec, .Clip_Path), 1) testing.expect_value(t, rec_count(&rec, .Pop_Clip), 1) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, kinds[len(kinds) - 1] == .Pop_Clip, "the clip is popped last, not early") } // --------------------------------------------------------------------------- // Colour // --------------------------------------------------------------------------- @(test) test_content_device_colour_operators :: proc(t: ^testing.T) { Case :: struct { src: string, rgb: [3]f64, n: int, } cases := []Case{ {"0.5 g 0 0 1 1 re f", {0.5, 0.5, 0.5}, 1}, {"1 0 0 rg 0 0 1 1 re f", {1, 0, 0}, 3}, {"0 1 1 0 k 0 0 1 1 re f", {1, 0, 0}, 4}, } for c in cases { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, c.src), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil { testing.expectf(t, false, "%s did not paint", c.src) continue } testing.expectf(t, e.paint.color.n == c.n, "%s: %d components", c.src, e.paint.color.n) for i in 0 ..< 3 { ct_near(t, e.paint.rgb[i], c.rgb[i], c.src, 1e-6) } } } @(test) test_content_stroke_colour_is_separate :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "1 0 0 rg 0 0 1 RG 0 0 1 1 re B"), Error.None) f := rec_nth(&rec, .Fill_Path) s := rec_nth(&rec, .Stroke_Path) if f == nil || s == nil { return } ct_near(t, f.paint.rgb[0], 1, "fill red", 1e-6) ct_near(t, s.paint.rgb[2], 1, "stroke blue", 1e-6) ct_near(t, s.paint.rgb[0], 0, "stroke has no red", 1e-6) } @(test) test_content_named_colourspace_and_scn :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // A Separation whose tint transform takes 1 to pure red. testing.expect_value(t, ct_run(doc, &rec, "/Sep cs 1 scn 0 0 1 1 re f", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil { testing.expect(t, false, "the separation fill did not paint") return } testing.expect_value(t, e.paint.color.n, 1) ct_near(t, e.paint.rgb[0], 1, "separation red", 1e-6) ct_near(t, e.paint.rgb[1], 0, "separation green", 1e-6) } // Selecting a colour space resets the colour to that space's initial value, // so a `cs` after a red `rg` paints black, not red (8.6.8). @(test) test_content_cs_resets_the_colour :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "1 0 0 rg /DeviceRGB cs 0 0 1 1 re f"), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil { return } ct_near(t, e.paint.rgb[0], 0, "cs must reset the colour to black", 1e-6) } @(test) test_content_alpha_and_blend_from_extgstate :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/G1 gs 0 0 10 10 re B", ct_res(doc)), Error.None) f := rec_nth(&rec, .Fill_Path) s := rec_nth(&rec, .Stroke_Path) if f == nil || s == nil { testing.expect(t, false, "the ExtGState fill/stroke did not paint") return } ct_near(t, f.paint.alpha, 0.5, "ca is the fill alpha") ct_near(t, s.paint.alpha, 0.25, "CA is the stroke alpha") testing.expect_value(t, f.paint.blend, Blend_Mode.Multiply) ct_near(t, s.stroke.line_width, 4, "LW") testing.expect_value(t, s.stroke.line_cap, Line_Cap.Round) testing.expect_value(t, s.stroke.line_join, Line_Join.Bevel) testing.expect_value(t, s.stroke.dash_count, 2) ct_near(t, s.stroke.dash_phase, 1, "the /D phase") } // --------------------------------------------------------------------------- // Text // // The fixture font is exactly half an em wide for every code, so at 10 point // every advance below is 5.0 and the expected positions are arithmetic. // --------------------------------------------------------------------------- @(private = "file") ct_origin :: proc(items: []Text_Item, i: int) -> Point { if i < 0 || i >= len(items) { return Point{-1e30, -1e30} } return transform_point(items[i].trm, Point{0, 0}) } @(test) test_content_text_matrix_sequence :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // Td sets the line matrix; each glyph advances the text matrix alone, so // T* returns to where the LINE started rather than to where it ended. testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf 20 TL 100 200 Td (AB) Tj T* (C) Tj ET", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Text), 2) first := rec_nth(&rec, .Fill_Text, 0) second := rec_nth(&rec, .Fill_Text, 1) if first == nil || second == nil { return } testing.expect_value(t, len(first.items), 2) ct_point(t, ct_origin(first.items, 0), 100, 200, "first glyph", 1e-9) ct_point(t, ct_origin(first.items, 1), 105, 200, "second glyph advances by 0.5 em") testing.expect_value(t, len(second.items), 1) ct_point(t, ct_origin(second.items, 0), 100, 180, "T* moves the line, not the pen") } @(test) test_content_TD_sets_leading :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // TD is Td with TL set to -ty, so the following T* moves by the same // amount again. testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf 10 -20 TD (A) Tj T* (B) Tj ET", ct_res(doc)), Error.None) a := rec_nth(&rec, .Fill_Text, 0) b := rec_nth(&rec, .Fill_Text, 1) if a == nil || b == nil { return } ct_point(t, ct_origin(a.items, 0), 10, -20, "TD") ct_point(t, ct_origin(b.items, 0), 10, -40, "T* after TD") } @(test) test_content_Tm_scales_glyph_and_advance :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf 2 0 0 2 30 40 Tm (AB) Tj ET", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Text) if e == nil || len(e.items) < 2 { return } ct_point(t, ct_origin(e.items, 0), 30, 40, "Tm places the first glyph") // The advance is 5 in text space, and Tm doubles everything. ct_point(t, ct_origin(e.items, 1), 40, 40, "Tm scales the advance") // The rendering matrix folds in the font matrix (1/1000), the size and Tm. ct_near(t, e.items[0].trm.a, 0.02, "trm scale") ct_near(t, e.items[0].advance, 5, "advance is unscaled text space") } @(test) test_content_Tz_scales_horizontally_only :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf 200 Tz (AB) Tj ET", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Text) if e == nil || len(e.items) < 2 { return } // Th multiplies the horizontal scale of the glyph and the advance, and // nothing else: putting it in `d` as well is the classic Tz bug. ct_near(t, e.items[0].trm.a, 0.02, "Tz scales the glyph horizontally") ct_near(t, e.items[0].trm.d, 0.01, "Tz must not scale the glyph vertically") ct_point(t, ct_origin(e.items, 1), 10, 0, "Tz scales the advance") ct_near(t, e.items[0].advance, 5, "the recorded advance is before Th") } @(test) test_content_Ts_raises_the_glyph_not_the_pen :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf 7 Ts (AB) Tj ET", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Text) if e == nil || len(e.items) < 2 { return } ct_point(t, ct_origin(e.items, 0), 0, 7, "Ts lifts the glyph") ct_point(t, ct_origin(e.items, 1), 5, 7, "the rise does not accumulate along the line") } // 9.3.3: word spacing applies to a single-byte code 32 and to nothing else. @(test) test_content_word_spacing_single_byte :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf 3 Tw 1 Tc ( A) Tj ET", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Text) if e == nil || len(e.items) < 2 { return } testing.expect(t, e.items[0].is_space, "a one-byte 32 is a space") // 0.5 em + Tc + Tw = 5 + 1 + 3. ct_point(t, ct_origin(e.items, 1), 9, 0, "word spacing applies to a one-byte space") } @(test) test_content_word_spacing_not_on_two_byte_code :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // <0020> in an Identity-H font is CID 32, an ordinary glyph, and stretching // it would space CJK text at random. testing.expect_value(t, ct_run(doc, &rec, "BT /F0 10 Tf 3 Tw 1 Tc <00200041> Tj ET", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Text) if e == nil { testing.expect(t, false, "the composite font did not draw") return } testing.expect_value(t, len(e.items), 2) if len(e.items) < 2 { return } testing.expect(t, !e.items[0].is_space, "a two-byte 0x0020 is not a space") // 0.5 em + Tc only. ct_point(t, ct_origin(e.items, 1), 6, 0, "no word spacing on a two-byte code") } // A positive TJ number pulls the following glyphs CLOSER; a negative one // pushes them apart. Getting the sign wrong reverses every kern in the file. @(test) test_content_TJ_number_direction :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf [(A) -1000 (B) 200 (C)] TJ ET", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Text), 3) a := rec_nth(&rec, .Fill_Text, 0) b := rec_nth(&rec, .Fill_Text, 1) c := rec_nth(&rec, .Fill_Text, 2) if a == nil || b == nil || c == nil { return } ct_point(t, ct_origin(a.items, 0), 0, 0, "first glyph") // 5 (advance) + 1000/1000 * 10 (the negative number moves right). ct_point(t, ct_origin(b.items, 0), 15, 0, "a negative TJ number opens a gap") // 15 + 5 - 200/1000 * 10. ct_point(t, ct_origin(c.items, 0), 18, 0, "a positive TJ number closes one") } @(test) test_content_TJ_adjustment_obeys_Tz :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf 200 Tz [(A) -1000 (B)] TJ ET", ct_res(doc)), Error.None) b := rec_nth(&rec, .Fill_Text, 1) if b == nil { return } // (5 + 10) * 2. ct_point(t, ct_origin(b.items, 0), 30, 0, "the horizontal scale applies to TJ too") } @(test) test_content_quote_operators :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // ' is T* then Tj. " additionally sets word and character spacing. testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf 12 TL 5 100 Td (A) ' 3 1 (X A) \" ET", ct_res(doc)), Error.None) a := rec_nth(&rec, .Fill_Text, 0) b := rec_nth(&rec, .Fill_Text, 1) if a == nil || b == nil { return } ct_point(t, ct_origin(a.items, 0), 5, 88, "' moves down one line first") ct_point(t, ct_origin(b.items, 0), 5, 76, "\" moves down another line") if len(b.items) < 3 { return } // X: 5 + 1 (Tc). space: 5 + 1 + 3 (Tw). ct_point(t, ct_origin(b.items, 1), 11, 76, "\" sets the character spacing") ct_point(t, ct_origin(b.items, 2), 20, 76, "\" sets the word spacing") } @(test) test_content_render_modes :: proc(t: ^testing.T) { Case :: struct { mode: string, fill: int, stroke: int, ignore: int, clip: int, } cases := []Case{ {"0", 1, 0, 0, 0}, {"1", 0, 1, 0, 0}, {"2", 1, 1, 0, 0}, {"3", 0, 0, 1, 0}, {"4", 1, 0, 0, 1}, {"5", 0, 1, 0, 1}, {"6", 1, 1, 0, 1}, {"7", 0, 0, 1, 1}, } for c in cases { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) src := strings.concatenate({"BT /F1 10 Tf ", c.mode, " Tr (A) Tj ET"}) defer delete(src) testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) testing.expectf(t, rec_count(&rec, .Fill_Text) == c.fill, "Tr %s: fills", c.mode) testing.expectf(t, rec_count(&rec, .Stroke_Text) == c.stroke, "Tr %s: strokes", c.mode) testing.expectf(t, rec_count(&rec, .Ignore_Text) == c.ignore, "Tr %s: ignores", c.mode) testing.expectf(t, rec_count(&rec, .Clip_Text) == c.clip, "Tr %s: clips", c.mode) } } // Mode 3 draws nothing and must still be handed over: an OCR layer under a // scan is written that way and is the whole of what such a page extracts. @(test) test_content_invisible_text_still_reaches_the_device :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "BT /F1 10 Tf 3 Tr 40 50 Td (AB) Tj ET", ct_res(doc)), Error.None) e := rec_nth(&rec, .Ignore_Text) if e == nil { testing.expect(t, false, "invisible text must still reach ignore_text") return } testing.expect_value(t, len(e.items), 2) testing.expect_value(t, e.mode, Text_Render_Mode.Invisible) ct_point(t, ct_origin(e.items, 0), 40, 50, "invisible text is still positioned") } @(test) test_content_text_clip_is_installed_at_ET :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // The clip is the union of everything shown in a clipping mode between BT // and ET, and only arrives at ET -- after both showing operators. testing.expect_value(t, ct_run(doc, &rec, "q BT /F1 10 Tf 7 Tr (AB) Tj (CD) Tj ET Q 0 0 1 1 re f", ct_res(doc)), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{.Ignore_Text, .Ignore_Text, .Clip_Text, .Pop_Clip, .Fill_Path}), "the text clip is installed once, at ET, and popped by Q") e := rec_nth(&rec, .Clip_Text) if e == nil { return } testing.expect_value(t, len(e.items), 4) } @(test) test_content_missing_font_draws_nothing :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "BT /Nope 10 Tf (A) Tj ET 0 0 1 1 re f", ct_res(doc)), Error.None) testing.expect(t, len(doc.repairs) > before, "a missing font is a repair") testing.expect_value(t, rec_count(&rec, .Fill_Text), 0) // And the rest of the page still draws. testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } // --------------------------------------------------------------------------- // Type 3 fonts: a glyph is a content stream // --------------------------------------------------------------------------- @(test) test_content_type3_glyph_runs_its_content_stream :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // /FontMatrix is 1/100, so a 100-unit glyph box is one em, and at 10 point // the glyph's content runs under a CTM of 0.1. testing.expect_value(t, ct_run(doc, &rec, "BT /T3 10 Tf (A) Tj ET", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Ignore_Text), 1) e := rec_nth(&rec, .Fill_Path) if e == nil { testing.expect(t, false, "the Type 3 glyph description did not run") return } ct_near(t, e.ctm.a, 0.1, "the glyph runs under font matrix x size") // The glyph's own square is 100 units, which is 10 device units here. ct_point(t, transform_point(e.ctm, Point{100, 100}), 10, 10, "glyph box") } @(test) test_content_type3_advance_uses_the_font_matrix :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // A Type 3 width is in the font's own glyph space: 100 units through a // 1/100 matrix is one em, so at 10 point the advance is 10, not 1. testing.expect_value(t, ct_run(doc, &rec, "BT /T3 10 Tf (AB) Tj ET", ct_res(doc)), Error.None) e := rec_nth(&rec, .Ignore_Text) if e == nil || len(e.items) < 2 { return } ct_near(t, e.items[0].advance, 10, "Type 3 advance") ct_point(t, ct_origin(e.items, 1), 10, 0, "the second glyph") // The second glyph draws nothing, so only the first left a mark. testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } @(test) test_content_type3_invisible_mode_draws_nothing :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "BT /T3 10 Tf 3 Tr (A) Tj ET", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Ignore_Text), 1) testing.expect_value(t, rec_count(&rec, .Fill_Path), 0) } // --------------------------------------------------------------------------- // Form XObjects // --------------------------------------------------------------------------- @(test) test_content_form_that_invokes_itself_terminates :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "/Self Do", ct_res(doc)), Error.None) testing.expect(t, len(doc.repairs) > before, "the recursion must be recorded") // Exactly one pass through the form's body. testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{.Clip_Path, .Fill_Path, .Pop_Clip}), "the form's BBox clip must be pushed and popped exactly once") } @(test) test_content_form_matrix_and_bbox :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/Box Do", ct_res(doc)), Error.None) clip := rec_nth(&rec, .Clip_Path) fill := rec_nth(&rec, .Fill_Path) if clip == nil || fill == nil { testing.expect(t, false, "the form did not run") return } // /Matrix [2 0 0 2 5 5] composes in front of the invoking CTM. ct_near(t, fill.ctm.a, 2, "the form matrix scales the contents") ct_near(t, fill.ctm.e, 5, "the form matrix translates the contents") // The BBox is clipped in the form's own space, under the same matrix. if len(clip.points) >= 3 { ct_point(t, transform_point(clip.ctm, clip.points[2]), 25, 25, "BBox corner") } } @(test) test_content_form_inherits_resources :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // /Self names itself out of the page's resources, which is only possible // because a form with no /Resources inherits the invoking stream's. testing.expect_value(t, ct_run(doc, &rec, "/Self Do", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } @(test) test_content_transparency_group :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/G1 gs /Grp Do", ct_res(doc)), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{.Clip_Path, .Begin_Group, .Fill_Path, .End_Group, .Pop_Clip}), "the group brackets the form's contents inside its BBox clip") g := rec_nth(&rec, .Begin_Group) inner := rec_nth(&rec, .Fill_Path) if g == nil || inner == nil { return } testing.expect(t, g.isolated, "/I") testing.expect(t, g.knockout, "/K") ct_near(t, g.alpha, 0.5, "the group takes the current fill alpha") testing.expect_value(t, g.blend, Blend_Mode.Multiply) ct_point(t, Point{g.area.x0, g.area.y0}, 0, 0, "group area") ct_point(t, Point{g.area.x1, g.area.y1}, 10, 10, "group area") // 11.6.6: the contents composite into the group with the parameters reset. ct_near(t, inner.paint.alpha, 1, "the group's contents are opaque inside it") testing.expect_value(t, inner.paint.blend, Blend_Mode.Normal) } // --------------------------------------------------------------------------- // Images // --------------------------------------------------------------------------- @(test) test_content_image_xobject :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "q 8 0 0 8 3 4 cm /Im Do Q", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Image) if e == nil || e.img == nil { testing.expect(t, false, "the image did not reach the device") return } testing.expect_value(t, e.img.width, 2) testing.expect_value(t, e.img.height, 2) // An image always covers the unit square of the CTM (8.9.5.2). ct_point(t, transform_point(e.ctm, Point{1, 1}), 11, 12, "the image's far corner") } @(test) test_content_image_mask_takes_the_fill_colour :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "1 0 0 rg /Stencil Do", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Image), 0) e := rec_nth(&rec, .Fill_Image_Mask) if e == nil { testing.expect(t, false, "the stencil did not reach fill_image_mask") return } testing.expect(t, e.paint.is_mask, "a stencil paints a shape, not a colour") ct_near(t, e.paint.rgb[0], 1, "the stencil takes the current fill colour", 1e-6) } @(test) test_content_ignore_image_hint :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) rec.dev.hints = {.Ignore_Image} // Decoding a photograph to extract the text off the page it sits on is // pure waste, and the hint is what lets the interpreter skip it. testing.expect_value(t, ct_run(doc, &rec, "/Im Do 0 0 1 1 re f", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Image), 0) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } // --------------------------------------------------------------------------- // Inline images // --------------------------------------------------------------------------- @(test) test_content_inline_image_unfiltered :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) src := "BI /W 2 /H 2 /CS /G /BPC 8 ID \x00\x40\x80\xff EI 0 0 1 1 re f" testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Image) if e == nil || e.img == nil { testing.expect(t, false, "the inline image did not reach the device") return } testing.expect_value(t, e.img.width, 2) testing.expect_value(t, e.img.height, 2) // The operator after EI must still be read: a mis-measured image // desynchronises the whole rest of the page. testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } // The bytes "EI" occur inside real image data. Unfiltered data has exactly // one possible length, and computing it is the only way to be right. @(test) test_content_inline_image_data_containing_EI :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) before := len(doc.repairs) // Four grey samples that spell " EI ", followed by the real EI. src := "BI /W 4 /H 1 /CS /G /BPC 8 ID EI EI 0 0 1 1 re f" testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Image) if e == nil || e.img == nil { testing.expect(t, false, "the image ended at the EI inside its own data") return } testing.expect_value(t, e.img.width, 4) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) testing.expect_value(t, len(doc.repairs), before) } // Exactly one whitespace byte separates ID from the data (8.9.7). Consuming // none, or two, shifts every sample by a byte -- which the length arithmetic // then papers over, so only the pixels show it. @(test) test_content_inline_image_ID_separator :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // One mid-grey sample. A shift by one takes the separator space (0x20) // for the pixel instead. testing.expect_value(t, ct_run(doc, &rec, "BI /W 1 /H 1 /CS /G /BPC 8 ID \x80 EI", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Image) if e == nil || e.img == nil { testing.expect(t, false, "the inline image did not load") return } bmp, derr := image_decode(doc, e.img) testing.expect_value(t, derr, Error.None) if bmp == nil || len(bmp.pixels) < 1 { testing.expect(t, false, "the inline image did not decode") return } testing.expect_value(t, bmp.pixels[0], u8(0x80)) } @(test) test_content_inline_image_filtered :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // Filtered data has no computable length, so the end is scanned for: the // EI must be preceded by whitespace and followed by whitespace or the end. src := "BI /W 2 /H 2 /CS /G /BPC 8 /F /AHx ID 004080ff> EI 0 0 1 1 re f" testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Image) if e == nil || e.img == nil { testing.expect(t, false, "the filtered inline image did not load") return } testing.expect_value(t, e.img.width, 2) bmp, derr := image_decode(doc, e.img) testing.expect_value(t, derr, Error.None) testing.expect(t, bmp != nil, "the hex data decoded") testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } @(test) test_content_inline_image_explicit_length :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // PDF 2.0's /L removes the guesswork even for filtered data. src := "BI /W 2 /H 2 /CS /G /BPC 8 /F /AHx /L 9 ID 004080ff> EI 0 0 1 1 re f" testing.expect_value(t, ct_run(doc, &rec, src, ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Image), 1) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } @(test) test_content_inline_image_truncated :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // No EI at all. The run has to end rather than spin, and the damage has // to be recorded. before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "0 0 1 1 re f BI /W 2 /H 2 /CS /G /BPC 8 ID \x00\x40", ct_res(doc)), Error.None) testing.expect(t, len(doc.repairs) > before, "a truncated inline image is a repair") testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } @(test) test_content_inline_image_no_ID :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // A BI whose dictionary never reaches ID must not eat the rest of the page. before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "BI /W 2 /H 2 EI 0 0 1 1 re f", ct_res(doc)), Error.None) testing.expect(t, len(doc.repairs) > before, "") testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } // --------------------------------------------------------------------------- // Shadings and patterns // --------------------------------------------------------------------------- @(test) test_content_sh_paints_through_the_current_clip :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // `sh` has no bounding box and no matrix of its own: it paints under the // live CTM, over whatever the clip allows. testing.expect_value(t, ct_run(doc, &rec, "2 0 0 2 9 9 cm /Sh sh", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Shade) if e == nil || e.shade == nil { testing.expect(t, false, "sh did not reach the device") return } testing.expect_value(t, e.shade.kind, Shading_Kind.Axial) ct_near(t, e.ctm.a, 2, "sh uses the current CTM") ct_near(t, e.ctm.e, 9, "sh uses the current CTM") } @(test) test_content_ignore_shade_hint :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) rec.dev.hints = {.Ignore_Shade} testing.expect_value(t, ct_run(doc, &rec, "/Sh sh 0 0 1 1 re f", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Shade), 0) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } // A shading pattern paints through the shape as a clip, with the pattern // matrix anchored to the stream's default space -- NOT to the CTM of the fill. @(test) test_content_shading_pattern_fill :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/Pattern cs /Shade scn 0 0 10 10 re f", ct_res(doc)), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{.Clip_Path, .Fill_Shade, .Pop_Clip}), "a shading pattern is a shading clipped to the shape") e := rec_nth(&rec, .Fill_Shade) if e == nil { return } ct_point(t, transform_point(e.ctm, Point{0, 0}), 3, 4, "the pattern matrix") } // Pattern space is anchored to the page, not to the fill: the same pattern // under a different CTM must land on exactly the same lattice, or the texture // slides around under a transformed shape. @(test) test_content_pattern_space_ignores_the_fill_ctm :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/Pattern cs /Shade scn 0 0 10 10 re f " + "q 5 0 0 5 100 100 cm /Pattern cs /Shade scn 0 0 10 10 re f Q", ct_res(doc)), Error.None) a := rec_nth(&rec, .Fill_Shade, 0) b := rec_nth(&rec, .Fill_Shade, 1) if a == nil || b == nil { testing.expect(t, false, "both patterned fills must paint") return } testing.expect(t, a.ctm == b.ctm, "the pattern matrix must not follow the CTM of the fill") } @(test) test_content_tiling_pattern_device_tiles :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) rec.tiles = 0 // "I will place the copies; run the cell once" testing.expect_value(t, ct_run(doc, &rec, "/Pattern cs /Tile scn 0 0 20 20 re f", ct_res(doc)), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{ .Clip_Path, // the shape .Begin_Tile, .Clip_Path, // the cell's BBox .Fill_Path, // the cell's content .Pop_Clip, .End_Tile, .Pop_Clip, }), "a device that tiles gets the cell exactly once") bt := rec_nth(&rec, .Begin_Tile) if bt == nil { return } ct_near(t, bt.xstep, 10, "xstep") ct_near(t, bt.ystep, 10, "ystep") ct_point(t, Point{bt.view.x1, bt.view.y1}, 10, 10, "the cell's bbox") ct_point(t, Point{bt.area.x1, bt.area.y1}, 20, 20, "the area to cover") } @(test) test_content_tiling_pattern_interpreter_places_cells :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) rec.tiles = 4 // "I cannot tile; place four copies for me" testing.expect_value(t, ct_run(doc, &rec, "/Pattern cs /Tile scn 0 0 20 20 re f", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Begin_Tile), 1) testing.expect_value(t, rec_count(&rec, .End_Tile), 1) testing.expect_value(t, rec_count(&rec, .Fill_Path), 4) // Every cell is placed by a translation of the same matrix, so no two // land on top of one another. a := rec_nth(&rec, .Fill_Path, 0) b := rec_nth(&rec, .Fill_Path, 1) if a == nil || b == nil { return } ct_near(t, a.ctm.a, 1, "cells share the pattern's scale") ct_near(t, b.ctm.a, 1, "cells share the pattern's scale") testing.expect(t, a.ctm.e != b.ctm.e || a.ctm.f != b.ctm.f, "cells must not coincide") } // /PaintType 2: the cell is a stencil, and every colour operator inside it is // ignored in favour of the colour given with the pattern name. @(test) test_content_uncoloured_tiling_pattern_takes_the_callers_colour :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) rec.tiles = 1 testing.expect_value(t, ct_run(doc, &rec, "/PatRGB cs 0 1 0 /Uncol scn 0 0 20 20 re f", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil { testing.expect(t, false, "the uncoloured cell did not paint") return } // The cell says `1 0 0 rg`; the caller said green. ct_near(t, e.paint.rgb[0], 0, "the cell's own red must be ignored", 1e-6) ct_near(t, e.paint.rgb[1], 1, "the cell paints in the caller's colour", 1e-6) } @(test) test_content_pattern_without_a_name_paints_nothing :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // A Pattern colour space with no pattern selected has nothing to paint // with, and must not fall back to black. testing.expect_value(t, ct_run(doc, &rec, "/Pattern cs 0 0 10 10 re f", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 0) testing.expect_value(t, rec_count(&rec, .Fill_Shade), 0) } // --------------------------------------------------------------------------- // Soft masks // --------------------------------------------------------------------------- @(test) test_content_soft_mask_is_rendered_at_install_time :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/GMask gs 0 0 1 1 re f", ct_res(doc)), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{ .Begin_Mask, .Clip_Path, .Fill_Path, .Pop_Clip, .End_Mask, .Fill_Path, }), "the mask's group is drawn between begin_mask and end_mask") bm := rec_nth(&rec, .Begin_Mask) painted := rec_nth(&rec, .Fill_Path, 1) if bm == nil || painted == nil { return } testing.expect(t, bm.luminosity, "/S /Luminosity") ct_point(t, Point{bm.area.x1, bm.area.y1}, 20, 20, "the mask's area is its BBox") testing.expect(t, painted.has_smask, "the paint carries the installed mask") if painted.has_smask { testing.expect(t, painted.smask.luminosity, "") } } @(test) test_content_smask_none_clears_it :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/GMask gs /GNone gs 0 0 1 1 re f", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Path, 1) if e == nil { return } testing.expect(t, e.paint.soft_mask == nil, "/SMask /None must clear the mask") } @(test) test_content_soft_mask_is_restored_by_Q :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "q /GMask gs Q 0 0 1 1 re f", ct_res(doc)), Error.None) // The last fill is the page's, after the mask went out of scope. e := rec_nth(&rec, .Fill_Path, 1) if e == nil { return } testing.expect(t, e.paint.soft_mask == nil, "Q must take the soft mask with it") } // --------------------------------------------------------------------------- // Marked content and optional content // --------------------------------------------------------------------------- // ct_optional_content adds an /OCProperties to the catalog with `off` switched // off, and returns resources whose /Properties name both groups. @(private = "file") ct_optional_content :: proc(doc: ^Document, res: ^Dict) -> (on_ocg, off_ocg: ^Dict) { on_ocg, _ = to_dict(cf_parse(doc, "<< /Type /OCG /Name (Visible) >>")) off_ocg, _ = to_dict(cf_parse(doc, "<< /Type /OCG /Name (Hidden) >>")) props := new_dict(doc.allocator) dict_set(props, "L1", on_ocg) dict_set(props, "L2", off_ocg) dict_set(res, "Properties", props) off_list := new_array(doc.allocator) array_push(off_list, off_ocg) cfg := new_dict(doc.allocator) dict_set(cfg, "OFF", off_list) ocp := new_dict(doc.allocator) dict_set(ocp, "D", cfg) dict_set(doc.catalog, "OCProperties", ocp) return } @(test) test_content_bmc_emc_do_not_disturb_drawing :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/Span BMC 0 0 1 1 re f EMC /P << /MCID 0 >> BDC 0 0 1 1 re f EMC"), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) testing.expect_value(t, rec_count(&rec, .Begin_Layer), 0) } @(test) test_content_visible_layer_brackets_its_content :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) ct_optional_content(doc, res) testing.expect_value(t, ct_run(doc, &rec, "/OC /L1 BDC 0 0 1 1 re f EMC", res), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{.Begin_Layer, .Fill_Path, .End_Layer}), "") e := rec_nth(&rec, .Begin_Layer) if e == nil { return } testing.expect_value(t, e.name, "Visible") } @(test) test_content_hidden_layer_is_skipped :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) ct_optional_content(doc, res) testing.expect_value(t, ct_run(doc, &rec, "/OC /L2 BDC 0 0 1 1 re f /Im Do BT /F1 10 Tf (A) Tj ET EMC 0 0 1 1 re f", res), Error.None) // Only the fill after EMC survives. testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) testing.expect_value(t, rec_count(&rec, .Fill_Image), 0) testing.expect_value(t, rec_count(&rec, .Fill_Text), 0) testing.expect_value(t, rec_count(&rec, .Begin_Layer), 0) } // The hidden region ends at the MATCHING EMC, so a nested section inside it // must not turn drawing back on. @(test) test_content_hidden_layer_nesting :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) ct_optional_content(doc, res) testing.expect_value(t, ct_run(doc, &rec, "/OC /L2 BDC /Span BMC 0 0 1 1 re f EMC 0 0 1 1 re f EMC 0 0 1 1 re f", res), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } @(test) test_content_optional_content_can_be_ignored :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) ct_optional_content(doc, res) // Hidden text is still text: an extractor asks for all of it. err := ct_run(doc, &rec, "/OC /L2 BDC 0 0 1 1 re f EMC", res, MATRIX_IDENTITY, Content_Options{ignore_optional_content = true}) testing.expect_value(t, err, Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } @(test) test_content_stray_EMC_is_ignored :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "EMC EMC 0 0 1 1 re f"), Error.None) testing.expect(t, len(doc.repairs) > before, "a stray EMC is a repair") testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } // A layer left open at the end of the stream still has to be closed, or the // device's layer stack never unwinds. @(test) test_content_unclosed_layer_is_closed_at_the_end :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) ct_optional_content(doc, res) testing.expect_value(t, ct_run(doc, &rec, "/OC /L1 BDC 0 0 1 1 re f", res), Error.None) testing.expect_value(t, rec_count(&rec, .Begin_Layer), 1) testing.expect_value(t, rec_count(&rec, .End_Layer), 1) } // --------------------------------------------------------------------------- // Robustness // // Content streams are the most attacker-controlled data in a PDF and the most // malformed. Nothing below may abort a page. // --------------------------------------------------------------------------- @(test) test_content_unknown_operator_is_skipped :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "0 0 1 1 re f /Foo 1 2 (x) frobnicate 0 0 1 1 re f"), Error.None) testing.expect(t, len(doc.repairs) > before, "an unknown operator is a repair") // It takes its operands with it, so the next operator starts clean. testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) } @(test) test_content_BX_EX_permits_unknown_operators :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "BX 1 2 frobnicate ] >> EX 0 0 1 1 re f"), Error.None) testing.expect_value(t, len(doc.repairs), before) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) // The same operator outside the bracket is damage again. testing.expect_value(t, ct_run(doc, &rec, "frobnicate"), Error.None) testing.expect(t, len(doc.repairs) > before, "EX must end the compatibility section") } @(test) test_content_truncated_stream_ends_cleanly :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // Ends inside a text object, inside a q, with a clip open and an operator // half-written. testing.expect_value(t, ct_run(doc, &rec, "q 0 0 10 10 re W n 0 0 1 1 re f BT /F1 10 Tf 7 Tr (A) T", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) // Every clip pushed is popped, including the text clip the missing ET owed. pushes := rec_count(&rec, .Clip_Path) + rec_count(&rec, .Clip_Text) testing.expect_value(t, rec_count(&rec, .Pop_Clip), pushes) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, kinds[len(kinds) - 1] == .Pop_Clip, "the clip stack unwinds last") } @(test) test_content_unclosed_q_unwinds :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "q q q 0 0 10 10 re W n 0 0 1 1 re f"), Error.None) testing.expect(t, len(doc.repairs) > before, "an unclosed q is a repair") testing.expect_value(t, rec_count(&rec, .Clip_Path), 1) testing.expect_value(t, rec_count(&rec, .Pop_Clip), 1) } // The operand stack is fixed, and an operator reads its arguments from the // END of it, so a stream that pushes hundreds still runs with the right ones. @(test) test_content_operand_stack_keeps_the_newest :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) b := strings.builder_make() defer strings.builder_destroy(&b) for i in 1 ..= MAX_CONTENT_OPERANDS + 8 { strings.write_int(&b, i) strings.write_byte(&b, ' ') } strings.write_string(&b, "re f") testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b)), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil || len(e.points) < 1 { testing.expect(t, false, "the overloaded operator did not run") return } last :: MAX_CONTENT_OPERANDS + 8 ct_point(t, e.points[0], f64(last - 3), f64(last - 2), "the last four operands are the ones used") } @(test) test_content_too_few_operands_is_skipped :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) before := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, "1 2 re f 0 0 1 1 re f"), Error.None) testing.expect(t, len(doc.repairs) > before, "a short operator is a repair") // The `f` still runs, on an empty path, and the next rectangle is intact. testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) e := rec_nth(&rec, .Fill_Path) if e == nil || len(e.points) < 1 { return } ct_point(t, e.points[0], 0, 0, "the following operator is unaffected") } @(test) test_content_fuel_bounds_the_run :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) b := strings.builder_make() defer strings.builder_destroy(&b) for _ in 0 ..< 200 { strings.write_string(&b, "0 0 1 1 re f ") } // Five rectangles is 30 tokens; a budget of 40 must stop well short of 200. err := ct_run(doc, &rec, strings.to_string(b), nil, MATRIX_IDENTITY, Content_Options{max_ops = 40}) testing.expect_value(t, err, Error.None) n := rec_count(&rec, .Fill_Path) testing.expect(t, n > 0 && n < 20, "the budget must bound the run without emptying it") } @(test) test_content_nil_device_and_empty_input :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // A run with no sink executes everything and draws nothing. It is how the // interpreter is measured, and it must not crash on a nil device. testing.expect_value(t, run_content(doc, nil, transmute([]byte)string("q 1 0 0 1 2 2 cm 0 0 1 1 re W n /Im Do Q BT /F1 9 Tf (hi) Tj ET"), ct_res(doc), MATRIX_IDENTITY), Error.None) testing.expect_value(t, run_content(doc, nil, nil, nil, MATRIX_IDENTITY), Error.None) testing.expect_value(t, run_content(nil, nil, transmute([]byte)string("f"), nil, MATRIX_IDENTITY), Error.Invalid_Argument) bad := Matrix{1, 0, 0, 1, 0, math.INF_F64} testing.expect_value(t, run_content(doc, nil, transmute([]byte)string("f"), nil, bad), Error.Invalid_Argument) } @(test) test_content_mutually_recursive_forms_terminate :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) xobj := dict_dict(doc, res, "XObject") dict_set(xobj, "Ping", cf_stream(doc, "<< /Subtype /Form /BBox [0 0 10 10] >>", transmute([]byte)string("/Pong Do"))) dict_set(xobj, "Pong", cf_stream(doc, "<< /Subtype /Form /BBox [0 0 10 10] >>", transmute([]byte)string("/Ping Do 0 0 1 1 re f"))) testing.expect_value(t, ct_run(doc, &rec, "/Ping Do", res), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) testing.expect_value(t, rec_count(&rec, .Clip_Path), rec_count(&rec, .Pop_Clip)) } @(test) test_content_type3_glyph_that_shows_itself_terminates :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) // A glyph description that shows its own character through its own font. t3, _ := to_dict(dict_get(doc, dict_dict(doc, res, "Font"), "T3")) procs := dict_dict(doc, t3, "CharProcs") dict_set(procs, "square", cf_stream(doc, "<< >>", transmute([]byte)string("100 0 d0 0 0 100 100 re f BT /T3 10 Tf (A) Tj ET"))) t3res := new_dict(doc.allocator) dict_set(t3res, "Font", dict_get_raw(res, "Font")) dict_set(t3, "Resources", t3res) testing.expect_value(t, ct_run(doc, &rec, "BT /T3 10 Tf (A) Tj ET", res), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } @(test) test_content_d0_d1_outside_a_glyph_are_harmless :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // d1 declares a stencil, and outside a Type 3 glyph description that // would silently swallow every colour operator on the page. testing.expect_value(t, ct_run(doc, &rec, "100 0 0 0 100 100 d1 1 0 0 rg 0 0 1 1 re f"), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil { return } ct_near(t, e.paint.rgb[0], 1, "colour still works after a stray d1", 1e-6) testing.expect_value(t, rec_count(&rec, .Clip_Path), 0) } // Inside a glyph description d1 does take effect: the whole glyph paints in // the colour that was current when it was shown. @(test) test_content_d1_ignores_colour_inside_a_glyph :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) t3, _ := to_dict(dict_get(doc, dict_dict(doc, res, "Font"), "T3")) procs := dict_dict(doc, t3, "CharProcs") dict_set(procs, "square", cf_stream(doc, "<< >>", transmute([]byte)string("100 0 0 0 100 100 d1 0 0 1 rg 0 0 100 100 re f"))) testing.expect_value(t, ct_run(doc, &rec, "1 0 0 rg BT /T3 10 Tf (A) Tj ET", res), Error.None) e := rec_nth(&rec, .Fill_Path) if e == nil { testing.expect(t, false, "the glyph did not draw") return } ct_near(t, e.paint.rgb[0], 1, "d1 keeps the caller's colour", 1e-6) ct_near(t, e.paint.rgb[2], 0, "the glyph's own blue must be ignored", 1e-6) // The declared bounding box clips the glyph. testing.expect_value(t, rec_count(&rec, .Clip_Path), 1) testing.expect_value(t, rec_count(&rec, .Pop_Clip), 1) } @(test) test_content_text_operators_outside_BT :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) // Producers omit BT. The text matrix starts as the identity either way, // so showing text still works rather than being dropped. testing.expect_value(t, ct_run(doc, &rec, "/F1 10 Tf 5 5 Td (A) Tj ET", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Text) if e == nil { testing.expect(t, false, "text without BT must still draw") return } ct_point(t, ct_origin(e.items, 0), 5, 5, "") } @(test) test_content_junk_bytes_do_not_stop_the_page :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "0 0 1 1 re f ] } ) >> << /A 1 >> [1 2 3] % a comment\n 0 0 1 1 re f"), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 2) } // Truncating a rich stream at every offset must never crash, hang or leave the // device's clip stack unbalanced. Reaching the end of this test is the property. @(test) test_content_truncation_fuzz :: proc(t: ^testing.T) { SRC :: "q 2 0 0 2 1 1 cm /G1 gs 0 0 10 10 re W n 1 0 0 rg /Pattern cs /Tile scn " + "0 0 20 20 re f BT /F1 10 Tf 1 Tc 2 Tw 120 Tz 3 Ts 4 Tr [(AB) -300 (C)] TJ ET " + "/Sh sh /Box Do /Im Do BI /W 2 /H 2 /CS /G /BPC 8 ID \x01\x02\x03\x04 EI " + "/OC /L1 BDC 0 0 1 1 re f EMC Q" src := SRC for cut in 0 ..= len(src) { doc := cf_open(t) rec: Recorder rec_init(&rec) err := ct_run(doc, &rec, src[:cut], ct_res(doc)) testing.expectf(t, err == .None, "truncation at %d returned %v", cut, err) pushes := rec_count(&rec, .Clip_Path) + rec_count(&rec, .Clip_Stroke_Path) + rec_count(&rec, .Clip_Text) + rec_count(&rec, .Clip_Image_Mask) testing.expectf(t, pushes == rec_count(&rec, .Pop_Clip), "truncation at %d left the clip stack unbalanced", cut) testing.expectf(t, rec_count(&rec, .Begin_Layer) == rec_count(&rec, .End_Layer), "truncation at %d left a layer open", cut) testing.expectf(t, rec_count(&rec, .Begin_Tile) == rec_count(&rec, .End_Tile), "truncation at %d left a tile open", cut) testing.expectf(t, rec_count(&rec, .Begin_Group) == rec_count(&rec, .End_Group), "truncation at %d left a group open", cut) rec_destroy(&rec) cf_close(t, doc) } testing.expect(t, true, "every truncation was survived") } // The same, byte by byte: one flipped byte anywhere must not change that. @(test) test_content_bit_flip_fuzz :: proc(t: ^testing.T) { SRC :: "q 1 0 0 1 2 2 cm 0 0 10 10 re W n BT /F1 12 Tf (hello) Tj ET " + "/Box Do BI /W 2 /H 2 /CS /G /BPC 8 ID \x01\x02\x03\x04 EI Q" buf := make([]byte, len(SRC)) defer delete(buf) for i in 0 ..< len(SRC) { for bit in 0 ..< 3 { copy(buf, transmute([]byte)string(SRC)) buf[i] ~= 1 << u8(bit * 3) doc := cf_open(t) rec: Recorder rec_init(&rec) err := run_content(doc, &rec.dev, buf, ct_res(doc), MATRIX_IDENTITY) testing.expectf(t, err == .None, "flip at %d/%d returned %v", i, bit, err) pushes := rec_count(&rec, .Clip_Path) + rec_count(&rec, .Clip_Stroke_Path) + rec_count(&rec, .Clip_Text) + rec_count(&rec, .Clip_Image_Mask) testing.expectf(t, pushes == rec_count(&rec, .Pop_Clip), "flip at %d/%d left the clip stack unbalanced", i, bit) rec_destroy(&rec) cf_close(t, doc) } } testing.expect(t, true, "every single-bit flip was survived") } // A tiling pattern replays its cell thousands of times, and every replay is a // fresh run of the interpreter. Scratch memory has to be reused between them: // the run's allocator is an arena and reclaims nothing until the end, so a // runner that allocated its own operand stack per cell would cost tens of // megabytes for one fill. @(test) test_content_scratch_memory_is_reused_between_runs :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) rec.tiles = 4096 arena: virtual.Arena if aerr := virtual.arena_init_growing(&arena); aerr != nil { testing.expect(t, false, "could not make an arena") return } defer virtual.arena_destroy(&arena) err := ct_run(doc, &rec, "/Pattern cs /Tile scn 0 0 2000 2000 re f", ct_res(doc), MATRIX_IDENTITY, Content_Options{allocator = virtual.arena_allocator(&arena)}) testing.expect_value(t, err, Error.None) cells := rec_count(&rec, .Fill_Path) testing.expect(t, cells > 1000, "the cell must actually have been replayed") used := int(arena.total_used) testing.expectf(t, used < 4 << 20, "%d bytes of scratch for %d cells", used, cells) } // --------------------------------------------------------------------------- // Corpus // // The interpreter driven over every page of every real PDF in the directory // named by PDF_ODIN_CORPUS, and skipped silently when that is unset. There is // no correct answer to compare a real page against -- that is what the tests // above are for -- so what is asserted here is the contract the three devices // will be written against: the run terminates, it never returns an error, and // the clip, layer, group and tile stacks it hands the device are balanced when // it does. That is exactly the class of bug a corpus finds and a unit test // cannot. // --------------------------------------------------------------------------- @(private = "file") CC_MAX_FILE :: 64 << 20 @(private = "file") CC_MAX_DEPTH :: 8 @(private = "file") Cc_Dev :: struct { dev: Device, // Live nesting, so an unbalanced pop is caught where it happens. clip: int, layer: int, group: int, tile: int, mask: int, // The worst each reached, and whether anything ever went negative. negative: bool, // Totals, reported at the end so a silent no-op run cannot pass for a // successful one. fills, strokes, texts, glyphs, images, shades: int, } @(private = "file") cc :: proc(dev: ^Device) -> ^Cc_Dev { return (^Cc_Dev)(dev.user) } @(private = "file") cc_fill_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { cc(dev).fills += 1 } @(private = "file") cc_stroke_path :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { cc(dev).strokes += 1 } @(private = "file") cc_clip_path :: proc(dev: ^Device, path: ^Path, even_odd: bool, ctm: Matrix) { cc(dev).clip += 1 } @(private = "file") cc_clip_stroke :: proc(dev: ^Device, path: ^Path, stroke: ^Stroke_State, ctm: Matrix) { cc(dev).clip += 1 } @(private = "file") cc_clip_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { cc(dev).clip += 1 } @(private = "file") cc_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) { cc(dev).clip += 1 } @(private = "file") cc_pop_clip :: proc(dev: ^Device) { d := cc(dev) d.clip -= 1 if d.clip < 0 { d.negative = true } } @(private = "file") cc_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix, paint: Paint) { d := cc(dev) d.texts += 1 d.glyphs += len(txt.items) } @(private = "file") cc_stroke_text :: proc(dev: ^Device, txt: ^Text, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { cc(dev).texts += 1 } @(private = "file") cc_ignore_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { d := cc(dev) d.texts += 1 d.glyphs += len(txt.items) } @(private = "file") cc_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { cc(dev).images += 1 } @(private = "file") cc_shade :: proc(dev: ^Device, shade: ^Shading, ctm: Matrix, paint: Paint) { cc(dev).shades += 1 } @(private = "file") cc_begin_mask :: proc(dev: ^Device, area: Rect, luminosity: bool, backdrop: Paint) { cc(dev).mask += 1 } @(private = "file") cc_end_mask :: proc(dev: ^Device, mask: ^Soft_Mask, transfer: ^Function) { d := cc(dev) d.mask -= 1 if d.mask < 0 { d.negative = true } } @(private = "file") cc_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { cc(dev).group += 1 } @(private = "file") cc_end_group :: proc(dev: ^Device) { d := cc(dev) d.group -= 1 if d.group < 0 { d.negative = true } } @(private = "file") cc_begin_tile :: proc(dev: ^Device, area, view: Rect, xstep, ystep: f64, ctm: Matrix) -> int { cc(dev).tile += 1 return 0 } @(private = "file") cc_end_tile :: proc(dev: ^Device) { d := cc(dev) d.tile -= 1 if d.tile < 0 { d.negative = true } } @(private = "file") cc_begin_layer :: proc(dev: ^Device, name: string) { cc(dev).layer += 1 } @(private = "file") cc_end_layer :: proc(dev: ^Device) { d := cc(dev) d.layer -= 1 if d.layer < 0 { d.negative = true } } @(private = "file") cc_init :: proc(d: ^Cc_Dev) { d.dev = Device{ user = d, fill_path = cc_fill_path, stroke_path = cc_stroke_path, clip_path = cc_clip_path, clip_stroke_path = cc_clip_stroke, clip_text = cc_clip_text, clip_image_mask = cc_clip_image_mask, pop_clip = cc_pop_clip, fill_text = cc_text, stroke_text = cc_stroke_text, ignore_text = cc_ignore_text, fill_image = cc_image, fill_image_mask = cc_image, fill_shade = cc_shade, begin_mask = cc_begin_mask, end_mask = cc_end_mask, begin_group = cc_begin_group, end_group = cc_end_group, begin_tile = cc_begin_tile, end_tile = cc_end_tile, begin_layer = cc_begin_layer, end_layer = cc_end_layer, } } @(private = "file") cc_corpus_dir :: proc(allocator := context.allocator) -> string { raw := os.get_env("PDF_ODIN_CORPUS", allocator) trimmed := strings.trim_space(raw) out := strings.clone(trimmed, allocator) delete(raw, allocator) return out } @(private = "file") cc_walk :: proc(dir: string, out: ^[dynamic]string, depth := 0) { if depth > CC_MAX_DEPTH { return } entries, err := os.read_directory_by_path(dir, -1, context.allocator) if err != nil { return } defer os.file_info_slice_delete(entries, context.allocator) slice.sort_by(entries, proc(a, b: os.File_Info) -> bool { return a.name < b.name }) for e in entries { if e.type == .Directory { cc_walk(e.fullpath, out, depth + 1) continue } if e.type != .Regular || e.size > CC_MAX_FILE { continue } lower := strings.to_lower(e.name, context.temp_allocator) if !strings.has_suffix(lower, ".pdf") { continue } append(out, strings.clone(e.fullpath)) } } @(test) test_content_corpus :: proc(t: ^testing.T) { dir := cc_corpus_dir() defer delete(dir) if dir == "" { log.info("PDF_ODIN_CORPUS is unset; skipping the content corpus test") return } if !os.is_dir(dir) { log.infof("PDF_ODIN_CORPUS is %q, which is not a directory; skipping", dir) return } paths := make([dynamic]string) defer { for path in paths { delete(path) } delete(paths) } cc_walk(dir, &paths) if len(paths) == 0 { log.infof("no PDFs under %q; skipping", dir) return } // One arena for the whole sweep, reset between pages. That is how a // renderer is meant to drive the interpreter, and it is the difference // between a 900-page document costing one page's scratch and 900. arena: virtual.Arena if aerr := virtual.arena_init_growing(&arena); aerr != nil { testing.expect(t, false, "could not make a scratch arena") return } defer virtual.arena_destroy(&arena) opts := Content_Options{allocator = virtual.arena_allocator(&arena)} total: Cc_Dev files, pages, peak := 0, 0, 0 for path in paths { doc, err := open_file(path) if err != .None || doc == nil { continue } defer close(doc) files += 1 for i in 0 ..< page_count(doc) { page, perr := page_get(doc, i) if perr != .None { continue } data, cerr := page_content(&page, context.allocator) defer delete(data) if cerr != .None || len(data) == 0 { continue } dev: Cc_Dev cc_init(&dev) // The page flip a renderer would use, so the geometry is real. flip := Matrix{1, 0, 0, -1, -page.media_box.x0, page.media_box.y1} rerr := run_content(doc, &dev.dev, data, page.resources, flip, opts) name := filepath.base(path) testing.expectf(t, rerr == .None, "%s page %d: run_content returned %v", name, i, rerr) testing.expectf(t, dev.clip == 0, "%s page %d: %d clips left open", name, i, dev.clip) testing.expectf(t, !dev.negative, "%s page %d: a device stack went negative", name, i) testing.expectf(t, dev.layer == 0, "%s page %d: %d layers left open", name, i, dev.layer) testing.expectf(t, dev.group == 0, "%s page %d: %d groups left open", name, i, dev.group) testing.expectf(t, dev.tile == 0, "%s page %d: %d tiles left open", name, i, dev.tile) testing.expectf(t, dev.mask == 0, "%s page %d: %d masks left open", name, i, dev.mask) total.fills += dev.fills total.strokes += dev.strokes total.texts += dev.texts total.glyphs += dev.glyphs total.images += dev.images total.shades += dev.shades pages += 1 peak = max(peak, int(arena.total_used)) free_all(virtual.arena_allocator(&arena)) } } log.infof("content corpus: %d files, %d pages, %d fills, %d strokes, %d text runs, " + "%d glyphs, %d images, %d shadings, peak scratch %d KiB", files, pages, total.fills, total.strokes, total.texts, total.glyphs, total.images, total.shades, peak / 1024) // A sweep that drew nothing would pass every assertion above. testing.expect(t, total.glyphs > 0, "the corpus produced no glyphs at all") testing.expect(t, total.fills + total.strokes > 0, "the corpus produced no path painting") } // 8.7.4.3: a shading pattern's own /ExtGState is put into effect while it // paints, and replaces the compositing parameters rather than compounding // with them. @(test) test_content_shading_pattern_extgstate :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/G1 gs /Pattern cs /ShadeGS scn 0 0 10 10 re f", ct_res(doc)), Error.None) e := rec_nth(&rec, .Fill_Shade) if e == nil { testing.expect(t, false, "the shading pattern did not paint") return } // /G1 set ca 0.5 and Multiply; the pattern's own state says 0.25 and Screen. ct_near(t, e.paint.alpha, 0.25, "the pattern's ExtGState alpha wins") testing.expect_value(t, e.paint.blend, Blend_Mode.Screen) } // A BDC that arrives without its two operands still OPENS a section. If it did // not, the matching EMC would close the ENCLOSING one: the /OC section here // would end at the first EMC, the fill would stop being hidden, and the rest of // the outer section would draw. `content_mc_begin` has the machinery for a BDC // it cannot record -- the `mc_over` counter -- and the short-operand path used // to return before reaching it. @(test) test_content_short_bdc_still_pairs_with_its_emc :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) ct_optional_content(doc, res) // The inner BDC is one operand short, so the first EMC must close IT. testing.expect_value(t, ct_run(doc, &rec, "/OC /L2 BDC /Foo BDC EMC 0 0 1 1 re f EMC", res), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 0) } // The same stream over a VISIBLE layer: the fill belongs inside the layer, so // end_layer must not arrive before it. @(test) test_content_short_bdc_does_not_close_a_layer_early :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) ct_optional_content(doc, res) testing.expect_value(t, ct_run(doc, &rec, "/OC /L1 BDC /Foo BDC EMC 0 0 1 1 re f EMC", res), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{.Begin_Layer, .Fill_Path, .End_Layer}), "the fill is inside the layer it was written inside") } // The same nesting with a WELL-FORMED inner BDC, which is the behaviour the // anonymous section above has to imitate: the first EMC closes the INNER // section and the second the outer one, so the fill between them is still // inside the hidden section and the one after both of them is not. @(test) test_content_nested_bdc_pairs_innermost_first :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) ct_optional_content(doc, res) testing.expect_value(t, ct_run(doc, &rec, "/OC /L2 BDC /P << /MCID 0 >> BDC EMC 0 0 1 1 re f EMC 0 0 1 1 re f", res), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) } // And over a VISIBLE layer, where the device layer's span is what is at stake: // the inner EMC must not end it early. @(test) test_content_nested_bdc_brackets_a_visible_layer :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := ct_res(doc) ct_optional_content(doc, res) testing.expect_value(t, ct_run(doc, &rec, "/OC /L1 BDC /P << /MCID 0 >> BDC EMC 0 0 1 1 re f EMC", res), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{.Begin_Layer, .Fill_Path, .End_Layer}), "the inner EMC closes the inner section, not the layer") } // 8.10.2 makes /BBox a clip on the form's contents, so a degenerate box means // nothing is drawn. Skipping the clip because the box has no area meant the // opposite: the form painted its whole content, unclipped, over the page. @(test) test_content_form_with_a_zero_area_bbox_is_clipped_away :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) res := new_dict(doc.allocator) xo := new_dict(doc.allocator) dict_set(xo, "Zero", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 0 0] >>", transmute([]byte)string("0 0 20 20 re f"))) dict_set(xo, "Flat", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 100 0] >>", transmute([]byte)string("0 0 20 20 re f"))) dict_set(res, "XObject", xo) testing.expect_value(t, ct_run(doc, &rec, "/Zero Do /Flat Do", res), Error.None) kinds := rec_kinds(&rec) defer delete(kinds) testing.expect(t, slice.equal(kinds, []Rec_Kind{ .Clip_Path, .Fill_Path, .Pop_Clip, .Clip_Path, .Fill_Path, .Pop_Clip, }), "a degenerate /BBox is pushed as a clip like any other") } // --------------------------------------------------------------------------- // What one page may spend // // A page is not one run -- the content stream is one and every annotation's // appearance stream is another -- and the things below are bounded per PAGE // on the shared state, because a per-run cap would let MAX_ANNOTS of them // multiply it. Each of these is a small file that used to cost minutes or // gigabytes. // --------------------------------------------------------------------------- // The same ExtGState applied again under the same CTM is the same mask. // Rebuilding it costs a slot of the page's budget, an allocation, and -- since // the device would not recognise the new one -- a whole re-render of the // mask's group: 3000 `/S gs` measured 57 seconds at 1x. @(test) test_content_repeated_gs_renders_one_soft_mask :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) N :: 3000 b: strings.Builder strings.builder_init(&b) defer strings.builder_destroy(&b) for _ in 0 ..< N { strings.write_string(&b, "/GMask gs ") } strings.write_string(&b, "0 0 1 1 re f") start := time.now() testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b), ct_res(doc)), Error.None) ms := time.duration_milliseconds(time.since(start)) testing.expectf(t, rec_count(&rec, .Begin_Mask) == 1, "%d gs installed the same mask %d times", N, rec_count(&rec, .Begin_Mask)) // The first fill is the mask group's own; the second is the page's. e := rec_nth(&rec, .Fill_Path, 1) testing.expect(t, e != nil && e.has_smask, "and the mask is still in force") // Not a performance bound: one re-render per `gs` is 57 seconds. testing.expectf(t, ms < 5000, "%d gs took %.0f ms", N, ms) } // The cache is keyed by the CTM as well, because the CTM in force when a mask // is installed is where the mask lands (11.6.5.2). The same ExtGState under a // different CTM is a different mask and has to be drawn again. @(test) test_content_soft_mask_cache_respects_the_ctm :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, "/GMask gs 1 0 0 1 30 0 cm /GMask gs 0 0 1 1 re f", ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Begin_Mask), 2) a := rec_nth(&rec, .Begin_Mask, 0) c := rec_nth(&rec, .Begin_Mask, 1) if a == nil || c == nil { return } testing.expectf(t, c.area.x0 - a.area.x0 == 30, "the second mask is drawn where the second CTM puts it: %v then %v", a.area, c.area) } // A Soft_Mask outlives the operator that installed it -- a `Q` can bring it // back -- but not the run: it belongs to the interpreter's scratch, which the // page reclaims. On the Document it would be reclaimed only by `close`, so a // viewer re-rendering the same page at every zoom step would add a thousand // of them per render for as long as the file was open. @(test) test_content_soft_masks_do_not_grow_the_document :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) b: strings.Builder strings.builder_init(&b) defer strings.builder_destroy(&b) // A different CTM every time, so every one of these is a mask the cache // above cannot answer and the interpreter has to build. for _ in 0 ..< MAX_SOFT_MASKS { strings.write_string(&b, "1 0 0 1 0.1 0 cm /GMask gs ") } src := strings.to_string(b) run :: proc(t: ^testing.T, doc: ^Document, res: ^Dict, src: string) -> int { rec: Recorder rec_init(&rec) defer rec_destroy(&rec) testing.expect_value(t, ct_run(doc, &rec, src, res), Error.None) return rec_count(&rec, .Begin_Mask) } // ct_res builds its dictionaries in the Document arena, so it is built // once, here, rather than inside what is being measured. The first run // warms everything the Document legitimately caches with it: the mask // form's decoded stream, its colour space, its group dictionary. res := ct_res(doc) testing.expect_value(t, run(t, doc, res, src), MAX_SOFT_MASKS) before := document_memory_used(doc) testing.expect_value(t, run(t, doc, res, src), MAX_SOFT_MASKS) growth := document_memory_used(doc) - before testing.expectf(t, growth < 8192, "rendering the same content twice added %d bytes of Document memory", growth) } // `BI ID EI` is two tokens, so the token budget alone paid for two million // inline images from a 37 KB file -- each with a dictionary in the Document // arena, which only `close` reclaims. Nothing drawable comes out of any of // them. @(test) test_content_empty_inline_images_cost_nothing :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) N :: 200_000 b: strings.Builder strings.builder_init(&b) defer strings.builder_destroy(&b) for _ in 0 ..< N { strings.write_string(&b, "BI ID EI ") } // The stream must still be in step after all of them. strings.write_string(&b, "0 0 1 1 re f") before := document_memory_used(doc) start := time.now() testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b), ct_res(doc)), Error.None) ms := time.duration_milliseconds(time.since(start)) growth := document_memory_used(doc) - before testing.expect_value(t, rec_count(&rec, .Fill_Image), 0) testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) testing.expectf(t, growth < 1 << 20, "%d inline images added %d bytes", N, growth) testing.expectf(t, ms < 5000, "%d inline images took %.0f ms", N, ms) } // And the ones that ARE images are counted, because what they cost the // Document is the same whether they draw or not. A refused one still has to // consume exactly its own bytes: these carry `EI` inside their sample data -- // which is why the length is computed and not scanned for -- and drawing // operators after it, so a resynchronisation that lands anywhere but the end // paints something the file never asked for. @(test) test_content_inline_images_are_capped_per_page :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) rec: Recorder rec_init(&rec) defer rec_destroy(&rec) PAYLOAD :: " EI 1 0 0 rg 5 5 10 10 re f " one := fmt.tprintf("BI /W %d /H 1 /BPC 8 /CS /G ID %sEI ", len(PAYLOAD), PAYLOAD) N :: MAX_INLINE_IMAGES + 100 b: strings.Builder strings.builder_init(&b) defer strings.builder_destroy(&b) for _ in 0 ..< N { strings.write_string(&b, one) } strings.write_string(&b, "0 0 1 1 re f") repairs := len(doc.repairs) testing.expect_value(t, ct_run(doc, &rec, strings.to_string(b), ct_res(doc)), Error.None) testing.expect_value(t, rec_count(&rec, .Fill_Image), MAX_INLINE_IMAGES) testing.expect(t, len(doc.repairs) > repairs, "hitting the cap is recorded as a Repair") // One fill: the marker at the end. Any more came out of an image's pixels. testing.expect_value(t, rec_count(&rec, .Fill_Path), 1) }