mod hri;
#[cfg(feature = "png")]
pub mod png;
#[cfg(feature = "svg")]
pub mod svg;
#[cfg(feature = "png")]
pub use png::Png;
#[cfg(feature = "svg")]
pub use svg::Svg;
use alloc::string::String;
use crate::error::{Error, Result};
use crate::symbology::Symbol;
const MAX_DIMENSION_PX: u32 = 20_000;
pub(crate) const MAX_PIXELS: u64 = 64_000_000;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Length {
Px(f64),
Mm(f64),
Mils(f64),
Inch(f64),
}
impl Length {
pub fn to_px(self, dpi: u32) -> f64 {
let dpi = f64::from(dpi);
match self {
Self::Px(v) => v,
Self::Mm(v) => v / 25.4 * dpi,
Self::Mils(v) => v / 1000.0 * dpi,
Self::Inch(v) => v * dpi,
}
}
pub fn to_mm(self, dpi: u32) -> f64 {
self.to_px(dpi) / f64::from(dpi) * 25.4
}
fn is_positive(self) -> bool {
let v = match self {
Self::Px(v) | Self::Mm(v) | Self::Mils(v) | Self::Inch(v) => v,
};
v.is_finite() && v > 0.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
}
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub const fn is_opaque(self) -> bool {
self.a == 255
}
pub fn to_hex(self) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(9);
s.push('#');
let mut push = |v: u8| {
s.push(HEX[(v >> 4) as usize] as char);
s.push(HEX[(v & 0x0f) as usize] as char);
};
push(self.r);
push(self.g);
push(self.b);
if !self.is_opaque() {
push(self.a);
}
s
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum QuietZone {
#[default]
Standard,
Modules(u32),
None,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RenderOptions {
module_width: Length,
height: Length,
quiet_zone: QuietZone,
dpi: u32,
foreground: Color,
background: Color,
human_readable: bool,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
module_width: Length::Mils(13.0),
height: Length::Mm(25.0),
quiet_zone: QuietZone::Standard,
dpi: 300,
foreground: Color::BLACK,
background: Color::WHITE,
human_readable: true,
}
}
}
impl RenderOptions {
pub fn builder() -> RenderOptionsBuilder {
RenderOptionsBuilder::default()
}
pub fn module_width(&self) -> Length {
self.module_width
}
pub fn height(&self) -> Length {
self.height
}
pub fn quiet_zone(&self) -> QuietZone {
self.quiet_zone
}
pub fn dpi(&self) -> u32 {
self.dpi
}
pub fn foreground(&self) -> Color {
self.foreground
}
pub fn background(&self) -> Color {
self.background
}
pub fn human_readable(&self) -> bool {
self.human_readable
}
pub fn layout(&self, symbol: &Symbol) -> Result<Layout> {
Layout::compute(symbol, self)
}
}
#[derive(Debug, Clone, Default)]
pub struct RenderOptionsBuilder {
options: RenderOptions,
}
impl RenderOptionsBuilder {
pub fn module_width(mut self, width: Length) -> Self {
self.options.module_width = width;
self
}
pub fn height(mut self, height: Length) -> Self {
self.options.height = height;
self
}
pub fn quiet_zone(mut self, quiet_zone: QuietZone) -> Self {
self.options.quiet_zone = quiet_zone;
self
}
pub fn dpi(mut self, dpi: u32) -> Self {
self.options.dpi = dpi;
self
}
pub fn colors(mut self, foreground: Color, background: Color) -> Self {
self.options.foreground = foreground;
self.options.background = background;
self
}
pub fn human_readable(mut self, enabled: bool) -> Self {
self.options.human_readable = enabled;
self
}
pub fn build(self) -> Result<RenderOptions> {
let o = &self.options;
if o.dpi == 0 {
return Err(Error::InvalidRenderOptions("dpi must be positive".into()));
}
if !o.module_width.is_positive() {
return Err(Error::InvalidRenderOptions(
"module_width must be a positive, finite length".into(),
));
}
if !o.height.is_positive() {
return Err(Error::InvalidRenderOptions(
"height must be a positive, finite length".into(),
));
}
Ok(self.options)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Layout {
pub module_px: u32,
pub quiet_x_px: u32,
pub quiet_y_px: u32,
pub symbol_w_px: u32,
pub symbol_h_px: u32,
pub symbol_x_px: u32,
pub symbol_y_px: u32,
pub hri_scale: u32,
pub hri_block_h_px: u32,
pub hri_x_px: u32,
pub hri_y_px: u32,
pub width_px: u32,
pub height_px: u32,
}
impl Layout {
fn compute(symbol: &Symbol, options: &RenderOptions) -> Result<Self> {
let modules = symbol.modules();
let module_px = round_to_u32(options.module_width.to_px(options.dpi)).max(1);
let quiet_modules = match options.quiet_zone {
QuietZone::Standard => symbol.kind().required_quiet_zone(),
QuietZone::Modules(n) => n,
QuietZone::None => 0,
};
let quiet_x_px = quiet_modules.saturating_mul(module_px);
let quiet_y_px = if symbol.is_linear() { 0 } else { quiet_x_px };
let symbol_w_px = modules.width().saturating_mul(module_px);
let symbol_h_px = if symbol.is_linear() {
round_to_u32(options.height.to_px(options.dpi)).max(1)
} else {
modules.height().saturating_mul(module_px)
};
let char_count = symbol.payload().chars().count() as u32;
let draw_hri = options.human_readable && char_count > 0;
let (hri_scale, hri_block_h_px, text_w_px) = if draw_hri {
let natural_w = hri::text_width(char_count);
let width_limited = symbol_w_px.checked_div(natural_w).unwrap_or(1);
let height_limited = symbol_h_px / hri::GLYPH_H;
let scale = width_limited.min(height_limited).max(1);
let gap = module_px;
(scale, gap + hri::GLYPH_H * scale + gap, natural_w * scale)
} else {
(0, 0, 0)
};
let content_w_px = symbol_w_px.max(text_w_px);
let width_px = content_w_px.saturating_add(quiet_x_px.saturating_mul(2));
let height_px = symbol_h_px
.saturating_add(quiet_y_px.saturating_mul(2))
.saturating_add(hri_block_h_px);
let symbol_x_px = quiet_x_px + (content_w_px - symbol_w_px) / 2;
let (hri_x_px, hri_y_px) = if draw_hri {
(
quiet_x_px + (content_w_px - text_w_px) / 2,
quiet_y_px
.saturating_mul(2)
.saturating_add(symbol_h_px)
.saturating_add(module_px),
)
} else {
(0, 0)
};
if width_px == 0 || height_px == 0 {
return Err(Error::InvalidRenderOptions(
"computed image has zero area".into(),
));
}
if width_px > MAX_DIMENSION_PX || height_px > MAX_DIMENSION_PX {
return Err(Error::InvalidRenderOptions(alloc::format!(
"computed image is {width_px}x{height_px} px, exceeding the {MAX_DIMENSION_PX} px limit; \
reduce module_width, height, or dpi"
)));
}
if u64::from(width_px) * u64::from(height_px) > MAX_PIXELS {
return Err(Error::InvalidRenderOptions(alloc::format!(
"computed image is {width_px}x{height_px} px, over the {} megapixel limit; \
reduce module_width, height, or dpi",
MAX_PIXELS / 1_000_000
)));
}
Ok(Self {
module_px,
quiet_x_px,
quiet_y_px,
symbol_w_px,
symbol_h_px,
symbol_x_px,
symbol_y_px: quiet_y_px,
hri_scale,
hri_block_h_px,
hri_x_px,
hri_y_px,
width_px,
height_px,
})
}
}
fn round_to_u32(v: f64) -> u32 {
if !v.is_finite() || v <= 0.0 {
return 0;
}
let rounded = round_half_up(v);
if rounded >= f64::from(u32::MAX) {
u32::MAX
} else {
rounded as u32
}
}
fn round_half_up(v: f64) -> f64 {
let truncated = v as i64 as f64;
if v - truncated >= 0.5 {
truncated + 1.0
} else {
truncated
}
}
pub fn hri_supports(text: &str) -> bool {
text.chars().all(hri::is_supported)
}
pub trait Renderer {
type Output;
fn render(&self, symbol: &Symbol, options: &RenderOptions) -> Result<Self::Output>;
}
#[cfg(all(test, feature = "code128"))]
mod tests {
use super::*;
use crate::symbology::{Code128, Symbology};
fn symbol() -> Symbol {
Code128.encode("PKG-9ED9285C").unwrap()
}
#[test]
fn lengths_convert_consistently() {
assert_eq!(Length::Inch(1.0).to_px(300), 300.0);
assert_eq!(Length::Mils(1000.0).to_px(300), 300.0);
assert_eq!(Length::Px(42.0).to_px(300), 42.0);
assert!((Length::Mm(25.4).to_px(300) - 300.0).abs() < 1e-9);
assert!((Length::Inch(1.0).to_mm(300) - 25.4).abs() < 1e-9);
}
#[test]
fn module_width_snaps_to_whole_pixels() {
let s = symbol();
let layout = RenderOptions::default().layout(&s).unwrap();
assert_eq!(layout.module_px, 4);
assert_eq!(layout.symbol_w_px % layout.module_px, 0);
}
#[test]
fn module_width_never_collapses_to_zero() {
let opts = RenderOptions::builder()
.module_width(Length::Mils(1.0))
.dpi(72)
.build()
.unwrap();
assert_eq!(opts.layout(&symbol()).unwrap().module_px, 1);
}
#[test]
fn standard_quiet_zone_is_ten_modules_per_side() {
let s = symbol();
let layout = RenderOptions::default().layout(&s).unwrap();
assert_eq!(layout.quiet_x_px, 10 * layout.module_px);
assert_eq!(layout.width_px, layout.symbol_w_px + 2 * layout.quiet_x_px);
}
#[test]
fn linear_symbols_get_no_vertical_quiet_zone() {
let layout = RenderOptions::default().layout(&symbol()).unwrap();
assert_eq!(layout.quiet_y_px, 0);
assert_eq!(layout.symbol_y_px, 0);
}
#[test]
fn quiet_zone_can_be_overridden() {
let s = symbol();
let none = RenderOptions::builder()
.quiet_zone(QuietZone::None)
.build()
.unwrap()
.layout(&s)
.unwrap();
assert_eq!(none.quiet_x_px, 0);
assert_eq!(none.width_px, none.symbol_w_px);
let explicit = RenderOptions::builder()
.quiet_zone(QuietZone::Modules(2))
.build()
.unwrap()
.layout(&s)
.unwrap();
assert_eq!(explicit.quiet_x_px, 2 * explicit.module_px);
}
#[test]
fn hri_is_centred_and_fits_within_the_symbol() {
let s = symbol();
let layout = RenderOptions::default().layout(&s).unwrap();
assert!(layout.hri_scale >= 1);
let text_w = hri::text_width(s.payload().chars().count() as u32) * layout.hri_scale;
assert!(text_w <= layout.symbol_w_px, "HRI wider than the symbol");
assert!(layout.hri_x_px >= layout.quiet_x_px);
assert!(layout.hri_x_px + text_w <= layout.width_px);
assert!(layout.hri_y_px + hri::GLYPH_H * layout.hri_scale <= layout.height_px);
}
#[test]
fn hri_is_padded_away_from_both_edges() {
let layout = RenderOptions::default().layout(&symbol()).unwrap();
assert!(layout.hri_y_px > layout.symbol_y_px + layout.symbol_h_px);
let text_bottom = layout.hri_y_px + hri::GLYPH_H * layout.hri_scale;
assert!(
text_bottom < layout.height_px,
"HRI is flush against the bottom edge and may be clipped"
);
}
#[test]
fn disabling_hri_removes_the_text_block() {
let layout = RenderOptions::builder()
.human_readable(false)
.build()
.unwrap()
.layout(&symbol())
.unwrap();
assert_eq!(layout.hri_scale, 0);
assert_eq!(layout.hri_block_h_px, 0);
assert_eq!(layout.height_px, layout.symbol_h_px);
}
#[test]
fn builder_rejects_degenerate_options() {
assert!(RenderOptions::builder().dpi(0).build().is_err());
assert!(RenderOptions::builder()
.module_width(Length::Mm(0.0))
.build()
.is_err());
assert!(RenderOptions::builder()
.height(Length::Mm(-1.0))
.build()
.is_err());
assert!(RenderOptions::builder()
.module_width(Length::Mm(f64::NAN))
.build()
.is_err());
}
#[test]
fn absurd_geometry_is_rejected_rather_than_allocated() {
let opts = RenderOptions::builder()
.module_width(Length::Inch(10.0))
.dpi(1200)
.build()
.unwrap();
assert!(matches!(
opts.layout(&symbol()),
Err(Error::InvalidRenderOptions(_))
));
}
#[test]
#[cfg(feature = "qr")]
fn a_huge_but_within_axis_limits_image_is_still_rejected() {
use crate::symbology::{Qr, QrVersion};
let big = Qr::new()
.version(QrVersion::Fixed(40))
.encode("PKG-9ED9285C")
.unwrap();
let opts = RenderOptions::builder()
.module_width(Length::Px(100.0))
.human_readable(false)
.build()
.unwrap();
let err = opts.layout(&big).unwrap_err();
assert!(matches!(err, Error::InvalidRenderOptions(_)), "got {err:?}");
assert!(
alloc::format!("{err}").contains("megapixel"),
"the message should name the limit that was hit: {err}"
);
let sane = RenderOptions::builder()
.module_width(Length::Px(8.0))
.build()
.unwrap();
assert!(sane.layout(&big).is_ok());
}
#[test]
fn colors_format_as_css_hex() {
assert_eq!(Color::BLACK.to_hex(), "#000000");
assert_eq!(Color::WHITE.to_hex(), "#ffffff");
assert_eq!(Color::rgba(1, 2, 3, 4).to_hex(), "#01020304");
assert!(Color::BLACK.is_opaque());
assert!(!Color::TRANSPARENT.is_opaque());
}
#[test]
fn rounding_is_half_up_and_saturating() {
assert_eq!(round_to_u32(3.4), 3);
assert_eq!(round_to_u32(3.5), 4);
assert_eq!(round_to_u32(-1.0), 0);
assert_eq!(round_to_u32(f64::NAN), 0);
assert_eq!(round_to_u32(f64::INFINITY), 0);
assert_eq!(round_to_u32(1e30), u32::MAX);
}
}