1use roxmltree::Node;
9
10use crate::escpos::{unescape, Align, EscPos, Hri, QrLevel};
11use crate::xml::{self, attr, bad, elements, flag, number, text};
12use crate::{DialectError, Outcome, PrintRequest, PrinterStatus};
13
14pub mod code {
16 pub const SUCCESS: &str = "0";
18 pub const OFFLINE: &str = "1100";
20 pub const BUSY: &str = "2001";
22}
23
24pub fn parse(document: &str) -> Result<PrintRequest, DialectError> {
34 let doc = xml::parse(document)?;
35 let root = doc.root_element();
36 if root.tag_name().name() != "root" {
37 return Err(DialectError::new(format!(
38 "expected <root>, not <{}>",
39 root.tag_name().name()
40 )));
41 }
42 let mut out = EscPos::new();
43 let mut warnings = Vec::new();
44 let mut size = (1u8, 1u8);
45 for node in elements(root) {
46 element(node, &mut out, &mut size, &mut warnings)?;
47 }
48 Ok(PrintRequest {
49 escpos: out.finish(),
50 warnings,
51 })
52}
53
54fn element(
55 node: Node<'_, '_>,
56 doc: &mut EscPos,
57 size: &mut (u8, u8),
58 warnings: &mut Vec<String>,
59) -> Result<(), DialectError> {
60 match node.tag_name().name() {
61 "initialization" => {
62 if flag(node, "reset")? == Some(true) {
63 doc.raw(&[0x1B, 0x40]);
64 *size = (1, 1);
65 }
66 Ok(())
67 }
68 "text" => text_element(node, doc, size, warnings),
69 "alignment" => {
70 let position = attr(node, "position")
72 .or_else(|| attr(node, "poistion"))
73 .unwrap_or("left");
74 doc.align(match position {
75 "left" => Align::Left,
76 "center" => Align::Center,
77 "right" => Align::Right,
78 other => return Err(bad(node, "position", other)),
79 });
80 Ok(())
81 }
82 "feed" => {
83 if let Some(lines) = number::<u8>(node, "line")?.filter(|&n| n > 0) {
84 doc.feed_lines(lines);
85 } else if let Some(dots) = number::<u32>(node, "unit")?.filter(|&n| n > 0) {
86 doc.feed_lines(u8::try_from(dots.div_ceil(24)).unwrap_or(u8::MAX));
88 }
89 Ok(())
90 }
91 "cutpaper" => {
92 let partial = match attr(node, "type").unwrap_or("full") {
93 "full" => false,
94 "partial" => true,
95 other => return Err(bad(node, "type", other)),
96 };
97 doc.cut(partial, flag(node, "feed")?.unwrap_or(true));
98 Ok(())
99 }
100 "barcode" => barcode(node, doc),
101 "qrcode" => {
102 let model = match attr(node, "model").unwrap_or("model2") {
103 "model1" => 1,
104 "model2" => 2,
105 other => return Err(bad(node, "model", other)),
106 };
107 let level = match attr(node, "level").unwrap_or("level_l") {
108 "level_l" => QrLevel::L,
109 "level_m" => QrLevel::M,
110 "level_q" => QrLevel::Q,
111 "level_h" => QrLevel::H,
112 other => return Err(bad(node, "level", other)),
113 };
114 let cell = number::<u8>(node, "cell")?.unwrap_or(3);
115 doc.qr(model, cell, level, &unescape(&text(node)));
116 Ok(())
117 }
118 "pdf417" => {
119 doc.other_symbol(48, &unescape(&text(node)));
120 Ok(())
121 }
122 "bitimage" | "bitImage" => {
123 let width = number::<u32>(node, "width")?.unwrap_or(0);
124 let height = number::<u32>(node, "height")?.unwrap_or(0).min(2400);
125 let encoded: String = text(node).chars().filter(|c| !c.is_whitespace()).collect();
126 let data = hardless_core::base64::decode(&encoded)
127 .map_err(|e| DialectError::new(format!("<bitimage> data is not base64: {e}")))?;
128 if width > 0 && height > 0 {
129 doc.raster(width, height, &data);
130 }
131 Ok(())
132 }
133 "peripheral" => {
134 let pin5 = match number::<u8>(node, "channel")?.unwrap_or(1) {
135 1 => false,
136 2 => true,
137 other => return Err(bad(node, "channel", &other.to_string())),
138 };
139 let on = number::<u32>(node, "on")?.unwrap_or(200);
140 let off = number::<u32>(node, "off")?.unwrap_or(200);
141 doc.pulse(pin5, on, off);
142 Ok(())
143 }
144 "rawdata" => {
145 let encoded: String = text(node).chars().filter(|c| !c.is_whitespace()).collect();
146 let data = hardless_core::base64::decode(&encoded)
147 .map_err(|e| DialectError::new(format!("<rawdata> is not base64: {e}")))?;
148 doc.raw(&data);
149 Ok(())
150 }
151 "logo" => {
152 let number = number::<u8>(node, "number")?.unwrap_or(1);
153 warnings.push(format!("logo {number}: the emulator stores no logos"));
154 doc.raw(&[
156 0x1D, 0x28, 0x4C, 0x06, 0x00, 0x30, 0x45, 0x30, number, 0x01, 0x01,
157 ]);
158 Ok(())
159 }
160 "sound" | "sound_with_setting" => {
161 warnings.push(format!(
162 "<{}>: the emulated printer has no buzzer",
163 node.tag_name().name()
164 ));
165 Ok(())
166 }
167 "ruledline" | "holdprint" => {
168 warnings.push(format!(
169 "<{}> is not reproduced by the emulator",
170 node.tag_name().name()
171 ));
172 Ok(())
173 }
174 other => Err(DialectError::new(format!(
175 "<{other}> is not a webPRNT request element"
176 ))),
177 }
178}
179
180fn text_element(
181 node: Node<'_, '_>,
182 doc: &mut EscPos,
183 size: &mut (u8, u8),
184 warnings: &mut Vec<String>,
185) -> Result<(), DialectError> {
186 if let Some(font) = attr(node, "font") {
187 match font {
188 "font_a" => doc.font_b(false),
189 "font_b" => doc.font_b(true),
190 other => return Err(bad(node, "font", other)),
191 }
192 }
193 if let Some(on) = flag(node, "emphasis")? {
194 doc.bold(on);
195 }
196 if let Some(on) = flag(node, "underline")? {
197 doc.underline(u8::from(on));
198 }
199 let width = number::<u8>(node, "width")?.map_or(size.0, |w| w.clamp(1, 6));
200 let height = number::<u8>(node, "height")?.map_or(size.1, |h| h.clamp(1, 6));
201 if (width, height) != *size {
202 doc.size(width, height);
203 *size = (width, height);
204 }
205 for name in ["invert", "characterspace", "linespace"] {
206 if attr(node, name).is_some() {
207 warnings.push(format!("<text {name}>: not reproduced by the emulator"));
208 }
209 }
210 let content = unescape(&text(node));
213 doc.text(&String::from_utf8_lossy(&content));
214 Ok(())
215}
216
217fn barcode(node: Node<'_, '_>, doc: &mut EscPos) -> Result<(), DialectError> {
218 let m = match attr(node, "symbology").unwrap_or("UPCE") {
220 "UPCA" => 65,
221 "UPCE" => 66,
222 "JAN13" => 67,
223 "JAN8" => 68,
224 "Code39" => 69,
225 "ITF" => 70,
226 "NW7" => 71,
227 "Code93" => 72,
228 "Code128" => 73,
229 other => return Err(bad(node, "symbology", other)),
230 };
231 let module = match attr(node, "width").unwrap_or("width3") {
232 "width2" => 2,
233 "width3" => 3,
234 "width4" => 4,
235 mode => match mode
236 .strip_prefix("width_mode")
237 .and_then(|n| n.parse::<u8>().ok())
238 {
239 Some(n @ 1..=9) => 2 + (n - 1) / 2,
240 _ => return Err(bad(node, "width", mode)),
241 },
242 };
243 let height = number::<u8>(node, "height")?.unwrap_or(162);
244 let hri = if flag(node, "hri")?.unwrap_or(true) {
245 Hri::Below
246 } else {
247 Hri::None
248 };
249 let mut data = unescape(&text(node));
250 if m == 73 && !data.starts_with(b"{") {
251 data.splice(0..0, *b"{B");
253 }
254 doc.barcode(m, &data, hri, module, height);
255 Ok(())
256}
257
258pub fn asb(printer: &PrinterStatus) -> [u8; 11] {
262 let mut bytes = [0u8; 11];
263 bytes[0] = 0x23;
264 bytes[1] = 0x86;
265 if printer.drawer_pin3_high {
266 bytes[2] |= 0x04;
267 }
268 if !printer.online {
269 bytes[2] |= 0x08;
270 }
271 if printer.cover_open {
272 bytes[2] |= 0x20;
273 }
274 if printer.paper_near_end {
275 bytes[5] |= 0x04;
276 }
277 if printer.paper_end {
278 bytes[5] |= 0x08;
279 }
280 bytes
281}
282
283pub fn response(outcome: &Outcome, printer: &PrinterStatus) -> String {
294 let code = match outcome {
295 Outcome::Printed => code::SUCCESS,
296 Outcome::Timeout => code::BUSY,
297 _ => code::OFFLINE,
298 };
299 let status: String = asb(printer).iter().map(|b| format!("{b:02X}")).collect();
300 format!(
301 r#"<?xml version="1.0" encoding="UTF-8"?><root><success>{}</success><code>{code}</code><status>{status}</status></root>"#,
302 outcome.printed()
303 )
304}