use alloc::string::String;
use core::fmt::Write;
use super::{hri, Layout, RenderOptions, Renderer};
use crate::error::Result;
use crate::symbology::Symbol;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Svg;
impl Renderer for Svg {
type Output = String;
fn render(&self, symbol: &Symbol, options: &RenderOptions) -> Result<String> {
let layout = options.layout(symbol)?;
let mut out = String::with_capacity(1024);
let width_mm = px_to_mm(layout.width_px, options.dpi());
let height_mm = px_to_mm(layout.height_px, options.dpi());
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
let _ = writeln!(
out,
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" \
width=\"{width_mm:.4}mm\" height=\"{height_mm:.4}mm\" \
viewBox=\"0 0 {} {}\" shape-rendering=\"crispEdges\" role=\"img\">",
layout.width_px, layout.height_px
);
let _ = writeln!(
out,
" <title>{} barcode: {}</title>",
escape_xml(symbol.kind().name()),
escape_xml(symbol.payload())
);
let bg = options.background();
if bg.a > 0 {
let _ = writeln!(
out,
" <rect x=\"0\" y=\"0\" width=\"{}\" height=\"{}\" fill=\"{}\"/>",
layout.width_px,
layout.height_px,
bg.to_hex()
);
}
let fg = options.foreground().to_hex();
let _ = writeln!(out, " <g fill=\"{fg}\">");
write_modules(&mut out, symbol, &layout);
if layout.hri_scale > 0 {
write_hri(&mut out, symbol, &layout);
}
out.push_str(" </g>\n");
out.push_str("</svg>\n");
Ok(out)
}
}
fn write_modules(out: &mut String, symbol: &Symbol, layout: &Layout) {
let modules = symbol.modules();
for my in 0..modules.height() {
let (y, h) = if symbol.is_linear() {
(layout.symbol_y_px, layout.symbol_h_px)
} else {
(layout.symbol_y_px + my * layout.module_px, layout.module_px)
};
for (start, len) in dark_runs(modules.row(my)) {
let _ = writeln!(
out,
" <rect x=\"{}\" y=\"{y}\" width=\"{}\" height=\"{h}\"/>",
layout.symbol_x_px + start * layout.module_px,
len * layout.module_px
);
}
}
}
fn write_hri(out: &mut String, symbol: &Symbol, layout: &Layout) {
let scale = layout.hri_scale;
for (index, ch) in symbol.payload().chars().enumerate() {
let glyph = hri::glyph(ch);
let origin_x = layout.hri_x_px + (index as u32) * hri::ADVANCE * scale;
for gy in 0..hri::GLYPH_H {
let row: alloc::vec::Vec<bool> = (0..hri::GLYPH_W)
.map(|gx| hri::pixel(glyph, gx, gy))
.collect();
for (start, len) in dark_runs(&row) {
let _ = writeln!(
out,
" <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{scale}\"/>",
origin_x + start * scale,
layout.hri_y_px + gy * scale,
len * scale
);
}
}
}
}
fn dark_runs(row: &[bool]) -> impl Iterator<Item = (u32, u32)> + '_ {
let mut i = 0usize;
core::iter::from_fn(move || {
while i < row.len() && !row[i] {
i += 1;
}
if i >= row.len() {
return None;
}
let start = i;
while i < row.len() && row[i] {
i += 1;
}
Some((start as u32, (i - start) as u32))
})
}
fn px_to_mm(px: u32, dpi: u32) -> f64 {
f64::from(px) / f64::from(dpi) * 25.4
}
fn escape_xml(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
#[cfg(all(test, feature = "code128"))]
mod tests {
use super::*;
use crate::render::{Color, QuietZone};
use crate::symbology::{Code128, Symbology};
use alloc::format;
fn symbol() -> Symbol {
Code128.encode("PKG-9ED9285C").unwrap()
}
fn render(opts: &RenderOptions) -> String {
Svg.render(&symbol(), opts).unwrap()
}
#[test]
fn declares_physical_size_and_a_matching_viewbox() {
let opts = RenderOptions::default();
let layout = opts.layout(&symbol()).unwrap();
let doc = render(&opts);
assert!(doc.contains(&format!(
"viewBox=\"0 0 {} {}\"",
layout.width_px, layout.height_px
)));
assert!(doc.contains("mm\""), "physical units missing");
assert!(doc.contains("shape-rendering=\"crispEdges\""));
}
#[test]
fn geometry_matches_the_png_renderer() {
let opts = RenderOptions::default();
let s = symbol();
let layout = opts.layout(&s).unwrap();
let doc = Svg.render(&s, &opts).unwrap();
assert!(doc.contains(&format!("<rect x=\"{}\" y=\"0\"", layout.symbol_x_px)));
}
#[test]
fn adjacent_modules_are_merged_into_single_rects() {
let opts = RenderOptions::builder()
.human_readable(false)
.build()
.unwrap();
let s = symbol();
let doc = Svg.render(&s, &opts).unwrap();
let rects = doc.matches("<rect").count() - 1; let dark_modules = (0..s.modules().width())
.filter(|x| s.modules().get(*x, 0))
.count();
assert!(
rects < dark_modules,
"expected merged runs, got {rects} rects for {dark_modules} dark modules"
);
}
#[test]
fn run_merging_is_correct() {
let runs: alloc::vec::Vec<_> =
dark_runs(&[false, true, true, false, true, false, false, true]).collect();
assert_eq!(runs, alloc::vec![(1, 2), (4, 1), (7, 1)]);
assert_eq!(dark_runs(&[false, false]).count(), 0);
assert_eq!(
dark_runs(&[true, true, true]).collect::<alloc::vec::Vec<_>>(),
alloc::vec![(0, 3)]
);
}
#[test]
fn carries_an_accessible_title() {
let doc = render(&RenderOptions::default());
assert!(doc.contains("<title>Code 128 barcode: PKG-9ED9285C</title>"));
}
#[test]
fn title_text_is_xml_escaped() {
let s = Code128.encode("A<B&C\"D").unwrap();
let doc = Svg.render(&s, &RenderOptions::default()).unwrap();
assert!(doc.contains("A<B&C"D"));
assert!(!doc.contains("A<B&C"));
}
#[test]
fn transparent_background_emits_no_background_rect() {
let opts = RenderOptions::builder()
.colors(Color::BLACK, Color::TRANSPARENT)
.human_readable(false)
.build()
.unwrap();
let doc = render(&opts);
assert!(!doc.contains("fill=\"#00000000\""));
}
#[test]
fn custom_foreground_is_applied() {
let opts = RenderOptions::builder()
.colors(Color::rgb(0x12, 0x34, 0x56), Color::WHITE)
.build()
.unwrap();
assert!(render(&opts).contains("fill=\"#123456\""));
}
#[test]
fn no_quiet_zone_shifts_the_symbol_to_the_origin() {
let opts = RenderOptions::builder()
.quiet_zone(QuietZone::None)
.human_readable(false)
.build()
.unwrap();
assert!(render(&opts).contains("<rect x=\"0\" y=\"0\" width="));
}
#[test]
fn output_is_reproducible() {
let opts = RenderOptions::default();
assert_eq!(render(&opts), render(&opts));
}
}