Printers

TSPL labels

TSC TSPL labels for TSC, Xprinter, Rollo and Munbyn printers: UTF-8 text and blocks, Code 128, GS1-128, EAN-13, ITF-14, QR and Data Matrix, boxes and pictures, and the printer's status before a job goes to port 9100.

Version 1.0.0 · 7 functions · 2 crates

#printing#labels#barcodes#shipping#devices

Functions

What use tspl.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use tspl_label, tspl_job from tspl (Quillo 0.6).

tspl_label(fields: list, options: any) → stror an error

A label as TSPL, written for people (pictures shown by their size). Each field is a map at x, y (millimetres, or units: "dots"): { text, size: 3 (mm, the nearest resident font) or font: "3" | "0" | "ROMAN.TTF" with multiply, rotate: 90, block: 60 (wrapped in a box that wide), block_height, align, reverse }, { barcode: "code128" | "gs1_128" | "code39" | "ean13" | "upca" | "itf" | "itf14" | "qr" | "datamatrix", data, height, module, readable, ecc, magnification }, { box: true, width, height, thickness, radius }, { line: true, width, thickness, vertical }, { bar: true, width, height }, { image: "logo.png", width, dither } or { raw: "..." }. options: { dpi: 203, width: 100, height: 50, media: "gap" | "mark" | "continuous", gap: 3, speed, density: 0-15, quantity, copies, after: "tear" | "cut" | "peel" }.

tspl_job(fields: list, options: any) → stror an error

The label's bytes in base64.

tspl_write(fields: list, options: any, path: str) → intor an error

Writes the label to a file or a USB printer's device (/dev/usb/lp0). Gives the bytes written.

tspl_send(fields: list, options: any, host: str, timeout_ms: int) → intor an error

Sends the label to a printer's raw port (host or host:port, 9100 when not given). Gives the bytes sent.

tspl_status(host: str, timeout_ms: int) → anyor an error

Asks the printer how it is (<ESC>!?, one byte back) and reads the answer: { ready, head_open, paper_jam, paper_out, ribbon_out, paused, printing, other_error, code }.

tspl_command(action: str, options: any) → stror an error

Commands to set the printer up once: the sensor calibrated for the label size (GAPDETECT), or SELFTEST, or the settings saved; action is "calibrate", "selftest", "feed" or "home".

tspl_send_commands(commands: str, host: str, timeout_ms: int) → intor an error

Sends TSPL commands written as text (from tspl_command, or by hand) to a printer. Gives the bytes sent.

Get it

$ quillo install tspl               # tspl.qwer into this folder$ quillo install tspl --example     # and its example, tspl.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/tspl.qwer$ curl -O https://quillolang.org/plugins/files/tspl.qwe$ curl -O https://quillolang.org/plugins/files/tspl_sample.png

Then, in your program, all of it or only what you call:

main.qwe
use tspl.qweruse tspl_label, tspl_job from tspl      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }base64 = "0.22"

Example

tspl.qwe
// A small online shop's parcel labels on a TSPL printer (a TSC TE210, a Rollo or a Munbyn):// a 100 × 150 mm label with the sender's logo, the address in a block, the parcel's GS1-128// and a QR code, and the printer's status before printing.////   quillo run tspl.qwe      then open http://localhost:8080/label//   PRINTER_HOST=192.168.1.62 quillo run tspl.qwe      and http://localhost:8080/printuse "tspl.qwer"let OPTIONS = { dpi: 203, width: 100, height: 150, gap: 3, speed: 4, density: 8 }fn parcel(p) = [    { image: "tspl_sample.png", x: 4, y: 4, width: 36 },    { text: "Ship to:", x: 4, y: 22, size: 3 },    { text: p.to, x: 4, y: 28, size: 5, block: 92, block_height: 30 },    { line: true, x: 2, y: 62, width: 96, thickness: 0.5 },    { text: `Order "${p.order}"`, x: 4, y: 66, size: 4, reverse: true },    { barcode: "qr", data: `https://shop.example/track/${p.order}`, x: 70, y: 64, magnification: 4 },    { barcode: "gs1_128", data: p.sscc, x: 4, y: 100, height: 25, module: 3 },    { box: true, x: 1, y: 1, width: 98, height: 146, thickness: 0.5, radius: 2 },]let PARCEL = { to: "Anna Kowalska, ul. Długa 5/12, 00-238 Warszawa, Polska", order: "A-5531", sscc: "(00)095011015300000010" }route GET /label = respond(200, tspl_label(parcel(PARCEL), OPTIONS)?, { "content-type": "text/plain; charset=utf-8" })route GET /shelf = tspl_label([    { text: "Aisle 4 · Bin 12", x: 2, y: 2, size: 6 },    { barcode: "ean13", data: "4006381333931", x: 4, y: 12, height: 10 },    { barcode: "itf14", data: "1540014128876", x: 40, y: 12, height: 10, module: 2 },    { barcode: "datamatrix", data: "BIN-04-12", x: 80, y: 2, size: 12 },], { width: 100, height: 30, media: "mark", copies: 3 })route GET /saved = { bytes: tspl_write(parcel(PARCEL), OPTIONS, "data/parcel.tspl")?, calibrate: tspl_command("calibrate", OPTIONS)? }route GET /print {    let host = env("PRINTER_HOST", null) ?? reply 503 "set PRINTER_HOST to the printer's address (host or host:port)"    let status = tspl_status(host, 3000)?    if !status.ready { reply 409 { error: "the printer is not ready", status: status } }    { sent: tspl_send(parcel(PARCEL), OPTIONS, host, 5000)?, status: status }}
$ quillo run tspl.qwe$ curl 'localhost:8080/print'$ curl 'localhost:8080/label'$ curl 'localhost:8080/shelf'

It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.

Source

tspl.qwer
//! TSC TSPL and TSPL2 labels (TSC, and the many printers that speak it: Xprinter, Munbyn, iDPRT, Rollo) built from fields in millimetres: text in UTF-8, text blocks, Code 128, GS1-128, Code 39, EAN-13, UPC-A, ITF-14, QR and Data Matrix, bars, boxes and pictures, size, gap or black mark, speed, density and copies; sent to port 9100 or written to a USB printer.//! version: 1.0.0//!//! [dependencies]//! image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }//! base64 = "0.22"use serde_json::{json, Value};/// A TSPL string: a double quote inside is written `\["]`; line breaks are dropped.fn quoted(s: &str) -> String {    let clean: String = s.chars().filter(|c| *c != '\n' && *c != '\r').collect();    format!("\"{}\"", clean.replace('"', "\\[\"]"))}/// The resident font and multiplier nearest to `target` dots high (fonts 1–5: 12, 20, 24, 32 and 48 dots at 203 dpi).fn font_for(target: i64, dpi: i64) -> (&'static str, i64) {    let k = if dpi >= 250 { 1.5 } else { 1.0 };    let fonts = [("1", 12.0), ("2", 20.0), ("3", 24.0), ("4", 32.0), ("5", 48.0)];    let mut best = ("3", 1, f64::MAX);    for (f, h) in fonts {        for m in 1..=10 {            let d = (h * k * m as f64 - target as f64).abs();            if d < best.2 {                best = (f, m, d);            }        }    }    (best.0, best.1)}fn rotation(v: &Value) -> i64 {    match v.as_i64().unwrap_or(0).rem_euclid(360) {        r @ (90 | 180 | 270) => r,        _ => 0,    }}fn gs1_fixed(ai2: &str) -> Option<usize> {    Some(match ai2 {        "00" => 20,        "01" | "02" | "03" => 16,        "04" => 18,        "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19" => 8,        "20" => 4,        "31" | "32" | "33" | "34" | "35" | "36" => 10,        "41" => 16,        _ => return None,    })}/// `(01)09501101530003(10)LOT7` as Code 128 manual mode (`128M`): `!105` start C, `!102` FNC1, `!100` to B, `!099` to C.fn gs1_128m(s: &str) -> Result<String, String> {    let mut tokens: Vec<Option<char>> = vec![];    let mut rest = s.trim();    if !rest.starts_with('(') {        return Err("GS1 data is written with its AIs in brackets: (01)09501101530003(10)LOT7".into());    }    while let Some(r) = rest.strip_prefix('(') {        let end = r.find(')').ok_or("a ( without its )")?;        let ai = &r[..end];        if ai.len() < 2 || !ai.chars().all(|c| c.is_ascii_digit()) {            return Err(format!("({}) is not an application identifier", ai));        }        let r = &r[end + 1..];        let next = r.find('(').unwrap_or(r.len());        let data = &r[..next];        if let Some(total) = gs1_fixed(&ai[..2]) {            if ai.len() + data.len() != total {                return Err(format!("({}) takes {} characters, not {}", ai, total - ai.len(), data.len()));            }        }        if !data.chars().all(|c| ('!'..='~').contains(&c)) || data.contains('!') {            return Err(format!("({}) {:?}: GS1 data is printable ASCII without !", ai, data));        }        tokens.extend(ai.chars().map(Some));        tokens.extend(data.chars().map(Some));        rest = &r[next..];        if gs1_fixed(&ai[..2]).is_none() && !rest.is_empty() {            tokens.push(None);        }    }    let digit = |t: &Option<char>| matches!(t, Some(c) if c.is_ascii_digit());    let run = |i: usize| tokens[i..].iter().take_while(|t| digit(t)).count();    let mut out = String::from("!105!102");    let mut in_c = true;    let mut i = 0;    while i < tokens.len() {        let r = run(i);        if in_c {            if r >= 2 {                out.push(tokens[i].unwrap());                out.push(tokens[i + 1].unwrap());                i += 2;            } else if tokens[i].is_none() {                out.push_str("!102");                i += 1;            } else {                out.push_str("!100");                in_c = false;            }            continue;        }        if r >= 4 && r % 2 == 0 {            out.push_str("!099");            in_c = true;            continue;        }        match tokens[i] {            None => out.push_str("!102"),            Some(c) => out.push(c),        }        i += 1;    }    Ok(out)}fn mod10(d: &str) -> u32 {    let s: u32 = d.chars().rev().enumerate().map(|(i, c)| c.to_digit(10).unwrap_or(0) * if i % 2 == 0 { 3 } else { 1 }).sum();    (10 - s % 10) % 10}fn build(fields: &[Value], options: &Value) -> Result<Out, String> {    if fields.len() > 2000 {        return Err("at most 2,000 fields on a label".into());    }    let dpi = options["dpi"].as_i64().unwrap_or(203);    let dpmm = if dpi >= 250 { 12.0 } else { 8.0 };    let units = options["units"].as_str().unwrap_or("mm").to_string();    let d = |v: &Value, def: f64| to_dots(v, def, dpmm, &units);    let mm = |v: &Value, def: f64| -> String {        let x = v.as_f64().unwrap_or(def);        let x = match units.as_str() {            "dots" => x / dpmm,            "in" | "inch" | "inches" => x * 25.4,            _ => x,        };        format!("{}", (x * 10.0).round() / 10.0)    };    let mut o = Out::new();    o.s(&format!("SIZE {} mm,{} mm\r\n", mm(&options["width"], 100.0), mm(&options["height"], 50.0)));    match options["media"].as_str().unwrap_or("gap") {        "gap" => o.s(&format!("GAP {} mm,0 mm\r\n", mm(&options["gap"], 3.0))),        "mark" | "black_mark" => o.s(&format!("BLINE {} mm,{} mm\r\n", mm(&options["gap"], 3.0), mm(&options["mark_offset"], 0.0))),        "continuous" => o.s("GAP 0,0\r\n"),        other => return Err(format!("media {:?}: gap, mark or continuous", other)),    }    if let Some(s) = options["speed"].as_f64() {        o.s(&format!("SPEED {}\r\n", s.clamp(1.0, 18.0)));    }    if let Some(dn) = options["density"].as_i64() {        o.s(&format!("DENSITY {}\r\n", dn.clamp(0, 15)));    }    o.s(&format!("DIRECTION {},0\r\n", if options["upside_down"] == true { 0 } else { 1 }));    o.s("REFERENCE 0,0\r\n");    if options["tear"] == true || options["after"].as_str() == Some("tear") {        o.s("SET TEAR ON\r\n");    }    if options["after"].as_str() == Some("cut") {        o.s("SET CUTTER 1\r\n");    }    if options["after"].as_str() == Some("peel") {        o.s("SET PEEL ON\r\n");    }    o.s("CODEPAGE UTF-8\r\n");    o.s("CLS\r\n");    for f in fields {        if let Some(raw) = f["raw"].as_str() {            o.t(raw, raw.as_bytes());            o.s("\r\n");            continue;        }        let x = d(&f["x"], 0.0);        let y = d(&f["y"], 0.0);        let rot = rotation(&f["rotate"]);        if !f["text"].is_null() {            let text = text_of(&f["text"]);            let (font, m) = match f["font"].as_str() {                Some(fnt) => (fnt, f["multiply"].as_i64().unwrap_or(1).clamp(1, 10)),                None => font_for(d(&f["size"], 3.0).max(1), dpi),            };            let (font, m) = (font.to_string(), m);            if !f["block"].is_null() {                let align = match f["align"].as_str().unwrap_or("left") {                    "center" | "centre" => 2,                    "right" => 3,                    _ => 1,                };                let line = format!("BLOCK {},{},{},{},{},{},{},{},0,{},{}\r\n", x, y, d(&f["block"], 50.0), d(&f["block_height"], 20.0), quoted(&font), rot, m, m, align, quoted(&text));                o.t(&line, line.as_bytes());            } else {                let line = format!("TEXT {},{},{},{},{},{},{}\r\n", x, y, quoted(&font), rot, m, m, quoted(&text));                o.t(&line, line.as_bytes());            }            if f["reverse"] == true {                // REVERSE inverts an area: the text's box, from its size.                let h = d(&f["size"], 3.0).max(8);                let w = (text.chars().count() as i64 * h * 3 / 5).max(8);                o.s(&format!("REVERSE {},{},{},{}\r\n", x.saturating_sub(2), y.saturating_sub(2), w + 4, h + 4));            }        } else if let Some(kind) = f["barcode"].as_str() {            let data = text_of(&f["data"]);            if data.is_empty() {                return Err("a barcode needs its `data`".into());            }            let h = d(&f["height"], 15.0).max(1);            let narrow = f["module"].as_i64().unwrap_or(2).clamp(1, 10);            let wide = f["wide"].as_i64().unwrap_or(narrow * 2).clamp(1, 30);            let readable = if f["readable"] == false { 0 } else { 2 };            let digits = data.chars().all(|c| c.is_ascii_digit());            let (code, content) = match kind {                "code128" => {                    if !data.chars().all(|c| (' '..='~').contains(&c)) {                        return Err("Code 128 takes printable ASCII".into());                    }                    ("128", data.clone())                }                "gs1_128" | "gs1-128" | "ean128" => ("128M", gs1_128m(&data)?),                "code39" => ("39", data.clone()),                "ean13" if digits && (data.len() == 12 || data.len() == 13) => ("EAN13", data[..12].to_string()),                "upca" if digits && (data.len() == 11 || data.len() == 12) => ("UPCA", data[..11].to_string()),                "itf14" if digits && (data.len() == 13 || data.len() == 14) => ("ITF14", format!("{}{}", &data[..13], mod10(&data[..13]))),                "itf" if digits && data.len() % 2 == 0 => ("25", data.clone()),                "ean13" | "upca" | "itf14" | "itf" => return Err(format!("{}: {:?} has the wrong digits", kind, data)),                "qr" => {                    let ecc = match f["ecc"].as_str().unwrap_or("M") {                        "L" => "L",                        "Q" => "Q",                        "H" => "H",                        _ => "M",                    };                    let cell = f["magnification"].as_i64().unwrap_or(4).clamp(1, 10);                    let line = format!("QRCODE {},{},{},{},A,{},M2,{}\r\n", x, y, ecc, cell, rot, quoted(&data));                    o.t(&line, line.as_bytes());                    continue;                }                "datamatrix" => {                    let size = d(&f["size"], 15.0).max(10);                    let line = format!("DMATRIX {},{},{},{},x{},{}\r\n", x, y, size, size, f["module"].as_i64().unwrap_or(4).clamp(1, 20), quoted(&data));                    o.t(&line, line.as_bytes());                    continue;                }                other => return Err(format!("unknown barcode {:?}: code128, gs1_128, code39, ean13, upca, itf, itf14, qr or datamatrix", other)),            };            let line = format!("BARCODE {},{},{},{},{},{},{},{},{}\r\n", x, y, quoted(code), h, if kind.starts_with("gs1") || kind == "ean128" { 0 } else { readable }, rot, narrow, wide, quoted(&content));            o.t(&line, line.as_bytes());            if (kind.starts_with("gs1") || kind == "ean128") && f["readable"] != false {                let th = (h / 6).clamp(12, 48);                let (font, m) = font_for(th, dpi);                o.s(&format!("TEXT {},{},\"{}\",0,{},{},{}\r\n", x, y + h + 4, font, m, m, quoted(&data)));            }        } else if f["box"] == true {            let t = d(&f["thickness"], 0.3).max(1);            let r = d(&f["radius"], 0.0);            o.s(&format!("BOX {},{},{},{},{}{}\r\n", x, y, x + d(&f["width"], 10.0), y + d(&f["height"], 10.0), t, if r > 0 { format!(",{}", r) } else { String::new() }));        } else if f["line"] == true || f["bar"] == true {            let t = d(&f["thickness"], 0.3).max(1);            let (w, h) = if f["bar"] == true { (d(&f["width"], 10.0).max(1), d(&f["height"], 10.0).max(1)) } else if f["vertical"] == true { (t, d(&f["height"], 10.0).max(1)) } else { (d(&f["width"], 10.0).max(1), t) };            o.s(&format!("BAR {},{},{},{}\r\n", x, y, w, h));        } else if let Some(path) = f["image"].as_str() {            let w = if f["width"].is_null() { None } else { Some(d(&f["width"], 0.0) as u32) };            let (pw, ph, rows) = bitmap(path, w, f["threshold"].as_u64().unwrap_or(128).clamp(1, 255) as u8, f["dither"] == true)?;            // TSPL's BITMAP: a 0 bit prints, a 1 bit is white; mode 0 overwrites.            let (bpr, data) = pack_rows(pw, &rows, false);            o.s(&format!("BITMAP {},{},{},{},0,", x, y, bpr, ph));            o.blob(&data);            o.s("\r\n");        } else {            return Err(format!("a field is text, barcode, box, line, bar, image or raw: {}", f));        }    }    let sets = options["quantity"].as_i64().unwrap_or(1).clamp(1, 999_999_999);    let copies = options["copies"].as_i64().unwrap_or(1).clamp(1, 999_999_999);    o.s(&format!("PRINT {},{}\r\n", sets, copies));    Ok(o)}/// A label as TSPL, written for people (pictures shown by their size). Each field is a map at `x`, `y` (millimetres, or `units: "dots"`):/// `{ text, size: 3 (mm, the nearest resident font) or font: "3" | "0" | "ROMAN.TTF" with multiply, rotate: 90, block: 60 (wrapped in a box that wide), block_height, align, reverse }`,/// `{ barcode: "code128" | "gs1_128" | "code39" | "ean13" | "upca" | "itf" | "itf14" | "qr" | "datamatrix", data, height, module, readable, ecc, magnification }`,/// `{ box: true, width, height, thickness, radius }`, `{ line: true, width, thickness, vertical }`, `{ bar: true, width, height }`, `{ image: "logo.png", width, dither }` or `{ raw: "..." }`./// `options`: `{ dpi: 203, width: 100, height: 50, media: "gap" | "mark" | "continuous", gap: 3, speed, density: 0-15, quantity, copies, after: "tear" | "cut" | "peel" }`.pub fn tspl_label(fields: Vec<serde_json::Value>, options: serde_json::Value) -> Result<String, String> {    Ok(build(&fields, &options)?.view())}/// The label's bytes in base64.pub fn tspl_job(fields: Vec<serde_json::Value>, options: serde_json::Value) -> Result<String, String> {    Ok(b64(&build(&fields, &options)?.bytes))}/// Writes the label to a file or a USB printer's device (`/dev/usb/lp0`). Gives the bytes written.pub fn tspl_write(fields: Vec<serde_json::Value>, options: serde_json::Value, path: &str) -> Result<i64, String> {    write_bytes(&build(&fields, &options)?.bytes, path)}/// Sends the label to a printer's raw port (`host` or `host:port`, 9100 when not given). Gives the bytes sent.pub fn tspl_send(fields: Vec<serde_json::Value>, options: serde_json::Value, host: &str, timeout_ms: i64) -> Result<i64, String> {    send_bytes(&build(&fields, &options)?.bytes, host, timeout_ms)}/// Asks the printer how it is (`<ESC>!?`, one byte back) and reads the answer: `{ ready, head_open, paper_jam, paper_out, ribbon_out, paused, printing, other_error, code }`.pub fn tspl_status(host: &str, timeout_ms: i64) -> Result<serde_json::Value, String> {    use std::io::{Read, Write};    let mut s = connect(host, timeout_ms)?;    s.write_all(b"\x1b!?").map_err(|e| e.to_string())?;    let mut b = [0u8; 1];    s.read_exact(&mut b).map_err(|e| format!("no status from the printer: {}", e))?;    let c = b[0];    Ok(json!({        "ready": c == 0, "code": c,        "head_open": c & 0x01 != 0, "paper_jam": c & 0x02 != 0, "paper_out": c & 0x04 != 0, "ribbon_out": c & 0x08 != 0,        "paused": c & 0x10 != 0, "printing": c & 0x20 != 0, "other_error": c & 0x80 != 0,    }))}/// Commands to set the printer up once: the sensor calibrated for the label size (`GAPDETECT`), or `SELFTEST`, or the settings saved; `action` is "calibrate", "selftest", "feed" or "home".pub fn tspl_command(action: &str, options: serde_json::Value) -> Result<String, String> {    let size = build(&[], &options)?.view();    let head: String = size.lines().take(2).map(|l| format!("{}\r\n", l)).collect();    Ok(match action {        "calibrate" => format!("{}GAPDETECT\r\n", head),        "selftest" => "SELFTEST\r\n".into(),        "feed" => format!("{}FORMFEED\r\n", head),        "home" => format!("{}HOME\r\n", head),        other => return Err(format!("{:?}: calibrate, selftest, feed or home", other)),    })}/// Sends TSPL commands written as text (from `tspl_command`, or by hand) to a printer. Gives the bytes sent.pub fn tspl_send_commands(commands: &str, host: &str, timeout_ms: i64) -> Result<i64, String> {    send_bytes(commands.as_bytes(), host, timeout_ms)}// ---- Shared by the printer plugins: the job's bytes and a readable copy, pictures as bits, the network./// A job's bytes, and the same job written for people: control bytes as `<ESC>`, graphics as `<N bytes>`.struct Out {    bytes: Vec<u8>,    text: String,}fn control_name(c: u8) -> String {    match c {        0x00 => "<NUL>".into(),        0x02 => "<STX>".into(),        0x03 => "<ETX>".into(),        0x0a => "\n".into(),        0x0c => "<FF>".into(),        0x0d => "<CR>".into(),        0x17 => "<ETB>".into(),        0x18 => "<CAN>".into(),        0x1b => "<ESC>".into(),        0x1e => "<RS>".into(),        c if c < 0x20 || c == 0x7f => format!("<{:02X}>", c),        c => (c as char).to_string(),    }}impl Out {    fn new() -> Out {        Out { bytes: vec![], text: String::new() }    }    /// Command bytes, shown as they are with control bytes named.    fn b(&mut self, data: &[u8]) {        self.bytes.extend_from_slice(data);        for c in data {            self.text.push_str(&control_name(*c));        }    }    /// ASCII command text.    fn s(&mut self, s: &str) {        self.b(s.as_bytes());    }    /// Text already encoded for the printer (`enc`), shown as written.    fn t(&mut self, shown: &str, enc: &[u8]) {        self.bytes.extend_from_slice(enc);        for c in shown.chars() {            if (c as u32) < 0x20 {                self.text.push_str(&control_name(c as u8));            } else {                self.text.push(c);            }        }    }    /// Text for the readable copy only (a line break between commands), not sent.    fn note(&mut self, s: &str) {        self.text.push_str(s);    }    /// The job for people: CR LF line ends shown as plain line breaks.    fn view(&self) -> String {        self.text.replace("<CR>\n", "\n")    }    /// Binary data (a picture), shown by its size.    fn blob(&mut self, data: &[u8]) {        self.bytes.extend_from_slice(data);        self.text.push_str(&format!("<{} bytes>", data.len()));    }}/// Dots per millimetre for a resolution in dpi.fn dots_per_mm(dpi: i64) -> f64 {    dpi.clamp(100, 1200) as f64 / 25.4}/// A length given in `units` ("mm", the default, "in" or "dots") as dots.fn to_dots(v: &Value, default: f64, dpmm: f64, units: &str) -> i64 {    let x = v.as_f64().unwrap_or(default);    let d = match units {        "dots" => x,        "in" | "inch" | "inches" => x * 25.4 * dpmm,        _ => x * dpmm,    };    d.round().max(0.0) as i64}/// A picture as rows of bits, true for black: scaled to `width` dots when given, dithered or cut at a threshold, transparency as white.fn bitmap(path: &str, width: Option<u32>, threshold: u8, dither: bool) -> Result<(usize, usize, Vec<Vec<bool>>), String> {    let img = image::open(path).map_err(|e| format!("{}: {}", path, e))?;    let img = match width {        Some(w) if w > 0 && w != img.width() => {            let h = ((img.height() as f64) * (w as f64) / (img.width() as f64)).round().max(1.0) as u32;            img.resize_exact(w, h, image::imageops::FilterType::Triangle)        }        _ => img,    };    let rgba = img.to_rgba8();    let (w, h) = (rgba.width() as usize, rgba.height() as usize);    if w * h > 8_000_000 {        return Err("the picture is over 8 million dots: give a width".into());    }    let mut lum: Vec<f32> = rgba        .pixels()        .map(|p| {            let a = p[3] as f32 / 255.0;            (0.299 * p[0] as f32 + 0.587 * p[1] as f32 + 0.114 * p[2] as f32) * a + 255.0 * (1.0 - a)        })        .collect();    let mut rows = vec![vec![false; w]; h];    for y in 0..h {        for x in 0..w {            let old = lum[y * w + x];            let black = old < threshold as f32;            rows[y][x] = black;            if dither {                let err = old - if black { 0.0 } else { 255.0 };                for (dx, dy, f) in [(1isize, 0usize, 7.0f32 / 16.0), (-1, 1, 3.0 / 16.0), (0, 1, 5.0 / 16.0), (1, 1, 1.0 / 16.0)] {                    let nx = x as isize + dx;                    if nx >= 0 && (nx as usize) < w && y + dy < h {                        lum[(y + dy) * w + nx as usize] += err * f;                    }                }            }        }    }    Ok((w, h, rows))}/// Rows of bits packed 8 dots to a byte, the first dot in the high bit; `black_is_one` false for printers where 0 prints.fn pack_rows(w: usize, rows: &[Vec<bool>], black_is_one: bool) -> (usize, Vec<u8>) {    let bpr = (w + 7) / 8;    let mut out = Vec::with_capacity(bpr * rows.len());    for r in rows {        for b in 0..bpr {            let mut byte = 0u8;            for bit in 0..8 {                let x = b * 8 + bit;                let black = x < w && r[x];                if black == black_is_one {                    byte |= 0x80 >> bit;                }            }            out.push(byte);        }    }    (bpr, out)}/// `host`, `host:port` or `[v6]:port` as an address; `port` when none is written.fn printer_addr(host: &str, port: u16) -> Result<std::net::SocketAddr, String> {    use std::net::ToSocketAddrs;    let h = host.trim();    let with_port = if h.starts_with('[') {        if h.contains("]:") { h.to_string() } else { format!("{}:{}", h, port) }    } else if h.matches(':').count() == 1 {        h.to_string()    } else if h.contains(':') {        format!("[{}]:{}", h, port)    } else {        format!("{}:{}", h, port)    };    with_port.to_socket_addrs().map_err(|e| format!("{}: {}", host, e))?.next().ok_or(format!("{} has no address", host))}fn connect(host: &str, timeout_ms: i64) -> Result<std::net::TcpStream, String> {    let t = std::time::Duration::from_millis(timeout_ms.clamp(100, 120_000) as u64);    let addr = printer_addr(host, 9100)?;    let s = std::net::TcpStream::connect_timeout(&addr, t).map_err(|e| format!("cannot reach the printer at {}: {}", addr, e))?;    s.set_write_timeout(Some(t)).map_err(|e| e.to_string())?;    s.set_read_timeout(Some(t)).map_err(|e| e.to_string())?;    Ok(s)}/// Sends bytes to a printer's raw port and closes it.fn send_bytes(bytes: &[u8], host: &str, timeout_ms: i64) -> Result<i64, String> {    use std::io::Write;    let mut s = connect(host, timeout_ms)?;    s.write_all(bytes).map_err(|e| format!("sending to the printer: {}", e))?;    s.flush().map_err(|e| e.to_string())?;    Ok(bytes.len() as i64)}/// Writes bytes to a file or a device (`/dev/usb/lp0`), folders made for a file.fn write_bytes(bytes: &[u8], path: &str) -> Result<i64, String> {    use std::io::Write;    let p = std::path::Path::new(path);    let device = path.starts_with("/dev/");    if !device {        if let Some(dir) = p.parent() {            if !dir.as_os_str().is_empty() {                std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;            }        }    }    let mut f = std::fs::OpenOptions::new().write(true).create(!device).truncate(!device).open(p).map_err(|e| format!("{}: {}", path, e))?;    f.write_all(bytes).map_err(|e| format!("{}: {}", path, e))?;    Ok(bytes.len() as i64)}fn b64(bytes: &[u8]) -> String {    use base64::Engine;    base64::engine::general_purpose::STANDARD.encode(bytes)}fn text_of(v: &Value) -> String {    match v {        Value::String(s) => s.clone(),        Value::Null => String::new(),        other => other.to_string(),    }}
Zebra ZPL labels zpl.qwer · 1.0.0Zebra ZPL II labels from fields in millimetres: UTF-8 text, Code 128, GS1-128, QR, Data Matrix and other barcodes, boxes, images as compressed graphics, serial numbers and printer settings, sent to port 9100.printinglabelsbarcodesshippingdevices8 views · 8 downloadsDatamax DPL labels dpl.qwer · 1.0.0Honeywell and Datamax-O'Neil DPL labels from fields placed in millimetres from the top left: resident fonts, Code 128, EAN-13, QR and Data Matrix, lines and boxes, heat, speed and quantity, sent to port 9100.printinglabelsbarcodesdevices7 views · 9 downloadsEPL2 labels epl.qwer · 1.0.0Zebra and Eltron EPL2 labels from fields in millimetres: text in the resident fonts, Code 128, EAN-13, Code 39 and QR barcodes, lines, boxes and pictures, with label size, gap, speed and copies, sent to port 9100.printinglabelsbarcodesdevices6 views · 5 downloadsGodex EZPL labels ezpl.qwer · 1.0.0Godex EZPL labels for G500, RT200 and other Godex printers: text in the resident fonts, Code 128, EAN-13, Code 39 and QR codes, lines and boxes, label size, darkness, speed and copies, sent to port 9100.printinglabelsbarcodesdevices6 views · 8 downloadsIntermec IPL labels ipl.qwer · 1.0.0Intermec IPL labels for EasyCoder, PM4i and PF2i printers: a format saved with its text, Code 128, SSCC, UPC/EAN, QR and Data Matrix fields, lines and boxes, filled with data for each print, sent to port 9100.printinglabelsbarcodesdevices7 views · 7 downloadsSATO SBPL labels sbpl.qwer · 1.0.0SATO SBPL labels for CL4NX, CT4-LX and other SATO printers: text in the built-in fonts, Code 128 and GS1-128 with the subsets chosen, EAN-13, Code 39 and QR, lines and frames, speed, darkness and quantity, sent to port 9100.printinglabelsbarcodesdevices8 views · 7 downloads
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On TSPL labels: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.