package pdf import "core:fmt" import "core:log" import "core:mem/virtual" import "core:os" import "core:path/filepath" import "core:slice" import "core:strings" import "core:testing" // Annotation tests. // // These build whole files rather than Documents by hand: an annotation is a // dictionary that names an appearance stream that names a resource, and the // cases worth testing -- an /AS that picks one of two states, a /Rect written // backwards -- only exist once the references are real. `Pdf_Builder` comes // from xref_test.odin. // // Everything that draws is checked through `An_Dev`, a Device that records the // calls it is handed. The interpreter's output IS its sequence of device // calls, so pinning the calls pins the behaviour; the appearance matrix is // then checked twice over, once against a hand-computed value out of // `annot_matrix` and once against where the ink actually landed. // --------------------------------------------------------------------------- // The recording device // --------------------------------------------------------------------------- @(private = "file") An_Kind :: enum u8 { Fill, Stroke, Clip, Pop_Clip, Begin_Group, End_Group, Begin_Layer, End_Layer, } @(private = "file") An_Rec :: struct { kind: An_Kind, ctm: Matrix, // Bounding box of the path in DEVICE space: where the ink went, which is // what the placement algorithm is ultimately about. box: Rect, // The area a group was opened over. area: Rect, } @(private = "file") An_Dev :: struct { dev: Device, log: [dynamic]An_Rec, } // an_box is the device-space bound of a path. Nothing is cloned: the // interpreter reuses the path after the call, and a bounding box is all these // tests ever ask of it. @(private = "file") an_box :: proc(p: ^Path, ctm: Matrix) -> (r: Rect) { if p == nil || len(p.points) == 0 { return RECT_EMPTY } first := transform_point(ctm, p.points[0]) r = Rect{first.x, first.y, first.x, first.y} for pt in p.points { d := transform_point(ctm, pt) r.x0 = min(r.x0, d.x) r.y0 = min(r.y0, d.y) r.x1 = max(r.x1, d.x) r.y1 = max(r.y1, d.y) } return } @(private = "file") an_add :: proc(dev: ^Device, rec: An_Rec) { append(&(^An_Dev)(dev.user).log, rec) } @(private = "file") an_fill :: proc(dev: ^Device, p: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { an_add(dev, An_Rec{kind = .Fill, ctm = ctm, box = an_box(p, ctm)}) } @(private = "file") an_stroke :: proc(dev: ^Device, p: ^Path, stroke: ^Stroke_State, ctm: Matrix, paint: Paint) { an_add(dev, An_Rec{kind = .Stroke, ctm = ctm, box = an_box(p, ctm)}) } @(private = "file") an_clip :: proc(dev: ^Device, p: ^Path, even_odd: bool, ctm: Matrix) { an_add(dev, An_Rec{kind = .Clip, ctm = ctm, box = an_box(p, ctm)}) } @(private = "file") an_pop_clip :: proc(dev: ^Device) { an_add(dev, An_Rec{kind = .Pop_Clip}) } @(private = "file") an_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { an_add(dev, An_Rec{kind = .Begin_Group, area = area}) } @(private = "file") an_end_group :: proc(dev: ^Device) { an_add(dev, An_Rec{kind = .End_Group}) } @(private = "file") an_begin_layer :: proc(dev: ^Device, name: string) { an_add(dev, An_Rec{kind = .Begin_Layer}) } @(private = "file") an_end_layer :: proc(dev: ^Device) { an_add(dev, An_Rec{kind = .End_Layer}) } @(private = "file") an_dev_init :: proc(d: ^An_Dev) { d.log = make([dynamic]An_Rec, 0, 16) d.dev = Device{ user = d, fill_path = an_fill, stroke_path = an_stroke, clip_path = an_clip, pop_clip = an_pop_clip, begin_group = an_begin_group, end_group = an_end_group, begin_layer = an_begin_layer, end_layer = an_end_layer, } } @(private = "file") an_dev_destroy :: proc(d: ^An_Dev) { delete(d.log) d.log = nil } @(private = "file") an_count :: proc(d: ^An_Dev, kind: An_Kind) -> (n: int) { for e in d.log { if e.kind == kind { n += 1 } } return } @(private = "file") an_nth :: proc(d: ^An_Dev, kind: An_Kind, n := 0) -> ^An_Rec { seen := 0 for &e in d.log { if e.kind != kind { continue } if seen == n { return &e } seen += 1 } return nil } // --------------------------------------------------------------------------- // Assertions // --------------------------------------------------------------------------- @(private = "file") AN_EPS :: 1e-9 @(private = "file") an_expect_matrix :: proc(t: ^testing.T, got, want: Matrix, what: string, loc := #caller_location) { ok := abs(got.a - want.a) <= AN_EPS && abs(got.b - want.b) <= AN_EPS && abs(got.c - want.c) <= AN_EPS && abs(got.d - want.d) <= AN_EPS && abs(got.e - want.e) <= AN_EPS && abs(got.f - want.f) <= AN_EPS testing.expectf(t, ok, "%s: matrix %v, wanted %v", what, got, want, loc = loc) } @(private = "file") an_expect_rect :: proc(t: ^testing.T, got, want: Rect, what: string, tol := AN_EPS, loc := #caller_location) { ok := abs(got.x0 - want.x0) <= tol && abs(got.y0 - want.y0) <= tol && abs(got.x1 - want.x1) <= tol && abs(got.y1 - want.y1) <= tol testing.expectf(t, ok, "%s: rect %v, wanted %v", what, got, want, loc = loc) } // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- // The appearance every placement test draws: a rectangle filling the whole of // its own /BBox, so the ink the device records is exactly the box the // algorithm mapped onto the /Rect. @(private = "file") AN_FILL_BBOX :: "0 0 100 50 re f" // an_page writes the one-page skeleton these tests share: catalog 1, page tree // 2, page 3. Test objects start at 4. @(private = "file") an_page :: proc(pb: ^Pdf_Builder, annots: string, catalog_extra := "") { pb_obj(pb, 1, fmt.tprintf("<< /Type /Catalog /Pages 2 0 R %s >>", catalog_extra)) pb_obj(pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") pb_obj(pb, 3, fmt.tprintf( "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] /Annots %s >>", annots)) } @(private = "file") an_finish :: proc(pb: ^Pdf_Builder) { at := pb_classic_xref(pb) pb_str(pb, "trailer\n<< /Size ") pb_num(pb, len(pb.offs)) pb_str(pb, " /Root 1 0 R >>\n") pb_startxref(pb, at) } // an_ap writes an appearance stream object. @(private = "file") an_ap :: proc(pb: ^Pdf_Builder, num: u32, dict_head: string, content := AN_FILL_BBOX) { pb_stream_obj(pb, num, fmt.tprintf("/Type /XObject /Subtype /Form %s", dict_head), transmute([]byte)content) } @(private = "file") an_open :: proc(t: ^testing.T, pb: ^Pdf_Builder, loc := #caller_location) -> (^Document, Page) { an_finish(pb) doc, err := open_bytes(pb.buf[:]) testing.expectf(t, err == .None, "open failed: %v", err, loc = loc) if doc == nil { return nil, Page{} } page, perr := page_get(doc, 0) testing.expectf(t, perr == .None, "page_get failed: %v", perr, loc = loc) return doc, page } // an_one loads the page's annotations and returns the first, for the many // tests that build exactly one. @(private = "file") an_one :: proc(t: ^testing.T, doc: ^Document, page: ^Page, loc := #caller_location) -> (Annot, bool) { annots, err := annot_load(doc, page) defer delete(annots) testing.expectf(t, err == .None, "annot_load failed: %v", err, loc = loc) if len(annots) != 1 { testing.expectf(t, false, "loaded %d annotations, wanted 1", len(annots), loc = loc) return Annot{}, false } return annots[0], true } // --------------------------------------------------------------------------- // The appearance matrix -- ISO 32000-2, 12.5.5 // --------------------------------------------------------------------------- // A 100x50 appearance in a 200x100 rectangle at (20, 30): scaled by two in // both directions and moved to the rectangle's corner. @(test) test_annot_matrix_bbox_to_rect :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } form, invoke, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) testing.expect(t, mok, "annot_matrix refused a well-formed appearance") an_expect_matrix(t, form, Matrix{2, 0, 0, 2, 20, 30}, "form matrix") // No /Matrix on the form, so the two are the same here; the rotating case // below is where they part company. an_expect_matrix(t, invoke, Matrix{2, 0, 0, 2, 20, 30}, "invoke matrix") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) fill := an_nth(&dev, .Fill) testing.expect(t, fill != nil, "appearance did not draw") if fill == nil { return } an_expect_matrix(t, fill.ctm, Matrix{2, 0, 0, 2, 20, 30}, "ctm the fill was handed") // The appearance fills its own /BBox, so its ink must land on exactly the // annotation's rectangle. an_expect_rect(t, fill.box, Rect{20, 30, 220, 130}, "ink") } // A /Matrix that rotates 90 degrees. The transformed appearance box is 50 wide // and 100 tall where the /BBox is 100 wide and 50 tall, so a placement that // skipped the bounding-box step would scale both axes wrongly and land the ink // outside the rectangle. @(test) test_annot_matrix_rotated :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 50 100] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50] /Matrix [0 1 -1 0 0 0]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } // By hand: the corners (0,0) (100,0) (0,50) (100,50) rotate to (0,0) // (0,100) (-50,0) (-50,100), whose bounding box is [-50 0 0 100] -- 50 by // 100, the same size as the /Rect. A is therefore a pure translation of // +50 in x, and the full matrix is /Matrix * A. form, invoke, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) testing.expect(t, mok, "annot_matrix refused a rotated appearance") an_expect_matrix(t, invoke, Matrix{1, 0, 0, 1, 50, 0}, "A") an_expect_matrix(t, form, Matrix{0, 1, -1, 0, 50, 0}, "form matrix") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) fill := an_nth(&dev, .Fill) testing.expect(t, fill != nil, "rotated appearance did not draw") if fill == nil { return } an_expect_matrix(t, fill.ctm, Matrix{0, 1, -1, 0, 50, 0}, "ctm the fill was handed") an_expect_rect(t, fill.box, Rect{0, 0, 50, 100}, "ink") } // 7.9.5: a rectangle's corners may be given in either order. A /Rect written // backwards must place the appearance exactly as the same rectangle written // forwards does. @(test) test_annot_rect_corners_reversed :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [220 130 20 30] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } an_expect_rect(t, a.rect, Rect{20, 30, 220, 130}, "loaded /Rect") form, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) testing.expect(t, mok, "annot_matrix refused a reversed /Rect") an_expect_matrix(t, form, Matrix{2, 0, 0, 2, 20, 30}, "form matrix") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) fill := an_nth(&dev, .Fill) testing.expect(t, fill != nil, "appearance did not draw") if fill != nil { an_expect_rect(t, fill.box, Rect{20, 30, 220, 130}, "ink") } } // The page CTM is the last multiplication of step 4, and a renderer's is // always a flip: y-up page space to y-down device space. @(test) test_annot_page_ctm_applies :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } flip := Matrix{1, 0, 0, -1, 0, 400} form, _, mok := annot_matrix(doc, &a, flip) testing.expect(t, mok, "annot_matrix refused a flipped page") an_expect_matrix(t, form, Matrix{2, 0, 0, -2, 20, 370}, "form matrix under the page flip") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, flip), Error.None) fill := an_nth(&dev, .Fill) testing.expect(t, fill != nil, "appearance did not draw") if fill != nil { // The rectangle's y range 30..130 in page space is 270..370 in a // 400-point-tall device space. an_expect_rect(t, fill.box, Rect{20, 270, 220, 370}, "ink") } } // A /BBox with no width has nothing in it to stretch onto the rectangle, and // step 3's scale factor would divide by zero. @(test) test_annot_degenerate_bbox_draws_nothing :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R 6 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 0 50]") // A /Matrix that collapses an honest /BBox is the same defect written the // other way round, and the bounding-box step is where it shows up. pb_obj(&pb, 6, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 7 0 R >> >>") an_ap(&pb, 7, "/BBox [0 0 100 50] /Matrix [1 0 0 0 0 0]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } annots, err := annot_load(doc, &page) defer delete(annots) testing.expect_value(t, err, Error.None) testing.expect_value(t, len(annots), 2) if len(annots) != 2 { return } dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) for &a in annots { _, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) testing.expect(t, !mok, "annot_matrix accepted a degenerate appearance box") testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) } testing.expectf(t, len(dev.log) == 0, "a degenerate appearance drew %d calls", len(dev.log)) } // An annotation whose /Rect has no area shows nothing however the appearance // is scaled -- and a missing /Rect reads as exactly that rectangle. @(test) test_annot_empty_rect_draws_nothing :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } _, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) testing.expect(t, !mok, "annot_matrix accepted an annotation with no /Rect") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expectf(t, len(dev.log) == 0, "an annotation with no /Rect drew %d calls", len(dev.log)) } // /BBox is required and is nonetheless sometimes missing. There is nothing to // map onto the /Rect then, so the appearance is drawn where its own // coordinates put it rather than not at all. @(test) test_annot_missing_bbox_draws_unplaced :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/Matrix [1 0 0 1 7 9]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } before := len(doc.repairs) form, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) testing.expect(t, mok, "a form with no /BBox should still be placed") an_expect_matrix(t, form, Matrix{1, 0, 0, 1, 7, 9}, "form matrix with no /BBox") testing.expect(t, len(doc.repairs) > before, "a missing /BBox recorded no repair") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) fill := an_nth(&dev, .Fill) testing.expect(t, fill != nil, "a form with no /BBox drew nothing") if fill != nil { an_expect_rect(t, fill.box, Rect{7, 9, 107, 59}, "ink") } // With no /BBox there is no box to clip to either. testing.expect_value(t, an_count(&dev, .Clip), 0) } // A scale factor that overflows to infinity is the finiteness boundary this // file owes the rest of the library: nothing below the Device line may see a // NaN, and a 1e300-wide rectangle over a 1e-300-wide box produces one. @(test) test_annot_matrix_overflow_rejected :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 1e300 1e300] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 1e-300 1e-300]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } _, _, mok := annot_matrix(doc, &a, MATRIX_IDENTITY) testing.expect(t, !mok, "annot_matrix returned a non-finite placement") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expectf(t, len(dev.log) == 0, "a non-finite placement drew %d calls", len(dev.log)) } // --------------------------------------------------------------------------- // Choosing an appearance -- /AP, /N and /AS // --------------------------------------------------------------------------- // The two states a checkbox holds. They are told apart by how many times they // fill, which is a property of the stream that ran and not of where it landed. @(private = "file") AN_STATE_OFF :: "0 0 100 50 re f" @(private = "file") AN_STATE_YES :: "0 0 100 50 re f 10 10 20 20 re f" @(private = "file") an_states :: proc(pb: ^Pdf_Builder, as_entry: string) { an_page(pb, "[4 0 R]") pb_obj(pb, 4, fmt.tprintf( "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] %s "+ "/AP << /N << /Off 5 0 R /Yes 6 0 R >> >> >>", as_entry)) an_ap(pb, 5, "/BBox [0 0 100 50]", AN_STATE_OFF) an_ap(pb, 6, "/BBox [0 0 100 50]", AN_STATE_YES) } // /AS is how a checkbox says which of its two looks it is wearing. @(test) test_annot_as_selects_state :: proc(t: ^testing.T) { Case :: struct { state: string, fills: int } // A composite literal cannot open a `for ... in`: the parser would read its // brace as the loop body. cases := []Case{{"Off", 1}, {"Yes", 2}} for c in cases { state, want_fills := c.state, c.fills pb := pb_make() defer pb_destroy(&pb) an_states(&pb, fmt.tprintf("/AS /%s", state)) doc, page := an_open(t, &pb) defer close(doc) if doc == nil { continue } a, ok := an_one(t, doc, &page) if !ok { continue } testing.expectf(t, a.state == state, "/AS %v selected state %q", state, a.state) dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expectf(t, an_count(&dev, .Fill) == want_fills, "/AS %v drew %d fills, wanted %d", state, an_count(&dev, .Fill), want_fills) } } // The unchecked checkbox: /N holds only /Yes and /AS says /Off. The missing // entry IS the appearance of the off state, and falling back to the only // stream present would tick every empty box on the page. @(test) test_annot_as_names_missing_state :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] /AS /Off "+ "/AP << /N << /Yes 5 0 R >> >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } testing.expect(t, a.ap == nil, "/AS /Off selected an appearance that is not there") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expectf(t, len(dev.log) == 0, "an unchecked box drew %d calls", len(dev.log)) } // With one state and no /AS there is nothing to disambiguate, and viewers all // draw it. With two there is no way to choose, and drawing the wrong one is // worse than drawing none. @(test) test_annot_no_as_falls_back_to_sole_state :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] "+ "/AP << /N << /Yes 5 0 R >> >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } testing.expect(t, a.ap != nil, "the only appearance state was not selected") testing.expectf(t, a.state == "Yes", "state %q, wanted Yes", a.state) dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expect_value(t, an_count(&dev, .Fill), 1) } @(test) test_annot_no_as_with_two_states_draws_nothing :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_states(&pb, "") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } before := len(doc.repairs) a, ok := an_one(t, doc, &page) if !ok { return } testing.expect(t, a.ap == nil, "an ambiguous appearance was drawn anyway") testing.expect(t, len(doc.repairs) > before, "an ambiguous appearance recorded no repair") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expectf(t, len(dev.log) == 0, "an ambiguous appearance drew %d calls", len(dev.log)) } // An annotation with no /AP draws nothing: this library does not synthesise // appearances. It is still loaded, because a link's rectangle is worth having // whether or not anything is painted in it. @(test) test_annot_without_ap_is_loaded_but_not_drawn :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R 5 0 R]") // A link with a border a synthesising reader would draw. pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Link /Rect [0 0 100 50] /Border [0 0 3] >>") // Only /N is page content: a /D and an /R are answers to a pointer. pb_obj(&pb, 5, "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] "+ "/AP << /D 6 0 R /R 6 0 R >> >>") an_ap(&pb, 6, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } annots, err := annot_load(doc, &page) defer delete(annots) testing.expect_value(t, err, Error.None) testing.expect_value(t, len(annots), 2) if len(annots) != 2 { return } testing.expect(t, annots[0].ap == nil, "a /Link with no /AP found an appearance") testing.expect(t, annots[1].ap == nil, "/D was drawn as if it were /N") testing.expect(t, annots[0].visible, "an annotation with no /AP is still visible content") dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expectf(t, len(dev.log) == 0, "annotations with no /AP drew %d calls", len(dev.log)) } // --------------------------------------------------------------------------- // /F flags and subtypes // --------------------------------------------------------------------------- @(test) test_annot_flags_read :: proc(t: ^testing.T) { testing.expect_value(t, annot_flags_read(0), Annot_Flags{}) // The format numbers its bits from 1, so /F 2 is the second bit. testing.expect_value(t, annot_flags_read(1), Annot_Flags{.Invisible}) testing.expect_value(t, annot_flags_read(2), Annot_Flags{.Hidden}) testing.expect_value(t, annot_flags_read(4), Annot_Flags{.Print}) testing.expect_value(t, annot_flags_read(32), Annot_Flags{.No_View}) testing.expect_value(t, annot_flags_read(6), Annot_Flags{.Hidden, .Print}) testing.expect_value(t, annot_flags_read(0x3ff), Annot_Flags{ .Invisible, .Hidden, .Print, .No_Zoom, .No_Rotate, .No_View, .Read_Only, .Locked, .Toggle_No_View, .Locked_Contents, }) // Bits above the ten the format defines are reserved, and a set holding // members no enumerator names is not something to hand a caller. testing.expect_value(t, annot_flags_read(1 << 20), Annot_Flags{}) testing.expect_value(t, annot_flags_read(-1), annot_flags_read(0x3ff)) } // Hidden and NoView suppress the drawing; Invisible does not, because it only // governs an annotation the reader has no handler for and an appearance stream // is a handler. @(test) test_annot_flags_suppress_drawing :: proc(t: ^testing.T) { Case :: struct { f: string, subtype: string, draws: bool } cases := []Case{ {"/F 0", "Stamp", true}, {"/F 1", "Stamp", true}, // Invisible {"/F 2", "Stamp", false}, // Hidden {"/F 3", "Stamp", false}, // Invisible + Hidden {"/F 4", "Stamp", true}, // Print {"/F 32", "Stamp", false}, // NoView {"/F 36", "Stamp", false}, // Print + NoView {"", "Stamp", true}, // A popup is the window a markup annotation opens, never page content. {"/F 4", "Popup", false}, } for c in cases { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, fmt.tprintf( "<< /Type /Annot /Subtype /%s /Rect [0 0 100 50] %s /AP << /N 5 0 R >> >>", c.subtype, c.f)) an_ap(&pb, 5, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { continue } a, ok := an_one(t, doc, &page) if !ok { continue } testing.expectf(t, a.visible == c.draws, "%s /%s: visible %v, wanted %v", c.f, c.subtype, a.visible, c.draws) dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run(doc, &a, &dev.dev, MATRIX_IDENTITY), Error.None) drew := an_count(&dev, .Fill) > 0 testing.expectf(t, drew == c.draws, "%s /%s: drew %v, wanted %v", c.f, c.subtype, drew, c.draws) } } // --------------------------------------------------------------------------- // Loading a page's /Annots // --------------------------------------------------------------------------- @(test) test_annot_load_page :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) // A null entry and a number are both how a producer leaves a hole in the // array rather than rewriting it; a direct dictionary is legal and has no // object number of its own. an_page(&pb, "[4 0 R null 5 0 R 7 << /Type /Annot /Subtype /Text /Rect [1 2 3 4] >>]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Link /Rect [10 20 110 70] /F 4 >>") pb_obj(&pb, 5, "<< /Type /Annot /Subtype /Widget /Rect [0 0 100 50] /F 2 "+ "/AP << /N 6 0 R >> >>") an_ap(&pb, 6, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } annots, err := annot_load(doc, &page) defer delete(annots) testing.expect_value(t, err, Error.None) testing.expect_value(t, len(annots), 3) if len(annots) != 3 { return } testing.expectf(t, annots[0].subtype == "Link", "subtype %q", annots[0].subtype) an_expect_rect(t, annots[0].rect, Rect{10, 20, 110, 70}, "link /Rect") testing.expect_value(t, annots[0].flags, Annot_Flags{.Print}) testing.expect_value(t, annots[0].ref, Ref{4, 0}) testing.expect(t, annots[0].dict != nil, "the annotation dictionary was not kept") testing.expectf(t, annots[1].subtype == "Widget", "subtype %q", annots[1].subtype) testing.expect(t, annots[1].ap != nil, "the widget's appearance was not found") testing.expect(t, !annots[1].visible, "/F 2 left the widget visible") testing.expect_value(t, annots[1].ref, Ref{5, 0}) // A dictionary written into the array directly has no object number. testing.expectf(t, annots[2].subtype == "Text", "subtype %q", annots[2].subtype) testing.expect_value(t, annots[2].ref, Ref{}) } // A page with no /Annots, and one whose /Annots is not an array, both have no // annotations. Neither is a failure. @(test) test_annot_load_no_annots :: proc(t: ^testing.T) { sources := []string{"", "/Annots 99 0 R", "/Annots << /Bad true >>", "/Annots []"} for src in sources { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") pb_obj(&pb, 3, fmt.tprintf( "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] %s >>", src)) doc, page := an_open(t, &pb) defer close(doc) if doc == nil { continue } annots, err := annot_load(doc, &page) defer delete(annots) testing.expectf(t, err == .None, "%q: annot_load failed: %v", src, err) testing.expectf(t, len(annots) == 0, "%q: loaded %d annotations", src, len(annots)) dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expectf(t, len(dev.log) == 0, "%q: drew %d calls", src, len(dev.log)) } } // annot_run_page draws the drawable ones in /Annots order, which is the order // they paint over one another in, and silently steps over the rest. @(test) test_annot_run_page_order :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R 5 0 R 6 0 R 7 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 10 10] /AP << /N 8 0 R >> >>") pb_obj(&pb, 5, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 10 10] /F 2 /AP << /N 8 0 R >> >>") pb_obj(&pb, 6, "<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] >>") pb_obj(&pb, 7, "<< /Type /Annot /Subtype /Stamp /Rect [100 100 110 110] /AP << /N 8 0 R >> >>") an_ap(&pb, 8, "/BBox [0 0 10 10]", "0 0 10 10 re f") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expect_value(t, an_count(&dev, .Fill), 2) if an_count(&dev, .Fill) != 2 { return } an_expect_rect(t, an_nth(&dev, .Fill, 0).box, Rect{0, 0, 10, 10}, "first stamp") an_expect_rect(t, an_nth(&dev, .Fill, 1).box, Rect{100, 100, 110, 110}, "second stamp") } // --------------------------------------------------------------------------- // What the appearance gets from the interpreter // --------------------------------------------------------------------------- // A form XObject's /BBox clips its contents (8.10.2), and an appearance stream // is a form XObject. The clip has to be pushed before the paint and popped // after it, or every annotation after this one draws through it. @(test) test_annot_bbox_clips_and_balances :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") // The content spills far outside the /BBox that clips it. an_ap(&pb, 5, "/BBox [0 0 100 50]", "-500 -500 2000 2000 re f") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expect_value(t, len(dev.log), 3) if len(dev.log) != 3 { return } testing.expect_value(t, dev.log[0].kind, An_Kind.Clip) testing.expect_value(t, dev.log[1].kind, An_Kind.Fill) testing.expect_value(t, dev.log[2].kind, An_Kind.Pop_Clip) // The clip is the /BBox under the appearance matrix: the annotation's // rectangle, exactly. an_expect_rect(t, dev.log[0].box, Rect{20, 30, 220, 130}, "clip") } // An appearance with a transparency group composites as one object, which is // how a stamp with a drop shadow keeps its alpha from being applied twice. @(test) test_annot_transparency_group :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [20 30 220 130] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50] /Group << /S /Transparency /I true >>") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expect_value(t, an_count(&dev, .Begin_Group), 1) testing.expect_value(t, an_count(&dev, .End_Group), 1) testing.expect_value(t, an_count(&dev, .Fill), 1) grp := an_nth(&dev, .Begin_Group) if grp != nil { an_expect_rect(t, grp.area, Rect{20, 30, 220, 130}, "group area") } } // An appearance that invokes itself is a file that would otherwise render // until the stack ends. The interpreter's visited set stops it; what this // pins is that annotations go through that interpreter rather than around it. @(test) test_annot_self_referencing_appearance :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 100 50] /AP << /N 5 0 R >> >>") pb_stream_obj(&pb, 5, "/Type /XObject /Subtype /Form /BBox [0 0 100 50] "+ "/Resources << /XObject << /Me 5 0 R >> >>", transmute([]byte)string("/Me Do 0 0 100 50 re f")) doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expect_value(t, an_count(&dev, .Fill), 1) } // An annotation may belong to an optional-content group (12.5.2). One the // catalog's default configuration switches off is not drawn -- and a run that // asks for hidden content, which is what text extraction does, draws it. @(test) test_annot_optional_content :: proc(t: ^testing.T) { build :: proc(pb: ^Pdf_Builder) { an_page(pb, "[4 0 R]", "/OCProperties << /OCGs [7 0 R] /D << /OFF [7 0 R] >> >>") pb_obj(pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 100 50] "+ "/OC 7 0 R /AP << /N 5 0 R >> >>") an_ap(pb, 5, "/BBox [0 0 100 50]") pb_obj(pb, 7, "<< /Type /OCG /Name (Watermark) >>") } { pb := pb_make() defer pb_destroy(&pb) build(&pb) doc, page := an_open(t, &pb) defer close(doc) if doc != nil { dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expectf(t, an_count(&dev, .Fill) == 0, "a switched-off annotation drew %d fills", an_count(&dev, .Fill)) } } { pb := pb_make() defer pb_destroy(&pb) build(&pb) doc, page := an_open(t, &pb) defer close(doc) if doc != nil { dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) opts := Content_Options{ignore_optional_content = true} testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY, opts), Error.None) testing.expect_value(t, an_count(&dev, .Fill), 1) } } } // An annotation with no /OC must not be wrapped in a layer, or every display // list gains a pair of calls per annotation. @(test) test_annot_without_oc_opens_no_layer :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 100 50] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) testing.expect_value(t, annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY), Error.None) testing.expect_value(t, an_count(&dev, .Begin_Layer), 0) testing.expect_value(t, an_count(&dev, .End_Layer), 0) } // --------------------------------------------------------------------------- // Damage and caller error // --------------------------------------------------------------------------- @(test) test_annot_invalid_arguments :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Stamp /Rect [0 0 100 50] /AP << /N 5 0 R >> >>") an_ap(&pb, 5, "/BBox [0 0 100 50]") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } a, ok := an_one(t, doc, &page) if !ok { return } nothing, err := annot_load(nil, &page) testing.expect_value(t, err, Error.Invalid_Argument) testing.expect(t, nothing == nil, "annot_load returned a slice for a nil Document") _, err2 := annot_load(doc, nil) testing.expect_value(t, err2, Error.Invalid_Argument) testing.expect_value(t, annot_run(nil, &a, nil, MATRIX_IDENTITY), Error.Invalid_Argument) testing.expect_value(t, annot_run(doc, nil, nil, MATRIX_IDENTITY), Error.Invalid_Argument) testing.expect_value(t, annot_run_page(doc, nil, nil, MATRIX_IDENTITY), Error.Invalid_Argument) // A matrix with a NaN or an infinity in it must not reach the geometry. bad := Matrix{1, 0, 0, 1, 0, max(f64) * 2} testing.expect_value(t, annot_run(doc, &a, nil, bad), Error.Invalid_Argument) testing.expect_value(t, annot_run_page(doc, &page, nil, bad), Error.Invalid_Argument) // A nil Device runs the whole interpreter and draws nothing, which is how // the annotation layer is measured and how a caller collects its repairs. testing.expect_value(t, annot_run(doc, &a, nil, MATRIX_IDENTITY), Error.None) testing.expect_value(t, annot_run_page(doc, &page, nil, MATRIX_IDENTITY), Error.None) } // Every one of these is a real shape of damage, and none of them may crash, // hang, or stop the page. @(test) test_annot_malformed :: proc(t: ^testing.T) { Case :: struct { subtype, annot, ap, content: string, draws: bool } BOX :: "/BBox [0 0 100 50]" cases := []Case{ // /AP that is not a dictionary, and /N that is not a stream. {"/Subtype /Stamp", "/AP 7", BOX, AN_FILL_BBOX, false}, {"/Subtype /Stamp", "/AP << /N 7 >>", BOX, AN_FILL_BBOX, false}, {"/Subtype /Stamp", "/AP << /N (text) >>", BOX, AN_FILL_BBOX, false}, {"/Subtype /Stamp", "/AP << /N << >> >>", BOX, AN_FILL_BBOX, false}, // /Rect that is not a rectangle, or is not four numbers. Entries that // are not numbers read as zero, which is an empty rectangle. {"/Subtype /Stamp", "/Rect (nope) /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, false}, {"/Subtype /Stamp", "/Rect [0 0 100] /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, false}, {"/Subtype /Stamp", "/Rect [/a /b /c /d] /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, false}, // A /Subtype that is missing entirely, and one nobody has heard of. // Neither is a reason not to draw what the file says to draw. {"", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, true}, {"/Subtype /Hologram", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, true}, // A /Matrix too short to be one falls back to the identity, and a // /BBox with its corners the wrong way round is still a box. {"/Subtype /Stamp", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", "/BBox [0 0 100 50] /Matrix [1 0 0]", AN_FILL_BBOX, true}, {"/Subtype /Stamp", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", "/BBox [100 50 0 0]", AN_FILL_BBOX, true}, // /F that is not a number reads as no flags at all. {"/Subtype /Stamp", "/Rect [0 0 100 50] /F (2) /AP << /N 5 0 R >>", BOX, AN_FILL_BBOX, true}, // An appearance with no content in it draws nothing and is not damage. {"/Subtype /Stamp", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", BOX, "", false}, // Operator soup in the appearance: the interpreter steps over it and // the fill that follows still lands. {"/Subtype /Stamp", "/Rect [0 0 100 50] /AP << /N 5 0 R >>", BOX, "q 1 0 0 rg /Nope gs ] ] } 0 0 100 50 re f Q", true}, } for c, i in cases { pb := pb_make() defer pb_destroy(&pb) an_page(&pb, "[4 0 R]") pb_obj(&pb, 4, fmt.tprintf("<< /Type /Annot %s %s >>", c.subtype, c.annot)) an_ap(&pb, 5, c.ap, c.content) doc, page := an_open(t, &pb) defer close(doc) if doc == nil { continue } dev: An_Dev an_dev_init(&dev) defer an_dev_destroy(&dev) err := annot_run_page(doc, &page, &dev.dev, MATRIX_IDENTITY) testing.expectf(t, err == .None, "case %d: annot_run_page returned %v", i, err) drew := an_count(&dev, .Fill) > 0 testing.expectf(t, drew == c.draws, "case %d (%s | %s): drew %v, wanted %v", i, c.annot, c.ap, drew, c.draws) } } // An /Annots array far longer than any page has is clamped rather than walked. @(test) test_annot_load_clamps :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) refs := make([dynamic]byte) defer delete(refs) append(&refs, '[') for _ in 0 ..< MAX_ANNOTS + 100 { append(&refs, ..transmute([]byte)string("4 0 R ")) } append(&refs, ']') an_page(&pb, string(refs[:])) pb_obj(&pb, 4, "<< /Type /Annot /Subtype /Link /Rect [0 0 10 10] >>") doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } annots, err := annot_load(doc, &page) defer delete(annots) testing.expect_value(t, err, Error.None) testing.expect_value(t, len(annots), MAX_ANNOTS) } // --------------------------------------------------------------------------- // Corpus // // Every annotation of every page of every real PDF in the directory named by // PDF_ODIN_CORPUS, skipped silently when that is unset. There is no correct // answer to compare a real annotation against, so what is asserted is the // contract: loading terminates and never fails, every rectangle and matrix // that comes out is finite, the device stacks balance, and -- the property the // whole file exists for -- the appearance's own bounding box lands on exactly // the annotation's /Rect. // --------------------------------------------------------------------------- @(private = "file") AC_MAX_FILE :: 64 << 20 @(private = "file") AC_MAX_DEPTH :: 8 @(private = "file") Ac_Dev :: struct { dev: Device, // Live nesting, so an unbalanced pop is caught where it happens. clip, group, layer: int, negative: bool, fills, strokes, images, shades, texts: int, } @(private = "file") ac_self :: proc(dev: ^Device) -> ^Ac_Dev { return (^Ac_Dev)(dev.user) } @(private = "file") ac_fill :: proc(dev: ^Device, p: ^Path, even_odd: bool, ctm: Matrix, paint: Paint) { ac_self(dev).fills += 1 } @(private = "file") ac_stroke :: proc(dev: ^Device, p: ^Path, s: ^Stroke_State, ctm: Matrix, paint: Paint) { ac_self(dev).strokes += 1 } @(private = "file") ac_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix, paint: Paint) { ac_self(dev).texts += len(txt.items) } @(private = "file") ac_ignore_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { ac_self(dev).texts += len(txt.items) } @(private = "file") ac_image :: proc(dev: ^Device, img: ^Image, ctm: Matrix, paint: Paint) { ac_self(dev).images += 1 } @(private = "file") ac_shade :: proc(dev: ^Device, sh: ^Shading, ctm: Matrix, paint: Paint) { ac_self(dev).shades += 1 } @(private = "file") ac_clip :: proc(dev: ^Device, p: ^Path, even_odd: bool, ctm: Matrix) { ac_self(dev).clip += 1 } @(private = "file") ac_clip_stroke :: proc(dev: ^Device, p: ^Path, s: ^Stroke_State, ctm: Matrix) { ac_self(dev).clip += 1 } @(private = "file") ac_clip_text :: proc(dev: ^Device, txt: ^Text, ctm: Matrix) { ac_self(dev).clip += 1 } @(private = "file") ac_clip_image_mask :: proc(dev: ^Device, img: ^Image, ctm: Matrix) { ac_self(dev).clip += 1 } @(private = "file") ac_pop_clip :: proc(dev: ^Device) { d := ac_self(dev) d.clip -= 1 if d.clip < 0 { d.negative = true } } @(private = "file") ac_begin_group :: proc(dev: ^Device, area: Rect, isolated, knockout: bool, blend: Blend_Mode, alpha: f64, soft_mask: ^Soft_Mask) { ac_self(dev).group += 1 } @(private = "file") ac_end_group :: proc(dev: ^Device) { d := ac_self(dev) d.group -= 1 if d.group < 0 { d.negative = true } } @(private = "file") ac_begin_layer :: proc(dev: ^Device, name: string) { ac_self(dev).layer += 1 } @(private = "file") ac_end_layer :: proc(dev: ^Device) { d := ac_self(dev) d.layer -= 1 if d.layer < 0 { d.negative = true } } @(private = "file") ac_init :: proc(d: ^Ac_Dev) { d.dev = Device{ user = d, fill_path = ac_fill, stroke_path = ac_stroke, fill_text = ac_text, stroke_text = proc(dev: ^Device, txt: ^Text, s: ^Stroke_State, ctm: Matrix, paint: Paint) { ac_self(dev).texts += len(txt.items) }, ignore_text = ac_ignore_text, fill_image = ac_image, fill_image_mask = ac_image, fill_shade = ac_shade, clip_path = ac_clip, clip_stroke_path = ac_clip_stroke, clip_text = ac_clip_text, clip_image_mask = ac_clip_image_mask, pop_clip = ac_pop_clip, begin_group = ac_begin_group, end_group = ac_end_group, begin_layer = ac_begin_layer, end_layer = ac_end_layer, } } @(private = "file") ac_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") ac_walk :: proc(dir: string, out: ^[dynamic]string, depth := 0) { if depth > AC_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 { ac_walk(e.fullpath, out, depth + 1) continue } if e.type != .Regular || e.size > AC_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_annot_corpus :: proc(t: ^testing.T) { dir := ac_corpus_dir() defer delete(dir) if dir == "" { log.info("PDF_ODIN_CORPUS is unset; skipping the annotation 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) } ac_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: the way a renderer // is meant to drive this. 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: Ac_Dev files, pages, n_annots, n_ap, n_drawn, n_hidden, n_states, n_oc, n_placed := 0, 0, 0, 0, 0, 0, 0, 0, 0 for path in paths { doc, err := open_file(path) if err != .None || doc == nil { continue } defer close(doc) files += 1 name := filepath.base(path) for i in 0 ..< page_count(doc) { page, perr := page_get(doc, i) if perr != .None { continue } pages += 1 annots, aerr := annot_load(doc, &page, context.allocator) defer delete(annots) testing.expectf(t, aerr == .None, "%s page %d: annot_load returned %v", name, i, aerr) for &a in annots { n_annots += 1 testing.expectf(t, rect_is_finite(a.rect), "%s page %d: /Rect %v is not finite", name, i, a.rect) if a.ap == nil { continue } n_ap += 1 if a.state != "" { n_states += 1 } if dict_get_raw(a.dict, "OC") != nil { n_oc += 1 } if !a.visible { n_hidden += 1 continue } // The placement invariant, in page space: the appearance's own // bounding box, mapped by the matrix the algorithm produced, // must be the annotation's rectangle. Everything in 12.5.5 is // in service of this one equality. form, _, ok := annot_matrix(doc, &a, MATRIX_IDENTITY) if !ok { continue } n_drawn += 1 testing.expectf(t, matrix_is_finite(form), "%s page %d: appearance matrix %v is not finite", name, i, form) bbox, has_bbox := object_to_rect(doc, dict_get(doc, a.ap.dict, "BBox")) if !has_bbox || rect_is_empty(bbox) { continue } got := transform_rect(form, bbox) tol := 1e-6 * max(1, abs(a.rect.x0), abs(a.rect.y0), abs(a.rect.x1), abs(a.rect.y1)) placed := abs(got.x0 - a.rect.x0) <= tol && abs(got.y0 - a.rect.y0) <= tol && abs(got.x1 - a.rect.x1) <= tol && abs(got.y1 - a.rect.y1) <= tol testing.expectf(t, placed, "%s page %d: appearance landed on %v, not on /Rect %v", name, i, got, a.rect) n_placed += 1 } dev: Ac_Dev ac_init(&dev) flip := Matrix{1, 0, 0, -1, -page.media_box.x0, page.media_box.y1} rerr := annot_run_page(doc, &page, &dev.dev, flip, opts) testing.expectf(t, rerr == .None, "%s page %d: annot_run_page 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.group == 0, "%s page %d: %d groups left open", name, i, dev.group) testing.expectf(t, dev.layer == 0, "%s page %d: %d layers left open", name, i, dev.layer) testing.expectf(t, !dev.negative, "%s page %d: a device stack went negative", name, i) total.fills += dev.fills total.strokes += dev.strokes total.texts += dev.texts total.images += dev.images total.shades += dev.shades free_all(virtual.arena_allocator(&arena)) } } log.infof("annotation corpus: %d files, %d pages, %d annotations, %d with an appearance "+ "(%d state-keyed, %d optional-content, %d hidden), %d placed on their /Rect; "+ "drew %d fills, %d strokes, %d glyphs, %d images, %d shadings", files, pages, n_annots, n_ap, n_states, n_oc, n_hidden, n_placed, total.fills, total.strokes, total.texts, total.images, total.shades) testing.expect(t, pages > 0, "no page of the corpus could be read") if n_ap == 0 { // Worth saying out loud rather than passing quietly: a corpus of // exports from word processors is all /Link annotations with no // appearance at all, so this sweep has exercised the loading path and // the "no /AP, draw nothing" path and none of the drawing. The unit // tests above are then the only thing holding the placement algorithm. log.warn("no annotation in this corpus has an appearance stream; " + "the drawing path was not exercised") } else { testing.expect(t, n_drawn > 0, "every appearance in the corpus was refused a placement") testing.expect(t, n_placed > 0, "no appearance in the corpus landed on its /Rect") testing.expect(t, total.fills + total.strokes + total.texts + total.images + total.shades > 0, "appearances were found and none of them painted anything") } } // --------------------------------------------------------------------------- // A page is not one run // --------------------------------------------------------------------------- // Every annotation's appearance stream is its own `run_content`, so anything // bounded per RUN is really bounded per run TIMES MAX_ANNOTS. The soft-mask // budget is the one that costs memory -- a Soft_Mask outlives the operator // that installed it -- so it is counted on the state the whole page shares, // and hitting it here is what proves the annotations are counted with the // page's content rather than each starting fresh. @(test) test_annot_soft_masks_share_the_page_budget :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) N :: MAX_SOFT_MASKS + 64 refs := strings.builder_make() defer strings.builder_destroy(&refs) strings.write_string(&refs, "[") for i in 0 ..< N { fmt.sbprintf(&refs, "%d 0 R ", 8 + i) } strings.write_string(&refs, "]") an_page(&pb, strings.to_string(refs)) // 4: the appearance, which installs one soft mask. 5: the ExtGState that // names it. 6: the mask's group. pb_stream_obj(&pb, 4, "/Type /XObject /Subtype /Form /BBox [0 0 100 50] " + "/Resources << /ExtGState << /S 5 0 R >> >>", transmute([]byte)string("/S gs 0 0 100 50 re f")) pb_obj(&pb, 5, "<< /Type /ExtGState /SMask << /S /Luminosity /G 6 0 R >> >>") pb_stream_obj(&pb, 6, "/Type /XObject /Subtype /Form /BBox [0 0 20 20] " + "/Group << /S /Transparency /CS /DeviceGray >>", transmute([]byte)string("0 0 20 20 re f")) pb_obj(&pb, 7, "<< >>") // Each annotation sits somewhere else, so each install is a mask under a // CTM of its own that the "same ExtGState, same CTM" cache cannot answer. for i in 0 ..< N { body := fmt.tprintf( "<< /Type /Annot /Subtype /Widget /F 4 /Rect [%d 0 %d 50] /AP << /N 4 0 R >> >>", i % 300, i % 300 + 100) pb_obj(&pb, u32(8 + i), body) } doc, page := an_open(t, &pb) defer close(doc) if doc == nil { return } repairs := len(doc.repairs) bmp, err := render_page(&page, Render_Options{}) defer bitmap_destroy(&bmp) testing.expect_value(t, err, Error.None) testing.expect(t, len(doc.repairs) > repairs, "more annotations than MAX_SOFT_MASKS must exhaust the PAGE's budget") // And nothing the render built stays behind: a viewer re-renders the same // page at every zoom step, and every one of those masks used to be a // Document allocation that only `close` reclaimed. before := document_memory_used(doc) bmp2, err2 := render_page(&page, Render_Options{}) defer bitmap_destroy(&bmp2) testing.expect_value(t, err2, Error.None) growth := document_memory_used(doc) - before testing.expectf(t, growth < 1 << 16, "the second render added %d bytes", growth) }