Files
pdf-odin-llm/shading_test.odin
T

1732 lines
68 KiB
Odin
Raw Normal View History

2026-09-25 23:32:29 +02:00
package pdf
import "core:testing"
// Shading tests.
//
// The fixtures -- `cf_open`, `cf_parse`, `cf_stream`, `cf_bytes` -- live in
// function_test.odin, which is also where the Document these build on comes
// from.
//
// Every target bitmap is three components, no alpha. That is deliberate: with
// no alpha channel the destination reads as opaque and `blend_over` collapses
// to "the source colour", so an assertion about a pixel is an assertion about
// what the shading computed rather than about compositing. It also gives
// "painted nothing" a spelling that "painted black" cannot fake -- the bitmap
// starts filled with SH_BG, a colour no shading in this file produces.
@(private = "file")
SH_BG :: [3]u8{9, 200, 30}
@(private = "file")
sh_target :: proc(t: ^testing.T, w, h: int, loc := #caller_location) -> Bitmap {
bmp, err := bitmap_make(w, h, 3)
testing.expect_value(t, err, Error.None, loc = loc)
bitmap_fill(&bmp, [4]u8{SH_BG[0], SH_BG[1], SH_BG[2], 255})
return bmp
}
@(private = "file")
sh_pixel :: proc(bmp: ^Bitmap, x, y: int) -> [3]u8 {
off, ok := bitmap_pixel_offset(bmp, x, y)
if !ok {
return [3]u8{}
}
return [3]u8{bmp.pixels[off], bmp.pixels[off + 1], bmp.pixels[off + 2]}
}
@(private = "file")
expect_pixel :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, want: [3]u8, tol: int,
what: string, loc := #caller_location) {
got := sh_pixel(bmp, x, y)
for i in 0 ..< 3 {
if abs(int(got[i]) - int(want[i])) > tol {
testing.expectf(t, false, "%s: pixel (%d,%d) is %v, want %v (tolerance %d)",
what, x, y, got, want, tol, loc = loc)
return
}
}
}
@(private = "file")
expect_gray :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, g: u8, tol: int,
what: string, loc := #caller_location) {
expect_pixel(t, bmp, x, y, [3]u8{g, g, g}, tol, what, loc)
}
@(private = "file")
expect_untouched :: proc(t: ^testing.T, bmp: ^Bitmap, x, y: int, what: string,
loc := #caller_location) {
got := sh_pixel(bmp, x, y)
if got != SH_BG {
testing.expectf(t, false, "%s: pixel (%d,%d) was painted %v, expected it left alone",
what, x, y, got, loc = loc)
}
}
@(private = "file")
expect_all_painted :: proc(t: ^testing.T, bmp: ^Bitmap, what: string, loc := #caller_location) {
for y in 0 ..< bmp.height {
for x in 0 ..< bmp.width {
if sh_pixel(bmp, x, y) == SH_BG {
testing.expectf(t, false, "%s: pixel (%d,%d) was never painted", what, x, y, loc = loc)
return
}
}
}
}
// sh_gray is the byte a linear 0..1 grey function paints at parametric
// position `s`, through the sampled colour table: the table is indexed by the
// nearest of its 256 entries, and entry i holds grey i/255.
@(private = "file")
sh_gray :: proc(s: f64) -> u8 {
return u8(clamp(int(s * 255 + 0.5), 0, 255))
}
@(private = "file")
sh_load :: proc(t: ^testing.T, doc: ^Document, src: string, loc := #caller_location) -> ^Shading {
sh, err := shading_load(doc, cf_parse(doc, src), nil)
testing.expect_value(t, err, Error.None, loc = loc)
testing.expect(t, sh != nil, "shading did not load", loc = loc)
return sh
}
@(private = "file")
sh_load_stream :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte,
loc := #caller_location) -> ^Shading {
sh, err := shading_load(doc, cf_stream(doc, dict_src, payload), nil)
testing.expect_value(t, err, Error.None, loc = loc)
testing.expect(t, sh != nil, "shading did not load", loc = loc)
return sh
}
@(private = "file")
sh_paint :: proc(t: ^testing.T, doc: ^Document, sh: ^Shading, bmp: ^Bitmap,
ctm := MATRIX_IDENTITY, loc := #caller_location) {
err := shading_paint(doc, sh, bmp, ctm,
Raster_Clip{0, 0, bmp.width, bmp.height}, 1, .Normal, nil)
testing.expect_value(t, err, Error.None, loc = loc)
}
// A linear grey ramp: f(t) = t.
@(private = "file")
RAMP :: "/Function << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>"
// ---------------------------------------------------------------------------
// Type 2: axial
// ---------------------------------------------------------------------------
// The axis runs from device x = 2.5 to x = 10.5, so t is exactly 0, 0.5 and 1
// at the centres of pixels 2, 6 and 10 rather than somewhere between them.
@(private = "file")
AXIAL_SRC :: "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [2.5 0 10.5 0] " + RAMP
@(test)
test_shading_axial_endpoints :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, AXIAL_SRC + " /Extend [false false] >>")
bmp := sh_target(t, 13, 3)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_gray(t, &bmp, 2, 1, 0, 0, "t = 0")
expect_gray(t, &bmp, 6, 1, 128, 0, "t = 0.5")
expect_gray(t, &bmp, 10, 1, 255, 0, "t = 1")
// Every row sees the same colour: the axis is horizontal.
expect_gray(t, &bmp, 6, 0, 128, 0, "t = 0.5, first row")
expect_gray(t, &bmp, 6, 2, 128, 0, "t = 0.5, last row")
// Quarter points, to catch an axis that lands right but runs at the wrong
// rate.
expect_gray(t, &bmp, 4, 1, sh_gray(0.25), 1, "t = 0.25")
expect_gray(t, &bmp, 8, 1, sh_gray(0.75), 1, "t = 0.75")
}
@(test)
test_shading_axial_extend_false_leaves_ends_alone :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, AXIAL_SRC + " /Extend [false false] >>")
bmp := sh_target(t, 13, 1)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_untouched(t, &bmp, 0, 0, "before the axis")
expect_untouched(t, &bmp, 1, 0, "before the axis")
expect_untouched(t, &bmp, 11, 0, "past the axis")
expect_untouched(t, &bmp, 12, 0, "past the axis")
expect_gray(t, &bmp, 2, 0, 0, 0, "the first pixel on the axis")
expect_gray(t, &bmp, 10, 0, 255, 0, "the last pixel on the axis")
}
@(test)
test_shading_axial_extend_true_paints_ends :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, AXIAL_SRC + " /Extend [true true] >>")
bmp := sh_target(t, 13, 1)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_gray(t, &bmp, 0, 0, 0, 0, "extended start")
expect_gray(t, &bmp, 1, 0, 0, 0, "extended start")
expect_gray(t, &bmp, 11, 0, 255, 0, "extended end")
expect_gray(t, &bmp, 12, 0, 255, 0, "extended end")
}
@(test)
test_shading_axial_extend_one_end_only :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, AXIAL_SRC + " /Extend [false true] >>")
bmp := sh_target(t, 13, 1)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_untouched(t, &bmp, 0, 0, "start is not extended")
expect_gray(t, &bmp, 12, 0, 255, 0, "end is extended")
}
// /Domain remaps the parametric variable before the function sees it, so the
// far end of the axis asks the ramp for 0.5 and gets mid grey.
@(test)
test_shading_axial_domain :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [2.5 0 10.5 0] /Domain [0 0.5] " +
RAMP + " >>")
bmp := sh_target(t, 13, 1)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_gray(t, &bmp, 2, 0, 0, 0, "domain start")
expect_gray(t, &bmp, 10, 0, 128, 0, "domain end is t = 0.5")
expect_gray(t, &bmp, 6, 0, sh_gray(0.25), 1, "domain midpoint is t = 0.25")
}
// The same picture through a CTM: the shading's coordinates are half as big
// and the matrix doubles them, so the pixels must come out where they did
// before.
@(test)
test_shading_axial_under_ctm :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [1.25 0 5.25 0] " + RAMP +
" /Extend [false false] >>")
bmp := sh_target(t, 13, 1)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp, scale_matrix(2, 2))
expect_gray(t, &bmp, 2, 0, 0, 0, "t = 0")
expect_gray(t, &bmp, 6, 0, 128, 0, "t = 0.5")
expect_gray(t, &bmp, 10, 0, 255, 0, "t = 1")
expect_untouched(t, &bmp, 12, 0, "past the axis")
}
@(test)
test_shading_axial_vertical :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0.5 0 8.5] " + RAMP + " >>")
bmp := sh_target(t, 3, 9)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_gray(t, &bmp, 1, 0, 0, 0, "top of the axis")
expect_gray(t, &bmp, 1, 4, 128, 0, "middle of the axis")
expect_gray(t, &bmp, 1, 8, 255, 0, "bottom of the axis")
}
@(test)
test_shading_axial_degenerate_axis_paints_nothing :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [4 4 4 4] " + RAMP +
" /Extend [true true] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
for y in 0 ..< 8 {
for x in 0 ..< 8 {
expect_untouched(t, &bmp, x, y, "zero-length axis")
}
}
}
@(test)
test_shading_axial_rgb :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Three one-output functions, the other legal spelling of /Function.
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0.5 0 8.5 0] /Function [" +
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [0] /N 1 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>] >>")
bmp := sh_target(t, 9, 1)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_pixel(t, &bmp, 0, 0, [3]u8{0, 0, 255}, 0, "t = 0 is blue")
expect_pixel(t, &bmp, 8, 0, [3]u8{255, 0, 0}, 0, "t = 1 is red")
expect_pixel(t, &bmp, 4, 0, [3]u8{128, 0, 127}, 1, "t = 0.5 is purple")
}
// A /Function array is indexed by colour component, so a sub-function that
// will not load has to hold its slot. Dropping it moves every later function
// down one component and repaints the whole shading -- red where the file
// asked for green -- which is far worse than the one missing component the
// header promises.
@(test)
test_shading_broken_subfunction_keeps_its_slot :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Red will not load; green ramps to 1 and blue to 0.5.
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0.5 0 8.5 0] /Function [" +
"<< /FunctionType 99 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [0.5] /N 1 >>] >>")
if sh == nil { return }
testing.expect_value(t, len(sh.funcs), 3)
testing.expect(t, sh.funcs[0] == nil, "the broken entry is a nil placeholder")
one := [1]f64{1}
rgb := shade_eval(sh, one[:])
testing.expect_value(t, rgb[0], 0.0)
testing.expect_value(t, rgb[1], 1.0)
testing.expect_value(t, rgb[2], 0.5)
bmp := sh_target(t, 9, 1)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_pixel(t, &bmp, 8, 0, [3]u8{0, 255, 128}, 1, "t = 1: no red, full green, half blue")
expect_pixel(t, &bmp, 0, 0, [3]u8{0, 0, 0}, 1, "t = 0: every ramp starts at zero")
}
// The same defect at its worst: with only one survivor the array would shrink
// to length 1, which is the spelling that means "one function supplying every
// component" -- so the blue ramp would be evaluated into all three and paint
// the shading red.
@(test)
test_shading_one_surviving_subfunction_is_not_promoted :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0.5 0 8.5 0] /Function [" +
"<< /FunctionType 99 >> << /FunctionType 99 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [0.5] /N 1 >>] >>")
if sh == nil { return }
testing.expect_value(t, len(sh.funcs), 3)
one := [1]f64{1}
rgb := shade_eval(sh, one[:])
testing.expect_value(t, rgb[0], 0.0)
testing.expect_value(t, rgb[1], 0.0)
testing.expect_value(t, rgb[2], 0.5)
bmp := sh_target(t, 9, 1)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_pixel(t, &bmp, 8, 0, [3]u8{0, 0, 128}, 1, "the survivor stays on blue")
}
// Nothing in the array loads: an axial shading with no usable function has no
// colour at all, and there is nothing to paint with.
@(test)
test_shading_all_subfunctions_broken_fails :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh, err := shading_load(doc, cf_parse(doc,
"<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0.5 0 8.5 0] /Function [" +
"<< /FunctionType 99 >> << /FunctionType 99 >> << /FunctionType 99 >>] >>"), nil)
testing.expect_value(t, err, Error.Bad_Shading)
testing.expect(t, sh == nil, "a shading with no usable function must not load")
}
// ---------------------------------------------------------------------------
// Type 3: radial
// ---------------------------------------------------------------------------
// Concentric circles with a zero inner radius: the ordinary "glow" gradient,
// and the case where s is simply the distance from the centre over the radius.
@(test)
test_shading_radial_zero_inner_radius :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 0 4 4 4] " + RAMP +
" /Extend [false false] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
// (4.5, 4.5) is sqrt(0.5) from the centre, so s = 0.1768.
expect_gray(t, &bmp, 4, 4, sh_gray(0.176777), 1, "near the centre")
// (2.5, 4.5) is sqrt(2.5) away, s = 0.3953.
expect_gray(t, &bmp, 2, 4, sh_gray(0.395285), 1, "half way out")
// (0.5, 0.5) is 4.95 away: past the outer circle, and not extended.
expect_untouched(t, &bmp, 0, 0, "outside the last circle")
expect_untouched(t, &bmp, 7, 7, "outside the last circle")
}
// Concentric circles with a non-zero inner radius are nested, and the hole in
// the middle belongs to s < 0: unpainted unless the first end is extended.
@(test)
test_shading_radial_nested_concentric :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
src :: "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 2 4 4 4] " + RAMP
sh := sh_load(t, doc, src + " /Extend [false false] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_untouched(t, &bmp, 4, 4, "inside the inner circle")
// (7.5, 4.5) is 3.5355 from the centre: s = (3.5355 - 2) / 2.
expect_gray(t, &bmp, 7, 4, sh_gray(0.767767), 1, "between the circles")
expect_untouched(t, &bmp, 0, 0, "outside the outer circle")
sh2 := sh_load(t, doc, src + " /Extend [true true] >>")
bmp2 := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp2)
sh_paint(t, doc, sh2, &bmp2)
expect_gray(t, &bmp2, 4, 4, 0, 0, "the hole is filled by the first extension")
expect_gray(t, &bmp2, 0, 0, 255, 0, "the outside is filled by the second")
expect_all_painted(t, &bmp2, "extended at both ends")
}
// Nested but not concentric: the small circle sits off to one side, which is
// the case that separates a real quadratic solve from "distance over radius".
@(test)
test_shading_radial_nested_offset :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [3 4 1 4 4 4] " + RAMP +
" /Extend [false false] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
// At (4.5, 4.5): a = -8, b = 4.5, c = 1.5, so s = (4.5 - sqrt(32.25)) / -8.
expect_gray(t, &bmp, 4, 4, sh_gray(0.147361), 1, "inside the family")
// At (0.5, 0.5) the larger root is 1.418 (past the end, not extended) and
// the smaller one is -1.543, where the circle radius has gone negative.
expect_untouched(t, &bmp, 0, 0, "outside the outer circle")
}
// A cone: circles growing from a point. Points outside it give a negative
// discriminant and must stay unpainted rather than take an end colour.
@(test)
test_shading_radial_cone_and_root_choice :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
src :: "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [0 4 0 8 4 4] " + RAMP
sh := sh_load(t, doc, src + " /Extend [false false] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
// (1.5, 0.5) is well outside the cone: b*b - a*c is -552.
expect_untouched(t, &bmp, 1, 0, "outside the cone")
expect_untouched(t, &bmp, 1, 7, "outside the cone")
// (2.5, 4.5) is inside the narrow end, near the apex: s = 0.6121. The
// apex is a circle of radius zero, and the painted region reaches back to
// it, so nothing may clip the cone off short of it.
expect_gray(t, &bmp, 2, 4, sh_gray(0.612146), 1, "the narrow end of the cone")
// (4.5, 4.5) is inside. The larger root is 1.118, which /Extend rejects,
// so the smaller root 0.382 is the one that paints.
expect_gray(t, &bmp, 4, 4, sh_gray(0.382005), 1, "smaller root, end not extended")
// With the far end extended the larger root is accepted and clamps to 1.
sh2 := sh_load(t, doc, src + " /Extend [false true] >>")
bmp2 := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp2)
sh_paint(t, doc, sh2, &bmp2)
expect_gray(t, &bmp2, 4, 4, 255, 0, "larger root wins when the end is extended")
}
@(test)
test_shading_radial_under_ctm :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [2 2 0 2 2 2] " + RAMP + " >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp, scale_matrix(2, 2))
// Device (4.5, 4.5) maps back to (2.25, 2.25), sqrt(0.125) from the
// centre, over a radius of 2.
expect_gray(t, &bmp, 4, 4, sh_gray(0.176777), 1, "near the centre")
expect_untouched(t, &bmp, 0, 0, "outside the last circle")
}
// ---------------------------------------------------------------------------
// Type 1: function-based
// ---------------------------------------------------------------------------
// /Matrix maps the domain onto the page. Here it scales the unit square to
// 8x8 device pixels, so a function of the domain's x is a horizontal ramp.
// The function is `{ pop }`: the inputs arrive as x then y, so dropping the
// top of the stack leaves x.
@(private = "file")
sh_type1 :: proc(t: ^testing.T, doc: ^Document, dict_src: string) -> ^Shading {
d, ok := to_dict(cf_parse(doc, dict_src))
testing.expect(t, ok, "type 1 fixture is not a dictionary")
fn := cf_stream(doc, "<< /FunctionType 4 /Domain [0 1 0 1] /Range [0 1] >>", cf_bytes("{ pop }"))
dict_set(d, "Function", fn)
sh, err := shading_load(doc, d, nil)
testing.expect_value(t, err, Error.None)
return sh
}
@(test)
test_shading_function_based_matrix :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_type1(t, doc,
"<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 1 0 1] /Matrix [8 0 0 8 0 0] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_all_painted(t, &bmp, "the matrix covers the whole bitmap")
for x in 0 ..< 8 {
expect_gray(t, &bmp, x, 3, sh_gray((f64(x) + 0.5) / 8), 1, "ramp across the domain")
}
// The function ignores y, so a column is flat.
expect_gray(t, &bmp, 4, 0, sh_gray(4.5 / 8), 1, "first row")
expect_gray(t, &bmp, 4, 7, sh_gray(4.5 / 8), 1, "last row")
}
// Without a /Matrix the domain is one unit square at the origin, so only the
// first pixel is inside it. This is the same shading as above with the matrix
// removed: if /Matrix were being ignored, the test above would paint nothing.
@(test)
test_shading_function_based_domain_clips :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_type1(t, doc, "<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 1 0 1] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_gray(t, &bmp, 0, 0, sh_gray(0.5), 1, "the one pixel inside the domain")
expect_untouched(t, &bmp, 1, 0, "outside the domain in x")
expect_untouched(t, &bmp, 0, 1, "outside the domain in y")
expect_untouched(t, &bmp, 7, 7, "outside the domain")
}
// Half a domain paints half the area.
@(test)
test_shading_function_based_half_domain :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_type1(t, doc,
"<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 0.5 0 1] /Matrix [8 0 0 8 0 0] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_gray(t, &bmp, 3, 3, sh_gray(3.5 / 8), 1, "inside the domain")
expect_untouched(t, &bmp, 4, 3, "past the domain's x")
expect_untouched(t, &bmp, 7, 0, "past the domain's x")
}
// ---------------------------------------------------------------------------
// Types 4 and 5: triangle meshes
//
// Every mesh below uses 16-bit coordinates with /Decode [0 65535 ...], which
// makes a coordinate equal to the integer written in the stream, and 8-bit
// colour components and flags, which makes every field a whole byte. The
// payloads are then readable as written rather than as a bit-packing puzzle.
// ---------------------------------------------------------------------------
@(private = "file")
MESH_DICT :: "/ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " +
"/BitsPerFlag 8 /Decode [0 65535 0 65535 0 1] >>"
// Both triangles of an 8x8 square, coloured so that grey is x/8 in each: the
// two together must reproduce one seamless horizontal ramp.
@(test)
test_shading_type4_shared_edge :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, (0,0), grey 0
0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, // flag 0, (8,0), grey 1
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, // flag 0, (0,8), grey 0
0x01, 0x00, 0x08, 0x00, 0x08, 0xFF, // flag 1, (8,8), grey 1
}
sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
// Nothing is missed along the shared diagonal, where eight pixel centres
// land exactly on the edge both triangles own half of.
expect_all_painted(t, &bmp, "two triangles tiling the square")
for y in 0 ..< 8 {
for x in 0 ..< 8 {
expect_gray(t, &bmp, x, y, sh_gray((f64(x) + 0.5) / 8), 1, "Gouraud ramp")
}
}
}
// The same square, built the other way: a lattice of two rows of two.
@(test)
test_shading_type5_lattice :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0
0x00, 0x08, 0x00, 0x00, 0xFF, // (8,0) grey 1
0x00, 0x00, 0x00, 0x08, 0x00, // (0,8) grey 0
0x00, 0x08, 0x00, 0x08, 0xFF, // (8,8) grey 1
}
sh := sh_load_stream(t, doc,
"<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " +
"/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_all_painted(t, &bmp, "a two-by-two lattice")
for y in 0 ..< 8 {
for x in 0 ..< 8 {
expect_gray(t, &bmp, x, y, sh_gray((f64(x) + 0.5) / 8), 1, "Gouraud ramp")
}
}
}
// A three-column lattice whose middle column is pulled to one side: the rows
// are no longer a rectangle, which is what a lattice is for.
@(test)
test_shading_type5_three_columns :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0
0x00, 0x04, 0x00, 0x00, 0x80, // (4,0) grey 0.5
0x00, 0x08, 0x00, 0x00, 0xFF, // (8,0) grey 1
0x00, 0x00, 0x00, 0x08, 0x00, // (0,8) grey 0
0x00, 0x04, 0x00, 0x08, 0x80, // (4,8) grey 0.5
0x00, 0x08, 0x00, 0x08, 0xFF, // (8,8) grey 1
}
sh := sh_load_stream(t, doc,
"<< /ShadingType 5 /VerticesPerRow 3 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " +
"/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_all_painted(t, &bmp, "a two-by-three lattice")
// 0x80 is 128/255, so the middle column is a shade off exact mid grey and
// the ramp has a slight kink in it -- which is what the vertices say.
mid := 128.0 / 255.0
expect_gray(t, &bmp, 0, 4, sh_gray(0.125 * mid), 1, "left half")
expect_gray(t, &bmp, 4, 4, sh_gray(mid + 0.125 * (1 - mid)), 1, "right half")
}
// With a /Function the stream carries one parametric value per vertex instead
// of one value per colour component, which changes the size of every vertex.
@(test)
test_shading_type4_with_function :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, (0,0), t = 0
0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, // flag 0, (8,0), t = 1
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, // flag 0, (0,8), t = 0
0x01, 0x00, 0x08, 0x00, 0x08, 0xFF, // flag 1, (8,8), t = 1
}
sh := sh_load_stream(t, doc,
"<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 /BitsPerComponent 8 " +
"/BitsPerFlag 8 /Decode [0 65535 0 65535 0 1] " +
"/Function << /FunctionType 2 /Domain [0 1] /C0 [0 0 1] /C1 [1 0 0] /N 1 >> >>",
payload[:])
testing.expect_value(t, sh.n_comp, 1)
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_all_painted(t, &bmp, "a mesh coloured through a function")
for x in 0 ..< 8 {
s := (f64(x) + 0.5) / 8
expect_pixel(t, &bmp, x, 4, [3]u8{sh_gray(s), 0, sh_gray(1 - s)}, 2, "blue to red")
}
}
// A stream that stops in the middle of a vertex draws what it had and stops.
@(test)
test_shading_type4_truncated :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, (0,0), grey 0
0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, // flag 0, (8,0), grey 1
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, // flag 0, (0,8), grey 0
0x01, 0x00, 0x08, 0x00, // flag 1, and then the file ends
}
sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
// The first triangle is complete and is drawn; the second never arrives.
expect_gray(t, &bmp, 0, 0, sh_gray(0.5 / 8), 1, "the complete triangle")
expect_gray(t, &bmp, 6, 0, sh_gray(6.5 / 8), 1, "the complete triangle")
expect_untouched(t, &bmp, 7, 7, "the truncated triangle")
expect_untouched(t, &bmp, 5, 5, "the truncated triangle")
}
// A mesh stream that ends between two whole vertices is not an error either.
@(test)
test_shading_type5_partial_row :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0
0x00, 0x08, 0x00, 0x00, 0xFF, // (8,0) grey 1
0x00, 0x00, 0x00, 0x08, 0x00, // (0,8) grey 0 -- and no partner
}
sh := sh_load_stream(t, doc,
"<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " +
"/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
// Half a row has no triangles in it, so nothing is drawn at all.
for y in 0 ..< 8 {
for x in 0 ..< 8 {
expect_untouched(t, &bmp, x, y, "an incomplete lattice row")
}
}
}
@(test)
test_shading_mesh_empty_stream :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
empty: [0]u8
sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, empty[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_untouched(t, &bmp, 4, 4, "an empty mesh")
}
// ---------------------------------------------------------------------------
// Types 6 and 7: Coons and tensor patches
// ---------------------------------------------------------------------------
@(private = "file")
PATCH_DICT :: "/ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " +
"/BitsPerFlag 8 /Decode [0 65535 0 65535 0 1] >>"
// A flat Coons patch: every control point sits on the bilinear surface of a
// 24x24 square, spaced in thirds, and the corner colours vary in x alone so
// that the patch's bilinear colour and a triangle mesh's linear one agree
// everywhere. It must then paint exactly what the equivalent lattice paints.
@(test)
test_shading_type6_flat_patch_matches_mesh :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
patch := [?]u8{
0x00, // flag 0: a whole patch follows
0x00, 0x00, 0x00, 0x00, // p1 (0,0)
0x00, 0x08, 0x00, 0x00, // p2 (8,0)
0x00, 0x10, 0x00, 0x00, // p3 (16,0)
0x00, 0x18, 0x00, 0x00, // p4 (24,0)
0x00, 0x18, 0x00, 0x08, // p5 (24,8)
0x00, 0x18, 0x00, 0x10, // p6 (24,16)
0x00, 0x18, 0x00, 0x18, // p7 (24,24)
0x00, 0x10, 0x00, 0x18, // p8 (16,24)
0x00, 0x08, 0x00, 0x18, // p9 (8,24)
0x00, 0x00, 0x00, 0x18, // p10 (0,24)
0x00, 0x00, 0x00, 0x10, // p11 (0,16)
0x00, 0x00, 0x00, 0x08, // p12 (0,8)
0x00, 0xFF, 0xFF, 0x00, // c1 at p1, c2 at p4, c3 at p7, c4 at p10
}
sh := sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, patch[:])
bmp := sh_target(t, 24, 24)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
mesh := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0
0x00, 0x18, 0x00, 0x00, 0xFF, // (24,0) grey 1
0x00, 0x00, 0x00, 0x18, 0x00, // (0,24) grey 0
0x00, 0x18, 0x00, 0x18, 0xFF, // (24,24) grey 1
}
sh2 := sh_load_stream(t, doc,
"<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " +
"/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", mesh[:])
ref := sh_target(t, 24, 24)
defer bitmap_destroy(&ref)
sh_paint(t, doc, sh2, &ref)
expect_all_painted(t, &bmp, "the patch covers its square")
expect_all_painted(t, &ref, "the mesh covers its square")
for y in 0 ..< 24 {
for x in 0 ..< 24 {
expect_pixel(t, &bmp, x, y, sh_pixel(&ref, x, y), 1, "patch against mesh")
}
}
// And both are the ramp they were built to be, so a pair of matching but
// equally wrong pictures cannot pass.
expect_gray(t, &bmp, 0, 12, sh_gray(0.5 / 24), 1, "left edge")
expect_gray(t, &bmp, 23, 12, sh_gray(23.5 / 24), 1, "right edge")
}
// The same square as a tensor patch: the four interior control points are
// written out instead of derived, and put where the Coons rule would have put
// them, so the picture must not change.
@(test)
test_shading_type7_matches_coons :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
coons := [?]u8{
0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00,
0x00, 0x18, 0x00, 0x08, 0x00, 0x18, 0x00, 0x10,
0x00, 0x18, 0x00, 0x18, 0x00, 0x10, 0x00, 0x18,
0x00, 0x08, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08,
0x00, 0xFF, 0xFF, 0x00,
}
tensor := [?]u8{
0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00,
0x00, 0x18, 0x00, 0x08, 0x00, 0x18, 0x00, 0x10,
0x00, 0x18, 0x00, 0x18, 0x00, 0x10, 0x00, 0x18,
0x00, 0x08, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08,
// p13 [1][1], p14 [1][2], p15 [2][2], p16 [2][1]
0x00, 0x08, 0x00, 0x08, 0x00, 0x10, 0x00, 0x08,
0x00, 0x10, 0x00, 0x10, 0x00, 0x08, 0x00, 0x10,
0x00, 0xFF, 0xFF, 0x00,
}
a := sh_target(t, 24, 24)
defer bitmap_destroy(&a)
sh_paint(t, doc, sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, coons[:]), &a)
b := sh_target(t, 24, 24)
defer bitmap_destroy(&b)
sh_paint(t, doc, sh_load_stream(t, doc, "<< /ShadingType 7 " + PATCH_DICT, tensor[:]), &b)
expect_all_painted(t, &a, "the Coons patch")
for y in 0 ..< 24 {
for x in 0 ..< 24 {
expect_pixel(t, &b, x, y, sh_pixel(&a, x, y), 1, "tensor against Coons")
}
}
}
// Two patches, the second continuing from the first's D1 edge with a flag of
// 1. The pair tiles a 24x24 square with a ramp that runs up and back down, and
// the seam down the middle must be complete.
@(test)
test_shading_type6_shared_edge :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
// Patch 1: x from 0 to 12, black at the left edge, white at x = 12.
0x00,
0x00, 0x00, 0x00, 0x00, // p1 (0,0)
0x00, 0x04, 0x00, 0x00, // p2 (4,0)
0x00, 0x08, 0x00, 0x00, // p3 (8,0)
0x00, 0x0C, 0x00, 0x00, // p4 (12,0)
0x00, 0x0C, 0x00, 0x08, // p5 (12,8)
0x00, 0x0C, 0x00, 0x10, // p6 (12,16)
0x00, 0x0C, 0x00, 0x18, // p7 (12,24)
0x00, 0x08, 0x00, 0x18, // p8 (8,24)
0x00, 0x04, 0x00, 0x18, // p9 (4,24)
0x00, 0x00, 0x00, 0x18, // p10 (0,24)
0x00, 0x00, 0x00, 0x10, // p11 (0,16)
0x00, 0x00, 0x00, 0x08, // p12 (0,8)
0x00, 0xFF, 0xFF, 0x00, // c1..c4
// Patch 2: flag 1 takes p4 p5 p6 p7 and c2 c3 as its first edge, so
// only eight points and two colours are written.
0x01,
0x00, 0x10, 0x00, 0x18, // (16,24)
0x00, 0x14, 0x00, 0x18, // (20,24)
0x00, 0x18, 0x00, 0x18, // (24,24)
0x00, 0x18, 0x00, 0x10, // (24,16)
0x00, 0x18, 0x00, 0x08, // (24,8)
0x00, 0x18, 0x00, 0x00, // (24,0)
0x00, 0x14, 0x00, 0x00, // (20,0)
0x00, 0x10, 0x00, 0x00, // (16,0)
0x00, 0x00, // c3, c4: black again at x = 24
}
sh := sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, payload[:])
bmp := sh_target(t, 24, 24)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_all_painted(t, &bmp, "two patches sharing an edge")
expect_gray(t, &bmp, 0, 12, sh_gray(0.5 / 12), 1, "start of the first patch")
expect_gray(t, &bmp, 11, 12, sh_gray(11.5 / 12), 1, "end of the first patch")
expect_gray(t, &bmp, 12, 12, sh_gray(11.5 / 12), 1, "start of the second patch")
expect_gray(t, &bmp, 23, 12, sh_gray(0.5 / 12), 1, "end of the second patch")
expect_gray(t, &bmp, 5, 3, sh_gray(5.5 / 12), 1, "first patch, another row")
expect_gray(t, &bmp, 18, 20, sh_gray(5.5 / 12), 1, "second patch, another row")
}
// A patch with curved edges still has to stay inside the box its control
// points span, and cover the corners it is anchored at.
@(test)
test_shading_type6_curved_patch :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// The top edge bulges downwards: p2 and p3 are pulled to y = 12 instead of
// sitting on the straight edge at y = 0.
payload := [?]u8{
0x00,
0x00, 0x00, 0x00, 0x00, // p1 (0,0)
0x00, 0x08, 0x00, 0x0C, // p2 (8,12)
0x00, 0x10, 0x00, 0x0C, // p3 (16,12)
0x00, 0x18, 0x00, 0x00, // p4 (24,0)
0x00, 0x18, 0x00, 0x08, // p5 (24,8)
0x00, 0x18, 0x00, 0x10, // p6 (24,16)
0x00, 0x18, 0x00, 0x18, // p7 (24,24)
0x00, 0x10, 0x00, 0x18, // p8 (16,24)
0x00, 0x08, 0x00, 0x18, // p9 (8,24)
0x00, 0x00, 0x00, 0x18, // p10 (0,24)
0x00, 0x00, 0x00, 0x10, // p11 (0,16)
0x00, 0x00, 0x00, 0x08, // p12 (0,8)
0x00, 0xFF, 0xFF, 0x00,
}
sh := sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, payload[:])
bmp := sh_target(t, 24, 24)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
// The top edge is x = 24v, y = 36v(1-v): it dives away from the corner at
// once, so the whole top of the square is outside the patch.
expect_untouched(t, &bmp, 12, 1, "under the bulge")
expect_untouched(t, &bmp, 12, 8, "under the bulge")
expect_untouched(t, &bmp, 0, 0, "beside the bulge, where the edge has already dropped")
// The bottom edge is straight and its colour runs left to right along it.
for x in 0 ..< 24 {
testing.expectf(t, sh_pixel(&bmp, x, 23) != SH_BG,
"the bottom row of a curved patch: pixel (%d,23) was never painted", x)
}
left := int(sh_pixel(&bmp, 1, 23)[0])
mid := int(sh_pixel(&bmp, 12, 23)[0])
right := int(sh_pixel(&bmp, 22, 23)[0])
testing.expectf(t, left < mid && mid < right,
"the ramp should run left to right along the bottom edge: %d %d %d", left, mid, right)
testing.expectf(t, abs(mid - 128) <= 24, "the middle of the bottom edge is mid grey: %d", mid)
}
// ---------------------------------------------------------------------------
// Clipping, alpha, blend mode and soft mask
// ---------------------------------------------------------------------------
// A shading that is one flat colour everywhere, for testing what happens to it
// on the way into the bitmap rather than what colour it computes.
@(private = "file")
FLAT_HALF :: "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /Extend [true true] " +
"/Function << /FunctionType 2 /Domain [0 1] /C0 [0.5] /C1 [0.5] /N 1 >> >>"
@(private = "file")
FLAT_WHITE :: "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /Extend [true true] " +
"/Function << /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [1] /N 1 >> >>"
@(test)
test_shading_paint_respects_clip :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, FLAT_WHITE)
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
err := shading_paint(doc, sh, &bmp, MATRIX_IDENTITY, Raster_Clip{2, 3, 5, 6}, 1, .Normal, nil)
testing.expect_value(t, err, Error.None)
for y in 0 ..< 8 {
for x in 0 ..< 8 {
inside := x >= 2 && x < 5 && y >= 3 && y < 6
if inside {
expect_gray(t, &bmp, x, y, 255, 0, "inside the clip")
} else {
expect_untouched(t, &bmp, x, y, "outside the clip")
}
}
}
}
@(test)
test_shading_paint_clip_outside_bitmap :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, FLAT_WHITE)
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
// A clip larger than the bitmap is clamped, not trusted.
testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY,
Raster_Clip{-100, -100, 1000, 1000}, 1, .Normal, nil), Error.None)
expect_gray(t, &bmp, 0, 0, 255, 0, "painted")
expect_gray(t, &bmp, 7, 7, 255, 0, "painted")
// And one entirely off it paints nothing.
bmp2 := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp2)
testing.expect_value(t, shading_paint(doc, sh, &bmp2, MATRIX_IDENTITY,
Raster_Clip{20, 20, 30, 30}, 1, .Normal, nil), Error.None)
expect_untouched(t, &bmp2, 4, 4, "a clip off the bitmap")
}
@(test)
test_shading_paint_alpha :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, FLAT_WHITE)
bmp := sh_target(t, 4, 4)
defer bitmap_destroy(&bmp)
bitmap_fill(&bmp, [4]u8{0, 0, 0, 255})
testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY,
Raster_Clip{0, 0, 4, 4}, 0.5, .Normal, nil), Error.None)
expect_gray(t, &bmp, 1, 1, 128, 1, "half of white over black")
// Zero alpha is not "paint nothing visible", it is "do not touch".
bmp2 := sh_target(t, 4, 4)
defer bitmap_destroy(&bmp2)
testing.expect_value(t, shading_paint(doc, sh, &bmp2, MATRIX_IDENTITY,
Raster_Clip{0, 0, 4, 4}, 0, .Normal, nil), Error.None)
expect_untouched(t, &bmp2, 2, 2, "alpha 0")
}
@(test)
test_shading_paint_blend_mode :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, FLAT_HALF)
bmp := sh_target(t, 4, 4)
defer bitmap_destroy(&bmp)
bitmap_fill(&bmp, [4]u8{255, 0, 0, 255})
testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY,
Raster_Clip{0, 0, 4, 4}, 1, .Multiply, nil), Error.None)
// Multiply against red keeps the red channel and kills the other two;
// Normal would have painted a flat grey.
expect_pixel(t, &bmp, 2, 2, [3]u8{128, 0, 0}, 1, "half grey multiplied over red")
}
@(test)
test_shading_paint_soft_mask :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, FLAT_WHITE)
bmp := sh_target(t, 8, 2)
defer bitmap_destroy(&bmp)
bitmap_fill(&bmp, [4]u8{0, 0, 0, 255})
mask, mask_err := bitmap_make(8, 2, 1)
testing.expect_value(t, mask_err, Error.None)
defer bitmap_destroy(&mask)
for y in 0 ..< 2 {
row := bitmap_row(&mask, y)
for x in 0 ..< 8 {
row[x] = x < 3 ? 0 : (x < 6 ? 128 : 255)
}
}
testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY,
Raster_Clip{0, 0, 8, 2}, 1, .Normal, &mask), Error.None)
expect_gray(t, &bmp, 0, 0, 0, 0, "masked out entirely")
expect_gray(t, &bmp, 4, 0, 128, 1, "half masked")
expect_gray(t, &bmp, 7, 0, 255, 0, "not masked")
}
// A mask narrower than the clip must show the content it does not cover
// rather than erase it.
@(test)
test_shading_paint_short_mask :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, FLAT_WHITE)
bmp := sh_target(t, 8, 4)
defer bitmap_destroy(&bmp)
bitmap_fill(&bmp, [4]u8{0, 0, 0, 255})
mask, mask_err := bitmap_make(3, 1, 1)
testing.expect_value(t, mask_err, Error.None)
defer bitmap_destroy(&mask)
bitmap_clear(&mask, 0)
testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY,
Raster_Clip{0, 0, 8, 4}, 1, .Normal, &mask), Error.None)
expect_gray(t, &bmp, 0, 0, 0, 0, "covered by the mask")
expect_gray(t, &bmp, 5, 0, 255, 0, "past the mask in x")
expect_gray(t, &bmp, 0, 3, 255, 0, "past the mask in y")
}
// ---------------------------------------------------------------------------
// Degenerate transforms and arguments
// ---------------------------------------------------------------------------
@(test)
test_shading_singular_ctm_paints_nothing :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
singular := [?]Matrix{
{0, 0, 0, 0, 0, 0}, // collapses everything to a point
{1, 0, 0, 0, 0, 0}, // flattens y
{0, 0, 0, 1, 0, 0}, // flattens x
{2, 4, 1, 2, 3, 3}, // rank 1: the rows are parallel
}
sources := [?]string{
FLAT_WHITE,
"<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 0 4 4 4] " + RAMP +
" /Extend [true true] >>",
}
for src in sources {
sh := sh_load(t, doc, src)
for m in singular {
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
testing.expect_value(t, shading_paint(doc, sh, &bmp, m,
Raster_Clip{0, 0, 8, 8}, 1, .Normal, nil), Error.None)
for y in 0 ..< 8 {
for x in 0 ..< 8 {
expect_untouched(t, &bmp, x, y, "a singular CTM")
}
}
}
}
}
@(test)
test_shading_singular_ctm_mesh :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x08, 0x00,
0x01, 0x00, 0x08, 0x00, 0x08, 0xFF,
}
sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
testing.expect_value(t, shading_paint(doc, sh, &bmp, Matrix{0, 0, 0, 0, 0, 0},
Raster_Clip{0, 0, 8, 8}, 1, .Normal, nil), Error.None)
expect_untouched(t, &bmp, 4, 4, "a singular CTM on a mesh")
}
@(test)
test_shading_non_finite_ctm_paints_nothing :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, FLAT_WHITE)
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
big := f64(1e308)
inf := big * 10
testing.expect_value(t, shading_paint(doc, sh, &bmp, Matrix{1, 0, 0, 1, inf, 0},
Raster_Clip{0, 0, 8, 8}, 1, .Normal, nil), Error.None)
expect_untouched(t, &bmp, 4, 4, "an infinite CTM")
}
@(test)
test_shading_paint_bad_arguments :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc, FLAT_WHITE)
bmp := sh_target(t, 4, 4)
defer bitmap_destroy(&bmp)
testing.expect_value(t, shading_paint(doc, nil, &bmp, MATRIX_IDENTITY,
Raster_Clip{0, 0, 4, 4}, 1, .Normal, nil), Error.Invalid_Argument)
testing.expect_value(t, shading_paint(doc, sh, nil, MATRIX_IDENTITY,
Raster_Clip{0, 0, 4, 4}, 1, .Normal, nil), Error.Invalid_Argument)
// An empty clip is not an error, it is a no-op.
testing.expect_value(t, shading_paint(doc, sh, &bmp, MATRIX_IDENTITY,
Raster_Clip{4, 4, 4, 4}, 1, .Normal, nil), Error.None)
expect_untouched(t, &bmp, 0, 0, "an empty clip")
}
// A /None separation marks nothing at all.
@(test)
test_shading_none_colorspace_paints_nothing :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace [/Separation /None /DeviceGray " +
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>] " +
"/Coords [0 0 8 0] /Extend [true true] " + RAMP + " >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_untouched(t, &bmp, 4, 4, "a /None separation")
}
// ---------------------------------------------------------------------------
// /BBox
// ---------------------------------------------------------------------------
@(test)
test_shading_bbox_limits_painting :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /Extend [true true] " +
"/BBox [2 2 6 6] " + RAMP + " >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
for y in 0 ..< 8 {
for x in 0 ..< 8 {
if x >= 2 && x <= 5 && y >= 2 && y <= 5 {
testing.expectf(t, sh_pixel(&bmp, x, y) != SH_BG,
"inside /BBox: pixel (%d,%d) was never painted", x, y)
} else {
expect_untouched(t, &bmp, x, y, "outside /BBox")
}
}
}
}
@(test)
test_shading_bbox_limits_mesh :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x08, 0x00,
0x01, 0x00, 0x08, 0x00, 0x08, 0xFF,
}
sh := sh_load_stream(t, doc,
"<< /ShadingType 4 /BBox [0 0 4 8] " + MESH_DICT, payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_gray(t, &bmp, 3, 4, sh_gray(3.5 / 8), 1, "inside /BBox")
expect_untouched(t, &bmp, 4, 4, "outside /BBox")
expect_untouched(t, &bmp, 7, 0, "outside /BBox")
}
// ---------------------------------------------------------------------------
// Loading: what is refused
// ---------------------------------------------------------------------------
@(private = "file")
expect_refused :: proc(t: ^testing.T, doc: ^Document, src: string, loc := #caller_location) {
sh, err := shading_load(doc, cf_parse(doc, src), nil)
testing.expect_value(t, err, Error.Bad_Shading, loc = loc)
testing.expect(t, sh == nil, "a refused shading must not be handed back", loc = loc)
}
@(private = "file")
expect_stream_refused :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte,
loc := #caller_location) {
sh, err := shading_load(doc, cf_stream(doc, dict_src, payload), nil)
testing.expect_value(t, err, Error.Bad_Shading, loc = loc)
testing.expect(t, sh == nil, "a refused shading must not be handed back", loc = loc)
}
@(test)
test_shading_load_refuses_malformed :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
expect_refused(t, doc, "<< >>")
expect_refused(t, doc, "42")
expect_refused(t, doc, "<< /ShadingType 0 /ColorSpace /DeviceGray >>")
expect_refused(t, doc, "<< /ShadingType 8 /ColorSpace /DeviceGray >>")
// No colour space at all.
expect_refused(t, doc, "<< /ShadingType 2 /Coords [0 0 8 0] " + RAMP + " >>")
// 8.7.4.5.1: a shading's colour space shall not be a Pattern space.
expect_refused(t, doc, "<< /ShadingType 2 /ColorSpace /Pattern /Coords [0 0 8 0] " + RAMP + " >>")
// Types 1 to 3 have no colour without a function.
expect_refused(t, doc, "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] >>")
expect_refused(t, doc, "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [0 0 0 8 0 8] >>")
// Not enough coordinates.
expect_refused(t, doc, "<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8] " + RAMP + " >>")
expect_refused(t, doc, "<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [0 0 0 8 0] " + RAMP + " >>")
// A mesh type has to be a stream: a dictionary carries no vertices.
expect_refused(t, doc, "<< /ShadingType 4 " + MESH_DICT)
expect_refused(t, doc, "<< /ShadingType 6 " + PATCH_DICT)
_, nil_err := shading_load(nil, nil, nil)
testing.expect_value(t, nil_err, Error.Invalid_Argument)
}
@(test)
test_shading_load_refuses_bad_mesh_layout :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{0, 0, 0, 0, 0, 0, 0, 0}
base :: "/ColorSpace /DeviceGray /Decode [0 65535 0 65535 0 1] "
// /BitsPerCoordinate is one of 1 2 4 8 12 16 24 32.
expect_stream_refused(t, doc, "<< /ShadingType 4 " + base +
"/BitsPerCoordinate 64 /BitsPerComponent 8 /BitsPerFlag 8 >>", payload[:])
expect_stream_refused(t, doc, "<< /ShadingType 4 " + base +
"/BitsPerCoordinate 0 /BitsPerComponent 8 /BitsPerFlag 8 >>", payload[:])
// /BitsPerComponent is one of 1 2 4 8 12 16.
expect_stream_refused(t, doc, "<< /ShadingType 4 " + base +
"/BitsPerCoordinate 16 /BitsPerComponent 7 /BitsPerFlag 8 >>", payload[:])
// /BitsPerFlag is one of 2 4 8.
expect_stream_refused(t, doc, "<< /ShadingType 4 " + base +
"/BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 3 >>", payload[:])
// /Decode has to describe every field of a vertex.
expect_stream_refused(t, doc, "<< /ShadingType 4 /ColorSpace /DeviceGray " +
"/BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 /Decode [0 1 0 1] >>", payload[:])
expect_stream_refused(t, doc, "<< /ShadingType 4 /ColorSpace /DeviceRGB " +
"/BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8 /Decode [0 1 0 1 0 1] >>", payload[:])
}
// An absurd /VerticesPerRow is refused outright rather than sizing a buffer.
@(test)
test_shading_load_refuses_absurd_vertex_count :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{0, 0, 0, 0, 0, 0, 0, 0}
lattice :: "/ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " +
"/Decode [0 65535 0 65535 0 1] "
expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + "/VerticesPerRow 99999999 >>", payload[:])
expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + "/VerticesPerRow 2147483647 >>", payload[:])
expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + "/VerticesPerRow -4 >>", payload[:])
// One vertex per row cannot form a triangle.
expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + "/VerticesPerRow 1 >>", payload[:])
expect_stream_refused(t, doc, "<< /ShadingType 5 " + lattice + ">>", payload[:])
// The largest row the library will take still loads.
sh, err := shading_load(doc, cf_stream(doc,
"<< /ShadingType 5 " + lattice + "/VerticesPerRow 16384 >>", payload[:]), nil)
testing.expect_value(t, err, Error.None)
testing.expect(t, sh != nil, "a row at the limit should still load")
}
// Without a /Function a mesh vertex carries one value per colour component,
// which is what makes a DeviceRGB vertex seven bytes wide here rather than
// five.
@(test)
test_shading_mesh_multi_component_vertices :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, // (0,0) red
0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0xFF, // (8,0) blue
0x00, 0x00, 0x00, 0x08, 0xFF, 0x00, 0x00, // (0,8) red
0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0xFF, // (8,8) blue
}
sh := sh_load_stream(t, doc,
"<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceRGB /BitsPerCoordinate 16 " +
"/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1 0 1 0 1] >>", payload[:])
testing.expect_value(t, sh.n_comp, 3)
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_all_painted(t, &bmp, "an RGB lattice")
for x in 0 ..< 8 {
s := (f64(x) + 0.5) / 8
expect_pixel(t, &bmp, x, 4, [3]u8{sh_gray(1 - s), 0, sh_gray(s)}, 2, "red to blue")
}
}
// ---------------------------------------------------------------------------
// Bounds
// ---------------------------------------------------------------------------
@(test)
test_shading_bounds :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
testing.expect_value(t, shading_bounds(nil, MATRIX_IDENTITY), RECT_EMPTY)
// An axial shading is unbounded across its axis, whatever /Extend says.
axial := sh_load(t, doc, AXIAL_SRC + " >>")
testing.expect_value(t, shading_bounds(axial, MATRIX_IDENTITY), RECT_INFINITE)
// /BBox is what bounds it, and it is in the shading's own space.
boxed := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /BBox [1 2 3 4] " + RAMP + " >>")
testing.expect_value(t, shading_bounds(boxed, MATRIX_IDENTITY), Rect{1, 2, 3, 4})
testing.expect_value(t, shading_bounds(boxed, scale_matrix(2, 2)), Rect{2, 4, 6, 8})
// An unextended radial reaches no further than its two circles.
radial := sh_load(t, doc,
"<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 1 10 4 2] " + RAMP + " >>")
testing.expect_value(t, shading_bounds(radial, MATRIX_IDENTITY), Rect{3, 2, 12, 6})
testing.expect_value(t, shading_bounds(radial, scale_matrix(2, 2)), Rect{6, 4, 24, 12})
// An extended one keeps going.
radial_ext := sh_load(t, doc,
"<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [4 4 1 10 4 2] /Extend [false true] " +
RAMP + " >>")
testing.expect_value(t, shading_bounds(radial_ext, MATRIX_IDENTITY), RECT_INFINITE)
// A function-based shading is bounded by its domain through /Matrix.
fb := sh_type1(t, doc,
"<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 1 0 1] /Matrix [8 0 0 8 0 0] >>")
testing.expect_value(t, shading_bounds(fb, MATRIX_IDENTITY), Rect{0, 0, 8, 8})
testing.expect_value(t, shading_bounds(fb, translate_matrix(10, 20)), Rect{10, 20, 18, 28})
// A mesh is bounded by /Decode without reading a byte of the stream.
payload := [?]u8{0, 0, 0, 0, 0, 0}
mesh := sh_load_stream(t, doc,
"<< /ShadingType 4 /ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " +
"/BitsPerFlag 8 /Decode [10 20 30 40 0 1] >>", payload[:])
testing.expect_value(t, shading_bounds(mesh, MATRIX_IDENTITY), Rect{10, 30, 20, 40})
testing.expect_value(t, shading_bounds(mesh, scale_matrix(2, 2)), Rect{20, 60, 40, 80})
}
// A rotated CTM turns the bounding box of a mesh into the box of the rotated
// box, which is what a caller intersecting it with a clip needs.
@(test)
test_shading_bounds_rotated :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{0, 0, 0, 0, 0, 0}
mesh := sh_load_stream(t, doc,
"<< /ShadingType 4 /ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " +
"/BitsPerFlag 8 /Decode [0 10 0 20 0 1] >>", payload[:])
got := shading_bounds(mesh, rotate_matrix(90))
testing.expect(t, abs(got.x0 - (-20)) < 1e-9 && abs(got.y0 - 0) < 1e-9 &&
abs(got.x1 - 0) < 1e-9 && abs(got.y1 - 10) < 1e-9,
"a rotated mesh bound")
}
// ---------------------------------------------------------------------------
// Mesh bit packing and the remaining edge flags
// ---------------------------------------------------------------------------
// The same two triangles as test_shading_type4_shared_edge, but with a 2-bit
// flag, so a vertex is 42 bits and the reader has to pad each record out to
// the next byte (8.7.4.5.5) and pull every field across byte boundaries. The
// bytes were packed by hand from the field widths: 2 bits of flag, 16 of x,
// 16 of y, 8 of colour, then 6 bits of padding.
@(test)
test_shading_type4_bit_packed :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, (0,0), grey 0
0x00, 0x02, 0x00, 0x00, 0x3F, 0xC0, // flag 0, (8,0), grey 1
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, // flag 0, (0,8), grey 0
0x40, 0x02, 0x00, 0x02, 0x3F, 0xC0, // flag 1, (8,8), grey 1
}
sh := sh_load_stream(t, doc,
"<< /ShadingType 4 /ColorSpace /DeviceGray /BitsPerCoordinate 16 /BitsPerComponent 8 " +
"/BitsPerFlag 2 /Decode [0 65535 0 65535 0 1] >>", payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_all_painted(t, &bmp, "a bit-packed mesh")
for y in 0 ..< 8 {
for x in 0 ..< 8 {
expect_gray(t, &bmp, x, y, sh_gray((f64(x) + 0.5) / 8), 1, "Gouraud ramp")
}
}
}
// A flag of 2 keeps the previous triangle's first and third vertices, where a
// flag of 1 keeps its second and third. The two produce different triangles
// from the same bytes, which is what this pins down.
@(test)
test_shading_type4_flag_two :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 0, va (0,0), grey 0
0x00, 0x00, 0x08, 0x00, 0x00, 0xFF, // flag 0, vb (8,0), grey 1
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, // flag 0, vc (0,8), grey 0
0x02, 0x00, 0x08, 0x00, 0x08, 0xFF, // flag 2: va, vc and (8,8)
}
sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
// The second triangle is (0,0) (0,8) (8,8): below the main diagonal.
expect_gray(t, &bmp, 2, 7, sh_gray(2.5 / 8), 1, "inside the flag-2 triangle")
expect_gray(t, &bmp, 1, 1, sh_gray(1.5 / 8), 1, "inside the first triangle")
// A flag of 1 would have made it (8,0) (0,8) (8,8) and covered this pixel.
expect_untouched(t, &bmp, 6, 2, "covered by neither triangle")
}
// A continuation flag with no triangle to continue is malformed. Starting a
// fresh triangle loses the least, and must not lose the whole mesh.
@(test)
test_shading_type4_leading_continuation_flag :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // flag 1 with nothing before it
0x00, 0x00, 0x08, 0x00, 0x00, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x08, 0x00,
}
sh := sh_load_stream(t, doc, "<< /ShadingType 4 " + MESH_DICT, payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_gray(t, &bmp, 1, 1, sh_gray(1.5 / 8), 1, "the recovered triangle")
expect_untouched(t, &bmp, 7, 7, "outside it")
}
// /Background is parsed but deliberately not painted: it applies to a pattern
// fill's uncovered area, not to the shading itself.
@(test)
test_shading_background_is_not_painted :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [2.5 0 10.5 0] /Background [1] " +
RAMP + " /Extend [false false] >>")
testing.expect(t, sh.has_background, "/Background should be read")
testing.expect_value(t, len(sh.background), 1)
bmp := sh_target(t, 13, 1)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_untouched(t, &bmp, 0, 0, "before the axis")
expect_untouched(t, &bmp, 12, 0, "past the axis")
}
// ---------------------------------------------------------------------------
// The rules that only a rotated CTM or an extended end can reach
// ---------------------------------------------------------------------------
// An extension continues the family of circles beyond the ends, but only while
// the radius stays non-negative (8.7.4.5.4). Here the circles shrink to a
// point at (6,4); behind that point the radius would be negative, so the
// mirror image of the cone must stay unpainted even though both ends are
// extended.
@(test)
test_shading_radial_extension_stops_at_zero_radius :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 3 /ColorSpace /DeviceGray /Coords [6 4 0 0 4 3] " + RAMP +
" /Extend [true true] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_untouched(t, &bmp, 7, 4, "behind the apex, where the radius goes negative")
testing.expect(t, sh_pixel(&bmp, 2, 4) != SH_BG, "inside the cone")
testing.expect(t, sh_pixel(&bmp, 0, 4) != SH_BG, "the extended far end")
}
// A patch whose colour runs down the page rather than across it. The u
// direction of the patch is the one the twelve control points walk down the
// sides, so this pins the other half of the point-to-grid mapping that a
// horizontal ramp cannot see.
@(test)
test_shading_type6_vertical_ramp_matches_mesh :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
patch := [?]u8{
0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00,
0x00, 0x18, 0x00, 0x08, 0x00, 0x18, 0x00, 0x10,
0x00, 0x18, 0x00, 0x18, 0x00, 0x10, 0x00, 0x18,
0x00, 0x08, 0x00, 0x18, 0x00, 0x00, 0x00, 0x18,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08,
0x00, 0x00, 0xFF, 0xFF, // black along the top edge, white along the bottom
}
sh := sh_load_stream(t, doc, "<< /ShadingType 6 " + PATCH_DICT, patch[:])
bmp := sh_target(t, 24, 24)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
mesh := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) grey 0
0x00, 0x18, 0x00, 0x00, 0x00, // (24,0) grey 0
0x00, 0x00, 0x00, 0x18, 0xFF, // (0,24) grey 1
0x00, 0x18, 0x00, 0x18, 0xFF, // (24,24) grey 1
}
sh2 := sh_load_stream(t, doc,
"<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " +
"/BitsPerComponent 8 /Decode [0 65535 0 65535 0 1] >>", mesh[:])
ref := sh_target(t, 24, 24)
defer bitmap_destroy(&ref)
sh_paint(t, doc, sh2, &ref)
expect_all_painted(t, &bmp, "the patch covers its square")
for y in 0 ..< 24 {
for x in 0 ..< 24 {
expect_pixel(t, &bmp, x, y, sh_pixel(&ref, x, y), 1, "patch against mesh")
}
}
expect_gray(t, &bmp, 12, 0, sh_gray(0.5 / 24), 1, "top edge")
expect_gray(t, &bmp, 12, 23, sh_gray(23.5 / 24), 1, "bottom edge")
}
// rot45 turns 45 degrees about the middle of an 8x8 bitmap.
@(private = "file")
rot45 :: proc() -> Matrix {
return matrix_mul(matrix_mul(translate_matrix(-4, -4), rotate_matrix(45)),
translate_matrix(4, 4))
}
// /BBox is a rectangle in the shading's own space, so under a rotated CTM it
// is a diamond on the page. Testing a pixel against the transformed box's
// bounding rectangle would paint the corners of the diamond's bounding box;
// the test has to happen in the shading's space.
@(test)
test_shading_bbox_under_rotation :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_load(t, doc,
"<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 8 0] /Extend [true true] " +
"/BBox [2 2 6 6] " + RAMP + " >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp, rot45())
testing.expect(t, sh_pixel(&bmp, 4, 4) != SH_BG, "the middle of the diamond")
// (1.5, 1.5) is inside the diamond's bounding box and outside the diamond.
expect_untouched(t, &bmp, 1, 1, "a corner of the rotated box's bounds")
expect_untouched(t, &bmp, 6, 6, "a corner of the rotated box's bounds")
}
// The same argument for a function-based shading's /Domain.
@(test)
test_shading_function_based_domain_under_rotation :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
sh := sh_type1(t, doc,
"<< /ShadingType 1 /ColorSpace /DeviceGray /Domain [0 1 0 1] /Matrix [8 0 0 8 0 0] >>")
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp, rot45())
// Device (4.5, 4.5) maps back to (4.707, 4) in the shading's space, which
// /Matrix turns into a domain x of 0.588.
expect_gray(t, &bmp, 4, 4, sh_gray(4.7071067 / 8), 1, "the middle of the diamond")
expect_untouched(t, &bmp, 0, 0, "a corner outside the rotated domain")
expect_untouched(t, &bmp, 7, 7, "a corner outside the rotated domain")
}
// A Pattern space in its array form has a component count, so the check that
// rejects it has to be on the kind and not on the count.
@(test)
test_shading_load_refuses_pattern_space :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
expect_refused(t, doc,
"<< /ShadingType 2 /ColorSpace [/Pattern /DeviceRGB] /Coords [0 0 8 0] " +
"/Function [<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>] >>")
}
// Every mesh above uses a /Decode that happens to be the identity on the raw
// samples, which is what makes the payloads readable. This one does not: the
// coordinate range is twice the sample range, so a raw 4 is a device 8, and
// the colour range runs backwards, so a raw 0 is white.
@(test)
test_shading_mesh_decode_ranges :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
payload := [?]u8{
0x00, 0x00, 0x00, 0x00, 0x00, // (0,0) -> device (0,0), grey 1
0x00, 0x04, 0x00, 0x00, 0xFF, // (4,0) -> device (8,0), grey 0
0x00, 0x00, 0x00, 0x04, 0x00, // (0,4) -> device (0,8), grey 1
0x00, 0x04, 0x00, 0x04, 0xFF, // (4,4) -> device (8,8), grey 0
}
sh := sh_load_stream(t, doc,
"<< /ShadingType 5 /VerticesPerRow 2 /ColorSpace /DeviceGray /BitsPerCoordinate 16 " +
"/BitsPerComponent 8 /Decode [0 131070 0 131070 1 0] >>", payload[:])
bmp := sh_target(t, 8, 8)
defer bitmap_destroy(&bmp)
sh_paint(t, doc, sh, &bmp)
expect_all_painted(t, &bmp, "a mesh whose /Decode doubles its coordinates")
for x in 0 ..< 8 {
expect_gray(t, &bmp, x, 4, sh_gray(1 - (f64(x) + 0.5) / 8), 1, "a reversed colour ramp")
}
}