1732 lines
68 KiB
Odin
1732 lines
68 KiB
Odin
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")
|
|
}
|
|
}
|