1use std::fmt::Write as _;
8
9use hardless_escpos::{Align, Font, HriPosition, Node};
10
11use crate::codes;
12use crate::html::{escape_into, symbology_name};
13use crate::layout::{layout, Block, Line, Segment};
14use crate::{PaperWidth, RenderOptions};
15
16const LINE_DOTS: u32 = 30;
18const MARGIN_X: u32 = 18;
20const MARGIN_Y: u32 = 12;
22const BLOCK_PAD: u32 = 6;
24const DESCENT: u32 = 7;
26const ADVANCE_EM: f64 = 0.6;
28const SMALL_LINE_DOTS: u32 = 22;
30pub(crate) const FONT_FAMILY: &str = "'Go Mono', ui-monospace, Menlo, Consolas, monospace";
32const INK: &str = "#111";
33
34pub fn render_svg(nodes: &[Node], options: &RenderOptions) -> String {
47 let paper = options.paper;
48 let width = paper.dots() + 2 * MARGIN_X;
49 let mut body = String::new();
50 let mut y = MARGIN_Y;
51 for block in layout(nodes, paper, options.show_unknown) {
52 match block {
53 Block::Line(line) => y += line_svg(&mut body, &line, y, paper),
54 Block::Cut { partial } => {
55 let mid = y + LINE_DOTS / 2;
56 let dashes = if partial { "2 4" } else { "8 6" };
57 let label = if partial { "partial cut" } else { "cut" };
58 let _ = write!(
59 body,
60 r##"<line x1="0" y1="{mid}" x2="{width}" y2="{mid}" stroke="#999" stroke-width="2" stroke-dasharray="{dashes}" aria-label="{label}"/>"##
61 );
62 y += LINE_DOTS;
63 }
64 Block::Qr {
65 align,
66 data,
67 text,
68 module_dots,
69 level,
70 } => {
71 y += BLOCK_PAD;
72 y += match codes::qr(&data, module_dots, level) {
73 Ok(svg) => place(&mut body, &svg, align, y, paper),
74 Err(e) => error(&mut body, &format!("{e}: {text}"), y),
75 };
76 y += BLOCK_PAD;
77 }
78 Block::Barcode {
79 align,
80 symbology,
81 data,
82 height_dots,
83 module_dots,
84 hri,
85 } => {
86 y += BLOCK_PAD;
87 let label = codes::barcode_text(symbology, &data);
88 match codes::barcode(symbology, &data, module_dots, height_dots) {
89 Ok(svg) => {
90 let center = f64::from(left_of(svg.width, align, paper))
91 + f64::from(svg.width) / 2.0;
92 if matches!(hri, HriPosition::Above | HriPosition::Both) {
93 y += hri_svg(&mut body, &label, center, y, paper);
94 }
95 y += place(&mut body, &svg, align, y, paper);
96 if matches!(hri, HriPosition::Below | HriPosition::Both) {
97 y += hri_svg(&mut body, &label, center, y, paper);
98 }
99 }
100 Err(e) => {
101 y += error(&mut body, &format!("{} {e}", symbology_name(symbology)), y);
102 }
103 }
104 y += BLOCK_PAD;
105 }
106 Block::Image {
107 align,
108 width_bytes,
109 height,
110 double_width,
111 double_height,
112 data,
113 } => {
114 y += BLOCK_PAD;
115 let svg = codes::image(width_bytes, height, double_width, double_height, &data);
116 y += place(&mut body, &svg, align, y, paper);
117 y += BLOCK_PAD;
118 }
119 }
120 }
121 let height = y + MARGIN_Y;
122 format!(
123 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>"##
124 )
125}
126
127fn line_svg(out: &mut String, line: &Line, top: u32, paper: PaperWidth) -> u32 {
129 let (cols_a, cols_b) = paper.columns();
130 let line_units = cols_a * cols_b;
131 let dots_per_unit = f64::from(paper.dots()) / f64::from(line_units);
132 let pitch = LINE_DOTS * u32::from(line.height().max(1));
133 let bottom = top + pitch;
134 let free = f64::from(line_units.saturating_sub(line.columns)) * dots_per_unit;
135 let mut x = f64::from(MARGIN_X)
136 + match line.align {
137 Align::Left => 0.0,
138 Align::Center => free / 2.0,
139 Align::Right => free,
140 };
141 for segment in &line.segments {
142 match segment {
143 Segment::Text { text, style } => {
144 let columns = match style.font {
145 Font::A => cols_a,
146 Font::B | Font::C => cols_b,
147 };
148 let char_dots = f64::from(line_units / columns) * dots_per_unit;
149 let (w, h) = (style.width.max(1), style.height.max(1));
150 let n = text.chars().count() as f64;
151 let run = char_dots * n;
152 let baseline = bottom - DESCENT * u32::from(h);
153 if style.underline > 0 {
154 let _ = write!(
155 out,
156 r#"<rect x="{x:.2}" y="{}" width="{:.2}" height="{}"/>"#,
157 baseline + 2,
158 run * f64::from(w),
159 style.underline
160 );
161 }
162 if text.chars().any(|c| c != ' ') {
163 let _ = write!(
164 out,
165 r#"<text transform="translate({x:.2} {bottom}) scale({w} {h})" y="-{DESCENT}" font-size="{:.2}" textLength="{run:.2}" lengthAdjust="spacingAndGlyphs""#,
166 char_dots / ADVANCE_EM
167 );
168 if style.bold {
169 out.push_str(r#" font-weight="bold""#);
170 }
171 out.push('>');
172 escape_into(out, text);
173 out.push_str("</text>");
174 }
175 x += run * f64::from(w);
176 }
177 Segment::Unknown { label, title } => {
178 let badge = label.chars().count() as f64 * 7.0 + 8.0;
179 out.push_str("<g><title>");
180 escape_into(out, title);
181 let _ = write!(
182 out,
183 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">"##,
184 bottom - 22,
185 x + 4.0,
186 bottom - 9
187 );
188 escape_into(out, label);
189 out.push_str("</text></g>");
190 x += badge + 4.0;
191 }
192 }
193 }
194 pitch
195}
196
197fn left_of(width: u32, align: Align, paper: PaperWidth) -> u32 {
199 let free = paper.dots().saturating_sub(width);
200 MARGIN_X
201 + match align {
202 Align::Left => 0,
203 Align::Center => free / 2,
204 Align::Right => free,
205 }
206}
207
208fn place(out: &mut String, svg: &codes::Svg, align: Align, y: u32, paper: PaperWidth) -> u32 {
210 let x = left_of(svg.width, align, paper);
211 let position = format!(
212 r#"<svg x="{x}" y="{y}" width="{}" height="{}" "#,
213 svg.width, svg.height
214 );
215 out.push_str(&svg.body.replacen("<svg ", &position, 1));
216 svg.height
217}
218
219fn hri_svg(out: &mut String, label: &str, center: f64, y: u32, paper: PaperWidth) -> u32 {
221 let (_, cols_b) = paper.columns();
223 let size = f64::from(paper.dots()) / f64::from(cols_b) / ADVANCE_EM;
224 let _ = write!(
225 out,
226 r#"<text x="{center:.2}" y="{}" font-size="{size:.2}" text-anchor="middle">"#,
227 y + 16
228 );
229 escape_into(out, label);
230 out.push_str("</text>");
231 SMALL_LINE_DOTS
232}
233
234fn error(out: &mut String, message: &str, y: u32) -> u32 {
236 let _ = write!(
237 out,
238 r##"<text x="{MARGIN_X}" y="{}" font-size="14" fill="#b91c1c">"##,
239 y + 16
240 );
241 escape_into(out, message);
242 out.push_str("</text>");
243 SMALL_LINE_DOTS
244}