/** * ESC/POS Driver Implementation * Converts high-level print commands to ESC/POS byte sequences */ import { IPrinterDriver, IQrOptions } from '@/types'; import { EncodingService } from '@/encoding'; import { Encoding } from '@/utils/encoding'; import { ImageProcessing } from '@/utils/image'; import { Logger } from '@/utils/logger'; /** * ESC/POS driver options */ export interface EscPosOptions { /** Use the new EncodingService for better GBK support (default: true) */ useEncodingService?: boolean; /** Show warnings for unsupported characters (default: true) */ showEncodingWarnings?: boolean; /** Fallback character for unsupported characters (default: '?') */ fallbackChar?: string; } /** * ESC/POS thermal printer driver * * Implements the standard ESC/POS command set used by most thermal receipt printers. * Supports text, images, QR codes, and paper control commands. * * @example * ```typescript * const driver = new EscPos(); * const commands = [ * ...driver.init(), * ...driver.text('Hello World!'), * ...driver.feed(2), * ...driver.cut() * ]; * ``` */ export class EscPos implements IPrinterDriver { private readonly logger = Logger.scope('EscPos'); private readonly encodingService: EncodingService; private readonly useEncodingService: boolean; /** * Creates a new EscPos driver instance * @param options - Driver options */ constructor(options?: EscPosOptions) { this.useEncodingService = options?.useEncodingService ?? true; this.encodingService = new EncodingService({ showWarnings: options?.showEncodingWarnings ?? true, fallbackChar: options?.fallbackChar ?? '?', }); } /** * Initializes the printer * Sends ESC @ command to reset printer to default state * * @returns Array of command buffers */ init(): Uint8Array[] { return [new Uint8Array([0x1b, 0x40])]; // ESC @ } /** * Generates text print command * * @param content - Text content to print * @param encoding - Text encoding (default: 'GBK') * @returns Array of command buffers * * @example * ```typescript * driver.text('你好世界', 'GBK'); * ``` */ text(content: string, encoding = 'GBK'): Uint8Array[] { if (!content || typeof content !== 'string') { return []; } // Use new EncodingService for better GBK/GB2312/Big5 support if (this.useEncodingService && this.encodingService.isSupported(encoding)) { const encoded = this.encodingService.encode(content, encoding); return [encoded]; } // Fall back to legacy encoding for backward compatibility const encoded = Encoding.encode(content, encoding); return [encoded]; } /** * Generates line feed command * * @param lines - Number of lines to feed (default: 1) * @returns Array of command buffers * * @example * ```typescript * driver.feed(3); // Feed 3 lines * ``` */ feed(lines = 1): Uint8Array[] { // 限制行数范围 1-255 const safeLines = Math.max(1, Math.min(255, Math.floor(lines))); return [new Uint8Array([0x1b, 0x64, safeLines])]; // ESC d n } /** * Generates paper cut command * * @returns Array of command buffers * * @example * ```typescript * driver.cut(); * ``` */ cut(): Uint8Array[] { return [new Uint8Array([0x1d, 0x56, 0x00])]; // GS V 0 } /** * Generates image print command * Uses Floyd-Steinberg dithering for better quality * * @param data - RGBA pixel data * @param width - Image width in pixels * @param height - Image height in pixels * @returns Array of command buffers * * @example * ```typescript * const imageData = new Uint8Array(width * height * 4); // RGBA * driver.image(imageData, 200, 100); * ``` */ image(data: Uint8Array, width: number, height: number): Uint8Array[] { // 参数验证 if (!data || !(data instanceof Uint8Array) || width <= 0 || height <= 0) { return []; } // 确保数据长度正确 if (data.length !== width * height * 4) { this.logger.warn( `Invalid image data length: expected ${width * height * 4}, got ${data.length}` ); return []; } const bitmap = ImageProcessing.toBitmap(data, width, height); const bytesPerLine = Math.ceil(width / 8); const xL = bytesPerLine % 256; const xH = Math.floor(bytesPerLine / 256); const yL = height % 256; const yH = Math.floor(height / 256); // GS v 0 m xL xH yL yH d1...dk const header = new Uint8Array([0x1d, 0x76, 0x30, 0x00, xL, xH, yL, yH]); return [header, bitmap]; } /** * Generates QR code print command * * @param content - QR code content (URL, text, etc.) * @param options - QR code options * @returns Array of command buffers * * @example * ```typescript * driver.qr('https://example.com', { * model: 2, * size: 8, * errorCorrection: 'M' * }); * ``` */ qr(content: string, options?: IQrOptions): Uint8Array[] { // 参数验证 if (!content || typeof content !== 'string') { return []; } const model = options?.model ?? 2; // 限制模块大小 1-16 const size = Math.max(1, Math.min(16, options?.size ?? 6)); const errorCorrection = options?.errorCorrection ?? 'M'; const commands: Uint8Array[] = []; // 1. Set Model (Function 165) // GS ( k 04 00 31 41 n1 n2 commands.push( new Uint8Array([0x1d, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, model === 1 ? 49 : 50, 0]) ); // 2. Set Module Size (Function 167) // GS ( k 03 00 31 43 n (n = 1-16) commands.push(new Uint8Array([0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, size])); // 3. Set Error Correction (Function 169) // GS ( k 03 00 31 45 n // n: 48 (L=7%), 49 (M=15%), 50 (Q=25%), 51 (H=30%) const ecMap: Record = { L: 48, M: 49, Q: 50, H: 51 }; const ecValue = ecMap[errorCorrection] ?? 49; // Default to M (15%) commands.push(new Uint8Array([0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, ecValue])); // 4. Store Data (Function 180) // GS ( k pL pH 31 50 30 d1...dk // pL, pH: length of data + 3 // Use new EncodingService for better GBK support const data = this.useEncodingService && this.encodingService.isSupported('GBK') ? this.encodingService.encode(content, 'GBK') : Encoding.encode(content, 'GBK'); const len = data.length + 3; const pL = len % 256; const pH = Math.floor(len / 256); commands.push(new Uint8Array([0x1d, 0x28, 0x6b, pL, pH, 0x31, 0x50, 0x30])); commands.push(data); // 5. Print Symbol (Function 181) // GS ( k 03 00 31 51 30 commands.push(new Uint8Array([0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30])); return commands; } /** * ESC/POS: Open cash drawer (钱箱控制) * Sends ESC p command to trigger cash drawer kick-out * * @param pin - Cash drawer pin (0 or 1, default: 0) * @returns Array of command buffers */ openCashDrawer(pin = 0): Uint8Array[] { return [new Uint8Array([0x1b, 0x70, pin, 50, 200])]; } /** * ESC/POS: Sound buzzer (蜂鸣器) * Sends ESC B command to activate the printer's built-in buzzer * * @param times - Number of beeps (1-9, default: 3) * @param duration - Duration in ms (default: 50) * @returns Array of command buffers */ beep(times = 3, duration = 50): Uint8Array[] { return [new Uint8Array([0x1b, 0x42, times, duration])]; } /** * ESC/POS: Self test (自检) * Sends ESC i command to print a self-test page and return status * * @returns Array of command buffers */ selfTest(): Uint8Array[] { return [new Uint8Array([0x1b, 0x69])]; } /** * ESC/POS: Get printer status (状态查询) * Sends DLE EOT n commands to query printer, offline, error, and paper status * * @returns Array of command buffers (4 status queries) */ getStatus(): Uint8Array[] { const buffers: Uint8Array[] = []; for (let i = 1; i <= 4; i++) { buffers.push(new Uint8Array([0x10, 0x04, i])); } return buffers; } /** * ESC/POS: Set character code page (代码页设置) * Sends ESC t command to select character code page * * @param codePage - Code page number (0-255) * @returns Array of command buffers */ setCodePage(codePage: number): Uint8Array[] { return [new Uint8Array([0x1b, 0x74, codePage])]; } /** * ESC/POS: Set left margin (左边界设置) * Sends ESC l command to set left margin in characters * * @param n - Number of characters from left edge * @returns Array of command buffers */ setLeftMargin(n: number): Uint8Array[] { return [new Uint8Array([0x1b, 0x6c, n])]; } /** * ESC/POS: Set print area width (打印区域宽度) * Sends ESC W command to set print width in characters * * @param n - Width in characters * @returns Array of command buffers */ setPrintWidth(n: number): Uint8Array[] { return [new Uint8Array([0x1b, 0x57, n])]; } }