use std::fmt::Write as _;
use hardless_escpos::{Align, Font, HriPosition, Node};
use crate::codes;
use crate::html::{escape_into, symbology_name};
use crate::layout::{layout, Block, Line, Segment};
use crate::{PaperWidth, RenderOptions};
const LINE_DOTS: u32 = 30;
const MARGIN_X: u32 = 18;
const MARGIN_Y: u32 = 12;
const BLOCK_PAD: u32 = 6;
const DESCENT: u32 = 7;
const ADVANCE_EM: f64 = 0.6;
const SMALL_LINE_DOTS: u32 = 22;
pub(crate) const FONT_FAMILY: &str = "'Go Mono', ui-monospace, Menlo, Consolas, monospace";
const INK: &str = "#111";
pub fn render_svg(nodes: &[Node], options: &RenderOptions) -> String {
let paper = options.paper;
let width = paper.dots() + 2 * MARGIN_X;
let mut body = String::new();
let mut y = MARGIN_Y;
for block in layout(nodes, paper, options.show_unknown) {
match block {
Block::Line(line) => y += line_svg(&mut body, &line, y, paper),
Block::Cut { partial } => {
let mid = y + LINE_DOTS / 2;
let dashes = if partial { "2 4" } else { "8 6" };
let label = if partial { "partial cut" } else { "cut" };
let _ = write!(
body,
r##"<line x1="0" y1="{mid}" x2="{width}" y2="{mid}" stroke="#999" stroke-width="2" stroke-dasharray="{dashes}" aria-label="{label}"/>"##
);
y += LINE_DOTS;
}
Block::Qr {
align,
data,
text,
module_dots,
level,
} => {
y += BLOCK_PAD;
y += match codes::qr(&data, module_dots, level) {
Ok(svg) => place(&mut body, &svg, align, y, paper),
Err(e) => error(&mut body, &format!("{e}: {text}"), y),
};
y += BLOCK_PAD;
}
Block::Barcode {
align,
symbology,
data,
height_dots,
module_dots,
hri,
} => {
y += BLOCK_PAD;
let label = codes::barcode_text(symbology, &data);
match codes::barcode(symbology, &data, module_dots, height_dots) {
Ok(svg) => {
let center = f64::from(left_of(svg.width, align, paper))
+ f64::from(svg.width) / 2.0;
if matches!(hri, HriPosition::Above | HriPosition::Both) {
y += hri_svg(&mut body, &label, center, y, paper);
}
y += place(&mut body, &svg, align, y, paper);
if matches!(hri, HriPosition::Below | HriPosition::Both) {
y += hri_svg(&mut body, &label, center, y, paper);
}
}
Err(e) => {
y += error(&mut body, &format!("{} {e}", symbology_name(symbology)), y);
}
}
y += BLOCK_PAD;
}
Block::Image {
align,
width_bytes,
height,
double_width,
double_height,
data,
} => {
y += BLOCK_PAD;
let svg = codes::image(width_bytes, height, double_width, double_height, &data);
y += place(&mut body, &svg, align, y, paper);
y += BLOCK_PAD;
}
}
}
let height = y + MARGIN_Y;
format!(
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" xml:space="preserve" font-family="{FONT_FAMILY}" role="img" aria-label="Receipt"><rect width="{width}" height="{height}" fill="#fff"/><g fill="{INK}">{body}</g></svg>"##
)
}
fn line_svg(out: &mut String, line: &Line, top: u32, paper: PaperWidth) -> u32 {
let (cols_a, cols_b) = paper.columns();
let line_units = cols_a * cols_b;
let dots_per_unit = f64::from(paper.dots()) / f64::from(line_units);
let pitch = LINE_DOTS * u32::from(line.height().max(1));
let bottom = top + pitch;
let free = f64::from(line_units.saturating_sub(line.columns)) * dots_per_unit;
let mut x = f64::from(MARGIN_X)
+ match line.align {
Align::Left => 0.0,
Align::Center => free / 2.0,
Align::Right => free,
};
for segment in &line.segments {
match segment {
Segment::Text { text, style } => {
let columns = match style.font {
Font::A => cols_a,
Font::B | Font::C => cols_b,
};
let char_dots = f64::from(line_units / columns) * dots_per_unit;
let (w, h) = (style.width.max(1), style.height.max(1));
let n = text.chars().count() as f64;
let run = char_dots * n;
let baseline = bottom - DESCENT * u32::from(h);
if style.underline > 0 {
let _ = write!(
out,
r#"<rect x="{x:.2}" y="{}" width="{:.2}" height="{}"/>"#,
baseline + 2,
run * f64::from(w),
style.underline
);
}
if text.chars().any(|c| c != ' ') {
let _ = write!(
out,
r#"<text transform="translate({x:.2} {bottom}) scale({w} {h})" y="-{DESCENT}" font-size="{:.2}" textLength="{run:.2}" lengthAdjust="spacingAndGlyphs""#,
char_dots / ADVANCE_EM
);
if style.bold {
out.push_str(r#" font-weight="bold""#);
}
out.push('>');
escape_into(out, text);
out.push_str("</text>");
}
x += run * f64::from(w);
}
Segment::Unknown { label, title } => {
let badge = label.chars().count() as f64 * 7.0 + 8.0;
out.push_str("<g><title>");
escape_into(out, title);
let _ = write!(
out,
r##"</title><rect x="{x:.2}" y="{}" width="{badge:.2}" height="18" rx="3" fill="#fde68a"/><text x="{:.2}" y="{}" font-size="11" fill="#78350f">"##,
bottom - 22,
x + 4.0,
bottom - 9
);
escape_into(out, label);
out.push_str("</text></g>");
x += badge + 4.0;
}
}
}
pitch
}
fn left_of(width: u32, align: Align, paper: PaperWidth) -> u32 {
let free = paper.dots().saturating_sub(width);
MARGIN_X
+ match align {
Align::Left => 0,
Align::Center => free / 2,
Align::Right => free,
}
}
fn place(out: &mut String, svg: &codes::Svg, align: Align, y: u32, paper: PaperWidth) -> u32 {
let x = left_of(svg.width, align, paper);
let position = format!(
r#"<svg x="{x}" y="{y}" width="{}" height="{}" "#,
svg.width, svg.height
);
out.push_str(&svg.body.replacen("<svg ", &position, 1));
svg.height
}
fn hri_svg(out: &mut String, label: &str, center: f64, y: u32, paper: PaperWidth) -> u32 {
let (_, cols_b) = paper.columns();
let size = f64::from(paper.dots()) / f64::from(cols_b) / ADVANCE_EM;
let _ = write!(
out,
r#"<text x="{center:.2}" y="{}" font-size="{size:.2}" text-anchor="middle">"#,
y + 16
);
escape_into(out, label);
out.push_str("</text>");
SMALL_LINE_DOTS
}
fn error(out: &mut String, message: &str, y: u32) -> u32 {
let _ = write!(
out,
r##"<text x="{MARGIN_X}" y="{}" font-size="14" fill="#b91c1c">"##,
y + 16
);
escape_into(out, message);
out.push_str("</text>");
SMALL_LINE_DOTS
}