initial commit
This commit is contained in:
@@ -0,0 +1,862 @@
|
||||
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 <FEFF00480069> /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 <FEFF00A7> /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)))
|
||||
}
|
||||
Reference in New Issue
Block a user