use nox_gfx::canvas::{Canvas, Paint, Stroke, TOL}; use nox_gfx::color::{blend_over, lerp_premul, rgba, scale_premul, Color}; use nox_gfx::geom::{pt, Point, Rect, Transform, IDENTITY}; use nox_gfx::path::Path; use nox_gfx::raster::Rasterizer; fn unpack(v: u32) -> [f64; 4] { [ ((v >> 24) & 0xff) as f64 / 255.0, ((v >> 16) & 0xff) as f64 / 255.0, ((v >> 8) & 0xff) as f64 / 255.0, (v & 0xff) as f64 / 255.0, ] } fn chan(v: u32, sh: u32) -> i32 { ((v >> sh) & 0xff) as i32 } fn assert_close(got: u32, want: [f64; 4], tol: i32, what: &str) { for (i, sh) in [24u32, 16, 8, 0].iter().enumerate() { let g = chan(got, *sh); let w = (want[i] * 255.0).round() as i32; assert!( (g - w).abs() <= tol, "{what}: channel {i} got {g} want {w} (got 0x{got:08x})" ); } } #[test] fn blend_over_matches_float_reference() { let mut cases = Vec::new(); for a in [0u32, 1, 7, 64, 128, 200, 254, 255] { for c in [0u32, 1, 33, 128, 254, 255] { let c = c.min(a); cases.push((a << 24) | (c << 16) | (c << 8) | c); } } for &dst in &cases { for &src in &cases { let got = blend_over(dst, src); let d = unpack(dst); let s = unpack(src); let inv = 1.0 - s[0]; let want = [ s[0] + d[0] * inv, s[1] + d[1] * inv, s[2] + d[2] * inv, s[3] + d[3] * inv, ]; assert_close(got, want, 1, "blend_over"); } } } #[test] fn scale_premul_matches_float_reference() { for v in [0u32, 0x8040_2010, 0xffff_ffff, 0xff00_0000, 0x7f3f_1f0f] { for f in 0u32..=255 { let got = scale_premul(v, f); let a = unpack(v); let k = f as f64 / 255.0; assert_close(got, [a[0] * k, a[1] * k, a[2] * k, a[3] * k], 1, "scale_premul"); } } } #[test] fn lerp_premul_matches_float_reference() { for a in [0u32, 0xff00_0000, 0xffff_ffff, 0x8040_2010] { for b in [0u32, 0xff80_4020, 0xffff_ffff, 0x40201008] { for t in [0u32, 1, 63, 128, 200, 254, 255] { let got = lerp_premul(a, b, t); let x = unpack(a); let y = unpack(b); let k = t as f64 / 255.0; let want = [ x[0] + (y[0] - x[0]) * k, x[1] + (y[1] - x[1]) * k, x[2] + (y[2] - x[2]) * k, x[3] + (y[3] - x[3]) * k, ]; assert_close(got, want, 2, "lerp_premul"); } } } } #[test] fn premul_roundtrip_is_stable() { for a in [255u8, 254, 200, 128, 64, 8, 1] { for c in [0u8, 1, 50, 128, 200, 255] { let orig = Color { r: c, g: c / 2, b: 255 - c, a }; let back = Color::from_premul(orig.premul()); assert_eq!(back.a, orig.a); let d = |x: u8, y: u8| (x as i32 - y as i32).abs(); let slack = if a >= 128 { 2 } else { 255 / a as i32 + 1 }; assert!(d(back.r, orig.r) <= slack, "r {:?} -> {:?}", orig, back); assert!(d(back.g, orig.g) <= slack, "g {:?} -> {:?}", orig, back); assert!(d(back.b, orig.b) <= slack, "b {:?} -> {:?}", orig, back); } } } #[test] fn opaque_over_anything_is_identity() { for dst in [0u32, 0x1234_5678, 0xffff_ffff] { for src in [0xff00_0000u32, 0xffff_ffff, 0xff12_3456] { assert_eq!(blend_over(dst, src), src); } } } #[test] fn transparent_over_anything_is_noop() { for dst in [0u32, 0x1234_5678, 0xffff_ffff] { assert_eq!(blend_over(dst, 0), dst); } } fn winding_at(segs: &[(Point, Point)], x: f32, y: f32) -> i32 { let mut w = 0; for (a, b) in segs { if a.y <= y { if b.y > y && (b.x - a.x) * (y - a.y) - (x - a.x) * (b.y - a.y) > 0.0 { w += 1; } } else if b.y <= y && (b.x - a.x) * (y - a.y) - (x - a.x) * (b.y - a.y) < 0.0 { w -= 1; } } w } fn brute_force_coverage(path: &Path, w: usize, h: usize, ss: usize) -> Vec { let mut segs = Vec::new(); path.flatten(&IDENTITY, TOL, |a, b| segs.push((a, b))); let mut out = vec![0.0f32; w * h]; let step = 1.0 / ss as f32; for y in 0..h { for x in 0..w { let mut hits = 0; for sy in 0..ss { for sx in 0..ss { let px = x as f32 + (sx as f32 + 0.5) * step; let py = y as f32 + (sy as f32 + 0.5) * step; if winding_at(&segs, px, py) != 0 { hits += 1; } } } out[y * w + x] = hits as f32 / (ss * ss) as f32; } } out } fn rasterize(path: &Path, w: usize, h: usize) -> Vec { let mut ras = Rasterizer::new(w, h); path.flatten(&IDENTITY, TOL, |a, b| ras.line(a, b)); let mut out = vec![0.0f32; w * h]; ras.spans(|x, y, c| out[y * w + x] = c); out } fn compare_coverage(path: &Path, w: usize, h: usize, name: &str, max_tol: f32, mean_tol: f32) { let got = rasterize(path, w, h); let want = brute_force_coverage(path, w, h, 16); let mut worst = 0.0f32; let mut sum = 0.0f64; let mut worst_at = (0usize, 0usize); for y in 0..h { for x in 0..w { let d = (got[y * w + x] - want[y * w + x]).abs(); sum += d as f64; if d > worst { worst = d; worst_at = (x, y); } } } let mean = sum / (w * h) as f64; assert!( worst <= max_tol, "{name}: worst coverage error {worst:.4} at {worst_at:?} (limit {max_tol})" ); assert!(mean <= mean_tol as f64, "{name}: mean coverage error {mean:.5} (limit {mean_tol})"); } #[test] fn raster_matches_supersampled_rect() { let mut p = Path::new(); p.rect(Rect::new(3.25, 2.6, 28.75, 21.4)); compare_coverage(&p, 32, 24, "rect", 0.04, 0.002); } #[test] fn raster_matches_supersampled_circle() { let mut p = Path::new(); p.circle(pt(24.3, 20.7), 17.6); compare_coverage(&p, 48, 42, "circle", 0.04, 0.003); } #[test] fn raster_matches_supersampled_rrect() { let mut p = Path::new(); p.rrect(Rect::new(2.5, 3.5, 45.5, 30.5), 9.0); compare_coverage(&p, 48, 34, "rrect", 0.04, 0.003); } #[test] fn raster_matches_supersampled_triangle() { let mut p = Path::new(); p.move_to(pt(2.0, 30.0)); p.line_to(pt(31.0, 25.0)); p.line_to(pt(17.5, 1.5)); p.close(); compare_coverage(&p, 34, 32, "triangle", 0.04, 0.003); } #[test] fn raster_matches_supersampled_star() { let mut p = Path::new(); let c = pt(30.0, 30.0); for i in 0..10 { let a = -core::f32::consts::FRAC_PI_2 + i as f32 * core::f32::consts::PI / 5.0; let r = if i % 2 == 0 { 27.0 } else { 11.0 }; let q = pt(c.x + r * a.cos(), c.y + r * a.sin()); if i == 0 { p.move_to(q); } else { p.line_to(q); } } p.close(); compare_coverage(&p, 60, 60, "star", 0.05, 0.004); } #[test] fn shape_fully_outside_canvas_draws_nothing() { let mut c = Canvas::new(16, 16); c.clear(rgba(0, 0, 0, 255)); let before = c.px.clone(); let mut p = Path::new(); p.rect(Rect::new(-500.0, -500.0, -100.0, -100.0)); c.fill(&p, &IDENTITY, &Paint::Solid(rgba(255, 255, 255, 255))); p.clear(); p.rect(Rect::new(900.0, 900.0, 1200.0, 1200.0)); c.fill(&p, &IDENTITY, &Paint::Solid(rgba(255, 255, 255, 255))); assert_eq!(before, c.px); } #[test] fn shape_larger_than_canvas_fills_everything() { let mut c = Canvas::new(24, 18); c.clear(rgba(0, 0, 0, 255)); let mut p = Path::new(); p.rect(Rect::new(-40.0, -40.0, 80.0, 80.0)); c.fill(&p, &IDENTITY, &Paint::Solid(rgba(255, 255, 255, 255))); for (i, v) in c.px.iter().enumerate() { assert_eq!(*v, 0xffff_ffff, "pixel {i} not filled: 0x{v:08x}"); } } #[test] fn integer_rect_fill_is_exact_and_bounded() { let mut c = Canvas::new(20, 20); c.clear(rgba(0, 0, 0, 255)); c.fill_rect(Rect::new(5.0, 4.0, 15.0, 12.0), rgba(255, 0, 0, 255)); for y in 0..20 { for x in 0..20 { let v = c.px[y * 20 + x]; let inside = (5..15).contains(&x) && (4..12).contains(&y); let want = if inside { 0xffff_0000 } else { 0xff00_0000 }; assert_eq!(v, want, "pixel ({x},{y}) = 0x{v:08x}"); } } } #[test] fn clip_confines_drawing() { let mut c = Canvas::new(32, 32); c.clear(rgba(0, 0, 0, 255)); c.push_clip(Rect::new(8.0, 8.0, 24.0, 24.0)); let mut p = Path::new(); p.rect(Rect::new(0.0, 0.0, 32.0, 32.0)); c.fill(&p, &IDENTITY, &Paint::Solid(rgba(255, 255, 255, 255))); c.pop_clip(); for y in 0..32 { for x in 0..32 { let inside = (8..24).contains(&x) && (8..24).contains(&y); let v = c.px[y * 32 + x]; assert_eq!(v, if inside { 0xffff_ffff } else { 0xff00_0000 }, "({x},{y})"); } } } #[test] fn nested_clips_intersect_and_restore() { let mut c = Canvas::new(32, 32); c.push_clip(Rect::new(4.0, 4.0, 28.0, 28.0)); c.push_clip(Rect::new(0.0, 0.0, 10.0, 10.0)); assert_eq!(c.clip(), Rect::new(4.0, 4.0, 10.0, 10.0)); c.pop_clip(); assert_eq!(c.clip(), Rect::new(4.0, 4.0, 28.0, 28.0)); c.pop_clip(); assert_eq!(c.clip(), Rect::new(0.0, 0.0, 32.0, 32.0)); } #[test] fn opaque_fill_is_independent_of_background() { let mut a = Canvas::new(24, 24); let mut b = Canvas::new(24, 24); a.clear(rgba(0, 0, 0, 255)); b.clear(rgba(255, 255, 0, 255)); let mut p = Path::new(); p.rrect(Rect::new(2.0, 2.0, 22.0, 22.0), 6.0); let paint = Paint::Solid(rgba(10, 20, 30, 255)); a.fill(&p, &IDENTITY, &paint); b.fill(&p, &IDENTITY, &paint); for y in 6..18 { for x in 6..18 { let i = y * 24 + x; assert_eq!(a.px[i], b.px[i], "interior ({x},{y}) depends on backdrop"); } } } #[test] fn gradient_endpoints_are_exact() { let mut c = Canvas::new(64, 8); c.clear(rgba(0, 0, 0, 255)); let r = Rect::new(0.0, 0.0, 64.0, 8.0); let mut p = Path::new(); p.rect(r); c.fill(&p, &IDENTITY, &Paint::hgrad(r, rgba(255, 0, 0, 255), rgba(0, 0, 255, 255))); let left = c.px[4 * 64]; let right = c.px[4 * 64 + 63]; assert!(chan(left, 16) > 250 && chan(left, 0) < 5, "left 0x{left:08x}"); assert!(chan(right, 0) > 250 && chan(right, 16) < 5, "right 0x{right:08x}"); for x in 1..64 { let a = chan(c.px[4 * 64 + x - 1], 16); let b = chan(c.px[4 * 64 + x], 16); assert!(b <= a, "gradient not monotonic at x={x}: {a} then {b}"); } } #[test] fn stroke_is_centred_on_the_path() { let mut c = Canvas::new(32, 32); c.clear(rgba(0, 0, 0, 255)); let mut p = Path::new(); p.move_to(pt(0.0, 16.0)); p.line_to(pt(32.0, 16.0)); c.stroke(&p, &IDENTITY, &Paint::Solid(rgba(255, 255, 255, 255)), &Stroke::new(4.0)); for x in 4..28 { for y in 14..18 { assert_eq!(c.px[y * 32 + x], 0xffff_ffff, "({x},{y}) should be inside stroke"); } assert_eq!(c.px[13 * 32 + x], 0xff00_0000, "({x},13) should be outside stroke"); assert_eq!(c.px[18 * 32 + x], 0xff00_0000, "({x},18) should be outside stroke"); } } #[test] fn closed_stroke_leaves_a_hole() { let mut c = Canvas::new(40, 40); c.clear(rgba(0, 0, 0, 255)); let mut p = Path::new(); p.rect(Rect::new(8.0, 8.0, 32.0, 32.0)); c.stroke(&p, &IDENTITY, &Paint::Solid(rgba(255, 255, 255, 255)), &Stroke::new(4.0)); assert_eq!(c.px[20 * 40 + 20], 0xff00_0000, "centre of a stroked rect must stay empty"); assert_eq!(c.px[8 * 40 + 20], 0xffff_ffff, "top edge should be painted"); } #[test] fn shadow_is_darkest_under_the_shape_and_fades_out() { let mut c = Canvas::new(120, 120); c.clear(rgba(0, 0, 0, 0)); let mut p = Path::new(); p.rrect(Rect::new(40.0, 40.0, 80.0, 80.0), 8.0); c.shadow(&p, &IDENTITY, 8.0, rgba(0, 0, 0, 255), pt(0.0, 0.0)); let centre = c.px[60 * 120 + 60] >> 24; let edge = c.px[60 * 120 + 36] >> 24; let far = c.px[60 * 120 + 5] >> 24; assert!(centre > 240, "shadow centre alpha {centre}"); assert!(edge > 20 && edge < 240, "shadow edge alpha {edge}"); assert!(far < 4, "shadow should have faded by 35px out, got {far}"); } #[test] fn shadow_is_symmetric_for_a_symmetric_shape() { let mut c = Canvas::new(100, 100); c.clear(rgba(0, 0, 0, 0)); let mut p = Path::new(); p.circle(pt(50.5, 50.5), 20.0); c.shadow(&p, &IDENTITY, 6.0, rgba(0, 0, 0, 255), pt(0.0, 0.0)); for d in 1..40 { let l = (c.px[50 * 100 + (50 - d)] >> 24) as i32; let r = (c.px[50 * 100 + (50 + d)] >> 24) as i32; assert!((l - r).abs() <= 2, "asymmetric at d={d}: {l} vs {r}"); let u = (c.px[(50 - d) * 100 + 50] >> 24) as i32; assert!((l - u).abs() <= 3, "not radially symmetric at d={d}: {l} vs {u}"); } } #[test] fn blur_preserves_total_energy() { let w = 64; let h = 64; let mut buf = vec![0u32; w * h]; for y in 24..40 { for x in 24..40 { buf[y * w + x] = 0xffff_ffff; } } let before: u64 = buf.iter().map(|v| (v >> 24) as u64).sum(); nox_gfx::canvas::blur_premul(&mut buf, w, h, 5.0); let after: u64 = buf.iter().map(|v| (v >> 24) as u64).sum(); let drift = (before as f64 - after as f64).abs() / before as f64; assert!(drift < 0.05, "blur lost energy: {before} -> {after} ({drift:.3})"); let centre = buf[32 * w + 32] >> 24; assert!(centre > 200, "blur centre too dark: {centre}"); assert!(buf[2 * w + 2] >> 24 < 8, "blur bled too far"); } #[test] fn transform_compose_and_invert() { let a = Transform::translate(13.0, -4.0); let b = Transform::scale(2.0, 3.0); let c = Transform::rotate(0.7); let m = a.then(&b).then(&c); let inv = m.invert().expect("invertible"); for p in [pt(0.0, 0.0), pt(1.0, -2.0), pt(100.0, 55.0)] { let q = inv.apply(m.apply(p)); assert!((q.x - p.x).abs() < 1e-3 && (q.y - p.y).abs() < 1e-3, "{p:?} -> {q:?}"); } let step = a.then(&b); for p in [pt(3.0, 4.0), pt(-9.0, 2.5)] { let want = b.apply(a.apply(p)); let got = step.apply(p); assert!((want.x - got.x).abs() < 1e-4 && (want.y - got.y).abs() < 1e-4); } } #[test] fn flatten_stays_within_tolerance() { let mut p = Path::new(); p.move_to(pt(0.0, 0.0)); p.cubic_to(pt(0.0, 80.0), pt(120.0, 80.0), pt(120.0, 0.0)); let mut segs = Vec::new(); p.flatten(&IDENTITY, 0.1, |a, b| segs.push((a, b))); assert!(segs.len() > 8, "too few segments: {}", segs.len()); for t in 0..=200 { let t = t as f32 / 200.0; let mt = 1.0 - t; let exact = pt( 3.0 * mt * t * t * 120.0 + t * t * t * 120.0, 3.0 * mt * mt * t * 80.0 + 3.0 * mt * t * t * 80.0, ); let mut best = f32::MAX; for (a, b) in &segs { let d = b.sub_dist(*a, exact); best = best.min(d); } assert!(best <= 0.15, "curve point {exact:?} is {best} from the polyline"); } } trait SegDist { fn sub_dist(self, a: Point, p: Point) -> f32; } impl SegDist for Point { fn sub_dist(self, a: Point, p: Point) -> f32 { let ab = self - a; let len2 = ab.len_sq(); if len2 < 1e-9 { return p.dist(a); } let t = ((p - a).dot(ab) / len2).clamp(0.0, 1.0); p.dist(pt(a.x + ab.x * t, a.y + ab.y * t)) } } #[test] fn png_structure_is_valid() { let mut c = Canvas::new(9, 7); c.clear(rgba(30, 60, 90, 255)); let png = c.to_png(); assert_eq!(&png[..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]); let mut i = 8; let mut tags = Vec::new(); while i + 12 <= png.len() { let len = u32::from_be_bytes(png[i..i + 4].try_into().unwrap()) as usize; let tag = String::from_utf8_lossy(&png[i + 4..i + 8]).to_string(); let body = &png[i + 8..i + 8 + len]; let want = u32::from_be_bytes(png[i + 8 + len..i + 12 + len].try_into().unwrap()); let mut crc = 0xffff_ffffu32; for &b in png[i + 4..i + 8 + len].iter() { crc ^= b as u32; for _ in 0..8 { let m = (crc & 1).wrapping_neg(); crc = (crc >> 1) ^ (0xEDB8_8320 & m); } } assert_eq!(!crc, want, "bad CRC on chunk {tag}"); if tag == "IHDR" { assert_eq!(u32::from_be_bytes(body[0..4].try_into().unwrap()), 9); assert_eq!(u32::from_be_bytes(body[4..8].try_into().unwrap()), 7); assert_eq!(body[8], 8); assert_eq!(body[9], 6); } tags.push(tag); i += 12 + len; } assert_eq!(i, png.len(), "trailing bytes after last chunk"); assert_eq!(tags, vec!["IHDR", "IDAT", "IEND"]); } fn lcg(state: &mut u64) -> u32 { *state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); (*state >> 33) as u32 } fn random_row(seed: &mut u64, n: usize) -> (Vec, Vec) { let mut dst = Vec::with_capacity(n); let mut cov = Vec::with_capacity(n); for _ in 0..n { let a = (lcg(seed) & 0xff) as u32; let c = |s: &mut u64| ((lcg(s) & 0xff) * a / 255) & 0xff; dst.push((a << 24) | (c(seed) << 16) | (c(seed) << 8) | c(seed)); cov.push(match lcg(seed) % 5 { 0 => 0.0, 1 => 1.0, _ => (lcg(seed) % 1001) as f32 / 1000.0, }); } (dst, cov) } fn opaque_lut() -> [u32; 256] { let mut lut = [0u32; 256]; for (i, v) in lut.iter_mut().enumerate() { *v = 0xff00_0000 | ((i as u32) << 16) | ((255 - i as u32) << 8) | 0x40; } lut } fn translucent_lut() -> [u32; 256] { let mut lut = [0u32; 256]; for (i, v) in lut.iter_mut().enumerate() { let a = i as u32; *v = (a << 24) | ((a / 2) << 16) | ((a / 3) << 8) | (a / 4); } lut } #[test] fn simd_solid_matches_scalar() { let mut seed = 0x1234_5678_9abc_def0; for n in [0usize, 1, 3, 4, 7, 8, 16, 31, 64, 129] { for color in [0xff00_0000u32, 0xffff_ffff, 0x8040_2010, 0x0100_0000, 0] { let (dst, cov) = random_row(&mut seed, n); let mut a = dst.clone(); let mut b = dst; nox_gfx::simd::blend_solid_cov(&mut a, color, &cov); nox_gfx::simd::blend_solid_cov_scalar(&mut b, color, &cov); assert_eq!(a, b, "solid n={n} color=0x{color:08x}"); } } } #[test] fn simd_alpha_matches_scalar() { let mut seed = 0xfeed_face_cafe_babe; for n in [0usize, 1, 5, 8, 33, 100] { for color in [0xff00_0000u32, 0x8040_2010, 0xffff_ffff] { let (dst, _) = random_row(&mut seed, n); let alpha: Vec = (0..n).map(|_| (lcg(&mut seed) & 0xff) as u8).collect(); let mut a = dst.clone(); let mut b = dst; nox_gfx::simd::blend_solid_alpha(&mut a, color, &alpha); nox_gfx::simd::blend_solid_alpha_scalar(&mut b, color, &alpha); assert_eq!(a, b, "alpha n={n} color=0x{color:08x}"); } } } #[test] fn simd_linear_gradient_matches_scalar() { let mut seed = 0x0bad_c0de_dead_beef; for lut in [opaque_lut(), translucent_lut()] { for n in [0usize, 2, 4, 9, 17, 64, 130] { for (t0, dt) in [(0.0f32, 0.01f32), (-0.5, 0.003), (0.9, -0.02), (0.5, 0.0)] { let (dst, cov) = random_row(&mut seed, n); let mut a = dst.clone(); let mut b = dst; nox_gfx::simd::blend_lut_cov(&mut a, &lut, t0, dt, &cov, lut[0] >> 24 == 255); nox_gfx::simd::blend_lut_cov_scalar(&mut b, &lut, t0, dt, &cov); for i in 0..n { let d = |sh: u32| { (((a[i] >> sh) & 0xff) as i32 - ((b[i] >> sh) & 0xff) as i32).abs() }; assert!( d(24) <= 1 && d(16) <= 1 && d(8) <= 1 && d(0) <= 1, "linear n={n} i={i} t0={t0} dt={dt}: 0x{:08x} vs 0x{:08x}", a[i], b[i] ); } } } } } #[test] fn simd_radial_gradient_matches_scalar() { let mut seed = 0x5eed_1234_5678_9abc; for lut in [opaque_lut(), translucent_lut()] { for n in [0usize, 3, 4, 11, 40] { for (px0, py2, inv_r) in [(-20.0f32, 100.0f32, 0.02f32), (0.0, 0.0, 0.1), (5.0, 25.0, 0.005)] { let (dst, cov) = random_row(&mut seed, n); let mut a = dst.clone(); let mut b = dst; nox_gfx::simd::blend_radial_cov(&mut a, &lut, px0, py2, inv_r, &cov, lut[0] >> 24 == 255); nox_gfx::simd::blend_radial_cov_scalar(&mut b, &lut, px0, py2, inv_r, &cov); for i in 0..n { let d = |sh: u32| { (((a[i] >> sh) & 0xff) as i32 - ((b[i] >> sh) & 0xff) as i32).abs() }; assert!(d(24) <= 1 && d(16) <= 1 && d(8) <= 1 && d(0) <= 1, "radial n={n} i={i}"); } } } } } #[test] fn shadow_occluder_leaves_covered_region_untouched() { let mut a = Canvas::new(140, 140); let mut b = Canvas::new(140, 140); a.clear(rgba(0, 0, 0, 0)); b.clear(rgba(0, 0, 0, 0)); let mut p = Path::new(); p.rrect(Rect::new(40.0, 40.0, 100.0, 100.0), 10.0); let hole = Rect::new(45.0, 45.0, 95.0, 95.0); a.shadow(&p, &IDENTITY, 10.0, rgba(0, 0, 0, 255), pt(0.0, 6.0)); b.shadow_occluded(&p, &IDENTITY, 10.0, rgba(0, 0, 0, 255), pt(0.0, 6.0), hole); for y in 0..140 { for x in 0..140 { let i = y * 140 + x; let inside = (45..95).contains(&x) && (45..95).contains(&y); if inside { assert_eq!(b.px[i], 0, "occluded pixel ({x},{y}) should be skipped"); } else { assert_eq!(a.px[i], b.px[i], "visible pixel ({x},{y}) changed"); } } } } #[test] fn png_round_trips_through_a_real_inflater() { // Written out and read back by an independent decoder (python zlib) in // tests/decode_png.py, driven by the shell test below. Here we at least // guarantee the deflate stream is self-consistent for a decoder we write // ourselves: fixed-Huffman blocks, correct adler32, correct crc32. let mut c = Canvas::new(97, 53); c.clear(rgba(20, 30, 40, 255)); let mut p = Path::new(); p.rrect(Rect::new(8.5, 6.25, 80.0, 44.0), 11.0); c.fill(&p, &IDENTITY, &Paint::hgrad(Rect::new(0.0, 0.0, 97.0, 1.0), rgba(255, 0, 0, 255), rgba(0, 128, 255, 255))); let png = c.to_png(); let raw_size = 97 * 53 * 4; assert!(png.len() < raw_size / 2, "png did not compress: {} vs {raw_size} raw", png.len()); assert_eq!(&png[..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]); let idat_start = png.windows(4).position(|w| w == b"IDAT").expect("IDAT"); assert_eq!(png[idat_start + 4], 0x78, "zlib CMF byte"); assert_eq!(png[idat_start + 5], 0x9c, "zlib FLG byte"); } #[test] fn deflate_output_is_smaller_than_input_for_repetitive_data() { let mut data = Vec::new(); for i in 0..8000u32 { data.extend_from_slice(&(i % 61).to_le_bytes()); } let z = nox_gfx::deflate::zlib(&data, 24); assert!(z.len() < data.len() / 4, "expected strong compression, got {} of {}", z.len(), data.len()); assert_eq!( u32::from_be_bytes(z[z.len() - 4..].try_into().unwrap()), nox_gfx::deflate::adler32(&data) ); }