Printers

ZPL label preview

ZPL labels drawn as PNG pictures without a printer, with text, boxes, Code 128, EAN-13 and QR codes and graphics, and read back field by field for tests.

Version 1.0.0 · 4 functions · 6 crates

#printing#labels#previews#barcodes#testing

Functions

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

zpl_preview_png(zpl: str, path: str, options: any) → anyor an error

Draws a ZPL label as a PNG file at path (folders made). options: { dpi: 203, width, height (dots; else ^PW and ^LL, else 4 × 6 in), label: 0 } (which label of several). Gives { path, width, height, black (the share of black dots), fields, unsupported }: commands it could not draw are listed, never silently dropped.

zpl_preview_data_url(zpl: str, options: any) → stror an error

The label as a data:image/png;base64,... address, for an <img> on a page.

zpl_fields(zpl: str, options: any) → listor an error

What the label holds, field by field: { type: "text" | "code128" | "ean13" | "upca" | "qr" | "box" | "circle" | "graphic", x, y, width, height, data } in dots, for tests that a label says what it should.

zpl_label_count(zpl: str) → int

How many labels (^XA … ^XZ) the ZPL holds.

Get it

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

Or with curl:

$ curl -O https://quillolang.org/plugins/files/zpl_preview.qwer$ curl -O https://quillolang.org/plugins/files/zpl_preview.qwe$ curl -O https://quillolang.org/plugins/files/zpl_preview_sample.zpl

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

main.qwe
use zpl_preview.qweruse zpl_preview_png, zpl_preview_data_url from zpl_preview      // 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"] }ab_glyph = "0.2"dejavu = "2.37"qrcode = { version = "0.14", default-features = false }base64 = "0.22"miniz_oxide = "0.8"

Example

zpl_preview.qwe
// Checking labels without a printer: a bin label's ZPL drawn as a picture, shown on a page,// and read back field by field, as a test of what it says.////   quillo run zpl_preview.qwe      then open http://localhost:8080/ and http://localhost:8080/label.pnguse "zpl_preview.qwer"let ZPL = read_file("zpl_preview_sample.zpl")?route GET /label.png {    zpl_preview_png(ZPL, "data/label.png", { dpi: 203 })?    file("data/label.png")}route GET / = <html><body style="font-family: sans-serif; background: #eee">    <h2>Bin label, 4 × 3 in at 203 dpi</h2>    <img src={zpl_preview_data_url(ZPL, {})?} style="background: white; border: 1px solid #999; width: 600px" /></body></html>// What the label says: its text and barcodes as data, and what the preview could not draw.route GET /check {    let fields = zpl_fields(ZPL, {})?    let info = zpl_preview_png(ZPL, "data/label.png", {})?    {        labels: zpl_label_count(ZPL),        size: `${info.width} × ${info.height}`,        unsupported: info.unsupported,        barcodes: fields | filter((f) => f.type != "text" && f.type != "box") | map((f) => `${f.type}: ${f.data ?? f.name ?? ""}`),        text: fields | filter((f) => f.type == "text") | map((f) => f.data),    }}
$ quillo run zpl_preview.qwe$ curl 'localhost:8080/check'$ curl 'localhost:8080/'

Source

zpl_preview.qwer
//! ZPL labels drawn as PNG pictures without a printer, to check them on screen or in tests: text (^A, ^CF, ^FB blocks, ^FH), boxes and circles, Code 128, EAN-13, UPC-A and QR barcodes, ^GF graphics (hex, compressed and Z64), stored ~DG graphics, reversed fields; and the fields read back as data.//! version: 1.0.0//!//! [dependencies]//! image = { version = "0.25", default-features = false, features = ["png"] }//! ab_glyph = "0.2"//! dejavu = "2.37"//! qrcode = { version = "0.14", default-features = false }//! base64 = "0.22"//! miniz_oxide = "0.8"use ab_glyph::{Font, FontRef, PxScale, ScaleFont};use base64::Engine;use serde_json::{json, Value};use std::collections::HashMap;const CODE128: [&str; 107] = [    "212222", "222122", "222221", "121223", "121322", "131222", "122213", "122312", "132212", "221213", "221312", "231212", "112232", "122132", "122231", "113222",    "123122", "123221", "223211", "221132", "221231", "213212", "223112", "312131", "311222", "321122", "321221", "312212", "322112", "322211", "212123", "212321",    "232121", "111323", "131123", "131321", "112313", "132113", "132311", "211313", "231113", "231311", "112133", "112331", "132131", "113123", "113321", "133121",    "313121", "211331", "231131", "213113", "213311", "213131", "311123", "311321", "331121", "312113", "312311", "332111", "314111", "221411", "431111", "111224",    "111422", "121124", "121421", "141122", "141221", "112214", "112412", "122114", "122411", "142112", "142211", "241211", "221114", "413111", "241112", "134111",    "111242", "121142", "121241", "114212", "124112", "124211", "411212", "421112", "421211", "212141", "214121", "412121", "111143", "111341", "131141", "114113",    "114311", "411113", "411311", "113141", "114131", "311141", "411131", "211412", "211214", "211232", "2331112",];/// A field drawn on its own, before it is turned and placed: true is black.struct Bits {    w: usize,    h: usize,    px: Vec<bool>,}impl Bits {    fn new(w: usize, h: usize) -> Bits {        Bits { w, h, px: vec![false; w * h] }    }    fn set(&mut self, x: i64, y: i64) {        if x >= 0 && y >= 0 && (x as usize) < self.w && (y as usize) < self.h {            self.px[y as usize * self.w + x as usize] = true;        }    }    fn rect(&mut self, x: i64, y: i64, w: i64, h: i64) {        for yy in y..y + h {            for xx in x..x + w {                self.set(xx, yy);            }        }    }    /// Turned clockwise by `o`: N, R (90), I (180) or B (270).    fn turned(self, o: char) -> Bits {        let (w, h) = (self.w, self.h);        match o {            'R' => {                let mut b = Bits::new(h, w);                for y in 0..h {                    for x in 0..w {                        if self.px[y * w + x] {                            b.px[x * h + (h - 1 - y)] = true;                        }                    }                }                b            }            'I' => {                let mut b = Bits::new(w, h);                for i in 0..w * h {                    b.px[w * h - 1 - i] = self.px[i];                }                b            }            'B' => {                let mut b = Bits::new(h, w);                for y in 0..h {                    for x in 0..w {                        if self.px[y * w + x] {                            b.px[(w - 1 - x) * h + y] = true;                        }                    }                }                b            }            _ => self,        }    }}struct Canvas {    w: usize,    h: usize,    px: Vec<bool>,}impl Canvas {    fn put(&mut self, x: i64, y: i64, xor: bool) {        if x >= 0 && y >= 0 && (x as usize) < self.w && (y as usize) < self.h {            let i = y as usize * self.w + x as usize;            self.px[i] = if xor { !self.px[i] } else { true };        }    }    fn clear(&mut self, x: i64, y: i64) {        if x >= 0 && y >= 0 && (x as usize) < self.w && (y as usize) < self.h {            self.px[y as usize * self.w + x as usize] = false;        }    }    fn blit(&mut self, b: &Bits, x0: i64, y0: i64, xor: bool) {        for y in 0..b.h {            for x in 0..b.w {                if b.px[y * b.w + x] {                    self.put(x0 + x as i64, y0 + y as i64, xor);                }            }        }    }}fn font() -> FontRef<'static> {    FontRef::try_from_slice(dejavu::sans_condensed::bold()).expect("the built-in font")}/// One line of text drawn `h` dots high and `w` wide a character (as ^A's height and width).fn text_line(f: &FontRef, s: &str, h: f32, w: f32) -> Bits {    let scale = PxScale { x: w.max(1.0) * 1.1, y: h.max(1.0) };    let sf = f.as_scaled(scale);    let width: f32 = s.chars().map(|c| sf.h_advance(f.glyph_id(c))).sum();    let mut b = Bits::new(width.ceil().max(1.0) as usize + 1, h.ceil().max(1.0) as usize + 1);    let mut caret = 0.0;    for c in s.chars() {        let id = f.glyph_id(c);        let g = id.with_scale_and_position(scale, ab_glyph::point(caret, sf.ascent()));        caret += sf.h_advance(id);        if let Some(og) = f.outline_glyph(g) {            let bb = og.px_bounds();            og.draw(|gx, gy, cov| {                if cov > 0.5 {                    b.set(bb.min.x as i64 + gx as i64, bb.min.y as i64 + gy as i64);                }            });        }    }    b}fn text_width(f: &FontRef, s: &str, h: f32, w: f32) -> f32 {    let sf = f.as_scaled(PxScale { x: w.max(1.0) * 1.1, y: h.max(1.0) });    s.chars().map(|c| sf.h_advance(f.glyph_id(c))).sum()}/// Code 128 values from ^BC data: invocation codes (>5, >6, >8, >:, ...) in mode N, subsets picked here in mode A, AIs in brackets in mode D. Gives the values and the human-readable text.fn code128_values(data: &str, mode: char) -> Result<(Vec<u8>, String), String> {    let mut vals: Vec<u8> = vec![];    let mut human = String::new();    let chars: Vec<char> = data.chars().collect();    if mode == 'A' || mode == 'D' || mode == 'U' {        // Encode plainly: digits pairs in C, the rest in B, FNC1 first for GS1.        let gs1 = mode != 'A';        let digits: Vec<char> = if gs1 { chars.iter().filter(|c| !matches!(c, '(' | ')' | ' ')).cloned().collect() } else { chars.clone() };        human = data.to_string();        let mut i = 0;        let run = |i: usize| digits[i..].iter().take_while(|c| c.is_ascii_digit()).count();        let mut c_set = gs1 || run(0) >= 4 || (run(0) == digits.len() && digits.len() % 2 == 0);        vals.push(if c_set { 105 } else { 104 });        if gs1 {            vals.push(102);        }        while i < digits.len() {            let r = run(i);            if c_set {                if r >= 2 {                    vals.push((digits[i] as u8 - b'0') * 10 + (digits[i + 1] as u8 - b'0'));                    i += 2;                    continue;                }                vals.push(100);                c_set = false;                continue;            }            if r >= 4 && r % 2 == 0 {                vals.push(99);                c_set = true;                continue;            }            let c = digits[i];            if !(' '..='~').contains(&c) {                return Err(format!("{:?} is not in Code 128's set B", c));            }            vals.push(c as u8 - 32);            i += 1;        }        return Ok((vals, human));    }    // Mode N: subset B unless the data starts with >9 (A), >: (B) or >; (C).    let mut set = 'B';    let mut i = 0;    if chars.len() >= 2 && chars[0] == '>' && matches!(chars[1], '9' | ':' | ';') {        set = match chars[1] {            '9' => 'A',            ';' => 'C',            _ => 'B',        };        i = 2;    }    vals.push(match set {        'A' => 103,        'C' => 105,        _ => 104,    });    while i < chars.len() {        let c = chars[i];        if c == '>' && i + 1 < chars.len() {            let v = match chars[i + 1] {                '<' => {                    human.push('>');                    if set == 'C' { None } else { Some(30) }                }                '0' => Some(30),                '=' => {                    human.push('~');                    Some(94)                }                d @ '1'..='8' => Some(94 + (d as u8 - b'0')),                _ => None,            };            let v = v.ok_or(format!("bad invocation code >{}", chars[i + 1]))?;            vals.push(v);            match v {                99 => set = 'C',                100 if set != 'B' => set = 'B',                101 if set != 'A' => set = 'A',                _ => {}            }            i += 2;            continue;        }        match set {            'C' => {                if i + 1 >= chars.len() || !c.is_ascii_digit() || !chars[i + 1].is_ascii_digit() {                    return Err("subset C takes digits in pairs".into());                }                vals.push((c as u8 - b'0') * 10 + (chars[i + 1] as u8 - b'0'));                human.push(c);                human.push(chars[i + 1]);                i += 2;            }            'A' => {                let b = c as u32;                if b > 95 {                    return Err(format!("{:?} is not in subset A", c));                }                vals.push(if b < 32 { (b + 64) as u8 } else { (b - 32) as u8 });                human.push(c);                i += 1;            }            _ => {                if !(' '..='\u{7f}').contains(&c) {                    return Err(format!("{:?} is not in subset B", c));                }                vals.push(c as u8 - 32);                human.push(c);                i += 1;            }        }    }    Ok((vals, human))}/// Bar widths in modules (bar, space, bar, ...) with the check value and stop.fn code128_modules(vals: &[u8]) -> Vec<bool> {    let mut sum = vals[0] as u32;    for (i, v) in vals.iter().enumerate().skip(1) {        sum += i as u32 * *v as u32;    }    let mut all = vals.to_vec();    all.push((sum % 103) as u8);    all.push(106);    let mut m = vec![];    for v in all {        for (k, d) in CODE128[v as usize].chars().enumerate() {            for _ in 0..d.to_digit(10).unwrap() {                m.push(k % 2 == 0);            }        }    }    m}fn ean13_modules(d: &[u8]) -> Vec<bool> {    const L: [&str; 10] = ["0001101", "0011001", "0010011", "0111101", "0100011", "0110001", "0101111", "0111011", "0110111", "0001011"];    const PARITY: [&str; 10] = ["LLLLLL", "LLGLGG", "LLGGLG", "LLGGGL", "LGLLGG", "LGGLLG", "LGGGLL", "LGLGLG", "LGLGGL", "LGGLGL"];    let r = |n: u8| -> String { L[n as usize].chars().map(|c| if c == '0' { '1' } else { '0' }).collect() };    let g = |n: u8| -> String { r(n).chars().rev().collect() };    let mut s = String::from("101");    for (k, p) in PARITY[d[0] as usize].chars().enumerate() {        s.push_str(&if p == 'L' { L[d[k + 1] as usize].to_string() } else { g(d[k + 1]) });    }    s.push_str("01010");    for k in 7..13 {        s.push_str(&r(d[k]));    }    s.push_str("101");    s.chars().map(|c| c == '1').collect()}fn check_digit(d: &[u8]) -> u8 {    let s: u32 = d.iter().rev().enumerate().map(|(i, x)| *x as u32 * if i % 2 == 0 { 3 } else { 1 }).sum();    ((10 - s % 10) % 10) as u8}/// A one-dimensional barcode with its text under it (or above).fn linear(f: &FontRef, modules: &[bool], module: i64, h: i64, human: Option<(&str, bool)>) -> Bits {    let w = modules.len() as i64 * module;    let th = (module * 9).clamp(12, 60);    let extra = if human.is_some() { th + 4 } else { 0 };    let mut b = Bits::new(w.max(1) as usize, (h + extra).max(1) as usize);    let top = if matches!(human, Some((_, true))) { extra } else { 0 };    for (i, on) in modules.iter().enumerate() {        if *on {            b.rect(i as i64 * module, top, module, h);        }    }    if let Some((t, above)) = human {        let line = text_line(f, t, th as f32, th as f32 * 0.6);        let x0 = ((w - line.w as i64) / 2).max(0);        let y0 = if above { 0 } else { h + 3 };        for y in 0..line.h {            for x in 0..line.w {                if line.px[y * line.w + x] {                    b.set(x0 + x as i64, y0 + y as i64);                }            }        }    }    b}/// ^GF's ASCII hex, with ZPL's compression (G–Y, g–z counts, `,` `!` `:`), as rows of bytes.fn gf_ascii(data: &str, bpr: usize) -> Vec<u8> {    let mut out: Vec<u8> = vec![];    let mut row: Vec<u8> = vec![]; // hex digits of the row being read    let mut prev: Vec<u8> = vec![0; bpr * 2];    let mut count = 0usize;    let flush = |row: &mut Vec<u8>, out: &mut Vec<u8>, prev: &mut Vec<u8>| {        row.resize(bpr * 2, b'0');        for k in 0..bpr {            let hx = std::str::from_utf8(&row[k * 2..k * 2 + 2]).unwrap_or("00");            out.push(u8::from_str_radix(hx, 16).unwrap_or(0));        }        *prev = row.clone();        row.clear();    };    for c in data.bytes() {        match c {            b'G'..=b'Y' => count += (c - b'G' + 1) as usize,            b'g'..=b'z' => count += 20 * (c - b'g' + 1) as usize,            b',' => {                row.resize(bpr * 2, b'0');                flush(&mut row, &mut out, &mut prev);                count = 0;            }            b'!' => {                row.resize(bpr * 2, b'F');                flush(&mut row, &mut out, &mut prev);                count = 0;            }            b':' => {                if !row.is_empty() {                    flush(&mut row, &mut out, &mut prev);                }                row = prev.clone();                flush(&mut row, &mut out, &mut prev);                count = 0;            }            c if c.is_ascii_hexdigit() => {                let n = count.max(1);                count = 0;                for _ in 0..n {                    row.push(c.to_ascii_uppercase());                    if row.len() == bpr * 2 {                        flush(&mut row, &mut out, &mut prev);                    }                }            }            _ => {}        }    }    if !row.is_empty() {        flush(&mut row, &mut out, &mut prev);    }    out}/// A graphic's bytes from ^GF/~DG data: ASCII hex (compressed or not), or :Z64:/:B64: base64.fn graphic_bytes(data: &str, bpr: usize) -> Result<Vec<u8>, String> {    let d = data.trim();    if let Some(rest) = d.strip_prefix(":Z64:").or_else(|| d.strip_prefix(":B64:")) {        let b64 = rest.split(':').next().unwrap_or("");        let raw = base64::engine::general_purpose::STANDARD.decode(b64).map_err(|e| format!("^GF base64: {}", e))?;        if d.starts_with(":Z64:") {            return miniz_oxide::inflate::decompress_to_vec_zlib(&raw).map_err(|e| format!("^GF Z64: {:?}", e));        }        return Ok(raw);    }    Ok(gf_ascii(d, bpr))}fn graphic_bits(bytes: &[u8], bpr: usize, mx: i64, my: i64) -> Bits {    let rows = if bpr == 0 { 0 } else { bytes.len() / bpr };    let (mx, my) = (mx.clamp(1, 10), my.clamp(1, 10));    let mut b = Bits::new((bpr * 8) * mx as usize, rows * my as usize);    for r in 0..rows {        for x in 0..bpr * 8 {            if bytes[r * bpr + x / 8] & (0x80 >> (x % 8)) != 0 {                b.rect(x as i64 * mx, r as i64 * my, mx, my);            }        }    }    b}/// Words wrapped to `width` dots, `\&` and line breaks kept.fn wrap(f: &FontRef, s: &str, h: f32, w: f32, width: f32) -> Vec<String> {    let mut lines = vec![];    for para in s.split("\\&").flat_map(|p| p.split('\n')) {        let mut cur = String::new();        for word in para.split(' ') {            let trial = if cur.is_empty() { word.to_string() } else { format!("{} {}", cur, word) };            if text_width(f, &trial, h, w) <= width || cur.is_empty() {                cur = trial;            } else {                lines.push(cur);                cur = word.to_string();            }        }        lines.push(cur);    }    lines}struct State {    home: (i64, i64),    origin: (i64, i64),    baseline: bool,    font: (char, i64, i64, char),    default_font: (char, i64, i64),    block: Option<(i64, i64, i64, char)>,    reverse: bool,    label_reverse: bool,    hex: Option<char>,    by: (i64, f64, i64),    barcode: Option<(String, Vec<String>)>,    serial: bool,}/// Height and width in dots of Zebra's bitmap fonts at 203 dpi, for ^A without sizes.fn font_base(c: char) -> (i64, i64) {    match c {        'A' => (9, 5),        'B' => (11, 7),        'C' | 'D' => (18, 10),        'E' => (28, 15),        'F' => (26, 13),        'G' => (60, 40),        'H' => (21, 13),        _ => (15, 12),    }}fn decode_fh(s: &str, ind: char) -> String {    let mut bytes = vec![];    let cs: Vec<char> = s.chars().collect();    let mut i = 0;    while i < cs.len() {        if cs[i] == ind && i + 2 < cs.len() + 0 && cs[i + 1].is_ascii_hexdigit() && cs.get(i + 2).map_or(false, |c| c.is_ascii_hexdigit()) {            let h: String = cs[i + 1..i + 3].iter().collect();            bytes.push(u8::from_str_radix(&h, 16).unwrap());            i += 3;        } else {            let mut buf = [0u8; 4];            bytes.extend_from_slice(cs[i].encode_utf8(&mut buf).as_bytes());            i += 1;        }    }    String::from_utf8_lossy(&bytes).into_owned()}/// ZPL split into commands: (prefix, name, parameters), ^FD/^FX data kept whole up to the next ^.fn commands(zpl: &str) -> Vec<(char, String, String)> {    let cs: Vec<char> = zpl.chars().collect();    let mut out = vec![];    let mut i = 0;    while i < cs.len() {        let p = cs[i];        if p != '^' && p != '~' {            i += 1;            continue;        }        let mut name: String = cs[i + 1..(i + 3).min(cs.len())].iter().collect::<String>().to_uppercase();        let mut j = (i + 3).min(cs.len());        if name.starts_with('A') && name != "A@" {            // ^Af: the font is the next character, the rest are parameters.            name = format!("A{}", cs.get(i + 2).copied().unwrap_or('0').to_ascii_uppercase());        }        let data_cmd = matches!(name.as_str(), "FD" | "FX" | "SN" | "FV");        let start = j;        while j < cs.len() && cs[j] != '^' && (data_cmd || cs[j] != '~') {            j += 1;        }        let params: String = cs[start..j].iter().filter(|c| **c != '\r' && **c != '\n').collect();        out.push((p, name, params));        i = j;    }    out}struct Render {    canvas: Canvas,    fields: Vec<Value>,    unsupported: Vec<String>,}fn num(v: Option<&&str>, d: i64) -> i64 {    v.and_then(|s| s.trim().parse::<f64>().ok()).map(|f| f as i64).unwrap_or(d)}fn render(zpl: &str, options: &Value) -> Result<Render, String> {    if zpl.len() > 20_000_000 {        return Err("at most 20 MB of ZPL".into());    }    let want = options["label"].as_u64().unwrap_or(0) as usize;    let dpi = options["dpi"].as_i64().unwrap_or(203);    let dpmm = match dpi {        0..=152 => 6,        153..=250 => 8,        251..=450 => 12,        _ => 24,    };    let cmds = commands(zpl);    // The size: from the options, else ^PW and ^LL, else 4 × 6 inches.    let mut pw = None;    let mut ll = None;    for (_, n, p) in &cmds {        if n == "PW" {            pw = p.trim().parse::<i64>().ok();        }        if n == "LL" {            ll = p.split(',').next().and_then(|s| s.trim().parse::<i64>().ok());        }    }    let w = options["width"].as_i64().or(pw).unwrap_or(4 * 254 * dpmm / 10).clamp(8, 8000) as usize;    let h = options["height"].as_i64().or(ll).unwrap_or(6 * 254 * dpmm / 10).clamp(8, 16000) as usize;    let f = font();    let mut r = Render { canvas: Canvas { w, h, px: vec![false; w * h] }, fields: vec![], unsupported: vec![] };    let mut st = State {        home: (0, 0),        origin: (0, 0),        baseline: false,        font: ('0', 9 * dpmm / 8 * 3, 0, 'N'),        default_font: ('A', 9, 5),        block: None,        reverse: false,        label_reverse: false,        hex: None,        by: (2, 3.0, 10 * dpmm),        barcode: None,        serial: false,    };    let mut stored: HashMap<String, (usize, Vec<u8>)> = HashMap::new();    let mut label = 0usize;    let mut inside = false;    let mut field_font_set = false;    let mut field_orient = 'N';    let mut data: Option<String> = None;    let mut po_invert = false;    for (prefix, name, params) in &cmds {        let ps: Vec<&str> = params.split(',').collect();        if *prefix == '~' {            if name == "DG" {                // ~DGname,total,bytes_per_row,data                let mut it = params.splitn(4, ',');                let nm = it.next().unwrap_or("").trim().to_uppercase();                let total: usize = it.next().and_then(|s| s.trim().parse().ok()).unwrap_or(0);                let bpr: usize = it.next().and_then(|s| s.trim().parse().ok()).unwrap_or(1).max(1);                let bytes = graphic_bytes(it.next().unwrap_or(""), bpr)?;                let _ = total;                stored.insert(if nm.contains(':') { nm } else { format!("R:{}", nm) }, (bpr, bytes));            } else if !matches!(name.as_str(), "SD" | "JC" | "JA" | "TA" | "HS" | "PS" | "JR") {                r.unsupported.push(format!("~{}", name));            }            continue;        }        match name.as_str() {            "XA" => {                inside = true;                st.home = (0, 0);                st.label_reverse = false;            }            "XZ" => {                if inside {                    label += 1;                }                inside = false;                if label > want {                    break;                }            }            _ if !inside || label != want => {}            "FO" | "FT" => {                st.origin = (num(ps.first(), 0) + st.home.0, num(ps.get(1), 0) + st.home.1);                st.baseline = name == "FT";            }            "LH" => st.home = (num(ps.first(), 0), num(ps.get(1), 0)),            "LR" => st.label_reverse = params.trim().starts_with('Y'),            "PO" => po_invert = params.trim().starts_with('I'),            "CF" => {                let c = ps.first().and_then(|s| s.trim().chars().next()).unwrap_or('A').to_ascii_uppercase();                let (bh, bw) = font_base(c);                let hh = num(ps.get(1), if c == '0' { 0 } else { bh });                let ww = num(ps.get(2), 0);                st.default_font = (c, hh, if ww == 0 { if c == '0' { hh } else { bw * hh.max(1) / bh.max(1) } } else { ww });            }            n if n.starts_with('A') && n.len() == 2 => {                let c = n.chars().nth(1).unwrap();                let o = ps.first().and_then(|s| s.trim().chars().next()).unwrap_or('N').to_ascii_uppercase();                let (bh, bw) = font_base(c);                let hh = num(ps.get(1), if c == '0' || c == '@' { st.default_font.1.max(15) } else { bh });                let ww = num(ps.get(2), 0);                let ww = if ww == 0 { if c == '0' || c == '@' { hh } else { bw * hh / bh.max(1) } } else { ww };                st.font = (c, hh, ww, if "NRIB".contains(o) { o } else { 'N' });                field_font_set = true;            }            "FW" => field_orient = ps.first().and_then(|s| s.trim().chars().next()).unwrap_or('N'),            "FB" => st.block = Some((num(ps.first(), 0), num(ps.get(1), 1).max(1), num(ps.get(2), 0), ps.get(3).and_then(|s| s.trim().chars().next()).unwrap_or('L'))),            "FR" => st.reverse = true,            "FH" => st.hex = Some(params.trim().chars().next().unwrap_or('_')),            "FX" | "CI" | "PW" | "LL" | "PQ" | "PR" | "MD" | "MN" | "MT" | "MM" | "JU" | "LS" | "LT" | "PM" | "PF" | "XB" | "FN" | "DF" | "XF" | "MU" | "CC" | "CT" | "CD" | "JZ" | "PP" | "SZ" => {}            "BY" => {                st.by = (num(ps.first(), st.by.0).clamp(1, 10), ps.get(1).and_then(|s| s.trim().parse().ok()).unwrap_or(st.by.1), num(ps.get(2), st.by.2));            }            n if n.starts_with('B') => {                st.barcode = Some((n.to_string(), ps.iter().map(|s| s.trim().to_string()).collect()));            }            "FD" | "SN" | "FV" => {                let raw = if name == "SN" { ps.first().copied().unwrap_or("").to_string() } else { params.clone() };                let d = match st.hex {                    Some(ind) => decode_fh(&raw, ind),                    None => raw,                };                data = Some(d);                st.serial = name == "SN";            }            "GB" => {                let bw = num(ps.first(), 1).max(1);                let bh = num(ps.get(1), 1).max(1);                let t = num(ps.get(2), 1).max(1);                let (bw, bh) = (bw.max(t), bh.max(t));                let white = ps.get(3).map_or(false, |s| s.trim().eq_ignore_ascii_case("W"));                let xor = st.reverse || st.label_reverse;                let (x0, y0) = st.origin;                for y in 0..bh {                    for x in 0..bw {                        if x < t || y < t || x >= bw - t || y >= bh - t {                            if white && !xor {                                r.canvas.clear(x0 + x, y0 + y);                            } else {                                r.canvas.put(x0 + x, y0 + y, xor);                            }                        }                    }                }                r.fields.push(json!({ "type": "box", "x": x0, "y": y0, "width": bw, "height": bh, "thickness": t }));            }            "GC" => {                let d = num(ps.first(), 3).max(3);                let t = num(ps.get(1), 1).max(1);                let rad = d as f64 / 2.0;                let (x0, y0) = st.origin;                for y in 0..d {                    for x in 0..d {                        let dist = ((x as f64 + 0.5 - rad).powi(2) + (y as f64 + 0.5 - rad).powi(2)).sqrt();                        if dist <= rad && dist >= rad - t as f64 {                            r.canvas.put(x0 + x, y0 + y, st.reverse || st.label_reverse);                        }                    }                }                r.fields.push(json!({ "type": "circle", "x": x0, "y": y0, "diameter": d }));            }            "GF" => {                // ^GFa,b,c,d,data                let mut it = params.splitn(5, ',');                let fmt = it.next().unwrap_or("A").trim().to_uppercase();                let _b = it.next();                let _c = it.next();                let bpr: usize = it.next().and_then(|s| s.trim().parse().ok()).unwrap_or(1).max(1);                let payload = it.next().unwrap_or("");                if fmt != "A" {                    r.unsupported.push(format!("^GF{} (binary)", fmt));                } else {                    let bytes = graphic_bytes(payload, bpr)?;                    let b = graphic_bits(&bytes, bpr, 1, 1);                    r.fields.push(json!({ "type": "graphic", "x": st.origin.0, "y": st.origin.1, "width": b.w, "height": b.h }));                    r.canvas.blit(&b, st.origin.0, st.origin.1, st.reverse || st.label_reverse);                }            }            "XG" => {                let nm = ps.first().map(|s| s.trim().to_uppercase()).unwrap_or_default();                let nm = if nm.contains(':') { nm } else { format!("R:{}", nm) };                let nm = if nm.contains('.') { nm } else { format!("{}.GRF", nm) };                match stored.get(&nm) {                    Some((bpr, bytes)) => {                        let b = graphic_bits(bytes, *bpr, num(ps.get(1), 1), num(ps.get(2), 1));                        r.canvas.blit(&b, st.origin.0, st.origin.1, st.reverse || st.label_reverse);                        r.fields.push(json!({ "type": "graphic", "name": nm, "x": st.origin.0, "y": st.origin.1, "width": b.w, "height": b.h }));                    }                    None => r.unsupported.push(format!("^XG {} (not stored in this ZPL)", nm)),                }            }            "FS" => {                let xor = st.reverse || st.label_reverse;                if let Some(d) = data.take() {                    let (x0, y0) = st.origin;                    if let Some((bc, bp)) = st.barcode.take() {                        let o = bp.first().and_then(|s| s.chars().next()).unwrap_or(field_orient).to_ascii_uppercase();                        let p = |i: usize| bp.get(i).map(String::as_str).unwrap_or("");                        let yes = |i: usize, d: bool| match p(i) { "Y" | "y" => true, "N" | "n" => false, _ => d };                        let bh = p(1).parse::<i64>().unwrap_or(st.by.2).max(1);                        let module = st.by.0;                        let drawn: Option<(Bits, Value)> = match bc.as_str() {                            "BC" => {                                let mode = p(5).chars().next().unwrap_or('N').to_ascii_uppercase();                                let (vals, human) = code128_values(&d, mode)?;                                let m = code128_modules(&vals);                                let shown = if yes(2, true) { Some((human.as_str(), yes(3, false))) } else { None };                                Some((linear(&f, &m, module, bh, shown), json!({ "type": "code128", "data": human })))                            }                            "BE" | "BU" => {                                let digits: Vec<u8> = d.chars().filter(|c| c.is_ascii_digit()).map(|c| c as u8 - b'0').collect();                                let n = if bc == "BE" { 12 } else { 11 };                                if digits.len() < n {                                    return Err(format!("^{} needs {} digits, not {:?}", bc, n, d));                                }                                let mut code: Vec<u8> = if bc == "BU" { std::iter::once(0).chain(digits[..n].iter().copied()).collect() } else { digits[..n].to_vec() };                                code.push(check_digit(&code));                                let m = ean13_modules(&code);                                let text: String = code.iter().skip(if bc == "BU" { 1 } else { 0 }).map(|d| (b'0' + d) as char).collect();                                let shown = if yes(2, true) { Some((text.as_str(), yes(3, false))) } else { None };                                Some((linear(&f, &m, module, bh, shown), json!({ "type": if bc == "BE" { "ean13" } else { "upca" }, "data": text })))                            }                            "BQ" => {                                let mag = p(2).parse::<i64>().unwrap_or(if dpmm <= 8 { 2 } else { 3 }).clamp(1, 10);                                let (ecc, text) = match d.split_once(',') {                                    Some((head, rest)) if head.len() <= 3 => (head.chars().next().unwrap_or('Q').to_ascii_uppercase(), rest.to_string()),                                    _ => ('Q', d.clone()),                                };                                let level = match ecc {                                    'L' => qrcode::EcLevel::L,                                    'M' => qrcode::EcLevel::M,                                    'H' => qrcode::EcLevel::H,                                    _ => qrcode::EcLevel::Q,                                };                                let code = qrcode::QrCode::with_error_correction_level(text.as_bytes(), level).map_err(|e| format!("QR: {}", e))?;                                let n = code.width();                                let colors = code.to_colors();                                let mut b = Bits::new(n * mag as usize, n * mag as usize);                                for (i, c) in colors.iter().enumerate() {                                    if *c == qrcode::Color::Dark {                                        b.rect((i % n) as i64 * mag, (i / n) as i64 * mag, mag, mag);                                    }                                }                                Some((b, json!({ "type": "qr", "data": text, "modules": n })))                            }                            other => {                                r.unsupported.push(format!("^{} (drawn as a grey box)", other));                                let bw = (d.chars().count() as i64 * module * 11).clamp(40, 600);                                let mut b = Bits::new(bw as usize, bh as usize);                                for y in 0..bh {                                    for x in 0..bw {                                        if (x + y) % 6 == 0 || x == 0 || y == 0 || x == bw - 1 || y == bh - 1 {                                            b.set(x, y);                                        }                                    }                                }                                Some((b, json!({ "type": other.to_lowercase(), "data": d })))                            }                        };                        if let Some((b, mut v)) = drawn {                            let b = b.turned(o);                            let y = if st.baseline { y0 - b.h as i64 } else { y0 };                            v["x"] = json!(x0);                            v["y"] = json!(y);                            v["width"] = json!(b.w);                            v["height"] = json!(b.h);                            r.canvas.blit(&b, x0, y, xor);                            r.fields.push(v);                        }                    } else {                        // Text, in the field's font or the default one.                        let (c, fh, fw, o) = if field_font_set { st.font } else { (st.default_font.0, st.default_font.1.max(9), st.default_font.2.max(5), field_orient) };                        let (fh, fw) = (fh as f32, fw as f32);                        let lines: Vec<String> = match st.block {                            Some((bw, max, _, _)) => {                                let mut l = wrap(&f, &d, fh, fw, bw as f32);                                l.truncate(max as usize);                                l                            }                            None => vec![d.replace("\\&", " ")],                        };                        let spacing = st.block.map_or(0, |b| b.2);                        let bw = st.block.map_or_else(|| lines.iter().map(|l| text_width(&f, l, fh, fw)).fold(0.0, f32::max).ceil() as i64 + 1, |b| b.0.max(1));                        let mut b = Bits::new(bw.max(1) as usize, (lines.len() as i64 * (fh as i64 + spacing)).max(1) as usize);                        for (k, l) in lines.iter().enumerate() {                            let lb = text_line(&f, l, fh, fw);                            let x = match st.block.map(|b| b.3) {                                Some('C') => (bw - lb.w as i64) / 2,                                Some('R') => bw - lb.w as i64,                                _ => 0,                            };                            let y = k as i64 * (fh as i64 + spacing);                            for yy in 0..lb.h {                                for xx in 0..lb.w {                                    if lb.px[yy * lb.w + xx] {                                        b.set(x + xx as i64, y + yy as i64);                                    }                                }                            }                        }                        let b = b.turned(o);                        let y = if st.baseline && o == 'N' { y0 - (fh * 0.8) as i64 } else { y0 };                        r.fields.push(json!({ "type": "text", "x": x0, "y": y, "font": c.to_string(), "height": fh as i64, "data": d, "lines": lines.len(), "serial": st.serial }));                        r.canvas.blit(&b, x0, y, xor);                    }                } else {                    st.barcode = None;                }                st.reverse = false;                st.block = None;                st.hex = None;                st.serial = false;                field_font_set = false;            }            other => r.unsupported.push(format!("^{}", other)),        }    }    if label <= want && !inside {        if label == 0 {            return Err("no label (^XA … ^XZ) in this ZPL".into());        }        return Err(format!("there are {} labels; label {} is not one", label, want));    }    if po_invert {        r.canvas.px.reverse();    }    r.unsupported.sort();    r.unsupported.dedup();    Ok(r)}fn png(r: &Render) -> Result<Vec<u8>, String> {    let img = image::GrayImage::from_fn(r.canvas.w as u32, r.canvas.h as u32, |x, y| image::Luma([if r.canvas.px[y as usize * r.canvas.w + x as usize] { 0 } else { 255 }]));    let mut out = std::io::Cursor::new(Vec::new());    img.write_to(&mut out, image::ImageFormat::Png).map_err(|e| e.to_string())?;    Ok(out.into_inner())}fn summary(r: &Render) -> Value {    let black = r.canvas.px.iter().filter(|b| **b).count();    json!({ "width": r.canvas.w, "height": r.canvas.h, "black": (black as f64 / r.canvas.px.len() as f64 * 10000.0).round() / 10000.0, "fields": r.fields.len(), "unsupported": r.unsupported })}/// Draws a ZPL label as a PNG file at `path` (folders made). `options`: `{ dpi: 203, width, height (dots; else ^PW and ^LL, else 4 × 6 in), label: 0 }` (which label of several)./// Gives `{ path, width, height, black (the share of black dots), fields, unsupported }`: commands it could not draw are listed, never silently dropped.pub fn zpl_preview_png(zpl: &str, path: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let r = render(zpl, &options)?;    if let Some(dir) = std::path::Path::new(path).parent() {        if !dir.as_os_str().is_empty() {            std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;        }    }    std::fs::write(path, png(&r)?).map_err(|e| format!("{}: {}", path, e))?;    let mut s = summary(&r);    s["path"] = json!(path);    Ok(s)}/// The label as a `data:image/png;base64,...` address, for an `<img>` on a page.pub fn zpl_preview_data_url(zpl: &str, options: serde_json::Value) -> Result<String, String> {    let r = render(zpl, &options)?;    Ok(format!("data:image/png;base64,{}", base64::engine::general_purpose::STANDARD.encode(png(&r)?)))}/// What the label holds, field by field: `{ type: "text" | "code128" | "ean13" | "upca" | "qr" | "box" | "circle" | "graphic", x, y, width, height, data }` in dots, for tests that a label says what it should.pub fn zpl_fields(zpl: &str, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> {    Ok(render(zpl, &options)?.fields)}/// How many labels (^XA … ^XZ) the ZPL holds.pub fn zpl_label_count(zpl: &str) -> i64 {    commands(zpl).iter().filter(|(p, n, _)| *p == '^' && n == "XZ").count() as i64}
CPCL mobile printers cpcl.qwer · 1.0.0Zebra CPCL for mobile printers on vans and shop floors: labels and receipts with magnified text, centred lines, Code 128, EAN-13 and QR codes with their text, boxes, inverse lines and pictures, sent over Wi-Fi.printinglabelsreceiptsbarcodesdeliverymobile7 views · 7 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 downloadsLabel sheets label_sheets.qwer · 1.0.0Labels on A4 and Letter sheets (Avery L7160, L7163, L7651, 5160 and others, or your own measurements) as a PDF: addresses and tags with text fitted to each label, Code 128 and QR codes, a start position for part-used sheets.labelspdfprintingaddressesbarcodes8 views · 6 downloads
Thread
89Useful? Like it, or add to its thread below.

Thread 0 posts

On ZPL label preview: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.