//! Comparison against Python `render_label` (qrcode 8.2 + Pillow 12.3). //! //! Golden data from `tools/golden.py`: //! * `qr_matrices.txt`: module matrices, versions and masks; //! * `label_*.png`: complete Python labels; //! * `text_info.txt`: Pillow's chosen font size, text bbox and draw origin. use std::collections::HashMap; use std::fs::File; use std::path::{Path, PathBuf}; use lamp_label::{Bitmap, LabelFont, TEXT_HEIGHT, qr, render_label, render_label_with_font}; fn golden() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("golden") } fn load_png(path: &Path) -> Bitmap { let mut decoder = png::Decoder::new(File::open(path).unwrap()); decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16); let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).unwrap(); let channels = info.color_type.samples(); Bitmap::from_fn(info.width, info.height, |x, y| { buf[y as usize * info.line_size + x as usize * channels] < 128 }) } /// id -> label png file name fn label_files() -> HashMap { std::fs::read_to_string(golden().join("labels.txt")) .unwrap() .lines() .filter(|l| !l.is_empty()) .map(|l| { let (id, file) = l.split_once('\t').unwrap(); (id.to_string(), file.to_string()) }) .collect() } struct GoldenQr { id: String, version: u8, mask: u8, modules: Vec>, } fn golden_qr() -> Vec { let text = std::fs::read_to_string(golden().join("qr_matrices.txt")).unwrap(); let mut lines = text.lines(); let mut out = Vec::new(); while let Some(header) = lines.next() { if header.is_empty() { continue; } let mut parts = header.rsplitn(4, ' '); let size: usize = parts .next() .unwrap() .strip_prefix("size=") .unwrap() .parse() .unwrap(); let mask: u8 = parts .next() .unwrap() .strip_prefix("mask=") .unwrap() .parse() .unwrap(); let version: u8 = parts .next() .unwrap() .strip_prefix("version=") .unwrap() .parse() .unwrap(); let id = parts.next().unwrap().to_string(); let modules = (0..size) .map(|_| lines.next().unwrap().bytes().map(|b| b == b'1').collect()) .collect(); out.push(GoldenQr { id, version, mask, modules, }); } out } #[test] fn qr_matrices_match_python() { let cases = golden_qr(); assert!(cases.len() >= 28, "golden data missing"); let mut failures = Vec::new(); for g in &cases { let q = qr::encode(&g.id.to_uppercase()).unwrap(); let same_modules = (0..q.size).all(|r| (0..q.size).all(|c| q.get(c, r) == g.modules[r][c])); if q.version != g.version || q.mask != g.mask || q.size != g.modules.len() || !same_modules { failures.push(format!( "{}: rust v{} mask {} vs python v{} mask {} (modules equal: {same_modules})", g.id, q.version, q.mask, g.version, g.mask )); } } println!( "QR: {}/{} matrices identical (version, mask and modules)", cases.len() - failures.len(), cases.len() ); assert!(failures.is_empty(), "{}", failures.join("\n")); } #[test] fn qr_region_of_label_matches_python() { let files = label_files(); let qr_rows = 202 - TEXT_HEIGHT; let mut checked = 0; for g in golden_qr() { let py = load_png(&golden().join(&files[&g.id])); let rs = render_label(&g.id); let diff = (0..qr_rows) .flat_map(|y| (0..202).map(move |x| (x, y))) .filter(|&(x, y)| py.get(x, y) != rs.get(x, y)) .count(); assert_eq!(diff, 0, "{}: {diff} pixels differ in the QR area", g.id); checked += 1; } println!("QR area (rows 0..150) pixel-identical for {checked} labels"); } struct TextGolden { id: String, font: String, size: u32, bbox: (i32, i32, i32, i32), origin: (i32, i32), } fn text_golden() -> Vec { let text = std::fs::read_to_string(golden().join("text_info.txt")).unwrap(); text.lines() .filter(|l| !l.is_empty()) .map(|l| { let mut f = l.split('\t'); let id = f.next().unwrap().to_string(); let mut kv = HashMap::new(); for p in f { let (k, v) = p.split_once('=').unwrap(); kv.insert(k, v); } let nums = |s: &str| { s.split(',') .map(|n| n.parse::().unwrap()) .collect::>() }; let b = nums(kv["bbox"]); let o = nums(kv["origin"]); TextGolden { id, font: kv["font"].to_string(), size: kv["size"].parse().unwrap(), bbox: (b[0], b[1], b[2], b[3]), origin: (o[0], o[1]), } }) .collect() } #[test] fn text_strip_close_to_python_with_arial_bold() { let arial = Path::new("C:/Windows/Fonts/arialbd.ttf"); if !arial.exists() { eprintln!("arialbd.ttf not available, skipping the text comparison"); return; } let font = LabelFont::from_file(arial).unwrap(); let files = label_files(); let top = 202 - TEXT_HEIGHT; let mut report = Vec::new(); let mut layout_mismatch = Vec::new(); let mut too_different = Vec::new(); for g in text_golden() { assert_eq!(g.font, "arialbd.ttf"); let (rs, layout) = render_label_with_font(&g.id, &font).unwrap(); let py = load_png(&golden().join(&files[&g.id])); let (mut diff, mut ink_py, mut ink_rs) = (0, 0, 0); for y in top..202 { for x in 0..202 { let (p, r) = (py.get(x, y), rs.get(x, y)); ink_py += usize::from(p); ink_rs += usize::from(r); diff += usize::from(p != r); } } report.push(format!( "{:<16} size py {:>2} rs {:>2} | bbox py {:?} rs {:?} | origin py {:?} rs {:?} | ink py {:>4} rs {:>4} | differing px {:>4} ({:.1} % of ink)", g.id, g.size, layout.size, g.bbox, layout.bbox, g.origin, layout.origin, ink_py, ink_rs, diff, 100.0 * diff as f64 / ink_py.max(1) as f64 )); if (layout.size, layout.bbox, layout.origin) != (g.size, g.bbox, g.origin) { layout_mismatch.push(g.id.clone()); } // glyph rasterisation may differ (no TrueType hinting), but the text // must be essentially the same pixels if diff * 4 >= ink_py { too_different.push(format!( "{}: {diff} differing pixels for {ink_py} ink pixels", g.id )); } } println!("text strip vs Python (arialbd.ttf):\n{}", report.join("\n")); assert!(too_different.is_empty(), "{}", too_different.join("\n")); // chosen size, text bbox and draw origin must be exactly Pillow's assert!( layout_mismatch.is_empty(), "size/bbox/origin differ for {layout_mismatch:?}" ); } #[test] fn builtin_font_fallback_renders() { let font = LabelFont::builtin(); let (img, layout) = render_label_with_font("L0006", &font).unwrap(); assert_eq!(layout.font, "builtin-5x7"); assert_eq!(layout.size, 44); // 5 glyphs of 5x7 cells at scale 4: 116 x 28, centred in the strip assert_eq!(layout.bbox, (0, 0, 116, 28)); assert_eq!(layout.origin, (43, 162)); let ink: usize = (150..202) .flat_map(|y| (0..202).map(move |x| (x, y))) .filter(|&(x, y)| img.get(x, y)) .count(); assert!(ink > 500, "text missing ({ink} px)"); // long IDs shrink let (_, layout) = render_label_with_font("L0123456789012345", &font).unwrap(); assert!(layout.size < 44 && layout.bbox.2 <= 194, "{layout:?}"); // same QR as with the TrueType font let (img2, _) = render_label_with_font("L0006", &LabelFont::system()).unwrap(); assert!((0..150).all(|y| (0..202).all(|x| img.get(x, y) == img2.get(x, y)))); } #[test] fn png_roundtrip_and_preview() { let label = render_label("L0006"); let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")); let path = dir.join("lamp_label_L0006.png"); lamp_label::save_png(&label, &path).unwrap(); assert_eq!(load_png(&path), label); println!("preview written to {}", path.display()); } #[test] fn instructions_for_two_labels() { let data = lamp_label::label_instructions("L0006", 2).unwrap(); // Python output for the Python-rendered L0006 label (brother-ql golden data): // same length and identical command bytes; only raster lines carrying // text pixels may differ. let py = std::fs::read( Path::new(env!("CARGO_MANIFEST_DIR")).join("../brother-ql/tests/golden/label_L0006_x2.bin"), ) .unwrap(); assert_eq!(data.len(), py.len()); // preamble 4 + 400 + 2 + 4, page header 3 + 13 + 4 + 4 + 4 + 5, 202 lines of 93, 0x1A let (pre, head, lines) = (410, 33, 202 * 93); let page = head + lines + 1; assert_eq!(data.len(), pre + 2 * page); assert_eq!(data[..pre + head], py[..pre + head]); for p in 0..2 { let start = pre + p * page; assert_eq!(data[start..start + head], py[start..start + head]); assert_eq!(data[start + page - 1], 0x1A); for l in 0..202 { let row = start + head + l * 93; assert_eq!(data[row..row + 3], [0x67, 0x00, 90]); } } }