// Barcode symbologies, rendered as SVG so a label prints crisply at any size and needs no font. // // A garment nobody can scan silently vanishes from every count, so a reprint has to reproduce the // SAME symbol the supplier printed: a valid 13-digit code goes out as EAN-13 (what the supplier's // own label was), anything else as Code 128 set B. const EAN_A = ["0001101", "0011001", "0010011", "0111101", "0100011", "0110001", "0101111", "0111011", "0110111", "0001011"]; const EAN_B = ["0100111", "0110011", "0011011", "0100001", "0011101", "0111001", "0000101", "0010001", "0001001", "0010111"]; const EAN_C = ["1110010", "1100110", "1101100", "1000010", "1011100", "1001110", "1010000", "1000100", "1001000", "1110100"]; // Which of the first six digits use the B table, chosen by the leading digit. const EAN_PARITY = ["AAAAAA", "AABABB", "AABBAB", "AABBBA", "ABAABB", "ABBAAB", "ABBBAA", "ABABAB", "ABABBA", "ABBABA"]; export function ean13CheckDigit(first12: string): number { let sum = 0; for (let i = 0; i < 12; i++) sum += +first12[i] * (i % 2 === 0 ? 1 : 3); return (10 - (sum % 10)) % 10; } export function isEan13(code: string): boolean { return /^\d{13}$/.test(code) && ean13CheckDigit(code.slice(0, 12)) === +code[12]; } /* A barcode for a garment that arrived without one. * * Plenty of stock reaches a linen room unlabelled — the cafe shirts came with nothing at all — and * a garment nobody can scan drops out of every count. So the room prints its own, and the number it * prints has to be one that can never turn out to belong to somebody else's product. * * GS1 reserves the prefixes 20-29 for restricted circulation: codes used inside one business, on * its own shelves, which GS1 undertakes never to issue to a manufacturer. That is exactly this * case, so a generated code starts 29 and carries a real check digit. Two things follow from it * being a genuine EAN-13 rather than an invented string: any scanner in the building reads it * without being taught anything, and it is the width the label sheet was laid out around. * * The number says nothing about the garment, on purpose. Encoding the item and size would make the * printed label wrong the moment a size is removed or a product renamed, and the binding in the * database already knows what it points at. */ export function inHouseEan13(seq: number): string { const body = "29" + String(Math.max(1, Math.floor(seq))).padStart(10, "0"); return body + ean13CheckDigit(body); } /** True for a code this facility printed itself, rather than one that came in on a garment. */ export const isInHouse = (code: string) => /^29\d{11}$/.test(code) && isEan13(code); /** EAN-13 as a run of 1/0 modules, 95 wide. */ function ean13Bits(code: string): string { const parity = EAN_PARITY[+code[0]]; let out = "101"; for (let i = 1; i <= 6; i++) out += (parity[i - 1] === "A" ? EAN_A : EAN_B)[+code[i]]; out += "01010"; for (let i = 7; i <= 12; i++) out += EAN_C[+code[i]]; return out + "101"; } // Code 128: 107 symbols of 11 modules each, given as bar/space run lengths. const C128 = ["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", "233111"]; const C128_STOP = "2331112"; /** Symbol values for a Code 128 set B string, START B first, check symbol last. */ export function code128Values(text: string): number[] { const vals = [104]; // START B for (const ch of text) { const c = ch.charCodeAt(0); vals.push(c >= 32 && c <= 127 ? c - 32 : 0); } let sum = vals[0]; for (let i = 1; i < vals.length; i++) sum += vals[i] * i; vals.push(sum % 103); // check symbol return vals; } /** Code 128 set B as a run of 1/0 modules. Set B covers ASCII 32–127, which is every code we bind. */ function code128Bits(text: string): string { const vals = code128Values(text); let bits = ""; for (const v of vals) { let bar = true; for (const run of C128[v]) { bits += (bar ? "1" : "0").repeat(+run); bar = !bar; } } let bar = true; for (const run of C128_STOP) { bits += (bar ? "1" : "0").repeat(+run); bar = !bar; } return bits; } const esc = (v: string) => v.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c] as string)); /** Bars plus the human-readable code beneath, as a self-contained SVG string. */ export function barcodeSvg(code: string, opts: { height?: number; module?: number; quiet?: number; text?: boolean } = {}): string { const clean = String(code || "").trim(); if (!clean) return ""; const ean = isEan13(clean); const bits = ean ? ean13Bits(clean) : code128Bits(clean); const m = opts.module ?? 2; const h = opts.height ?? 54; const quiet = opts.quiet ?? 10; const showText = opts.text !== false; const w = bits.length * m + quiet * 2 * m; const textH = showText ? 16 : 0; const rects: string[] = []; let i = 0; while (i < bits.length) { if (bits[i] === "0") { i++; continue; } let run = 0; while (i + run < bits.length && bits[i + run] === "1") run++; rects.push(``); i += run; } const label = showText ? `${esc(clean)}` : ""; return `${rects.join("")}${label}`; } export const barcodeKind = (code: string) => (isEan13(code) ? "EAN-13" : "Code 128");