package pdf import "core:fmt" import "core:testing" import "core:time" // The fixtures `cf_open`, `cf_stream` and `cf_close` live in function_test.odin. // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- @(private = "file") WHITE :: [4]u8{255, 255, 255, 255} @(private = "file") rt_bitmap :: proc(t: ^testing.T, w, h: int, fill := WHITE, loc := #caller_location) -> Bitmap { bmp, err := bitmap_make(w, h, 4) testing.expectf(t, err == .None, "bitmap_make: %v", err, loc = loc) bitmap_fill(&bmp, fill) // Opaque and fully transparent pixels are the same premultiplied as // straight, which is what makes a cleared page bitmap a legal target. bmp.premultiplied = true return bmp } @(private = "file") rt_dev :: proc(t: ^testing.T, bmp: ^Bitmap, doc: ^Document = nil, loc := #caller_location) -> ^Raster_Device { rd, err := raster_device_make(bmp, Raster_Clip{0, 0, bmp.width, bmp.height}, context.allocator, doc) testing.expectf(t, err == .None, "raster_device_make: %v", err, loc = loc) return rd } @(private = "file") rt_px :: proc(bmp: ^Bitmap, x, y: int) -> [4]u8 { off, ok := bitmap_pixel_offset(bmp, x, y) if !ok { return {} } return {bmp.pixels[off], bmp.pixels[off + 1], bmp.pixels[off + 2], bmp.pixels[off + 3]} } @(private = "file") rt_expect :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [4]u8, what: string, loc := #caller_location) { got := rt_px(bmp, x, y) if got != want { testing.expectf(t, false, "%s at (%d,%d): got %v, want %v", what, x, y, got, want, loc = loc) } } @(private = "file") rt_expect_near :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [4]u8, tol: int, what: string, loc := #caller_location) { got := rt_px(bmp, x, y) for i in 0 ..< 4 { if abs(int(got[i]) - int(want[i])) > tol { testing.expectf(t, false, "%s at (%d,%d): got %v, want %v +-%d", what, x, y, got, want, tol, loc = loc) return } } } // rt_smask is the most recently installed soft-mask plane, or nil. @(private = "file") rt_smask :: proc(rd: ^Raster_Device) -> ^Rdev_Smask { best: ^Rdev_Smask for i in 0 ..< RDEV_MAX_SOFT_MASKS { if !rd.smasks[i].live { continue } if best == nil || rd.smasks[i].clock > best.clock { best = &rd.smasks[i] } } return best } // rt_mask makes a Soft_Mask distinguishable from every other one built here. // The device keys its plane cache on a mask's CONTENTS -- everything a render // depends on is in there -- so two all-zero Soft_Masks are one mask to it, and // a test that wants two different masks has to build two different masks. @(private = "file") rt_mask :: proc(n: int) -> Soft_Mask { return Soft_Mask{ctm = translate_matrix(f64(n), 0)} } @(private = "file") rt_paint :: proc(r, g, b: f64, alpha := 1.0) -> Paint { return Paint{rgb = {r, g, b}, alpha = alpha, blend = .Normal} } @(private = "file") RED :: [4]u8{255, 0, 0, 255} @(private = "file") GREEN :: [4]u8{0, 255, 0, 255} @(private = "file") BLUE :: [4]u8{0, 0, 255, 255} @(private = "file") BLACK :: [4]u8{0, 0, 0, 255} // rt_rect fills one axis-aligned rectangle in device space. @(private = "file") rt_rect :: proc(rd: ^Raster_Device, x, y, w, h: f64, paint: Paint, ctm := MATRIX_IDENTITY, even_odd := false) { p := path_make() defer path_destroy(&p) path_rect(&p, x, y, w, h) dev_fill_path(raster_device_as_device(rd), &p, even_odd, ctm, paint) } // rt_ink counts pixels that differ from the background. @(private = "file") rt_ink :: proc(bmp: ^Bitmap, background := WHITE) -> int { n := 0 for y in 0 ..< bmp.height { for x in 0 ..< bmp.width { if rt_px(bmp, x, y) != background { n += 1 } } } return n } // --------------------------------------------------------------------------- // Fills // --------------------------------------------------------------------------- // A pixel-aligned rectangle must land on exactly the pixels it covers, with // full-strength colour inside and nothing at all outside. Everything else in // this file is built on that being exact. @(test) test_rdev_fill_rect_exact :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) rt_rect(rd, 2, 3, 8, 4, rt_paint(1, 0, 0)) rt_expect(t, &bmp, 2, 3, RED, "top-left corner of the fill") rt_expect(t, &bmp, 9, 6, RED, "bottom-right corner of the fill") rt_expect(t, &bmp, 5, 4, RED, "inside") rt_expect(t, &bmp, 1, 3, WHITE, "one pixel left of the fill") rt_expect(t, &bmp, 10, 3, WHITE, "one pixel right of the fill") rt_expect(t, &bmp, 2, 2, WHITE, "one pixel above the fill") rt_expect(t, &bmp, 2, 7, WHITE, "one pixel below the fill") testing.expect_value(t, rt_ink(&bmp), 8 * 4) } // Constant alpha is the paint's, not the coverage's: a fully covered pixel at // alpha 0.5 is half the colour, and the result is opaque because the backdrop // was. @(test) test_rdev_fill_alpha :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) rt_rect(rd, 0, 0, 4, 4, rt_paint(1, 0, 0, 0.5)) rt_expect(t, &bmp, 1, 1, {255, 128, 128, 255}, "red at half alpha over white") rt_expect(t, &bmp, 5, 5, WHITE, "outside") } // Half a pixel of coverage is half the ink. The rasteriser's coverage is // analytic in x, so a fill that stops mid-pixel is a real test of the whole // coverage-to-pixel path and not just of the geometry. @(test) test_rdev_fill_partial_coverage :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) rt_rect(rd, 1, 0, 1.5, 4, rt_paint(0, 0, 0)) rt_expect(t, &bmp, 1, 1, BLACK, "the fully covered column") rt_expect_near(t, &bmp, 2, 1, {128, 128, 128, 255}, 2, "the half-covered column") rt_expect(t, &bmp, 3, 1, WHITE, "past the fill") } // Even-odd is not a corner case: `f*` means it and Type1 counters depend on // it. A rectangle inside a rectangle is filled as a frame by even-odd and // solid by nonzero. @(test) test_rdev_fill_rules_differ :: proc(t: ^testing.T) { for even_odd in ([2]bool{false, true}) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) p := path_make() defer path_destroy(&p) path_rect(&p, 2, 2, 12, 12) path_rect(&p, 5, 5, 6, 6) dev_fill_path(raster_device_as_device(rd), &p, even_odd, MATRIX_IDENTITY, rt_paint(0, 0, 0)) rt_expect(t, &bmp, 3, 3, BLACK, "the outer ring is filled either way") if even_odd { rt_expect(t, &bmp, 8, 8, WHITE, "even-odd leaves the inner rectangle open") } else { rt_expect(t, &bmp, 8, 8, BLACK, "nonzero fills it, both windings being the same") } } } // --------------------------------------------------------------------------- // Strokes // --------------------------------------------------------------------------- // A stroke of width 4 centred on x = 8 covers columns 6..9 and nothing else. @(test) test_rdev_stroke_width :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) p := path_make() defer path_destroy(&p) path_move_to(&p, 8, 2) path_line_to(&p, 8, 14) st := STROKE_STATE_DEFAULT st.line_width = 4 dev_stroke_path(raster_device_as_device(rd), &p, &st, MATRIX_IDENTITY, rt_paint(0, 0, 0)) for x in 6 ..< 10 { rt_expect(t, &bmp, x, 8, BLACK, "inside the pen") } rt_expect(t, &bmp, 5, 8, WHITE, "left of the pen") rt_expect(t, &bmp, 10, 8, WHITE, "right of the pen") // Butt caps: the ends are square at y = 2 and y = 14 exactly. rt_expect(t, &bmp, 7, 2, BLACK, "the first row of the stroke") rt_expect(t, &bmp, 7, 1, WHITE, "above the butt cap") rt_expect(t, &bmp, 7, 13, BLACK, "the last row of the stroke") rt_expect(t, &bmp, 7, 14, WHITE, "below the butt cap") testing.expect_value(t, rt_ink(&bmp), 4 * 12) } // A zero line width is one device pixel, whatever the CTM does (8.4.3.2). // Thin table rules are written that way and they must not vanish. @(test) test_rdev_stroke_hairline :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) p := path_make() defer path_destroy(&p) path_move_to(&p, 8.5, 0) path_line_to(&p, 8.5, 16) st := STROKE_STATE_DEFAULT st.line_width = 0 dev_stroke_path(raster_device_as_device(rd), &p, &st, MATRIX_IDENTITY, rt_paint(0, 0, 0)) rt_expect(t, &bmp, 8, 8, BLACK, "the hairline column") testing.expect_value(t, rt_ink(&bmp), 16) } // --------------------------------------------------------------------------- // Clipping // --------------------------------------------------------------------------- @(private = "file") rt_clip_rect :: proc(rd: ^Raster_Device, x, y, w, h: f64, ctm := MATRIX_IDENTITY) { p := path_make() defer path_destroy(&p) path_rect(&p, x, y, w, h) dev_clip_path(raster_device_as_device(rd), &p, false, ctm) } // Clips nest by intersection, and a rectangular one under an axis-aligned CTM // costs no coverage plane at all. @(test) test_rdev_nested_rect_clips :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 20, 20) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) rt_clip_rect(rd, 2, 2, 10, 10) rt_clip_rect(rd, 6, 0, 14, 8) testing.expect_value(t, len(rd.masks), 0) testing.expect_value(t, rd.scissor, Raster_Clip{6, 2, 12, 8}) rt_rect(rd, 0, 0, 20, 20, rt_paint(1, 0, 0)) rt_expect(t, &bmp, 6, 2, RED, "the top-left of the intersection") rt_expect(t, &bmp, 11, 7, RED, "the bottom-right of the intersection") rt_expect(t, &bmp, 5, 4, WHITE, "left of the intersection") rt_expect(t, &bmp, 12, 4, WHITE, "right of the intersection") rt_expect(t, &bmp, 8, 1, WHITE, "above the intersection") rt_expect(t, &bmp, 8, 8, WHITE, "below the intersection") testing.expect_value(t, rt_ink(&bmp), 6 * 6) dev_pop_clip(raster_device_as_device(rd)) testing.expect_value(t, rd.scissor, Raster_Clip{2, 2, 12, 12}) dev_pop_clip(raster_device_as_device(rd)) testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 20, 20}) } // A page with /Rotate 90 puts a cos(pi/2) of 6.1e-17 into the CTM. That is // precisely why `matrix_is_rectilinear` is tolerance-based, and it has to keep // the scissor path or every rotated page in existence allocates a plane. @(test) test_rdev_rotated_page_clip_is_a_scissor :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 20, 20) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) // 90 degrees about the origin, then back into view: (x,y) -> (20-y, x). ctm := matrix_mul(rotate_matrix(90), translate_matrix(20, 0)) rt_clip_rect(rd, 2, 3, 6, 5, ctm) testing.expect_value(t, len(rd.masks), 0) testing.expect_value(t, rd.scissor, Raster_Clip{12, 2, 17, 8}) rt_rect(rd, 0, 0, 20, 20, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 5 * 6) rt_expect(t, &bmp, 12, 2, BLACK, "inside the rotated clip") rt_expect(t, &bmp, 11, 2, WHITE, "outside it") } // Anything that is not an axis-aligned rectangle becomes a coverage plane. // A triangle is the smallest shape that proves the plane is consulted rather // than just its bounding box. @(test) test_rdev_non_rectilinear_clip_uses_a_mask :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) p := path_make() defer path_destroy(&p) path_move_to(&p, 0, 0) path_line_to(&p, 16, 0) path_line_to(&p, 0, 16) path_close(&p) dev_clip_path(raster_device_as_device(rd), &p, false, MATRIX_IDENTITY) testing.expect_value(t, len(rd.masks), 1) testing.expect_value(t, rd.masks[0].box, Raster_Clip{0, 0, 16, 16}) rt_rect(rd, 0, 0, 16, 16, rt_paint(1, 0, 0)) rt_expect(t, &bmp, 1, 1, RED, "well inside the triangle") rt_expect(t, &bmp, 14, 14, WHITE, "well outside it, inside its bounding box") rt_expect(t, &bmp, 12, 12, WHITE, "the far side of the hypotenuse, x + y > 16") rt_expect(t, &bmp, 12, 2, RED, "and the near side of it, x + y < 16") // Roughly half the box, which a bounding-box clip would not be. ink := rt_ink(&bmp) testing.expectf(t, ink > 100 && ink < 156, "triangle ink was %d, want about 128", ink) dev_pop_clip(raster_device_as_device(rd)) testing.expect_value(t, len(rd.masks), 0) } // A rotated rectangle is a rectangle to the eye and not to the scissor. @(test) test_rdev_rotated_rect_clip_uses_a_mask :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 32, 32) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) ctm := matrix_mul(rotate_matrix(30), translate_matrix(16, 4)) rt_clip_rect(rd, -6, -6, 12, 12, ctm) testing.expect_value(t, len(rd.masks), 1) rt_rect(rd, 0, 0, 32, 32, rt_paint(0, 0, 0)) rt_expect(t, &bmp, 16, 4, BLACK, "the centre of the rotated square") rt_expect(t, &bmp, 0, 0, WHITE, "a corner of the page it cannot reach") } // Nesting a mask inside a scissor and a scissor inside a mask must both come // out as the intersection, and popping must give back exactly what was there. @(test) test_rdev_mixed_clip_nesting :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) rt_clip_rect(rd, 0, 0, 16, 8) tri := path_make() defer path_destroy(&tri) path_move_to(&tri, 0, 0) path_line_to(&tri, 16, 0) path_line_to(&tri, 0, 16) path_close(&tri) dev_clip_path(dev, &tri, false, MATRIX_IDENTITY) rt_rect(rd, 0, 0, 16, 16, rt_paint(1, 0, 0)) rt_expect(t, &bmp, 1, 1, RED, "inside both") rt_expect(t, &bmp, 1, 9, WHITE, "inside the triangle, below the rectangle") rt_expect(t, &bmp, 14, 6, WHITE, "inside the rectangle, outside the triangle") dev_pop_clip(dev) dev_pop_clip(dev) testing.expect_value(t, len(rd.masks), 0) testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 16, 16}) } // An empty clip is a legal way to hide content and must hide all of it. @(test) test_rdev_empty_clip_paints_nothing :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) rt_clip_rect(rd, 4, 4, 0, 0) rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 0) } // --------------------------------------------------------------------------- // Bounding // --------------------------------------------------------------------------- // "A shape 10^9 units across that touches one pixel costs one pixel." The // assertion with teeth is the ink count: the rasteriser clips every edge to // the clip box and replaces the parts outside with walls, so a path this size // must neither hang nor paint anything it should not. @(test) test_rdev_huge_shape_is_bounded_by_the_clip :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 64, 64) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) rt_clip_rect(rd, 10, 10, 1, 1) rt_rect(rd, -1e9, -1e9, 2e9, 2e9, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 1) rt_expect(t, &bmp, 10, 10, BLACK, "the one pixel the clip allows") } // The same path entirely outside the page must cost nothing and paint nothing. @(test) test_rdev_shape_outside_the_clip_paints_nothing :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 32, 32) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) rt_rect(rd, 1e9, 1e9, 10, 10, rt_paint(0, 0, 0)) rt_rect(rd, -1e9, 4, 10, 10, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 0) } // --------------------------------------------------------------------------- // Transparency groups // --------------------------------------------------------------------------- // A group's alpha applies to the group as a whole. Two overlapping opaque // rectangles inside a 50% group are 50% everywhere, including where they // overlap -- which is the entire reason groups exist. Painting them at 50% // each without a group would darken the overlap to 64. @(test) test_rdev_group_alpha :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 20, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_group(dev, Rect{0, 0, 20, 16}, true, false, .Normal, 0.5, nil) rt_rect(rd, 2, 2, 8, 8, rt_paint(1, 0, 0)) rt_rect(rd, 6, 2, 8, 8, rt_paint(1, 0, 0)) dev_end_group(dev) rt_expect(t, &bmp, 3, 3, {255, 128, 128, 255}, "the first rectangle at the group's alpha") rt_expect(t, &bmp, 7, 3, {255, 128, 128, 255}, "the overlap composites once, not twice") rt_expect(t, &bmp, 12, 3, {255, 128, 128, 255}, "the second rectangle") rt_expect(t, &bmp, 1, 1, WHITE, "outside both") testing.expect_value(t, len(rd.layers), 0) } // The group's bitmap covers only its area, so content outside it is dropped // rather than composited from somewhere else in the buffer. @(test) test_rdev_group_area_bounds_it :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_group(dev, Rect{4, 4, 12, 12}, true, false, .Normal, 1, nil) testing.expect_value(t, len(rd.layers), 1) testing.expect_value(t, rd.layers[0].box, Raster_Clip{4, 4, 12, 12}) rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 1)) dev_end_group(dev) rt_expect(t, &bmp, 4, 4, BLUE, "the group's own corner") rt_expect(t, &bmp, 11, 11, BLUE, "its far corner") rt_expect(t, &bmp, 3, 4, WHITE, "outside the group's area") rt_expect(t, &bmp, 12, 12, WHITE, "past its far corner") testing.expect_value(t, rt_ink(&bmp), 8 * 8) } // A group inherits the clip in force where it was opened. @(test) test_rdev_group_inherits_the_clip :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) rt_clip_rect(rd, 0, 0, 8, 16) dev_begin_group(dev, Rect{0, 0, 16, 16}, true, false, .Normal, 1, nil) rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) dev_end_group(dev) dev_pop_clip(dev) rt_expect(t, &bmp, 7, 8, BLACK, "inside the clip") rt_expect(t, &bmp, 8, 8, WHITE, "outside it") testing.expect_value(t, rt_ink(&bmp), 8 * 16) } // A group blends as a unit: Multiply applied to the group, not to each of its // pieces, so overlapping ink does not multiply twice. @(test) test_rdev_group_blend_mode :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16, {128, 128, 128, 255}) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_group(dev, Rect{0, 0, 16, 16}, true, false, .Multiply, 1, nil) rt_rect(rd, 0, 0, 8, 16, rt_paint(0.5, 0.5, 0.5)) rt_rect(rd, 4, 0, 8, 16, rt_paint(0.5, 0.5, 0.5)) dev_end_group(dev) // 128/255 squared, quantised: the same everywhere the group has ink. want := u8(blend_quantize((128.0 / 255) * (128.0 / 255))) rt_expect(t, &bmp, 1, 1, {want, want, want, 255}, "the left rectangle multiplied once") rt_expect(t, &bmp, 6, 1, {want, want, want, 255}, "the overlap multiplied once, not twice") rt_expect(t, &bmp, 14, 1, {128, 128, 128, 255}, "untouched backdrop") } // A non-isolated group at alpha 1 in Normal mode is seeded with its backdrop, // so a Multiply INSIDE it sees the page underneath. An isolated one does not. @(test) test_rdev_non_isolated_group_sees_the_backdrop :: proc(t: ^testing.T) { for isolated in ([2]bool{false, true}) { bmp := rt_bitmap(t, 8, 8, {128, 128, 128, 255}) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_group(dev, Rect{0, 0, 8, 8}, isolated, false, .Normal, 1, nil) p := rt_paint(0.5, 0.5, 0.5) p.blend = .Multiply rt_rect(rd, 0, 0, 8, 8, p) dev_end_group(dev) got := rt_px(&bmp, 4, 4) if isolated { // Nothing to multiply with, so the source passes through. rt_expect(t, &bmp, 4, 4, {128, 128, 128, 255}, "isolated group has no backdrop") } else { want := u8(blend_quantize((128.0 / 255) * (128.0 / 255))) testing.expectf(t, got[0] == want, "non-isolated group: got %v, want %d", got, want) } } } // --------------------------------------------------------------------------- // Soft masks // --------------------------------------------------------------------------- // A luminosity mask: what the mask's form paints white shows, what it leaves // bare is masked out by the black backdrop. @(test) test_rdev_soft_mask_luminosity :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) rt_rect(rd, 0, 0, 8, 16, rt_paint(1, 1, 1)) sm: Soft_Mask dev_end_mask(dev, &sm, nil) installed := rt_smask(rd) testing.expect(t, installed != nil, "end_mask must install a mask") testing.expect_value(t, installed.plane.outside, u8(0)) p := rt_paint(1, 0, 0) p.soft_mask = &sm rt_rect(rd, 0, 0, 16, 16, p) rt_expect(t, &bmp, 3, 3, RED, "under the white part of the mask") rt_expect(t, &bmp, 12, 3, WHITE, "under the part the mask left black") testing.expect_value(t, rt_ink(&bmp), 8 * 16) } // An alpha mask takes the group's alpha and ignores its colour: black at half // alpha masks to half, exactly as white at half alpha would. @(test) test_rdev_soft_mask_alpha :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_mask(dev, Rect{0, 0, 8, 8}, false, rt_paint(0, 0, 0)) rt_rect(rd, 0, 0, 4, 8, rt_paint(0, 0, 0, 0.5)) sm: Soft_Mask dev_end_mask(dev, &sm, nil) p := rt_paint(1, 0, 0) p.soft_mask = &sm rt_rect(rd, 0, 0, 8, 8, p) rt_expect_near(t, &bmp, 1, 1, {255, 127, 127, 255}, 2, "half-alpha region of the mask") rt_expect(t, &bmp, 6, 1, WHITE, "the region the mask never covered") } // The mask multiplies coverage rather than replacing it, so a half-covered // pixel under a half mask is a quarter of the ink. @(test) test_rdev_soft_mask_multiplies_coverage :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_mask(dev, Rect{0, 0, 8, 8}, false, rt_paint(0, 0, 0)) rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0, 0.5)) sm: Soft_Mask dev_end_mask(dev, &sm, nil) p := rt_paint(0, 0, 0) p.soft_mask = &sm // Half a pixel wide, so coverage is 0.5 and the mask another 0.5. rt_rect(rd, 2, 0, 0.5, 8, p) rt_expect_near(t, &bmp, 2, 4, {191, 191, 191, 255}, 3, "a quarter of full black") } // The mask applies only to paints that carry one. That is what makes // "/SMask /None", and the `Q` that restores it, work without the device // modelling the graphics state. @(test) test_rdev_soft_mask_only_applies_when_the_paint_has_one :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_mask(dev, Rect{0, 0, 8, 8}, true, rt_paint(0, 0, 0)) sm: Soft_Mask dev_end_mask(dev, &sm, nil) // an empty mask: everything is masked out masked := rt_paint(1, 0, 0) masked.soft_mask = &sm rt_rect(rd, 0, 0, 4, 8, masked) testing.expect_value(t, rt_ink(&bmp), 0) rt_rect(rd, 0, 0, 4, 8, rt_paint(1, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 4 * 8) } // The mask's own form is not subject to the clip that was in force where the // ExtGState installed it, nor to any mask already in place. @(test) test_rdev_soft_mask_form_ignores_the_current_clip :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) rt_clip_rect(rd, 0, 0, 4, 16) dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) testing.expect(t, rd.smask_suspended, "a mask must not mask itself") rt_rect(rd, 0, 0, 16, 16, rt_paint(1, 1, 1)) sm: Soft_Mask dev_end_mask(dev, &sm, nil) testing.expect(t, !rd.smask_suspended, "end_mask re-arms masking") dev_pop_clip(dev) p := rt_paint(1, 0, 0) p.soft_mask = &sm rt_rect(rd, 0, 0, 16, 16, p) // The mask covered the whole page, not just the clip that was in force. testing.expect_value(t, rt_ink(&bmp), 16 * 16) } // --------------------------------------------------------------------------- // Images // --------------------------------------------------------------------------- // A 2x2 picture: red and green on the top row, blue and black on the bottom. @(private = "file") RT_RGB2 :: "\xff\x00\x00\x00\xff\x00" + "\x00\x00\xff\x00\x00\x00" @(private = "file") rt_image :: proc(t: ^testing.T, doc: ^Document, dict_src, payload: string, loc := #caller_location) -> ^Image { img, err := image_load(doc, cf_stream(doc, dict_src, transmute([]byte)payload), nil) testing.expectf(t, err == .None, "image_load: %v", err, loc = loc) return img } // The upright placement: the unit square flipped in y, which is what a page's // base matrix does to every image on it. @(private = "file") rt_place :: proc(size: f64) -> Matrix { return Matrix{size, 0, 0, -size, 0, size} } // An image covers the unit square of the CTM. Under a plain scale its four // source pixels land in the four quadrants, top row first. @(test) test_rdev_image_scaled :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := rt_image(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev_fill_image(raster_device_as_device(rd), img, rt_place(8), rt_paint(0, 0, 0)) rt_expect(t, &bmp, 0, 0, RED, "image pixel (0,0) is the top-left") rt_expect(t, &bmp, 7, 0, GREEN, "image pixel (1,0) is the top-right") rt_expect(t, &bmp, 0, 7, BLUE, "image pixel (0,1) is the bottom-left") rt_expect(t, &bmp, 7, 7, BLACK, "image pixel (1,1) is the bottom-right") testing.expect_value(t, rt_ink(&bmp), 8 * 8) } // Without the y flip the image is upside down, which is the whole difference // between PDF's y-up user space and the y-down bitmap. @(test) test_rdev_image_flipped :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := rt_image(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev_fill_image(raster_device_as_device(rd), img, scale_matrix(8, 8), rt_paint(0, 0, 0)) rt_expect(t, &bmp, 0, 7, RED, "the top row is now at the bottom") rt_expect(t, &bmp, 7, 7, GREEN, "") rt_expect(t, &bmp, 0, 0, BLUE, "and the bottom row at the top") rt_expect(t, &bmp, 7, 0, BLACK, "") } // Rotated by 90 degrees the four pixels move round one quadrant, and the // magnification is deliberately kept under the smoothing threshold so the // corners are exact source pixels rather than blends. @(test) test_rdev_image_rotated :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := rt_image(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) bmp := rt_bitmap(t, 6, 6) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) // (x, y) -> (6 - y, x) after the upright placement. ctm := matrix_mul(rt_place(6), matrix_mul(rotate_matrix(90), translate_matrix(6, 0))) dev_fill_image(raster_device_as_device(rd), img, ctm, rt_paint(0, 0, 0)) rt_expect(t, &bmp, 5, 0, RED, "the top-left pixel rotated to the top-right") rt_expect(t, &bmp, 5, 5, GREEN, "") rt_expect(t, &bmp, 0, 0, BLUE, "") rt_expect(t, &bmp, 0, 5, BLACK, "") testing.expect_value(t, rt_ink(&bmp), 6 * 6) } // Minification averages. A source of alternating white and black rows squeezed // four to one must come out mid-grey; point-sampling it would come out solid // white or solid black, which is what makes a downscaled photograph look // broken. @(test) test_rdev_image_minified_is_averaged :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // 4x4 DeviceGray, rows 255 0 255 0. rows := "\xff\xff\xff\xff" + "\x00\x00\x00\x00" + "\xff\xff\xff\xff" + "\x00\x00\x00\x00" img := rt_image(t, doc, "<< /Width 4 /Height 4 /BitsPerComponent 8 /ColorSpace /DeviceGray >>", rows) bmp := rt_bitmap(t, 2, 2) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev_fill_image(raster_device_as_device(rd), img, rt_place(2), rt_paint(0, 0, 0)) for y in 0 ..< 2 { for x in 0 ..< 2 { rt_expect_near(t, &bmp, x, y, {128, 128, 128, 255}, 1, "averaged 2x2 block") } } } // /Interpolate asks for smoothing at any magnification; without it a modest // blow-up stays blocky, which is what the format specifies. @(test) test_rdev_image_interpolate_flag :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) for interpolate in ([2]bool{false, true}) { src := "<< /Width 2 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceGray >>" if interpolate { src = "<< /Width 2 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceGray /Interpolate true >>" } img := rt_image(t, doc, src, "\x00\xff") bmp := rt_bitmap(t, 6, 2) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev_fill_image(raster_device_as_device(rd), img, Matrix{6, 0, 0, -2, 0, 2}, rt_paint(0, 0, 0)) // The pixel straddling the two source samples: a hard edge without // interpolation, a blend with it. got := rt_px(&bmp, 3, 0) if interpolate { testing.expectf(t, got[0] > 128 && got[0] < 255, "/Interpolate must smooth the join, got %v", got) } else { testing.expectf(t, got[0] == 255, "no /Interpolate must keep it hard, got %v", got) } } } // An image mask is a stencil painted in the current fill colour. @(test) test_rdev_image_mask_paints_the_fill_colour :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // 2x2, one bit per pixel, one byte per row. A zero sample paints. img := rt_image(t, doc, "<< /Width 2 /Height 2 /ImageMask true /BitsPerComponent 1 >>", "\x40\x80") bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev_fill_image_mask(raster_device_as_device(rd), img, rt_place(8), rt_paint(0, 0, 1)) rt_expect(t, &bmp, 0, 0, BLUE, "sample 0 paints in the fill colour") rt_expect(t, &bmp, 7, 0, WHITE, "sample 1 paints nothing") rt_expect(t, &bmp, 0, 7, WHITE, "") rt_expect(t, &bmp, 7, 7, BLUE, "") } // The same stencil used as a clip: what it would have painted is what shows. @(test) test_rdev_clip_image_mask :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := rt_image(t, doc, "<< /Width 2 /Height 2 /ImageMask true /BitsPerComponent 1 >>", "\x40\x80") bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_clip_image_mask(dev, img, rt_place(8)) testing.expect_value(t, len(rd.masks), 1) rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0)) dev_pop_clip(dev) rt_expect(t, &bmp, 0, 0, RED, "the stencil's set quadrant") rt_expect(t, &bmp, 7, 0, WHITE, "its clear quadrant") rt_expect(t, &bmp, 7, 7, RED, "") testing.expect_value(t, len(rd.masks), 0) } // An image is clipped like anything else, and its constant alpha applies. @(test) test_rdev_image_clipped_and_alpha :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := rt_image(t, doc, "<< /Width 1 /Height 1 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", "\xff\x00\x00") bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) rt_clip_rect(rd, 0, 0, 4, 8) dev_fill_image(raster_device_as_device(rd), img, rt_place(8), rt_paint(0, 0, 0, 0.5)) rt_expect(t, &bmp, 1, 1, {255, 128, 128, 255}, "half-alpha image inside the clip") rt_expect(t, &bmp, 5, 1, WHITE, "outside the clip") } // An image under a degenerate CTM paints nothing rather than dividing by zero. @(test) test_rdev_image_degenerate_ctm :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := rt_image(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_fill_image(dev, img, scale_matrix(0, 8), rt_paint(0, 0, 0)) dev_fill_image(dev, img, Matrix{}, rt_paint(0, 0, 0)) inf := max(f64) dev_fill_image(dev, img, Matrix{inf * 2, 0, 0, 8, 0, 0}, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 0) } // --------------------------------------------------------------------------- // Shadings and patterns // --------------------------------------------------------------------------- @(private = "file") RT_AXIAL :: `<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 16 0] /Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> /Extend [true true] >>` // `sh` fills the current clip with the shading. The ramp must run the right // way and must stop at the clip. @(test) test_rdev_fill_shade :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) sh, err := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) testing.expectf(t, err == .None, "shading_load: %v", err) bmp := rt_bitmap(t, 16, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev_fill_shade(raster_device_as_device(rd), sh, MATRIX_IDENTITY, rt_paint(0, 0, 0)) left := rt_px(&bmp, 0, 4) right := rt_px(&bmp, 15, 4) testing.expectf(t, left[0] < 24, "the dark end of the ramp: %v", left) testing.expectf(t, right[0] > 231, "the light end of the ramp: %v", right) prev := -1 for x in 0 ..< 16 { v := int(rt_px(&bmp, x, 4)[0]) testing.expectf(t, v > prev, "the ramp must increase at x=%d: %d after %d", x, v, prev) prev = v } } // A shading is bounded by the clip, including by a non-rectangular one, which // is the path that materialises a full-target mask plane. @(test) test_rdev_fill_shade_through_a_mask_clip :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) sh, err := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) testing.expectf(t, err == .None, "shading_load: %v", err) bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) tri := path_make() defer path_destroy(&tri) path_move_to(&tri, 0, 0) path_line_to(&tri, 16, 0) path_line_to(&tri, 0, 16) path_close(&tri) dev_clip_path(dev, &tri, false, MATRIX_IDENTITY) testing.expect_value(t, len(rd.masks), 1) dev_fill_shade(dev, sh, MATRIX_IDENTITY, rt_paint(0, 0, 0)) dev_pop_clip(dev) testing.expect(t, rt_px(&bmp, 12, 2) != WHITE, "inside the clip the shading paints") rt_expect(t, &bmp, 12, 12, WHITE, "outside it nothing does") } // A shading pattern named on a fill paints through the shape. The interpreter // normally resolves patterns itself; this is the device's fallback for one // that arrives on a Paint. @(test) test_rdev_shading_pattern_fill :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) src := "<< /PatternType 2 /Shading " + RT_AXIAL + " >>" pat, err := pattern_load(doc, cf_parse(doc, src), nil) testing.expectf(t, err == .None, "pattern_load: %v", err) bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) p := rt_paint(0, 0, 0) p.pattern = pat rt_rect(rd, 4, 4, 8, 8, p) testing.expect(t, rt_px(&bmp, 5, 5) != WHITE, "the shape is painted with the shading") testing.expect(t, rt_px(&bmp, 10, 5)[0] > rt_px(&bmp, 5, 5)[0], "and the ramp runs across it") rt_expect(t, &bmp, 2, 5, WHITE, "outside the shape nothing is painted") rt_expect(t, &bmp, 13, 5, WHITE, "") testing.expect_value(t, len(rd.clips), 0) testing.expect_value(t, len(rd.masks), 0) } // A tiling pattern cannot be replayed by the device, so it paints nothing -- // but it must also leave the clip stack exactly as it found it. @(test) test_rdev_tiling_pattern_on_a_paint_is_left_alone :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) src := `<< /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 4 4] /XStep 4 /YStep 4 /Resources << >> /Length 0 >>` pat, err := pattern_load(doc, cf_stream(doc, src, nil), nil) testing.expectf(t, err == .None, "pattern_load: %v", err) bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) p := rt_paint(0, 0, 0) p.pattern = pat rt_rect(rd, 0, 0, 16, 16, p) testing.expect_value(t, rt_ink(&bmp), 0) testing.expect_value(t, len(rd.clips), 0) } // begin_tile reports how many cells meet the area and clips to it; end_tile // gives the clip back. @(test) test_rdev_begin_tile_counts_cells :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 32, 32) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) // Cells of 4 covering 0..16 meet indices -1..4 in each axis: 6 x 6. n := dev_begin_tile(dev, Rect{0, 0, 16, 16}, Rect{0, 0, 4, 4}, 4, 4, MATRIX_IDENTITY) testing.expect_value(t, n, 36) testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 16, 16}) dev_end_tile(dev) testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 32, 32}) // A degenerate step is not a lattice at all. testing.expect_value(t, dev_begin_tile(dev, Rect{0, 0, 16, 16}, Rect{0, 0, 4, 4}, 0, 4, MATRIX_IDENTITY), 0) dev_end_tile(dev) // A singular matrix collapses every cell. testing.expect_value(t, dev_begin_tile(dev, Rect{0, 0, 16, 16}, Rect{0, 0, 4, 4}, 4, 4, Matrix{}), 0) dev_end_tile(dev) // A step small enough to make one fill cost a million content streams is // capped rather than reported honestly. testing.expect_value(t, dev_begin_tile(dev, Rect{0, 0, 32, 32}, Rect{0, 0, 1, 1}, 1e-6, 1e-6, MATRIX_IDENTITY), MAX_PATTERN_TILES) dev_end_tile(dev) testing.expect_value(t, len(rd.clips), 0) } // --------------------------------------------------------------------------- // Text // // A standard-14 font has metrics but no program, so it draws nothing by // design. Testing the text hooks needs a font that really has an outline, and // the smallest one that does is a TrueType with a single square glyph. // --------------------------------------------------------------------------- // RT_GLYF is one simple glyph: a square from (100,0) to (900,700) in a // 1000-unit em. One contour, four on-curve points, 16-bit deltas. @(private = "file") RT_GLYF := [?]u8{ 0x00, 0x01, // numberOfContours 0x00, 0x64, 0x00, 0x00, 0x03, 0x84, 0x02, 0xBC, // xMin yMin xMax yMax 0x00, 0x03, // endPtsOfContours[0] 0x00, 0x00, // instructionLength 0x01, 0x01, 0x01, 0x01, // flags: on-curve, long deltas 0x00, 0x64, 0x03, 0x20, 0x00, 0x00, 0xFC, 0xE0, // x: 100 800 0 -800 0x00, 0x00, 0x00, 0x00, 0x02, 0xBC, 0x00, 0x00, // y: 0 0 700 0 } @(private = "file") rt_be16 :: proc(b: []byte, off: int, v: u16) { b[off] = u8(v >> 8) b[off + 1] = u8(v) } @(private = "file") rt_be32 :: proc(b: []byte, off: int, v: u32) { b[off] = u8(v >> 24); b[off + 1] = u8(v >> 16) b[off + 2] = u8(v >> 8); b[off + 3] = u8(v) } // rt_square_ttf assembles head, maxp, loca and glyf around RT_GLYF. Glyph 1 is // the square; glyph 0 is the empty .notdef every font must have. @(private = "file") rt_square_ttf :: proc(doc: ^Document) -> []byte { head := make([]byte, 54, doc.allocator) rt_be16(head, 18, 1000) // unitsPerEm; indexToLocFormat stays 0 maxp := make([]byte, 32, doc.allocator) rt_be32(maxp, 0, 0x00010000) rt_be16(maxp, 4, 2) loca := make([]byte, 6, doc.allocator) rt_be16(loca, 0, 0) rt_be16(loca, 2, 0) rt_be16(loca, 4, u16(len(RT_GLYF) / 2)) tags := [4]string{"head", "maxp", "loca", "glyf"} datas := [4][]byte{head, maxp, loca, RT_GLYF[:]} total := 12 + 16 * len(tags) for d in datas { total += (len(d) + 3) & ~int(3) } out := make([]byte, total, doc.allocator) rt_be32(out, 0, 0x00010000) rt_be16(out, 4, u16(len(tags))) pos := 12 + 16 * len(tags) for tag, i in tags { rec := 12 + 16 * i copy(out[rec:rec + 4], transmute([]byte)tag) rt_be32(out, rec + 8, u32(pos)) rt_be32(out, rec + 12, u32(len(datas[i]))) copy(out[pos:], datas[i]) pos += (len(datas[i]) + 3) & ~int(3) } return out } @(private = "file") rt_square_font :: proc(t: ^testing.T, doc: ^Document, loc := #caller_location) -> ^Font { d, _ := to_dict(cf_parse(doc, "<< /Type /Font /Subtype /TrueType /BaseFont /Sq /FirstChar 65 /LastChar 65 /Widths [800] >>")) desc, _ := to_dict(cf_parse(doc, "<< /Flags 4 >>")) dict_set(desc, "FontFile2", cf_stream(doc, "<< >>", rt_square_ttf(doc))) dict_set(d, "FontDescriptor", desc) f, err := font_load(doc, d) testing.expectf(t, err == .None, "font_load: %v", err, loc = loc) return f } // rt_square_text is the square glyph at 24pt with its origin at (4, 26) and // the page's y flip, so its ink is x 6.4..25.6 and y 9.2..26. @(private = "file") rt_square_text :: proc(f: ^Font, place := MATRIX_IDENTITY) -> Text { txt := text_make() txt.font = f base := matrix_mul(scale_matrix(24, -24), translate_matrix(4, 26)) trm := matrix_mul(font_matrix(f), matrix_mul(place, base)) append(&txt.items, Text_Item{gid = 1, code = 65, trm = trm, advance = 0.8}) return txt } // RT_TILT rotates the glyph 20 degrees about its origin, which is what makes // its outline stop being an axis-aligned rectangle. Applied before the // placement, so the ink stays inside a 32x32 page. @(private = "file") rt_tilt :: proc() -> Matrix { return matrix_mul(rotate_matrix(20), translate_matrix(-0.08, -0.1)) } // The glyph lands where `trm` puts it, to the pixel, and nowhere else. `trm` // is glyph space to device space complete, so the CTM argument is not applied // a second time. @(test) test_rdev_fill_text :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) font := rt_square_font(t, doc) bmp := rt_bitmap(t, 32, 32) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) txt := rt_square_text(font) defer text_destroy(&txt) dev_fill_text(raster_device_as_device(rd), &txt, MATRIX_IDENTITY, rt_paint(0, 0, 0)) rt_expect(t, &bmp, 10, 15, BLACK, "inside the glyph's square") rt_expect(t, &bmp, 24, 25, BLACK, "its bottom-right corner") rt_expect(t, &bmp, 5, 15, WHITE, "left of it") rt_expect(t, &bmp, 26, 15, WHITE, "right of it") rt_expect(t, &bmp, 10, 8, WHITE, "above it") rt_expect(t, &bmp, 10, 26, WHITE, "below the baseline") ink := rt_ink(&bmp) testing.expectf(t, ink > 300 && ink < 400, "the square covers about 340 pixels, got %d", ink) } // Stroked text outlines the glyph instead of filling it: the middle stays // white and the edges do not. @(test) test_rdev_stroke_text :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) font := rt_square_font(t, doc) bmp := rt_bitmap(t, 32, 32) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) txt := rt_square_text(font) defer text_destroy(&txt) st := STROKE_STATE_DEFAULT st.line_width = 2 dev_stroke_text(raster_device_as_device(rd), &txt, &st, MATRIX_IDENTITY, rt_paint(0, 0, 0)) rt_expect(t, &bmp, 16, 17, WHITE, "the inside of a stroked glyph is not filled") testing.expect(t, rt_px(&bmp, 16, 25) != WHITE, "its bottom edge is inked") testing.expect(t, rt_px(&bmp, 16, 10) != WHITE, "its top edge is inked") testing.expect(t, rt_px(&bmp, 7, 17) != WHITE, "its left edge is inked") // The pen is 2 units wide in user space; a fill would be four times the ink. ink := rt_ink(&bmp) testing.expectf(t, ink > 60 && ink < 200, "a stroked square outline, got %d pixels", ink) } // A glyph used as a clip covers the same pixels as filling it does. The // square glyph is deliberately tilted first: an upright one really is an // axis-aligned rectangle and takes the scissor path, which is correct and is // asserted below, but it would not exercise the coverage plane. @(test) test_rdev_clip_text :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) font := rt_square_font(t, doc) filled := rt_bitmap(t, 32, 32) defer bitmap_destroy(&filled) clipped := rt_bitmap(t, 32, 32) defer bitmap_destroy(&clipped) txt := rt_square_text(font, rt_tilt()) defer text_destroy(&txt) rd1 := rt_dev(t, &filled, doc) defer raster_device_destroy(rd1) dev_fill_text(raster_device_as_device(rd1), &txt, MATRIX_IDENTITY, rt_paint(0, 0, 0)) testing.expect(t, rt_ink(&filled) > 250, "the tilted glyph must land on the page") rd2 := rt_dev(t, &clipped, doc) defer raster_device_destroy(rd2) dev2 := raster_device_as_device(rd2) dev_clip_text(dev2, &txt, MATRIX_IDENTITY) testing.expect_value(t, len(rd2.masks), 1) rt_rect(rd2, 0, 0, 32, 32, rt_paint(0, 0, 0)) dev_pop_clip(dev2) testing.expect_value(t, len(rd2.masks), 0) diff := 0 for y in 0 ..< 32 { for x in 0 ..< 32 { if (rt_px(&filled, x, y) != WHITE) != (rt_px(&clipped, x, y) != WHITE) { diff += 1 } } } testing.expectf(t, diff == 0, "%d pixels differ between filling and clipping the glyph", diff) // An upright rectangular glyph is a rectangle, and costs no plane. upright := rt_square_text(font) defer text_destroy(&upright) rd3 := rt_dev(t, &clipped, doc) defer raster_device_destroy(rd3) dev_clip_text(raster_device_as_device(rd3), &upright, MATRIX_IDENTITY) testing.expect_value(t, len(rd3.masks), 0) testing.expect_value(t, rd3.scissor, Raster_Clip{6, 9, 26, 26}) } // A text object with no font, no items, or a font whose glyphs are content // streams must not draw and must not crash. @(test) test_rdev_text_degenerate :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) empty := text_make() defer text_destroy(&empty) dev_fill_text(dev, &empty, MATRIX_IDENTITY, rt_paint(0, 0, 0)) dev_stroke_text(dev, &empty, nil, MATRIX_IDENTITY, rt_paint(0, 0, 0)) dev_fill_text(dev, nil, MATRIX_IDENTITY, rt_paint(0, 0, 0)) dev_ignore_text(dev, &empty, MATRIX_IDENTITY) testing.expect_value(t, rt_ink(&bmp), 0) // A clip from a text object with nothing in it hides everything, which is // what render mode 7 with no drawable glyph means. dev_clip_text(dev, &empty, MATRIX_IDENTITY) rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 0) dev_pop_clip(dev) testing.expect_value(t, len(rd.clips), 0) } // --------------------------------------------------------------------------- // Colour, blend modes and unusual targets // --------------------------------------------------------------------------- // The blend mode on the Paint reaches `blend_over` unchanged. @(test) test_rdev_blend_mode_on_a_fill :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8, {128, 128, 128, 255}) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) p := rt_paint(0.5, 0.5, 0.5) p.blend = .Multiply rt_rect(rd, 0, 0, 4, 8, p) want := u8(blend_quantize((128.0 / 255) * (128.0 / 255))) rt_expect(t, &bmp, 1, 1, {want, want, want, 255}, "Multiply against the backdrop") rt_expect(t, &bmp, 6, 1, {128, 128, 128, 255}, "untouched") } // A /None separation marks nothing at all on the page (8.6.6.4). @(test) test_rdev_none_colorspace_paints_nothing :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) cs := Colorspace{kind = .Separation, n = 1, is_none = true} p := rt_paint(0, 0, 0) p.cs = &cs rt_rect(rd, 0, 0, 8, 8, p) testing.expect_value(t, rt_ink(&bmp), 0) } // A greyscale target has no alpha channel at all. Colour is reduced to // luminosity by `blend_source_for`, and a group over it still has to work. @(test) test_rdev_grey_target :: proc(t: ^testing.T) { bmp, err := bitmap_make(8, 8, 1) testing.expect_value(t, err, Error.None) defer bitmap_destroy(&bmp) bitmap_clear(&bmp, 255) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, 0.5, nil) rt_rect(rd, 0, 0, 4, 8, rt_paint(0, 0, 0)) dev_end_group(dev) off, _ := bitmap_pixel_offset(&bmp, 1, 1) testing.expectf(t, bmp.pixels[off] == 128, "black at half through a group: got %d", bmp.pixels[off]) off2, _ := bitmap_pixel_offset(&bmp, 6, 1) testing.expect_value(t, bmp.pixels[off2], u8(255)) } // A soft mask's /TR is honoured, including outside the mask's own area, where // the value is the transfer of the backdrop's luminosity. @(test) test_rdev_soft_mask_transfer_function :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // An inverting transfer: 1 - x. fn, err := function_load(doc, cf_parse(doc, "<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>")) testing.expectf(t, err == .None, "function_load: %v", err) bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_mask(dev, Rect{0, 0, 16, 8}, true, rt_paint(0, 0, 0)) rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 1, 1)) sm: Soft_Mask dev_end_mask(dev, &sm, fn) installed := rt_smask(rd) testing.expect(t, installed != nil, "end_mask must install a mask") testing.expect_value(t, installed.plane.outside, u8(255)) p := rt_paint(1, 0, 0) p.soft_mask = &sm rt_rect(rd, 0, 0, 16, 16, p) rt_expect(t, &bmp, 3, 3, WHITE, "the inverted white part of the mask hides") rt_expect(t, &bmp, 12, 3, RED, "the inverted black part shows") rt_expect(t, &bmp, 3, 12, RED, "and outside the mask's area the backdrop inverts to 1") } // --------------------------------------------------------------------------- // Robustness // --------------------------------------------------------------------------- // Nothing may be assumed about the arguments. Every hook has to survive nil // geometry, nil resources and a device with no Document. @(test) test_rdev_nil_arguments :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) st := STROKE_STATE_DEFAULT paint := rt_paint(0, 0, 0) dev_fill_path(dev, nil, false, MATRIX_IDENTITY, paint) dev_stroke_path(dev, nil, &st, MATRIX_IDENTITY, paint) dev_fill_image(dev, nil, MATRIX_IDENTITY, paint) dev_fill_image_mask(dev, nil, MATRIX_IDENTITY, paint) dev_fill_shade(dev, nil, MATRIX_IDENTITY, paint) dev_fill_text(dev, nil, MATRIX_IDENTITY, paint) dev_stroke_text(dev, nil, &st, MATRIX_IDENTITY, paint) // Clip hooks must still push, so the interpreter's pops stay balanced. dev_clip_path(dev, nil, false, MATRIX_IDENTITY) dev_clip_stroke_path(dev, nil, &st, MATRIX_IDENTITY) dev_clip_image_mask(dev, nil, MATRIX_IDENTITY) testing.expect_value(t, len(rd.clips), 3) dev_pop_clip(dev) dev_pop_clip(dev) dev_pop_clip(dev) testing.expect_value(t, len(rd.clips), 0) testing.expect_value(t, rt_ink(&bmp), 0) testing.expect(t, raster_device_as_device(nil) == nil, "no device from no device") raster_device_destroy(nil) } // A NaN or an infinity in the CTM must not reach the geometry, and a clip // built from one must hide rather than reveal. @(test) test_rdev_non_finite_matrices :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) nan := math_nan() inf := max(f64) * 2 for m in ([3]Matrix{{nan, 0, 0, 1, 0, 0}, {1, 0, 0, 1, inf, 0}, {inf, inf, inf, inf, inf, inf}}) { rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0), m) } testing.expect_value(t, rt_ink(&bmp), 0) rt_clip_rect(rd, 0, 0, 8, 8, Matrix{nan, 0, 0, 1, 0, 0}) rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 0) dev_pop_clip(dev) rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 64) } @(private = "file") math_nan :: proc() -> f64 { z := 0.0 return z / z } // More pops than pushes must not underflow, and the clip must still be the // page afterwards. @(test) test_rdev_unbalanced_pops :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) for _ in 0 ..< 5 { dev_pop_clip(dev) } testing.expect_value(t, len(rd.clips), 0) testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 8, 8}) dev_end_group(dev) dev_end_mask(dev, nil, nil) dev_end_tile(dev) testing.expect_value(t, len(rd.layers), 0) rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 64) } // A content stream that opens groups without closing them must be bounded. // Past the cap the layers are recorded but paint nothing, and every one of // them still accepts its end_group. @(test) test_rdev_layer_depth_is_capped :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) n := RDEV_MAX_LAYERS + 8 for _ in 0 ..< n { dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, 1, nil) } testing.expect_value(t, len(rd.layers), n) testing.expect_value(t, rd.suppress, 8) rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) for _ in 0 ..< n { dev_end_group(dev) } testing.expect_value(t, len(rd.layers), 0) testing.expect_value(t, rd.suppress, 0) // Everything drawn inside the suppressed layers is dropped; the page is // still usable afterwards. testing.expect_value(t, rt_ink(&bmp), 0) rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 64) } // A group whose area is empty, or entirely off the page, costs no bitmap and // still balances. @(test) test_rdev_degenerate_group_areas :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) for area in ([3]Rect{{4, 4, 4, 4}, {100, 100, 200, 200}, RECT_EMPTY}) { dev_begin_group(dev, area, true, false, .Normal, 1, nil) testing.expect(t, !rd.layers[0].ok, "an empty group needs no bitmap") rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) dev_end_group(dev) testing.expect_value(t, len(rd.layers), 0) testing.expect_value(t, rt_ink(&bmp), 0) } // RECT_INFINITE means "the whole clip", which is a real group. dev_begin_group(dev, RECT_INFINITE, true, false, .Normal, 1, nil) testing.expect(t, rd.layers[0].ok, "an infinite area is the clip, not nothing") testing.expect_value(t, rd.layers[0].box, Raster_Clip{0, 0, 8, 8}) dev_end_group(dev) } // close() unwinds whatever the content stream left open: the group's ink still // reaches the page and the clip goes back to the whole of it. @(test) test_rdev_close_unwinds :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) rt_clip_rect(rd, 0, 0, 4, 8) dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, 1, nil) rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 0, 0)) dev_begin_mask(dev, Rect{0, 0, 8, 8}, true, rt_paint(0, 0, 0)) dev_close(dev) testing.expect_value(t, len(rd.layers), 0) testing.expect_value(t, len(rd.clips), 0) testing.expect_value(t, len(rd.masks), 0) testing.expect(t, rt_smask(rd) == nil, "close drops every soft mask") testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 8, 8}) testing.expect_value(t, rt_ink(&bmp), 4 * 8) } // The device refuses a bitmap it cannot paint into rather than taking one. @(test) test_rdev_make_rejects_bad_targets :: proc(t: ^testing.T) { rd, err := raster_device_make(nil, Raster_Clip{0, 0, 8, 8}) testing.expect_value(t, err, Error.Invalid_Argument) testing.expect(t, rd == nil, "no device from no bitmap") empty: Bitmap rd2, err2 := raster_device_make(&empty, Raster_Clip{0, 0, 8, 8}) testing.expect_value(t, err2, Error.Invalid_Argument) testing.expect(t, rd2 == nil, "no device from an unallocated bitmap") } // The base clip is the page, whatever the caller asks for, and a clip pushed // outside it cannot widen it. @(test) test_rdev_base_clip_bounds_everything :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd, err := raster_device_make(&bmp, Raster_Clip{-100, -100, 1000, 4}) testing.expect_value(t, err, Error.None) defer raster_device_destroy(rd) testing.expect_value(t, rd.base_clip, Raster_Clip{0, 0, 16, 4}) rt_clip_rect(rd, -50, -50, 500, 500) testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 16, 4}) rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp), 16 * 4) } // Images and shadings need the Document. Without one they are skipped, not // crashed through. @(test) test_rdev_without_a_document :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := rt_image(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) sh, _ := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) // deliberately no Document defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_fill_image(dev, img, rt_place(8), rt_paint(0, 0, 0)) dev_fill_shade(dev, sh, MATRIX_IDENTITY, rt_paint(0, 0, 0)) dev_clip_image_mask(dev, img, rt_place(8)) dev_pop_clip(dev) testing.expect_value(t, rt_ink(&bmp), 0) } // --------------------------------------------------------------------------- // The clip stack's exact behaviour // --------------------------------------------------------------------------- // A rectangular clip becomes a scissor rounded to the NEAREST pixel boundary. // Rounding outward instead would leak up to a pixel of content past every clip // edge in every content stream, which is exactly the artefact this pins. @(test) test_rdev_rect_clip_rounds_to_the_nearest_pixel :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) // 2.6 .. 9.4 across, 1.4 .. 6.6 down. rt_clip_rect(rd, 2.6, 1.4, 6.8, 5.2) testing.expect_value(t, rd.scissor, Raster_Clip{3, 1, 9, 7}) rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) rt_expect(t, &bmp, 3, 1, BLACK, "the first pixel the clip keeps") rt_expect(t, &bmp, 2, 1, WHITE, "the pixel the edge at 2.6 mostly misses") rt_expect(t, &bmp, 8, 6, BLACK, "the last pixel it keeps") rt_expect(t, &bmp, 9, 6, WHITE, "and the one past the edge at 9.4") testing.expect_value(t, rt_ink(&bmp), 6 * 6) } // A coverage plane really is coverage: the pixel the hypotenuse cuts in half // gets half the ink, not all of it and not none. @(test) test_rdev_clip_plane_is_anti_aliased :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) tri := path_make() defer path_destroy(&tri) path_move_to(&tri, 0, 0) path_line_to(&tri, 16, 0) path_line_to(&tri, 0, 16) path_close(&tri) dev_clip_path(raster_device_as_device(rd), &tri, false, MATRIX_IDENTITY) rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) // Pixel (7,8) is cut corner to corner by x + y = 16: half in, half out. got := rt_px(&bmp, 7, 8) testing.expectf(t, got[0] > 100 && got[0] < 155, "the half-covered clip edge should be mid-grey, got %v", got) rt_expect(t, &bmp, 6, 8, BLACK, "the pixel fully inside it") rt_expect(t, &bmp, 8, 8, WHITE, "the pixel fully outside it") } // The group's bitmap starts at the group's own corner, and its contents have // to be shifted into it. Getting that wrong moves everything inside a group by // the group's origin, which is the kind of bug that only shows on pages whose // groups are not anchored at (0,0). @(test) test_rdev_group_content_keeps_its_place :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_group(dev, Rect{4, 4, 12, 12}, true, false, .Normal, 1, nil) rt_rect(rd, 4, 4, 4, 4, rt_paint(0, 0, 1)) dev_end_group(dev) rt_expect(t, &bmp, 4, 4, BLUE, "the group's contents land where the page put them") rt_expect(t, &bmp, 7, 7, BLUE, "") rt_expect(t, &bmp, 8, 8, WHITE, "not displaced by the group's own origin") testing.expect_value(t, rt_ink(&bmp), 16) } // A soft mask's form is not clipped by a coverage plane that happens to be in // force where the ExtGState installed it, any more than by a scissor. @(test) test_rdev_soft_mask_form_ignores_a_clip_plane :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) tri := path_make() defer path_destroy(&tri) path_move_to(&tri, 0, 0) path_line_to(&tri, 16, 0) path_line_to(&tri, 0, 16) path_close(&tri) dev_clip_path(dev, &tri, false, MATRIX_IDENTITY) testing.expect_value(t, len(rd.masks), 1) dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) testing.expect_value(t, rd.mask_base, 1) rt_rect(rd, 0, 0, 16, 16, rt_paint(1, 1, 1)) sm: Soft_Mask dev_end_mask(dev, &sm, nil) testing.expect_value(t, rd.mask_base, 0) dev_pop_clip(dev) p := rt_paint(1, 0, 0) p.soft_mask = &sm rt_rect(rd, 0, 0, 16, 16, p) testing.expect_value(t, rt_ink(&bmp), 16 * 16) } // A clip built from text whose glyphs cannot be outlined at all -- a font with // no embedded program, or a Type3 one -- has to let content through. The // alternative is a blank page every time a font fails to load. @(test) test_rdev_clip_text_without_outlines_reveals :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) // Standard-14: metrics, but no program and so no outlines. fd, _ := to_dict(cf_parse(doc, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")) font, err := font_load(doc, fd) testing.expectf(t, err == .None, "font_load: %v", err) bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) txt := text_make() defer text_destroy(&txt) txt.font = font append(&txt.items, Text_Item{gid = 43, trm = scale_matrix(0.012, -0.012)}) dev_clip_text(dev, &txt, MATRIX_IDENTITY) rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) dev_pop_clip(dev) testing.expect_value(t, rt_ink(&bmp), 16 * 16) // Text with no items at all still means "clip everything". empty := text_make() defer text_destroy(&empty) empty.font = font bmp2 := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp2) rd2 := rt_dev(t, &bmp2, doc) defer raster_device_destroy(rd2) dev_clip_text(raster_device_as_device(rd2), &empty, MATRIX_IDENTITY) rt_rect(rd2, 0, 0, 16, 16, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&bmp2), 0) } // A long random sequence of hook calls -- including non-finite matrices, // unbalanced pops and nested layers -- must leave the device consistent and // must not crash, leak or hang. Content streams are the most malformed data in // a PDF and this is the shape of what they do. @(test) test_rdev_random_operations_stay_consistent :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) img := rt_image(t, doc, "<< /Width 2 /Height 2 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", RT_RGB2) stencil := rt_image(t, doc, "<< /Width 2 /Height 2 /ImageMask true /BitsPerComponent 1 >>", "\x40\x80") sh, _ := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) bmp := rt_bitmap(t, 24, 24) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) // A tiny xorshift, so the sequence is the same on every machine and a // failure is reproducible. state: u32 = 0x1234_5678 next :: proc(s: ^u32) -> u32 { s^ ~= s^ << 13 s^ ~= s^ >> 17 s^ ~= s^ << 5 return s^ } coord :: proc(s: ^u32) -> f64 { switch next(s) % 8 { case 0: return 1e9 case 1: return -1e9 case 2: return f64(next(s) % 32) * 1e30 // finite but vast } return f64(next(s) % 40) - 8 } sm: Soft_Mask open_clips := 0 open_layers := 0 st := STROKE_STATE_DEFAULT for i in 0 ..< 4000 { p := rt_paint(f64(next(&state) % 8) / 7, 0.4, 0.9, f64(next(&state) % 4) / 3) p.blend = Blend_Mode(next(&state) % 16) if next(&state) & 1 == 0 { p.soft_mask = &sm } ctm := MATRIX_IDENTITY if next(&state) % 4 == 0 { ctm = Matrix{coord(&state), coord(&state), coord(&state), coord(&state), coord(&state), coord(&state)} } switch next(&state) % 12 { case 0: rt_rect(rd, coord(&state), coord(&state), coord(&state), coord(&state), p, ctm) case 1: path := path_make() defer path_destroy(&path) path_move_to(&path, coord(&state), coord(&state)) path_curve_to(&path, coord(&state), coord(&state), coord(&state), coord(&state), coord(&state), coord(&state)) st.line_width = f64(next(&state) % 6) dev_stroke_path(dev, &path, &st, ctm, p) case 2: rt_clip_rect(rd, coord(&state), coord(&state), coord(&state), coord(&state), ctm) open_clips += 1 case 3: if open_clips > 0 { dev_pop_clip(dev) open_clips -= 1 } case 4: dev_fill_image(dev, img, ctm, p) case 5: dev_fill_image_mask(dev, stencil, ctm, p) case 6: dev_fill_shade(dev, sh, ctm, p) case 7: dev_begin_group(dev, Rect{coord(&state), coord(&state), coord(&state), coord(&state)}, i & 1 == 0, i & 2 == 0, p.blend, p.alpha, nil) open_layers += 1 case 8: if open_layers > 0 { dev_end_group(dev) open_layers -= 1 } case 9: dev_begin_mask(dev, Rect{coord(&state), coord(&state), coord(&state), coord(&state)}, i & 1 == 0, p) open_layers += 1 case 10: if open_layers > 0 { dev_end_mask(dev, nil, nil) open_layers -= 1 } case 11: dev_clip_image_mask(dev, stencil, ctm) open_clips += 1 } } for open_clips > 0 { dev_pop_clip(dev); open_clips -= 1 } for open_layers > 0 { dev_end_group(dev); open_layers -= 1 } dev_close(dev) testing.expect_value(t, len(rd.clips), 0) testing.expect_value(t, len(rd.masks), 0) testing.expect_value(t, len(rd.layers), 0) testing.expect_value(t, rd.suppress, 0) testing.expect_value(t, rd.scissor, rd.base_clip) // Whatever happened, the device is still usable. fresh := rt_bitmap(t, 8, 8) defer bitmap_destroy(&fresh) rd2 := rt_dev(t, &fresh, doc) defer raster_device_destroy(rd2) rt_rect(rd2, 0, 0, 8, 8, rt_paint(0, 0, 0)) testing.expect_value(t, rt_ink(&fresh), 64) } // A group's bitmap is smaller than the clip that surrounds it, so the scissor // handed to the rasteriser has to be clamped to the bitmap and not just // translated into it. Left unclamped, a run can come out wider than the // per-row scratch and the soft mask silently stops being applied -- which is // the failure this pins, and it only appears when a masked paint spills past // a group narrower than the clip. @(test) test_rdev_group_narrower_than_the_clip_still_masks :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) rt_rect(rd, 0, 0, 4, 16, rt_paint(1, 1, 1)) sm: Soft_Mask dev_end_mask(dev, &sm, nil) dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, 1, nil) testing.expect_value(t, rd.layers[0].box, Raster_Clip{0, 0, 8, 8}) testing.expect_value(t, rd.scissor, Raster_Clip{0, 0, 16, 16}) p := rt_paint(1, 0, 0) p.soft_mask = &sm rt_rect(rd, 0, 0, 16, 16, p) dev_end_group(dev) rt_expect(t, &bmp, 1, 1, RED, "inside the group, under the lit part of the mask") rt_expect(t, &bmp, 5, 1, WHITE, "inside the group, under the dark part of the mask") rt_expect(t, &bmp, 9, 1, WHITE, "outside the group entirely") testing.expect_value(t, rt_ink(&bmp), 4 * 8) } // A stroke outline is FILLED, and it must be filled nonzero. Two overlapping // strokes are one solid shape; even-odd would cancel them and punch a hole // through exactly the place where a join doubles back or two dashes land on // the same ink. @(test) test_rdev_stroke_outline_is_nonzero :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 12) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) p := path_make() defer path_destroy(&p) path_move_to(&p, 2, 4) path_line_to(&p, 14, 4) path_move_to(&p, 2, 6) path_line_to(&p, 14, 6) st := STROKE_STATE_DEFAULT st.line_width = 6 dev_stroke_path(raster_device_as_device(rd), &p, &st, MATRIX_IDENTITY, rt_paint(0, 0, 0)) // Each pen covers six rows; together they cover y 1..8 solidly, and the // rows they share are the ones even-odd would erase. for y in 1 ..< 9 { rt_expect(t, &bmp, 8, y, BLACK, "the two overlapping strokes are one solid shape") } rt_expect(t, &bmp, 8, 0, WHITE, "above them") rt_expect(t, &bmp, 8, 9, WHITE, "below them") testing.expect_value(t, rt_ink(&bmp), 12 * 8) } // A clip built from a stroke is the same outline, filled the same way: the // overlap of two strokes is inside the clip, not punched out of it. @(test) test_rdev_clip_stroke_path :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 12) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) p := path_make() defer path_destroy(&p) path_move_to(&p, 2, 4) path_line_to(&p, 14, 4) path_move_to(&p, 2, 6) path_line_to(&p, 14, 6) st := STROKE_STATE_DEFAULT st.line_width = 6 dev_clip_stroke_path(dev, &p, &st, MATRIX_IDENTITY) testing.expect_value(t, len(rd.masks), 1) rt_rect(rd, 0, 0, 16, 12, rt_paint(0, 0, 0)) dev_pop_clip(dev) for y in 1 ..< 9 { rt_expect(t, &bmp, 8, y, BLACK, "the whole stroked region clips in") } rt_expect(t, &bmp, 8, 0, WHITE, "above it") rt_expect(t, &bmp, 8, 9, WHITE, "below it") rt_expect(t, &bmp, 1, 4, WHITE, "left of it") } // Two overlapping glyphs are one shape. Filling a text run even-odd would // knock a hole through every ligature, every accent that touches its letter // and every piece of tightly-kerned script. @(test) test_rdev_overlapping_glyphs_fill_nonzero :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) font := rt_square_font(t, doc) bmp := rt_bitmap(t, 32, 32) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) txt := text_make() defer text_destroy(&txt) txt.font = font fm := font_matrix(font) trm1 := matrix_mul(fm, matrix_mul(scale_matrix(24, -24), translate_matrix(4, 26))) trm2 := matrix_mul(fm, matrix_mul(scale_matrix(24, -24), translate_matrix(10, 26))) append(&txt.items, Text_Item{gid = 1, trm = trm1}) append(&txt.items, Text_Item{gid = 1, trm = trm2}) dev_fill_text(raster_device_as_device(rd), &txt, MATRIX_IDENTITY, rt_paint(0, 0, 0)) // The first square spans x 6.4..25.6, the second 12.4..31.6. rt_expect(t, &bmp, 8, 15, BLACK, "only the first glyph") rt_expect(t, &bmp, 20, 15, BLACK, "where the two overlap") rt_expect(t, &bmp, 29, 15, BLACK, "only the second glyph") rt_expect(t, &bmp, 5, 15, WHITE, "before both") } // A group's alpha comes out of a content stream, so it can be anything. Above // 1 it saturates; a NaN paints nothing rather than reaching the arithmetic. @(test) test_rdev_group_alpha_out_of_range :: proc(t: ^testing.T) { for alpha, i in ([2]f64{2.0, math_nan()}) { bmp := rt_bitmap(t, 8, 8) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) dev_begin_group(dev, Rect{0, 0, 8, 8}, true, false, .Normal, alpha, nil) rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0)) dev_end_group(dev) if i == 0 { rt_expect(t, &bmp, 4, 4, RED, "an alpha above 1 saturates at opaque") } else { testing.expect_value(t, rt_ink(&bmp), 0) } } } // A soft mask whose group could not be rendered leaves nothing behind. If the // previous mask survived, every later paint would be modulated by a mask that // the graphics state has already replaced. @(test) test_rdev_failed_mask_replaces_the_previous_one :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) // A mask whose form paints nothing: black everywhere, so it hides all. dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) sm := rt_mask(1) dev_end_mask(dev, &sm, nil) testing.expect(t, rt_smask(rd) != nil, "") p := rt_paint(1, 0, 0) p.soft_mask = &sm rt_rect(rd, 0, 0, 16, 16, p) testing.expect_value(t, rt_ink(&bmp), 0) // Now one that cannot be rendered at all. other := rt_mask(2) dev_begin_mask(dev, RECT_EMPTY, true, rt_paint(0, 0, 0)) testing.expect(t, !rd.layers[0].ok, "an empty mask area needs no bitmap") dev_end_mask(dev, &other, nil) testing.expect(t, rdev_smask_lookup(rd, &other) == nil, "a mask that could not be rendered installs no plane") // The paint that was going to use it carries a mask nothing rendered, so // it must not pick up the one that came before. p2 := rt_paint(1, 0, 0) p2.soft_mask = &other rt_rect(rd, 0, 0, 16, 16, p2) testing.expect_value(t, rt_ink(&bmp), 16 * 16) } // The same thing again, with a mask that came before and was never painted // through: a mask that failed to render must not hand its predecessor's plane // to the next paint that comes along. @(test) test_rdev_failed_mask_does_not_donate_an_unused_one :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) // Rendered, but nothing has been painted through it yet. first := rt_mask(1) rt_half_mask(rd, 0, 8, &first) testing.expect(t, rdev_smask_lookup(rd, &first) != nil, "the plane is installed") dev_begin_mask(dev, RECT_EMPTY, true, rt_paint(0, 0, 0)) sm := rt_mask(2) dev_end_mask(dev, &sm, nil) p := rt_paint(1, 0, 0) p.soft_mask = &sm rt_rect(rd, 0, 0, 16, 16, p) rt_expect(t, &bmp, 2, 2, RED, "") rt_expect(t, &bmp, 12, 2, RED, "the other mask's plane is not handed to this paint") testing.expect_value(t, rt_ink(&bmp), 16 * 16) } // A shading is painted with the paint's alpha and blend mode, not at full // strength. @(test) test_rdev_shade_honours_alpha :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) sh, err := shading_load(doc, cf_parse(doc, RT_AXIAL), nil) testing.expectf(t, err == .None, "shading_load: %v", err) opaque := rt_bitmap(t, 16, 8) defer bitmap_destroy(&opaque) rd1 := rt_dev(t, &opaque, doc) defer raster_device_destroy(rd1) dev_fill_shade(raster_device_as_device(rd1), sh, MATRIX_IDENTITY, rt_paint(0, 0, 0, 1)) half := rt_bitmap(t, 16, 8) defer bitmap_destroy(&half) rd2 := rt_dev(t, &half, doc) defer raster_device_destroy(rd2) dev_fill_shade(raster_device_as_device(rd2), sh, MATRIX_IDENTITY, rt_paint(0, 0, 0, 0.5)) a := int(rt_px(&opaque, 0, 4)[0]) b := int(rt_px(&half, 0, 4)[0]) testing.expectf(t, a < 24, "the opaque dark end: %d", a) testing.expectf(t, b > 110 && b < 145, "half of it over white should be mid-grey: %d", b) } // --------------------------------------------------------------------------- // Shape, not speed // --------------------------------------------------------------------------- // Not a benchmark: a guard against something here being accidentally // quadratic in the size of the page or of an image. A real page of work -- // five thousand fills and one photograph minified into half of it -- runs in // well under half a second, so the budget is a factor of thirty above what it // costs and only a change of algorithmic shape can breach it. @(test) test_rdev_page_sized_workload :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp, err := bitmap_make(1275, 1650, 4) testing.expect_value(t, err, Error.None) defer bitmap_destroy(&bmp) bitmap_fill(&bmp, WHITE) bmp.premultiplied = true rd, derr := raster_device_make(&bmp, Raster_Clip{0, 0, bmp.width, bmp.height}, context.allocator, doc) testing.expect_value(t, derr, Error.None) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) start := time.now() p := path_make() defer path_destroy(&p) for i in 0 ..< 5000 { path_clear(&p) path_rect(&p, f64((i * 37) % 1200), f64((i * 53) % 1600), 40, 12) dev_fill_path(dev, &p, false, MATRIX_IDENTITY, rt_paint(0.1, 0.2, 0.3)) } px := make([]byte, 3 * 1024 * 1024, doc.allocator) for i in 0 ..< len(px) { px[i] = u8(i * 7) } img := rt_image(t, doc, "<< /Width 1024 /Height 1024 /BitsPerComponent 8 /ColorSpace /DeviceRGB >>", string(px)) dev_fill_image(dev, img, Matrix{600, 0, 0, -800, 100, 900}, rt_paint(0, 0, 0)) dev_close(dev) elapsed := time.duration_seconds(time.since(start)) testing.expectf(t, elapsed < 15, "a page of work took %.2fs", elapsed) testing.expect(t, rt_ink(&bmp) > 100_000, "and it has to have actually drawn") } // --------------------------------------------------------------------------- // The soft-mask cache // --------------------------------------------------------------------------- // rt_half_mask renders a luminosity mask lit over `x0 ..< x1` and black // everywhere else, belonging to `owner` -- which `end_mask` names, exactly as // the interpreter does. @(private = "file") rt_half_mask :: proc(rd: ^Raster_Device, x0, x1: f64, owner: ^Soft_Mask) { dev := raster_device_as_device(rd) dev_begin_mask(dev, Rect{0, 0, 16, 16}, true, rt_paint(0, 0, 0)) rt_rect(rd, x0, 0, x1 - x0, 16, rt_paint(1, 1, 1)) dev_end_mask(dev, owner, nil) } // An ExtGState renders its soft mask once, when it installs it, so a `Q` that // restores an outer mask never draws it again -- the device has to still have // it. Holding only the newest would modulate everything after the `Q` with the // inner mask instead, which is the failure this pins. @(test) test_rdev_soft_mask_cache_survives_a_restore :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) outer, inner := rt_mask(1), rt_mask(2) po := rt_paint(1, 0, 0); po.soft_mask = &outer pi := rt_paint(0, 1, 0); pi.soft_mask = &inner // The outer mask lights the left half. rt_half_mask(rd, 0, 8, &outer) rt_rect(rd, 0, 0, 16, 4, po) // Inside a `q`, an inner mask lights the right half. rt_half_mask(rd, 8, 16, &inner) rt_rect(rd, 0, 4, 16, 4, pi) // After the `Q` the outer mask is in force again, and nothing re-rendered it. rt_rect(rd, 0, 8, 16, 4, po) rt_rect(rd, 0, 12, 16, 4, pi) rt_expect(t, &bmp, 2, 1, RED, "the outer mask lights the left half") rt_expect(t, &bmp, 12, 1, WHITE, "") rt_expect(t, &bmp, 2, 5, WHITE, "the inner mask lights the right half") rt_expect(t, &bmp, 12, 5, GREEN, "") rt_expect(t, &bmp, 2, 9, RED, "and the outer one is still there after the restore") rt_expect(t, &bmp, 12, 9, WHITE, "") rt_expect(t, &bmp, 2, 13, WHITE, "as is the inner one") rt_expect(t, &bmp, 12, 13, GREEN, "") } // The cache is bounded. Past its size the least recently used mask goes, and // a paint that wanted it is drawn unmasked rather than through the wrong one. @(test) test_rdev_soft_mask_cache_is_bounded :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) owners: [RDEV_MAX_SOFT_MASKS + 1]Soft_Mask for i in 0 ..< len(owners) { owners[i] = rt_mask(i + 1) p := rt_paint(0, 0, 0) p.soft_mask = &owners[i] // Every one of them lights only the left half. rt_half_mask(rd, 0, 8, &owners[i]) rt_rect(rd, 0, 0, 1, 1, p) // use it } live := 0 for i in 0 ..< RDEV_MAX_SOFT_MASKS { if rd.smasks[i].live { live += 1 } } testing.expect_value(t, live, RDEV_MAX_SOFT_MASKS) // The first mask has been evicted, so its paint is unmasked and covers the // whole row; the last is still there and covers only the left half. first := rt_paint(1, 0, 0) first.soft_mask = &owners[0] rt_rect(rd, 0, 4, 16, 2, first) rt_expect(t, &bmp, 2, 5, RED, "an evicted mask leaves the paint unmasked") rt_expect(t, &bmp, 12, 5, RED, "") last := rt_paint(0, 0, 1) last.soft_mask = &owners[len(owners) - 1] rt_rect(rd, 0, 8, 16, 2, last) rt_expect(t, &bmp, 2, 9, BLUE, "the newest is still cached") rt_expect(t, &bmp, 12, 9, WHITE, "") } // A mask that was rendered and never painted through is dead weight, so it is // the first thing evicted -- ahead of any mask that has been used at least // once. An ExtGState that installs a mask and then draws nothing is common // enough (a `q /GS gs Q` around content that turned out to be empty) that // letting those push out the masks still in force would empty the cache. @(test) test_rdev_soft_mask_cache_evicts_unused_planes_first :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) owners: [RDEV_MAX_SOFT_MASKS]Soft_Mask for i in 0 ..< len(owners) { owners[i] = rt_mask(i + 1) p := rt_paint(0, 0, 0) p.soft_mask = &owners[i] rt_half_mask(rd, 0, 8, &owners[i]) rt_rect(rd, 0, 0, 1, 1, p) // use it } // Two more masks, of the opposite shape, that nothing ever paints through. unused := [2]Soft_Mask{rt_mask(101), rt_mask(102)} rt_half_mask(rd, 8, 16, &unused[0]) rt_half_mask(rd, 8, 16, &unused[1]) // The second-oldest used mask must still be there: only the oldest was // evicted, and the second unused plane reused the first one's slot. p := rt_paint(1, 0, 0) p.soft_mask = &owners[1] rt_rect(rd, 0, 4, 16, 4, p) rt_expect(t, &bmp, 2, 5, RED, "the mask it was rendered with lights the left half") rt_expect(t, &bmp, 12, 5, WHITE, "and not the right, which is what an unused plane would have done") } // --------------------------------------------------------------------------- // Transparency groups: what the outer compositing parameters apply to // --------------------------------------------------------------------------- // rt_group_res builds the resources these tests share: an ExtGState whose // /SMask is a 50% luminosity group, a plain half-alpha ExtGState, and the same // drawing as a form with and without /Group. @(private = "file") rt_group_res :: proc(doc: ^Document) -> ^Dict { res := new_dict(doc.allocator) smd := new_dict(doc.allocator) dict_set(smd, "S", Name("Luminosity")) dict_set(smd, "G", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] " + "/Group << /S /Transparency /CS /DeviceGray >> >>", transmute([]byte)string("0.5 g 0 0 20 20 re f"))) gm := new_dict(doc.allocator) dict_set(gm, "Type", Name("ExtGState")) dict_set(gm, "SMask", smd) gs := new_dict(doc.allocator) dict_set(gs, "GM", gm) dict_set(gs, "HA", cf_parse(doc, "<< /Type /ExtGState /ca 0.5 >>")) dict_set(res, "ExtGState", gs) xo := new_dict(doc.allocator) dict_set(xo, "Grp", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] " + "/Group << /S /Transparency /CS /DeviceGray >> >>", transmute([]byte)string("0 0 20 20 re f"))) dict_set(xo, "Plain", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] >>", transmute([]byte)string("0 0 20 20 re f"))) // The left half as a group, the right half as a plain form, so a paint // through the mask on one side cannot be confused with the other. dict_set(xo, "GrpL", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 10 20] " + "/Group << /S /Transparency /CS /DeviceGray >> >>", transmute([]byte)string("0 0 10 20 re f"))) dict_set(xo, "Right", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [10 0 20 20] >>", transmute([]byte)string("10 0 10 20 re f"))) dict_set(xo, "Zero", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 0 0] >>", transmute([]byte)string("0 0 20 20 re f"))) // A transparency group whose whole content is another transparency group. // A form with no /Resources of its own inherits the invoking stream's, so // /Grp resolves here (8.10.1). dict_set(xo, "Nest", cf_stream(doc, "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] " + "/Group << /S /Transparency /CS /DeviceGray >> >>", transmute([]byte)string("/Grp Do"))) dict_set(res, "XObject", xo) return res } @(private = "file") rt_run :: proc(t: ^testing.T, doc: ^Document, bmp: ^Bitmap, res: ^Dict, src: string, loc := #caller_location) { rd := rt_dev(t, bmp, doc, loc) defer raster_device_destroy(rd) err := run_content(doc, raster_device_as_device(rd), transmute([]byte)src, res, MATRIX_IDENTITY) testing.expectf(t, err == .None, "run_content: %v", err, loc = loc) } // 11.6.6: the outer alpha, blend mode AND soft mask apply to a transparency // group's RESULT. `content_do_form` clears all three for the group's contents, // so a mask that `begin_group` did not carry was applied nowhere at all -- the // gradient fade every drop shadow is built from rendered as a solid shape. // Alpha was already carried, which is what isolates the missing parameter. @(test) test_rdev_soft_mask_applies_to_a_group_result :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) res := rt_group_res(doc) Case :: struct { src: string, want: [4]u8, what: string } cases := []Case{ {"/GM gs 0 0 20 20 re f", {127, 127, 127, 255}, "a bare fill under the mask"}, {"/GM gs /Plain Do", {127, 127, 127, 255}, "a form with no /Group"}, {"/GM gs /Grp Do", {127, 127, 127, 255}, "a transparency group"}, {"0 0 20 20 re f", {0, 0, 0, 255}, "no mask at all"}, {"/HA gs /Grp Do", {128, 128, 128, 255}, "a group's own alpha"}, } for c in cases { bmp := rt_bitmap(t, 20, 20) defer bitmap_destroy(&bmp) rt_run(t, doc, &bmp, res, c.src) rt_expect_near(t, &bmp, 10, 10, c.want, 1, c.what) } } // A paint carrying no soft mask must not cost a later paint the plane it // needs. Every paint inside a transparency group is exactly such a paint -- // 11.6.6 clears the mask for the contents -- so one group used to disable an // ExtGState's mask for the whole rest of the page. @(test) test_rdev_a_group_does_not_consume_the_soft_mask :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) res := rt_group_res(doc) alone := rt_bitmap(t, 20, 20) defer bitmap_destroy(&alone) rt_run(t, doc, &alone, res, "q /GM gs /Right Do Q") rt_expect_near(t, &alone, 15, 10, {127, 127, 127, 255}, 1, "the plain form alone") after := rt_bitmap(t, 20, 20) defer bitmap_destroy(&after) rt_run(t, doc, &after, res, "q /GM gs /GrpL Do /Right Do Q") rt_expect_near(t, &after, 5, 10, {127, 127, 127, 255}, 1, "the group is masked") rt_expect_near(t, &after, 15, 10, {127, 127, 127, 255}, 1, "and the plain form after it still is") } // The device half of the same story, with no interpreter in it: the mask // arrives on `begin_group` and `rdev_layer_finish` is the only thing that can // apply it. It used to composite the finished layer with // `blend_bitmap(..., l.alpha, l.blend, nil)` -- alpha and blend mode honoured, // the mask hard-nil -- so a group under a 50% mask came back fully opaque. @(test) test_rdev_group_result_is_composited_through_its_mask :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 100, 100) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) // A luminosity mask over the whole page whose form paints mid-grey: half // of whatever it modulates gets through. dev_begin_mask(dev, Rect{0, 0, 100, 100}, true, rt_paint(0, 0, 0)) rt_rect(rd, 0, 0, 100, 100, rt_paint(0.5, 0.5, 0.5)) sm := rt_mask(31) dev_end_mask(dev, &sm, nil) // 11.6.6: the group's CONTENTS carry no mask -- that is the interpreter's // job and it is what makes this the only channel the mask has left. dev_begin_group(dev, Rect{0, 0, 50, 100}, true, false, .Normal, 1, &sm) rt_rect(rd, 0, 0, 50, 100, rt_paint(0, 0, 0)) dev_end_group(dev) rt_expect_near(t, &bmp, 25, 50, {127, 127, 127, 255}, 1, "the group's result through the mask") rt_expect(t, &bmp, 75, 50, WHITE, "outside the group") } // The plane a group's RESULT will be composited through has to survive the // cache until `end_group` -- which can be a whole content stream later. Until // then nothing has painted through it, and "a slot nothing ever painted // through goes first" is exactly the rule that would give it away: an // ExtGState mask over a group, with more masks installed inside the group, is // ordinary Illustrator output. @(test) test_rdev_group_mask_survives_cache_pressure :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 16, 16) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) // Fill every other slot with a mask that has been painted through, so the // next install has to evict one of them or the group's. others: [RDEV_MAX_SOFT_MASKS - 1]Soft_Mask for i in 0 ..< len(others) { others[i] = rt_mask(i + 1) rt_half_mask(rd, 0, 16, &others[i]) p := rt_paint(1, 1, 1) // white on white: used, but invisible p.soft_mask = &others[i] rt_rect(rd, 0, 0, 1, 1, p) } group_mask := rt_mask(9) rt_half_mask(rd, 0, 8, &group_mask) dev_begin_group(dev, Rect{0, 0, 16, 16}, true, false, .Normal, 1, &group_mask) rt_rect(rd, 0, 0, 16, 16, rt_paint(0, 0, 0)) // A mask installed INSIDE the group: the interpreter does this whenever // the group's own contents carry an /SMask of their own. inner := rt_mask(10) rt_half_mask(rd, 8, 16, &inner) dev_end_group(dev) rt_expect(t, &bmp, 4, 8, BLACK, "the lit half of the group's mask") rt_expect(t, &bmp, 12, 8, WHITE, "the half its mask holds back") } // A group nested inside a masked group. The mask applies to the OUTER group's // result exactly once: the interpreter clears it for the outer group's // contents, so the inner group is begun with no mask of its own. Applying it // again there would square it -- a 50% mask would show as 25%. @(test) test_rdev_soft_mask_over_a_nested_group :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) res := rt_group_res(doc) bmp := rt_bitmap(t, 20, 20) defer bitmap_destroy(&bmp) rt_run(t, doc, &bmp, res, "/GM gs /Nest Do") // 0 is the mask dropped, 191 is the mask applied twice. rt_expect_near(t, &bmp, 10, 10, {127, 127, 127, 255}, 1, "a group inside a masked group, masked once") } // Defect 5's shape end to end: a masked group, then a paint with no mask at // all, then another masked paint. The plane used to be claimed by the first // paint to arrive after `end_mask`, so the group's interior fill -- which // carries no mask by design -- threw the claim away and everything after it // was painted unmasked. @(test) test_rdev_soft_mask_survives_an_unmasked_paint_between_two_masked_ones :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) res := rt_group_res(doc) bmp := rt_bitmap(t, 20, 20) defer bitmap_destroy(&bmp) rt_run(t, doc, &bmp, res, "q /GM gs /GrpL Do Q " + // masked transparency group, x < 10 "10 0 4 20 re f " + // no mask in force at all "q /GM gs 14 0 6 20 re f Q") // masked again rt_expect_near(t, &bmp, 5, 10, {127, 127, 127, 255}, 1, "the masked group") rt_expect(t, &bmp, 12, 10, BLACK, "the unmasked paint between them") rt_expect_near(t, &bmp, 17, 10, {127, 127, 127, 255}, 1, "the masked paint after both of them") } // The same defect one level up: a form whose /BBox has no area must draw // nothing, because the box is a clip on its contents (8.10.2). @(test) test_rdev_form_with_a_zero_area_bbox_draws_nothing :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) res := rt_group_res(doc) bmp := rt_bitmap(t, 20, 20) defer bitmap_destroy(&bmp) rt_run(t, doc, &bmp, res, "/Zero Do") testing.expect_value(t, rt_ink(&bmp), 0) } // A NON-ISOLATED group is handed a copy of its backdrop, so its finished // bitmap already IS "contents over backdrop". Compositing that OVER the // backdrop again blends the backdrop with itself: `C over C` under Normal // takes alpha 0.5 to 0.75. That is wrong wherever the backdrop is not opaque, // which is every nested group and every page rendered with a transparent // background -- and it damages the whole area the group covers, whether or not // the group draws anything. @(test) test_rdev_non_isolated_group_over_a_transparent_backdrop :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 8, 8, {0, 0, 0, 0}) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0, 0.5)) before := rt_px(&bmp, 4, 4) rt_expect(t, &bmp, 4, 4, {128, 0, 0, 128}, "half a red, premultiplied") dev_begin_group(dev, Rect{0, 0, 8, 8}, false, false, .Normal, 1, nil) dev_end_group(dev) testing.expectf(t, rt_px(&bmp, 4, 4) == before, "an empty non-isolated group must leave the backdrop alone: got %v, was %v", rt_px(&bmp, 4, 4), before) } // And with contents: wrapping a paint in a non-isolated group must give // exactly what painting it directly gives, once the group's own alpha is 1 and // its blend mode Normal. @(test) test_rdev_non_isolated_group_matches_painting_directly :: proc(t: ^testing.T) { direct := rt_bitmap(t, 8, 8, {0, 0, 0, 0}) defer bitmap_destroy(&direct) { rd := rt_dev(t, &direct) defer raster_device_destroy(rd) rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0, 0.5)) rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 1, 0, 0.5)) } grouped := rt_bitmap(t, 8, 8, {0, 0, 0, 0}) defer bitmap_destroy(&grouped) { rd := rt_dev(t, &grouped) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) rt_rect(rd, 0, 0, 8, 8, rt_paint(1, 0, 0, 0.5)) dev_begin_group(dev, Rect{0, 0, 8, 8}, false, false, .Normal, 1, nil) rt_rect(rd, 0, 0, 8, 8, rt_paint(0, 1, 0, 0.5)) dev_end_group(dev) } for y in 0 ..< 8 { for x in 0 ..< 8 { a := rt_px(&direct, x, y) b := rt_px(&grouped, x, y) if a != b { testing.expectf(t, false, "(%d,%d): grouped %v, direct %v", x, y, b, a) return } } } } // --------------------------------------------------------------------------- // Minification // --------------------------------------------------------------------------- // A one-pixel checkerboard is exactly half black, so however far it is // minified the answer is mid-grey. Capping the taps at RDEV_MAX_TAPS without // reducing the source first turned the "box filter" into a strided point // sample: at 32x the stride is 2, every tap landed on the same parity, and the // whole image came out solid. Bilevel one-pixel detail -- fax scans, halftone // screens, hairline rules -- is exactly the content that hits this, and // thumbnail-scale rendering is exactly where it happens. @(test) test_rdev_heavy_minification_does_not_alias :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) W :: 320 samples := make([]byte, W * W) defer delete(samples) for y in 0 ..< W { for x in 0 ..< W { samples[y * W + x] = (x + y) % 2 == 0 ? 255 : 0 } } res := new_dict(doc.allocator) xo := new_dict(doc.allocator) dict_set(xo, "Im", cf_stream(doc, "<< /Type /XObject /Subtype /Image /Width 320 /Height 320 " + "/ColorSpace /DeviceGray /BitsPerComponent 8 >>", samples)) dict_set(res, "XObject", xo) // 10x10 device pixels for a 320x320 source: 32x minification per axis. for size in ([3]int{10, 5, 2}) { bmp := rt_bitmap(t, size, size) defer bitmap_destroy(&bmp) src := fmt.tprintf("q %d 0 0 %d 0 0 cm /Im Do Q", size, size) rt_run(t, doc, &bmp, res, src) for y in 0 ..< size { for x in 0 ..< size { got := rt_px(&bmp, x, y) if abs(int(got[0]) - 128) > 24 { testing.expectf(t, false, "%dx minification at (%d,%d): got %v, want ~128 grey", W / size, x, y, got) return } } } } } // The same source across the whole range of minification, not just the one // factor that broke. A box filter is a box filter only while its taps still // cover the footprint, and there are two ways to lose that: too few taps for // the footprint (which is what capping them without reducing the source did) // and a reduction that drops or double-counts source pixels at the edges. Both // show as the mean of the output drifting away from the mean of the source, so // that is what this asserts -- at every factor from 1x to 64x, and per pixel // from 2x up, where one device pixel covers a whole number of cells. @(test) test_rdev_minification_sweep_holds_the_source_mean :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) W :: 320 samples := make([]byte, W * W) defer delete(samples) for y in 0 ..< W { for x in 0 ..< W { samples[y * W + x] = (x + y) % 2 == 0 ? 255 : 0 } } res := new_dict(doc.allocator) xo := new_dict(doc.allocator) dict_set(xo, "Im", cf_stream(doc, "<< /Type /XObject /Subtype /Image /Width 320 /Height 320 " + "/ColorSpace /DeviceGray /BitsPerComponent 8 >>", samples)) dict_set(res, "XObject", xo) // Every divisor of 320 in the range, so a device pixel covers a whole // number of source pixels and the exact answer is the source mean. for size in ([]int{320, 160, 80, 64, 40, 32, 20, 16, 10, 8, 5}) { bmp := rt_bitmap(t, size, size) defer bitmap_destroy(&bmp) rt_run(t, doc, &bmp, res, fmt.tprintf("q %d 0 0 %d 0 0 cm /Im Do Q", size, size)) factor := W / size sum := 0 for y in 0 ..< size { for x in 0 ..< size { v := int(rt_px(&bmp, x, y)[0]) sum += v // At 1x the image IS the checkerboard and no filtering is // wanted; past it every pixel is an average of both colours. if factor > 1 && abs(v - 128) > 24 { testing.expectf(t, false, "%dx minification at (%d,%d): got %d, want ~128 grey", factor, x, y, v) return } } } mean := f64(sum) / f64(size * size) testing.expectf(t, abs(mean - 127.5) <= 4, "%dx minification: mean %.1f, want ~127.5", factor, mean) } } // A stencil takes a different span procedure to a colour image and the same // sampler, and a 1-bit stencil is the content the aliasing actually shows up // on: a fax scan or a halftone screen minified to thumbnail size. Half the // bits are set, so half the ink lands, whatever the factor. @(test) test_rdev_minified_stencil_is_averaged :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) W :: 320 STRIDE :: W / 8 bits := make([]byte, STRIDE * W) defer delete(bits) for y in 0 ..< W { // 0xAA and 0x55 are the two phases of a one-pixel checkerboard. fill := u8(y % 2 == 0 ? 0xAA : 0x55) for i in 0 ..< STRIDE { bits[y * STRIDE + i] = fill } } res := new_dict(doc.allocator) xo := new_dict(doc.allocator) dict_set(xo, "Im", cf_stream(doc, "<< /Type /XObject /Subtype /Image /Width 320 /Height 320 " + "/ImageMask true /BitsPerComponent 1 >>", bits)) dict_set(res, "XObject", xo) for size in ([]int{10, 5}) { bmp := rt_bitmap(t, size, size) defer bitmap_destroy(&bmp) rt_run(t, doc, &bmp, res, fmt.tprintf("q %d 0 0 %d 0 0 cm /Im Do Q", size, size)) for y in 0 ..< size { for x in 0 ..< size { v := int(rt_px(&bmp, x, y)[0]) if abs(v - 128) > 24 { testing.expectf(t, false, "%dx stencil minification at (%d,%d): got %d, want ~128 grey", W / size, x, y, v) return } } } } } // --------------------------------------------------------------------------- // The clip stack is bounded // --------------------------------------------------------------------------- // `W n` needs no `q`, and nothing pops until the enclosing scope ends, so a // page whose content is one non-rectangular full-page clip repeated pushes a // stack entry and a coverage plane the size of the scissor every time. At // 612x792 that is half a megabyte each and the token budget buys hundreds of // thousands of them; every other stack in this device is capped and this one // was not. @(test) test_rdev_clip_stack_is_bounded :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 200, 200) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) // Five points, so it is not one of the rectangles the scissor fast path // recognises, and its bounds are the whole page, so the scissor never // narrows and every plane is full-sized. p := path_make() defer path_destroy(&p) path_move_to(&p, 0, 0) path_line_to(&p, 200, 0) path_line_to(&p, 200, 200) path_line_to(&p, 0, 200) path_line_to(&p, 1, 1) N :: RDEV_MAX_CLIPS + 1000 start := time.now() for _ in 0 ..< N { dev_clip_path(dev, &p, false, MATRIX_IDENTITY) } elapsed := time.duration_seconds(time.since(start)) testing.expectf(t, len(rd.clips) <= RDEV_MAX_CLIPS, "%d clip entries", len(rd.clips)) testing.expectf(t, len(rd.masks) <= RDEV_MAX_CLIP_MASKS, "%d coverage planes", len(rd.masks)) testing.expectf(t, rd.mask_bytes <= RDEV_MAX_CLIP_MASK_BYTES, "%d bytes of coverage plane", rd.mask_bytes) // Not a performance bound: uncapped, this same loop allocates 200 MB of // planes and rasterises the path into every one of them. testing.expectf(t, elapsed < 10, "%d clips took %.2fs", N, elapsed) // A refused push must not be popped by somebody else's `Q`: the pops are // matched here, so the scissor has to come back to the whole page and the // planes have to be gone. for _ in 0 ..< N { dev_pop_clip(dev) } testing.expect_value(t, len(rd.clips), 0) testing.expect_value(t, len(rd.masks), 0) testing.expect_value(t, rd.mask_bytes, 0) testing.expect_value(t, rdev_scissor(rd), Raster_Clip{0, 0, 200, 200}) // And it still draws: a fill after all that lands on the page. rt_rect(rd, 10, 10, 20, 20, rt_paint(1, 0, 0)) rt_expect(t, &bmp, 15, 15, RED, "the device is still usable") } // --------------------------------------------------------------------------- // Geometry the clip cannot reach costs nothing // --------------------------------------------------------------------------- // One `c` with control points ten trillion units away flattens to 65 537 // points, and sixteen of them exhaust MAX_FLATTEN_POINTS: a fill that costs // 250 bytes of content materialises a megapoint of polyline, and the token // budget pays for thirty thousand such fills. The device hands the clip to // the flattener so that none of it is built. @(test) test_rdev_unreachable_curves_cost_nothing :: proc(t: ^testing.T) { bmp := rt_bitmap(t, 612, 792) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp) defer raster_device_destroy(rd) dev := raster_device_as_device(rd) p := path_make() defer path_destroy(&p) path_move_to(&p, 100, 100) for _ in 0 ..< 16 { path_curve_to(&p, -4.98e13, 5.61e13, 7.23e13, 9.65e13, -7.82e13, 9.74e13) } path_line_to(&p, 300, 300) st := STROKE_STATE_DEFAULT st.line_width = 2 // The load-independent half: one fill, and what it had to build for it. dev_fill_path(dev, &p, false, MATRIX_IDENTITY, rt_paint(0, 0, 1)) testing.expectf(t, len(rd.fp.points) <= 128, "one fill flattened %d points", len(rd.fp.points)) dev_clip_path(dev, &p, false, MATRIX_IDENTITY) testing.expectf(t, len(rd.fp.points) <= 128, "one clip flattened %d points", len(rd.fp.points)) dev_pop_clip(dev) start := time.now() for _ in 0 ..< 200 { dev_fill_path(dev, &p, false, MATRIX_IDENTITY, rt_paint(0, 0, 1)) dev_clip_path(dev, &p, false, MATRIX_IDENTITY) dev_pop_clip(dev) dev_stroke_path(dev, &p, &st, MATRIX_IDENTITY, rt_paint(0, 0, 1)) } elapsed := time.duration_seconds(time.since(start)) // Not a performance bound: without the clip in hand this is a megapoint of // polyline per operator and the same loop takes 79 seconds unloaded, so // the budget is fifteen times what the work costs and still an order of // magnitude below the failure it guards. testing.expectf(t, elapsed < 30, "800 operators on an unreachable path took %.2fs", elapsed) } // A path too big to flatten whole is truncated, and truncation is damage: the // page is drawn with part of a shape missing. A caller reading `doc.repairs` // has to be able to tell that from a page this library drew faithfully. @(test) test_rdev_truncated_flattening_is_recorded :: proc(t: ^testing.T) { doc := cf_open(t) defer cf_close(t, doc) bmp := rt_bitmap(t, 64, 64) defer bitmap_destroy(&bmp) rd := rt_dev(t, &bmp, doc) defer raster_device_destroy(rd) // Every segment lands on the page, so nothing here is culled and the cap // is the only thing that stops it. p := path_make() defer path_destroy(&p) path_move_to(&p, 0, 0) for i in 0 ..< MAX_FLATTEN_POINTS + 16 { path_line_to(&p, f64(i % 61) + 1, f64((i * 7) % 61) + 1) } before := len(doc.repairs) dev_fill_path(raster_device_as_device(rd), &p, false, MATRIX_IDENTITY, rt_paint(0, 0, 0)) testing.expectf(t, len(rd.fp.points) == MAX_FLATTEN_POINTS, "the flattening stops at the cap: %d points", len(rd.fp.points)) testing.expect(t, len(doc.repairs) > before, "and says so") testing.expect(t, rt_ink(&bmp) > 0, "what did fit is still drawn") }