/** * Thermal printer commands * Create ESC/POS commands for thermal printers */ /** * Initialize printer * Reset printer to default settings */ export function init(): Uint8Array { return new Uint8Array([0x1b, 0x40]) } /** * Set print area width with margins subtracted * IMPORTANT: GS W command expects width in DOTS, not characters! * Use with setLeftMargin() to create symmetric margins * @param paperWidthMm - Paper width in millimeters (any value, common: 32, 40, 50, 58, 60, 80) * @param marginMm - Margin in millimeters for BOTH sides (will be subtracted from width) */ export function setPrintAreaWidth(paperWidthMm: number, marginMm: number = 0): Uint8Array { // Standard thermal printer: 203 DPI = 8 dots/mm const DOTS_PER_MM = 8 // Calculate print area width = paper width - margins on both sides const printAreaMm = paperWidthMm - marginMm * 2 const widthDots = printAreaMm * DOTS_PER_MM // Examples with 2.5mm margin each side: // 58mm paper → 53mm print area → 424 dots // 80mm paper → 75mm print area → 600 dots // GS W nL nH: Set print area width // Width = nL + nH * 256 (in dots) const nL = widthDots & 0xff // Low byte const nH = (widthDots >> 8) & 0xff // High byte return new Uint8Array([0x1d, 0x57, nL, nH]) } /** * Change text size * @param width - Width multiplier (1-8 times) * @param height - Height multiplier (1-8 times) * * Example: setCharSize(2, 1) = double width, normal height */ export function setCharSize(width: number = 1, height: number = 1): Uint8Array { const w = Math.max(0, Math.min(7, width - 1)) // 0-7 = 1x-8x const h = Math.max(0, Math.min(7, height - 1)) // 0-7 = 1x-8x const n = (h << 4) | w // Combine 2 values into 1 byte return new Uint8Array([0x1d, 0x21, n]) } /** * Set text alignment * @param mode - 'left', 'center', or 'right' */ export function align(mode: 'left' | 'center' | 'right'): Uint8Array { const m = mode === 'left' ? 0 : mode === 'center' ? 1 : 2 return new Uint8Array([0x1b, 0x61, m]) } /** * Make text bold * @param on - true = enable bold, false = disable bold */ export function bold(on: boolean): Uint8Array { return new Uint8Array([0x1b, 0x45, on ? 1 : 0]) } /** * Move to next line * @param n - Number of lines to move down (default: 1) */ export function lf(n = 1): Uint8Array { // Use ESC d n (Print and feed n lines) for better compatibility // Some printers ignore multiple LF bytes when custom line spacing is set return new Uint8Array([0x1b, 0x64, n & 0xff]) } /** * Select character set * @param n - Character set number (27 = Vietnamese CP1258, 0 = English) */ export function selectCodepage(n: number): Uint8Array { return new Uint8Array([0x1b, 0x74, n & 0xff]) } /** * Underline text * @param on - true = enable underline, false = disable underline */ export function underline(on: boolean): Uint8Array { return new Uint8Array([0x1b, 0x2d, on ? 1 : 0]) } /** * Strike through text (for old prices) * @param on - true = enable strike, false = disable strike */ export function doubleStrike(on: boolean): Uint8Array { return new Uint8Array([0x1b, 0x47, on ? 1 : 0]) } /** * Reverse text (white text on black background) * @param on - true = enable reverse, false = disable reverse */ export function reverse(on: boolean): Uint8Array { return new Uint8Array([0x1d, 0x42, on ? 1 : 0]) } /** * Select character font (ESC M n) * @param font - Font type: 'A' (default) or 'B' (compact) * * Font specifications: * - Font A: 12×24 dots character matrix → ~32 chars/line on 58mm paper * - Font B: 9×17 dots character matrix → ~42 chars/line on 58mm paper (~33% more text) * * Important: Font A and B may have different typeface designs depending on printer. * - Some printers: Same typeface, only size differs (Font B = scaled down Font A) * - Other printers: Different designs (e.g., Font A = regular, Font B = condensed/narrow) * * Font B always uses smaller character matrix and allows more characters per line. * * Use Font B when you need to fit more information on a single line while * maintaining readability (e.g., long product names, detailed receipts). */ export function selectFont(font: 'A' | 'B' = 'A'): Uint8Array { const n = font === 'A' ? 0 : 1 return new Uint8Array([0x1b, 0x4d, n]) // ESC M n } /** * Set line spacing * @param spacing - Line spacing (1-255, default: 30) */ export function setLineSpacing(spacing: number = 30): Uint8Array { return new Uint8Array([0x1b, 0x33, spacing & 0xff]) } /** * Set left margin * @param marginMm - Left margin in millimeters (default: 0) */ export function setLeftMargin(marginMm: number = 0): Uint8Array { // Convert mm to dots (8 dots/mm for thermal printers) const DOTS_PER_MM = 8 const marginDots = Math.floor(marginMm * DOTS_PER_MM) // GS L nL nH: Set left margin // Margin = nL + nH * 256 (in dots) const nL = marginDots & 0xff const nH = (marginDots >> 8) & 0xff return new Uint8Array([0x1d, 0x4c, nL, nH]) } /** * Print barcode * @param content - Barcode content (e.g. "123456789") * @param format - Barcode type ('CODE128', 'EAN13', 'CODE39', etc.) * @param height - Barcode height (1-255, default: 100) * @param width - Bar width (2-6, default: 2) * @param textPosition - Text position ('none', 'above', 'below', 'both') */ export function barcode( content: string, format: 'CODE39' | 'CODE128' | 'EAN13' | 'EAN8' | 'UPC_A' = 'CODE128', height = 100, width = 2, textPosition: 'none' | 'above' | 'below' | 'both' = 'below', ): Uint8Array { const chunks: Uint8Array[] = [] // Set barcode height chunks.push(new Uint8Array([0x1d, 0x68, height & 0xff])) // Set bar width chunks.push(new Uint8Array([0x1d, 0x77, Math.min(6, Math.max(2, width))])) // Set text position const textPos = textPosition === 'none' ? 0 : textPosition === 'above' ? 1 : textPosition === 'below' ? 2 : 3 chunks.push(new Uint8Array([0x1d, 0x48, textPos])) // Barcode type mapping const typeMap = { UPC_A: 65, // US product barcode EAN13: 67, // European product barcode EAN8: 68, // Short product barcode CODE39: 69, // Numeric barcode CODE128: 73, // Universal barcode (recommended) } const type = typeMap[format] || 73 // Default to CODE128 // Print barcode const data = new TextEncoder().encode(content) chunks.push(new Uint8Array([0x1d, 0x6b, type, data.length])) chunks.push(data) chunks.push(new Uint8Array([0x00])) // End command // Combine all into one array const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0) const result = new Uint8Array(totalLength) let offset = 0 for (const chunk of chunks) { result.set(chunk, offset) offset += chunk.length } return result } /** * Feed paper and cut * @param feedLines - Number of lines to feed before cutting (default: 3) */ export function feedAndCut(feedLines = 3): Uint8Array { return new Uint8Array([0x1d, 0x56, 66, feedLines & 0xff]) } /** * Cut paper * @param mode - 'full' (complete cut), 'partial' (partial cut) */ export function cut(mode: 'full' | 'partial'): Uint8Array { const m = mode === 'full' ? 0 : 1 return new Uint8Array([0x1d, 0x56, m]) } /** * Print QR code * @param data - QR data (as Uint8Array) * @param size - Cell size (1-16, default: 6) * @param ecc - Error correction level ('L' low, 'M' medium, 'Q' high, 'H' very high) */ export function qrNative(data: Uint8Array, size = 6, ecc: 'L' | 'M' | 'Q' | 'H' = 'M'): Uint8Array[] { const seq: Uint8Array[] = [] const eccMap = {L: 48, M: 49, Q: 50, H: 51} as const // Skip model selection - some printer firmwares leak the model byte (print "1" or "2") // Printers default to model 2 anyway // Set QR cell size seq.push(new Uint8Array([0x1d, 0x28, 0x6b, 3, 0, 0x31, 0x43, Math.max(1, Math.min(16, size))])) // Set error correction level seq.push(new Uint8Array([0x1d, 0x28, 0x6b, 3, 0, 0x31, 0x45, eccMap[ecc]])) // Store QR data const len = data.length + 3 seq.push(new Uint8Array([0x1d, 0x28, 0x6b, len & 0xff, (len >> 8) & 0xff, 0x31, 0x50, 0x30, ...data])) // Print stored QR code seq.push(new Uint8Array([0x1d, 0x28, 0x6b, 3, 0, 0x31, 0x51, 0x30])) return seq }