// Vector CODE128 barcodes for jsPDF label layouts. // // Why vector and not a PNG: the label printers are 203 dpi Zebra thermal // printers (1 dot = 0.125 mm). A raster barcode (jsbarcode/bwip PNG) gets // resampled by CUPS onto that dot grid and its anti-aliased bar edges are then // halftoned to 1-bit — bars end up blurry/speckled, especially on the small // 35×17 mm labels. Filled rectangles are rasterised by Ghostscript with hard // edges instead, so every bar is as crisp as the printer can make it. // // bwip-js (already a dependency, pure JS on Node) does the CODE128 encoding // (auto subset switching A/B/C, checksum). We only take the bar geometry from // its SVG output — one per bar width, // in a viewBox whose unit is HALF a module (each bar is drawn as a stroked // centre line of width 2·modules). import * as bwipjs from 'bwip-js' export interface Code128Bars { /** Total symbol width in modules (no quiet zones). */ modules: number /** Bars as [left, width] in modules, left-to-right. */ bars: Array<[number, number]> } export const encodeCode128 = (text: string): Code128Bars => { const svg: string = (bwipjs as any).toSVG({ bcid: 'code128', text: String(text), includetext: false }) const vb = /viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"/.exec(svg) if (!vb) throw new Error('bwip-js: no viewBox in SVG output') const modules = Number(vb[1]) / 2 const bars: Array<[number, number]> = [] const pathRe = /]*stroke-width="(\d+(?:\.\d+)?)"[^>]*d="([^"]+)"/g let m: RegExpExecArray | null while ((m = pathRe.exec(svg))) { const w = Number(m[1]) / 2 const dRe = /M(\d+(?:\.\d+)?) /g let d: RegExpExecArray | null while ((d = dRe.exec(m[2]))) { bars.push([Number(d[1]) / 2 - w / 2, w]) } } if (!bars.length || !modules) throw new Error('bwip-js: empty CODE128 symbol') bars.sort((a, b) => a[0] - b[0]) return { modules, bars } } export interface DrawBarcodeOptions { /** Minimum quiet zone on each side in mm (CODE128 spec: ≥10 modules). Default 2.5. */ quietMm?: number /** Cap on the module width in mm (keeps very short codes from looking chunky). Default 0.5. */ maxModuleMm?: number } /** * Draws a CODE128 barcode as filled rectangles, centred inside the box * (x, y, w, h) in the document's unit (mm). Returns the drawn bar area. * Throws when the text can't be encoded — callers fall back to an image. */ export const drawCode128 = (doc: any, text: string, x: number, y: number, w: number, h: number, opts: DrawBarcodeOptions = {}) => { const { modules, bars } = encodeCode128(text) const quiet = opts.quietMm ?? 2.5 const maxModule = opts.maxModuleMm ?? 0.5 // Module width: fill the box minus quiet zones, but never wider than maxModule. let module = (w - 2 * quiet) / modules if (module > maxModule) module = maxModule // Quiet zone must also be ≥ 10 modules. const q = Math.max(quiet, 10 * module) module = Math.min(module, (w - 2 * q) / modules) const barsW = module * modules const left = x + (w - barsW) / 2 doc.setFillColor(0, 0, 0) for (const [bx, bw] of bars) { doc.rect(left + bx * module, y, bw * module, h, 'F') } return { x: left, y, w: barsW, h, module } } /** * Fits a single line of text into maxWidth: shrinks the font from maxFs down * to minFs, then truncates with an ellipsis. Leaves the doc at the chosen size. */ export const fitTextLine = (doc: any, text: string, maxWidth: number, maxFs: number, minFs: number) => { let str = String(text ?? '').replace(/\s+/g, ' ').trim() let fs = maxFs doc.setFontSize(fs) while (fs > minFs && doc.getTextWidth(str) > maxWidth) { fs = Math.max(minFs, Math.round((fs - 0.5) * 10) / 10) doc.setFontSize(fs) } if (doc.getTextWidth(str) > maxWidth) { while (str.length > 1 && doc.getTextWidth(str + '…') > maxWidth) { str = str.slice(0, -1).trimEnd() } str += '…' } return { text: str, fontSize: fs } }