pos_bluetooth_thermal_printer/static/src/js/escpos_graphics.js

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;