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.
This commit is contained in:
commit
2a3f28001c
241 files changed
+46390
No files matched your search
@@ -0,0 +1,272 @@
|
||||
//! A minimal 1-bit image type.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// A 1-bit image. `true` means black (a printed dot), `false` means white.
|
||||
///
|
||||
/// Pixels are addressed as `(x, y)` with `(0, 0)` in the top-left corner,
|
||||
/// `x` growing to the right and `y` growing downwards (the same convention as
|
||||
/// PIL).
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Bitmap {
|
||||
width: u32,
|
||||
height: u32,
|
||||
/// Row-major pixels, `true` = black.
|
||||
pixels: Vec<bool>,
|
||||
}
|
||||
|
||||
impl Bitmap {
|
||||
/// A new all-white bitmap.
|
||||
pub fn new(width: u32, height: u32) -> Self {
|
||||
Self::filled(width, height, false)
|
||||
}
|
||||
|
||||
/// A new bitmap with every pixel set to `black`.
|
||||
pub fn filled(width: u32, height: u32, black: bool) -> Self {
|
||||
let len = width as usize * height as usize;
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
pixels: vec![black; len],
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a bitmap by evaluating `f(x, y)` (returns `true` for black) for every pixel.
|
||||
pub fn from_fn(width: u32, height: u32, mut f: impl FnMut(u32, u32) -> bool) -> Self {
|
||||
let mut pixels = Vec::with_capacity(width as usize * height as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
pixels.push(f(x, y));
|
||||
}
|
||||
}
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
pixels,
|
||||
}
|
||||
}
|
||||
|
||||
/// Width in pixels.
|
||||
pub fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
/// Height in pixels.
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
/// `(width, height)`.
|
||||
pub fn size(&self) -> (u32, u32) {
|
||||
(self.width, self.height)
|
||||
}
|
||||
|
||||
fn index(&self, x: u32, y: u32) -> usize {
|
||||
assert!(
|
||||
x < self.width && y < self.height,
|
||||
"pixel ({x}, {y}) out of bounds for a {}x{} bitmap",
|
||||
self.width,
|
||||
self.height
|
||||
);
|
||||
y as usize * self.width as usize + x as usize
|
||||
}
|
||||
|
||||
/// Returns `true` if the pixel is black.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `(x, y)` is outside the bitmap.
|
||||
pub fn get(&self, x: u32, y: u32) -> bool {
|
||||
self.pixels[self.index(x, y)]
|
||||
}
|
||||
|
||||
/// Sets a pixel (`true` = black).
|
||||
///
|
||||
/// # Panics
|
||||
/// If `(x, y)` is outside the bitmap.
|
||||
pub fn set(&mut self, x: u32, y: u32, black: bool) {
|
||||
let i = self.index(x, y);
|
||||
self.pixels[i] = black;
|
||||
}
|
||||
|
||||
/// Sets a pixel given signed coordinates; pixels outside the bitmap are ignored.
|
||||
pub fn set_clipped(&mut self, x: i64, y: i64, black: bool) {
|
||||
if x >= 0 && y >= 0 && x < i64::from(self.width) && y < i64::from(self.height) {
|
||||
self.set(x as u32, y as u32, black);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills the rectangle `[x, x + w) x [y, y + h)` (clipped to the bitmap).
|
||||
pub fn fill_rect(&mut self, x: i64, y: i64, w: u32, h: u32, black: bool) {
|
||||
let x0 = x.max(0);
|
||||
let y0 = y.max(0);
|
||||
let x1 = (x + i64::from(w)).min(i64::from(self.width));
|
||||
let y1 = (y + i64::from(h)).min(i64::from(self.height));
|
||||
for yy in y0..y1 {
|
||||
for xx in x0..x1 {
|
||||
self.set(xx as u32, yy as u32, black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies `src` into this bitmap with its top-left corner at `(x, y)`,
|
||||
/// overwriting both black and white pixels (like PIL `Image.paste` without
|
||||
/// a mask). Parts falling outside are clipped.
|
||||
pub fn paste(&mut self, src: &Bitmap, x: i64, y: i64) {
|
||||
for sy in 0..src.height {
|
||||
let dy = y + i64::from(sy);
|
||||
if dy < 0 || dy >= i64::from(self.height) {
|
||||
continue;
|
||||
}
|
||||
for sx in 0..src.width {
|
||||
let dx = x + i64::from(sx);
|
||||
if dx < 0 || dx >= i64::from(self.width) {
|
||||
continue;
|
||||
}
|
||||
self.set(dx as u32, dy as u32, src.get(sx, sy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the sub-image `[x, x + w) x [y, y + h)`; pixels outside the
|
||||
/// source are white.
|
||||
pub fn crop(&self, x: i64, y: i64, w: u32, h: u32) -> Bitmap {
|
||||
Bitmap::from_fn(w, h, |cx, cy| {
|
||||
let sx = x + i64::from(cx);
|
||||
let sy = y + i64::from(cy);
|
||||
sx >= 0
|
||||
&& sy >= 0
|
||||
&& sx < i64::from(self.width)
|
||||
&& sy < i64::from(self.height)
|
||||
&& self.get(sx as u32, sy as u32)
|
||||
})
|
||||
}
|
||||
|
||||
/// Rotates 90 degrees counter-clockwise (PIL `Image.rotate(90, expand=True)`
|
||||
/// / `Transpose.ROTATE_90`).
|
||||
pub fn rotate90(&self) -> Bitmap {
|
||||
let (w, h) = (self.width, self.height);
|
||||
Bitmap::from_fn(h, w, |x, y| self.get(w - 1 - y, x))
|
||||
}
|
||||
|
||||
/// Rotates 180 degrees.
|
||||
pub fn rotate180(&self) -> Bitmap {
|
||||
let (w, h) = (self.width, self.height);
|
||||
Bitmap::from_fn(w, h, |x, y| self.get(w - 1 - x, h - 1 - y))
|
||||
}
|
||||
|
||||
/// Rotates 270 degrees counter-clockwise (= 90 degrees clockwise, PIL
|
||||
/// `Transpose.ROTATE_270`).
|
||||
pub fn rotate270(&self) -> Bitmap {
|
||||
let (w, h) = (self.width, self.height);
|
||||
Bitmap::from_fn(h, w, |x, y| self.get(y, h - 1 - x))
|
||||
}
|
||||
|
||||
/// Mirrors left-right (PIL `Transpose.FLIP_LEFT_RIGHT`).
|
||||
pub fn flip_horizontal(&self) -> Bitmap {
|
||||
let w = self.width;
|
||||
Bitmap::from_fn(w, self.height, |x, y| self.get(w - 1 - x, y))
|
||||
}
|
||||
|
||||
/// Nearest-neighbour resize exactly as Pillow does it for mode `"1"`
|
||||
/// images (Pillow forces `NEAREST` for 1-bit images whatever filter is
|
||||
/// requested): source coordinate = `floor(scale * 0.5 + i * scale)` with the
|
||||
/// position accumulated in `f64` like `ImagingScaleAffine`.
|
||||
pub fn resize_nearest(&self, width: u32, height: u32) -> Bitmap {
|
||||
if width == self.width && height == self.height {
|
||||
return self.clone();
|
||||
}
|
||||
let xmap = nearest_map(self.width, width);
|
||||
let ymap = nearest_map(self.height, height);
|
||||
Bitmap::from_fn(width, height, |x, y| {
|
||||
match (xmap[x as usize], ymap[y as usize]) {
|
||||
(Some(sx), Some(sy)) => self.get(sx, sy),
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Number of black pixels.
|
||||
pub fn count_black(&self) -> usize {
|
||||
self.pixels.iter().filter(|&&p| p).count()
|
||||
}
|
||||
|
||||
/// Row `y` as booleans (`true` = black).
|
||||
pub fn row(&self, y: u32) -> &[bool] {
|
||||
let start = self.index(0, y);
|
||||
&self.pixels[start..start + self.width as usize]
|
||||
}
|
||||
|
||||
/// All pixels in row-major order (`true` = black).
|
||||
pub fn pixels(&self) -> &[bool] {
|
||||
&self.pixels
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_map(src: u32, dst: u32) -> Vec<Option<u32>> {
|
||||
let scale = f64::from(src) / f64::from(dst);
|
||||
let mut pos = scale * 0.5;
|
||||
let mut out = Vec::with_capacity(dst as usize);
|
||||
for _ in 0..dst {
|
||||
// COORD(v) in Pillow: v < 0 ? -1 : (int)v
|
||||
let i = if pos < 0.0 { -1 } else { pos as i64 };
|
||||
out.push(if i >= 0 && i < i64::from(src) {
|
||||
Some(i as u32)
|
||||
} else {
|
||||
None
|
||||
});
|
||||
pos += scale;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
impl fmt::Debug for Bitmap {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Bitmap")
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.field("black_pixels", &self.count_black())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rotations_match_pil_direction() {
|
||||
// single black pixel in the top-left corner of a 3x2 image
|
||||
let mut b = Bitmap::new(3, 2);
|
||||
b.set(0, 0, true);
|
||||
// PIL rotate(90) is counter-clockwise: top-left goes to bottom-left
|
||||
let r = b.rotate90();
|
||||
assert_eq!(r.size(), (2, 3));
|
||||
assert!(r.get(0, 2));
|
||||
let r = b.rotate270();
|
||||
assert!(r.get(1, 0));
|
||||
let r = b.rotate180();
|
||||
assert!(r.get(2, 1));
|
||||
assert_eq!(b.rotate90().rotate90(), b.rotate180());
|
||||
assert_eq!(b.rotate90().rotate270(), b);
|
||||
assert!(b.flip_horizontal().get(2, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_and_crop_clip() {
|
||||
let mut b = Bitmap::new(4, 4);
|
||||
let src = Bitmap::filled(3, 3, true);
|
||||
b.paste(&src, -1, 2);
|
||||
assert_eq!(b.count_black(), 4);
|
||||
assert!(b.get(0, 2) && b.get(1, 3));
|
||||
let c = b.crop(-1, 2, 3, 3);
|
||||
assert_eq!(c.count_black(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_halves_like_pillow() {
|
||||
// PIL picks the odd source columns when halving (floor(2*i + 1)).
|
||||
let b = Bitmap::from_fn(4, 1, |x, _| x % 2 == 1);
|
||||
let r = b.resize_nearest(2, 1);
|
||||
assert!(r.get(0, 0) && r.get(1, 0));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user