package pdf import "core:testing" // --------------------------------------------------------------------------- // Page tree // // These build a whole file rather than a Document by hand, because the page // tree's job is to survive what real files do to it, and the interesting cases // -- a /Kids cycle, a page reached only by scanning -- are structural. // --------------------------------------------------------------------------- // pt_open opens a builder's bytes with the page tree walked, which is the // thing under test here and so must not be skipped. @(private = "file") pt_open :: proc(pb: ^Pdf_Builder) -> (^Document, Error) { return open_bytes(pb.buf[:]) } // pt_finish closes off a file whose objects are already written. @(private = "file") pt_finish :: proc(pb: ^Pdf_Builder, root := "1 0 R") { at := pb_classic_xref(pb) pb_str(pb, "trailer\n<< /Size ") pb_num(pb, len(pb.offs)) pb_str(pb, " /Root ") pb_str(pb, root) pb_str(pb, " >>\n") pb_startxref(pb, at) } @(private = "file") pt_width :: proc(p: Page) -> f64 { return rect_width(p.media_box) } // --------------------------------------------------------------------------- // Flattening // --------------------------------------------------------------------------- @(test) test_pagetree_single_page :: proc(t: ^testing.T) { pb := pb_sample_classic() defer pb_destroy(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) testing.expect(t, !doc.is_damaged, "a well-formed file recorded a repair") page, perr := page_get(doc, 0) testing.expect(t, perr == .None, "page_get failed") testing.expectf(t, pt_width(page) == 200, "width %v, wanted 200", pt_width(page)) testing.expectf(t, page.ref.num == 3, "page ref %v, wanted 3", page.ref.num) testing.expect(t, page.index == 0, "page index is not 0") testing.expect(t, page.rotate == 0, "rotate is not 0") } @(test) test_pagetree_document_order :: proc(t: ^testing.T) { // Object numbers deliberately run backwards against page order: a walk that // happened to be scanning objects instead of following /Kids would pass // every other assertion in this file but fail this one. pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [5 0 R 4 0 R 3 0 R] /Count 3 >>") pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 10] >>") pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 10] >>") pb_obj(&pb, 5, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 3, "page_count %v, wanted 3", page_count(doc)) for want, i in ([3]f64{100, 200, 300}) { page, _ := page_get(doc, i) testing.expectf(t, pt_width(page) == want, "page %v width %v, wanted %v", i, pt_width(page), want) } } @(test) test_pagetree_nested :: proc(t: ^testing.T) { 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 4 0 R] /Count 4 >>") pb_obj(&pb, 3, "<< /Type /Pages /Parent 2 0 R /Kids [5 0 R 6 0 R] /Count 2 >>") pb_obj(&pb, 4, "<< /Type /Pages /Parent 2 0 R /Kids [7 0 R 8 0 R] /Count 2 >>") pb_obj(&pb, 5, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 10 10] >>") pb_obj(&pb, 6, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 20 10] >>") pb_obj(&pb, 7, "<< /Type /Page /Parent 4 0 R /MediaBox [0 0 30 10] >>") pb_obj(&pb, 8, "<< /Type /Page /Parent 4 0 R /MediaBox [0 0 40 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 4, "page_count %v, wanted 4", page_count(doc)) for want, i in ([4]f64{10, 20, 30, 40}) { page, _ := page_get(doc, i) testing.expectf(t, pt_width(page) == want, "page %v width %v, wanted %v", i, pt_width(page), want) } } // A /Count that disagrees with the number of leaves is a hint, and this // library never trusts it. Both directions are tested because both occur: a // stale count after pages were deleted, and an inflated one after a merge. @(test) test_pagetree_count_is_not_trusted :: proc(t: ^testing.T) { 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 4 0 R] /Count 97 >>") pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>") pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 20 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 2, "page_count %v, wanted 2", page_count(doc)) } // A kid written inline instead of as "N 0 R". Legal, and produced by at least // one generator; the page has no object number of its own. @(test) test_pagetree_direct_kid :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [ << /Type /Page /MediaBox [0 0 55 10] >> ] /Count 1 >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) page, _ := page_get(doc, 0) testing.expectf(t, pt_width(page) == 55, "width %v, wanted 55", pt_width(page)) testing.expect(t, page.ref.num == 0, "a direct kid should have no object number") } // A node with neither /Type nor /Kids, reached through /Kids, is a page by // position. Omitting /Type on a page is common. @(test) test_pagetree_untyped_leaf :: proc(t: ^testing.T) { 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, "<< /Parent 2 0 R /MediaBox [0 0 66 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) page, _ := page_get(doc, 0) testing.expectf(t, pt_width(page) == 66, "width %v, wanted 66", pt_width(page)) } // A leaf that also carries /Kids, left behind by an editing tool. /Type wins: // treating it as an interior node would lose the page. @(test) test_pagetree_page_with_stray_kids :: proc(t: ^testing.T) { 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, "<< /Type /Page /Parent 2 0 R /Kids [] /MediaBox [0 0 77 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) page, _ := page_get(doc, 0) testing.expectf(t, pt_width(page) == 77, "width %v, wanted 77", pt_width(page)) } // The same page object under two parents. Rare, but the file means both, and a // visited set that covered leaves would silently drop the second. @(test) test_pagetree_shared_leaf_appears_twice :: proc(t: ^testing.T) { 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 3 0 R] /Count 2 >>") pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 88 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 2, "page_count %v, wanted 2", page_count(doc)) } // --------------------------------------------------------------------------- // Inherited attributes -- ISO 32000-2, 7.7.3.4 // --------------------------------------------------------------------------- @(test) test_pagetree_inherits_from_root :: proc(t: ^testing.T) { 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 " + "/MediaBox [0 0 111 222] /Rotate 90 /Resources << /Foo 9 0 R >> >>") pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } page, _ := page_get(doc, 0) testing.expectf(t, pt_width(page) == 111, "width %v, wanted 111", pt_width(page)) testing.expectf(t, rect_height(page.media_box) == 222, "height %v, wanted 222", rect_height(page.media_box)) testing.expectf(t, page.rotate == 90, "rotate %v, wanted 90", page.rotate) testing.expect(t, page.resources != nil, "resources were not inherited") } // The nearest ancestor supplying an attribute wins over a more distant one. @(test) test_pagetree_nearest_ancestor_wins :: proc(t: ^testing.T) { 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 2 /MediaBox [0 0 10 10] /Rotate 90 >>") pb_obj(&pb, 3, "<< /Type /Pages /Parent 2 0 R /Kids [4 0 R 5 0 R] /Count 2 " + "/MediaBox [0 0 20 20] >>") // 4 takes the middle node's box; 5 overrides it itself. pb_obj(&pb, 4, "<< /Type /Page /Parent 3 0 R >>") pb_obj(&pb, 5, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 30 30] /Rotate 180 >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } p0, _ := page_get(doc, 0) testing.expectf(t, pt_width(p0) == 20, "page 0 width %v, wanted 20", pt_width(p0)) testing.expectf(t, p0.rotate == 90, "page 0 rotate %v, wanted 90", p0.rotate) p1, _ := page_get(doc, 1) testing.expectf(t, pt_width(p1) == 30, "page 1 width %v, wanted 30", pt_width(p1)) testing.expectf(t, p1.rotate == 180, "page 1 rotate %v, wanted 180", p1.rotate) } // Inheritance must follow the path actually walked, not /Parent. Here /Parent // points at a node that is not the real parent -- the sort of thing an // incremental update leaves behind -- and the walked path is authoritative. @(test) test_pagetree_wrong_parent_does_not_mislead :: proc(t: ^testing.T) { 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, "<< /Type /Pages /Parent 2 0 R /Kids [4 0 R] /Count 1 /MediaBox [0 0 41 41] >>") // /Parent lies: it names 9, a node with a different box, which is not on // the path from the root. pb_obj(&pb, 4, "<< /Type /Page /Parent 9 0 R >>") pb_obj(&pb, 9, "<< /Type /Pages /Kids [] /MediaBox [0 0 99 99] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } page, _ := page_get(doc, 0) testing.expectf(t, pt_width(page) == 41, "width %v, wanted 41 (inheritance followed /Parent instead of the walk)", pt_width(page)) } // --------------------------------------------------------------------------- // Geometry // --------------------------------------------------------------------------- @(test) test_pagetree_missing_mediabox_falls_back :: proc(t: ^testing.T) { 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, "<< /Type /Page /Parent 2 0 R >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } page, _ := page_get(doc, 0) testing.expect(t, page.media_box == DEFAULT_PAGE_RECT, "no /MediaBox anywhere: wanted the default") testing.expect(t, page.crop_box == DEFAULT_PAGE_RECT, "crop box should follow the media box") } // A degenerate box has to be replaced, not passed on: a renderer that divides // by the page height would produce an infinity from it. @(test) test_pagetree_degenerate_mediabox_falls_back :: proc(t: ^testing.T) { 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 4 0 R] /Count 2 >>") pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 0 0] >>") pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [10 10 10 900] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } for i in 0 ..< 2 { page, _ := page_get(doc, i) testing.expectf(t, page.media_box == DEFAULT_PAGE_RECT, "page %v kept a degenerate media box %v", i, page.media_box) } } // A /MediaBox written with its corners the wrong way round is legal and must // come back normalised. @(test) test_pagetree_reversed_mediabox :: proc(t: ^testing.T) { 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, "<< /Type /Page /Parent 2 0 R /MediaBox [612 792 0 0] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } page, _ := page_get(doc, 0) testing.expect(t, page.media_box == Rect{0, 0, 612, 792}, "reversed media box was not normalised") } @(test) test_pagetree_cropbox :: proc(t: ^testing.T) { 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 4 0 R 5 0 R] /Count 3 >>") // Inside the media box: kept as written. pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /CropBox [10 10 90 90] >>") // Overhanging: clipped to the media box. pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /CropBox [50 50 500 500] >>") // Disjoint: unusable, so the media box stands in. pb_obj(&pb, 5, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /CropBox [200 200 300 300] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } p0, _ := page_get(doc, 0) testing.expect(t, p0.crop_box == Rect{10, 10, 90, 90}, "an in-bounds crop box was altered") p1, _ := page_get(doc, 1) testing.expect(t, p1.crop_box == Rect{50, 50, 100, 100}, "an overhanging crop box was not clipped") p2, _ := page_get(doc, 2) testing.expect(t, p2.crop_box == p2.media_box, "a disjoint crop box was not discarded") } @(test) test_pagetree_rotate_normalisation :: proc(t: ^testing.T) { Rotate_Case :: struct { written: string, want: int } cases := [?]Rotate_Case{ {"0", 0}, {"90", 90}, {"180", 180}, {"270", 270}, {"360", 0}, {"450", 90}, // Negative angles are legal and mean anticlockwise. {"-90", 270}, {"-360", 0}, {"-450", 270}, // Not a multiple of 90. The format forbids it and files write it // anyway; rounding down to the quarter turn below is what viewers do. {"45", 0}, {"200", 180}, // An absurd value has to reduce, not overflow: 2^63-1 is 7 mod 360. {"9223372036854775807", 0}, } for c in cases { 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_mark(&pb, 3) pb_str(&pb, "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] /Rotate ") pb_str(&pb, c.written) pb_str(&pb, " >>\nendobj\n") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) if !testing.expectf(t, err == .None, "open failed for /Rotate %v", c.written) { continue } if doc == nil { continue } page, _ := page_get(doc, 0) testing.expectf(t, page.rotate == c.want, "/Rotate %v gave %v, wanted %v", c.written, page.rotate, c.want) } } // --------------------------------------------------------------------------- // Damage // --------------------------------------------------------------------------- // A /Kids array that points back at an ancestor. Without a visited set this // recurses until the stack ends. @(test) test_pagetree_cycle_is_broken :: proc(t: ^testing.T) { 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, "<< /Type /Pages /Parent 2 0 R /Kids [2 0 R 4 0 R] /Count 1 >>") pb_obj(&pb, 4, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 123 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expect(t, doc_has_repair(doc, .Cyclic_Page_Tree), "no cyclic-page-tree repair recorded") // The page below the cycle is still reachable and must still be listed. testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) page, _ := page_get(doc, 0) testing.expectf(t, pt_width(page) == 123, "width %v, wanted 123", pt_width(page)) } // A node that is its own kid. @(test) test_pagetree_self_referential_node :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [2 0 R 3 0 R] /Count 1 >>") pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 21 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) } // Deeper than MAX_DEPTH. The walk stops; it does not overflow the stack. @(test) test_pagetree_depth_limit :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") DEPTH :: MAX_DEPTH + 20 for i in 0 ..< DEPTH { num := u32(2 + i) pb_mark(&pb, num) pb_num(&pb, int(num)) pb_str(&pb, " 0 obj\n<< /Type /Pages /Count 1 /Kids [") pb_num(&pb, int(num) + 1) pb_str(&pb, " 0 R] >>\nendobj\n") } pb_obj(&pb, u32(2 + DEPTH), "<< /Type /Page /MediaBox [0 0 10 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) // The one page in this file sits below the depth limit, so it is not // reachable by the walk. It is still found by the scan fallback, which is // exactly the degradation that is wanted: no crash, no hang, and whatever // pages can still be identified. testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expect(t, doc.is_damaged, "a file this deep should have recorded a repair") } // A catalog whose /Pages is missing entirely. The pages are still objects in // the file and scanning finds them. @(test) test_pagetree_scan_fallback :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog >>") pb_obj(&pb, 2, "<< /Type /Page /MediaBox [0 0 31 10] >>") pb_obj(&pb, 3, "<< /Type /Page /MediaBox [0 0 32 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 2, "page_count %v, wanted 2", page_count(doc)) testing.expect(t, doc_has_repair(doc, .Missing_Page_Node), "no missing-page-node repair recorded") } // Scanned pages have no walked path, so inheritance has to come from /Parent. @(test) test_pagetree_scan_inherits_via_parent :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /MediaBox [0 0 44 44] /Rotate 270 >>") pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) page, _ := page_get(doc, 0) testing.expectf(t, pt_width(page) == 44, "width %v, wanted 44", pt_width(page)) testing.expectf(t, page.rotate == 270, "rotate %v, wanted 270", page.rotate) } // A /Parent chain that loops. The upward walk must stop rather than spin. @(test) test_pagetree_cyclic_parent_chain :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Parent 3 0 R >>") pb_obj(&pb, 3, "<< /Type /Pages /Kids [] /Parent 2 0 R >>") pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 51 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) } // A page whose /Parent is itself. @(test) test_pagetree_self_parent :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog >>") pb_obj(&pb, 2, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 52 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) } // /Kids entries that resolve to nothing, or to the wrong kind of object. @(test) test_pagetree_junk_kids :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [99 0 R (a string) 42 null 3 0 R] /Count 1 >>") pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 61 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expectf(t, page_count(doc) == 1, "page_count %v, wanted 1", page_count(doc)) page, _ := page_get(doc, 0) testing.expectf(t, pt_width(page) == 61, "width %v, wanted 61", pt_width(page)) } // A file with no page objects at all. This is the one case where the page tree // genuinely has nothing to hand back. @(test) test_pagetree_no_pages_is_an_error :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expectf(t, err == .No_Pages, "open gave %v, wanted .No_Pages", err) testing.expect(t, doc == nil, "a failed open must not hand back a document") } // ...unless the caller asked not to walk it, which is what skip_page_tree is // for: inspecting the objects of a document whose page tree is unusable. @(test) test_pagetree_skip_option :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") pt_finish(&pb) doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) defer close(doc) testing.expect(t, err == .None, "skip_page_tree should open a document with no pages") if doc == nil { return } testing.expect(t, page_count(doc) == 0, "skip_page_tree should leave the page list empty") } // --------------------------------------------------------------------------- // Public accessors // --------------------------------------------------------------------------- @(test) test_pagetree_page_get_bounds :: proc(t: ^testing.T) { pb := pb_sample_classic() defer pb_destroy(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } _, e1 := page_get(doc, -1) testing.expectf(t, e1 == .Page_Out_Of_Range, "index -1 gave %v", e1) _, e2 := page_get(doc, 1) testing.expectf(t, e2 == .Page_Out_Of_Range, "index 1 gave %v", e2) _, e3 := page_get(doc, 1 << 40) testing.expectf(t, e3 == .Page_Out_Of_Range, "a huge index gave %v", e3) _, e4 := page_get(nil, 0) testing.expectf(t, e4 == .Invalid_Argument, "a nil document gave %v", e4) testing.expect(t, page_count(nil) == 0, "page_count(nil) is not 0") } @(test) test_pagetree_index_of :: proc(t: ^testing.T) { 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 4 0 R] /Count 2 >>") pb_obj(&pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>") pb_obj(&pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 20 10] >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } testing.expect(t, page_index_of(doc, Ref{3, 0}) == 0, "object 3 is not page 0") testing.expect(t, page_index_of(doc, Ref{4, 0}) == 1, "object 4 is not page 1") testing.expect(t, page_index_of(doc, Ref{9, 0}) == -1, "an unknown ref should be -1") testing.expect(t, page_index_of(doc, Ref{0, 0}) == -1, "the null ref should be -1") testing.expect(t, page_index_of(nil, Ref{3, 0}) == -1, "a nil document should be -1") } // Fetching the same page twice must give equal values and must not accumulate // repairs, which it would if the fallbacks recorded damage on every call. @(test) test_pagetree_page_get_is_repeatable :: proc(t: ^testing.T) { 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, "<< /Type /Page /Parent 2 0 R >>") pt_finish(&pb) doc, err := pt_open(&pb) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } before := len(doc.repairs) a, _ := page_get(doc, 0) b, _ := page_get(doc, 0) testing.expect(t, a == b, "two fetches of the same page differ") testing.expectf(t, len(doc.repairs) == before, "page_get recorded %v repairs", len(doc.repairs) - before) } // --------------------------------------------------------------------------- // Page geometry and content accessors // --------------------------------------------------------------------------- @(private = "file") build_page_doc :: proc(page_body: string, contents: string = "") -> (^Document, Pdf_Builder) { pb := pb_make() 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, page_body) if contents != "" { pb_stream_obj(&pb, 4, "", transmute([]byte)contents) } at := pb_classic_xref(&pb) pb_trailer(&pb, "<< /Size 5 /Root 1 0 R >>", at) doc, _ := open_bytes(pb.buf[:]) return doc, pb } @(test) test_page_size_applies_rotation :: proc(t: ^testing.T) { doc, pb := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] >>") defer pb_destroy(&pb) if !testing.expect(t, doc != nil, "should open") { return } defer close(doc) page, err := page_get(doc, 0) testing.expect_value(t, err, Error.None) testing.expect_value(t, page_bounds(&page), Rect{0, 0, 200, 100}) w, h := page_size(&page, 1) testing.expect_value(t, w, 200) testing.expect_value(t, h, 100) // Scale multiplies both axes. w2, h2 := page_size(&page, 2) testing.expect_value(t, w2, 400) testing.expect_value(t, h2, 200) // A quarter turn swaps them. page.rotate = 90 w3, h3 := page_size(&page, 1) testing.expect_value(t, w3, 100) testing.expect_value(t, h3, 200) page.rotate = 180 w4, h4 := page_size(&page, 1) testing.expect_value(t, w4, 200) testing.expect_value(t, h4, 100) } // A fractional pixel must round outward, or the last sliver of content falls // off the right edge of the bitmap. @(test) test_page_size_rounds_outward_and_clamps :: proc(t: ^testing.T) { doc, pb := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>") defer pb_destroy(&pb) if !testing.expect(t, doc != nil, "should open") { return } defer close(doc) page, _ := page_get(doc, 0) w, h := page_size(&page, 1.004) testing.expect_value(t, w, 101) testing.expect_value(t, h, 101) // A degenerate scale must not produce a zero-sized bitmap. w2, h2 := page_size(&page, 0) testing.expect(t, w2 >= 1 && h2 >= 1, "size is clamped to at least 1x1") w3, h3 := page_size(&page, -5) testing.expect(t, w3 >= 1 && h3 >= 1, "a negative scale is refused, not honoured") testing.expect_value(t, page_bounds(nil), DEFAULT_PAGE_RECT) wn, hn := page_size(nil, 1) testing.expect_value(t, wn, 1) testing.expect_value(t, hn, 1) } @(test) test_page_user_unit :: proc(t: ^testing.T) { doc, pb := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 50] /UserUnit 3 >>") defer pb_destroy(&pb) if !testing.expect(t, doc != nil, "should open") { return } defer close(doc) page, _ := page_get(doc, 0) testing.expect_value(t, page.user_unit, 3.0) w, h := page_size(&page, 1) testing.expect_value(t, w, 300) testing.expect_value(t, h, 150) // Nonsense must fall back to 1 rather than collapsing the page. bad, pb2 := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 50] /UserUnit 0 >>") defer pb_destroy(&pb2) if bad != nil { defer close(bad) p2, _ := page_get(bad, 0) testing.expect_value(t, p2.user_unit, 1.0) } } @(test) test_page_content_single_stream :: proc(t: ^testing.T) { doc, pb := build_page_doc( "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] /Contents 4 0 R >>", "1 0 0 RG 0 0 m 10 10 l S") defer pb_destroy(&pb) if !testing.expect(t, doc != nil, "should open") { return } defer close(doc) page, _ := page_get(doc, 0) data, err := page_content(&page) defer delete(data) testing.expect_value(t, err, Error.None) testing.expect_value(t, string(data), "1 0 0 RG 0 0 m 10 10 l S") } // An operator may be split across the boundary between two content streams // (ISO 32000-2, 7.8.2), so they must be joined with a separator. Bare // concatenation would lex "1 0 0 1 50" + "50 cm" as the number 5050. @(test) test_page_content_array_is_newline_joined :: proc(t: ^testing.T) { 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, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] /Contents [4 0 R 5 0 R] >>") pb_stream_obj(&pb, 4, "", transmute([]byte)string("1 0 0 1 50")) pb_stream_obj(&pb, 5, "", transmute([]byte)string("50 cm")) at := pb_classic_xref(&pb) pb_trailer(&pb, "<< /Size 6 /Root 1 0 R >>", at) doc, _ := open_bytes(pb.buf[:]) if !testing.expect(t, doc != nil, "should open") { return } defer close(doc) page, _ := page_get(doc, 0) data, err := page_content(&page) defer delete(data) testing.expect_value(t, err, Error.None) testing.expect_value(t, string(data), "1 0 0 1 50\n50 cm") } @(test) test_page_content_missing_and_nil :: proc(t: ^testing.T) { doc, pb := build_page_doc("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>") defer pb_destroy(&pb) if !testing.expect(t, doc != nil, "should open") { return } defer close(doc) page, _ := page_get(doc, 0) data, err := page_content(&page) defer delete(data) testing.expect_value(t, err, Error.None) testing.expect_value(t, len(data), 0) _, nerr := page_content(nil) testing.expect_value(t, nerr, Error.Invalid_Argument) }