Lamp testbench in Rust around the HPCS 6500
lampbench station / export / label / tm30 / calibrate replace the Python lamp_station.py, lamp_export.py, label_printer.py, tm30.py and calibration.py that lived in HPCS6500-py. The driver is a submodule (HPCS6500/, like mppt-testbench). Workspace crates: tm30 (TM-30-18 port of colour-science 0.4.7), brother-ql (raster protocol, byte-identical to brother_ql) and lamp-label (QR + ID labels for the QL-820NWB). Bundles are byte-identical to lamp_export.py's (golden tests); the station runs end to end in a test against the simulated instrument.
No files matched your search
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "lamp-label"
|
||||
description = "Lamp ID labels (QR code + text) for the Brother QL-820NWB on DK-11221 23x23 mm labels"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "lamp_label"
|
||||
|
||||
[dependencies]
|
||||
brother-ql = { path = "../brother-ql" }
|
||||
fontdue = "0.9.4"
|
||||
png = "0.17"
|
||||
qrcode = { version = "0.14", default-features = false }
|
||||
@@ -0,0 +1,57 @@
|
||||
//! Tiny embedded 5x7 bitmap font, used only when no TrueType font file is
|
||||
//! available so that printing never fails because of fonts.
|
||||
|
||||
/// Glyph rows top to bottom; bit 4 is the leftmost column.
|
||||
pub(crate) fn glyph(c: char) -> [u8; 7] {
|
||||
match c.to_ascii_uppercase() {
|
||||
'0' => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E],
|
||||
'1' => [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E],
|
||||
'2' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F],
|
||||
'3' => [0x1F, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0E],
|
||||
'4' => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02],
|
||||
'5' => [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E],
|
||||
'6' => [0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E],
|
||||
'7' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
|
||||
'8' => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
|
||||
'9' => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C],
|
||||
'A' => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
|
||||
'B' => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E],
|
||||
'C' => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E],
|
||||
'D' => [0x1C, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1C],
|
||||
'E' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F],
|
||||
'F' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10],
|
||||
'G' => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0F],
|
||||
'H' => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
|
||||
'I' => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
|
||||
'J' => [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C],
|
||||
'K' => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
|
||||
'L' => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F],
|
||||
'M' => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
|
||||
'N' => [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11],
|
||||
'O' => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
|
||||
'P' => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10],
|
||||
'Q' => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D],
|
||||
'R' => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11],
|
||||
'S' => [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E],
|
||||
'T' => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
|
||||
'U' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
|
||||
'V' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04],
|
||||
'W' => [0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0A],
|
||||
'X' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
|
||||
'Y' => [0x11, 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04],
|
||||
'Z' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F],
|
||||
'-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
|
||||
'_' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F],
|
||||
'.' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C],
|
||||
':' => [0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x00],
|
||||
'/' => [0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10],
|
||||
' ' => [0; 7],
|
||||
// anything else: an empty box (like a .notdef glyph)
|
||||
_ => [0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F],
|
||||
}
|
||||
}
|
||||
|
||||
/// Glyph cell: 5 columns + 1 column spacing, 7 rows.
|
||||
pub(crate) const COLS: u32 = 5;
|
||||
pub(crate) const ADVANCE: u32 = 6;
|
||||
pub(crate) const ROWS: u32 = 7;
|
||||
@@ -0,0 +1,211 @@
|
||||
//! Lamp ID labels for the Brother QL-820NWB on DK-11221 (23 x 23 mm die-cut)
|
||||
//! labels: a Rust port of the HPCS `label_printer.py`.
|
||||
//!
|
||||
//! Each 202 x 202 dot label carries the lamp ID as a QR code (top 150 dots)
|
||||
//! and as text (bottom 52 dots). Scanning the QR code types the ID.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! let label = lamp_label::render_label("L0006");
|
||||
//! lamp_label::save_png(&label, std::path::Path::new("L0006.png"))?;
|
||||
//! lamp_label::print_labels("L0006", "tcp://192.168.1.50", 2)?;
|
||||
//! # Ok::<(), lamp_label::Error>(())
|
||||
//! ```
|
||||
|
||||
mod font5x7;
|
||||
pub mod qr;
|
||||
pub mod text;
|
||||
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
use std::path::Path;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub use brother_ql::Bitmap;
|
||||
use brother_ql::{ConvertOptions, DEFAULT_TIMEOUT, LABEL_23X23, QL_820NWB};
|
||||
pub use text::{LabelFont, TextLayout};
|
||||
|
||||
/// Printable area of the 23x23 label in dots (width, height).
|
||||
pub const DOTS: (u32, u32) = (202, 202);
|
||||
/// Height of the bottom strip holding the ID text.
|
||||
pub const TEXT_HEIGHT: u32 = 52;
|
||||
/// brother_ql model identifier of the label printer.
|
||||
pub const MODEL: &str = "QL-820NWB";
|
||||
/// brother_ql label identifier (DK-11221).
|
||||
pub const LABEL_ID: &str = "23x23";
|
||||
/// Labels printed per lamp by default (one for the box, one for the lamp).
|
||||
pub const DEFAULT_COPIES: u32 = 2;
|
||||
|
||||
/// Errors of this crate.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Conversion or sending failed.
|
||||
Printer(brother_ql::Error),
|
||||
/// The ID is too long for a version 40 QR code.
|
||||
QrDataTooLong(usize),
|
||||
/// A font file could not be parsed.
|
||||
Font(String),
|
||||
/// File I/O failed.
|
||||
Io(std::io::Error),
|
||||
/// PNG encoding failed.
|
||||
Png(png::EncodingError),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Printer(e) => write!(f, "label printer: {e}"),
|
||||
Error::QrDataTooLong(n) => write!(f, "lamp ID of {n} bytes does not fit in a QR code"),
|
||||
Error::Font(e) => write!(f, "font: {e}"),
|
||||
Error::Io(e) => write!(f, "I/O: {e}"),
|
||||
Error::Png(e) => write!(f, "PNG: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Error::Printer(e) => Some(e),
|
||||
Error::Io(e) => Some(e),
|
||||
Error::Png(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<brother_ql::Error> for Error {
|
||||
fn from(e: brother_ql::Error) -> Self {
|
||||
Error::Printer(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<png::EncodingError> for Error {
|
||||
fn from(e: png::EncodingError) -> Self {
|
||||
Error::Png(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// The font used by [`render_label`]: [`LabelFont::system`], loaded once.
|
||||
pub fn default_font() -> &'static LabelFont {
|
||||
static FONT: OnceLock<LabelFont> = OnceLock::new();
|
||||
FONT.get_or_init(LabelFont::system)
|
||||
}
|
||||
|
||||
/// Renders the 202x202 1-bit label for `lamp_id` with the default font.
|
||||
///
|
||||
/// QR code of the upper-cased ID (level M, quiet zone 2, module size
|
||||
/// `max(1, 150 // (modules + 4))`) centred in the top 150 dots; the ID as
|
||||
/// given, in the largest font size from 44 down (step 2) whose text box is at
|
||||
/// most 194 dots wide, centred in the bottom 52 dots.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the ID is too long for any QR version (thousands of characters); use
|
||||
/// [`try_render_label`] to get an error instead.
|
||||
pub fn render_label(lamp_id: &str) -> Bitmap {
|
||||
try_render_label(lamp_id).expect("lamp ID too long for a QR code")
|
||||
}
|
||||
|
||||
/// Like [`render_label`] but returns an error for IDs that do not fit a QR code.
|
||||
pub fn try_render_label(lamp_id: &str) -> Result<Bitmap, Error> {
|
||||
render_label_with_font(lamp_id, default_font()).map(|(b, _)| b)
|
||||
}
|
||||
|
||||
/// Renders the label with a specific font, also returning the text layout.
|
||||
pub fn render_label_with_font(
|
||||
lamp_id: &str,
|
||||
font: &LabelFont,
|
||||
) -> Result<(Bitmap, TextLayout), Error> {
|
||||
let (w, h) = DOTS;
|
||||
let mut image = Bitmap::new(w, h);
|
||||
|
||||
let upper = lamp_id.to_uppercase();
|
||||
let qr = qr::encode(&upper).map_err(|_| Error::QrDataTooLong(upper.len()))?;
|
||||
let border = 2;
|
||||
let modules = qr.size as u32 + 2 * border;
|
||||
let qr_size = h - TEXT_HEIGHT;
|
||||
let box_size = (qr_size / modules).max(1);
|
||||
let img_size = modules * box_size;
|
||||
let x0 = (i64::from(w) - i64::from(img_size)).div_euclid(2);
|
||||
let y0 = (i64::from(qr_size) - i64::from(img_size)).div_euclid(2);
|
||||
// the QR image (white quiet zone included) is pasted over the white label
|
||||
image.fill_rect(x0, y0, img_size, img_size, false);
|
||||
for r in 0..qr.size {
|
||||
for c in 0..qr.size {
|
||||
if qr.get(c, r) {
|
||||
image.fill_rect(
|
||||
x0 + i64::from((c as u32 + border) * box_size),
|
||||
y0 + i64::from((r as u32 + border) * box_size),
|
||||
box_size,
|
||||
box_size,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let layout = text::draw_text(
|
||||
&mut image,
|
||||
font,
|
||||
lamp_id,
|
||||
(h - TEXT_HEIGHT) as i32,
|
||||
TEXT_HEIGHT as i32,
|
||||
);
|
||||
Ok((image, layout))
|
||||
}
|
||||
|
||||
/// The raster instructions `print_labels` sends: `copies` labels for the
|
||||
/// QL-820NWB on 23x23 labels with cutting enabled (Python:
|
||||
/// `convert(BrotherQLRaster('QL-820NWB'), [image] * copies, '23x23', cut=True)`).
|
||||
pub fn label_instructions(lamp_id: &str, copies: u32) -> Result<Vec<u8>, Error> {
|
||||
let image = try_render_label(lamp_id)?;
|
||||
let images = vec![image; copies as usize];
|
||||
let opts = ConvertOptions {
|
||||
cut: true,
|
||||
..ConvertOptions::default()
|
||||
};
|
||||
Ok(brother_ql::convert(
|
||||
&QL_820NWB,
|
||||
&LABEL_23X23,
|
||||
&images,
|
||||
&opts,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Renders and prints `copies` labels on the QL-820NWB at `printer`
|
||||
/// (`tcp://host[:port]` or `host`, port 9100 by default). Returns once the
|
||||
/// job is written to the printer's socket; like the Python version it does
|
||||
/// not wait for a print confirmation (the network backend has no read-back).
|
||||
pub fn print_labels(lamp_id: &str, printer: &str, copies: u32) -> Result<(), Error> {
|
||||
let data = label_instructions(lamp_id, copies)?;
|
||||
brother_ql::send_network(printer, &data, true, DEFAULT_TIMEOUT)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes a bitmap as a 1-bit grayscale PNG (white = 1, like PIL mode "1").
|
||||
pub fn save_png(bitmap: &Bitmap, path: &Path) -> Result<(), Error> {
|
||||
let file = BufWriter::new(File::create(path)?);
|
||||
let mut enc = png::Encoder::new(file, bitmap.width(), bitmap.height());
|
||||
enc.set_color(png::ColorType::Grayscale);
|
||||
enc.set_depth(png::BitDepth::One);
|
||||
let mut writer = enc.write_header()?;
|
||||
let stride = bitmap.width().div_ceil(8) as usize;
|
||||
let mut data = vec![0u8; stride * bitmap.height() as usize];
|
||||
for y in 0..bitmap.height() {
|
||||
for (x, &black) in bitmap.row(y).iter().enumerate() {
|
||||
if !black {
|
||||
data[y as usize * stride + x / 8] |= 0x80 >> (x % 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.write_image_data(&data)?;
|
||||
writer.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
//! QR encoding that reproduces Python `qrcode` 8.2 exactly.
|
||||
//!
|
||||
//! The `qrcode` crate supplies bit packing, Reed-Solomon codewords, module
|
||||
//! placement and masking. Two decisions are made here the Python way so the
|
||||
//! module matrices are identical:
|
||||
//!
|
||||
//! * **segmentation**: `qrcode.util.optimal_data_chunks(data, minimum=20)`
|
||||
//! (`QRCode.add_data` default `optimize=20`), not the crate's optimiser;
|
||||
//! * **mask choice**: `QRCode.best_mask_pattern`, i.e. `util.lost_point`
|
||||
//! evaluated on matrices whose format/version information is all light
|
||||
//! ("test" mode), first minimum wins.
|
||||
|
||||
use qrcode::bits::Bits;
|
||||
use qrcode::canvas::{Canvas, MaskPattern};
|
||||
use qrcode::ec::construct_codewords;
|
||||
use qrcode::{Color, EcLevel, Version};
|
||||
|
||||
/// QR data segment mode.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
/// Digits only.
|
||||
Numeric,
|
||||
/// `0-9 A-Z $%*+-./:` and space.
|
||||
Alphanumeric,
|
||||
/// 8-bit bytes (UTF-8 of the input).
|
||||
Byte,
|
||||
}
|
||||
|
||||
/// A data segment with its mode.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Segment<'a> {
|
||||
/// Encoding mode.
|
||||
pub mode: Mode,
|
||||
/// The bytes of this segment.
|
||||
pub data: &'a [u8],
|
||||
}
|
||||
|
||||
const ALPHA_NUM: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
||||
|
||||
fn is_digit(b: u8) -> bool {
|
||||
b.is_ascii_digit()
|
||||
}
|
||||
|
||||
fn is_alnum(b: u8) -> bool {
|
||||
ALPHA_NUM.contains(&b)
|
||||
}
|
||||
|
||||
/// One `re.search` step of `_optimal_split`: the first match `(start, end)`.
|
||||
fn search(
|
||||
data: &[u8],
|
||||
class: fn(u8) -> bool,
|
||||
whole: bool,
|
||||
minimum: usize,
|
||||
) -> Option<(usize, usize)> {
|
||||
if whole {
|
||||
// ^X+$ ; `$` also matches right before a single trailing newline
|
||||
let all = |s: &[u8]| !s.is_empty() && s.iter().all(|&b| class(b));
|
||||
if all(data) {
|
||||
return Some((0, data.len()));
|
||||
}
|
||||
return match data.strip_suffix(b"\n") {
|
||||
Some(body) if all(body) => Some((0, body.len())),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
// X{minimum,}: leftmost start of a run of at least `minimum`, greedy
|
||||
let mut i = 0;
|
||||
while i < data.len() {
|
||||
if class(data[i]) {
|
||||
let start = i;
|
||||
while i < data.len() && class(data[i]) {
|
||||
i += 1;
|
||||
}
|
||||
if i - start >= minimum {
|
||||
return Some((start, i));
|
||||
}
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `_optimal_split`: alternating (matched, chunk) pieces.
|
||||
fn optimal_split(
|
||||
mut data: &[u8],
|
||||
class: fn(u8) -> bool,
|
||||
whole: bool,
|
||||
minimum: usize,
|
||||
) -> Vec<(bool, &[u8])> {
|
||||
let mut out = Vec::new();
|
||||
while !data.is_empty() {
|
||||
let Some((start, end)) = search(data, class, whole, minimum) else {
|
||||
break;
|
||||
};
|
||||
if start > 0 {
|
||||
out.push((false, &data[..start]));
|
||||
}
|
||||
out.push((true, &data[start..end]));
|
||||
data = &data[end..];
|
||||
}
|
||||
if !data.is_empty() {
|
||||
out.push((false, data));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Port of `qrcode.util.optimal_data_chunks(data, minimum)`.
|
||||
pub fn optimal_segments(data: &[u8], minimum: usize) -> Vec<Segment<'_>> {
|
||||
let whole = data.len() <= minimum;
|
||||
let mut out = Vec::new();
|
||||
for (is_num, chunk) in optimal_split(data, is_digit, whole, minimum) {
|
||||
if is_num {
|
||||
out.push(Segment {
|
||||
mode: Mode::Numeric,
|
||||
data: chunk,
|
||||
});
|
||||
} else {
|
||||
for (is_alpha, sub) in optimal_split(chunk, is_alnum, whole, minimum) {
|
||||
out.push(Segment {
|
||||
mode: if is_alpha {
|
||||
Mode::Alphanumeric
|
||||
} else {
|
||||
Mode::Byte
|
||||
},
|
||||
data: sub,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A QR symbol: modules without quiet zone, `true` = dark.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct QrMatrix {
|
||||
/// QR version (1-40).
|
||||
pub version: u8,
|
||||
/// Mask pattern number (0-7) chosen like Python qrcode.
|
||||
pub mask: u8,
|
||||
/// Modules per side (`4 * version + 17`).
|
||||
pub size: usize,
|
||||
/// Row-major modules, `true` = dark.
|
||||
pub modules: Vec<bool>,
|
||||
}
|
||||
|
||||
impl QrMatrix {
|
||||
/// Module at column `x`, row `y`.
|
||||
pub fn get(&self, x: usize, y: usize) -> bool {
|
||||
self.modules[y * self.size + x]
|
||||
}
|
||||
}
|
||||
|
||||
/// The input does not fit in a version 40 QR code at level M.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DataTooLong;
|
||||
|
||||
const PATTERNS: [MaskPattern; 8] = [
|
||||
MaskPattern::Checkerboard,
|
||||
MaskPattern::HorizontalLines,
|
||||
MaskPattern::VerticalLines,
|
||||
MaskPattern::DiagonalLines,
|
||||
MaskPattern::LargeCheckerboard,
|
||||
MaskPattern::Fields,
|
||||
MaskPattern::Diamonds,
|
||||
MaskPattern::Meadow,
|
||||
];
|
||||
|
||||
fn build_bits(segments: &[Segment<'_>], version: i16) -> Option<Bits> {
|
||||
let mut bits = Bits::new(Version::Normal(version));
|
||||
for s in segments {
|
||||
let r = match s.mode {
|
||||
Mode::Numeric => bits.push_numeric_data(s.data),
|
||||
Mode::Alphanumeric => bits.push_alphanumeric_data(s.data),
|
||||
Mode::Byte => bits.push_byte_data(s.data),
|
||||
};
|
||||
r.ok()?;
|
||||
}
|
||||
bits.push_terminator(EcLevel::M).ok()?;
|
||||
Some(bits)
|
||||
}
|
||||
|
||||
/// Encodes `data` exactly like
|
||||
/// `qrcode.QRCode(error_correction=ERROR_CORRECT_M); add_data(data); make(fit=True)`.
|
||||
pub fn encode(data: &str) -> Result<QrMatrix, DataTooLong> {
|
||||
let segments = optimal_segments(data.as_bytes(), 20);
|
||||
let (version, bits) = (1..=40)
|
||||
.find_map(|v| build_bits(&segments, v).map(|b| (v, b)))
|
||||
.ok_or(DataTooLong)?;
|
||||
let qr_version = Version::Normal(version);
|
||||
let raw = bits.into_bytes();
|
||||
let (codewords, ec) =
|
||||
construct_codewords(&raw, qr_version, EcLevel::M).map_err(|_| DataTooLong)?;
|
||||
let mut canvas = Canvas::new(qr_version, EcLevel::M);
|
||||
canvas.draw_all_functional_patterns();
|
||||
canvas.draw_data(&codewords, &ec);
|
||||
|
||||
let n = qr_version.width() as usize;
|
||||
let mut best = 0usize;
|
||||
let mut best_lost = 0u64;
|
||||
for (i, &p) in PATTERNS.iter().enumerate() {
|
||||
let mut c = canvas.clone();
|
||||
c.apply_mask(p);
|
||||
let mut m = to_bools(c, n);
|
||||
clear_type_info(&mut m, n, version);
|
||||
let lost = lost_point(&m, n);
|
||||
if i == 0 || best_lost > lost {
|
||||
best_lost = lost;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
canvas.apply_mask(PATTERNS[best]);
|
||||
Ok(QrMatrix {
|
||||
version: version as u8,
|
||||
mask: best as u8,
|
||||
size: n,
|
||||
modules: to_bools(canvas, n),
|
||||
})
|
||||
}
|
||||
|
||||
fn to_bools(c: Canvas, n: usize) -> Vec<bool> {
|
||||
let v: Vec<bool> = c
|
||||
.into_colors()
|
||||
.into_iter()
|
||||
.map(|c| c == Color::Dark)
|
||||
.collect();
|
||||
debug_assert_eq!(v.len(), n * n);
|
||||
v
|
||||
}
|
||||
|
||||
/// Python evaluates masks with `makeImpl(test=True)`: format information,
|
||||
/// the dark module and (version >= 7) version information are all light.
|
||||
fn clear_type_info(m: &mut [bool], n: usize, version: i16) {
|
||||
let mut clear = |r: usize, c: usize| m[r * n + c] = false;
|
||||
for i in 0..15 {
|
||||
// vertical
|
||||
let r = if i < 6 {
|
||||
i
|
||||
} else if i < 8 {
|
||||
i + 1
|
||||
} else {
|
||||
n - 15 + i
|
||||
};
|
||||
clear(r, 8);
|
||||
// horizontal
|
||||
let c = if i < 8 {
|
||||
n - i - 1
|
||||
} else if i < 9 {
|
||||
15 - i
|
||||
} else {
|
||||
15 - i - 1
|
||||
};
|
||||
clear(8, c);
|
||||
}
|
||||
clear(n - 8, 8);
|
||||
if version >= 7 {
|
||||
for i in 0..18 {
|
||||
clear(i / 3, i % 3 + n - 11);
|
||||
clear(i % 3 + n - 11, i / 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Port of `qrcode.util.lost_point` (penalty score used for the mask choice).
|
||||
pub fn lost_point(m: &[bool], n: usize) -> u64 {
|
||||
let at = |r: usize, c: usize| m[r * n + c];
|
||||
let mut lost = 0u64;
|
||||
|
||||
// level 1: runs of >= 5 same-coloured modules in rows and columns
|
||||
let runs = |get: &dyn Fn(usize, usize) -> bool| {
|
||||
let mut sum = 0u64;
|
||||
for a in 0..n {
|
||||
let mut prev = get(a, 0);
|
||||
let mut len = 0u64;
|
||||
for b in 0..n {
|
||||
let v = get(a, b);
|
||||
if v == prev {
|
||||
len += 1;
|
||||
} else {
|
||||
if len >= 5 {
|
||||
sum += len - 2;
|
||||
}
|
||||
len = 1;
|
||||
prev = v;
|
||||
}
|
||||
}
|
||||
if len >= 5 {
|
||||
sum += len - 2;
|
||||
}
|
||||
}
|
||||
sum
|
||||
};
|
||||
lost += runs(&|r, c| at(r, c));
|
||||
lost += runs(&|c, r| at(r, c));
|
||||
|
||||
// level 2: 2x2 blocks of one colour
|
||||
for r in 0..n - 1 {
|
||||
for c in 0..n - 1 {
|
||||
let v = at(r, c);
|
||||
if v == at(r, c + 1) && v == at(r + 1, c) && v == at(r + 1, c + 1) {
|
||||
lost += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// level 3: 1:1:3:1:1 finder-like patterns with 4 light modules on one side
|
||||
const P1: [bool; 11] = [
|
||||
true, false, true, true, true, false, true, false, false, false, false,
|
||||
];
|
||||
const P2: [bool; 11] = [
|
||||
false, false, false, false, true, false, true, true, true, false, true,
|
||||
];
|
||||
if n >= 11 {
|
||||
for a in 0..n {
|
||||
for b in 0..=n - 11 {
|
||||
let row_hit = |p: &[bool; 11]| (0..11).all(|k| at(a, b + k) == p[k]);
|
||||
if row_hit(&P1) || row_hit(&P2) {
|
||||
lost += 40;
|
||||
}
|
||||
let col_hit = |p: &[bool; 11]| (0..11).all(|k| at(b + k, a) == p[k]);
|
||||
if col_hit(&P1) || col_hit(&P2) {
|
||||
lost += 40;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// level 4: dark ratio, 10 points per 5 % away from 50 %
|
||||
let dark = m.iter().filter(|&&d| d).count() as f64;
|
||||
let percent = dark / (n * n) as f64;
|
||||
let rating = ((percent * 100.0 - 50.0).abs() / 5.0) as u64;
|
||||
lost + rating * 10
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn modes(data: &str) -> Vec<(Mode, String)> {
|
||||
optimal_segments(data.as_bytes(), 20)
|
||||
.into_iter()
|
||||
.map(|s| (s.mode, String::from_utf8_lossy(s.data).into_owned()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segmentation_like_python() {
|
||||
assert_eq!(modes("L0006"), [(Mode::Alphanumeric, "L0006".into())]);
|
||||
assert_eq!(modes("12345"), [(Mode::Numeric, "12345".into())]);
|
||||
assert_eq!(modes("LAMP_7"), [(Mode::Byte, "LAMP_7".into())]);
|
||||
assert_eq!(
|
||||
modes("123\n"),
|
||||
[(Mode::Numeric, "123".into()), (Mode::Byte, "\n".into())]
|
||||
);
|
||||
let long = format!("L{}", "0123456789".repeat(3));
|
||||
assert_eq!(
|
||||
modes(&long),
|
||||
[
|
||||
(Mode::Byte, "L".into()),
|
||||
(Mode::Numeric, "0123456789".repeat(3))
|
||||
]
|
||||
);
|
||||
let url = "HTTPS://EXAMPLE.COM/LAMP/L0006?X=1";
|
||||
assert_eq!(
|
||||
modes(url),
|
||||
[
|
||||
(Mode::Alphanumeric, "HTTPS://EXAMPLE.COM/LAMP/L0006".into()),
|
||||
(Mode::Byte, "?X=1".into())
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_1_for_lamp_ids() {
|
||||
let q = encode("L0006").unwrap();
|
||||
assert_eq!((q.version, q.size), (1, 21));
|
||||
assert!(encode(&"9".repeat(8000)).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
//! Text rendering for the label's ID strip, emulating Pillow's
|
||||
//! `ImageDraw.text` on a mode `"1"` image (FreeType monochrome rendering with
|
||||
//! raqm layout) as closely as practical with `fontdue`:
|
||||
//!
|
||||
//! * **layout** like Pillow + raqm: unhinted advances and kerning rounded to
|
||||
//! 1/64 px, glyph origins at the pen position rounded to whole pixels
|
||||
//! (Pillow's `PIXEL()`), text bbox for the `"la"` anchor (left / ascender
|
||||
//! line) that always includes the pen origin and the advance end, built from
|
||||
//! pixel-aligned glyph boxes; ascender = `ceil(hhea ascender)` (FreeType);
|
||||
//! * **glyphs**: outlines are point-sampled at pixel centres (8x
|
||||
//! supersampled coverage >= 50 %), i.e. 1-bit without anti-aliasing like
|
||||
//! FreeType's mono renderer. TrueType hinting is approximated vertically
|
||||
//! only: the cap height is snapped to `round(cap + 0.23 px)` (matches
|
||||
//! Arial Bold's hints at every size 10-60 px) and overshoots below 0.555 px
|
||||
//! are flattened. Horizontal stem snapping is not emulated, so stems and
|
||||
//! bowls can differ by a pixel from Pillow's output.
|
||||
//!
|
||||
//! If no font file can be loaded, a built-in 5x7 bitmap font scaled to the
|
||||
//! requested size is used instead.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use brother_ql::Bitmap;
|
||||
|
||||
use crate::Error;
|
||||
use crate::font5x7;
|
||||
|
||||
/// Supersampling factor for outline point sampling.
|
||||
const SS: f64 = 8.0;
|
||||
/// Cap-height snapping bias in px (see `TrueType::warp`).
|
||||
const CAP_BIAS: f64 = 0.23;
|
||||
/// Overshoots smaller than this (px) are suppressed.
|
||||
const OVERSHOOT_LIMIT: f64 = 0.555;
|
||||
|
||||
/// Font files tried in order by [`LabelFont::system`] (after `$LAMP_LABEL_FONT`
|
||||
/// and `%WINDIR%\Fonts`). Mirrors the Python `_font()` preference:
|
||||
/// Arial Bold, Arial, DejaVu Sans Bold (plus Liberation Sans Bold).
|
||||
pub const FONT_CANDIDATES: &[&str] = &[
|
||||
"C:/Windows/Fonts/arialbd.ttf",
|
||||
"C:/Windows/Fonts/arial.ttf",
|
||||
"/usr/share/fonts/truetype/msttcorefonts/Arial_Bold.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/dejavu-sans-fonts/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/liberation/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/liberation-sans/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/TTF/LiberationSans-Bold.ttf",
|
||||
];
|
||||
|
||||
/// The font used for the ID text: a TrueType file or the built-in bitmap font.
|
||||
pub struct LabelFont {
|
||||
kind: Kind,
|
||||
name: String,
|
||||
}
|
||||
|
||||
enum Kind {
|
||||
TrueType(Box<TrueType>),
|
||||
Builtin,
|
||||
}
|
||||
|
||||
struct TrueType {
|
||||
font: fontdue::Font,
|
||||
upem: f64,
|
||||
ascender: f64,
|
||||
/// Cap height (top of 'H') in font units.
|
||||
cap: Option<f64>,
|
||||
/// Overshoot of 'O' above the cap height / below the baseline, font units.
|
||||
overshoot_top: f64,
|
||||
overshoot_bottom: f64,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for LabelFont {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LabelFont")
|
||||
.field("name", &self.name)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn font_paths() -> Vec<PathBuf> {
|
||||
let mut v = Vec::new();
|
||||
if let Some(p) = std::env::var_os("LAMP_LABEL_FONT") {
|
||||
v.push(PathBuf::from(p));
|
||||
}
|
||||
if let Some(w) = std::env::var_os("WINDIR") {
|
||||
let fonts = Path::new(&w).join("Fonts");
|
||||
v.push(fonts.join("arialbd.ttf"));
|
||||
v.push(fonts.join("arial.ttf"));
|
||||
}
|
||||
v.extend(FONT_CANDIDATES.iter().map(PathBuf::from));
|
||||
v
|
||||
}
|
||||
|
||||
impl LabelFont {
|
||||
/// The first loadable font of `$LAMP_LABEL_FONT`, `%WINDIR%\Fonts\arialbd.ttf`,
|
||||
/// `arial.ttf` and [`FONT_CANDIDATES`]; the built-in bitmap font if none loads.
|
||||
pub fn system() -> LabelFont {
|
||||
font_paths()
|
||||
.iter()
|
||||
.find_map(|p| LabelFont::from_file(p).ok())
|
||||
.unwrap_or_else(LabelFont::builtin)
|
||||
}
|
||||
|
||||
/// Loads a TrueType/OpenType font file.
|
||||
pub fn from_file(path: &Path) -> Result<LabelFont, Error> {
|
||||
let bytes = std::fs::read(path)?;
|
||||
let name = path.file_name().map_or_else(
|
||||
|| path.display().to_string(),
|
||||
|n| n.to_string_lossy().into_owned(),
|
||||
);
|
||||
LabelFont::from_bytes(bytes, &name)
|
||||
}
|
||||
|
||||
/// Parses font data; `name` is only used for reporting.
|
||||
pub fn from_bytes(bytes: Vec<u8>, name: &str) -> Result<LabelFont, Error> {
|
||||
let font = fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default())
|
||||
.map_err(|e| Error::Font(format!("{name}: {e}")))?;
|
||||
let upem = f64::from(font.units_per_em());
|
||||
let units = upem as f32;
|
||||
let ascender = font
|
||||
.horizontal_line_metrics(units)
|
||||
.map_or(upem * 0.9, |m| f64::from(m.ascent));
|
||||
let top = |c: char| {
|
||||
font.has_glyph(c).then(|| {
|
||||
let b = font.metrics(c, units).bounds;
|
||||
(f64::from(b.ymin), f64::from(b.ymin + b.height))
|
||||
})
|
||||
};
|
||||
let cap = top('H').map(|(_, t)| t).filter(|&t| t > 0.0);
|
||||
let (overshoot_top, overshoot_bottom) = match (cap, top('O')) {
|
||||
(Some(cap), Some((b, t))) => ((t - cap).max(0.0), (-b).max(0.0)),
|
||||
_ => (0.0, 0.0),
|
||||
};
|
||||
Ok(LabelFont {
|
||||
kind: Kind::TrueType(Box::new(TrueType {
|
||||
font,
|
||||
upem,
|
||||
ascender,
|
||||
cap,
|
||||
overshoot_top,
|
||||
overshoot_bottom,
|
||||
})),
|
||||
name: name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The built-in 5x7 bitmap font (digits, A-Z, `- _ . : /`).
|
||||
pub fn builtin() -> LabelFont {
|
||||
LabelFont {
|
||||
kind: Kind::Builtin,
|
||||
name: "builtin-5x7".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// File name of the font, or `"builtin-5x7"`.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// `true` for the built-in bitmap font.
|
||||
pub fn is_builtin(&self) -> bool {
|
||||
matches!(self.kind, Kind::Builtin)
|
||||
}
|
||||
}
|
||||
|
||||
/// Where and how big the ID text is drawn (values as Pillow reports them).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TextLayout {
|
||||
/// Font name (file name or `builtin-5x7`).
|
||||
pub font: String,
|
||||
/// Chosen font size in pixels per em.
|
||||
pub size: u32,
|
||||
/// `ImageDraw.textbbox((0, 0), text)` at that size: (x0, y0, x1, y1).
|
||||
pub bbox: (i32, i32, i32, i32),
|
||||
/// The `xy` passed to `ImageDraw.text` (anchor "la").
|
||||
pub origin: (i32, i32),
|
||||
}
|
||||
|
||||
/// Vertical "hinting": maps unhinted heights (px above baseline) to snapped
|
||||
/// ones. The cap height is stretched to a whole pixel; overshoots (round
|
||||
/// glyphs poking up to one pixel past the baseline or cap height) are
|
||||
/// flattened when the font's overshoot is below [`OVERSHOOT_LIMIT`] at this size.
|
||||
struct Warp {
|
||||
k: f64,
|
||||
cap_u: f64,
|
||||
cap_s: f64,
|
||||
suppress_top: bool,
|
||||
suppress_bottom: bool,
|
||||
}
|
||||
|
||||
impl Warp {
|
||||
fn identity() -> Warp {
|
||||
Warp {
|
||||
k: 1.0,
|
||||
cap_u: f64::INFINITY,
|
||||
cap_s: f64::INFINITY,
|
||||
suppress_top: false,
|
||||
suppress_bottom: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapped height -> unhinted height (used for sampling pixel centres).
|
||||
fn inv(&self, h: f64) -> f64 {
|
||||
if h < 0.0 {
|
||||
if self.suppress_bottom && h > -1.0 {
|
||||
h - 1.0
|
||||
} else {
|
||||
h
|
||||
}
|
||||
} else if h <= self.cap_s {
|
||||
h / self.k
|
||||
} else if self.suppress_top && h < self.cap_s + 1.0 {
|
||||
h - self.cap_s + self.cap_u + 1.0
|
||||
} else {
|
||||
h - self.cap_s + self.cap_u
|
||||
}
|
||||
}
|
||||
|
||||
/// Unhinted height -> snapped height (used for the glyph boxes).
|
||||
fn fwd(&self, y: f64) -> f64 {
|
||||
if y < 0.0 {
|
||||
if self.suppress_bottom && y > -1.0 {
|
||||
0.0
|
||||
} else {
|
||||
y
|
||||
}
|
||||
} else if y <= self.cap_u {
|
||||
y * self.k
|
||||
} else if self.suppress_top && y < self.cap_u + 1.0 {
|
||||
self.cap_s
|
||||
} else {
|
||||
y - self.cap_u + self.cap_s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 1-bit glyph image in pen-relative pixel coordinates.
|
||||
#[derive(Default, Clone)]
|
||||
struct GlyphInk {
|
||||
/// Columns `x0..x1` relative to the integer pen position.
|
||||
x0: i32,
|
||||
x1: i32,
|
||||
/// Height bands `j0..j1` (band `j` covers `[j, j+1]` px above the baseline).
|
||||
j0: i32,
|
||||
j1: i32,
|
||||
/// Row-major, first row is band `j1 - 1`.
|
||||
ink: Vec<bool>,
|
||||
/// Pixel-aligned outline box (x0, y0, x1, y1), y up from the baseline,
|
||||
/// like `FT_Glyph_Get_CBox(FT_GLYPH_BBOX_PIXELS)`; `None` for empty glyphs.
|
||||
cbox: Option<(i32, i32, i32, i32)>,
|
||||
}
|
||||
|
||||
impl GlyphInk {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.x0 >= self.x1 || self.j0 >= self.j1
|
||||
}
|
||||
}
|
||||
|
||||
/// Pillow `PIXEL()`: 26.6 -> whole pixels, rounding half up.
|
||||
fn pixel(v: i64) -> i32 {
|
||||
(v + 32).div_euclid(64) as i32
|
||||
}
|
||||
|
||||
impl TrueType {
|
||||
fn to26(&self, units: f64, size: f64) -> i64 {
|
||||
(units * size * 64.0 / self.upem).round() as i64
|
||||
}
|
||||
|
||||
fn warp(&self, size: f64) -> Warp {
|
||||
let Some(cap) = self.cap else {
|
||||
return Warp::identity();
|
||||
};
|
||||
let cap_u = cap * size / self.upem;
|
||||
// Arial Bold's hints put the cap height at round(cap + 0.23 px) for
|
||||
// every size from 10 to 60 px (FreeType 2.14 mono); used for all fonts.
|
||||
let cap_s = (cap_u + CAP_BIAS + 0.5).floor();
|
||||
if cap_s <= 0.0 {
|
||||
return Warp::identity();
|
||||
}
|
||||
// overshoots below ~0.55 px are flattened by the hints (45 px: yes, 46 px: no)
|
||||
let suppressed = |units: f64| units * size / self.upem < OVERSHOOT_LIMIT;
|
||||
Warp {
|
||||
k: cap_s / cap_u,
|
||||
cap_u,
|
||||
cap_s,
|
||||
suppress_top: suppressed(self.overshoot_top),
|
||||
suppress_bottom: suppressed(self.overshoot_bottom),
|
||||
}
|
||||
}
|
||||
|
||||
fn glyph_ink(&self, index: u16, size: f64, warp: &Warp) -> GlyphInk {
|
||||
let (m, cov) = self.font.rasterize_indexed(index, (size * SS) as f32);
|
||||
if m.width == 0 || m.height == 0 {
|
||||
return GlyphInk::default();
|
||||
}
|
||||
let (w, h) = (m.width as i64, m.height as i64);
|
||||
let (hx0, hy0) = (i64::from(m.xmin), i64::from(m.ymin));
|
||||
let inside = |x: f64, y: f64| {
|
||||
let col = (x * SS).floor() as i64 - hx0;
|
||||
let row = (hy0 + h - 1) - (y * SS).floor() as i64;
|
||||
(0..w).contains(&col) && (0..h).contains(&row) && cov[(row * w + col) as usize] >= 128
|
||||
};
|
||||
let cx0 = (hx0 as f64 / SS).floor() as i32 - 1;
|
||||
let cx1 = ((hx0 + w) as f64 / SS).ceil() as i32 + 1;
|
||||
let j0 = warp.fwd(hy0 as f64 / SS).floor() as i32 - 2;
|
||||
let j1 = warp.fwd((hy0 + h) as f64 / SS).ceil() as i32 + 2;
|
||||
let mut pts = Vec::new();
|
||||
for j in j0..j1 {
|
||||
let y = warp.inv(f64::from(j) + 0.5);
|
||||
for c in cx0..cx1 {
|
||||
if inside(f64::from(c) + 0.5, y) {
|
||||
pts.push((c, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
if pts.is_empty() {
|
||||
return GlyphInk::default();
|
||||
}
|
||||
let x0 = pts.iter().map(|p| p.0).min().unwrap();
|
||||
let x1 = pts.iter().map(|p| p.0).max().unwrap() + 1;
|
||||
let j0 = pts.iter().map(|p| p.1).min().unwrap();
|
||||
let j1 = pts.iter().map(|p| p.1).max().unwrap() + 1;
|
||||
let width = (x1 - x0) as usize;
|
||||
let mut ink = vec![false; width * (j1 - j0) as usize];
|
||||
for (c, j) in pts {
|
||||
ink[(j1 - 1 - j) as usize * width + (c - x0) as usize] = true;
|
||||
}
|
||||
let b = m.bounds;
|
||||
let (bx0, by0) = (f64::from(b.xmin) / SS, f64::from(b.ymin) / SS);
|
||||
let (bx1, by1) = (
|
||||
bx0 + f64::from(b.width) / SS,
|
||||
by0 + f64::from(b.height) / SS,
|
||||
);
|
||||
const EPS: f64 = 1e-3;
|
||||
let cbox = Some((
|
||||
(bx0 + EPS).floor() as i32,
|
||||
(warp.fwd(by0) + EPS).floor() as i32,
|
||||
(bx1 - EPS).ceil() as i32,
|
||||
(warp.fwd(by1) - EPS).ceil() as i32,
|
||||
));
|
||||
GlyphInk {
|
||||
x0,
|
||||
x1,
|
||||
j0,
|
||||
j1,
|
||||
ink,
|
||||
cbox,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A laid-out line of text at one size.
|
||||
struct Shaped {
|
||||
/// (pen x in px, glyph ink)
|
||||
glyphs: Vec<(i32, GlyphInk)>,
|
||||
bbox: (i32, i32, i32, i32),
|
||||
ascender: i32,
|
||||
}
|
||||
|
||||
fn shape_truetype(tt: &TrueType, text: &str, size: u32) -> Shaped {
|
||||
let s = f64::from(size);
|
||||
let units = tt.upem as f32;
|
||||
let warp = tt.warp(s);
|
||||
let indices: Vec<u16> = text
|
||||
.chars()
|
||||
.map(|c| tt.font.lookup_glyph_index(c))
|
||||
.collect();
|
||||
// raqm/HarfBuzz: unhinted advances, pair kerning added to the first glyph
|
||||
let advances: Vec<i64> = indices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &g)| {
|
||||
let adv = f64::from(tt.font.metrics_indexed(g, units).advance_width);
|
||||
let kern = indices
|
||||
.get(i + 1)
|
||||
.and_then(|&n| tt.font.horizontal_kern_indexed(g, n, units))
|
||||
.map_or(0, |k| tt.to26(f64::from(k), s));
|
||||
tt.to26(adv, s) + kern
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut cache: HashMap<u16, GlyphInk> = HashMap::new();
|
||||
let (mut x_min, mut x_max, mut y_min, mut y_max) = (0i32, 0i32, 0i32, 0i32);
|
||||
let mut position = 0i64;
|
||||
let mut glyphs = Vec::with_capacity(indices.len());
|
||||
for (&g, &adv) in indices.iter().zip(&advances) {
|
||||
let px = pixel(position);
|
||||
position += adv;
|
||||
x_max = x_max.max(pixel(position));
|
||||
let ink = cache
|
||||
.entry(g)
|
||||
.or_insert_with(|| tt.glyph_ink(g, s, &warp))
|
||||
.clone();
|
||||
if let Some((bx0, by0, bx1, by1)) = ink.cbox {
|
||||
x_min = x_min.min(px + bx0);
|
||||
x_max = x_max.max(px + bx1);
|
||||
y_min = y_min.min(by0);
|
||||
y_max = y_max.max(by1);
|
||||
}
|
||||
glyphs.push((px, ink));
|
||||
}
|
||||
let ascender = (tt.ascender * s / tt.upem - 1e-9).ceil() as i32; // FreeType FT_PIX_CEIL
|
||||
Shaped {
|
||||
glyphs,
|
||||
bbox: (x_min, ascender - y_max, x_max, ascender - y_min),
|
||||
ascender,
|
||||
}
|
||||
}
|
||||
|
||||
/// Integer scale of the 5x7 font for a nominal size (cap height ~0.716 em, like Arial).
|
||||
fn builtin_scale(size: u32) -> u32 {
|
||||
let cap = (f64::from(size) * 0.716).round() as u32;
|
||||
(cap / font5x7::ROWS).max(1)
|
||||
}
|
||||
|
||||
fn builtin_bbox(text: &str, size: u32) -> (i32, i32, i32, i32) {
|
||||
let n = text.chars().count() as u32;
|
||||
let scale = builtin_scale(size);
|
||||
let w = if n == 0 {
|
||||
0
|
||||
} else {
|
||||
(n * font5x7::ADVANCE - (font5x7::ADVANCE - font5x7::COLS)) * scale
|
||||
};
|
||||
(0, 0, w as i32, (font5x7::ROWS * scale) as i32)
|
||||
}
|
||||
|
||||
/// Runs the Python size loop: 44, 42, ... down to 12, first size whose text
|
||||
/// bbox width is <= `max_width` (the last tried size if none fits).
|
||||
fn choose(
|
||||
font: &LabelFont,
|
||||
text: &str,
|
||||
max_width: i32,
|
||||
) -> (u32, Option<Shaped>, (i32, i32, i32, i32)) {
|
||||
let mut size = 44;
|
||||
let mut last = None;
|
||||
while size > 10 {
|
||||
let (shaped, bbox) = match &font.kind {
|
||||
Kind::TrueType(tt) => {
|
||||
let s = shape_truetype(tt, text, size);
|
||||
let b = s.bbox;
|
||||
(Some(s), b)
|
||||
}
|
||||
Kind::Builtin => (None, builtin_bbox(text, size)),
|
||||
};
|
||||
let fits = bbox.2 - bbox.0 <= max_width;
|
||||
last = Some((size, shaped, bbox));
|
||||
if fits {
|
||||
break;
|
||||
}
|
||||
size -= 2;
|
||||
}
|
||||
last.expect("loop runs at least once")
|
||||
}
|
||||
|
||||
/// Draws `text` centred in the strip `[strip_top, strip_top + strip_height)`
|
||||
/// of `image` exactly following the Python placement rules, and returns the
|
||||
/// layout.
|
||||
pub(crate) fn draw_text(
|
||||
image: &mut Bitmap,
|
||||
font: &LabelFont,
|
||||
text: &str,
|
||||
strip_top: i32,
|
||||
strip_height: i32,
|
||||
) -> TextLayout {
|
||||
let width = image.width() as i32;
|
||||
let (size, shaped, bbox) = choose(font, text, width - 8);
|
||||
let (bw, bh) = (bbox.2 - bbox.0, bbox.3 - bbox.1);
|
||||
let origin = (
|
||||
(width - bw).div_euclid(2) - bbox.0,
|
||||
strip_top + (strip_height - bh).div_euclid(2) - bbox.1,
|
||||
);
|
||||
match (&font.kind, shaped) {
|
||||
(Kind::TrueType(_), Some(shaped)) => {
|
||||
let baseline = origin.1 + shaped.ascender;
|
||||
for (px, ink) in &shaped.glyphs {
|
||||
if ink.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let w = (ink.x1 - ink.x0) as usize;
|
||||
for (r, row) in ink.ink.chunks(w).enumerate() {
|
||||
let j = ink.j1 - 1 - r as i32;
|
||||
let y = baseline - 1 - j;
|
||||
for (c, &on) in row.iter().enumerate() {
|
||||
if on {
|
||||
let x = origin.0 + px + ink.x0 + c as i32;
|
||||
image.set_clipped(i64::from(x), i64::from(y), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let scale = builtin_scale(size) as i32;
|
||||
for (i, ch) in text.chars().enumerate() {
|
||||
let rows = font5x7::glyph(ch);
|
||||
let gx = origin.0 + i as i32 * font5x7::ADVANCE as i32 * scale;
|
||||
for (r, bits) in rows.iter().enumerate() {
|
||||
for c in 0..font5x7::COLS as i32 {
|
||||
if bits & (0x10 >> c) != 0 {
|
||||
image.fill_rect(
|
||||
i64::from(gx + c * scale),
|
||||
i64::from(origin.1 + r as i32 * scale),
|
||||
scale as u32,
|
||||
scale as u32,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TextLayout {
|
||||
font: font.name().to_string(),
|
||||
size,
|
||||
bbox,
|
||||
origin,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
//! 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<String, String> {
|
||||
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<Vec<bool>>,
|
||||
}
|
||||
|
||||
fn golden_qr() -> Vec<GoldenQr> {
|
||||
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<TextGolden> {
|
||||
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::<i32>().unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 747 B |
|
After Width: | Height: | Size: 516 B |
|
After Width: | Height: | Size: 569 B |
|
After Width: | Height: | Size: 575 B |
|
After Width: | Height: | Size: 549 B |
|
After Width: | Height: | Size: 584 B |
|
After Width: | Height: | Size: 574 B |
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 592 B |
|
After Width: | Height: | Size: 584 B |
|
After Width: | Height: | Size: 535 B |
|
After Width: | Height: | Size: 529 B |
|
After Width: | Height: | Size: 569 B |
|
After Width: | Height: | Size: 591 B |
|
After Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 567 B |
|
After Width: | Height: | Size: 598 B |
|
After Width: | Height: | Size: 553 B |
|
After Width: | Height: | Size: 573 B |
|
After Width: | Height: | Size: 574 B |
|
After Width: | Height: | Size: 573 B |
|
After Width: | Height: | Size: 614 B |
|
After Width: | Height: | Size: 661 B |
|
After Width: | Height: | Size: 807 B |
|
After Width: | Height: | Size: 592 B |
|
After Width: | Height: | Size: 547 B |
|
After Width: | Height: | Size: 651 B |
|
After Width: | Height: | Size: 705 B |
|
After Width: | Height: | Size: 506 B |
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 604 B |
|
After Width: | Height: | Size: 532 B |
|
After Width: | Height: | Size: 680 B |
|
After Width: | Height: | Size: 617 B |
@@ -0,0 +1,35 @@
|
||||
L0001 label_L0001.png
|
||||
L0002 label_L0002.png
|
||||
L0003 label_L0003.png
|
||||
L0004 label_L0004.png
|
||||
L0005 label_L0005.png
|
||||
L0006 label_L0006.png
|
||||
L0007 label_L0007.png
|
||||
L0008 label_L0008.png
|
||||
L0009 label_L0009.png
|
||||
L0010 label_L0010.png
|
||||
L0011 label_L0011.png
|
||||
L0012 label_L0012.png
|
||||
L0013 label_L0013.png
|
||||
L0014 label_L0014.png
|
||||
L0015 label_L0015.png
|
||||
L0016 label_L0016.png
|
||||
L0017 label_L0017.png
|
||||
L0018 label_L0018.png
|
||||
L0019 label_L0019.png
|
||||
L0020 label_L0020.png
|
||||
L0999 label_L0999.png
|
||||
L1000 label_L1000.png
|
||||
L12345 label_L12345.png
|
||||
L123456 label_L123456.png
|
||||
12345678 label_12345678.png
|
||||
LAMP-7 X label_LAMP-7_X.png
|
||||
lamp_7 label_lamp_7.png
|
||||
https://example.com/lamp/L0006?x=1 label_https___example_com_lamp_L0006_x_1.png
|
||||
L0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 label_L012345678901234567890123456789012345678.png
|
||||
LABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ label_LABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHI.png
|
||||
L0123 label_L0123.png
|
||||
L9 label_L9.png
|
||||
L0123456789012 label_L0123456789012.png
|
||||
LAVA-7 label_LAVA-7.png
|
||||
WWWWWWWWWW label_WWWWWWWWWW.png
|
||||
@@ -0,0 +1,704 @@
|
||||
L0001 version=1 mask=7 size=21
|
||||
111111100000101111111
|
||||
100000100010101000001
|
||||
101110100010001011101
|
||||
101110100101101011101
|
||||
101110100101101011101
|
||||
100000101000001000001
|
||||
111111101010101111111
|
||||
000000000100000000000
|
||||
100101101011110100000
|
||||
011101001011010101000
|
||||
111111111101001110111
|
||||
110001011000111110000
|
||||
011101101011011100100
|
||||
000000001010011011011
|
||||
111111100001101011101
|
||||
100000101010001001011
|
||||
101110100000111000111
|
||||
101110101110011100111
|
||||
101110100011011100101
|
||||
100000100001100101001
|
||||
111111101110010101100
|
||||
L0002 version=1 mask=7 size=21
|
||||
111111100100001111111
|
||||
100000100010001000001
|
||||
101110100101101011101
|
||||
101110100110101011101
|
||||
101110100111101011101
|
||||
100000101010001000001
|
||||
111111101010101111111
|
||||
000000000000000000000
|
||||
100101101001110100000
|
||||
001100010101010101000
|
||||
110111100111001110111
|
||||
000010011010111110000
|
||||
111101110011011100100
|
||||
000000001000011011011
|
||||
111111100001101011101
|
||||
100000101100001001011
|
||||
101110100110111001111
|
||||
101110101100011100011
|
||||
101110100101011100101
|
||||
100000100111100101001
|
||||
111111101100010101100
|
||||
L0003 version=1 mask=0 size=21
|
||||
111111100110101111111
|
||||
100000101111001000001
|
||||
101110100111001011101
|
||||
101110100111001011101
|
||||
101110101111101011101
|
||||
100000100101001000001
|
||||
111111101010101111111
|
||||
000000000000000000000
|
||||
101010100010100010010
|
||||
001111010011010101000
|
||||
010101101011011100101
|
||||
110110011011110111001
|
||||
000101100011011100100
|
||||
000000001110001001001
|
||||
111111100010100010100
|
||||
100000100000001001011
|
||||
101110101010101011101
|
||||
101110100101010101110
|
||||
101110101101011100101
|
||||
100000100111110111011
|
||||
111111101101011100101
|
||||
L0004 version=1 mask=6 size=21
|
||||
111111101000001111111
|
||||
100000101101001000001
|
||||
101110101100101011101
|
||||
101110100111101011101
|
||||
101110101010101011101
|
||||
100000100111101000001
|
||||
111111101010101111111
|
||||
000000000011100000000
|
||||
100111111100110010111
|
||||
000000001010101010111
|
||||
101110111000011011101
|
||||
100110011111000001111
|
||||
011100111110001001110
|
||||
000000001111100100100
|
||||
111111101110111110111
|
||||
100000101111110110100
|
||||
101110101111101101001
|
||||
101110101101100011100
|
||||
101110100000001001111
|
||||
100000100100011010110
|
||||
111111101111000000110
|
||||
L0005 version=1 mask=7 size=21
|
||||
111111100111101111111
|
||||
100000100101101000001
|
||||
101110100010001011101
|
||||
101110100001101011101
|
||||
101110100101101011101
|
||||
100000101110001000001
|
||||
111111101010101111111
|
||||
000000000110000000000
|
||||
100101101011110100000
|
||||
111100010011010101000
|
||||
001011110011001110111
|
||||
100100001000111110000
|
||||
110001111011011100100
|
||||
000000001100011011011
|
||||
111111100001101011101
|
||||
100000101100001001011
|
||||
101110100100111000011
|
||||
101110101010011100111
|
||||
101110100101011100101
|
||||
100000100001100101001
|
||||
111111101010010101100
|
||||
L0006 version=1 mask=5 size=21
|
||||
111111100110001111111
|
||||
100000101010101000001
|
||||
101110101010101011101
|
||||
101110101101001011101
|
||||
101110100011101011101
|
||||
100000100000101000001
|
||||
111111101010101111111
|
||||
000000001101100000000
|
||||
100000101110111001110
|
||||
010010010010101010111
|
||||
011111110101010010100
|
||||
101011010110000010111
|
||||
000100110110001001110
|
||||
000000001001111100111
|
||||
111111100110101100101
|
||||
100000100101110110100
|
||||
101110100110100101000
|
||||
101110100100100000100
|
||||
101110100110001001111
|
||||
100000100000000010101
|
||||
111111101111010010100
|
||||
L0007 version=1 mask=1 size=21
|
||||
111111101100101111111
|
||||
100000100101001000001
|
||||
101110101010001011101
|
||||
101110100110001011101
|
||||
101110100010101011101
|
||||
100000101110001000001
|
||||
111111101010101111111
|
||||
000000000111000000000
|
||||
101000110111100100101
|
||||
111011011110000000010
|
||||
110100110000001001111
|
||||
110110011110100010011
|
||||
111100100110001001110
|
||||
000000001101011100011
|
||||
111111101111110111110
|
||||
100000100011011100001
|
||||
101110100011111110011
|
||||
101110100100000000100
|
||||
101110101110001001111
|
||||
100000100010100010001
|
||||
111111101100001001111
|
||||
L0008 version=1 mask=6 size=21
|
||||
111111101001101111111
|
||||
100000101100101000001
|
||||
101110101010101011101
|
||||
101110100101101011101
|
||||
101110101110101011101
|
||||
100000100111101000001
|
||||
111111101010101111111
|
||||
000000000001100000000
|
||||
100111111100110010111
|
||||
011001011110101010111
|
||||
100110100100011011101
|
||||
110000010111000001111
|
||||
101110100110001001110
|
||||
000000001011100100100
|
||||
111111101110111110111
|
||||
100000101101110111100
|
||||
101110101101101101101
|
||||
101110101111100011100
|
||||
101110100100001001111
|
||||
100000100100011010110
|
||||
111111101001000000110
|
||||
L0009 version=1 mask=6 size=21
|
||||
111111101011001111111
|
||||
100000101011101000001
|
||||
101110101001001011101
|
||||
101110100100001011101
|
||||
101110101100101011101
|
||||
100000100001101000001
|
||||
111111101010101111111
|
||||
000000000011100000000
|
||||
100111111110110010111
|
||||
011010011000101010111
|
||||
010110111010011011101
|
||||
001101011111000001111
|
||||
010110110110001001110
|
||||
000000001111100100100
|
||||
111111101100111110111
|
||||
100000101001110111100
|
||||
101110101011101101101
|
||||
101110101111100011000
|
||||
101110100100001001111
|
||||
100000100110011010110
|
||||
111111101001000000110
|
||||
L0010 version=1 mask=7 size=21
|
||||
111111100000001111111
|
||||
100000100111101000001
|
||||
101110100010101011101
|
||||
101110100011001011101
|
||||
101110100101101011101
|
||||
100000101100001000001
|
||||
111111101010101111111
|
||||
000000000010000000000
|
||||
100101101111110100000
|
||||
101000011101010101000
|
||||
000110101001001110111
|
||||
001001000100111110000
|
||||
010101100101011100100
|
||||
000000001010011011011
|
||||
111111100011101011001
|
||||
100000101000001001011
|
||||
101110100110111000111
|
||||
101110101010011100011
|
||||
101110100011011100101
|
||||
100000100011100101001
|
||||
111111101000010101100
|
||||
L0011 version=1 mask=2 size=21
|
||||
111111100100101111111
|
||||
100000100011001000001
|
||||
101110101110001011101
|
||||
101110101011001011101
|
||||
101110101011101011101
|
||||
100000101010101000001
|
||||
111111101010101111111
|
||||
000000001011100000000
|
||||
101111100010101111100
|
||||
011010001010100100110
|
||||
101010101011010010100
|
||||
001100010100000110111
|
||||
100011111011010010101
|
||||
000000001101111000111
|
||||
111111100110101100001
|
||||
100000101101111000101
|
||||
101110101100100100100
|
||||
101110101010100100000
|
||||
101110101101010010100
|
||||
100000100010000110101
|
||||
111111101111010010100
|
||||
L0012 version=1 mask=0 size=21
|
||||
111111100110001111111
|
||||
100000101010001000001
|
||||
101110100111101011101
|
||||
101110100001101011101
|
||||
101110101111101011101
|
||||
100000100001001000001
|
||||
111111101010101111111
|
||||
000000000110000000000
|
||||
101010100110100010010
|
||||
111010000101010101000
|
||||
101100111111011100101
|
||||
001110000111110111001
|
||||
001101101101011100100
|
||||
000000001110001001001
|
||||
111111100000100010000
|
||||
100000100010001001011
|
||||
101110101100101011101
|
||||
101110100001010101010
|
||||
101110101101011100101
|
||||
100000100101110111011
|
||||
111111101011011100101
|
||||
L0013 version=1 mask=3 size=21
|
||||
111111101010101111111
|
||||
100000101001001000001
|
||||
101110100100101011101
|
||||
101110101001101011101
|
||||
101110100110001011101
|
||||
100000100000001000001
|
||||
111111101010101111111
|
||||
000000001000000000000
|
||||
101101110100001001011
|
||||
001000010010100100110
|
||||
111111100010111111001
|
||||
110100001000110000001
|
||||
111011110011010010101
|
||||
000000001110010101010
|
||||
111111101010011010111
|
||||
100000101111111000101
|
||||
101110100001001000001
|
||||
101110101110010010110
|
||||
101110101011010010100
|
||||
100000100011101011000
|
||||
111111101011100100010
|
||||
L0014 version=1 mask=7 size=21
|
||||
111111100111001111111
|
||||
100000100000101000001
|
||||
101110100010101011101
|
||||
101110100111001011101
|
||||
101110100101101011101
|
||||
100000101010001000001
|
||||
111111101010101111111
|
||||
000000000000000000000
|
||||
100101101111110100000
|
||||
001001000101010101000
|
||||
110010100111001110111
|
||||
011100010100111110000
|
||||
111001110101011100100
|
||||
000000001100011011011
|
||||
111111100011101011001
|
||||
100000101110001001011
|
||||
101110100010111000011
|
||||
101110101110011100011
|
||||
101110100101011100101
|
||||
100000100011100101001
|
||||
111111101100010101100
|
||||
L0015 version=1 mask=4 size=21
|
||||
111111101111101111111
|
||||
100000100000001000001
|
||||
101110100101101011101
|
||||
101110101100101011101
|
||||
101110101111101011101
|
||||
100000101000101000001
|
||||
111111101010101111111
|
||||
000000001010000000000
|
||||
100010111110111111001
|
||||
100111001110111000101
|
||||
111101100110110001000
|
||||
111010000111100101011
|
||||
010011110111001110110
|
||||
000000001111100100100
|
||||
111111101101001111101
|
||||
100000100000011011001
|
||||
101110101100111000011
|
||||
101110100010111000011
|
||||
101110100000110001000
|
||||
100000100001100101001
|
||||
111111101111001110111
|
||||
L0016 version=1 mask=6 size=21
|
||||
111111101100001111111
|
||||
100000101000101000001
|
||||
101110101011101011101
|
||||
101110100010001011101
|
||||
101110101000101011101
|
||||
100000100001101000001
|
||||
111111101010101111111
|
||||
000000000001100000000
|
||||
100111111010110010111
|
||||
100100000010101010111
|
||||
011111110110011011101
|
||||
101101000001000001111
|
||||
110100101000001001110
|
||||
000000001101100100100
|
||||
111111101100111110011
|
||||
100000101011110110100
|
||||
101110101111101100001
|
||||
101110101011100011100
|
||||
101110100110001001111
|
||||
100000100000011010110
|
||||
111111101011000000110
|
||||
L0017 version=1 mask=3 size=21
|
||||
111111101101101111111
|
||||
100000101110001000001
|
||||
101110100100101011101
|
||||
101110101101101011101
|
||||
101110100110001011101
|
||||
100000100110001000001
|
||||
111111101010101111111
|
||||
000000001010000000000
|
||||
101101110100001001011
|
||||
101001001010100100110
|
||||
001011101100111111001
|
||||
100001011000110000001
|
||||
010111100011010010101
|
||||
000000001000010101010
|
||||
111111101010011010111
|
||||
100000101001111000101
|
||||
101110100101001000101
|
||||
101110101010010010110
|
||||
101110101101010010100
|
||||
100000100011101011000
|
||||
111111101111100100010
|
||||
L0018 version=1 mask=6 size=21
|
||||
111111101011101111111
|
||||
100000101110101000001
|
||||
101110101001101011101
|
||||
101110100010101011101
|
||||
101110101100101011101
|
||||
100000100101101000001
|
||||
111111101010101111111
|
||||
000000000101100000000
|
||||
100111111010110010111
|
||||
101111001110101010111
|
||||
101111101110011011101
|
||||
110101000011000001111
|
||||
011110111000001001110
|
||||
000000001111100100100
|
||||
111111101110111110011
|
||||
100000101011110111100
|
||||
101110101101101101101
|
||||
101110101011100011100
|
||||
101110100100001001111
|
||||
100000100100011010110
|
||||
111111101111000000110
|
||||
L0019 version=1 mask=6 size=21
|
||||
111111101001001111111
|
||||
100000101001101000001
|
||||
101110101010001011101
|
||||
101110100011001011101
|
||||
101110101110101011101
|
||||
100000100011101000001
|
||||
111111101010101111111
|
||||
000000000111100000000
|
||||
100111111000110010111
|
||||
101100001000101010111
|
||||
011111110000011011101
|
||||
001000001011000001111
|
||||
100110101000001001110
|
||||
000000001011100100100
|
||||
111111101100111110011
|
||||
100000101111110111100
|
||||
101110101011101101101
|
||||
101110101011100011000
|
||||
101110100100001001111
|
||||
100000100110011010110
|
||||
111111101111000000110
|
||||
L0020 version=1 mask=0 size=21
|
||||
111111100110001111111
|
||||
100000101111101000001
|
||||
101110100010001011101
|
||||
101110100010101011101
|
||||
101110101001101011101
|
||||
100000100011001000001
|
||||
111111101010101111111
|
||||
000000000100000000000
|
||||
101010100110100010010
|
||||
001101010001010101000
|
||||
110011101001011100101
|
||||
010101010111110111001
|
||||
011101110001011100100
|
||||
000000001110001000001
|
||||
111111100110100010100
|
||||
100000100100001001011
|
||||
101110101100101010101
|
||||
101110100101010101010
|
||||
101110101011011100101
|
||||
100000100101110111011
|
||||
111111101101011100101
|
||||
L0999 version=1 mask=0 size=21
|
||||
111111100001001111111
|
||||
100000101011101000001
|
||||
101110100110001011101
|
||||
101110100100101011101
|
||||
101110101101101011101
|
||||
100000100111001000001
|
||||
111111101010101111111
|
||||
000000000100000000000
|
||||
101010100100100010010
|
||||
010111011011010101000
|
||||
000001101011011101001
|
||||
011111010101110111001
|
||||
011000110001011101000
|
||||
000000001100001000101
|
||||
111111100100100010100
|
||||
100000100010001000011
|
||||
101110101100101010101
|
||||
101110100011010101110
|
||||
101110101011011100101
|
||||
100000100101110111011
|
||||
111111101111011100101
|
||||
L1000 version=1 mask=4 size=21
|
||||
111111101110101111111
|
||||
100000100100001000001
|
||||
101110100111101011101
|
||||
101110101011001011101
|
||||
101110101101101011101
|
||||
100000101010101000001
|
||||
111111101010101111111
|
||||
000000001000000000000
|
||||
100010111000111111001
|
||||
111101001000111000110
|
||||
110011110100110001000
|
||||
010011010001100101011
|
||||
010100101101001110110
|
||||
000000001011100100100
|
||||
111111101011001111001
|
||||
100000100110011011001
|
||||
101110101100111000111
|
||||
101110100100111000111
|
||||
101110100100110001000
|
||||
100000100101100101001
|
||||
111111101111001110111
|
||||
L12345 version=1 mask=2 size=21
|
||||
111111100111001111111
|
||||
100000100000101000001
|
||||
101110101010001011101
|
||||
101110101100101011101
|
||||
101110101100101011101
|
||||
100000101001001000001
|
||||
111111101010101111111
|
||||
000000001010000000000
|
||||
101111100001001111100
|
||||
111011010111111000101
|
||||
110101100110101100100
|
||||
011011001011111000011
|
||||
100110100110100101001
|
||||
000000001110100100011
|
||||
111111100011010010000
|
||||
100000101110000110111
|
||||
101110101101010010000
|
||||
101110101011111101000
|
||||
101110101110101100000
|
||||
100000100001111101101
|
||||
111111101010100100100
|
||||
L123456 version=1 mask=1 size=21
|
||||
111111101100101111111
|
||||
100000100111101000001
|
||||
101110101011001011101
|
||||
101110100111101011101
|
||||
101110100101101011101
|
||||
100000101111101000001
|
||||
111111101010101111111
|
||||
000000000100100000000
|
||||
101000110100000100101
|
||||
111100001001011100001
|
||||
110001111101110111111
|
||||
111011010111011100111
|
||||
111111101001111110010
|
||||
000000001100000000111
|
||||
111111101000001001010
|
||||
100000100010100010011
|
||||
101110100110001001011
|
||||
101110100101011001100
|
||||
101110101111110111011
|
||||
100000100111011011001
|
||||
111111101101111011111
|
||||
12345678 version=1 mask=6 size=21
|
||||
111111101011001111111
|
||||
100000101000101000001
|
||||
101110101101101011101
|
||||
101110100010001011101
|
||||
101110101100101011101
|
||||
100000100111101000001
|
||||
111111101010101111111
|
||||
000000000101100000000
|
||||
100111111110110010111
|
||||
101010010110101011110
|
||||
101110100100011010000
|
||||
111101011011000001111
|
||||
111001111010001001001
|
||||
000000001011100100111
|
||||
111111101100111110010
|
||||
100000101011110111100
|
||||
101110101001101100001
|
||||
101110101111100011100
|
||||
101110100000001001111
|
||||
100000100010011010101
|
||||
111111101001000000110
|
||||
LAMP-7 X version=1 mask=2 size=21
|
||||
111111100110101111111
|
||||
100000100011001000001
|
||||
101110101100001011101
|
||||
101110101010101011101
|
||||
101110101101101011101
|
||||
100000101100101000001
|
||||
111111101010101111111
|
||||
000000001111100000000
|
||||
101111100100101111100
|
||||
110001000100100101111
|
||||
011110110101010011001
|
||||
010110011010000111011
|
||||
100111101101010010001
|
||||
000000001011111101011
|
||||
111111100010101101000
|
||||
100000101101111101000
|
||||
101110101000100110110
|
||||
101110101010100111000
|
||||
101110101111010111000
|
||||
100000100100000010101
|
||||
111111101101010101000
|
||||
lamp_7 version=1 mask=7 size=21
|
||||
111111100101101111111
|
||||
100000100110101000001
|
||||
101110100110001011101
|
||||
101110100000101011101
|
||||
101110100111101011101
|
||||
100000101110001000001
|
||||
111111101010101111111
|
||||
000000000110000000000
|
||||
100101101001110100000
|
||||
011100011111010101010
|
||||
111000111111001111101
|
||||
101110001110111111011
|
||||
001010111101011100110
|
||||
000000001000011110100
|
||||
111111100001101010110
|
||||
100000101010001010011
|
||||
101110100010111101101
|
||||
101110101010011010011
|
||||
101110100101011010101
|
||||
100000100111100000000
|
||||
111111101000010100110
|
||||
https://example.com/lamp/L0006?x=1 version=2 mask=0 size=25
|
||||
1111111000000011101111111
|
||||
1000001011000110101000001
|
||||
1011101001000011001011101
|
||||
1011101000110100101011101
|
||||
1011101011001011101011101
|
||||
1000001000011100101000001
|
||||
1111111010101010101111111
|
||||
0000000001001100100000000
|
||||
1010101000010011000010010
|
||||
1001010011010101111010011
|
||||
1101111111011011100111111
|
||||
1100010011101100100100011
|
||||
1000011100011011111110100
|
||||
0000110100011010100100011
|
||||
1000011011001011101100000
|
||||
0101100011011110010111000
|
||||
1000101010111010111111001
|
||||
0000000010111100100010101
|
||||
1111111001100010101011101
|
||||
1000001001101111100010101
|
||||
1011101010111010111111110
|
||||
1011101000110001010011010
|
||||
1011101010101100000010001
|
||||
1000001000101011010011111
|
||||
1111111011100001110111101
|
||||
L0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 version=4 mask=1 size=33
|
||||
111111101100110101101111001111111
|
||||
100000100111000000110001101000001
|
||||
101110101101110101100101001011101
|
||||
101110100101000100011011001011101
|
||||
101110100100010100110010001011101
|
||||
100000101110011100000001001000001
|
||||
111111101010101010101010101111111
|
||||
000000000001100111001101100000000
|
||||
101000110011000011001101100100101
|
||||
011111001110010001101101110000001
|
||||
001010100000100111010000010101000
|
||||
011010010011010010000100000110000
|
||||
000001111010010101011011100111101
|
||||
100010000101100001100001000110110
|
||||
001100101110100100001100101011011
|
||||
011101011111110000101011100110100
|
||||
011110101100110100001101100101100
|
||||
010001011100100010111101001100110
|
||||
101111101110110110001110001101111
|
||||
101010011101000010011101110010001
|
||||
011001101110100010010101101101100
|
||||
010001011110101011001011100111100
|
||||
110100101100010010101111010010101
|
||||
001100000000100110011100101001110
|
||||
111011101001100011101101111110011
|
||||
000000001101111001110101100011101
|
||||
111111101010011010101000101011101
|
||||
100000100000001010011011100011001
|
||||
101110100111110010010000111110110
|
||||
101110100110011001000011001011000
|
||||
101110101101111100011100011010011
|
||||
100000100110010001010010000010100
|
||||
111111101000000001001000011011001
|
||||
LABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ version=8 mask=4 size=49
|
||||
1111111011011110001001011000110110001100101111111
|
||||
1000001001010010100101010111111001110011101000001
|
||||
1011101000110011010011010010110000001001101011101
|
||||
1011101011111100100001101110010010000001001011101
|
||||
1011101010010110001000111111100011100000001011101
|
||||
1000001011110101101101100010011001001110001000001
|
||||
1111111010101010101010101010101010101010101111111
|
||||
0000000011010011001111100011000100010000000000000
|
||||
1000101110100001010010111110011001010001011111001
|
||||
1101100001001010010100100011000010100001101010010
|
||||
0010101010001010110011001010000101111101011111111
|
||||
1010110111011011010100011010000110010010011101011
|
||||
1110111011110001000010010001001100111010110001000
|
||||
1010110111001100000110000100010011101011001110110
|
||||
0100101011001111011100111011001110111100010110011
|
||||
0111000011100011110001100010111010011010001111101
|
||||
1111001011011000111001010000001101011110011000000
|
||||
1110000010000110101000100111011100100011100110011
|
||||
0110101011100100010110101001101110000001011100011
|
||||
0111110010100101100001000001101011010010111100000
|
||||
0101111001010011110101111011110000110000101101111
|
||||
0100000001111110110011111110100011000101111010010
|
||||
0011111110001110101010111110101000100010111110011
|
||||
1101100010101011100100100011011100111010100010100
|
||||
0111101010110111111001101011110101110001101010110
|
||||
0111100010000001000011100011101100100110100011110
|
||||
1010111111100101110011111110110100011000111110011
|
||||
1101110111000000010000011100101111100111101101011
|
||||
1011101110100100011110111111111110110010000101101
|
||||
0100110011111101000001101010000110100011110001110
|
||||
0110111101110111001011100000011110001101011110011
|
||||
0011010010110010110110110111111011100110111100110
|
||||
1110001001100110000011101101100100011000110100111
|
||||
0001100101110001001111011000111110110000010101011
|
||||
1010001100001100110011001000011100100101100011001
|
||||
1011010001110000100101111111000010101010011111000
|
||||
1111101100101010001010011101001011110000111111011
|
||||
1100110111100111001011001011000001001110111100010
|
||||
0100011101110000001110011110111000110111100100110
|
||||
0111000010010001011100000111001110110010101110010
|
||||
1110001111101110011110111110010000111010111110110
|
||||
0000000010010011001001100010011000010000100011011
|
||||
1111111010101010111011101010011000111111101011101
|
||||
1000001001110010010101100011000011000010100011011
|
||||
1011101010100000100011111110111010101011111110110
|
||||
1011101001001001110111000000111100100101100011100
|
||||
1011101000011110101100100010001100000111101111100
|
||||
1000001000011101110000001111110110000010000101111
|
||||
1111111011101010011000110110001101111100100010101
|
||||
@@ -0,0 +1,10 @@
|
||||
L0001 font=arialbd.ttf size=44 bbox=0,8,125,40 origin=38,152
|
||||
L0006 font=arialbd.ttf size=44 bbox=0,8,125,40 origin=38,152
|
||||
L0123 font=arialbd.ttf size=44 bbox=0,8,125,40 origin=38,152
|
||||
L12345 font=arialbd.ttf size=44 bbox=0,8,149,40 origin=26,152
|
||||
L9 font=arialbd.ttf size=44 bbox=0,8,51,40 origin=75,152
|
||||
L123456 font=arialbd.ttf size=44 bbox=0,8,174,40 origin=14,152
|
||||
L0123456789012 font=arialbd.ttf size=24 bbox=0,5,188,22 origin=7,162
|
||||
LAVA-7 font=arialbd.ttf size=44 bbox=0,8,152,40 origin=25,152
|
||||
WWWWWWWWWW font=arialbd.ttf size=20 bbox=0,4,189,19 origin=6,164
|
||||
lamp_7 font=arialbd.ttf size=44 bbox=0,8,152,49 origin=25,147
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Generate golden data for the Rust lamp-label crate.
|
||||
|
||||
Writes into ``crates/lamp-label/tests/golden/``:
|
||||
|
||||
* ``qr_matrices.txt``: for each ID, the QR code Python ``qrcode`` 8.2 builds
|
||||
for ``render_label`` (ERROR_CORRECT_M, fit=True, automatic mask): a header
|
||||
line ``<id> version=<v> mask=<m> size=<n>`` followed by ``n`` rows of
|
||||
``0``/``1`` (1 = dark), quiet zone not included.
|
||||
* ``label_<id>.png``: the complete 202x202 label from Python ``render_label``.
|
||||
* ``text_info.txt``: per ID the font, chosen size, PIL text bbox and the draw
|
||||
origin used by ``render_label``.
|
||||
|
||||
Run with: C:/dev/HPCS/.venv/Scripts/python.exe -W ignore crates/lamp-label/tools/golden.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import qrcode
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
OUT = Path(__file__).resolve().parent.parent / "tests" / "golden"
|
||||
DOTS = (202, 202)
|
||||
TEXT_HEIGHT = 52
|
||||
|
||||
QR_IDS = [f"L{i:04d}" for i in range(1, 21)] + [
|
||||
"L0999",
|
||||
"L1000",
|
||||
"L12345",
|
||||
"L123456",
|
||||
# mode / version coverage
|
||||
"12345678", # numeric mode
|
||||
"LAMP-7 X", # alphanumeric with space
|
||||
"lamp_7", # upper-cased, '_' -> byte mode
|
||||
"https://example.com/lamp/L0006?x=1", # alnum chunk + byte chunk
|
||||
"L" + "0123456789" * 13, # byte 'L' + numeric chunk, version 4
|
||||
"L" + "ABCDEFGHIJ" * 20, # alphanumeric, version 8 (version info)
|
||||
]
|
||||
TEXT_IDS = ["L0001", "L0006", "L0123", "L12345", "L9", "L123456", "L0123456789012", "LAVA-7", "WWWWWWWWWW", "lamp_7"]
|
||||
|
||||
# ---- verbatim copy of label_printer.py (HPCS) -------------------------------
|
||||
def _font(size):
|
||||
for name in ("arialbd.ttf", "arial.ttf", "DejaVuSans-Bold.ttf"):
|
||||
try:
|
||||
return ImageFont.truetype(name, size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default(size)
|
||||
|
||||
|
||||
def render_label(lamp_id: str) -> Image.Image:
|
||||
image = Image.new("1", DOTS, 1)
|
||||
qr = qrcode.QRCode(
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
||||
box_size=1,
|
||||
border=2,
|
||||
)
|
||||
qr.add_data(lamp_id.upper())
|
||||
qr.make(fit=True)
|
||||
modules = len(qr.get_matrix())
|
||||
qr_size = DOTS[1] - TEXT_HEIGHT
|
||||
qr.box_size = max(1, qr_size // modules)
|
||||
qr_img = qr.make_image().get_image().convert("1")
|
||||
image.paste(qr_img, ((DOTS[0] - qr_img.width) // 2, (qr_size - qr_img.height) // 2))
|
||||
|
||||
draw = ImageDraw.Draw(image)
|
||||
size = 44
|
||||
while size > 10:
|
||||
font = _font(size)
|
||||
box = draw.textbbox((0, 0), lamp_id, font=font)
|
||||
if box[2] - box[0] <= DOTS[0] - 8:
|
||||
break
|
||||
size -= 2
|
||||
draw.text(
|
||||
((DOTS[0] - (box[2] - box[0])) // 2 - box[0], DOTS[1] - TEXT_HEIGHT + (TEXT_HEIGHT - (box[3] - box[1])) // 2 - box[1]),
|
||||
lamp_id,
|
||||
font=font,
|
||||
fill=0,
|
||||
)
|
||||
return image
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def text_layout(lamp_id):
|
||||
"""Same loop as render_label, returning what it decided."""
|
||||
draw = ImageDraw.Draw(Image.new("1", DOTS, 1))
|
||||
size = 44
|
||||
while size > 10:
|
||||
font = _font(size)
|
||||
box = draw.textbbox((0, 0), lamp_id, font=font)
|
||||
if box[2] - box[0] <= DOTS[0] - 8:
|
||||
break
|
||||
size -= 2
|
||||
origin = (
|
||||
(DOTS[0] - (box[2] - box[0])) // 2 - box[0],
|
||||
DOTS[1] - TEXT_HEIGHT + (TEXT_HEIGHT - (box[3] - box[1])) // 2 - box[1],
|
||||
)
|
||||
return font, size, box, origin
|
||||
|
||||
|
||||
def file_safe(lamp_id):
|
||||
return re.sub(r"[^A-Za-z0-9_-]", "_", lamp_id)[:40]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--reference", default=os.environ.get("LABEL_PRINTER_PY"))
|
||||
args = ap.parse_args()
|
||||
reference = None
|
||||
if args.reference and Path(args.reference).exists():
|
||||
spec = importlib.util.spec_from_file_location("label_printer_ref", args.reference)
|
||||
reference = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(reference)
|
||||
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
for f in OUT.iterdir():
|
||||
if f.suffix in (".png", ".txt"):
|
||||
f.unlink()
|
||||
|
||||
lines = []
|
||||
for lamp_id in QR_IDS:
|
||||
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=1, border=2)
|
||||
qr.add_data(lamp_id.upper())
|
||||
qr.make(fit=True)
|
||||
mask = qr.best_mask_pattern() # deterministic; same choice make() used
|
||||
qr.make(fit=True)
|
||||
n = qr.modules_count
|
||||
lines.append(f"{lamp_id} version={qr.version} mask={mask} size={n}")
|
||||
for row in qr.modules:
|
||||
lines.append("".join("1" if m else "0" for m in row))
|
||||
(OUT / "qr_matrices.txt").write_bytes(("\n".join(lines) + "\n").encode("utf-8"))
|
||||
|
||||
names = []
|
||||
for lamp_id in dict.fromkeys(QR_IDS + TEXT_IDS):
|
||||
im = render_label(lamp_id)
|
||||
if reference is not None:
|
||||
assert reference.render_label(lamp_id).tobytes() == im.tobytes(), lamp_id
|
||||
name = f"label_{file_safe(lamp_id)}.png"
|
||||
im.save(OUT / name)
|
||||
names.append(f"{lamp_id}\t{name}")
|
||||
(OUT / "labels.txt").write_bytes(("\n".join(names) + "\n").encode("utf-8"))
|
||||
|
||||
info = []
|
||||
for lamp_id in TEXT_IDS:
|
||||
font, size, box, origin = text_layout(lamp_id)
|
||||
path = Path(getattr(font, "path", "builtin")).name
|
||||
info.append(
|
||||
f"{lamp_id}\tfont={path}\tsize={size}\tbbox={box[0]},{box[1]},{box[2]},{box[3]}\torigin={origin[0]},{origin[1]}"
|
||||
)
|
||||
(OUT / "text_info.txt").write_bytes(("\n".join(info) + "\n").encode("utf-8"))
|
||||
print(f"{len(QR_IDS)} QR matrices, {len(names)} labels, {len(info)} text layouts -> {OUT}")
|
||||
print("reference render_label checked" if reference else "reference render_label NOT checked")
|
||||
for line in info:
|
||||
print(line)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||