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
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 errorLabels 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() → listThe 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 errorWhere 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/label_sheets.qwer$ curl -O https://quillolang.org/plugins/files/label_sheets.qweThen, in your program, all of it or only what you call:
use label_sheets.qweruse label_sheet_pdf, label_sheet_templates from label_sheets // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
printpdf = { version = "0.7", features = ["font_subsetting"] }dejavu = "2.37"ab_glyph = "0.2"qrcode = { version = "0.14", default-features = false }Example
// 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
//! 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())}Related
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.7 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.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 downloadsThread 0 posts
On Label sheets: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.