/** @odoo-module **/ /** * ESC/POS Graphics Generator * * Generates ESC/POS commands for printing bitmap graphics on thermal printers. * Supports raster graphics mode for printing images. */ // ESC/POS Command Constants const ESC = 0x1B; const GS = 0x1D; const LF = 0x0A; export class EscPosGraphics { constructor() { this.maxWidth = 384; // Full width for 58mm paper (48 bytes * 8 bits) // For 80mm paper, use 576 instead this.useCompression = false; // Set to true if printer supports compression } /** * Initialize printer * * @returns {Uint8Array} Initialization commands */ initialize() { return new Uint8Array([ESC, 0x40]); // ESC @ - Initialize printer } /** * Generate ESC/POS commands for bitmap printing (OPTIMIZED) * * @param {Object} bitmap - Bitmap data with width, height, and data * @returns {Uint8Array} Complete ESC/POS command sequence */ generateBitmapCommands(bitmap) { console.log('[EscPosGraphics] Generating bitmap commands (optimized)...'); console.log('[EscPosGraphics] Original dimensions:', bitmap.width, 'x', bitmap.height); const startTime = performance.now(); // OPTIMIZATION: Remove blank lines from top and bottom const optimizedBitmap = this._removeBlankLines(bitmap); console.log('[EscPosGraphics] Optimized dimensions:', optimizedBitmap.width, 'x', optimizedBitmap.height); console.log('[EscPosGraphics] Saved', bitmap.height - optimizedBitmap.height, 'blank lines'); const commands = []; // Initialize printer commands.push(...this.initialize()); // Print bitmap using raster graphics mode const rasterCommands = this._generateRasterGraphics(optimizedBitmap); commands.push(...rasterCommands); // Feed paper and cut commands.push(...this._feedAndCut(6)); const result = new Uint8Array(commands); const endTime = performance.now(); console.log('[EscPosGraphics] Command generation took:', (endTime - startTime).toFixed(2), 'ms'); console.log('[EscPosGraphics] Generated', result.length, 'bytes of commands'); return result; } /** * Remove blank lines from top and bottom of bitmap (OPTIMIZATION) * * @private * @param {Object} bitmap - Original bitmap * @returns {Object} Optimized bitmap */ _removeBlankLines(bitmap) { const { data, width, height, bytesPerLine } = bitmap; // Find first non-blank line from top let firstLine = 0; for (let y = 0; y < height; y++) { const lineStart = y * bytesPerLine; const lineEnd = lineStart + bytesPerLine; const lineData = data.slice(lineStart, lineEnd); // Check if line has any black pixels const hasContent = lineData.some(byte => byte !== 0); if (hasContent) { firstLine = y; break; } } // Find last non-blank line from bottom let lastLine = height - 1; for (let y = height - 1; y >= firstLine; y--) { const lineStart = y * bytesPerLine; const lineEnd = lineStart + bytesPerLine; const lineData = data.slice(lineStart, lineEnd); // Check if line has any black pixels const hasContent = lineData.some(byte => byte !== 0); if (hasContent) { lastLine = y; break; } } // Extract only the content lines const newHeight = lastLine - firstLine + 1; const newData = new Uint8Array(bytesPerLine * newHeight); for (let y = 0; y < newHeight; y++) { const srcStart = (firstLine + y) * bytesPerLine; const srcEnd = srcStart + bytesPerLine; const dstStart = y * bytesPerLine; newData.set(data.slice(srcStart, srcEnd), dstStart); } return { data: newData, width: width, height: newHeight, bytesPerLine: bytesPerLine }; } /** * Generate raster graphics commands (GS v 0) * This is the most compatible method for thermal printers * * @private * @param {Object} bitmap - Bitmap data * @returns {Array} Command bytes */ _generateRasterGraphics(bitmap) { const commands = []; const { data, width, height, bytesPerLine } = bitmap; // Calculate dimensions const widthBytes = bytesPerLine; const widthLow = widthBytes & 0xFF; const widthHigh = (widthBytes >> 8) & 0xFF; const heightLow = height & 0xFF; const heightHigh = (height >> 8) & 0xFF; console.log('[EscPosGraphics] Bitmap width:', width, 'pixels'); console.log('[EscPosGraphics] Bitmap height:', height, 'lines'); console.log('[EscPosGraphics] Bytes per line:', bytesPerLine); console.log('[EscPosGraphics] Width bytes (xL xH):', widthLow, widthHigh, '=', widthBytes); console.log('[EscPosGraphics] Height (yL yH):', heightLow, heightHigh, '=', height); console.log('[EscPosGraphics] Total data size:', data.length, 'bytes'); console.log('[EscPosGraphics] Expected data size:', bytesPerLine * height, 'bytes'); // GS v 0 - Print raster bitmap // Format: GS v 0 m xL xH yL yH d1...dk // m = mode (0 = normal, 1 = double width, 2 = double height, 3 = quadruple) commands.push(GS, 0x76, 0x30, 0x00); // GS v 0 m (m=0 for normal) commands.push(widthLow, widthHigh); // xL xH (width in bytes) commands.push(heightLow, heightHigh); // yL yH (height in dots) // Add bitmap data commands.push(...data); // Add line feed after image commands.push(LF); return commands; } /** * Alternative method: Print bitmap using ESC * command * Less compatible but works on some printers * * @private * @param {Object} bitmap - Bitmap data * @returns {Array} Command bytes */ _generateBitImageCommands(bitmap) { const commands = []; const { data, width, height, bytesPerLine } = bitmap; // Print line by line using ESC * command for (let y = 0; y < height; y++) { // ESC * m nL nH d1...dk // m = mode (33 = 24-dot double-density) const mode = 33; const nL = width & 0xFF; const nH = (width >> 8) & 0xFF; commands.push(ESC, 0x2A, mode, nL, nH); // Add line data const lineStart = y * bytesPerLine; const lineEnd = lineStart + bytesPerLine; commands.push(...data.slice(lineStart, lineEnd)); // Line feed commands.push(LF); } return commands; } /** * Feed paper and cut * * @private * @param {number} lines - Number of lines to feed * @returns {Array} Command bytes */ _feedAndCut(lines = 3) { const commands = []; // Feed lines for (let i = 0; i < lines; i++) { commands.push(LF); } // Cut paper (GS V m) // m = 0 (full cut), 1 (partial cut) commands.push(GS, 0x56, 0x00); // Initialize printer to force buffer flush and reset commands.push(ESC, 0x40); return commands; } /** * Split large bitmap into chunks for transmission * Some printers have buffer limitations * * @param {Uint8Array} commands - Complete command sequence * @param {number} chunkSize - Maximum chunk size in bytes * @returns {Array} Array of command chunks */ splitIntoChunks(commands, chunkSize = 1024) { const chunks = []; for (let i = 0; i < commands.length; i += chunkSize) { const chunk = commands.slice(i, i + chunkSize); chunks.push(chunk); } console.log('[EscPosGraphics] Split into', chunks.length, 'chunks'); return chunks; } /** * Generate test pattern for printer testing * * @returns {Uint8Array} Test pattern commands */ generateTestPattern() { const width = 576; const height = 200; const bytesPerLine = Math.ceil(width / 8); const data = new Uint8Array(bytesPerLine * height); // Create a test pattern (checkerboard) for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const isBlack = ((Math.floor(x / 8) + Math.floor(y / 8)) % 2) === 0; if (isBlack) { const byteIndex = y * bytesPerLine + Math.floor(x / 8); const bitIndex = 7 - (x % 8); data[byteIndex] |= (1 << bitIndex); } } } const bitmap = { data: data, width: width, height: height, bytesPerLine: bytesPerLine }; return this.generateBitmapCommands(bitmap); } } export default EscPosGraphics;