Skip to main content

hardless_http_dialects/
webprnt.rs

1//! The WebPRNT-compatible dialect (PRD section 9.1): `POST /StarWebPRNT/SendMessage` (or
2//! `/p/<deviceId>/StarWebPRNT/SendMessage`) with a `<root>` request document.
3//!
4//! Implemented from the public "Star webPRNT User's Manual" (request and response elements)
5//! and the public "ASB (Automatic Status Back) Function Specifications Manual" (status bytes).
6//! No vendor SDK code is used.
7
8use 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
14/// Response `code` values from the manual.
15pub mod code {
16    /// Success.
17    pub const SUCCESS: &str = "0";
18    /// The printer is offline or in an error state.
19    pub const OFFLINE: &str = "1100";
20    /// The printer is busy; send the data again.
21    pub const BUSY: &str = "2001";
22}
23
24/// Parses a request document. Elements and attribute values the manual does not define are
25/// an error.
26///
27/// ```
28/// use hardless_http_dialects::webprnt;
29/// let req = webprnt::parse(r#"<root><alignment position="center"/>
30///   <text emphasis="true">Hello\x0a</text><cutpaper type="partial" feed="true"/></root>"#).unwrap();
31/// assert_eq!(req.escpos, b"\x1b@\x1ba\x01\x1bE\x01Hello\n\x1dVB\x00");
32/// ```
33pub 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            // The manual's attribute table spells it "poistion"; accept both.
71            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                // 24 dots is one 3 mm line.
87                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            // GS ( L function 69 (print NV graphics); the printer reports it as unknown.
155            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    // Control characters arrive escaped as `\xNN`; text is UTF-8 and is printed in the code
211    // page that has its characters, so `codepage` and `international` need no translation.
212    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    // GS k function B symbol numbers.
219    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        // ESC/POS CODE128 data names its code set; plain text is code set B.
252        data.splice(0..0, *b"{B");
253    }
254    doc.barcode(m, &data, hri, module, height);
255    Ok(())
256}
257
258/// The Star Automatic Status Back bytes for the response `status`: two header bytes, then
259/// printer status bytes as in the ASB manual (status 1: drawer switch bit 2, offline bit 3,
260/// cover bit 5; status 4: paper near end bit 2, paper end bit 3).
261pub 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
283/// The response document.
284///
285/// ```
286/// use hardless_http_dialects::{webprnt, Outcome, PrinterStatus};
287/// let ok = PrinterStatus { online: true, ..Default::default() };
288/// assert_eq!(
289///     webprnt::response(&Outcome::Printed, &ok),
290///     r#"<?xml version="1.0" encoding="UTF-8"?><root><success>true</success><code>0</code><status>2386000000000000000000</status></root>"#
291/// );
292/// ```
293pub 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}