286 lines
9.3 KiB
JavaScript
Executable File
286 lines
9.3 KiB
JavaScript
Executable File
/** @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<Uint8Array>} 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;
|