1use roxmltree::Node;
9
10use crate::escpos::{unescape, Align, EscPos, Hri, QrLevel};
11use crate::xml::{self, attr, bad, child, elements, flag, number, text};
12use crate::{DialectError, Outcome, PrintRequest, PrinterStatus};
13
14pub const NAMESPACE: &str = "http://www.epson-pos.com/schemas/2011/03/epos-print";
16pub const SOAP_NAMESPACE: &str = "http://schemas.xmlsoap.org/soap/envelope/";
18
19pub mod status {
21 pub const NO_RESPONSE: u32 = 0x0000_0001;
23 pub const PRINT_SUCCESS: u32 = 0x0000_0002;
25 pub const DRAWER_KICK: u32 = 0x0000_0004;
27 pub const OFFLINE: u32 = 0x0000_0008;
29 pub const COVER_OPEN: u32 = 0x0000_0020;
31 pub const RECEIPT_NEAR_END: u32 = 0x0002_0000;
33 pub const RECEIPT_END: u32 = 0x0008_0000;
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub struct EposRequest {
40 pub print: PrintRequest,
42 pub devid: Option<String>,
44 pub timeout_ms: Option<u64>,
46 pub print_job_id: Option<String>,
48}
49
50pub fn parse(document: &str) -> Result<EposRequest, DialectError> {
63 let doc = xml::parse(document)?;
64 let root = doc.root_element();
65 let mut request = EposRequest::default();
66 let body = match root.tag_name().name() {
67 "epos-print" => root,
68 "Envelope" => {
69 if let Some(parameter) = child(root, "Header").and_then(|h| child(h, "parameter")) {
70 request.devid = child(parameter, "devid").map(|n| text(n).trim().to_owned());
71 request.timeout_ms = child(parameter, "timeout")
72 .map(|n| text(n).trim().parse())
73 .transpose()
74 .map_err(|_| DialectError::new("<timeout> must be a number"))?;
75 request.print_job_id =
76 child(parameter, "printjobid").map(|n| text(n).trim().to_owned());
77 }
78 child(root, "Body")
79 .and_then(|b| child(b, "epos-print"))
80 .ok_or_else(|| DialectError::new("the SOAP body has no <epos-print> element"))?
81 }
82 other => {
83 return Err(DialectError::new(format!(
84 "expected a SOAP envelope or <epos-print>, not <{other}>"
85 )))
86 }
87 };
88 request.print = translate(body)?;
89 Ok(request)
90}
91
92#[derive(Clone, Copy)]
94struct TextState {
95 width: u8,
96 height: u8,
97}
98
99fn translate(epos_print: Node<'_, '_>) -> Result<PrintRequest, DialectError> {
100 let mut doc = EscPos::new();
101 let mut warnings = Vec::new();
102 let mut state = TextState {
103 width: 1,
104 height: 1,
105 };
106 for node in elements(epos_print) {
107 element(node, &mut doc, &mut state, &mut warnings)?;
108 }
109 Ok(PrintRequest {
110 escpos: doc.finish(),
111 warnings,
112 })
113}
114
115fn element(
116 node: Node<'_, '_>,
117 doc: &mut EscPos,
118 state: &mut TextState,
119 warnings: &mut Vec<String>,
120) -> Result<(), DialectError> {
121 match node.tag_name().name() {
122 "text" => text_element(node, doc, state, warnings),
123 "feed" => {
124 if let Some(lines) = number::<u8>(node, "line")? {
125 doc.feed_lines(lines);
126 } else if let Some(dots) = number::<u32>(node, "unit")? {
127 doc.feed_lines(u8::try_from(dots.div_ceil(30)).unwrap_or(u8::MAX));
129 } else if attr(node, "pos").is_none() {
130 doc.line_feed();
131 }
132 Ok(())
133 }
134 "image" => image(node, doc),
135 "logo" => {
136 let k1 = number::<u8>(node, "key1")?.unwrap_or(32);
137 let k2 = number::<u8>(node, "key2")?.unwrap_or(32);
138 warnings.push(format!("NV logo {k1}/{k2}: the emulator stores no logos"));
139 doc.raw(&[0x1D, 0x28, 0x4C, 0x06, 0x00, 0x30, 0x45, k1, k2, 0x01, 0x01]);
141 Ok(())
142 }
143 "barcode" => barcode(node, doc, warnings),
144 "symbol" => symbol(node, doc),
145 "cut" => {
146 match attr(node, "type").unwrap_or("feed") {
147 "no_feed" => doc.cut(true, false),
148 "feed" | "reserve" => doc.cut(true, true),
149 other => return Err(bad(node, "type", other)),
150 }
151 Ok(())
152 }
153 "pulse" => {
154 let pin5 = match attr(node, "drawer").unwrap_or("drawer_1") {
155 "drawer_1" => false,
156 "drawer_2" => true,
157 other => return Err(bad(node, "drawer", other)),
158 };
159 let ms = match attr(node, "time").unwrap_or("pulse_100") {
160 "pulse_100" => 100,
161 "pulse_200" => 200,
162 "pulse_300" => 300,
163 "pulse_400" => 400,
164 "pulse_500" => 500,
165 other => return Err(bad(node, "time", other)),
166 };
167 doc.pulse(pin5, ms, ms);
168 Ok(())
169 }
170 "command" => {
171 let hex: String = text(node).chars().filter(|c| !c.is_whitespace()).collect();
172 let bytes = decode_hex(&hex)
173 .ok_or_else(|| DialectError::new("<command> must be hexadecimal bytes"))?;
174 doc.raw(&bytes);
175 Ok(())
176 }
177 "sound" => {
178 warnings.push("<sound>: the emulated printer has no buzzer".into());
179 Ok(())
180 }
181 "page" => {
182 warnings.push("<page>: page mode is printed in standard mode".into());
183 for inner in elements(node) {
184 match inner.tag_name().name() {
185 "area" | "direction" | "position" | "line" | "rectangle" => {}
186 _ => element(inner, doc, state, warnings)?,
187 }
188 }
189 Ok(())
190 }
191 "layout" | "hline" | "vline" | "recovery" | "reset" => {
192 warnings.push(format!(
193 "<{}> is not reproduced by the emulator",
194 node.tag_name().name()
195 ));
196 Ok(())
197 }
198 other => Err(DialectError::new(format!(
199 "<{other}> is not an epos-print element"
200 ))),
201 }
202}
203
204fn text_element(
205 node: Node<'_, '_>,
206 doc: &mut EscPos,
207 state: &mut TextState,
208 warnings: &mut Vec<String>,
209) -> Result<(), DialectError> {
210 if let Some(align) = attr(node, "align") {
211 doc.align(align_value(node, align)?);
212 }
213 if let Some(font) = attr(node, "font") {
214 match font {
215 "font_a" => doc.font_b(false),
216 "font_b" | "font_c" | "font_d" | "font_e" => doc.font_b(true),
217 other => return Err(bad(node, "font", other)),
218 }
219 }
220 if let Some(em) = flag(node, "em")? {
221 doc.bold(em);
222 }
223 if let Some(ul) = flag(node, "ul")? {
224 doc.underline(u8::from(ul));
225 }
226 let mut size = *state;
227 if let Some(dw) = flag(node, "dw")? {
228 size.width = if dw { 2 } else { 1 };
229 }
230 if let Some(dh) = flag(node, "dh")? {
231 size.height = if dh { 2 } else { 1 };
232 }
233 if let Some(w) = number::<u8>(node, "width")? {
234 size.width = w.clamp(1, 8);
235 }
236 if let Some(h) = number::<u8>(node, "height")? {
237 size.height = h.clamp(1, 8);
238 }
239 if (size.width, size.height) != (state.width, state.height) {
240 doc.size(size.width, size.height);
241 *state = size;
242 }
243 for name in ["reverse", "rotate", "x", "linespc"] {
244 if attr(node, name).is_some() {
245 warnings.push(format!("<text {name}>: not reproduced by the emulator"));
246 }
247 }
248 if let Some(lang) = attr(node, "lang") {
249 if !matches!(lang, "en" | "de" | "fr" | "it" | "es" | "multi") {
250 warnings.push(format!(
251 "<text lang=\"{lang}\">: only Latin code pages are emulated"
252 ));
253 }
254 }
255 doc.text(&text(node));
256 Ok(())
257}
258
259fn align_value(node: Node<'_, '_>, value: &str) -> Result<Align, DialectError> {
260 match value {
261 "left" => Ok(Align::Left),
262 "center" => Ok(Align::Center),
263 "right" => Ok(Align::Right),
264 other => Err(bad(node, "align", other)),
265 }
266}
267
268fn image(node: Node<'_, '_>, doc: &mut EscPos) -> Result<(), DialectError> {
269 let width =
270 number::<u32>(node, "width")?.ok_or_else(|| DialectError::new("<image> needs a width"))?;
271 let height = number::<u32>(node, "height")?
272 .ok_or_else(|| DialectError::new("<image> needs a height"))?;
273 if let Some(align) = attr(node, "align") {
274 doc.align(align_value(node, align)?);
275 }
276 let encoded: String = text(node).chars().filter(|c| !c.is_whitespace()).collect();
277 let data = hardless_core_base64(&encoded)?;
278 let data = match attr(node, "mode").unwrap_or("mono") {
279 "mono" => data,
280 "gray16" => gray16_to_mono(&data, width, height),
283 other => return Err(bad(node, "mode", other)),
284 };
285 doc.raster(width, height, &data);
286 Ok(())
287}
288
289fn gray16_to_mono(data: &[u8], width: u32, height: u32) -> Vec<u8> {
290 let row_bytes = width.div_ceil(8) as usize;
291 let mut out = vec![0u8; row_bytes * height as usize];
292 let src_row = width.div_ceil(2) as usize;
293 for y in 0..height as usize {
294 for x in 0..width as usize {
295 let Some(&byte) = data.get(y * src_row + x / 2) else {
296 continue;
297 };
298 let level = if x % 2 == 0 { byte >> 4 } else { byte & 0x0F };
299 if level >= 8 {
300 out[y * row_bytes + x / 8] |= 0x80 >> (x % 8);
301 }
302 }
303 }
304 out
305}
306
307fn barcode(
308 node: Node<'_, '_>,
309 doc: &mut EscPos,
310 warnings: &mut Vec<String>,
311) -> Result<(), DialectError> {
312 let kind = attr(node, "type").ok_or_else(|| DialectError::new("<barcode> needs a type"))?;
313 let m = match kind {
315 "upc_a" => 65,
316 "upc_e" => 66,
317 "ean13" | "jan13" => 67,
318 "ean8" | "jan8" => 68,
319 "code39" => 69,
320 "itf" => 70,
321 "codabar" => 71,
322 "code93" => 72,
323 "code128" | "code128_auto" => 73,
324 "gs1_128" => 74,
325 "gs1_databar_omnidirectional" => 75,
326 "gs1_databar_truncated" => 76,
327 "gs1_databar_limited" => 77,
328 "gs1_databar_expanded" => 78,
329 other => return Err(bad(node, "type", other)),
330 };
331 let hri = match attr(node, "hri").unwrap_or("none") {
332 "none" => Hri::None,
333 "above" => Hri::Above,
334 "below" => Hri::Below,
335 "both" => Hri::Both,
336 other => return Err(bad(node, "hri", other)),
337 };
338 if let Some(align) = attr(node, "align") {
339 doc.align(align_value(node, align)?);
340 }
341 if attr(node, "rotate").is_some() {
342 warnings.push("<barcode rotate>: not reproduced by the emulator".into());
343 }
344 let module = number::<u8>(node, "width")?.unwrap_or(3);
345 let height = number::<u8>(node, "height")?.unwrap_or(162);
346 let mut data = unescape(&text(node));
347 if m == 73 && !data.starts_with(b"{") {
348 data.splice(0..0, *b"{B");
350 }
351 doc.barcode(m, &data, hri, module, height);
352 Ok(())
353}
354
355fn symbol(node: Node<'_, '_>, doc: &mut EscPos) -> Result<(), DialectError> {
356 let kind = attr(node, "type").ok_or_else(|| DialectError::new("<symbol> needs a type"))?;
357 let data = unescape(&text(node));
358 let module = number::<u8>(node, "width")?.unwrap_or(3);
359 let qr = |model: u8, doc: &mut EscPos| -> Result<(), DialectError> {
360 let level = match attr(node, "level").unwrap_or("level_m") {
361 "level_l" => QrLevel::L,
362 "level_m" | "default" => QrLevel::M,
363 "level_q" => QrLevel::Q,
364 "level_h" => QrLevel::H,
365 other => return Err(bad(node, "level", other)),
366 };
367 doc.qr(model, module, level, &data);
368 Ok(())
369 };
370 match kind {
371 "qrcode_model_1" => qr(1, doc),
372 "qrcode_model_2" => qr(2, doc),
373 k if k.starts_with("pdf417") => {
375 doc.other_symbol(48, &data);
376 Ok(())
377 }
378 k if k.starts_with("maxicode") => {
379 doc.other_symbol(50, &data);
380 Ok(())
381 }
382 k if k.starts_with("gs1_databar") => {
383 doc.other_symbol(51, &data);
384 Ok(())
385 }
386 k if k.starts_with("azteccode") => {
387 doc.other_symbol(53, &data);
388 Ok(())
389 }
390 k if k.starts_with("datamatrix") || k == "qrcode_micro" => {
391 doc.other_symbol(54, &data);
392 Ok(())
393 }
394 other => Err(bad(node, "type", other)),
395 }
396}
397
398fn decode_hex(hex: &str) -> Option<Vec<u8>> {
399 if !hex.len().is_multiple_of(2) {
400 return None;
401 }
402 (0..hex.len())
403 .step_by(2)
404 .map(|i| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok())
405 .collect()
406}
407
408fn hardless_core_base64(text: &str) -> Result<Vec<u8>, DialectError> {
409 hardless_core::base64::decode(text)
410 .map_err(|e| DialectError::new(format!("<image> data is not base64: {e}")))
411}
412
413pub fn status_bits(outcome: &Outcome, printer: &PrinterStatus) -> u32 {
421 if matches!(outcome, Outcome::BadDocument(_) | Outcome::DeviceNotFound) {
422 return 0;
423 }
424 let mut bits = 0;
425 if outcome.printed() {
426 bits |= status::PRINT_SUCCESS;
427 }
428 if printer.drawer_pin3_high {
429 bits |= status::DRAWER_KICK;
430 }
431 if !printer.online {
432 bits |= status::OFFLINE;
433 }
434 if printer.cover_open {
435 bits |= status::COVER_OPEN;
436 }
437 if printer.paper_near_end {
438 bits |= status::RECEIPT_NEAR_END;
439 }
440 if printer.paper_end {
441 bits |= status::RECEIPT_END;
442 }
443 bits
444}
445
446pub fn error_code(outcome: &Outcome) -> &'static str {
448 match outcome {
449 Outcome::Printed => "",
450 Outcome::PaperEnd => "EPTR_REC_EMPTY",
451 Outcome::CoverOpen => "EPTR_COVER_OPEN",
452 Outcome::Timeout => "EX_TIMEOUT",
453 Outcome::BadDocument(_) => "SchemaError",
454 Outcome::DeviceNotFound => "DeviceNotFound",
455 }
456}
457
458pub fn response(outcome: &Outcome, printer: &PrinterStatus, print_job_id: Option<&str>) -> String {
467 let header = print_job_id.map_or(String::new(), |id| {
468 format!(
469 r#"<s:Header><parameter xmlns="{NAMESPACE}"><printjobid>{}</printjobid></parameter></s:Header>"#,
470 xml::escape(id)
471 )
472 });
473 format!(
474 r#"<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="{SOAP_NAMESPACE}">{header}<s:Body><response success="{}" code="{}" status="{}" battery="0" xmlns="{NAMESPACE}"/></s:Body></s:Envelope>"#,
475 outcome.printed(),
476 error_code(outcome),
477 status_bits(outcome, printer)
478 )
479}