Skip to main content

hardless_render/
text.rs

1//! Plain-text output for test assertions (FR-13).
2
3use hardless_escpos::{Align, Node};
4
5use crate::codes;
6use crate::html::symbology_name;
7use crate::layout::{layout, Block, Segment};
8use crate::RenderOptions;
9
10/// Lays out `nodes` as plain text in fixed columns, as an 80 mm printer with Font A
11/// (48 columns) would print them. See [`to_text_with`] for other widths.
12///
13/// Each printed line becomes a line of text, aligned with spaces and with trailing spaces
14/// removed; wide characters (`GS !`) count as several columns for alignment but are written
15/// once. QR codes, barcodes and images appear as `[QR: …]`, `[BARCODE …: …]` and
16/// `[IMAGE WxH]`. Cuts, drawer pulses, settings and unknown commands produce no text.
17///
18/// ```
19/// use hardless_escpos::parse;
20/// use hardless_render::to_text;
21/// let text = to_text(&parse(b"\x1ba\x02Total: 1,250\n"));
22/// assert_eq!(text, format!("{}Total: 1,250\n", " ".repeat(48 - 12)));
23/// ```
24pub fn to_text(nodes: &[Node]) -> String {
25    to_text_with(nodes, &RenderOptions::default())
26}
27
28/// [`to_text`] for a given paper width.
29pub fn to_text_with(nodes: &[Node], options: &RenderOptions) -> String {
30    let (columns_a, columns_b) = options.paper.columns();
31    let mut out = String::new();
32    for block in layout(nodes, options.paper, false) {
33        match block {
34            Block::Line(line) => {
35                let text: String = line
36                    .segments
37                    .iter()
38                    .filter_map(|s| match s {
39                        Segment::Text { text, .. } => Some(text.as_str()),
40                        Segment::Unknown { .. } => None,
41                    })
42                    .collect();
43                // Line width is measured in units of columns_a * columns_b; one Font A
44                // column is columns_b units.
45                let used = line.columns.div_ceil(columns_b.max(1));
46                push_aligned(&mut out, line.align, &text, used, columns_a);
47            }
48            Block::Cut { .. } => {}
49            Block::Qr { align, text, .. } => {
50                let label = format!("[QR: {}]", printable(&text));
51                push_label(&mut out, align, &label, columns_a);
52            }
53            Block::Barcode {
54                align,
55                symbology,
56                data,
57                ..
58            } => {
59                let label = format!(
60                    "[BARCODE {}: {}]",
61                    symbology_name(symbology),
62                    printable(&codes::barcode_text(symbology, &data))
63                );
64                push_label(&mut out, align, &label, columns_a);
65            }
66            Block::Image {
67                align,
68                width_bytes,
69                height,
70                double_width,
71                double_height,
72                ..
73            } => {
74                let w = u32::from(width_bytes) * 8 * if double_width { 2 } else { 1 };
75                let h = u32::from(height) * if double_height { 2 } else { 1 };
76                push_label(&mut out, align, &format!("[IMAGE {w}x{h}]"), columns_a);
77            }
78        }
79    }
80    out
81}
82
83fn push_label(out: &mut String, align: Align, label: &str, columns: u32) {
84    let used = u32::try_from(label.chars().count()).unwrap_or(u32::MAX);
85    push_aligned(out, align, label, used, columns);
86}
87
88fn push_aligned(out: &mut String, align: Align, text: &str, used: u32, columns: u32) {
89    let free = columns.saturating_sub(used);
90    let pad = match align {
91        Align::Left => 0,
92        Align::Center => free / 2,
93        Align::Right => free,
94    };
95    let line = format!("{}{}", " ".repeat(pad as usize), text);
96    out.push_str(line.trim_end());
97    out.push('\n');
98}
99
100/// Replaces control characters, which have no printed form, with U+FFFD.
101fn printable(text: &str) -> String {
102    text.chars()
103        .map(|c| if c.is_control() { '\u{fffd}' } else { c })
104        .collect()
105}