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