Version: 1.0.2
Enterprise-grade library for generating ESC/POS, StarLine, and StarPRNT command streams for thermal receipt printers. Features memory-efficient image processing, RLE compression, and streaming transmission for large payloads.
- ✅ Multi-protocol support: ESC/POS, StarLine, StarPRNT
- ✅ 33 built-in printer definitions with automatic capability detection
- ✅ Memory-efficient image processing - no stack overflow on large images
- ✅ Strip-based raster encoding - automatically splits large images into 256px-height strips to prevent memory overflow
- ✅ RLE compression for supported printers (40-98% size reduction)
- ✅ Streaming transmission with backpressure support
- ✅ TypeScript definitions included
- ✅ Works with: React Native, React Native Windows, Node.js, Browser
npm install @point-of-sale/receipt-printer-encoder
import ReceiptPrinterEncoder from "@point-of-sale/receipt-printer-encoder"; const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", }); encoder .initialize() .line("Hello World!") .newline() .line("Receipt printed successfully") .cut(); const data = encoder.encode(); // Send `data` (Uint8Array) to your printer via TCP/USB/Bluetooth
A typical POS receipt with formatting, tables, and barcodes:
import ReceiptPrinterEncoder from "@point-of-sale/receipt-printer-encoder"; async function printNormalReceipt(printerService) { const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", columns: 42, }); encoder .initialize() // Header .align("center") .bold(true) .size(2, 2) .line("MY STORE") .size(1, 1) .bold(false) .line("123 Main Street") .line("City, State 12345") .line("Tel: (555) 123-4567") .newline() // Receipt info .align("left") .line("================================") .line(`Date: ${new Date().toLocaleDateString()}`) .line(`Time: ${new Date().toLocaleTimeString()}`) .line(`Receipt #: INV-2026-001234`) .line("================================") .newline() // Items table .table( [ { width: 20, align: "left" }, { width: 8, align: "right" }, { width: 10, align: "right" }, ], [ ["Item", "Qty", "Price"], ["--------------------------------", "", ""], ["Coffee Latte", "2", "8ドル.50"], ["Croissant", "1", "3ドル.25"], ["Orange Juice", "2", "6ドル.00"], ["Sandwich", "1", "7ドル.50"], ["--------------------------------", "", ""], ] ) // Totals .newline() .table( [ { width: 22, align: "left" }, { width: 16, align: "right" }, ], [ ["Subtotal:", "25ドル.25"], ["Tax (8%):", "2ドル.02"], ["", "--------"], ] ) .bold(true) .table( [ { width: 22, align: "left" }, { width: 16, align: "right" }, ], [["TOTAL:", "27ドル.27"]] ) .bold(false) .newline() // Payment .line("Payment: VISA ****4242") .newline() // Barcode .align("center") .barcode("INV2026001234", "code128", { height: 60, text: true, }) .newline() // Footer .line("Thank you for your purchase!") .line("Please come again") .newline() .newline() // Cut paper .cut(); // Send to printer const data = encoder.encode(); await printerService.print(data); }
For receipts that need custom fonts, complex layouts, or graphics, render the entire receipt as an image:
Technical: Strip-Based Raster Encoding
Large raster images are automatically split into 256-pixel-height strips to prevent memory overflow. Each strip generates a separate ESC/POS GS v 0 command, which the printer concatenates seamlessly as continuous output. This architecture prevents single large buffer allocations while maintaining print quality.
Binary GS v 0 Command Structure:
0x1D 0x76 0x30- GS v 0 command headerm(1 byte) - Mode (0x00 = uncompressed, 0x01 = RLE compressed)xL xH(2 bytes) - Width in bytes (little-endian), each strip uses same width as full imageyL yH(2 bytes) - Height in pixels (little-endian), typically 256 for all strips except last[raster data]- 1-bit monochrome bitmap (MSB first, row-major)
Example for 500px tall ×ばつ 576px wide image:
- Strip 1: 256 rows → GS v 0 header + width (72 bytes) + height (256) + 18,432 bytes raster data
- Strip 2: 244 rows → GS v 0 header + width (72 bytes) + height (244) + 17,568 bytes raster data
- Memory pool: 4MB max, prevents allocation failures
import ReceiptPrinterEncoder from "@point-of-sale/receipt-printer-encoder"; /** * Print a full raster receipt (entire receipt is an image) * Useful for custom fonts, complex layouts, or branded designs * Automatically handles large images via strip-based encoding (256px height default) */ async function printRasterReceipt(printerService, receiptImageData) { const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", imageMode: "raster", // Use raster mode for full-page images }); // receiptImageData should be an ImageData object with: // - data: Uint8ClampedArray (RGBA pixels) // - width: number (must be multiple of 8, typically 384 or 576) // - height: number (can be unlimited; library auto-splits into 256px strips) encoder.initialize(); // Print the raster receipt - library handles large images via strip-based encoding await encoder.image( receiptImageData, receiptImageData.width, receiptImageData.height ); encoder.newline().cut(); const data = encoder.encode(); await printerService.print(data); } /** * Example: Create receipt image from HTML/Canvas (React Native) */ async function createReceiptImage(htmlContent) { // Option 1: Use react-native-view-shot to capture a View // Option 2: Use a headless canvas library // Option 3: Generate on server and send image data // The image should be: // - Width: 384px (58mm paper) or 576px (80mm paper) at 203 DPI // - Monochrome or grayscale (library converts to 1-bit) // - PNG or raw ImageData format return { data: new Uint8ClampedArray(/* RGBA pixel data */), width: 384, // Must be multiple of 8 height: 800, // Variable based on content }; }
Recommended image dimensions:
| Paper Width | DPI | Pixel Width | Max Height |
|---|---|---|---|
| 58mm | 203 | 384px | Unlimited |
| 80mm | 203 | 576px | Unlimited |
| 80mm | 180 | 512px | Unlimited |
Height is unlimited: Images are automatically split into 256-pixel-height strips. For example, a 2000px tall image is divided into 8 strips (7 ×ばつ 256px + 1 ×ばつ 48px). The library maintains a 4MB memory pool per strip to prevent allocation failures. Memory-efficient processing prevents main thread blocking during large image encoding.
Print logos, signatures, or graphics within a text receipt:
import ReceiptPrinterEncoder from "@point-of-sale/receipt-printer-encoder"; /** * Print receipt with embedded logo */ async function printReceiptWithLogo(printerService, logoImageData) { const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", }); encoder.initialize(); // Center and print logo encoder.align("center"); // Logo should be appropriately sized (e.g., 200x80 pixels) await encoder.image(logoImageData, logoImageData.width, logoImageData.height); encoder .newline() .bold(true) .size(2, 2) .line("RECEIPT") .size(1, 1) .bold(false) .newline() .align("left") .line("Order #12345") .line(`Date: ${new Date().toLocaleDateString()}`) .newline() // ... rest of receipt .cut(); const data = encoder.encode(); await printerService.print(data); } /** * Print signature capture */ async function printWithSignature(printerService, signatureImageData) { const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", }); encoder.initialize().line("Customer Signature:").newline(); // Print signature (typically 300x100 pixels) await encoder.image( signatureImageData, signatureImageData.width, signatureImageData.height ); encoder .newline() .line("_".repeat(30)) .line("I agree to the terms above") .newline() .cut(); const data = encoder.encode(); await printerService.print(data); }
For large images or full raster receipts, use streaming to prevent printer buffer overflow. Technical: Strip-Level Backpressure
Images are processed using strip-based encoding: each 256-pixel-height strip generates a separate GS v 0 command. Backpressure control operates at the strip level, not the monolithic image level. The encoder yields control after every 4 strips to prevent UI blocking. Memory pool (4MB max) ensures consistent performance regardless of image height.
import ReceiptPrinterEncoder from "@point-of-sale/receipt-printer-encoder"; /** * Print large image with streaming and backpressure control * This prevents printer buffer overflow and app crashes * Backpressure operates at 256px strip boundaries */ async function printLargeImage(printerService, largeImageData) { const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", imageMode: "raster", }); encoder.initialize(); // Add the large image - processed memory-efficiently via strip-based encoding // For example, a 2000px tall image becomes 8 strips (×ばつ256px + ×ばつ48px) // Each strip generates a separate GS v 0 command await encoder.image( largeImageData, largeImageData.width, largeImageData.height ); encoder.cut(); // Use streaming transmission with backpressure let totalBytesSent = 0; let stripCount = 0; for await (const chunk of encoder.encodeAsyncIterator({ chunkSize: 512, // 512 bytes per chunk (optimal for most printers) onChunkSent: async (info) => { console.log( `Progress: ${info.index + 1}/${info.total} chunks (${Math.round( (info.bytesSent / info.totalBytes) * 100 )}%)` ); // Track which strip we're in (each strip ≈ 18-20KB for 576px width) const estimatedStripIndex = Math.floor(info.bytesSent / 20480); if (estimatedStripIndex > stripCount) { stripCount = estimatedStripIndex; console.log(` Strip ${stripCount}: Async yield executed, resuming...`); } // Optional: Add delay between chunks to prevent buffer overflow // Adjust based on printer speed and connection quality if (!info.isLast) { await delay(5); // 5ms delay between chunks } }, })) { // Send chunk to printer await printerService.sendChunk(chunk); totalBytesSent += chunk.length; } console.log( `Print complete: ${totalBytesSent} bytes sent across ${stripCount} strips` ); } // Helper function function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Complete working example showing internals: * - Image encoding with strip-based architecture * - Async/await with yield control after every 4 strips * - Memory pool management (4MB max per strip) */ async function printWithStripTracking(printerService, imageData) { const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", imageMode: "raster", }); encoder.initialize(); // Image 1000px tall ×ばつ 576px wide example: // - Strip count: Math.ceil(1000 / 256) = 4 strips // - Each strip width: 576 / 8 = 72 bytes // - Strip 1-3: 256 rows ×ばつ 72 bytes = 18,432 bytes each // - Strip 4: 232 rows ×ばつ 72 bytes = 16,704 bytes // - Total: ~73,572 bytes (easily fits in 4MB pool) // // GS v 0 command per strip: // [0x1D 0x76 0x30] [mode:1] [xL:72 xH:0] [yL:256 yH:0] [18432 bytes raster data] // // Async execution: yield after strip 0, 4 (if exists) // - Strip 0 processed: await new Promise(resolve => setTimeout(resolve, 0)); // - Strip 1 processed: no yield (i=1, 1%4=1) // - Strip 2 processed: no yield (i=2, 2%4=2) // - Strip 3 processed: no yield (i=3, 3%4=3) // - Strip 4 processed: yield (i=4, 4%4=0 && i>0) console.log("Encoding image with strip-based architecture..."); const startTime = Date.now(); await encoder.image(imageData, imageData.width, imageData.height); encoder.cut(); console.log(`Image encoding complete in ${Date.now() - startTime}ms`); console.log("Streaming to printer..."); let chunkCount = 0; for await (const chunk of encoder.encodeAsyncIterator({ chunkSize: 512 })) { await printerService.sendChunk(chunk); chunkCount++; } console.log(`Sent ${chunkCount} chunks to printer`); } /** * Print large image with streaming and backpressure control * This prevents printer buffer overflow and app crashes */ async function printLargeImage(printerService, largeImageData) { const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", imageMode: "raster", }); encoder.initialize(); // Add the large image (this is processed memory-efficiently) await encoder.image( largeImageData, largeImageData.width, largeImageData.height ); encoder.cut(); // Use streaming transmission with backpressure let totalBytesSent = 0; for await (const chunk of encoder.encodeAsyncIterator({ chunkSize: 512, // 512 bytes per chunk (optimal for most printers) onChunkSent: async (info) => { console.log( `Progress: ${info.index + 1}/${info.total} chunks (${Math.round( (info.bytesSent / info.totalBytes) * 100 )}%)` ); // Optional: Add delay between chunks to prevent buffer overflow // Adjust based on printer speed and connection quality if (!info.isLast) { await delay(5); // 5ms delay between chunks } }, })) { // Send chunk to printer await printerService.sendChunk(chunk); totalBytesSent += chunk.length; } console.log(`Print complete: ${totalBytesSent} bytes sent`); } // Helper function function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
Advanced: With retry logic and error handling:
/** * Enterprise-grade large image printing with retry logic */ async function printLargeImageWithRetry( printerService, imageData, options = {} ) { const { maxRetries = 3, chunkSize = 512, chunkDelay = 5, timeout = 30000, } = options; const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", imageMode: "raster", }); encoder.initialize(); await encoder.image(imageData, imageData.width, imageData.height); encoder.cut(); const startTime = Date.now(); let currentChunkIndex = 0; for await (const chunk of encoder.encodeAsyncIterator({ chunkSize })) { // Timeout check if (Date.now() - startTime > timeout) { throw new Error("Print timeout exceeded"); } // Retry logic for each chunk let retries = 0; let success = false; while (!success && retries < maxRetries) { try { await printerService.sendChunk(chunk); success = true; } catch (error) { retries++; console.warn( `Chunk ${currentChunkIndex} failed, retry ${retries}/${maxRetries}` ); if (retries >= maxRetries) { throw new Error( `Failed to send chunk ${currentChunkIndex} after ${maxRetries} retries: ${error.message}` ); } // Wait before retry (exponential backoff) await delay(100 * Math.pow(2, retries)); } } // Delay between chunks await delay(chunkDelay); currentChunkIndex++; } return { success: true, chunks: currentChunkIndex }; }
import TcpSocket from "react-native-tcp-socket"; import ReceiptPrinterEncoder from "@point-of-sale/receipt-printer-encoder"; class TcpPrinterService { constructor(host, port = 9100) { this.host = host; this.port = port; this.socket = null; } async connect() { return new Promise((resolve, reject) => { this.socket = TcpSocket.createConnection( { host: this.host, port: this.port }, () => { console.log(`Connected to printer at ${this.host}:${this.port}`); resolve(); } ); this.socket.on("error", (error) => { console.error("Printer connection error:", error); reject(error); }); this.socket.on("close", () => { console.log("Printer connection closed"); this.socket = null; }); // Connection timeout setTimeout(() => { if (!this.socket) { reject(new Error("Connection timeout")); } }, 5000); }); } async sendChunk(data) { return new Promise((resolve, reject) => { if (!this.socket) { reject(new Error("Not connected")); return; } // Convert Uint8Array to Buffer for react-native-tcp-socket const buffer = Buffer.from(data); this.socket.write(buffer, "binary", (error) => { if (error) { reject(error); } else { resolve(); } }); }); } async print(data) { // For small payloads, send all at once await this.sendChunk(data); } async printLargeImage(imageData) { const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", imageMode: "raster", }); encoder.initialize(); await encoder.image(imageData, imageData.width, imageData.height); encoder.cut(); // Stream with backpressure for await (const chunk of encoder.encodeAsyncIterator({ chunkSize: 512, onChunkSent: async (info) => { // Small delay to prevent buffer overflow await this.delay(5); }, })) { await this.sendChunk(chunk); } } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } disconnect() { if (this.socket) { this.socket.destroy(); this.socket = null; } } } // Usage async function printReceipt() { const printer = new TcpPrinterService("192.168.1.100", 9100); try { await printer.connect(); const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", }); encoder.initialize().line("Hello from React Native!").cut(); await printer.print(encoder.encode()); } finally { printer.disconnect(); } }
For Windows-specific TCP communication using a custom native module:
import { NativeModules } from "react-native"; import ReceiptPrinterEncoder from "@point-of-sale/receipt-printer-encoder"; const { TcpPrinterModule } = NativeModules; class WindowsPrinterService { constructor(host, port = 9100) { this.host = host; this.port = port; this.isConnected = false; } async connect() { try { await TcpPrinterModule.connect(this.host, this.port); this.isConnected = true; console.log(`Connected to printer at ${this.host}:${this.port}`); } catch (error) { console.error("Failed to connect:", error); throw error; } } async sendChunk(data) { if (!this.isConnected) { throw new Error("Not connected to printer"); } // Convert Uint8Array to base64 for native module transfer const base64Data = this.uint8ArrayToBase64(data); await TcpPrinterModule.sendData(base64Data); } async print(data) { await this.sendChunk(data); } /** * Print large image with streaming * Prevents buffer overflow on Windows thermal printers */ async printLargeImage(imageData, options = {}) { const { chunkSize = 512, chunkDelay = 10 } = options; const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", imageMode: "raster", }); encoder.initialize(); await encoder.image(imageData, imageData.width, imageData.height); encoder.cut(); let progress = 0; for await (const chunk of encoder.encodeAsyncIterator({ chunkSize, onChunkSent: async (info) => { progress = Math.round((info.bytesSent / info.totalBytes) * 100); // Report progress to UI if needed if (options.onProgress) { options.onProgress(progress, info); } // Delay to prevent overwhelming the printer buffer if (!info.isLast) { await this.delay(chunkDelay); } }, })) { await this.sendChunk(chunk); } return { success: true, progress: 100 }; } uint8ArrayToBase64(uint8Array) { let binary = ""; for (let i = 0; i < uint8Array.length; i++) { binary += String.fromCharCode(uint8Array[i]); } return btoa(binary); } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async disconnect() { if (this.isConnected) { await TcpPrinterModule.disconnect(); this.isConnected = false; } } } // Usage with progress reporting async function printLargeReceipt(imageData) { const printer = new WindowsPrinterService("192.168.1.100", 9100); try { await printer.connect(); await printer.printLargeImage(imageData, { chunkSize: 512, chunkDelay: 10, onProgress: (percent, info) => { console.log( `Printing: ${percent}% (chunk ${info.index + 1}/${info.total})` ); // Update your UI progress bar here }, }); console.log("Print completed successfully!"); } catch (error) { console.error("Print failed:", error); throw error; } finally { await printer.disconnect(); } }
Example Windows Native Module Interface (C#):
// TcpPrinterModule.cs (Windows native module) [ReactModule("TcpPrinterModule")] public class TcpPrinterModule { private TcpClient _client; private NetworkStream _stream; [ReactMethod] public async Task ConnectAsync(string host, int port) { _client = new TcpClient(); await _client.ConnectAsync(host, port); _stream = _client.GetStream(); } [ReactMethod] public async Task SendDataAsync(string base64Data) { byte[] data = Convert.FromBase64String(base64Data); await _stream.WriteAsync(data, 0, data.Length); await _stream.FlushAsync(); } [ReactMethod] public Task DisconnectAsync() { _stream?.Close(); _client?.Close(); return Task.CompletedTask; } }
// Get list of all supported printers const printers = ReceiptPrinterEncoder.printerModels; console.log(printers); // [ // { id: 'epson-tm-t88vi', name: 'Epson TM-T88VI' }, // { id: 'star-tsp100iv', name: 'Star TSP100IV' }, // ... // ] // Use specific printer model const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", }); // Access printer capabilities console.log(encoder.printerCapabilities); // { // language: 'esc-pos', // codepages: 'epson', // fonts: { A: { size: '12x24', columns: 42 }, B: { size: '9x17', columns: 56 } }, // images: { supportsCompression: true }, // ... // }
| Brand | Models | RLE Compression |
|---|---|---|
| Epson | TM-T88V, TM-T88VI, TM-T88VII, TM-T70II, TM-m30II, TM-m30III, TM-T20III, TM-T20IV, TM-P20II | ✅ Yes |
| Epson (Legacy) | TM-T88II, TM-T88III, TM-T88IV, TM-T70, TM-T20II | ❌ No |
| Star | mC-Print2, mPOP, SM-L200, TSP100III, TSP100IV, TSP650, TSP650II | ❌ No |
| Bixolon | SRP-350III | ✅ Yes |
| Bixolon (Legacy) | SRP-350 | ❌ No |
| Citizen | CT-S310II | ✅ Yes |
| Fujitsu | FP-1000 | ✅ Yes |
| HP | A779 | ✅ Yes |
| Xprinter | XP-N160II, XP-T80Q | ❌ No |
| Generic | POS-5890, POS-8360, MPT-II, Youku-58T | ❌ No |
// Without specific printer model const encoder = new ReceiptPrinterEncoder({ language: "esc-pos", // 'esc-pos', 'star-prnt', or 'star-line' columns: 42, // Paper width in characters imageMode: "raster", // 'raster' or 'column' });
interface ReceiptPrinterEncoderOptions { printerModel?: string; // Printer model ID (recommended) language?: string; // 'esc-pos' | 'star-prnt' | 'star-line' columns?: number; // 32, 35, 42, 44, or 48 imageMode?: string; // 'column' | 'raster' feedBeforeCut?: number; // Lines to feed before cut createCanvas?: Function; // Canvas factory for image processing }
| Method | Returns | Description |
|---|---|---|
initialize() |
this |
Reset printer to default state |
text(value) |
this |
Print text |
line(value) |
this |
Print text with newline |
newline(count?) |
this |
Print empty lines |
align(value) |
this |
Set alignment: 'left', 'center', 'right' |
bold(enable?) |
this |
Toggle bold |
italic(enable?) |
this |
Toggle italic |
underline(enable?) |
this |
Toggle underline |
invert(enable?) |
this |
Toggle inverted (white on black) |
size(width, height) |
this |
Set text size (1-8) |
font(value) |
this |
Set font: 'A', 'B', or size string |
table(columns, rows) |
this |
Print table |
rule() |
this |
Print horizontal rule |
box(options, callback) |
this |
Draw box with content |
barcode(value, symbology, options?) |
this |
Print barcode |
qrcode(value, options?) |
this |
Print QR code |
pdf417(value, options?) |
this |
Print PDF417 |
image(data, width, height) |
Promise<this> |
Print image (async!) |
cut(type?) |
this |
Cut paper: 'full' or 'partial' |
pulse() |
this |
Open cash drawer |
raw(data) |
this |
Send raw bytes |
encode() |
Uint8Array |
Get encoded data |
encodeAsyncIterator(options?) |
AsyncGenerator |
Stream encoded data |
interface IImageData { data: Uint8ClampedArray | number[]; // RGBA pixels (4 bytes per pixel) width: number; // Must be multiple of 8 height: number; }
interface EncodeAsyncIteratorOptions { chunkSize?: number; // Bytes per chunk (default: 512) onChunkSent?: (info: ChunkInfo) => void | Promise<void>; } interface ChunkInfo { index: number; // Current chunk index (0-based) total: number; // Total chunks bytes: number; // Bytes in this chunk bytesSent: number; // Cumulative bytes sent totalBytes: number; // Total payload size isLast: boolean; // Is this the last chunk? }
Problem: Printing large raster receipts causes the app to crash or freeze.
Solution: Use encodeAsyncIterator() for streaming transmission:
// ❌ Bad - loads entire payload into memory const data = encoder.encode(); await printer.print(data); // ✅ Good - streams in chunks for await (const chunk of encoder.encodeAsyncIterator({ chunkSize: 512 })) { await printer.sendChunk(chunk); }
Problem: Large images print partially or printer stops mid-print.
Solution: Add delays between chunks to let the printer process:
for await (const chunk of encoder.encodeAsyncIterator({ chunkSize: 256, // Smaller chunks onChunkSent: async () => { await delay(10); // 10ms delay between chunks }, })) { await printer.sendChunk(chunk); }
Problem: Error: ImageEncoder: width must be a multiple of 8
Solution: Resize your image to valid width:
// Common valid widths for 80mm paper at 203 DPI const validWidths = [384, 512, 576]; // Choose based on printer function adjustImageWidth(imageData) { const targetWidth = Math.ceil(imageData.width / 8) * 8; // Resize image to targetWidth }
Problem: Text prints as garbage characters.
Solution: Ensure correct codepage:
const encoder = new ReceiptPrinterEncoder({ printerModel: "epson-tm-t88vi", }); encoder .initialize() .codepage("auto") // Auto-detect codepage .line("Special chars: áéíóú ñ €") .cut();
Problem: Native module connection times out.
Solution: Increase timeout and add retry logic:
async function connectWithRetry(printer, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { await printer.connect(); return; } catch (error) { console.warn(`Connection attempt ${i + 1} failed:`, error); await delay(1000 * (i + 1)); // Exponential backoff } } throw new Error("Failed to connect after multiple attempts"); }
Problem: Star printers don't support RLE compression.
Solution: Use column mode (default for Star):
const encoder = new ReceiptPrinterEncoder({ printerModel: "star-tsp100iv", // Automatically uses star-prnt language imageMode: "column", // Column mode for Star printers });
Problem: Multiple prints stack up and printer can't keep up.
Solution: Implement a print queue with proper waiting:
class PrintQueue { constructor(printer) { this.printer = printer; this.queue = []; this.isProcessing = false; } async add(encoder) { this.queue.push(encoder); if (!this.isProcessing) { await this.process(); } } async process() { this.isProcessing = true; while (this.queue.length > 0) { const encoder = this.queue.shift(); for await (const chunk of encoder.encodeAsyncIterator({ chunkSize: 512, })) { await this.printer.sendChunk(chunk); } // Wait between print jobs await delay(500); } this.isProcessing = false; } }
MIT License - See LICENSE for details.
Based on ReceiptPrinterEncoder by Niels Leenheer.
Enhanced with enterprise-grade image processing, RLE compression, and streaming support.