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) }