package pdf import "core:math" import "core:slice" import "core:testing" @(private = "file") EPS :: 1e-12 @(private = "file") expect_near :: proc(t: ^testing.T, got, want: f64, label: string, eps := EPS, loc := #caller_location) { testing.expectf(t, abs(got - want) <= eps, "%s: got %.17g, want %.17g", label, got, want, loc = loc) } @(private = "file") expect_near3 :: proc(t: ^testing.T, got, want: [3]f64, label: string, eps := EPS, loc := #caller_location) { ok := abs(got[0] - want[0]) <= eps && abs(got[1] - want[1]) <= eps && abs(got[2] - want[2]) <= eps testing.expectf(t, ok, "%s: got %.17g, want %.17g", label, got, want, loc = loc) } // A one-pixel-tall RGBA bitmap whose pixels are set from premultiplied bytes. @(private = "file") make_bmp :: proc(t: ^testing.T, w, h, n: int, fill: []u8, loc := #caller_location) -> Bitmap { bmp, err := bitmap_make(w, h, n, context.allocator) testing.expect_value(t, err, Error.None, loc = loc) bmp.premultiplied = true if fill != nil { for i in 0 ..< len(bmp.pixels) { bmp.pixels[i] = fill[i % len(fill)] } } return bmp } // --------------------------------------------------------------------------- // Separable blend functions (ISO 32000-2, 11.3.5.2) // --------------------------------------------------------------------------- @(private = "file") Sep_Case :: struct { mode: Blend_Mode, cb, cs: f64, want: f64, } @(test) test_blend_separable_boundaries :: proc(t: ^testing.T) { cases := []Sep_Case{ // Normal: the source, whatever the backdrop. {.Normal, 0, 0, 0}, {.Normal, 0, 1, 1}, {.Normal, 1, 0, 0}, {.Normal, 0.5, 0.25, 0.25}, // Multiply: cb * cs. {.Multiply, 0, 0, 0}, {.Multiply, 1, 1, 1}, {.Multiply, 0.5, 0.5, 0.25}, {.Multiply, 1, 0.5, 0.5}, {.Multiply, 0, 1, 0}, // Screen: cb + cs - cb*cs. {.Screen, 0, 0, 0}, {.Screen, 1, 1, 1}, {.Screen, 0.5, 0.5, 0.75}, {.Screen, 0, 0.5, 0.5}, {.Screen, 1, 0, 1}, // Overlay = HardLight with the operands exchanged, so the branch is on // the BACKDROP: dark backdrops multiply, light ones screen. {.Overlay, 0.5, 0.5, 0.5}, {.Overlay, 0.25, 0.5, 0.25}, {.Overlay, 0.75, 0.5, 0.75}, {.Overlay, 0, 1, 0}, {.Overlay, 1, 0, 1}, {.Overlay, 0.25, 1, 0.5}, {.Darken, 0.3, 0.7, 0.3}, {.Darken, 1, 0, 0}, {.Darken, 0.5, 0.5, 0.5}, {.Lighten, 0.3, 0.7, 0.7}, {.Lighten, 1, 0, 1}, {.Lighten, 0.5, 0.5, 0.5}, // HardLight branches on the SOURCE. {.Hard_Light, 0.5, 0.25, 0.25}, {.Hard_Light, 0.5, 0.75, 0.75}, {.Hard_Light, 0.5, 0.5, 0.5}, {.Hard_Light, 0.2, 1, 1}, {.Hard_Light, 0.8, 0, 0}, // Just above the threshold, where multiplying and screening disagree: // Screen(0.25, 0.1) = 0.325, while the wrong branch gives 0.275. {.Hard_Light, 0.25, 0.55, 0.325}, {.Overlay, 0.55, 0.25, 0.325}, // SoftLight. cs == 0.5 is the identity; cs == 0 darkens by cb*(1-cb); // cs == 1 lightens to D(cb). {.Soft_Light, 0.5, 0.5, 0.5}, {.Soft_Light, 0.5, 0, 0.25}, {.Soft_Light, 0.5, 1, 0.70710678118654752}, {.Soft_Light, 0.25, 1, 0.5}, {.Soft_Light, 0.05, 1, 0.172}, {.Soft_Light, 0, 0, 0}, {.Soft_Light, 1, 0, 1}, {.Soft_Light, 0, 1, 0}, {.Soft_Light, 1, 1, 1}, {.Difference, 0.25, 0.75, 0.5}, {.Difference, 1, 0, 1}, {.Difference, 0.5, 0.5, 0}, {.Difference, 0, 0, 0}, {.Difference, 1, 1, 0}, {.Exclusion, 0.5, 0.5, 0.5}, {.Exclusion, 1, 1, 0}, {.Exclusion, 0, 0.25, 0.25}, {.Exclusion, 1, 0, 1}, {.Exclusion, 0, 0, 0}, } for c in cases { got := blend_separable(c.mode, c.cb, c.cs) testing.expectf(t, abs(got - c.want) <= EPS, "%v(cb=%v, cs=%v): got %.17g, want %.17g", c.mode, c.cb, c.cs, got, c.want) } } // ColorDodge and ColorBurn are where the division by zero lives. ISO 32000-2 // orders the case split so that a black backdrop stays black under dodge and a // white backdrop stays white under burn; ISO 32000-1 tested the source first // and produced the opposite (and visibly wrong) answer at those two corners. @(test) test_blend_color_dodge_edges :: proc(t: ^testing.T) { cases := []Sep_Case{ {.Color_Dodge, 0, 1, 0}, // the corner ISO 32000-1 got wrong (it said 1) {.Color_Dodge, 0, 0, 0}, {.Color_Dodge, 0, 0.5, 0}, {.Color_Dodge, 0.5, 1, 1}, // cs == 1 with a non-black backdrop: white {.Color_Dodge, 1, 1, 1}, {.Color_Dodge, 0.5, 0, 0.5}, // cs == 0 is the identity {.Color_Dodge, 1, 0, 1}, {.Color_Dodge, 0.5, 0.5, 1}, // cb/(1-cs) == 1 {.Color_Dodge, 0.25, 0.5, 0.5}, {.Color_Dodge, 0.1, 0.5, 0.2}, {.Color_Dodge, 0.75, 0.5, 1}, // clamped by min(1, ...) } for c in cases { got := blend_separable(c.mode, c.cb, c.cs) testing.expectf(t, abs(got - c.want) <= EPS, "ColorDodge(cb=%v, cs=%v): got %.17g, want %.17g", c.cb, c.cs, got, c.want) } } @(test) test_blend_color_burn_edges :: proc(t: ^testing.T) { cases := []Sep_Case{ {.Color_Burn, 1, 0, 1}, // the corner ISO 32000-1 got wrong (it said 0) {.Color_Burn, 1, 1, 1}, {.Color_Burn, 1, 0.5, 1}, {.Color_Burn, 0, 0, 0}, // cs == 0 with a non-white backdrop: black {.Color_Burn, 0.5, 0, 0}, {.Color_Burn, 0.5, 1, 0.5}, // cs == 1 is the identity {.Color_Burn, 0, 1, 0}, {.Color_Burn, 0.5, 0.5, 0}, // (1-cb)/cs == 1 {.Color_Burn, 0.75, 0.5, 0.5}, {.Color_Burn, 0.9, 0.5, 0.8}, {.Color_Burn, 0.25, 0.5, 0}, // clamped by min(1, ...) } for c in cases { got := blend_separable(c.mode, c.cb, c.cs) testing.expectf(t, abs(got - c.want) <= EPS, "ColorBurn(cb=%v, cs=%v): got %.17g, want %.17g", c.cb, c.cs, got, c.want) } } // D(x) is piecewise: a cubic below 0.25 and sqrt above. Using sqrt everywhere // is the common bug, so the test pins a point where the two disagree by 0.05 -- // far more than any tolerance could hide. @(test) test_blend_soft_light_transfer :: proc(t: ^testing.T) { expect_near(t, blend_soft_light_d(0), 0, "D(0)") expect_near(t, blend_soft_light_d(1), 1, "D(1)") expect_near(t, blend_soft_light_d(0.05), 0.172, "D(0.05) is the cubic") testing.expect(t, abs(blend_soft_light_d(0.05) - math.sqrt(f64(0.05))) > 0.04, "the low branch must be the cubic, not sqrt") // The two branches meet at 0.25: both give exactly 0.5. expect_near(t, blend_soft_light_d(0.25), 0.5, "D(0.25) from the cubic") expect_near(t, blend_soft_light_d(0.25 + 1e-9), 0.5, "D just above 0.25", 1e-8) // 0.1 is a second point on the cubic: ((1.6-12)*0.1+4)*0.1 = 0.296. expect_near(t, blend_soft_light_d(0.1), 0.296, "D(0.1)") expect_near(t, blend_soft_light_d(0.81), 0.9, "D(0.81) is sqrt") } // Overlay is defined as HardLight with the operands exchanged; checking the // identity over a grid is stronger than any handful of sample points. @(test) test_blend_overlay_is_hard_light_swapped :: proc(t: ^testing.T) { for i in 0 ..= 8 { for j in 0 ..= 8 { cb := f64(i) / 8 cs := f64(j) / 8 expect_near(t, blend_separable(.Overlay, cb, cs), blend_separable(.Hard_Light, cs, cb), "Overlay == HardLight swapped") } } } // Every separable mode must stay inside the unit interval and never produce a // NaN, including at the corners where a division by zero lurks. @(test) test_blend_separable_stays_in_range :: proc(t: ^testing.T) { for mode in Blend_Mode { if !blend_mode_is_separable(mode) { continue } for i in 0 ..= 16 { for j in 0 ..= 16 { cb := f64(i) / 16 cs := f64(j) / 16 v := blend_separable(mode, cb, cs) testing.expectf(t, is_finite(v), "%v(%v,%v) is not finite: %v", mode, cb, cs, v) testing.expectf(t, v >= -EPS && v <= 1 + EPS, "%v(%v,%v) = %v out of range", mode, cb, cs, v) } } } } // --------------------------------------------------------------------------- // The non-separable helpers (11.3.5.3) // --------------------------------------------------------------------------- @(test) test_blend_lum_and_sat :: proc(t: ^testing.T) { expect_near(t, blend_lum({1, 0, 0}), 0.3, "Lum(red)") expect_near(t, blend_lum({0, 1, 0}), 0.59, "Lum(green)") expect_near(t, blend_lum({0, 0, 1}), 0.11, "Lum(blue)") expect_near(t, blend_lum({1, 1, 1}), 1, "Lum(white)") expect_near(t, blend_lum({0.5, 0.5, 0.5}), 0.5, "Lum(mid grey)") expect_near(t, blend_sat({0.2, 0.9, 0.5}), 0.7, "Sat") expect_near(t, blend_sat({0.4, 0.4, 0.4}), 0, "Sat(grey)") expect_near(t, blend_sat({0, 0, 1}), 1, "Sat(blue)") } // SetSat keeps each component in its own slot: the largest becomes s, the // smallest becomes 0, the middle is interpolated. An implementation that sorts // the values but writes them back in order passes a sorted input and fails // this one. @(test) test_blend_set_sat :: proc(t: ^testing.T) { expect_near3(t, blend_set_sat({1, 0.5, 0}, 0.4), {0.4, 0.2, 0}, "SetSat descending") expect_near3(t, blend_set_sat({0, 0.5, 1}, 1), {0, 0.5, 1}, "SetSat ascending") // 0.6 > 0.4 > 0.2 lives in components r > b > g, and must stay there. expect_near3(t, blend_set_sat({0.6, 0.2, 0.4}, 1), {1, 0, 0.5}, "SetSat unsorted") // A grey has no ordering to preserve and no range to scale by: all zero. expect_near3(t, blend_set_sat({0.5, 0.5, 0.5}, 0.8), {0, 0, 0}, "SetSat of a grey") expect_near(t, blend_sat(blend_set_sat({0.6, 0.2, 0.4}, 0.25)), 0.25, "SetSat sets Sat") } // ClipColor scales a colour towards the grey axis without changing its // luminosity. n, x and L all come from the colour as it arrived: recomputing // the maximum after the low clip has run changes the answer. @(test) test_blend_clip_color :: proc(t: ^testing.T) { expect_near3(t, blend_clip_color({0.5, 0.25, 0.75}), {0.5, 0.25, 0.75}, "in gamut: unchanged") // Lum = 0.5, x = 1.2, so everything scales by (1-L)/(x-L) = 5/7. got := blend_clip_color({1.2, 0.2, 0.2}) expect_near3(t, got, {1, 0.2857142857142857, 0.2857142857142857}, "clip high") expect_near(t, blend_lum(got), 0.5, "clipping preserves luminosity") // Lum = 0.29, n = -0.3, so everything scales by L/(L-n) = 0.29/0.59. got2 := blend_clip_color({-0.3, 0.7, -0.3}) expect_near3(t, got2, {0, 0.49152542372881356, 0}, "clip low") expect_near(t, blend_lum(got2), 0.29, "clipping preserves luminosity") // Both branches can fire for the same colour, and the second one uses the // maximum of the ORIGINAL colour, not the maximum left by the first. Here // the low clip brings the blue down to 0.765; recomputing the maximum // there would skip the high clip entirely and stop at that value, whereas // the spec keeps compressing with x = 1.5 and lands on 0.574. both := blend_clip_color({-0.5, 0.5, 1.5}) expect_near3(t, both, {0.13025210084033614, 0.35216308745720551, 0.57407407407407407}, "both branches") expect_near(t, blend_lum(both), 0.31, "luminosity survives both clips") // A uniform out-of-gamut grey drives L - n and x - L to zero. The constants // matter: at -0.5 the rounding of the Lum weights leaves a denominator of // ~2e-17 and the division still lands on the right answer by luck, but at // -0.001 the weighted sum rounds to exactly the component, the denominator // is exactly zero, and an unguarded divide yields 0/0 = NaN. expect_near3(t, blend_clip_color({-0.001, -0.001, -0.001}), {0, 0, 0}, "uniform negative") expect_near3(t, blend_clip_color({1.001, 1.001, 1.001}), {1, 1, 1}, "uniform above one") expect_near3(t, blend_clip_color({-0.5, -0.5, -0.5}), {0, 0, 0}, "uniform negative (large)") expect_near3(t, blend_clip_color({1.5, 1.5, 1.5}), {1, 1, 1}, "uniform above one (large)") } @(test) test_blend_set_lum :: proc(t: ^testing.T) { expect_near3(t, blend_set_lum({0.5, 0.5, 0.5}, 0.3), {0.3, 0.3, 0.3}, "SetLum of a grey") got := blend_set_lum({1, 0, 0}, 0.5) expect_near3(t, got, {1, 0.2857142857142857, 0.2857142857142857}, "SetLum needing a clip") expect_near(t, blend_lum(got), 0.5, "SetLum sets Lum") expect_near(t, blend_lum(blend_set_lum({0.2, 0.4, 0.6}, 0.9)), 0.9, "SetLum sets Lum (2)") } // Worked examples derived by hand from the spec's own pseudo-code. @(test) test_blend_non_separable_worked_examples :: proc(t: ^testing.T) { cb := [3]f64{0.2, 0.4, 0.6} // Lum 0.362, Sat 0.4 cs := [3]f64{1, 0.5, 0} // Lum 0.59, Sat 1 // Hue: SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb)) // SetSat((1,0.5,0), 0.4) = (0.4, 0.2, 0), Lum 0.238 // + (0.362 - 0.238) = (0.524, 0.324, 0.124), in gamut. expect_near3(t, blend_non_separable(.Hue, cb, cs), {0.524, 0.324, 0.124}, "Hue") // Saturation: SetLum(SetSat(Cb, Sat(Cs)), Lum(Cb)) // SetSat((0.2,0.4,0.6), 1) = (0, 0.5, 1), Lum 0.405 // + (0.362 - 0.405) = (-0.043, 0.457, 0.957), which clips low by // L/(L-n) = 0.362/0.405. expect_near3(t, blend_non_separable(.Saturation, cb, cs), {0, 0.44691358024691358, 0.89382716049382716}, "Saturation") grey := [3]f64{0.5, 0.5, 0.5} red := [3]f64{1, 0, 0} // Color: SetLum(Cs, Lum(Cb)) -- red pushed to luminosity 0.5, clipped high. expect_near3(t, blend_non_separable(.Color, grey, red), {1, 0.2857142857142857, 0.2857142857142857}, "Color") // Luminosity: SetLum(Cb, Lum(Cs)) -- grey pulled down to red's 0.3. expect_near3(t, blend_non_separable(.Luminosity, grey, red), {0.3, 0.3, 0.3}, "Luminosity") // Luminosity to full white: SetLum((1,0,0), 1) needs the high clip, and // (1-L)/(x-L) is exactly zero there, so every component lands on L. expect_near3(t, blend_non_separable(.Luminosity, red, {1, 1, 1}), {1, 1, 1}, "Luminosity to white") // ... and to black, through the low clip. expect_near3(t, blend_non_separable(.Luminosity, red, {0, 0, 0}), {0, 0, 0}, "Luminosity to black") } // The defining identities: Hue and Color take the backdrop's luminosity, Hue // and Saturation take a stated saturation, Luminosity takes the source's. @(test) test_blend_non_separable_identities :: proc(t: ^testing.T) { cb := [3]f64{0.15, 0.55, 0.35} cs := [3]f64{0.8, 0.1, 0.45} expect_near(t, blend_lum(blend_non_separable(.Hue, cb, cs)), blend_lum(cb), "Hue keeps Lum(Cb)") expect_near(t, blend_lum(blend_non_separable(.Saturation, cb, cs)), blend_lum(cb), "Sat keeps Lum(Cb)") expect_near(t, blend_lum(blend_non_separable(.Color, cb, cs)), blend_lum(cb), "Color keeps Lum(Cb)") expect_near(t, blend_lum(blend_non_separable(.Luminosity, cb, cs)), blend_lum(cs), "Luminosity takes Lum(Cs)") // Saturation survives exactly only while the result stays inside the unit // cube. Hue does here; Saturation with this pair lands at -0.0435 in red // and ClipColor compresses it back towards the grey axis, which can only // reduce the saturation. Both halves of that are asserted. expect_near(t, blend_sat(blend_non_separable(.Hue, cb, cs)), blend_sat(cb), "Hue takes Sat(Cb)") low_sat := [3]f64{0.5, 0.45, 0.5} // Sat 0.05: small enough to stay in gamut expect_near(t, blend_sat(blend_non_separable(.Saturation, cb, low_sat)), blend_sat(low_sat), "Saturation takes Sat(Cs)") testing.expect(t, blend_sat(blend_non_separable(.Saturation, cb, cs)) < blend_sat(cs) - 0.01, "clipping an out-of-gamut result reduces its saturation") // A grey source has no hue and no saturation to donate. expect_near3(t, blend_non_separable(.Hue, cb, {0.5, 0.5, 0.5}), {blend_lum(cb), blend_lum(cb), blend_lum(cb)}, "Hue of a grey source") } @(test) test_blend_non_separable_falls_back_for_separable_modes :: proc(t: ^testing.T) { cb := [3]f64{0.2, 0.5, 0.8} cs := [3]f64{0.5, 0.5, 0.5} expect_near3(t, blend_non_separable(.Multiply, cb, cs), {0.1, 0.25, 0.4}, "componentwise Multiply") expect_near3(t, blend_non_separable(.Normal, cb, cs), cs, "componentwise Normal") } @(test) test_blend_non_separable_stays_in_range :: proc(t: ^testing.T) { for mode in Blend_Mode { if blend_mode_is_separable(mode) { continue } for i in 0 ..= 5 { for j in 0 ..= 5 { for k in 0 ..= 5 { cb := [3]f64{f64(i) / 5, f64(j) / 5, f64(k) / 5} cs := [3]f64{f64(k) / 5, f64(i) / 5, f64(j) / 5} v := blend_non_separable(mode, cb, cs) for c in v { testing.expectf(t, is_finite(c), "%v produced %v", mode, c) testing.expectf(t, c >= -1e-9 && c <= 1 + 1e-9, "%v produced %v out of range", mode, c) } } } } } } // --------------------------------------------------------------------------- // The compositing formula (11.3.6) // --------------------------------------------------------------------------- // With Normal the union formula must collapse to plain source-over, because // B(Cb,Cs) = Cs makes the first two terms sum to as*Cs. @(test) test_blend_over_normal_is_source_over :: proc(t: ^testing.T) { for i in 0 ..= 4 { for j in 0 ..= 4 { ab := f64(i) / 4 as_ := f64(j) / 4 cb := [3]f64{0.2, 0.5, 0.9} cs := [3]f64{0.7, 0.1, 0.4} co, ao := blend_over(cb, ab, cs, as_, .Normal) for c in 0 ..< 3 { want := as_ * cs[c] + (1 - as_) * ab * cb[c] expect_near(t, co[c], want, "source-over colour") } expect_near(t, ao, as_ + ab * (1 - as_), "source-over alpha") } } } // A fully transparent backdrop leaves only the as*(1-ab)*Cs term, whatever the // blend mode: the B() term is multiplied by ab. @(test) test_blend_over_transparent_backdrop_gives_source :: proc(t: ^testing.T) { cs := [3]f64{0.25, 0.5, 0.75} for mode in Blend_Mode { for j in 0 ..= 4 { as_ := f64(j) / 4 co, ao := blend_over({0, 0, 0}, 0, cs, as_, mode) expect_near3(t, co, {as_ * cs[0], as_ * cs[1], as_ * cs[2]}, "premultiplied source") expect_near(t, ao, as_, "alpha is the source alpha") } } } // A source alpha of zero must leave the backdrop exactly as it was, whatever // the blend mode: this is what makes `0 gs` invisible rather than merely faint. @(test) test_blend_over_zero_source_alpha_is_a_no_op :: proc(t: ^testing.T) { cb := [3]f64{0.2, 0.6, 0.9} for mode in Blend_Mode { for i in 0 ..= 4 { ab := f64(i) / 4 co, ao := blend_over(cb, ab, {1, 1, 1}, 0, mode) expect_near3(t, co, {ab * cb[0], ab * cb[1], ab * cb[2]}, "backdrop unchanged") expect_near(t, ao, ab, "alpha unchanged") } } } // Over an opaque backdrop the result is opaque and is exactly B(Cb, Cs) when // the source is opaque too -- the blend function alone, with no interpolation. @(test) test_blend_over_opaque_is_the_blend_function :: proc(t: ^testing.T) { cb := [3]f64{0.25, 0.5, 0.75} cs := [3]f64{0.5, 0.5, 0.5} for mode in Blend_Mode { co, ao := blend_over(cb, 1, cs, 1, mode) want := blend_mode_is_separable(mode) \ ? [3]f64{blend_separable(mode, cb[0], cs[0]), blend_separable(mode, cb[1], cs[1]), blend_separable(mode, cb[2], cs[2])} \ : blend_non_separable(mode, cb, cs) expect_near3(t, co, want, "opaque over opaque is B()") expect_near(t, ao, 1, "opaque stays opaque") } } @(test) test_blend_over_never_produces_nan :: proc(t: ^testing.T) { for mode in Blend_Mode { for i in 0 ..= 3 { for j in 0 ..= 3 { cb := [3]f64{f64(i) / 3, 1 - f64(i) / 3, 0} cs := [3]f64{f64(j) / 3, 0, 1 - f64(j) / 3} co, ao := blend_over(cb, f64(i) / 3, cs, f64(j) / 3, mode) testing.expect(t, is_finite(ao), "alpha is finite") for c in co { testing.expectf(t, is_finite(c), "%v produced %v", mode, c) // Premultiplied output can never exceed its own alpha. testing.expectf(t, c >= -EPS && c <= ao + 1e-9, "%v produced %v with ao %v", mode, c, ao) } } } } } // --------------------------------------------------------------------------- // blend_span // --------------------------------------------------------------------------- // Painting onto nothing gives back the source, premultiplied. 0.25/0.5/0.75 // are exact in binary, so the bytes are exact too. @(test) test_blend_span_onto_transparent_gives_source :: proc(t: ^testing.T) { dst := make_bmp(t, 1, 1, 4, nil) defer bitmap_destroy(&dst) blend_span(&dst, 0, 0, {255}, {0.25, 0.5, 0.75, 0.5}, .Normal) testing.expect_value(t, dst.pixels[0], 32) // 0.25 * 0.5 * 255 = 31.875 testing.expect_value(t, dst.pixels[1], 64) // 0.5 * 0.5 * 255 = 63.75 testing.expect_value(t, dst.pixels[2], 96) // 0.75 * 0.5 * 255 = 95.625 testing.expect_value(t, dst.pixels[3], 128) // 0.5 * 255 = 127.5 // Undoing the premultiplication recovers the colour that went in. bitmap_unpremultiply(&dst) testing.expect_value(t, dst.pixels[0], 64) // 0.25 -> 63.75 testing.expect_value(t, dst.pixels[1], 128) // 0.5 -> 127.5 testing.expect_value(t, dst.pixels[2], 191) // 0.75 -> 191.25 testing.expect_value(t, dst.pixels[3], 128) } @(test) test_blend_span_sets_premultiplied :: proc(t: ^testing.T) { dst, _ := bitmap_make(1, 1, 4) defer bitmap_destroy(&dst) testing.expect(t, !dst.premultiplied, "bitmap_make starts straight") blend_span(&dst, 0, 0, {255}, {1, 1, 1, 1}, .Normal) testing.expect(t, dst.premultiplied, "compositing leaves premultiplied pixels") } @(test) test_blend_span_alpha_and_coverage_scale :: proc(t: ^testing.T) { // Opaque black background, opaque white source at half alpha. dst := make_bmp(t, 1, 1, 4, {0, 0, 0, 255}) defer bitmap_destroy(&dst) blend_span(&dst, 0, 0, {255}, {1, 1, 1, 0.5}, .Normal) testing.expect_value(t, dst.pixels[0], 128) testing.expect_value(t, dst.pixels[3], 255) // Half coverage of a fully opaque source is the same thing. dst2 := make_bmp(t, 1, 1, 4, {0, 0, 0, 255}) defer bitmap_destroy(&dst2) blend_span(&dst2, 0, 0, {128}, {1, 1, 1, 1}, .Normal) testing.expect_value(t, dst2.pixels[0], 128) testing.expect_value(t, dst2.pixels[3], 255) } @(test) test_blend_span_zero_alpha_changes_nothing :: proc(t: ^testing.T) { before := []u8{40, 80, 120, 255} dst := make_bmp(t, 1, 1, 4, before) defer bitmap_destroy(&dst) blend_span(&dst, 0, 0, {255}, {1, 1, 1, 0}, .Normal) testing.expect(t, slice.equal(dst.pixels, before), "alpha 0 must not change the destination") // Nor may zero coverage, whatever the alpha. blend_span(&dst, 0, 0, {0}, {1, 1, 1, 1}, .Multiply) testing.expect(t, slice.equal(dst.pixels, before), "coverage 0 must not change the destination") } // A mask of all 255 must be byte-for-byte identical to no mask at all: any // rounding introduced by the mask multiply would show as a seam at the edge of // a masked region. @(test) test_blend_span_full_mask_is_a_no_op :: proc(t: ^testing.T) { fill := []u8{40, 80, 120, 200} a := make_bmp(t, 6, 1, 4, fill) defer bitmap_destroy(&a) b := make_bmp(t, 6, 1, 4, fill) defer bitmap_destroy(&b) cov := []u8{255, 200, 128, 64, 1, 0} mask := []u8{255, 255, 255, 255, 255, 255} for mode in Blend_Mode { blend_span(&a, 0, 0, cov, {0.3, 0.6, 0.9, 0.7}, mode) blend_span(&b, 0, 0, cov, {0.3, 0.6, 0.9, 0.7}, mode, mask) } testing.expect(t, slice.equal(a.pixels, b.pixels), "an all-255 mask must change nothing") } @(test) test_blend_span_mask_scales_and_erases :: proc(t: ^testing.T) { before := []u8{0, 0, 0, 255} dst := make_bmp(t, 2, 1, 4, before) defer bitmap_destroy(&dst) // Mask 0 erases the first pixel entirely; mask 128 halves the second. blend_span(&dst, 0, 0, {255, 255}, {1, 1, 1, 1}, .Normal, {0, 128}) testing.expect_value(t, dst.pixels[0], 0) testing.expect_value(t, dst.pixels[3], 255) testing.expect_value(t, dst.pixels[4], 128) testing.expect_value(t, dst.pixels[7], 255) } // A span that starts left of the bitmap keeps its coverage and its mask in // step: pixel 0 must take coverage[2] and mask[2], not coverage[0]. @(test) test_blend_span_clips_left_edge_in_step :: proc(t: ^testing.T) { dst := make_bmp(t, 3, 1, 1, {0}) defer bitmap_destroy(&dst) blend_span(&dst, 0, -2, {0, 0, 255, 128}, {1, 1, 1, 1}, .Normal) testing.expect_value(t, dst.pixels[0], 255) testing.expect_value(t, dst.pixels[1], 128) testing.expect_value(t, dst.pixels[2], 0) // past the end of the coverage run dst2 := make_bmp(t, 3, 1, 1, {0}) defer bitmap_destroy(&dst2) blend_span(&dst2, 0, -2, {0, 0, 255, 128}, {1, 1, 1, 1}, .Normal, {255, 255, 0, 255}) testing.expect_value(t, dst2.pixels[0], 0) // mask[2] = 0 testing.expect_value(t, dst2.pixels[1], 128) // mask[3] = 255 } @(test) test_blend_span_clips_right_edge :: proc(t: ^testing.T) { dst := make_bmp(t, 3, 1, 1, {0}) defer bitmap_destroy(&dst) blend_span(&dst, 0, 2, {255, 255, 255}, {1, 1, 1, 1}, .Normal) testing.expect_value(t, dst.pixels[0], 0) testing.expect_value(t, dst.pixels[1], 0) testing.expect_value(t, dst.pixels[2], 255) } @(test) test_blend_span_out_of_range_is_ignored :: proc(t: ^testing.T) { before := []u8{7, 7, 7, 7} dst := make_bmp(t, 2, 2, 1, before) defer bitmap_destroy(&dst) blend_span(&dst, -1, 0, {255, 255}, {1, 1, 1, 1}, .Normal) blend_span(&dst, 2, 0, {255, 255}, {1, 1, 1, 1}, .Normal) blend_span(&dst, 0, 2, {255, 255}, {1, 1, 1, 1}, .Normal) blend_span(&dst, 0, -5, {255, 255}, {1, 1, 1, 1}, .Normal) blend_span(&dst, 0, 0, {}, {1, 1, 1, 1}, .Normal) blend_span(nil, 0, 0, {255}, {1, 1, 1, 1}, .Normal) testing.expect(t, slice.equal(dst.pixels, before), "out-of-range spans must not write") } // Numbers from a content stream reach the paint colour, so a NaN or an // infinity must be absorbed here rather than turning a pixel into garbage. @(test) test_blend_span_rejects_non_finite_colour :: proc(t: ^testing.T) { nan := math.nan_f64() inf := math.INF_F64 before := []u8{0, 0, 0, 255} dst := make_bmp(t, 1, 1, 4, before) defer bitmap_destroy(&dst) blend_span(&dst, 0, 0, {255}, {1, 1, 1, nan}, .Normal) testing.expect(t, slice.equal(dst.pixels, before), "a NaN alpha paints nothing") // A non-finite or out-of-range component clamps to the end of the range it // points at instead of escaping into the pixel. blend_span(&dst, 0, 0, {255}, {inf, -inf, nan, 1}, .Normal) testing.expect_value(t, dst.pixels[0], 255) testing.expect_value(t, dst.pixels[1], 0) testing.expect_value(t, dst.pixels[2], 0) testing.expect_value(t, dst.pixels[3], 255) } @(test) test_blend_span_modes_on_opaque_grey :: proc(t: ^testing.T) { // Multiply by white and Screen by black are both the identity; Multiply by // black is black and Screen by white is white. check :: proc(t: ^testing.T, mode: Blend_Mode, src: f64, want: u8, loc := #caller_location) { dst := make_bmp(t, 1, 1, 1, {128}, loc) defer bitmap_destroy(&dst) blend_span(&dst, 0, 0, {255}, {src, src, src, 1}, mode) testing.expectf(t, dst.pixels[0] == want, "%v with source %v: got %v, want %v", mode, src, dst.pixels[0], want, loc = loc) } check(t, .Multiply, 1, 128) check(t, .Multiply, 0, 0) check(t, .Screen, 0, 128) check(t, .Screen, 1, 255) check(t, .Darken, 1, 128) check(t, .Lighten, 0, 128) check(t, .Difference, 0, 128) check(t, .Normal, 1, 255) } // A greyscale destination takes the source's luminosity, not its red channel. @(test) test_blend_span_grey_destination_uses_luminosity :: proc(t: ^testing.T) { dst := make_bmp(t, 3, 1, 1, {0}) defer bitmap_destroy(&dst) blend_span(&dst, 0, 0, {255}, {1, 0, 0, 1}, .Normal) // Lum 0.3 -> 76.5 blend_span(&dst, 0, 1, {255}, {0, 1, 0, 1}, .Normal) // Lum 0.59 -> 150.45 blend_span(&dst, 0, 2, {255}, {0, 0, 1, 1}, .Normal) // Lum 0.11 -> 28.05 testing.expect(t, dst.pixels[0] == 76 || dst.pixels[0] == 77, "red luminosity is 0.3") testing.expect_value(t, dst.pixels[1], 150) testing.expect_value(t, dst.pixels[2], 28) } @(test) test_blend_span_rgb_destination_has_no_alpha :: proc(t: ^testing.T) { dst := make_bmp(t, 1, 1, 3, {255, 255, 255}) defer bitmap_destroy(&dst) blend_span(&dst, 0, 0, {255}, {1, 0, 0, 0.5}, .Normal) testing.expect_value(t, dst.pixels[0], 255) testing.expect_value(t, dst.pixels[1], 128) testing.expect_value(t, dst.pixels[2], 128) } // --------------------------------------------------------------------------- // blend_span_rgba8 // --------------------------------------------------------------------------- @(test) test_blend_span_rgba8_places_pixels :: proc(t: ^testing.T) { dst := make_bmp(t, 4, 1, 4, nil) defer bitmap_destroy(&dst) src := []u8{255, 0, 0, 255, 0, 255, 0, 128} blend_span_rgba8(&dst, 0, 1, src, 1, .Normal) testing.expect_value(t, dst.pixels[0], 0) // untouched testing.expect_value(t, dst.pixels[4], 255) testing.expect_value(t, dst.pixels[7], 255) testing.expect_value(t, dst.pixels[8], 0) testing.expect_value(t, dst.pixels[9], 128) // 0.5 premultiplied green testing.expect_value(t, dst.pixels[11], 128) testing.expect_value(t, dst.pixels[12], 0) // untouched } @(test) test_blend_span_rgba8_alpha_coverage_and_mask :: proc(t: ^testing.T) { src := []u8{255, 255, 255, 255, 255, 255, 255, 255} // Each of the three multipliers must reach the same place in the product. for k in 0 ..< 3 { dst := make_bmp(t, 2, 1, 4, {0, 0, 0, 255}) defer bitmap_destroy(&dst) switch k { case 0: blend_span_rgba8(&dst, 0, 0, src, 0.5, .Normal) case 1: blend_span_rgba8(&dst, 0, 0, src, 1, .Normal, {128, 128}) case 2: blend_span_rgba8(&dst, 0, 0, src, 1, .Normal, nil, {128, 128}) } testing.expectf(t, dst.pixels[0] == 128, "case %d: got %v", k, dst.pixels[0]) } } @(test) test_blend_span_rgba8_clips :: proc(t: ^testing.T) { before := []u8{9, 9, 9, 255, 9, 9, 9, 255} dst := make_bmp(t, 2, 1, 4, before) defer bitmap_destroy(&dst) blend_span_rgba8(&dst, 1, 0, {255, 255, 255, 255}, 1, .Normal) // y out of range blend_span_rgba8(&dst, 0, 2, {255, 255, 255, 255}, 1, .Normal) // x past the edge blend_span_rgba8(&dst, 0, 0, {}, 1, .Normal) blend_span_rgba8(nil, 0, 0, {255, 255, 255, 255}, 1, .Normal) testing.expect(t, slice.equal(dst.pixels, before), "clipped runs must not write") // A run starting left of zero keeps its source pixels in step. dst2 := make_bmp(t, 2, 1, 4, nil) defer bitmap_destroy(&dst2) blend_span_rgba8(&dst2, 0, -1, {255, 0, 0, 255, 0, 0, 255, 255}, 1, .Normal) testing.expect_value(t, dst2.pixels[2], 255) // the blue pixel landed at x = 0 testing.expect_value(t, dst2.pixels[0], 0) // ... and so do its coverage and its mask. Entry 0 of each belongs to the // pixel that was clipped away, so reading it would paint what should be // blank. white := []u8{255, 255, 255, 255, 255, 255, 255, 255} dst3 := make_bmp(t, 2, 1, 4, nil) defer bitmap_destroy(&dst3) blend_span_rgba8(&dst3, 0, -1, white, 1, .Normal, {255, 0}) testing.expect_value(t, dst3.pixels[0], 0) testing.expect_value(t, dst3.pixels[3], 0) dst4 := make_bmp(t, 2, 1, 4, nil) defer bitmap_destroy(&dst4) blend_span_rgba8(&dst4, 0, -1, white, 1, .Normal, nil, {255, 0}) testing.expect_value(t, dst4.pixels[0], 0) testing.expect_value(t, dst4.pixels[3], 0) } // --------------------------------------------------------------------------- // blend_bitmap // --------------------------------------------------------------------------- @(test) test_blend_bitmap_onto_transparent_gives_source :: proc(t: ^testing.T) { src := make_bmp(t, 2, 2, 4, {100, 50, 25, 200}) defer bitmap_destroy(&src) dst := make_bmp(t, 4, 4, 4, nil) defer bitmap_destroy(&dst) blend_bitmap(&dst, &src, 1, 1, 1, .Normal, nil) for j in 0 ..< 4 { for i in 0 ..< 4 { off := j * dst.stride + i * 4 inside := i >= 1 && i <= 2 && j >= 1 && j <= 2 want := inside ? []u8{100, 50, 25, 200} : []u8{0, 0, 0, 0} testing.expectf(t, slice.equal(dst.pixels[off:off + 4], want), "pixel (%d,%d): got %v, want %v", i, j, dst.pixels[off:off + 4], want) } } } @(test) test_blend_bitmap_constant_alpha_and_mask :: proc(t: ^testing.T) { src := make_bmp(t, 1, 1, 4, {255, 0, 0, 255}) defer bitmap_destroy(&src) dst := make_bmp(t, 1, 1, 4, nil) defer bitmap_destroy(&dst) blend_bitmap(&dst, &src, 0, 0, 0.5, .Normal, nil) testing.expect_value(t, dst.pixels[0], 128) testing.expect_value(t, dst.pixels[3], 128) // A one-component mask of 128 does the same job as a constant alpha of 0.5. mask := make_bmp(t, 1, 1, 1, {128}) defer bitmap_destroy(&mask) dst2 := make_bmp(t, 1, 1, 4, nil) defer bitmap_destroy(&dst2) blend_bitmap(&dst2, &src, 0, 0, 1, .Normal, &mask) testing.expect(t, slice.equal(dst.pixels, dst2.pixels), "mask 128 == alpha 0.5") // ... and a mask of 255 is no mask at all. full := make_bmp(t, 1, 1, 1, {255}) defer bitmap_destroy(&full) dst3 := make_bmp(t, 1, 1, 4, nil) defer bitmap_destroy(&dst3) dst4 := make_bmp(t, 1, 1, 4, nil) defer bitmap_destroy(&dst4) blend_bitmap(&dst3, &src, 0, 0, 1, .Normal, &full) blend_bitmap(&dst4, &src, 0, 0, 1, .Normal, nil) testing.expect(t, slice.equal(dst3.pixels, dst4.pixels), "an all-255 mask must change nothing") // Zero alpha leaves the destination untouched. dst5 := make_bmp(t, 1, 1, 4, {10, 20, 30, 255}) defer bitmap_destroy(&dst5) blend_bitmap(&dst5, &src, 0, 0, 0, .Normal, nil) testing.expect(t, slice.equal(dst5.pixels, []u8{10, 20, 30, 255}), "alpha 0 changes nothing") } @(test) test_blend_bitmap_clips_to_destination :: proc(t: ^testing.T) { src := make_bmp(t, 2, 2, 4, {255, 255, 255, 255}) defer bitmap_destroy(&src) // Overhanging the top-left: only src(1,1) lands, at dst(0,0). dst := make_bmp(t, 2, 2, 4, nil) defer bitmap_destroy(&dst) blend_bitmap(&dst, &src, -1, -1, 1, .Normal, nil) testing.expect_value(t, dst.pixels[0], 255) testing.expect_value(t, dst.pixels[4], 0) testing.expect_value(t, dst.pixels[dst.stride], 0) // Entirely outside: nothing at all. dst2 := make_bmp(t, 2, 2, 4, nil) defer bitmap_destroy(&dst2) blend_bitmap(&dst2, &src, 5, 5, 1, .Normal, nil) blend_bitmap(&dst2, &src, -9, 0, 1, .Normal, nil) blend_bitmap(&dst2, &src, 0, -9, 1, .Normal, nil) blend_bitmap(nil, &src, 0, 0, 1, .Normal, nil) blend_bitmap(&dst2, nil, 0, 0, 1, .Normal, nil) for b in dst2.pixels { testing.expect_value(t, b, 0) } } @(test) test_blend_bitmap_mixed_component_counts :: proc(t: ^testing.T) { // An opaque grey group composited onto an RGB page. src := make_bmp(t, 1, 1, 1, {128}) defer bitmap_destroy(&src) dst := make_bmp(t, 1, 1, 3, {0, 0, 0}) defer bitmap_destroy(&dst) blend_bitmap(&dst, &src, 0, 0, 1, .Normal, nil) testing.expect_value(t, dst.pixels[0], 128) testing.expect_value(t, dst.pixels[1], 128) testing.expect_value(t, dst.pixels[2], 128) // An RGBA group onto a grey page takes the luminosity. src2 := make_bmp(t, 1, 1, 4, {0, 255, 0, 255}) defer bitmap_destroy(&src2) dst2 := make_bmp(t, 1, 1, 1, {0}) defer bitmap_destroy(&dst2) blend_bitmap(&dst2, &src2, 0, 0, 1, .Normal, nil) testing.expect_value(t, dst2.pixels[0], 150) } @(test) test_blend_bitmap_applies_the_blend_mode :: proc(t: ^testing.T) { // A white group multiplied onto a grey page is the identity; a black one // wipes it out. src := make_bmp(t, 1, 1, 4, {255, 255, 255, 255}) defer bitmap_destroy(&src) dst := make_bmp(t, 1, 1, 4, {128, 128, 128, 255}) defer bitmap_destroy(&dst) blend_bitmap(&dst, &src, 0, 0, 1, .Multiply, nil) testing.expect_value(t, dst.pixels[0], 128) testing.expect_value(t, dst.pixels[3], 255) black := make_bmp(t, 1, 1, 4, {0, 0, 0, 255}) defer bitmap_destroy(&black) dst2 := make_bmp(t, 1, 1, 4, {128, 128, 128, 255}) defer bitmap_destroy(&dst2) blend_bitmap(&dst2, &black, 0, 0, 1, .Multiply, nil) testing.expect_value(t, dst2.pixels[0], 0) testing.expect_value(t, dst2.pixels[3], 255) } // --------------------------------------------------------------------------- // mask_from_bitmap (11.6.5) // --------------------------------------------------------------------------- @(test) test_mask_from_bitmap_luminosity :: proc(t: ^testing.T) { src := make_bmp(t, 6, 1, 3, nil) defer bitmap_destroy(&src) px := [][3]u8{{0, 0, 0}, {128, 128, 128}, {255, 255, 255}, {255, 0, 0}, {0, 255, 0}, {0, 0, 255}} for p, i in px { src.pixels[i * 3 + 0] = p[0] src.pixels[i * 3 + 1] = p[1] src.pixels[i * 3 + 2] = p[2] } m, err := mask_from_bitmap(&src, true, {0, 0, 0}, nil) testing.expect_value(t, err, Error.None) defer bitmap_destroy(&m) testing.expect_value(t, m.n, 1) testing.expect_value(t, m.width, 6) testing.expect_value(t, m.height, 1) testing.expect_value(t, m.pixels[0], 0) testing.expect_value(t, m.pixels[1], 128) // mid grey stays mid grey testing.expect_value(t, m.pixels[2], 255) testing.expect(t, m.pixels[3] == 76 || m.pixels[3] == 77, "red is 0.3") testing.expect_value(t, m.pixels[4], 150) // 0.59 * 255 = 150.45 testing.expect_value(t, m.pixels[5], 28) // 0.11 * 255 = 28.05 } // An alpha mask ignores the colour entirely; a bitmap with no alpha channel is // opaque, so its mask is solid. @(test) test_mask_from_bitmap_alpha :: proc(t: ^testing.T) { src := make_bmp(t, 2, 1, 4, nil) defer bitmap_destroy(&src) copy(src.pixels[0:4], []u8{200, 100, 50, 200}) copy(src.pixels[4:8], []u8{0, 0, 0, 0}) m, err := mask_from_bitmap(&src, false, {1, 1, 1}, nil) testing.expect_value(t, err, Error.None) defer bitmap_destroy(&m) testing.expect_value(t, m.pixels[0], 200) testing.expect_value(t, m.pixels[1], 0) opaque := make_bmp(t, 1, 1, 3, {10, 20, 30}) defer bitmap_destroy(&opaque) m2, err2 := mask_from_bitmap(&opaque, false, {0, 0, 0}, nil) testing.expect_value(t, err2, Error.None) defer bitmap_destroy(&m2) testing.expect_value(t, m2.pixels[0], 255) } // A luminosity mask composites the group onto an opaque backdrop first, so the // backdrop is what shows through wherever the group painted nothing. This is // why /BC defaults to black: unpainted area masks everything out. @(test) test_mask_from_bitmap_luminosity_uses_the_backdrop :: proc(t: ^testing.T) { src := make_bmp(t, 2, 1, 4, nil) defer bitmap_destroy(&src) copy(src.pixels[0:4], []u8{128, 128, 128, 128}) // premultiplied white at 50% copy(src.pixels[4:8], []u8{0, 0, 0, 0}) // nothing painted black, e1 := mask_from_bitmap(&src, true, {0, 0, 0}, nil) testing.expect_value(t, e1, Error.None) defer bitmap_destroy(&black) testing.expect_value(t, black.pixels[0], 128) testing.expect_value(t, black.pixels[1], 0) white, e2 := mask_from_bitmap(&src, true, {1, 1, 1}, nil) testing.expect_value(t, e2, Error.None) defer bitmap_destroy(&white) testing.expect_value(t, white.pixels[0], 255) // 0.5 white + 0.5 white testing.expect_value(t, white.pixels[1], 255) } // The same pixel written as straight alpha must give the same mask: the flag, // not the caller, decides whether the a*C factor is already there. @(test) test_mask_from_bitmap_honours_the_premultiplied_flag :: proc(t: ^testing.T) { straight := make_bmp(t, 1, 1, 4, {255, 255, 255, 128}) defer bitmap_destroy(&straight) straight.premultiplied = false m, err := mask_from_bitmap(&straight, true, {0, 0, 0}, nil) testing.expect_value(t, err, Error.None) defer bitmap_destroy(&m) testing.expect_value(t, m.pixels[0], 128) m2, err2 := mask_from_bitmap(&straight, true, {1, 1, 1}, nil) testing.expect_value(t, err2, Error.None) defer bitmap_destroy(&m2) testing.expect_value(t, m2.pixels[0], 255) } @(test) test_mask_from_bitmap_rejects_bad_input :: proc(t: ^testing.T) { _, err := mask_from_bitmap(nil, true, {0, 0, 0}, nil) testing.expect_value(t, err, Error.Invalid_Argument) empty: Bitmap _, err2 := mask_from_bitmap(&empty, false, {0, 0, 0}, nil) testing.expect_value(t, err2, Error.Invalid_Argument) } // /TR is applied to every mask value. A type 2 function with C0 = 1, C1 = 0 and // N = 1 is the inverting transfer, which is the one real files use to turn a // luminosity mask inside out. @(test) test_mask_from_bitmap_transfer_function :: proc(t: ^testing.T) { domain := [2]f64{0, 1} range := [2]f64{0, 1} c0 := [1]f64{1} c1 := [1]f64{0} fn := Function{ kind = .Exponential, n_in = 1, n_out = 1, domain = domain[:], range = range[:], c0 = c0[:], c1 = c1[:], exponent = 1, } src := make_bmp(t, 3, 1, 4, nil) defer bitmap_destroy(&src) copy(src.pixels[0:4], []u8{0, 0, 0, 0}) copy(src.pixels[4:8], []u8{0, 0, 0, 200}) copy(src.pixels[8:12], []u8{0, 0, 0, 255}) m, err := mask_from_bitmap(&src, false, {0, 0, 0}, &fn) testing.expect_value(t, err, Error.None) defer bitmap_destroy(&m) testing.expect_value(t, m.pixels[0], 255) testing.expect_value(t, m.pixels[1], 55) testing.expect_value(t, m.pixels[2], 0) // A function with the wrong arity is ignored rather than obeyed: a broken // /TR must not blank the mask. bad := fn bad.n_in = 2 m2, err2 := mask_from_bitmap(&src, false, {0, 0, 0}, &bad) testing.expect_value(t, err2, Error.None) defer bitmap_destroy(&m2) testing.expect_value(t, m2.pixels[1], 200) } @(test) test_blend_transfer_table_identity :: proc(t: ^testing.T) { lut := blend_transfer_table(nil) for i in 0 ..< 256 { testing.expect_value(t, lut[i], u8(i)) } } // --------------------------------------------------------------------------- // Quantisation // --------------------------------------------------------------------------- @(test) test_blend_quantize :: proc(t: ^testing.T) { testing.expect_value(t, blend_quantize(0), 0) testing.expect_value(t, blend_quantize(1), 255) testing.expect_value(t, blend_quantize(0.5), 128) // 127.5 rounds up testing.expect_value(t, blend_quantize(2), 255) // clamped testing.expect_value(t, blend_quantize(-1), 0) // clamped testing.expect_value(t, blend_quantize(math.nan_f64()), 0) // NaN must not wrap testing.expect_value(t, blend_quantize(math.INF_F64), 255) testing.expect_value(t, blend_quantize(1.0 / 255), 1) }