mod binarize;
mod linear;
use alloc::string::String;
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::symbology::{BitMatrix, Decoder, LinearCharacter, SymbologyKind};
const DEFAULT_SCAN_LINES: u32 = 32;
#[derive(Clone, PartialEq, Eq)]
pub struct GrayImage {
width: u32,
height: u32,
luma: Vec<u8>,
}
impl GrayImage {
pub fn from_luma(width: u32, height: u32, luma: Vec<u8>) -> Result<Self> {
let expected = check_dimensions(width, height, 1)?;
if luma.len() != expected {
return Err(Error::InvalidImage(alloc::format!(
"expected {expected} bytes for a {width}x{height} greyscale image, got {}",
luma.len()
)));
}
Ok(Self {
width,
height,
luma,
})
}
pub fn from_rgb8(width: u32, height: u32, rgb: &[u8]) -> Result<Self> {
let expected = check_dimensions(width, height, 3)?;
if rgb.len() != expected {
return Err(Error::InvalidImage(alloc::format!(
"expected {expected} bytes for a {width}x{height} RGB image, got {}",
rgb.len()
)));
}
let luma = rgb
.chunks_exact(3)
.map(|p| luminance(p[0], p[1], p[2]))
.collect();
Self::from_luma(width, height, luma)
}
pub fn from_rgba8(width: u32, height: u32, rgba: &[u8]) -> Result<Self> {
let expected = check_dimensions(width, height, 4)?;
if rgba.len() != expected {
return Err(Error::InvalidImage(alloc::format!(
"expected {expected} bytes for a {width}x{height} RGBA image, got {}",
rgba.len()
)));
}
let luma = rgba
.chunks_exact(4)
.map(|p| {
let [r, g, b] = [p[0], p[1], p[2]].map(|c| over_white(c, p[3]));
luminance(r, g, b)
})
.collect();
Self::from_luma(width, height, luma)
}
#[cfg(feature = "png")]
#[cfg_attr(docsrs, doc(cfg(feature = "png")))]
pub fn from_png(bytes: &[u8]) -> Result<Self> {
let mut decoder = ::png::Decoder::new(std::io::Cursor::new(bytes));
decoder.set_transformations(::png::Transformations::normalize_to_color8());
let mut reader = decoder
.read_info()
.map_err(|e| Error::InvalidImage(alloc::format!("{e}")))?;
let info = reader.info();
check_dimensions(info.width, info.height, 4)?;
let size = reader
.output_buffer_size()
.ok_or_else(|| Error::InvalidImage(String::from("image is too large to decode")))?;
let mut buf = alloc::vec![0u8; size];
let frame = reader
.next_frame(&mut buf)
.map_err(|e| Error::InvalidImage(alloc::format!("{e}")))?;
let pixels = &buf[..frame.buffer_size()];
let (width, height) = (frame.width, frame.height);
match frame.color_type {
::png::ColorType::Grayscale => Self::from_luma(width, height, pixels.to_vec()),
::png::ColorType::GrayscaleAlpha => {
let luma = pixels
.chunks_exact(2)
.map(|p| over_white(p[0], p[1]))
.collect();
Self::from_luma(width, height, luma)
}
::png::ColorType::Rgb => Self::from_rgb8(width, height, pixels),
::png::ColorType::Rgba => Self::from_rgba8(width, height, pixels),
other => Err(Error::InvalidImage(alloc::format!(
"unsupported PNG colour type {other:?}"
))),
}
}
#[cfg(all(feature = "png", feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "png", feature = "std"))))]
pub fn from_png_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
Self::from_png(&std::fs::read(path)?)
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn luma(&self) -> &[u8] {
&self.luma
}
pub fn pixel(&self, x: u32, y: u32) -> u8 {
if x >= self.width || y >= self.height {
return u8::MAX;
}
self.luma[(y as usize) * (self.width as usize) + (x as usize)]
}
}
impl core::fmt::Debug for GrayImage {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("GrayImage")
.field("width", &self.width)
.field("height", &self.height)
.finish_non_exhaustive()
}
}
fn check_dimensions(width: u32, height: u32, channels: usize) -> Result<usize> {
if width == 0 || height == 0 {
return Err(Error::InvalidImage(alloc::format!(
"image has a zero dimension ({width}x{height})"
)));
}
let pixels = u64::from(width) * u64::from(height);
if pixels > crate::render::MAX_PIXELS {
return Err(Error::InvalidImage(alloc::format!(
"image is {} megapixels, over the {} megapixel limit",
pixels / 1_000_000,
crate::render::MAX_PIXELS / 1_000_000
)));
}
Ok((pixels as usize) * channels)
}
fn luminance(r: u8, g: u8, b: u8) -> u8 {
((77 * u32::from(r) + 150 * u32::from(g) + 29 * u32::from(b)) >> 8) as u8
}
fn over_white(channel: u8, alpha: u8) -> u8 {
let (c, a) = (u32::from(channel), u32::from(alpha));
((c * a + 255 * (255 - a) + 127) / 255) as u8
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Scan {
kind: SymbologyKind,
payload: String,
}
impl Scan {
pub fn payload(&self) -> &str {
&self.payload
}
pub fn kind(&self) -> SymbologyKind {
self.kind
}
pub fn into_payload(self) -> String {
self.payload
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Axis {
Row,
Column,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Scanner {
max_scan_lines: u32,
}
impl Default for Scanner {
fn default() -> Self {
Self::new()
}
}
impl Scanner {
pub fn new() -> Self {
Self {
max_scan_lines: DEFAULT_SCAN_LINES,
}
}
pub fn max_scan_lines(mut self, lines: u32) -> Self {
self.max_scan_lines = lines.max(1);
self
}
pub fn scan(&self, image: &GrayImage) -> Result<Scan> {
self.scan_with(&crate::symbology::Code128, image)
}
pub fn scan_with<D: Decoder + ?Sized>(&self, decoder: &D, image: &GrayImage) -> Result<Scan> {
let kind = decoder.kind();
let character = kind.linear_character().ok_or_else(|| {
Error::Decode(alloc::format!(
"{kind} cannot be read from an image: it is not a linear symbology"
))
})?;
let matrix = binarize::binarize(image);
for axis in [Axis::Row, Axis::Column] {
if let Some(payload) = self.scan_axis(decoder, character, &matrix, axis) {
return Ok(Scan { kind, payload });
}
}
Err(Error::NoSymbolFound(alloc::format!(
"no {kind} symbol on any of the scan lines tried across a {}x{} image",
image.width(),
image.height()
)))
}
fn scan_axis<D: Decoder + ?Sized>(
&self,
decoder: &D,
character: LinearCharacter,
matrix: &BitMatrix,
axis: Axis,
) -> Option<String> {
let count = match axis {
Axis::Row => matrix.height(),
Axis::Column => matrix.width(),
};
for index in scan_line_order(count, self.max_scan_lines) {
for radius in [0, 1] {
if radius > 0 && count < 3 {
continue;
}
let line = scan_line(matrix, axis, index, radius);
if let Some(payload) = linear::decode_row(decoder, character, &line) {
return Some(payload);
}
let inverted: Vec<bool> = line.iter().map(|d| !d).collect();
if let Some(payload) = linear::decode_row(decoder, character, &inverted) {
return Some(payload);
}
}
}
None
}
}
fn scan_line_order(count: u32, max_lines: u32) -> Vec<u32> {
let lines = count.min(max_lines.max(1));
let mut order: Vec<u32> = (0..lines)
.map(|i| ((2 * i + 1) as u64 * count as u64 / (2 * lines as u64)) as u32)
.map(|i| i.min(count - 1))
.collect();
order.dedup();
let centre = i64::from(count) / 2;
order.sort_by_key(|&i| (i64::from(i) - centre).abs());
order
}
fn scan_line(matrix: &BitMatrix, axis: Axis, index: u32, radius: u32) -> Vec<bool> {
let (length, count) = match axis {
Axis::Row => (matrix.width(), matrix.height()),
Axis::Column => (matrix.height(), matrix.width()),
};
if radius == 0 {
return match axis {
Axis::Row => matrix.row(index).to_vec(),
Axis::Column => (0..length).map(|y| matrix.get(index, y)).collect(),
};
}
let lo = index.saturating_sub(radius);
let hi = (index + radius + 1).min(count);
let voters = hi - lo;
(0..length)
.map(|pos| {
let dark = (lo..hi)
.filter(|&i| match axis {
Axis::Row => matrix.get(pos, i),
Axis::Column => matrix.get(i, pos),
})
.count() as u32;
2 * dark > voters
})
.collect()
}
#[cfg(feature = "png")]
#[cfg_attr(docsrs, doc(cfg(feature = "png")))]
pub fn scan_png(bytes: &[u8]) -> Result<Scan> {
Scanner::new().scan(&GrayImage::from_png(bytes)?)
}
#[cfg(all(feature = "png", feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "png", feature = "std"))))]
pub fn scan_png_file(path: impl AsRef<std::path::Path>) -> Result<Scan> {
scan_png(&std::fs::read(path)?)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn rejects_a_buffer_that_does_not_match_its_dimensions() {
let err = GrayImage::from_luma(4, 4, vec![0; 15]).unwrap_err();
assert!(matches!(err, Error::InvalidImage(_)), "got {err:?}");
assert!(GrayImage::from_luma(4, 4, vec![0; 16]).is_ok());
assert!(GrayImage::from_rgb8(2, 2, &[0; 11]).is_err());
assert!(GrayImage::from_rgb8(2, 2, &[0; 12]).is_ok());
assert!(GrayImage::from_rgba8(2, 2, &[0; 15]).is_err());
assert!(GrayImage::from_rgba8(2, 2, &[0; 16]).is_ok());
}
#[test]
fn rejects_a_zero_dimension() {
assert!(GrayImage::from_luma(0, 4, vec![]).is_err());
assert!(GrayImage::from_luma(4, 0, vec![]).is_err());
}
#[test]
fn rejects_an_image_larger_than_the_allocation_limit() {
let err = GrayImage::from_luma(20_000, 20_000, Vec::new()).unwrap_err();
assert!(
alloc::format!("{err}").contains("megapixel"),
"expected a pixel-count error, got {err}"
);
}
#[test]
fn transparent_pixels_composite_over_white() {
let rgba = [0, 0, 0, 0, 0, 0, 0, 255];
let image = GrayImage::from_rgba8(2, 1, &rgba).unwrap();
assert_eq!(image.pixel(0, 0), 255, "transparent should read as paper");
assert_eq!(image.pixel(1, 0), 0, "opaque black should read as ink");
}
#[test]
fn out_of_bounds_pixels_read_as_paper() {
let image = GrayImage::from_luma(2, 2, vec![0; 4]).unwrap();
assert_eq!(image.pixel(0, 0), 0);
assert_eq!(image.pixel(2, 0), 255);
assert_eq!(image.pixel(0, 2), 255);
assert_eq!(image.pixel(u32::MAX, u32::MAX), 255);
}
#[test]
fn scan_lines_start_in_the_middle_and_stay_in_range() {
let order = scan_line_order(100, 8);
assert_eq!(order.len(), 8);
assert!(order.iter().all(|&i| i < 100));
assert!((40..=60).contains(&order[0]), "started at {}", order[0]);
}
#[test]
fn every_line_is_tried_when_there_are_fewer_than_the_cap() {
let mut order = scan_line_order(5, 32);
order.sort_unstable();
assert_eq!(order, vec![0, 1, 2, 3, 4]);
}
#[test]
fn a_single_line_image_still_yields_one_line() {
assert_eq!(scan_line_order(1, 32), vec![0]);
}
}