// ============================================================ // EAN-13 barcode encoder + 102x152mm thermal label renderer // ============================================================ (function () { const L = ['0001101','0011001','0010011','0111101','0100011','0110001','0101111','0111011','0110111','0001011']; const G = ['0100111','0110011','0011011','0100001','0011101','0111001','0000101','0010001','0001001','0010111']; const R = ['1110010','1100110','1101100','1000010','1011100','1001110','1010000','1000100','1001000','1110100']; const PARITY = ['LLLLLL','LLGLGG','LLGGLG','LLGGGL','LGLLGG','LGGLLG','LGGGLL','LGLGLG','LGLGGL','LGGLGL']; // Returns the 95-module string for a 13-digit code (assumes valid check digit). function encodeEAN13(raw) { let code = String(raw).replace(/\D/g, ''); if (code.length === 12) code += checkDigit(code); if (code.length !== 13) { // pad / trim defensively so preview never crashes code = (code + '0000000000000').slice(0, 13); } const first = +code[0]; const parity = PARITY[first]; let bits = '101'; // start guard for (let i = 0; i < 6; i++) { const d = +code[1 + i]; bits += parity[i] === 'L' ? L[d] : G[d]; } bits += '01010'; // center guard for (let i = 0; i < 6; i++) bits += R[+code[7 + i]]; bits += '101'; // end guard return { bits, code }; } function checkDigit(code12) { let sum = 0; for (let i = 0; i < 12; i++) sum += (+code12[i]) * (i % 2 === 0 ? 1 : 3); return (10 - (sum % 10)) % 10; } // Render crisp SVG bars. moduleW in px. Guard bars extend below baseline. function BarcodeSVG({ code, height = 56, moduleW = 1.7, color = '#0a0a0a', showText = true, fontSize = 8.5 }) { const { bits, code: full } = encodeEAN13(code); const guardIdx = new Set(); // start guard modules 0-2, center 45-49, end 92-94 [0,1,2, 45,46,47,48,49, 92,93,94].forEach(i => guardIdx.add(i)); const totalW = bits.length * moduleW; const textH = showText ? fontSize + 3 : 0; const barH = height - textH; const guardExtra = showText ? textH * 0.55 : 0; const rects = []; let x = 0; for (let i = 0; i < bits.length; i++) { if (bits[i] === '1') { const h = guardIdx.has(i) ? barH + guardExtra : barH; rects.push(React.createElement('rect', { key: i, x: x, y: 0, width: moduleW + 0.02, height: h, fill: color, shapeRendering: 'crispEdges' })); } x += moduleW; } const left = full.slice(1, 7), right = full.slice(7, 13); const children = [React.createElement('g', { key: 'bars' }, rects)]; if (showText) { const ty = height - 1.5; const mk = (t, tx, anchor) => React.createElement('text', { key: 'txt' + tx, x: tx, y: ty, fontSize: fontSize, fontFamily: "'IBM Plex Mono', monospace", fontWeight: 600, fill: color, textAnchor: anchor, letterSpacing: 0.3 }, t); children.push(mk(full[0], -1.5, 'end')); children.push(mk(left, 3 * moduleW + (42 * moduleW) / 2, 'middle')); children.push(mk(right, 50 * moduleW + (42 * moduleW) / 2, 'middle')); } return React.createElement('svg', { width: totalW + (showText ? 12 : 0), height: height, viewBox: `${showText ? -11 : 0} 0 ${totalW + (showText ? 12 : 0)} ${height}`, style: { display: 'block', shapeRendering: 'crispEdges' } }, children); } // ---- 38mm x 25mm thermal label (landscape). Rendered white sticker. ---- // scale = px per mm for preview. Real print = 203dpi (8 dots/mm). function ThermalLabel({ product, mfg, exp, batch, labelW = 102, labelH = 152, scale = 5 }) { if (!product) return null; const W = labelW * scale, H = labelH * scale; const pad = Math.max(1.6, labelW * 0.04) * scale / 4; const brandColor = '#0a0a0a'; const title = (product.title || '').replace(product.brand, '').trim() || product.title; const S = (mm) => mm * scale; return React.createElement('div', { className: 'thermal-label', style: { width: W, height: H, background: '#fff', color: '#0a0a0a', position: 'relative', boxSizing: 'border-box', padding: pad, display: 'flex', flexDirection: 'column', overflow: 'hidden', fontFamily: "'IBM Plex Sans', sans-serif", lineHeight: 1.05, borderRadius: Math.max(2, scale * 0.5) } }, // header: brand + mrp React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', borderBottom: '1.2px solid #0a0a0a', paddingBottom: S(0.4) } }, React.createElement('span', { style: { fontFamily: "'Space Grotesk', sans-serif", fontWeight: 700, fontSize: S(2.3), letterSpacing: S(0.05), lineHeight: 1 } }, product.brand), React.createElement('span', { style: { fontFamily: "'Space Grotesk', sans-serif", fontWeight: 700, fontSize: S(2.4), lineHeight: 1 } }, product.mrp) ), // title React.createElement('div', { style: { fontSize: S(1.78), fontWeight: 600, marginTop: S(0.5), display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', minHeight: S(4) } }, title), // netwt + batch React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: S(1.5), fontWeight: 500, marginTop: S(0.3), color: '#1a1a1a' } }, React.createElement('span', null, 'Net Wt: ', React.createElement('b', null, product.netwt || '—')), React.createElement('span', null, 'B.No: ', React.createElement('b', null, batch || product.batch || '—')) ), // barcode React.createElement('div', { style: { flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: S(0.3) } }, React.createElement(BarcodeSVG, { code: product.barcode, height: S(8.2), moduleW: (W - pad * 2 - S(3)) / 95, fontSize: S(1.9) }) ), // footer: mfg / exp / sku React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: S(1.45), fontWeight: 600, marginTop: S(0.2), borderTop: '0.8px solid #0a0a0a', paddingTop: S(0.35) } }, React.createElement('span', null, 'MFG ', mfg || product.mfg), React.createElement('span', null, 'EXP ', exp || product.exp) ), React.createElement('div', { style: { fontSize: S(1.3), fontWeight: 500, color: '#333', fontFamily: "'IBM Plex Mono', monospace", marginTop: S(0.1), textAlign: 'center', letterSpacing: S(0.02) } }, product.sku) ); } // ---- TSPL (TSC printer language) command generator over TCP ---- function buildTSPL({ product, mfg, exp, batch, qty, density = 12, speed = 4, labelW = 102, labelH = 152, gap = 2 }) { const title = (product.title || '').replace(/"/g, "'"); const margin = Math.max(6, Math.round(labelW * 0.06)); const infoY = margin + 26; const barcodeY = infoY + 22; const footerY = Math.max(barcodeY + Math.round(labelH * 0.28) + 18, labelH - 24); const barcodeNarrow = Math.max(3, Math.min(6, Math.round(labelW / 20))); const barcodeWide = Math.max(2, Math.min(6, barcodeNarrow * 2)); const lines = []; lines.push(`SIZE ${labelW} mm, ${labelH} mm`); lines.push(`GAP ${gap} mm, 0 mm`); lines.push('DIRECTION 1'); lines.push('REFERENCE 0,0'); lines.push('CLS'); lines.push(`DENSITY ${density}`); lines.push(`SPEED ${speed}`); lines.push(`TEXT ${margin},${margin + 8},"3",0,1,1,"${product.brand}"`); lines.push(`TEXT ${Math.max(margin, labelW - margin - 120)},${margin + 8},"3",0,1,1,"${product.mrp}"`); lines.push(`TEXT ${margin},${margin + 28},"2",0,1,1,"${title.slice(0, 48)}"`); lines.push(`TEXT ${margin},${infoY},"1",0,1,1,"Net Wt: ${product.netwt} B.No: ${batch || product.batch}"`); lines.push(`BARCODE ${margin + 10},${barcodeY},"EAN13",${Math.max(45, Math.round(labelH * 0.28))},1,0,${barcodeNarrow},${barcodeWide},"${product.barcode.slice(0, 12)}"`); lines.push(`TEXT ${margin},${footerY},"1",0,1,1,"MFG ${mfg || product.mfg} EXP ${exp || product.exp} ${product.sku}"`); lines.push(`PRINT 1,${qty}`); return lines.join('\n'); } Object.assign(window, { encodeEAN13, checkDigit, BarcodeSVG, ThermalLabel, buildTSPL }); })();