package pdf import "core:fmt" import "core:strings" import "core:testing" // --------------------------------------------------------------------------- // Outline, destinations and page labels // // These build whole files rather than Documents by hand: an outline is a // linked structure spread over a dozen objects, and the cases worth testing -- // a /Next that loops, a destination naming a deleted page -- only exist once // the references are real. // --------------------------------------------------------------------------- // ol_finish closes off a file whose objects are already written, with object 1 // as the catalog. @(private = "file") ol_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) } // ol_pages writes the page tree every test here shares: three pages, objects // 3, 4 and 5. `catalog_extra` goes into the catalog dictionary. @(private = "file") ol_pages :: proc(pb: ^Pdf_Builder, catalog_extra: string) { 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 4 0 R 5 0 R] /Count 3 >>") pb_obj(pb, 3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>") pb_obj(pb, 4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>") pb_obj(pb, 5, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>") } // ol_titles joins the titles of one level, so a test can state the order it // expects as one string instead of a loop of index assertions. @(private = "file") ol_titles :: proc(items: []Outline_Item, allocator := context.allocator) -> string { b := strings.builder_make(0, 32, allocator) for it, i in items { if i > 0 { strings.write_byte(&b, ',') } strings.write_string(&b, it.title) } return strings.to_string(b) } @(private = "file") ol_expect_titles :: proc(t: ^testing.T, items: []Outline_Item, want: string, where_: string) { got := ol_titles(items) defer delete(got) testing.expectf(t, got == want, "%s: titles %q, wanted %q", where_, got, want) } // --------------------------------------------------------------------------- // The tree // --------------------------------------------------------------------------- @(test) test_outline_three_levels :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Outlines 10 0 R") pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R /Last 12 0 R /Count 2 >>") pb_obj(&pb, 11, "<< /Title (One) /Parent 10 0 R /Next 12 0 R /Count 1 " + "/First 13 0 R /Last 13 0 R /Dest [3 0 R /Fit] >>") pb_obj(&pb, 12, "<< /Title (Two) /Parent 10 0 R /Prev 11 0 R /Count -1 " + "/First 16 0 R /Last 16 0 R /Dest [4 0 R /XYZ 10 20 0] >>") pb_obj(&pb, 13, "<< /Title (One.A) /Parent 11 0 R /Next 14 0 R /Dest [5 0 R /FitH 700] >>") pb_obj(&pb, 14, "<< /Title (One.B) /Parent 11 0 R /Prev 13 0 R /Count 1 /First 15 0 R /Last 15 0 R >>") pb_obj(&pb, 15, "<< /Title (One.B.i) /Parent 14 0 R >>") pb_obj(&pb, 16, "<< /Title (Two.A) /Parent 12 0 R >>") ol_finish(&pb) doc, err := open_bytes(pb.buf[:]) defer close(doc) testing.expect(t, err == .None, "open failed") if doc == nil { return } items, oerr := outline_load(doc) defer outline_destroy(items) testing.expect(t, oerr == .None, "outline_load failed") ol_expect_titles(t, items, "One,Two", "top level") if len(items) != 2 { return } // /Count 1 is open, /Count -1 is the same subtree collapsed. testing.expect(t, items[0].open, "One should be open") testing.expect(t, !items[1].open, "Two should be closed") // The linked chain 13 -> 14 is two children even though /Last names only // the first: a walk that trusted /Last would stop one short. ol_expect_titles(t, items[0].children, "One.A,One.B", "One's children") ol_expect_titles(t, items[1].children, "Two.A", "Two's children") if len(items[0].children) != 2 { return } ol_expect_titles(t, items[0].children[1].children, "One.B.i", "level three") testing.expectf(t, items[0].page == 0, "One page %v, wanted 0", items[0].page) testing.expectf(t, items[1].page == 1, "Two page %v, wanted 1", items[1].page) testing.expectf(t, items[0].children[0].page == 2, "One.A page %v, wanted 2", items[0].children[0].page) testing.expect(t, items[0].dest.kind == .Fit, "One's destination is not /Fit") testing.expect(t, items[0].children[0].dest.kind == .Fit_H, "One.A's destination is not /FitH") testing.expectf(t, items[0].children[0].dest.args[0] == 700, "FitH top %v, wanted 700", items[0].children[0].dest.args[0]) // An item with no /Dest and no /A has no page. testing.expectf(t, items[0].children[1].page == -1, "One.B page %v, wanted -1", items[0].children[1].page) } @(test) test_outline_next_cycle_terminates :: proc(t: ^testing.T) { // 11 -> 12 -> 11. A walk without a visited set never returns from this. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Outlines 10 0 R") pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R /Last 12 0 R >>") pb_obj(&pb, 11, "<< /Title (A) /Next 12 0 R >>") pb_obj(&pb, 12, "<< /Title (B) /Next 11 0 R >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } items, oerr := outline_load(doc) defer outline_destroy(items) testing.expect(t, oerr == .None, "outline_load failed") ol_expect_titles(t, items, "A,B", "cycle") testing.expect(t, doc.is_damaged, "a cyclic /Next recorded no repair") } @(test) test_outline_first_cycle_terminates :: proc(t: ^testing.T) { // /First pointing back at an ancestor: the same trap one level down. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Outlines 10 0 R") pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") pb_obj(&pb, 11, "<< /Title (A) /First 12 0 R >>") pb_obj(&pb, 12, "<< /Title (B) /First 11 0 R >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } items, _ := outline_load(doc) defer outline_destroy(items) ol_expect_titles(t, items, "A", "root") if len(items) != 1 { return } ol_expect_titles(t, items[0].children, "B", "A's children") if len(items[0].children) != 1 { return } testing.expectf(t, len(items[0].children[0].children) == 0, "B has %v children, wanted 0", len(items[0].children[0].children)) } @(test) test_outline_self_next :: proc(t: ^testing.T) { // The tightest loop there is: an item whose /Next is itself. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Outlines 10 0 R") pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") pb_obj(&pb, 11, "<< /Title (A) /Next 11 0 R >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } items, _ := outline_load(doc) defer outline_destroy(items) ol_expect_titles(t, items, "A", "self-linked item") } @(test) test_outline_first_not_a_dict :: proc(t: ^testing.T) { // /First as a number, as a reference to a number, and as a reference to // nothing at all. None of the three is an outline item. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Outlines 10 0 R") pb_obj(&pb, 10, "<< /Type /Outlines /First 42 /Last 42 /Count 3 >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } items, oerr := outline_load(doc) defer outline_destroy(items) testing.expect(t, oerr == .None, "a broken /First is not an error") testing.expectf(t, len(items) == 0, "got %v items from a numeric /First", len(items)) pb2 := pb_make() defer pb_destroy(&pb2) ol_pages(&pb2, "/Outlines 10 0 R") pb_obj(&pb2, 10, "<< /Type /Outlines /First 11 0 R >>") pb_obj(&pb2, 11, "<< /Title (A) /First 12 0 R /Next 13 0 R >>") pb_obj(&pb2, 12, "7") pb_obj(&pb2, 13, "<< /Title (B) /First 99 0 R >>") ol_finish(&pb2) doc2, _ := open_bytes(pb2.buf[:]) defer close(doc2) if doc2 == nil { return } items2, _ := outline_load(doc2) defer outline_destroy(items2) ol_expect_titles(t, items2, "A,B", "siblings past a broken /First") if len(items2) != 2 { return } testing.expectf(t, len(items2[0].children) == 0, "A has %v children from an integer /First", len(items2[0].children)) testing.expectf(t, len(items2[1].children) == 0, "B has %v children from a dangling /First", len(items2[1].children)) } @(test) test_outline_missing_and_broken_roots :: proc(t: ^testing.T) { // No /Outlines at all: an empty outline, and not an error. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } items, err := outline_load(doc) defer outline_destroy(items) testing.expect(t, err == .None, "a document with no outline reported an error") testing.expectf(t, len(items) == 0, "got %v items from no outline", len(items)) // /Outlines pointing at an array. pb2 := pb_make() defer pb_destroy(&pb2) ol_pages(&pb2, "/Outlines 10 0 R") pb_obj(&pb2, 10, "[1 2 3]") ol_finish(&pb2) doc2, _ := open_bytes(pb2.buf[:]) defer close(doc2) if doc2 == nil { return } items2, err2 := outline_load(doc2) defer outline_destroy(items2) testing.expect(t, err2 == .None, "a non-dictionary /Outlines is not an error") testing.expectf(t, len(items2) == 0, "got %v items from an array /Outlines", len(items2)) nil_items, nil_err := outline_load(nil) testing.expect(t, nil_err == .Invalid_Argument, "outline_load(nil) should be a caller error") testing.expect(t, nil_items == nil, "outline_load(nil) returned items") outline_destroy(nil) } @(test) test_outline_depth_capped :: proc(t: ^testing.T) { // A chain of 200 nested /First links. The walk must stop at // MAX_OUTLINE_DEPTH rather than recursing to the bottom. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Outlines 10 0 R") pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") for i in 0 ..< 200 { num := u32(11 + i) if i == 199 { pb_obj(&pb, num, fmt.tprintf("<< /Title (L%d) >>", i)) } else { pb_obj(&pb, num, fmt.tprintf("<< /Title (L%d) /First %d 0 R >>", i, num + 1)) } } ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } items, _ := outline_load(doc) defer outline_destroy(items) depth := 0 level := items for len(level) > 0 && depth < 1000 { depth += 1 level = level[0].children } testing.expectf(t, depth == MAX_OUTLINE_DEPTH, "walked %v levels, wanted the cap of %v", depth, MAX_OUTLINE_DEPTH) testing.expectf(t, depth < 200, "the depth cap did not apply: %v levels", depth) } @(test) test_outline_titles_decoded :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Outlines 10 0 R") pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") // UTF-16BE with a byte order mark: "Hi". pb_obj(&pb, 11, "<< /Title /Next 12 0 R >>") // 0x92 in PDFDocEncoding is a trademark sign, not the Latin-1 character. pb_obj(&pb, 12, "<< /Title (a\\222b) /Next 13 0 R >>") // No /Title at all, and a /Title that is not a string. pb_obj(&pb, 13, "<< /Next 14 0 R >>") pb_obj(&pb, 14, "<< /Title /NotAString >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } items, _ := outline_load(doc) defer outline_destroy(items) testing.expectf(t, len(items) == 4, "got %v items, wanted 4", len(items)) if len(items) != 4 { return } testing.expectf(t, items[0].title == "Hi", "UTF-16 title %q", items[0].title) testing.expectf(t, items[1].title == "a™b", "PDFDoc title %q", items[1].title) testing.expectf(t, items[2].title == "", "missing title %q", items[2].title) testing.expectf(t, items[3].title == "", "non-string title %q", items[3].title) } @(test) test_outline_action_destinations :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Outlines 10 0 R") pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") // A /GoTo action rather than a /Dest. pb_obj(&pb, 11, "<< /Title (A) /Next 12 0 R /A << /S /GoTo /D [4 0 R /Fit] >> >>") // A /URI action goes nowhere in this document. pb_obj(&pb, 12, "<< /Title (B) /Next 13 0 R /A << /S /URI /URI (http://example.com) >> >>") // Both: /Dest wins (ISO 32000-2, 12.3.3). pb_obj(&pb, 13, "<< /Title (C) /Next 14 0 R /Dest [5 0 R /Fit] /A << /S /GoTo /D [3 0 R /Fit] >> >>") // A /GoToR destination is a page of another file. Taking its /D as a page // of this one would send the reader somewhere arbitrary. pb_obj(&pb, 14, "<< /Title (D) /A << /S /GoToR /F (other.pdf) /D [1 /Fit] >> >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } items, _ := outline_load(doc) defer outline_destroy(items) testing.expectf(t, len(items) == 4, "got %v items, wanted 4", len(items)) if len(items) != 4 { return } testing.expectf(t, items[0].page == 1, "/GoTo action page %v, wanted 1", items[0].page) testing.expectf(t, items[1].page == -1, "/URI action page %v, wanted -1", items[1].page) testing.expectf(t, items[2].page == 2, "/Dest should win, page %v, wanted 2", items[2].page) testing.expectf(t, items[3].page == -1, "/GoToR action page %v, wanted -1", items[3].page) } // --------------------------------------------------------------------------- // Destinations // --------------------------------------------------------------------------- // ol_dest_doc builds a file whose object 30 is a dictionary of destinations to // be resolved by key, so one document serves a table of cases. @(private = "file") ol_dest_doc :: proc(pb: ^Pdf_Builder, catalog_extra, dests_body: string) -> (^Document, ^Dict) { ol_pages(pb, catalog_extra) pb_obj(pb, 30, dests_body) ol_finish(pb) doc, _ := open_bytes(pb.buf[:]) if doc == nil { return nil, nil } return doc, get_dict(doc, Ref{30, 0}) } @(test) test_dest_explicit_array :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) doc, cases := ol_dest_doc(&pb, "", `<< /a [3 0 R /XYZ 10 20 1.5] /b [4 0 R /Fit] /c [5 0 R /FitR 1 2 3 4] /d [4 0 R /FitBV 90] /e [5 0 R] /f [4 0 R /Bogus 1 2] /g [2 0 R /Fit] /h [] >>`) defer close(doc) if doc == nil || cases == nil { return } d, ok := dest_resolve(doc, dict_get(doc, cases, "a")) testing.expect(t, ok, "/XYZ did not resolve") testing.expectf(t, d.page == 0 && d.kind == .XYZ, "XYZ page %v kind %v", d.page, d.kind) testing.expectf(t, d.args == [4]f64{10, 20, 1.5, 0}, "XYZ args %v", d.args) testing.expectf(t, d.has == [4]bool{true, true, true, false}, "XYZ has %v", d.has) d, ok = dest_resolve(doc, dict_get(doc, cases, "b")) testing.expect(t, ok && d.page == 1 && d.kind == .Fit, "/Fit did not resolve") testing.expectf(t, d.has == [4]bool{}, "/Fit takes no arguments, has %v", d.has) d, _ = dest_resolve(doc, dict_get(doc, cases, "c")) testing.expectf(t, d.kind == .Fit_R && d.args == [4]f64{1, 2, 3, 4}, "/FitR kind %v args %v", d.kind, d.args) testing.expectf(t, d.has == [4]bool{true, true, true, true}, "/FitR has %v", d.has) d, _ = dest_resolve(doc, dict_get(doc, cases, "d")) testing.expectf(t, d.kind == .Fit_BV && d.args[0] == 90 && d.has[0] && !d.has[1], "/FitBV kind %v args %v has %v", d.kind, d.args, d.has) // A page and no view name: the page, with the view left alone. d, ok = dest_resolve(doc, dict_get(doc, cases, "e")) testing.expect(t, ok, "a one-element destination did not resolve") testing.expectf(t, d.page == 2 && d.kind == .XYZ && d.has == [4]bool{}, "bare page destination %v", d) // An unknown view name is not a reason to lose the page. d, ok = dest_resolve(doc, dict_get(doc, cases, "f")) testing.expect(t, ok, "an unknown view name failed the whole destination") testing.expectf(t, d.page == 1 && d.has == [4]bool{}, "unknown view %v", d) // A reference to something that is not a page. d, _ = dest_resolve(doc, dict_get(doc, cases, "g")) testing.expectf(t, d.page == -1, "the page tree root resolved to page %v", d.page) _, ok = dest_resolve(doc, dict_get(doc, cases, "h")) testing.expect(t, !ok, "an empty destination array resolved") _, ok = dest_resolve(doc, dict_get(doc, cases, "nope")) testing.expect(t, !ok, "a missing destination resolved") _, ok = dest_resolve(nil, nil) testing.expect(t, !ok, "dest_resolve(nil) resolved") } @(test) test_dest_xyz_nulls :: proc(t: ^testing.T) { // /XYZ null 700 null: keep the current left and zoom, scroll to y=700. // The distinction is not one a value can carry -- 0 is a legal coordinate // and a legal zoom means "unchanged" as well -- so `has` is the only place // it can live. pb := pb_make() defer pb_destroy(&pb) doc, cases := ol_dest_doc(&pb, "", `<< /a [3 0 R /XYZ null 700 null] /b [3 0 R /XYZ 0 0 0] /c [3 0 R /XYZ] /d [3 0 R /XYZ 5] /e [3 0 R /XYZ 0 0 1.5] /f [3 0 R /FitR 0 0 0 0] >>`) defer close(doc) if doc == nil || cases == nil { return } d, _ := dest_resolve(doc, dict_get(doc, cases, "a")) testing.expectf(t, d.has == [4]bool{false, true, false, false}, "null XYZ has %v", d.has) testing.expectf(t, d.args[1] == 700, "null XYZ top %v, wanted 700", d.args[1]) testing.expectf(t, d.args[0] == 0 && d.args[2] == 0, "an absent argument left a value behind: %v", d.args) // Zero left and zero top are coordinates; a zero zoom is the format's own // spelling of null (ISO 32000-2, 12.3.2.2), and every /XYZ in the PDF // specification's own bookmarks is written that way. d, _ = dest_resolve(doc, dict_get(doc, cases, "b")) testing.expectf(t, d.has == [4]bool{true, true, false, false}, "zero zoom must read as unchanged, has %v", d.has) d, _ = dest_resolve(doc, dict_get(doc, cases, "e")) testing.expectf(t, d.has == [4]bool{true, true, true, false} && d.args[2] == 1.5, "a real zoom was lost: args %v has %v", d.args, d.has) // The rule is /XYZ's alone: /FitR's numbers are coordinates and a zero // among them is a real one. d, _ = dest_resolve(doc, dict_get(doc, cases, "f")) testing.expectf(t, d.has == [4]bool{true, true, true, true} && d.args == [4]f64{0, 0, 0, 0}, "/FitR zeros %v has %v", d.args, d.has) d, _ = dest_resolve(doc, dict_get(doc, cases, "c")) testing.expectf(t, d.has == [4]bool{}, "/XYZ with no arguments has %v", d.has) d, _ = dest_resolve(doc, dict_get(doc, cases, "d")) testing.expectf(t, d.has == [4]bool{true, false, false, false}, "/XYZ truncated after one argument has %v", d.has) } @(test) test_dest_missing_page :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) doc, cases := ol_dest_doc(&pb, "", `<< /gone [99 0 R /XYZ 1 2 3] /free [7 0 R /Fit] /nth 3 >>`) defer close(doc) if doc == nil || cases == nil { return } d, ok := dest_resolve(doc, dict_get(doc, cases, "gone")) testing.expect(t, ok, "a destination to a deleted page should still resolve") testing.expectf(t, d.page == -1, "deleted page resolved to %v, wanted -1", d.page) // The view survives, so a viewer can still say where it would have gone. testing.expectf(t, d.kind == .XYZ && d.args[0] == 1, "view lost with the page: %v", d) d, _ = dest_resolve(doc, dict_get(doc, cases, "free")) testing.expectf(t, d.page == -1, "a free object resolved to page %v", d.page) // A destination is not a bare number, whatever it names. _, ok = dest_resolve(doc, dict_get(doc, cases, "nth")) testing.expect(t, !ok, "a number resolved as a destination") } @(test) test_dest_page_by_number :: proc(t: ^testing.T) { // The remote form, [pageIndex /Fit], which producers use locally too. pb := pb_make() defer pb_destroy(&pb) doc, cases := ol_dest_doc(&pb, "", `<< /a [2 /Fit] /b [0 /FitH 100] /c [9 /Fit] /d [-1 /Fit] >>`) defer close(doc) if doc == nil || cases == nil { return } d, _ := dest_resolve(doc, dict_get(doc, cases, "a")) testing.expectf(t, d.page == 2, "index 2 gave page %v", d.page) d, _ = dest_resolve(doc, dict_get(doc, cases, "b")) testing.expectf(t, d.page == 0 && d.kind == .Fit_H, "index 0 gave %v", d) d, _ = dest_resolve(doc, dict_get(doc, cases, "c")) testing.expectf(t, d.page == -1, "index 9 of a 3-page document gave %v", d.page) d, _ = dest_resolve(doc, dict_get(doc, cases, "d")) testing.expectf(t, d.page == -1, "index -1 gave %v", d.page) } @(test) test_dest_named_through_dests_dict :: proc(t: ^testing.T) { // PDF 1.1: the catalog's /Dests dictionary. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Dests 20 0 R") pb_obj(&pb, 20, "<< /chap1 [4 0 R /FitH 500] /chap2 21 0 R /chap3 << /D [5 0 R /Fit] >> >>") pb_obj(&pb, 21, "[3 0 R /XYZ null null 2]") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } d, ok := dest_resolve(doc, Name("chap1")) testing.expect(t, ok, "/chap1 did not resolve") testing.expectf(t, d.page == 1 && d.kind == .Fit_H && d.args[0] == 500, "chap1 %v", d) // The same name written as a string, which files do. d, ok = dest_resolve(doc, String(transmute([]byte)string("chap1"))) testing.expectf(t, ok && d.page == 1, "chap1 as a string gave %v", d) // Through an indirect reference, and through a << /D ... >> wrapper. d, ok = dest_resolve(doc, Name("chap2")) testing.expectf(t, ok && d.page == 0 && d.has == [4]bool{false, false, true, false}, "chap2 %v", d) d, ok = dest_resolve(doc, Name("chap3")) testing.expectf(t, ok && d.page == 2, "chap3 %v", d) _, ok = dest_resolve(doc, Name("chap9")) testing.expect(t, !ok, "an undefined name resolved") } @(test) test_dest_named_through_name_tree :: proc(t: ^testing.T) { // PDF 1.2 and later: the /Names /Dests name tree, with a /Kids level so // the lookup has to descend rather than read one /Names array. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Names 20 0 R") pb_obj(&pb, 20, "<< /Dests 21 0 R >>") pb_obj(&pb, 21, "<< /Kids [22 0 R 23 0 R] >>") pb_obj(&pb, 22, "<< /Limits [(alpha) (beta)] /Names [(alpha) [3 0 R /Fit] (beta) 24 0 R] >>") pb_obj(&pb, 23, "<< /Limits [(gamma) (gamma)] /Names [(gamma) [5 0 R /FitV 12]] >>") pb_obj(&pb, 24, "<< /D [4 0 R /XYZ 1 2 null] >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } d, ok := dest_resolve(doc, String(transmute([]byte)string("alpha"))) testing.expectf(t, ok && d.page == 0 && d.kind == .Fit, "alpha %v", d) // A tree value that is a dictionary with /D, which is the form the format // actually recommends for a named destination. d, ok = dest_resolve(doc, String(transmute([]byte)string("beta"))) testing.expectf(t, ok && d.page == 1 && d.has == [4]bool{true, true, false, false}, "beta %v", d) // In the second kid, so /Limits had to be read to get there. d, ok = dest_resolve(doc, Name("gamma")) testing.expectf(t, ok && d.page == 2 && d.kind == .Fit_V && d.args[0] == 12, "gamma %v", d) _, ok = dest_resolve(doc, String(transmute([]byte)string("delta"))) testing.expect(t, !ok, "a name not in the tree resolved") } @(test) test_dest_name_cycle_terminates :: proc(t: ^testing.T) { // A name whose destination is the same name, and a /D chain that never // reaches an array. Both must stop. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Dests 20 0 R") pb_obj(&pb, 20, "<< /loop /loop /deep 21 0 R >>") pb_obj(&pb, 21, "<< /D << /D << /D << /D << /D [3 0 R /Fit] >> >> >> >> >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } _, ok := dest_resolve(doc, Name("loop")) testing.expect(t, !ok, "a self-naming destination resolved") _, ok = dest_resolve(doc, Name("deep")) testing.expect(t, !ok, "a destination past the hop limit resolved") } @(test) test_outline_dest_named :: proc(t: ^testing.T) { // The whole path: a bookmark whose /Dest is a name in the name tree. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/Outlines 10 0 R /Names 20 0 R") pb_obj(&pb, 10, "<< /Type /Outlines /First 11 0 R >>") pb_obj(&pb, 11, "<< /Title (Chapter) /Dest (ch1) /Next 12 0 R >>") pb_obj(&pb, 12, "<< /Title (Appendix) /A << /S /GoTo /D /ch2 >> >>") pb_obj(&pb, 20, "<< /Dests 21 0 R >>") pb_obj(&pb, 21, "<< /Names [(ch1) [4 0 R /Fit] (ch2) [5 0 R /Fit]] >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } items, _ := outline_load(doc) defer outline_destroy(items) testing.expectf(t, len(items) == 2, "got %v items, wanted 2", len(items)) if len(items) != 2 { return } testing.expectf(t, items[0].page == 1, "named /Dest gave page %v, wanted 1", items[0].page) testing.expectf(t, items[1].page == 2, "named action gave page %v, wanted 2", items[1].page) testing.expect(t, items[0].dest.page == items[0].page, "item.page disagrees with dest.page") } // --------------------------------------------------------------------------- // Page labels // --------------------------------------------------------------------------- @(private = "file") ol_expect_label :: proc(t: ^testing.T, doc: ^Document, index: int, want: string) { got := page_label(doc, index) defer delete(got) testing.expectf(t, got == want, "page_label(%v) = %q, wanted %q", index, got, want) } @(test) test_page_label_styles :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/PageLabels 20 0 R") pb_obj(&pb, 20, `<< /Nums [ 0 << /S /r >> 4 << /S /D >> 8 << /S /a >> 12 << /S /A >> 16 << /S /R >> ] >>`) ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } ol_expect_label(t, doc, 0, "i") ol_expect_label(t, doc, 3, "iv") ol_expect_label(t, doc, 4, "1") ol_expect_label(t, doc, 7, "4") ol_expect_label(t, doc, 8, "a") ol_expect_label(t, doc, 11, "d") ol_expect_label(t, doc, 12, "A") ol_expect_label(t, doc, 15, "D") ol_expect_label(t, doc, 16, "I") // Past the last range's start: the last range runs to the end of the file. ol_expect_label(t, doc, 20, "V") ol_expect_label(t, doc, 25, "X") // Before any page: never a label. ol_expect_label(t, doc, -1, "") } @(test) test_page_label_prefix_and_start :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/PageLabels 20 0 R") pb_obj(&pb, 20, `<< /Nums [ 0 << /P (Cover) >> 1 << /S /D /P (A-) /St 7 >> 4 << /S /r /St 3 >> 6 << /S /D /St 0 >> 8 << /P /S /D >> ] >>`) ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } // A range with a prefix and no /S labels every page in it the same way. ol_expect_label(t, doc, 0, "Cover") // /St shifts the numbering; the prefix goes in front of every one. ol_expect_label(t, doc, 1, "A-7") ol_expect_label(t, doc, 3, "A-9") ol_expect_label(t, doc, 4, "iii") ol_expect_label(t, doc, 5, "iv") // /St 0 is out of range and clamps to 1 rather than labelling a page "0". ol_expect_label(t, doc, 6, "1") ol_expect_label(t, doc, 7, "2") // A UTF-16 prefix is a text string like any other. ol_expect_label(t, doc, 8, "§1") } @(test) test_page_label_no_tree :: proc(t: ^testing.T) { // With no /PageLabels the label is the page's own position, 1-based. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } ol_expect_label(t, doc, 0, "1") ol_expect_label(t, doc, 41, "42") ol_expect_label(t, doc, -3, "") ol_expect_label(t, nil, 0, "") } @(test) test_page_label_broken_tree :: proc(t: ^testing.T) { pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/PageLabels 20 0 R") // Entries out of order, a key with a non-dictionary value, a negative key, // an unknown style, and a range that starts after the pages under test. pb_obj(&pb, 20, `<< /Nums [ 4 << /S /D /St 100 >> 0 << /S /D >> 2 (not a dictionary) -1 << /S /A >> 3 << /S /Q /P (x) >> 20 << /S /D /St 1 >> ] >>`) ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } // Order in the file is not order in the tree: page 1 belongs to the range // keyed 0 even though it is written second. ol_expect_label(t, doc, 0, "1") ol_expect_label(t, doc, 1, "2") // The entry at key 2 is unusable, so page 2 falls back to the range at 0. ol_expect_label(t, doc, 2, "3") // An unknown /S numbers nothing; the prefix is still the label. ol_expect_label(t, doc, 3, "x") ol_expect_label(t, doc, 4, "100") ol_expect_label(t, doc, 5, "101") ol_expect_label(t, doc, 21, "2") // A /PageLabels that is not a tree at all. pb2 := pb_make() defer pb_destroy(&pb2) ol_pages(&pb2, "/PageLabels 20 0 R") pb_obj(&pb2, 20, "42") ol_finish(&pb2) doc2, _ := open_bytes(pb2.buf[:]) defer close(doc2) if doc2 == nil { return } ol_expect_label(t, doc2, 6, "7") } @(test) test_page_label_kids_tree :: proc(t: ^testing.T) { // The /Kids form of a number tree, which is what a document with many // ranges is written as. pb := pb_make() defer pb_destroy(&pb) ol_pages(&pb, "/PageLabels 20 0 R") pb_obj(&pb, 20, "<< /Kids [21 0 R 22 0 R] >>") pb_obj(&pb, 21, "<< /Limits [0 0] /Nums [0 << /S /r >>] >>") pb_obj(&pb, 22, "<< /Limits [5 5] /Nums [5 << /S /D /P (p) /St 2 >>] >>") ol_finish(&pb) doc, _ := open_bytes(pb.buf[:]) defer close(doc) if doc == nil { return } ol_expect_label(t, doc, 2, "iii") ol_expect_label(t, doc, 5, "p2") ol_expect_label(t, doc, 6, "p3") } @(test) test_page_label_numerals :: proc(t: ^testing.T) { // The numeral writers, driven directly: a range per case would be a very // long file, and these are pure functions of one number. check_roman :: proc(t: ^testing.T, n: i64, upper: bool, want: string) { b := strings.builder_make(0, 16) defer strings.builder_destroy(&b) label_write_roman(&b, n, upper) got := strings.to_string(b) testing.expectf(t, got == want, "roman(%v, upper=%v) = %q, wanted %q", n, upper, got, want) } check_alpha :: proc(t: ^testing.T, n: i64, upper: bool, want: string) { b := strings.builder_make(0, 16) defer strings.builder_destroy(&b) label_write_alpha(&b, n, upper) got := strings.to_string(b) testing.expectf(t, got == want, "alpha(%v, upper=%v) = %q, wanted %q", n, upper, got, want) } check_roman(t, 1, false, "i") check_roman(t, 4, false, "iv") check_roman(t, 9, false, "ix") check_roman(t, 14, false, "xiv") check_roman(t, 40, false, "xl") check_roman(t, 90, false, "xc") check_roman(t, 400, false, "cd") check_roman(t, 1990, false, "mcmxc") check_roman(t, 2024, true, "MMXXIV") check_roman(t, 3999, true, "MMMCMXCIX") check_roman(t, 0, false, "") check_roman(t, -5, false, "") // Beyond the cap the numeral would be thousands of Ms, so it is decimal. check_roman(t, 100000, false, "100000") check_alpha(t, 1, false, "a") check_alpha(t, 26, false, "z") check_alpha(t, 27, false, "aa") check_alpha(t, 52, false, "zz") check_alpha(t, 53, false, "aaa") check_alpha(t, 2, true, "B") check_alpha(t, 0, false, "") // The repetition count is capped: a file asking for page 10^9 must not ask // for a 38-megabyte label. long := strings.builder_make(0, 128) defer strings.builder_destroy(&long) label_write_alpha(&long, 1_000_000_000, false) testing.expectf(t, len(strings.to_string(long)) == MAX_LABEL_LETTERS, "a huge alphabetic label was %v bytes", len(strings.to_string(long))) }