Printers

Label sheets

Labels 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.

Version 1.0.0 · 3 functions · 4 crates

#labels#pdf#printing#addresses#barcodes

Functions

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

label_sheet_pdf(labels: list, template: any, path: str, options: any) → anyor an error

Labels on sheets as a PDF file at path: each label is lines of text (a list, or text with line breaks), or { lines, align: "center", size (points at most), bold_first: true, barcode: "A-1042" (Code 128), qr: "https://..." }. template is a name ("L7160", "L7163", "L7165", "L7651", "5160", "5163"...) or { page: "a4" | "letter", columns, rows, width, height, top, left, h_pitch, v_pitch } in mm. options: { start: 1 (the first free label of a part-used sheet), copies: 1, padding: 3 (mm), size: 12, align, outline (draw each label's edge, for a test print), title }. Gives { path, pages, labels, per_sheet, template }.

label_sheet_templates() → list

The templates this plugin knows: { name, page_mm, columns, rows, width, height, top, left, h_pitch, v_pitch } in mm.

label_sheet_layout(template: any) → listor an error

Where each label of a template sits, in mm from the page's top left: [{ n, column, row, x, y, width, height }], to check against a ruler or another program.

Get it

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

Or with curl:

$ curl -O https://quillolang.org/plugins/files/label_sheets.qwer$ curl -O https://quillolang.org/plugins/files/label_sheets.qwe

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

main.qwe
use label_sheets.qweruse label_sheet_pdf, label_sheet_templates from label_sheets      // Quillo 0.6: only these

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

[dependencies]
printpdf = { version = "0.7", features = ["font_subsetting"] }dejavu = "2.37"ab_glyph = "0.2"qrcode = { version = "0.14", default-features = false }

Example

label_sheets.qwe
// An office's labels on sheets for its laser printer: a mailing's addresses on Avery L7160// (21 to an A4 sheet, starting at the 5th of a part-used sheet), asset tags with a barcode and a// QR code on L7651, and the layout of a Letter 5160 sheet.////   quillo run label_sheets.qwe      then open http://localhost:8080/addresses.pdfuse "label_sheets.qwer"let PEOPLE = [    ["Ms Ada Byron", "12 St James's Square", "London SW1Y 4JH"],    ["Dr Grace Hopper", "1 Navy Yard", "Arlington VA 22202", "USA"],    ["Søren Kierkegaard", "Nytorv 2", "1450 København K", "Danmark"],    ["Zoë Ångström", "Drottninggatan 14", "111 51 Stockholm"],]route GET /addresses.pdf {    label_sheet_pdf(PEOPLE, "L7160", "data/addresses.pdf", { start: 5, size: 11 })?    file("data/addresses.pdf")}route GET /made = {    addresses: label_sheet_pdf(PEOPLE, "L7160", "data/addresses.pdf", { start: 20, copies: 2 })?,    assets: label_sheet_pdf(map(range(1, 4), (n) => { lines: [`Asset ${n}`, "IT dept"], barcode: `IT-00${n}`, qr: `https://it.example/asset/${n}` }), "L7651", "data/assets.pdf", { outline: true, padding: 1.5 })?,    custom: label_sheet_pdf([["Box 1"], ["Box 2"]], { page: "a4", columns: 2, rows: 3, width: 100, height: 90, top: 10, left: 5 }, "data/boxes.pdf", { align: "center", size: 36 })?,}route GET /layout = {    first_three: slice(label_sheet_layout("5160")?, 0, 3),    l7160: label_sheet_templates() | filter((t) => t.name == "L7160"),}
$ quillo run label_sheets.qwe$ curl 'localhost:8080/made'$ curl 'localhost:8080/layout'$ curl 'localhost:8080/addresses.pdf'

Source

label_sheets.qwer
//! Labels on sheets of A4 or Letter (Avery and compatible templates: L7160, L7163, L7165, L7651, 5160, 5163 and more, or your own measurements) as a PDF for an office printer: address, file, product and asset labels with lines of text fitted to each label, Code 128 barcodes and QR codes, a start position for a part-used sheet, and outlines to check the alignment.//! version: 1.0.0//!//! [dependencies]//! printpdf = { version = "0.7", features = ["font_subsetting"] }//! dejavu = "2.37"//! ab_glyph = "0.2"//! qrcode = { version = "0.14", default-features = false }use ab_glyph::{Font, FontRef};use printpdf::{path::PaintMode, Color, Greyscale, IndirectFontRef, Mm, PdfDocument, PdfLayerReference, Rect};use serde_json::{json, Value};/// A sheet: page size, columns, rows, label size, top and left margins, pitch across and down, all in mm.#[derive(Clone)]struct Sheet {    name: String,    page: (f64, f64),    cols: usize,    rows: usize,    w: f64,    h: f64,    top: f64,    left: f64,    hp: f64,    vp: f64,}const IN: f64 = 25.4;fn templates() -> Vec<Sheet> {    let a4 = (210.0, 297.0);    let letter = (8.5 * IN, 11.0 * IN);    let s = |name: &str, page: (f64, f64), cols, rows, w, h, top, left, hp, vp| Sheet { name: name.into(), page, cols, rows, w, h, top, left, hp, vp };    vec![        s("L7160", a4, 3, 7, 63.5, 38.1, 15.1, 7.2, 66.0, 38.1),        s("L7159", a4, 3, 8, 63.5, 33.9, 12.9, 7.2, 66.0, 33.9),        s("L7161", a4, 3, 6, 63.5, 46.6, 8.8, 7.2, 66.0, 46.6),        s("L7162", a4, 2, 8, 99.1, 33.9, 12.9, 4.7, 101.6, 33.9),        s("L7163", a4, 2, 7, 99.1, 38.1, 15.1, 4.7, 101.6, 38.1),        s("L7165", a4, 2, 4, 99.1, 67.7, 13.1, 4.7, 101.6, 67.7),        s("L7169", a4, 2, 2, 99.1, 139.0, 9.5, 4.6, 101.6, 139.0),        s("L7173", a4, 2, 5, 99.1, 57.0, 6.0, 4.7, 101.6, 57.0),        s("L7651", a4, 5, 13, 38.1, 21.2, 10.7, 4.7, 40.6, 21.2),        s("L7168", a4, 1, 2, 199.6, 143.5, 5.0, 5.2, 199.6, 143.5),        s("L7167", a4, 1, 1, 199.6, 289.1, 3.9, 5.2, 199.6, 289.1),        s("5160", letter, 3, 10, 2.625 * IN, 1.0 * IN, 0.5 * IN, 0.1875 * IN, 2.75 * IN, 1.0 * IN),        s("5161", letter, 2, 10, 4.0 * IN, 1.0 * IN, 0.5 * IN, 0.15625 * IN, 4.1875 * IN, 1.0 * IN),        s("5162", letter, 2, 7, 4.0 * IN, 1.333 * IN, 0.8333 * IN, 0.15625 * IN, 4.1875 * IN, 1.333 * IN),        s("5163", letter, 2, 5, 4.0 * IN, 2.0 * IN, 0.5 * IN, 0.15625 * IN, 4.1875 * IN, 2.0 * IN),        s("5164", letter, 2, 3, 4.0 * IN, 3.333 * IN, 0.5 * IN, 0.15625 * IN, 4.1875 * IN, 3.333 * IN),        s("5167", letter, 4, 20, 1.75 * IN, 0.5 * IN, 0.5 * IN, 0.28125 * IN, 2.0625 * IN, 0.5 * IN),    ]}fn sheet(t: &Value) -> Result<Sheet, String> {    if let Some(name) = t.as_str() {        let n = name.trim_start_matches("Avery").trim().trim_start_matches(['J', 'L']).to_string();        return templates()            .into_iter()            .find(|s| s.name == name || s.name.trim_start_matches('L') == n)            .ok_or_else(|| format!("unknown template {:?}: {}, or give the measurements", name, templates().iter().map(|s| s.name.clone()).collect::<Vec<_>>().join(", ")));    }    let f = |k: &str| t[k].as_f64().ok_or_else(|| format!("a template needs `{}` (mm)", k));    let page = match t["page"].as_str().unwrap_or("a4") {        "a4" => (210.0, 297.0),        "letter" => (8.5 * IN, 11.0 * IN),        other => return Err(format!("page {:?}: a4 or letter", other)),    };    let (w, h) = (f("width")?, f("height")?);    let s = Sheet {        name: "custom".into(),        page,        cols: t["columns"].as_u64().ok_or("a template needs `columns`")? as usize,        rows: t["rows"].as_u64().ok_or("a template needs `rows`")? as usize,        w,        h,        top: f("top")?,        left: f("left")?,        hp: t["h_pitch"].as_f64().unwrap_or(w),        vp: t["v_pitch"].as_f64().unwrap_or(h),    };    if s.cols == 0 || s.rows == 0 || s.cols * s.rows > 500 || s.left + s.hp * (s.cols - 1) as f64 + s.w > s.page.0 + 0.5 || s.top + s.vp * (s.rows - 1) as f64 + s.h > s.page.1 + 0.5 {        return Err("the labels do not fit the page".into());    }    Ok(s)}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",];/// Code 128 modules (true is a bar) for printable ASCII: digit runs of 4 or more in set C, the rest in B.fn code128(data: &str) -> Result<Vec<bool>, String> {    let cs: Vec<char> = data.chars().collect();    if cs.is_empty() || !cs.iter().all(|c| (' '..='~').contains(c)) {        return Err("Code 128 takes printable ASCII".into());    }    let run = |i: usize| cs[i..].iter().take_while(|c| c.is_ascii_digit()).count();    let mut in_c = run(0) >= 4 || (run(0) == cs.len() && cs.len() % 2 == 0);    let mut vals = vec![if in_c { 105u32 } else { 104 }];    let mut i = 0;    while i < cs.len() {        let r = run(i);        if in_c {            if r >= 2 {                vals.push((cs[i] as u32 - 48) * 10 + cs[i + 1] as u32 - 48);                i += 2;            } else {                vals.push(100);                in_c = false;            }            continue;        }        if r >= 4 && r % 2 == 0 {            vals.push(99);            in_c = true;            continue;        }        vals.push(cs[i] as u32 - 32);        i += 1;    }    let sum: u32 = vals[0] + vals.iter().enumerate().skip(1).map(|(k, v)| k as u32 * v).sum::<u32>();    vals.push(sum % 103);    vals.push(106);    let mut m = vec![];    for v in vals {        for (k, d) in CODE128[v as usize].chars().enumerate() {            for _ in 0..d.to_digit(10).unwrap() {                m.push(k % 2 == 0);            }        }    }    Ok(m)}struct Fonts {    regular: IndirectFontRef,    bold: IndirectFontRef,    measure: FontRef<'static>,    measure_bold: FontRef<'static>,}/// The width in mm of `s` at `pt` points.fn width_mm(f: &FontRef, s: &str, pt: f64) -> f64 {    let upm = f.units_per_em().unwrap_or(2048.0) as f64;    let u: f64 = s.chars().map(|c| f.h_advance_unscaled(f.glyph_id(c)) as f64).sum();    u / upm * pt * IN / 72.0}fn rect(layer: &PdfLayerReference, x: f64, y_top: f64, w: f64, h: f64, page_h: f64, stroke: bool) {    let r = Rect::new(Mm(x as f32), Mm((page_h - y_top - h) as f32), Mm((x + w) as f32), Mm((page_h - y_top) as f32));    layer.add_rect(if stroke { r.with_mode(PaintMode::Stroke) } else { r.with_mode(PaintMode::Fill) });}/// Draws one label's content inside its box (x, y from the top left of the page).fn draw(layer: &PdfLayerReference, fonts: &Fonts, label: &Value, x: f64, y: f64, s: &Sheet, options: &Value) -> Result<(), String> {    let pad = options["padding"].as_f64().unwrap_or(3.0).clamp(0.0, 20.0);    let (iw, ih) = (s.w - 2.0 * pad, s.h - 2.0 * pad);    let label = match label {        Value::Array(_) => json!({ "lines": label }),        Value::String(t) => json!({ "lines": t.split('\n').collect::<Vec<_>>() }),        other => other.clone(),    };    let lines: Vec<String> = label["lines"].as_array().map(|a| a.iter().map(|v| match v { Value::String(s) => s.clone(), o => o.to_string() }).collect()).unwrap_or_default();    // Room kept for a QR code on the right, or a barcode at the bottom.    let qr = label["qr"].as_str();    let bar = label["barcode"].as_str();    let bar_h = if bar.is_some() { (ih * 0.35).min(15.0) } else { 0.0 };    let text_h = ih - if bar.is_some() { bar_h + 1.5 } else { 0.0 };    // The QR code beside the text, above any barcode.    let qr_size = if qr.is_some() { text_h.min(iw * 0.4) } else { 0.0 };    let text_w = iw - if qr.is_some() { qr_size + 2.0 } else { 0.0 };    if !lines.is_empty() {        let bold_first = label["bold_first"].as_bool().unwrap_or(true);        let max_pt = label["size"].as_f64().or(options["size"].as_f64()).unwrap_or(12.0);        // The largest size up to max_pt at which every line fits.        let mut pt = max_pt;        loop {            let line_h = pt * 1.2 * IN / 72.0;            let fits_h = line_h * lines.len() as f64 <= text_h + 0.01;            let fits_w = lines.iter().enumerate().all(|(k, l)| width_mm(if k == 0 && bold_first { &fonts.measure_bold } else { &fonts.measure }, l, pt) <= text_w);            if (fits_h && fits_w) || pt <= 4.0 {                break;            }            pt -= 0.5;        }        let line_h = pt * 1.2 * IN / 72.0;        let block = line_h * lines.len() as f64;        let top = y + pad + ((text_h - block) / 2.0).max(0.0);        let center = label["align"].as_str().or(options["align"].as_str()) == Some("center");        for (k, l) in lines.iter().enumerate() {            let bold = k == 0 && bold_first;            let f = if bold { &fonts.measure_bold } else { &fonts.measure };            let lw = width_mm(f, l, pt);            let lx = if center { x + pad + (text_w - lw).max(0.0) / 2.0 } else { x + pad };            let baseline = top + line_h * k as f64 + pt * IN / 72.0 * 0.95;            layer.use_text(l.as_str(), pt as f32, Mm(lx as f32), Mm((s.page.1 - baseline) as f32), if bold { &fonts.bold } else { &fonts.regular });        }    }    if let Some(q) = qr {        let code = qrcode::QrCode::with_error_correction_level(q.as_bytes(), qrcode::EcLevel::M).map_err(|e| format!("QR: {}", e))?;        let n = code.width();        let m = qr_size / n as f64;        let (qx, qy) = (x + s.w - pad - qr_size, y + pad + (text_h - qr_size) / 2.0);        for (i, c) in code.to_colors().iter().enumerate() {            if *c == qrcode::Color::Dark {                rect(layer, qx + (i % n) as f64 * m, qy + (i / n) as f64 * m, m + 0.05, m + 0.05, s.page.1, false);            }        }    }    if let Some(b) = bar {        let mods = code128(b)?;        let mw = (iw / (mods.len() as f64 + 20.0)).min(0.5);        if mw < 0.19 {            return Err(format!("the barcode {:?} is too long for a {} mm label", b, s.w));        }        let bw = mw * mods.len() as f64;        let bx = x + pad + (iw - bw) / 2.0;        let by = y + s.h - pad - bar_h;        let mut k = 0;        while k < mods.len() {            if mods[k] {                let start = k;                while k < mods.len() && mods[k] {                    k += 1;                }                rect(layer, bx + start as f64 * mw, by, (k - start) as f64 * mw, bar_h, s.page.1, false);            } else {                k += 1;            }        }    }    Ok(())}fn render(labels: &[Value], template: &Value, options: &Value) -> Result<(Vec<u8>, usize, Sheet), String> {    let s = sheet(template)?;    if labels.len() > 20_000 {        return Err("at most 20,000 labels".into());    }    let per = s.cols * s.rows;    let start = options["start"].as_u64().unwrap_or(1).max(1) as usize - 1;    if start >= per {        return Err(format!("the sheet has {} labels; start at 1 to {}", per, per));    }    let copies = options["copies"].as_u64().unwrap_or(1).clamp(1, 1000) as usize;    let all: Vec<&Value> = labels.iter().flat_map(|l| std::iter::repeat(l).take(copies)).collect();    let pages = ((start + all.len()).max(1) + per - 1) / per;    let (doc, p1, l1) = PdfDocument::new(options["title"].as_str().unwrap_or("Labels"), Mm(s.page.0 as f32), Mm(s.page.1 as f32), "labels");    let fonts = Fonts {        regular: doc.add_external_font(std::io::Cursor::new(dejavu::sans::regular())).map_err(|e| e.to_string())?,        bold: doc.add_external_font(std::io::Cursor::new(dejavu::sans::bold())).map_err(|e| e.to_string())?,        measure: FontRef::try_from_slice(dejavu::sans::regular()).map_err(|e| e.to_string())?,        measure_bold: FontRef::try_from_slice(dejavu::sans::bold()).map_err(|e| e.to_string())?,    };    let mut layers = vec![doc.get_page(p1).get_layer(l1)];    for _ in 1..pages {        let (p, l) = doc.add_page(Mm(s.page.0 as f32), Mm(s.page.1 as f32), "labels");        layers.push(doc.get_page(p).get_layer(l));    }    for layer in &layers {        layer.set_fill_color(Color::Greyscale(Greyscale::new(0.0, None)));        layer.set_outline_color(Color::Greyscale(Greyscale::new(0.6, None)));        layer.set_outline_thickness(0.3);    }    let outline = options["outline"] == true;    for slot in 0..pages * per {        let layer = &layers[slot / per];        let k = slot % per;        let (c, r) = (k % s.cols, k / s.cols);        let x = s.left + c as f64 * s.hp;        let y = s.top + r as f64 * s.vp;        if outline {            rect(layer, x, y, s.w, s.h, s.page.1, true);        }        if slot >= start && slot - start < all.len() {            draw(layer, &fonts, all[slot - start], x, y, &s, options)?;        }    }    let bytes = doc.save_to_bytes().map_err(|e| e.to_string())?;    Ok((bytes, pages, s))}/// Labels on sheets as a PDF file at `path`: each label is lines of text (a list, or text with line breaks), or `{ lines, align: "center", size (points at most), bold_first: true, barcode: "A-1042" (Code 128), qr: "https://..." }`./// `template` is a name ("L7160", "L7163", "L7165", "L7651", "5160", "5163"...) or `{ page: "a4" | "letter", columns, rows, width, height, top, left, h_pitch, v_pitch }` in mm./// `options`: `{ start: 1 (the first free label of a part-used sheet), copies: 1, padding: 3 (mm), size: 12, align, outline (draw each label's edge, for a test print), title }`. Gives `{ path, pages, labels, per_sheet, template }`.pub fn label_sheet_pdf(labels: Vec<serde_json::Value>, template: serde_json::Value, path: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let (bytes, pages, s) = render(&labels, &template, &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, &bytes).map_err(|e| format!("{}: {}", path, e))?;    Ok(json!({ "path": path, "pages": pages, "labels": labels.len() * options["copies"].as_u64().unwrap_or(1).clamp(1, 1000) as usize, "per_sheet": s.cols * s.rows, "template": s.name, "bytes": bytes.len() }))}/// The templates this plugin knows: `{ name, page_mm, columns, rows, width, height, top, left, h_pitch, v_pitch }` in mm.pub fn label_sheet_templates() -> Vec<serde_json::Value> {    templates()        .iter()        .map(|s| json!({ "name": s.name, "page_mm": [s.page.0, s.page.1], "columns": s.cols, "rows": s.rows, "width": (s.w * 10.0).round() / 10.0, "height": (s.h * 10.0).round() / 10.0, "top": (s.top * 10.0).round() / 10.0, "left": (s.left * 10.0).round() / 10.0, "h_pitch": (s.hp * 10.0).round() / 10.0, "v_pitch": (s.vp * 10.0).round() / 10.0 }))        .collect()}/// Where each label of a template sits, in mm from the page's top left: `[{ n, column, row, x, y, width, height }]`, to check against a ruler or another program.pub fn label_sheet_layout(template: serde_json::Value) -> Result<Vec<serde_json::Value>, String> {    let s = sheet(&template)?;    Ok((0..s.cols * s.rows)        .map(|k| {            let (c, r) = (k % s.cols, k / s.cols);            json!({ "n": k + 1, "column": c + 1, "row": r + 1, "x": ((s.left + c as f64 * s.hp) * 10.0).round() / 10.0, "y": ((s.top + r as f64 * s.vp) * 10.0).round() / 10.0, "width": s.w, "height": s.h })        })        .collect())}
Brother QL and P-touch ptouch.qwer · 1.0.0Brother QL label printers and P-touch tape printers in raster mode: text made into a label picture, any picture made into the raster job for a DK roll or TZe tape, cut, compressed, sent over the network, and the printer's status read.printinglabelsimagesdevicesaddresses7 views · 7 downloadsCPCL 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 downloads
Thread
86Useful? Like it, or add to its thread below.

Thread 0 posts

On Label sheets: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.