Files
pdf-odin-llm/device_raster_test.odin
T

2966 lines
111 KiB
Odin
Raw Normal View History

2026-09-25 23:32:29 +02:00
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")
}