//! Our PNG writer is only correct if a decoder we did not write accepts it. //! This drives python3's zlib as an independent inflater, unfilters the rows by //! the spec, and requires an exact pixel match. Skips if python3 is unavailable. use nox_gfx::canvas::{Canvas, Paint, Stroke}; use nox_gfx::color::{hex, rgba}; use nox_gfx::geom::{pt, Rect, IDENTITY}; use nox_gfx::path::Path; use std::process::Command; const DECODER: &str = r#" import sys, zlib, struct png = open(sys.argv[1],'rb').read() assert png[:8] == b'\x89PNG\r\n\x1a\n', 'bad signature' i, idat, ihdr = 8, b'', None while i < len(png): n = struct.unpack_from('>I', png, i)[0] tag = png[i+4:i+8] body = png[i+8:i+8+n] want = struct.unpack_from('>I', png, i+8+n)[0] got = zlib.crc32(tag + body) & 0xffffffff assert got == want, 'crc mismatch on %s' % tag if tag == b'IHDR': ihdr = body if tag == b'IDAT': idat += body i += 12 + n w, h, depth, ctype = struct.unpack('>IIBB', ihdr[:10]) assert (depth, ctype) == (8, 6), 'expected 8-bit RGBA' raw = zlib.decompress(idat) bpp, stride = 4, w*4 out = bytearray() prev = bytearray(stride) p = 0 for y in range(h): f = raw[p]; p += 1 line = bytearray(raw[p:p+stride]); p += stride for x in range(stride): a = line[x-bpp] if x >= bpp else 0 b = prev[x] c = prev[x-bpp] if x >= bpp else 0 if f == 1: line[x] = (line[x] + a) & 0xff elif f == 2: line[x] = (line[x] + b) & 0xff elif f == 3: line[x] = (line[x] + (a+b)//2) & 0xff elif f == 4: pp = a + b - c pa, pb, pc = abs(pp-a), abs(pp-b), abs(pp-c) pr = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c) line[x] = (line[x] + pr) & 0xff elif f != 0: raise SystemExit('bad filter %d' % f) out += line prev = line assert p == len(raw), 'trailing data' open(sys.argv[2],'wb').write(bytes(out)) print('%d %d' % (w, h)) "#; fn scene() -> Canvas { let mut c = Canvas::new(233, 149); c.clear(hex(0x101018)); let mut p = Path::new(); p.rrect(Rect::new(10.5, 8.25, 220.0, 120.75), 14.0); c.fill(&p, &IDENTITY, &Paint::vgrad(Rect::new(0.0, 0.0, 1.0, 149.0), hex(0x7c5cff), hex(0x35d0ff))); c.stroke(&p, &IDENTITY, &Paint::Solid(rgba(255, 255, 255, 90)), &Stroke::new(1.5)); p.clear(); p.circle(pt(70.0, 60.0), 33.0); c.shadow(&p, &IDENTITY, 9.0, rgba(0, 0, 0, 200), pt(2.0, 5.0)); c.fill(&p, &IDENTITY, &Paint::glow(pt(70.0, 60.0), 33.0, hex(0xffd166), rgba(255, 122, 89, 120))); for i in 0..24 { let x = 8.0 + i as f32 * 9.3; c.fill_rect(Rect::new(x, 128.0, x + 6.0, 128.0 + (i % 7) as f32 * 2.5 + 3.0), hex(0x3ddc97)); } c } #[test] fn png_decodes_byte_identically_in_an_independent_decoder() { if Command::new("python3").arg("--version").output().is_err() { eprintln!("python3 not available, skipping"); return; } let dir = std::env::temp_dir().join("nox-png-test"); std::fs::create_dir_all(&dir).unwrap(); let png_path = dir.join("scene.png"); let out_path = dir.join("scene.raw"); let script = dir.join("decode.py"); std::fs::write(&script, DECODER).unwrap(); let c = scene(); let png = c.to_png(); std::fs::write(&png_path, &png).unwrap(); let expected = c.to_rgba(); let raw_bytes = c.w * c.h * 4; assert!( png.len() * 3 < raw_bytes, "png should be at least 3x smaller than raw ({} vs {raw_bytes})", png.len() ); let out = Command::new("python3") .arg(&script) .arg(&png_path) .arg(&out_path) .output() .expect("run decoder"); assert!( out.status.success(), "decoder rejected our png:\n{}", String::from_utf8_lossy(&out.stderr) ); assert_eq!( String::from_utf8_lossy(&out.stdout).trim(), format!("{} {}", c.w, c.h) ); let decoded = std::fs::read(&out_path).unwrap(); assert_eq!(decoded.len(), expected.len(), "decoded size mismatch"); for (i, (a, b)) in decoded.iter().zip(expected.iter()).enumerate() { assert_eq!(a, b, "pixel byte {i} differs: decoded {a} vs encoded {b}"); } } #[test] fn png_survives_awkward_sizes() { if Command::new("python3").arg("--version").output().is_err() { return; } let dir = std::env::temp_dir().join("nox-png-test"); std::fs::create_dir_all(&dir).unwrap(); let script = dir.join("decode.py"); std::fs::write(&script, DECODER).unwrap(); for (w, h) in [(1usize, 1usize), (1, 40), (40, 1), (3, 7), (255, 2), (2, 255)] { let mut c = Canvas::new(w, h); c.clear(rgba(9, 200, 30, 255)); c.fill_rect(Rect::new(0.0, 0.0, w as f32 / 2.0, h as f32 / 2.0), rgba(255, 0, 0, 255)); let png = c.to_png(); let p = dir.join(format!("s{w}x{h}.png")); let o = dir.join(format!("s{w}x{h}.raw")); std::fs::write(&p, &png).unwrap(); let out = Command::new("python3").arg(&script).arg(&p).arg(&o).output().unwrap(); assert!( out.status.success(), "{w}x{h} rejected:\n{}", String::from_utf8_lossy(&out.stderr) ); assert_eq!(std::fs::read(&o).unwrap(), c.to_rgba(), "{w}x{h} pixels differ"); } }