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
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 errorDraws 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 errorThe label as a data:image/png;base64,... address, for an <img> on a page.
zpl_fields(zpl: str, options: any) → listor an errorWhat 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) → intHow 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.qweOr 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.zplThen, in your program, all of it or only what you call:
use zpl_preview.qweruse zpl_preview_png, zpl_preview_data_url from zpl_preview // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
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
// 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 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}Related
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.7 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.7 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.6 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.6 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.7 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.8 views · 6 downloadsThread 0 posts
On ZPL label preview: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.