import type { LabelPrintPayload, LabelTemplateData, MonochromeImageData, PrintImageOptions, StructuredLabelTemplate, } from '../types/printer' import { resolveEscTemplate } from '../templateRenderer' import { createTestPrintTemplate } from '../templates/testPrintTemplate' import { toEscBarcodeTypeCode } from '../../barcodeFormat' function normalizePrinterText (str: string): string { return String(str || '') .normalize('NFKC') .replace(/[\u2018\u2019]/g, '\'') .replace(/[\u201C\u201D]/g, '"') .replace(/[\u2013\u2014]/g, '-') } function stringToBytes (str: string): number[] { const normalized = normalizePrinterText(str) const out: number[] = [] const cp1252Map: Record = { 0x20ac: 0x80, 0x201a: 0x82, 0x0192: 0x83, 0x201e: 0x84, 0x2026: 0x85, 0x2020: 0x86, 0x2021: 0x87, 0x02c6: 0x88, 0x2030: 0x89, 0x0160: 0x8a, 0x2039: 0x8b, 0x0152: 0x8c, 0x017d: 0x8e, 0x2018: 0x91, 0x2019: 0x92, 0x201c: 0x93, 0x201d: 0x94, 0x2022: 0x95, 0x2013: 0x96, 0x2014: 0x97, 0x02dc: 0x98, 0x2122: 0x99, 0x0161: 0x9a, 0x203a: 0x9b, 0x0153: 0x9c, 0x017e: 0x9e, 0x0178: 0x9f, } for (let i = 0; i < normalized.length; i++) { const code = normalized.charCodeAt(i) if (code < 0x80) { out.push(code) continue } if (code >= 0xa0 && code <= 0xff) { out.push(code) continue } if (cp1252Map[code] != null) { out.push(cp1252Map[code]) continue } out.push(0x3f) } return out } function appendText (out: number[], text: string) { const bytes = stringToBytes(text) for (let i = 0; i < bytes.length; i++) out.push(bytes[i]) } function appendLine (out: number[], text = '') { appendText(out, text) out.push(0x0a) } function appendBytes (out: number[], bytes: number[]) { for (let i = 0; i < bytes.length; i++) out.push(bytes[i]) } function appendAlign (out: number[], align: 0 | 1 | 2) { out.push(0x1b, 0x61, align) } function appendBold (out: number[], bold: boolean) { out.push(0x1b, 0x45, bold ? 1 : 0) } function appendSize (out: number[], width = 0, height = 0) { const value = ((width & 0x07) << 4) | (height & 0x07) out.push(0x1d, 0x21, value) } function clamp (value: number, min: number, max: number): number { return Math.max(min, Math.min(max, Math.round(value))) } function normalizeEscBarcodeType (value?: string): number { return toEscBarcodeTypeCode(value) } function normalizeEscQrLevel (value?: string): number { const key = String(value || 'M').trim().toUpperCase() const map: Record = { L: 48, M: 49, Q: 50, H: 51, } return map[key] || 49 } function appendHorizontalRule (out: number[], width = 32) { appendLine(out, '+' + '-'.repeat(Math.max(width - 2, 0)) + '+') } function appendBoxLine (out: number[], text = '', width = 32) { const innerWidth = Math.max(width - 4, 0) const value = text.length > innerWidth ? text.slice(0, innerWidth) : text.padEnd(innerWidth, ' ') appendLine(out, `| ${value} |`) } function createEscDocument (builder: (out: number[]) => void): number[] { const out: number[] = [] out.push(0x1b, 0x40) out.push(0x1b, 0x74, 16) builder(out) out.push(0x1b, 0x64, 0x04) // 打印完成后执行切刀(GS V 0):适配当前内置小票机,避免长纸不断。 out.push(0x1d, 0x56, 0x00) return out } function appendRasterImage (out: number[], image: MonochromeImageData) { const bytesPerRow = image.width / 8 const xL = bytesPerRow & 0xff const xH = (bytesPerRow >> 8) & 0xff const yL = image.height & 0xff const yH = (image.height >> 8) & 0xff out.push(0x1d, 0x76, 0x30, 0x00, xL, xH, yL, yH) for (let y = 0; y < image.height; y++) { for (let byteIndex = 0; byteIndex < bytesPerRow; byteIndex++) { let value = 0 for (let bit = 0; bit < 8; bit++) { const x = byteIndex * 8 + bit const pixel = image.pixels[y * image.width + x] if (pixel) value |= 1 << (7 - bit) } out.push(value & 0xff) } } } function appendBarcode (out: number[], item: { value: string align?: 0 | 1 | 2 symbology?: string height?: number width?: number showText?: boolean }) { const value = String(item.value || '') if (!value) return appendAlign(out, item.align ?? 1) out.push(0x1d, 0x48, item.showText === false ? 0 : 2) out.push(0x1d, 0x68, clamp(item.height || 96, 1, 255)) out.push(0x1d, 0x77, clamp(item.width || 3, 2, 6)) const type = normalizeEscBarcodeType(item.symbology) const bytes = stringToBytes(value) if (type >= 73) { out.push(0x1d, 0x6b, type, clamp(bytes.length, 0, 255)) appendBytes(out, bytes) } else { out.push(0x1d, 0x6b, type) appendBytes(out, bytes) out.push(0x00) } out.push(0x0a) } function appendQrCode (out: number[], item: { value: string align?: 0 | 1 | 2 size?: number level?: 'L' | 'M' | 'Q' | 'H' }) { const value = String(item.value || '') if (!value) return const bytes = stringToBytes(value) const storeLength = bytes.length + 3 const pL = storeLength & 0xff const pH = (storeLength >> 8) & 0xff appendAlign(out, item.align ?? 1) out.push(0x1d, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, 0x32, 0x00) out.push(0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, clamp(item.size || 5, 1, 16)) out.push(0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, normalizeEscQrLevel(item.level)) out.push(0x1d, 0x28, 0x6b, pL, pH, 0x31, 0x50, 0x30) appendBytes(out, bytes) out.push(0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30) out.push(0x0a) } export function buildEscPosTestPrintData (): number[] { return buildEscPosTemplateData(createTestPrintTemplate()) } export function buildEscPosLabelData (payload: LabelPrintPayload): number[] { const { productName, labelId, printQty = 1, category = '', extraLine = '', } = payload return createEscDocument((out) => { appendAlign(out, 1) appendBold(out, true) appendSize(out, 1, 1) appendLine(out, 'FOOD LABEL') appendBold(out, false) appendSize(out, 0, 0) appendLine(out, '-----------------------------') appendLine(out, 'Product: ' + productName) if (category) appendLine(out, 'Category: ' + category) appendLine(out, 'Label ID: ' + labelId) if (extraLine) appendLine(out, extraLine) appendLine(out, 'Qty: ' + String(printQty)) appendLine(out, '-----------------------------') }) } export function buildEscPosImageData ( image: MonochromeImageData, options: PrintImageOptions = {} ): number[] { const printQty = Math.max(1, Math.round(options.printQty || 1)) return createEscDocument((out) => { for (let i = 0; i < printQty; i++) { appendAlign(out, 1) appendRasterImage(out, image) appendLine(out) appendLine(out) } }) } export function buildEscPosTemplateData ( template: StructuredLabelTemplate, data: LabelTemplateData = {} ): number[] { const resolved = resolveEscTemplate(template, data) const printQty = Math.max(1, Math.round(resolved.printQty || 1)) const feedLines = Math.max(1, Math.round(resolved.feedLines || 4)) return createEscDocument((out) => { for (let i = 0; i < printQty; i++) { resolved.items.forEach((item) => { if (item.type === 'rule') { appendHorizontalRule(out, item.width || 32) return } if (item.type === 'qrcode') { appendQrCode(out, item) return } if (item.type === 'barcode') { appendBarcode(out, item) return } appendAlign(out, item.align ?? 0) appendBold(out, !!item.bold) appendSize(out, item.widthScale || 0, item.heightScale || 0) appendLine(out, item.text) appendBold(out, false) appendSize(out, 0, 0) }) out.push(0x1b, 0x64, feedLines) } }) }