initial commit
This commit is contained in:
@@ -0,0 +1,800 @@
|
||||
package pdf
|
||||
|
||||
import "core:math"
|
||||
import "core:mem"
|
||||
import "core:testing"
|
||||
|
||||
// Pattern tests.
|
||||
//
|
||||
// The fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_close` -- live in
|
||||
// function_test.odin. That document is opened by scanning (it has no xref), so
|
||||
// it already carries repairs; every assertion about a repair here counts the
|
||||
// growth of `doc.repairs` rather than its length.
|
||||
//
|
||||
// Almost everything below is about one question: where does a cell land? So
|
||||
// the assertions are about device-space positions of returned matrices, not
|
||||
// about pixels. `pt_origin` is the whole vocabulary -- a tile matrix maps the
|
||||
// cell's own space to device space, so the image of (0,0) is where that cell's
|
||||
// bbox origin sits on the page.
|
||||
|
||||
@(private = "file")
|
||||
PT_EPS :: 1e-9
|
||||
|
||||
// A 10x10 cell that abuts its neighbours, offset in pattern space so that a
|
||||
// composition done in the wrong order lands somewhere else.
|
||||
@(private = "file")
|
||||
TILE_SRC :: "<< /PatternType 1 /PaintType 1 /TilingType 1 " +
|
||||
"/BBox [0 0 10 10] /XStep 10 /YStep 10 /Matrix [1 0 0 1 5 7] >>"
|
||||
|
||||
// The same cell anchored at the origin, for counting.
|
||||
@(private = "file")
|
||||
PLAIN_SRC :: "<< /PatternType 1 /PaintType 1 /TilingType 1 " +
|
||||
"/BBox [0 0 10 10] /XStep 10 /YStep 10 >>"
|
||||
|
||||
@(private = "file")
|
||||
AXIAL :: "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 10 0] " +
|
||||
"/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " +
|
||||
"/Extend [true true] >>"
|
||||
|
||||
@(private = "file")
|
||||
pt_tiling :: proc(t: ^testing.T, doc: ^Document, src: string, res: ^Dict = nil,
|
||||
loc := #caller_location) -> ^Pattern {
|
||||
p, err := pattern_load(doc, cf_stream(doc, src, nil), res)
|
||||
testing.expect_value(t, err, Error.None, loc = loc)
|
||||
testing.expect(t, p != nil, "pattern did not load", loc = loc)
|
||||
return p
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
pt_dict_pattern :: proc(t: ^testing.T, doc: ^Document, src: string, res: ^Dict = nil,
|
||||
loc := #caller_location) -> ^Pattern {
|
||||
p, err := pattern_load(doc, cf_parse(doc, src), res)
|
||||
testing.expect_value(t, err, Error.None, loc = loc)
|
||||
testing.expect(t, p != nil, "pattern did not load", loc = loc)
|
||||
return p
|
||||
}
|
||||
|
||||
// pt_origin is where cell-space (0,0) -- the corner of /BBox -- lands.
|
||||
@(private = "file")
|
||||
pt_origin :: proc(m: Matrix) -> Point {
|
||||
return transform_point(m, Point{0, 0})
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
pt_near :: proc(a, b: Point) -> bool {
|
||||
return abs(a.x - b.x) < 1e-6 && abs(a.y - b.y) < 1e-6
|
||||
}
|
||||
|
||||
@(private = "file")
|
||||
pt_has_origin :: proc(tiles: []Matrix, want: Point) -> int {
|
||||
n := 0
|
||||
for m in tiles {
|
||||
if pt_near(pt_origin(m), want) {
|
||||
n += 1
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// pt_covers reports whether some returned cell contains the device point `q`.
|
||||
// The cell is the pattern's /BBox in that tile's own space, which is exactly
|
||||
// what the interpreter clips the replayed content to.
|
||||
@(private = "file")
|
||||
pt_covers :: proc(p: ^Pattern, tiles: []Matrix, q: Point) -> bool {
|
||||
for m in tiles {
|
||||
inv, ok := matrix_invert(m)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if rect_contains(p.bbox, transform_point(inv, q)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pt_covers_area checks a grid of sample points across `area`. Under-counting
|
||||
// the lattice -- an off-by-one in the index range, a step read as the cell
|
||||
// size when it is not -- leaves a stripe of the fill bare, and that is what
|
||||
// this catches.
|
||||
@(private = "file")
|
||||
expect_covers_area :: proc(t: ^testing.T, p: ^Pattern, tiles: []Matrix, area: Rect,
|
||||
steps: int, what: string, loc := #caller_location) {
|
||||
for iy in 0 ..= steps {
|
||||
for ix in 0 ..= steps {
|
||||
q := Point{
|
||||
area.x0 + (area.x1 - area.x0) * f64(ix) / f64(steps),
|
||||
area.y0 + (area.y1 - area.y0) * f64(iy) / f64(steps),
|
||||
}
|
||||
if !pt_covers(p, tiles, q) {
|
||||
testing.expectf(t, false, "%s: device point (%f,%f) is covered by no cell",
|
||||
what, q.x, q.y, loc = loc)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The headline: /Matrix is anchored to the page, not to the fill
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// A pattern's /Matrix maps pattern space to the page's DEFAULT space
|
||||
// (8.7.3.1). The transform in force when the fill runs must not move the
|
||||
// lattice: the same pattern used inside `q 2 0 0 2 30 40 cm ... Q` puts its
|
||||
// cells exactly where it puts them outside.
|
||||
//
|
||||
// The test pins two things a wrong implementation gets wrong in different
|
||||
// ways: the device position of the cell that holds pattern-space (0,0), which
|
||||
// catches composing in the wrong order, and the equality of the whole lattice
|
||||
// across two different fill CTMs, which catches composing with the live CTM at
|
||||
// all. The third assertion is the control -- feeding the fill's CTM in as if
|
||||
// it were the base does move the lattice -- so the second is known to be
|
||||
// testing something.
|
||||
@(test)
|
||||
test_pattern_matrix_is_anchored_to_default_space :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
p := pt_tiling(t, doc, TILE_SRC)
|
||||
|
||||
// A page flip: user (0,0) is at device (0,200). The area is chosen to
|
||||
// contain the cell that holds pattern-space (0,0), which the /Matrix puts
|
||||
// at device (5,193).
|
||||
base := Matrix{1, 0, 0, -1, 0, 200}
|
||||
area := Rect{0, 150, 40, 195}
|
||||
|
||||
tiles := pattern_tiles(p, area, base)
|
||||
defer delete(tiles)
|
||||
testing.expect(t, len(tiles) > 0, "no tiles for a covered area")
|
||||
|
||||
// /Matrix first, then the page CTM: (0,0) -> (5,7) -> (5,193).
|
||||
want := Point{5, 193}
|
||||
testing.expect_value(t, pt_has_origin(tiles, want), 1)
|
||||
|
||||
// The same pattern, filled under `q 2 0 0 2 30 40 cm`. The interpreter
|
||||
// still passes the base CTM, and the lattice must be bit-identical.
|
||||
fill_ctm := matrix_mul(Matrix{2, 0, 0, 2, 30, 40}, base)
|
||||
again := pattern_tiles(p, area, base)
|
||||
defer delete(again)
|
||||
testing.expect_value(t, len(again), len(tiles))
|
||||
for m, i in again {
|
||||
testing.expect(t, m == tiles[i], "the lattice moved between two fills")
|
||||
}
|
||||
|
||||
// Control: had the fill's CTM been composed in, the cells would have gone
|
||||
// somewhere else entirely. Without this the assertion above is vacuous.
|
||||
wrong := pattern_tiles(p, area, fill_ctm)
|
||||
defer delete(wrong)
|
||||
testing.expect_value(t, pt_has_origin(wrong, want), 0)
|
||||
}
|
||||
|
||||
// pattern_matrix is the single place the two matrices meet, so the order is
|
||||
// pinned here directly: /Matrix applies first, the page CTM second. The
|
||||
// reversed product is a different point for any /Matrix with a translation.
|
||||
@(test)
|
||||
test_pattern_matrix_composition_order :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
p := pt_tiling(t, doc, TILE_SRC)
|
||||
base := Matrix{1, 0, 0, -1, 0, 200}
|
||||
|
||||
m := pattern_matrix(p, base)
|
||||
testing.expect(t, pt_near(transform_point(m, Point{0, 0}), Point{5, 193}),
|
||||
"pattern matrix is not /Matrix then CTM")
|
||||
testing.expect(t, !pt_near(transform_point(matrix_mul(base, p.mtx), Point{0, 0}),
|
||||
Point{5, 193}), "the reversed product would have passed too")
|
||||
|
||||
// A nil pattern leaves the CTM alone rather than inventing one.
|
||||
testing.expect(t, pattern_matrix(nil, base) == base, "nil pattern changed the CTM")
|
||||
}
|
||||
|
||||
// The cell-to-cell offset in device space is the step carried through the same
|
||||
// composition, so a rotated pattern's lattice is rotated with it and no cell
|
||||
// drifts from the grid.
|
||||
@(test)
|
||||
test_pattern_tiles_are_one_lattice :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
// The step is deliberately not the cell size: a lattice whose pitch was
|
||||
// read off /BBox instead of /XStep would still tile, and would still
|
||||
// cover, and only the spacing would be wrong.
|
||||
p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 10] /XStep 8 /YStep 6 >>")
|
||||
base := matrix_mul(rotate_matrix(30), Matrix{2, 0, 0, 2, 11, 13})
|
||||
|
||||
area := Rect{0, 0, 60, 60}
|
||||
tiles := pattern_tiles(p, area, base)
|
||||
defer delete(tiles)
|
||||
testing.expect(t, len(tiles) > 4, "too few tiles to be a lattice")
|
||||
|
||||
m := pattern_matrix(p, base)
|
||||
step_x := transform_vector(m, Point{p.xstep, 0})
|
||||
step_y := transform_vector(m, Point{0, p.ystep})
|
||||
|
||||
// Every origin sits at base + i*step_x + j*step_y for whole i and j.
|
||||
o0 := pt_origin(m)
|
||||
for tm in tiles {
|
||||
o := pt_origin(tm)
|
||||
// Solve the 2x2 system rather than trusting a division: the steps are
|
||||
// not axis-aligned here.
|
||||
det := step_x.x * step_y.y - step_x.y * step_y.x
|
||||
testing.expect(t, abs(det) > PT_EPS, "degenerate lattice steps")
|
||||
dx := o.x - o0.x
|
||||
dy := o.y - o0.y
|
||||
i := (dx * step_y.y - dy * step_y.x) / det
|
||||
j := (step_x.x * dy - step_x.y * dx) / det
|
||||
testing.expectf(t, abs(i - math.round(i)) < 1e-6 && abs(j - math.round(j)) < 1e-6,
|
||||
"cell origin (%f,%f) is off the lattice: i=%f j=%f", o.x, o.y, i, j)
|
||||
}
|
||||
expect_covers_area(t, p, tiles, area, 12, "rotated lattice")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Counting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// A 10x10 cell over a 90x90 area, with the area deliberately not aligned to
|
||||
// the lattice so the count does not depend on how a cell that merely touches
|
||||
// the edge is treated.
|
||||
@(test)
|
||||
test_pattern_tile_count_small_cell_large_area :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
p := pt_tiling(t, doc, PLAIN_SRC)
|
||||
area := Rect{5, 5, 95, 95}
|
||||
tiles := pattern_tiles(p, area, MATRIX_IDENTITY)
|
||||
defer delete(tiles)
|
||||
|
||||
// i runs 0..9 and j runs 0..9: cell 0 spans [0,10] and cell 9 spans
|
||||
// [90,100], and both meet the area.
|
||||
testing.expect_value(t, len(tiles), 100)
|
||||
testing.expect_value(t, pt_has_origin(tiles, Point{0, 0}), 1)
|
||||
testing.expect_value(t, pt_has_origin(tiles, Point{90, 90}), 1)
|
||||
testing.expect_value(t, pt_has_origin(tiles, Point{100, 0}), 0)
|
||||
expect_covers_area(t, p, tiles, area, 20, "aligned lattice")
|
||||
}
|
||||
|
||||
// A cell larger than its step overlaps its neighbours, which is a real design
|
||||
// and not a malformation: the lattice just gets denser than the cell size.
|
||||
@(test)
|
||||
test_pattern_overlapping_cells :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
before := len(doc.repairs)
|
||||
p := pt_tiling(t, doc,
|
||||
"<< /PatternType 1 /BBox [0 0 10 10] /XStep 5 /YStep 10 >>")
|
||||
testing.expect_value(t, p.xstep, 5.0)
|
||||
testing.expect_value(t, len(doc.repairs), before)
|
||||
|
||||
area := Rect{5, 5, 95, 95}
|
||||
tiles := pattern_tiles(p, area, MATRIX_IDENTITY)
|
||||
defer delete(tiles)
|
||||
// Twice as many columns as the abutting case, same number of rows: i runs
|
||||
// -1..19, because the cell is two steps wide and so reaches back one.
|
||||
testing.expect_value(t, len(tiles), 21 * 10)
|
||||
// The pitch is the step, not the cell: cells sit every 5 units, so the one
|
||||
// at 5 exists and is distinct from the ones at 0 and 10.
|
||||
testing.expect_value(t, pt_has_origin(tiles, Point{-5, 0}), 1)
|
||||
testing.expect_value(t, pt_has_origin(tiles, Point{5, 0}), 1)
|
||||
testing.expect_value(t, pt_has_origin(tiles, Point{95, 90}), 1)
|
||||
expect_covers_area(t, p, tiles, area, 20, "overlapping cells")
|
||||
}
|
||||
|
||||
// A /BBox that is not at the origin is the case where the lattice index and
|
||||
// the cell extent stop being the same number, and it is common: a cell drawn
|
||||
// around a motif is written wherever the motif already was.
|
||||
@(test)
|
||||
test_pattern_offset_bbox :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
// Cells 20x40 at a 20x40 pitch, so they abut, starting at (10,20).
|
||||
p := pt_tiling(t, doc,
|
||||
"<< /PatternType 1 /BBox [10 20 30 60] /XStep 20 /YStep 40 >>")
|
||||
area := Rect{15, 25, 75, 135}
|
||||
tiles := pattern_tiles(p, area, MATRIX_IDENTITY)
|
||||
defer delete(tiles)
|
||||
|
||||
// Across: cell i spans [10+20i, 30+20i], so i runs 0..3 for x in 15..75.
|
||||
// Up: cell j spans [20+40j, 60+40j], so j runs 0..2 for y in 25..135.
|
||||
testing.expect_value(t, len(tiles), 4 * 3)
|
||||
// The matrix maps the cell's own space, in which the bbox corner is at
|
||||
// (10,20), not at the origin.
|
||||
testing.expect_value(t, pt_has_origin(tiles, Point{0, 0}), 1)
|
||||
testing.expect_value(t, pt_has_origin(tiles, Point{60, 80}), 1)
|
||||
expect_covers_area(t, p, tiles, area, 16, "offset bbox")
|
||||
}
|
||||
|
||||
// An area that falls in the gap between two cells gets no cells at all: a
|
||||
// /XStep larger than the cell leaves bare stripes, and painting them would
|
||||
// mean stretching the pattern to fill space the file left empty.
|
||||
@(test)
|
||||
test_pattern_area_between_cells :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 1 1] /XStep 100 /YStep 100 >>")
|
||||
gap := pattern_tiles(p, Rect{10, 10, 20, 20}, MATRIX_IDENTITY)
|
||||
defer delete(gap)
|
||||
testing.expect_value(t, len(gap), 0)
|
||||
|
||||
// The same pattern over an area that does reach a cell finds it.
|
||||
hit := pattern_tiles(p, Rect{-5, -5, 5, 5}, MATRIX_IDENTITY)
|
||||
defer delete(hit)
|
||||
testing.expect_value(t, len(hit), 1)
|
||||
testing.expect_value(t, pt_has_origin(hit, Point{0, 0}), 1)
|
||||
}
|
||||
|
||||
// The returned slice belongs to the caller and comes from the allocator it was
|
||||
// given, not from the context: a page is rendered through a scratch arena that
|
||||
// is reset between pages, and a lattice that quietly allocated from the
|
||||
// Document's arena instead would never be reclaimed.
|
||||
//
|
||||
// The assertion is the arena's own cursor, not a leak report: a slice taken
|
||||
// from somewhere else leaves the arena untouched.
|
||||
@(test)
|
||||
test_pattern_tiles_use_the_given_allocator :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
backing: [16 * 1024]byte
|
||||
arena: mem.Arena
|
||||
mem.arena_init(&arena, backing[:])
|
||||
|
||||
p := pt_tiling(t, doc, PLAIN_SRC)
|
||||
tiles := pattern_tiles(p, Rect{5, 5, 95, 95}, MATRIX_IDENTITY, mem.arena_allocator(&arena))
|
||||
testing.expect_value(t, len(tiles), 100)
|
||||
testing.expect(t, arena.offset >= 100 * size_of(Matrix),
|
||||
"the tiles did not come from the allocator that was passed in")
|
||||
lo := uintptr(raw_data(backing[:]))
|
||||
at := uintptr(raw_data(tiles))
|
||||
testing.expect(t, at >= lo && at < lo + uintptr(len(backing)),
|
||||
"the tiles do not live in the arena")
|
||||
}
|
||||
|
||||
// The pattern's own space is where the lattice is regular, so a rotated
|
||||
// pattern needs the area pulled back through the inverse before the indices
|
||||
// mean anything. If that pull-back were skipped the corners of the area would
|
||||
// come out bare.
|
||||
@(test)
|
||||
test_pattern_tiles_under_rotation_cover_the_area :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
p := pt_tiling(t, doc,
|
||||
"<< /PatternType 1 /BBox [0 0 8 8] /XStep 8 /YStep 8 /Matrix [0.866 0.5 -0.5 0.866 3 4] >>")
|
||||
area := Rect{-30, -30, 30, 30}
|
||||
tiles := pattern_tiles(p, area, MATRIX_IDENTITY)
|
||||
defer delete(tiles)
|
||||
|
||||
testing.expect(t, len(tiles) > 0, "rotated pattern produced no tiles")
|
||||
expect_covers_area(t, p, tiles, area, 24, "rotated pattern")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Guards
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// /XStep 0 would stack an infinite row of cells at one place. It falls back to
|
||||
// the cell width, which is what the step means when cells abut, and the
|
||||
// substitution is recorded.
|
||||
@(test)
|
||||
test_pattern_zero_step_clamped_to_bbox :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
before := len(doc.repairs)
|
||||
p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 20] /XStep 0 /YStep 0 >>")
|
||||
testing.expect_value(t, p.xstep, 10.0)
|
||||
testing.expect_value(t, p.ystep, 20.0)
|
||||
testing.expect(t, len(doc.repairs) >= before + 2, "a clamped step was not recorded")
|
||||
|
||||
area := Rect{5, 5, 95, 95}
|
||||
tiles := pattern_tiles(p, area, MATRIX_IDENTITY)
|
||||
defer delete(tiles)
|
||||
testing.expect_value(t, len(tiles), 10 * 5)
|
||||
expect_covers_area(t, p, tiles, area, 20, "zero step")
|
||||
}
|
||||
|
||||
// A step a ten-thousandth of the cell is the same denial of service as zero,
|
||||
// written so that it looks like a number: every device pixel would be covered
|
||||
// by ten thousand cells.
|
||||
@(test)
|
||||
test_pattern_absurd_step_clamped :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
before := len(doc.repairs)
|
||||
p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 10] /XStep 0.001 /YStep 10 >>")
|
||||
testing.expect_value(t, p.xstep, 10.0)
|
||||
testing.expect_value(t, p.ystep, 10.0)
|
||||
testing.expect(t, len(doc.repairs) > before, "a clamped step was not recorded")
|
||||
|
||||
// An overlap the guard is meant to allow through untouched.
|
||||
before2 := len(doc.repairs)
|
||||
q := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 10] /XStep 2.5 /YStep 10 >>")
|
||||
testing.expect_value(t, q.xstep, 2.5)
|
||||
testing.expect_value(t, len(doc.repairs), before2)
|
||||
}
|
||||
|
||||
// Only the magnitude of a step spaces the lattice, so a negative one is read
|
||||
// rather than repaired.
|
||||
@(test)
|
||||
test_pattern_negative_step_uses_magnitude :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
before := len(doc.repairs)
|
||||
p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 10 10] /XStep -10 /YStep -10 >>")
|
||||
testing.expect_value(t, p.xstep, 10.0)
|
||||
testing.expect_value(t, p.ystep, 10.0)
|
||||
testing.expect_value(t, len(doc.repairs), before)
|
||||
|
||||
tiles := pattern_tiles(p, Rect{5, 5, 95, 95}, MATRIX_IDENTITY)
|
||||
defer delete(tiles)
|
||||
testing.expect_value(t, len(tiles), 100)
|
||||
}
|
||||
|
||||
// A one-unit cell over a million-unit area is 10^12 replays of a content
|
||||
// stream. The count is capped, the shortfall is recorded once, and what comes
|
||||
// back is still a usable prefix of the lattice rather than nothing.
|
||||
@(test)
|
||||
test_pattern_tile_count_capped :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
p := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 1 1] /XStep 1 /YStep 1 >>")
|
||||
|
||||
before := len(doc.repairs)
|
||||
tiles := pattern_tiles(p, Rect{0, 0, 1e6, 1e6}, MATRIX_IDENTITY)
|
||||
defer delete(tiles)
|
||||
testing.expect_value(t, len(tiles), MAX_PATTERN_TILES)
|
||||
testing.expect(t, len(doc.repairs) > before, "the tile cap was not recorded")
|
||||
|
||||
// Every returned matrix is still a real lattice position, not filler.
|
||||
for m, i in tiles {
|
||||
o := pt_origin(m)
|
||||
testing.expectf(t, is_finite(o.x) && is_finite(o.y),
|
||||
"tile %d has a non-finite origin (%f,%f)", i, o.x, o.y)
|
||||
}
|
||||
|
||||
// A page that fills with the same hostile pattern many times must cost one
|
||||
// repair, not one per fill.
|
||||
mid := len(doc.repairs)
|
||||
more := pattern_tiles(p, Rect{0, 0, 1e6, 1e6}, MATRIX_IDENTITY)
|
||||
defer delete(more)
|
||||
testing.expect_value(t, len(more), MAX_PATTERN_TILES)
|
||||
testing.expect_value(t, len(doc.repairs), mid)
|
||||
}
|
||||
|
||||
// An index range computed from file numbers can be 1e300, and narrowing that
|
||||
// to an int is undefined. The clamp lives in tile_range, so it is pinned
|
||||
// there: what comes back is bounded, and no arithmetic on it can trap.
|
||||
@(test)
|
||||
test_pattern_tile_range_clamps :: proc(t: ^testing.T) {
|
||||
from, to, ok := tile_range(-1e300, 1e300, 0, 10, 1e-6)
|
||||
testing.expect(t, ok, "a huge but finite range was rejected")
|
||||
testing.expect_value(t, from, -MAX_TILE_INDEX)
|
||||
testing.expect_value(t, to, MAX_TILE_INDEX)
|
||||
|
||||
_, _, ok_step := tile_range(0, 10, 0, 1, 0)
|
||||
testing.expect(t, !ok_step, "a zero step produced a range")
|
||||
|
||||
_, _, ok_nan := tile_range(math.QNAN_F64, 10, 0, 1, 1)
|
||||
testing.expect(t, !ok_nan, "a NaN bound produced a range")
|
||||
|
||||
// An area that ends before the first cell begins has no cells at all.
|
||||
_, _, ok_empty := tile_range(100, 101, 0, 1, 1000)
|
||||
testing.expect(t, !ok_empty, "a gap between cells produced a range")
|
||||
}
|
||||
|
||||
// Areas that cannot be enumerated, and patterns that have no cells.
|
||||
@(test)
|
||||
test_pattern_tiles_rejects_unusable_input :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
p := pt_tiling(t, doc, PLAIN_SRC)
|
||||
|
||||
testing.expect_value(t, len(pattern_tiles(nil, Rect{0, 0, 10, 10}, MATRIX_IDENTITY)), 0)
|
||||
testing.expect_value(t, len(pattern_tiles(p, RECT_EMPTY, MATRIX_IDENTITY)), 0)
|
||||
// "Cover an unbounded area with a repeating cell" has no answer, and
|
||||
// RECT_INFINITE's corners are finite numbers, so it has to be caught by
|
||||
// identity or it would silently produce a capped field of cells.
|
||||
testing.expect_value(t, len(pattern_tiles(p, RECT_INFINITE, MATRIX_IDENTITY)), 0)
|
||||
nan_area := Rect{0, 0, math.QNAN_F64, 10}
|
||||
testing.expect_value(t, len(pattern_tiles(p, nan_area, MATRIX_IDENTITY)), 0)
|
||||
inf_ctm := Matrix{math.INF_F64, 0, 0, 1, 0, 0}
|
||||
testing.expect_value(t, len(pattern_tiles(p, Rect{0, 0, 10, 10}, inf_ctm)), 0)
|
||||
// A CTM that collapses the plane gives every cell zero area.
|
||||
singular := Matrix{1, 1, 1, 1, 0, 0}
|
||||
testing.expect_value(t, len(pattern_tiles(p, Rect{0, 0, 10, 10}, singular)), 0)
|
||||
|
||||
// A shading pattern paints in one pass; it has no cells to replay.
|
||||
sp := pt_dict_pattern(t, doc, "<< /PatternType 2 /Shading " + AXIAL + " >>")
|
||||
testing.expect_value(t, len(pattern_tiles(sp, Rect{0, 0, 10, 10}, MATRIX_IDENTITY)), 0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Uncoloured patterns
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// /PaintType 2 is a stencil: the cell's own colour operators are ignored and
|
||||
// the whole cell paints in the colour given alongside the pattern name, so the
|
||||
// interpreter has to be told before it replays anything.
|
||||
@(test)
|
||||
test_pattern_uncoloured_needs_the_callers_colour :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
unc := pt_tiling(t, doc,
|
||||
"<< /PatternType 1 /PaintType 2 /BBox [0 0 4 4] /XStep 4 /YStep 4 >>")
|
||||
testing.expect_value(t, unc.paint_type, Paint_Type.Uncoloured)
|
||||
testing.expect(t, pattern_needs_color(unc), "an uncoloured pattern must ask for a colour")
|
||||
|
||||
col := pt_tiling(t, doc, PLAIN_SRC)
|
||||
testing.expect_value(t, col.paint_type, Paint_Type.Coloured)
|
||||
testing.expect(t, !pattern_needs_color(col), "a coloured pattern supplies its own colour")
|
||||
|
||||
sh := pt_dict_pattern(t, doc, "<< /PatternType 2 /Shading " + AXIAL + " >>")
|
||||
testing.expect(t, !pattern_needs_color(sh), "a shading pattern supplies its own colour")
|
||||
testing.expect(t, !pattern_needs_color(nil), "nil asked for a colour")
|
||||
|
||||
// An out-of-range /PaintType is a broken file, not a third kind of paint.
|
||||
odd := pt_tiling(t, doc,
|
||||
"<< /PatternType 1 /PaintType 7 /BBox [0 0 4 4] /XStep 4 /YStep 4 >>")
|
||||
testing.expect_value(t, odd.paint_type, Paint_Type.Coloured)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@(test)
|
||||
test_pattern_tiling_fields :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
res := new_dict(doc.allocator)
|
||||
p, err := pattern_load(doc, cf_stream(doc,
|
||||
"<< /PatternType 1 /PaintType 1 /TilingType 3 /BBox [10 20 30 60] " +
|
||||
"/XStep 25 /YStep 45 /Matrix [2 0 0 2 1 1] >>", nil), res)
|
||||
testing.expect_value(t, err, Error.None)
|
||||
testing.expect_value(t, p.kind, Pattern_Kind.Tiling)
|
||||
testing.expect_value(t, p.tiling_type, Tiling_Type.Faster_Constant_Spacing)
|
||||
testing.expect_value(t, p.bbox, Rect{10, 20, 30, 60})
|
||||
testing.expect_value(t, p.xstep, 25.0)
|
||||
testing.expect_value(t, p.ystep, 45.0)
|
||||
testing.expect_value(t, p.mtx, Matrix{2, 0, 0, 2, 1, 1})
|
||||
testing.expect(t, p.content != nil, "the cell's content stream was dropped")
|
||||
testing.expect(t, p.doc == doc, "the pattern lost its document")
|
||||
// /Resources is required but harmless to omit for a cell that draws only
|
||||
// rectangles; the resources of the stream that named the pattern stand in.
|
||||
testing.expect(t, p.resources == res, "missing /Resources did not fall back")
|
||||
|
||||
// A pattern with resources of its own keeps them.
|
||||
q, q_err := pattern_load(doc, cf_stream(doc,
|
||||
"<< /PatternType 1 /BBox [0 0 4 4] /Resources << /Marker 7 >> >>", nil), res)
|
||||
testing.expect_value(t, q_err, Error.None)
|
||||
testing.expect(t, q.resources != res, "own /Resources were replaced by the fallback")
|
||||
testing.expect_value(t, dict_int(doc, q.resources, "Marker"), i64(7))
|
||||
// Absent /XStep and /YStep default to the cell size, the same as zero.
|
||||
testing.expect_value(t, q.xstep, 4.0)
|
||||
testing.expect_value(t, q.ystep, 4.0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_pattern_shading_loads_its_shading :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
p := pt_dict_pattern(t, doc,
|
||||
"<< /PatternType 2 /Matrix [1 0 0 1 20 30] /Shading " + AXIAL +
|
||||
" /ExtGState << /ca 0.5 >> >>")
|
||||
testing.expect_value(t, p.kind, Pattern_Kind.Shading)
|
||||
testing.expect(t, p.shading != nil, "the shading was not loaded")
|
||||
testing.expect_value(t, p.shading.kind, Shading_Kind.Axial)
|
||||
testing.expect_value(t, p.shading.coords[2], 10.0)
|
||||
testing.expect(t, p.ext_gstate != nil, "/ExtGState was dropped")
|
||||
testing.expect_value(t, dict_num(doc, p.ext_gstate, "ca"), 0.5)
|
||||
testing.expect(t, p.content == nil, "a shading pattern has no cell content")
|
||||
}
|
||||
|
||||
// A colour space named by a bare name inside the shading has to be resolved
|
||||
// through the resource dictionary the pattern was named from.
|
||||
@(test)
|
||||
test_pattern_shading_uses_the_resources :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
cs_dict := new_dict(doc.allocator)
|
||||
dict_set(cs_dict, "Spot", Ref{5, 0})
|
||||
res := new_dict(doc.allocator)
|
||||
dict_set(res, "ColorSpace", cs_dict)
|
||||
|
||||
p := pt_dict_pattern(t, doc,
|
||||
"<< /PatternType 2 /Shading << /ShadingType 2 /ColorSpace /Spot " +
|
||||
"/Coords [0 0 10 0] /Function << /FunctionType 2 /Domain [0 1] " +
|
||||
"/C0 [0] /C1 [1] /N 1 >> >> >>", res)
|
||||
testing.expect(t, p.shading != nil && p.shading.cs != nil, "the shading has no colour space")
|
||||
testing.expect_value(t, p.shading.cs.kind, Colorspace_Kind.Separation)
|
||||
}
|
||||
|
||||
// A missing /PatternType is a broken file that still says which kind it meant.
|
||||
@(test)
|
||||
test_pattern_type_inferred :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
before := len(doc.repairs)
|
||||
tile := pt_tiling(t, doc, "<< /BBox [0 0 4 4] /XStep 4 /YStep 4 >>")
|
||||
testing.expect_value(t, tile.kind, Pattern_Kind.Tiling)
|
||||
testing.expect(t, len(doc.repairs) > before, "the missing /PatternType was not recorded")
|
||||
|
||||
sh := pt_dict_pattern(t, doc, "<< /Shading " + AXIAL + " >>")
|
||||
testing.expect_value(t, sh.kind, Pattern_Kind.Shading)
|
||||
|
||||
// A stream that also carries a /Shading is a shading pattern: the entry
|
||||
// that only one kind has decides it.
|
||||
both := pt_tiling(t, doc, "<< /BBox [0 0 4 4] /Shading " + AXIAL + " >>")
|
||||
testing.expect_value(t, both.kind, Pattern_Kind.Shading)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_pattern_load_rejects_malformed :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
expect_bad :: proc(t: ^testing.T, doc: ^Document, obj: Object, what: string,
|
||||
loc := #caller_location) {
|
||||
p, err := pattern_load(doc, obj, nil)
|
||||
testing.expect_value(t, err, Error.Bad_Pattern, loc = loc)
|
||||
testing.expectf(t, p == nil, "%s produced a pattern", what, loc = loc)
|
||||
}
|
||||
|
||||
expect_bad(t, doc, cf_parse(doc, "42"), "a number")
|
||||
expect_bad(t, doc, nil, "a null")
|
||||
expect_bad(t, doc, cf_parse(doc, "<< /PatternType 9 >>"), "an unknown /PatternType")
|
||||
expect_bad(t, doc, cf_parse(doc, "<< /PatternType 1 /BBox [0 0 4 4] >>"),
|
||||
"a tiling pattern that is not a stream")
|
||||
expect_bad(t, doc, cf_stream(doc, "<< /PatternType 1 >>", nil), "no /BBox")
|
||||
expect_bad(t, doc, cf_stream(doc, "<< /PatternType 1 /BBox [0 0 0 8] >>", nil),
|
||||
"a /BBox with no width")
|
||||
expect_bad(t, doc, cf_stream(doc, "<< /PatternType 1 /BBox [1 2 3] >>", nil),
|
||||
"a short /BBox")
|
||||
// Both corners are finite; the width between them is not, and an infinite
|
||||
// step times a zero lattice index is a NaN cell matrix.
|
||||
expect_bad(t, doc, cf_stream(doc,
|
||||
"<< /PatternType 1 /BBox [-1e308 -1e308 1e308 1e308] >>", nil),
|
||||
"a /BBox as wide as the number line")
|
||||
expect_bad(t, doc, cf_parse(doc, "<< /PatternType 2 >>"), "no /Shading")
|
||||
expect_bad(t, doc, cf_parse(doc, "<< /PatternType 2 /Shading << /ShadingType 99 >> >>"),
|
||||
"an unknown shading type")
|
||||
|
||||
// A nil Document is a caller error, not a malformed file.
|
||||
p, err := pattern_load(nil, cf_parse(doc, "<< /PatternType 1 >>"), nil)
|
||||
testing.expect_value(t, err, Error.Invalid_Argument)
|
||||
testing.expect(t, p == nil, "a nil document produced a pattern")
|
||||
}
|
||||
|
||||
// A /Matrix that is not six finite numbers has to arrive as the identity --
|
||||
// the format's own default -- rather than as a NaN that would poison every
|
||||
// transform derived from it. The filter itself is `object_to_matrix`'s, in
|
||||
// geom.odin; what is pinned here is that pattern loading goes through it and
|
||||
// does not substitute a default of its own.
|
||||
@(test)
|
||||
test_pattern_bad_matrix_falls_back :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
short := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 4 4] /Matrix [1 0 0] >>")
|
||||
testing.expect_value(t, short.mtx, MATRIX_IDENTITY)
|
||||
|
||||
absent := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 4 4] >>")
|
||||
testing.expect_value(t, absent.mtx, MATRIX_IDENTITY)
|
||||
|
||||
// 1e400 parses to an infinity, which is exactly the value that must not
|
||||
// reach a lattice: a single infinite entry makes every cell origin a NaN.
|
||||
inf := pt_tiling(t, doc, "<< /PatternType 1 /BBox [0 0 4 4] /Matrix [1 0 0 1 0 1e400] >>")
|
||||
testing.expect_value(t, inf.mtx, MATRIX_IDENTITY)
|
||||
testing.expect(t, matrix_is_finite(inf.mtx), "a non-finite /Matrix was kept")
|
||||
|
||||
tiles := pattern_tiles(inf, Rect{0, 0, 8, 8}, MATRIX_IDENTITY)
|
||||
defer delete(tiles)
|
||||
testing.expect(t, len(tiles) > 0, "a repaired /Matrix still produced no cells")
|
||||
for m in tiles {
|
||||
o := pt_origin(m)
|
||||
testing.expect(t, is_finite(o.x) && is_finite(o.y), "a cell origin is not finite")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bounds
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@(test)
|
||||
test_pattern_bounds :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
// A tiling pattern repeats over the whole plane. RECT_INFINITE is the
|
||||
// identity for rect_intersect, so a caller gets back its own clip.
|
||||
tile := pt_tiling(t, doc, PLAIN_SRC)
|
||||
testing.expect_value(t, pattern_bounds(tile, MATRIX_IDENTITY), RECT_INFINITE)
|
||||
testing.expect_value(t, rect_intersect(pattern_bounds(tile, MATRIX_IDENTITY),
|
||||
Rect{3, 4, 5, 6}), Rect{3, 4, 5, 6})
|
||||
|
||||
// A transform that collapses the plane paints nothing, and nothing is the
|
||||
// opposite identity: a caller intersecting with RECT_INFINITE here would
|
||||
// flood the clip with a pattern that has no area.
|
||||
testing.expect_value(t, pattern_bounds(tile, Matrix{1, 1, 1, 1, 0, 0}), RECT_EMPTY)
|
||||
testing.expect_value(t, pattern_bounds(tile, Matrix{math.QNAN_F64, 0, 0, 1, 0, 0}), RECT_EMPTY)
|
||||
testing.expect_value(t, pattern_bounds(nil, MATRIX_IDENTITY), RECT_EMPTY)
|
||||
|
||||
// An un-extended radial shading is bounded by its two circles, and the
|
||||
// pattern matrix moves that bound with it.
|
||||
rad := pt_dict_pattern(t, doc,
|
||||
"<< /PatternType 2 /Matrix [1 0 0 1 20 30] /Shading << /ShadingType 3 " +
|
||||
"/ColorSpace /DeviceGray /Coords [0 0 0 0 0 5] " +
|
||||
"/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> >> >>")
|
||||
testing.expect_value(t, pattern_bounds(rad, MATRIX_IDENTITY), Rect{15, 25, 25, 35})
|
||||
// Under a page CTM the two compose, and only in that order.
|
||||
testing.expect_value(t, pattern_bounds(rad, Matrix{2, 0, 0, 2, 0, 0}),
|
||||
Rect{30, 50, 50, 70})
|
||||
|
||||
// An axial shading is unbounded across its axis whatever its extends say.
|
||||
ax := pt_dict_pattern(t, doc, "<< /PatternType 2 /Shading " + AXIAL + " >>")
|
||||
testing.expect_value(t, pattern_bounds(ax, MATRIX_IDENTITY), RECT_INFINITE)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Painting a shading pattern
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// The shading has to be painted through the pattern matrix composed with the
|
||||
// page's default CTM, so a pattern that translates its shading by 20 puts the
|
||||
// ramp 20 device units further along. Three components and no alpha means the
|
||||
// destination reads as opaque, so a pixel is what the shading computed.
|
||||
@(test)
|
||||
test_pattern_paint_shading_through_the_matrix :: proc(t: ^testing.T) {
|
||||
doc := cf_open(t)
|
||||
defer cf_close(t, doc)
|
||||
|
||||
// A ramp from black at x=0 to white at x=10 in pattern space, extended
|
||||
// both ways, shifted 20 to the right by the pattern matrix.
|
||||
p := pt_dict_pattern(t, doc,
|
||||
"<< /PatternType 2 /Matrix [1 0 0 1 20 0] /Shading " + AXIAL + " >>")
|
||||
|
||||
bmp, err := bitmap_make(40, 3, 3)
|
||||
testing.expect_value(t, err, Error.None)
|
||||
defer bitmap_destroy(&bmp)
|
||||
bitmap_fill(&bmp, [4]u8{9, 200, 30, 255})
|
||||
|
||||
perr := pattern_paint_shading(doc, p, &bmp, MATRIX_IDENTITY,
|
||||
Raster_Clip{0, 0, bmp.width, bmp.height}, 1, .Normal, nil)
|
||||
testing.expect_value(t, perr, Error.None)
|
||||
|
||||
px :: proc(bmp: ^Bitmap, x, y: int) -> u8 {
|
||||
off, ok := bitmap_pixel_offset(bmp, x, y)
|
||||
return ok ? bmp.pixels[off] : 0
|
||||
}
|
||||
// Pixel centres are at x+0.5, so the axis start (pattern x=0, device
|
||||
// x=20) is half a pixel left of pixel 20's centre: t = 0.05.
|
||||
testing.expect(t, px(&bmp, 10, 1) == 0, "the extended dark end is not black")
|
||||
testing.expect(t, px(&bmp, 35, 1) == 255, "the extended bright end is not white")
|
||||
mid := px(&bmp, 25, 1)
|
||||
testing.expectf(t, mid > 120 && mid < 160, "midpoint of the ramp is %d, want about 140", mid)
|
||||
// Without the pattern matrix the ramp would have finished by x = 10.
|
||||
testing.expect(t, px(&bmp, 12, 1) == 0, "the ramp started 20 units too early")
|
||||
|
||||
// A tiling pattern has no shading to paint, and neither does nil.
|
||||
tile := pt_tiling(t, doc, PLAIN_SRC)
|
||||
testing.expect_value(t, pattern_paint_shading(doc, tile, &bmp, MATRIX_IDENTITY,
|
||||
Raster_Clip{0, 0, bmp.width, bmp.height}, 1, .Normal, nil), Error.Invalid_Argument)
|
||||
testing.expect_value(t, pattern_paint_shading(doc, nil, &bmp, MATRIX_IDENTITY,
|
||||
Raster_Clip{0, 0, bmp.width, bmp.height}, 1, .Normal, nil), Error.Invalid_Argument)
|
||||
}
|
||||
Reference in New Issue
Block a user