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