import { PNG } from "pngjs"; import { encode } from "iconv-lite"; import { nfkd as unormNfkd } from "unorm"; import { Star } from "./types/star"; import { Epson } from "./types/epson"; import { PrinterType } from "./types/printer-type"; import { Interface } from "./interfaces/interface"; import { EpsonConfig } from "./types/epson-config"; import { StarConfig } from "./types/star-config"; interface ThermalPrinterOptions { interface: Interface; type: "epson" | "star"; width?: number; characterSet?: | keyof typeof EpsonConfig.CODE_PAGES | keyof typeof StarConfig.CODE_PAGES; removeSpecialCharacters?: boolean; lineCharacter?: string; options?: unknown; } export class ThermalPrinter { public printer: PrinterType; public interface: Interface; public buffer: Buffer | null; public config: Required; public type: ThermalPrinterOptions["type"]; constructor(conf: ThermalPrinterOptions) { const { interface: interfaceType, type = "epson" as const, characterSet = "SLOVENIA" as const, lineCharacter = "⎼", options, removeSpecialCharacters = false, width = 48, } = conf; this.interface = interfaceType; this.buffer = null; this.type = type; if (type === "star") { this.printer = new Star(); } else { this.printer = new Epson(); } this.config = { interface: interfaceType, type: type, width: width, characterSet: characterSet, removeSpecialCharacters: removeSpecialCharacters, lineCharacter: lineCharacter, options: options, }; // Set initial code page. this.setCharacterSet(this.config.characterSet); } setPrinterDriver(driver: any) { this.interface.driver = driver; } async execute() { try { if (!this.buffer) { return; } return await this.interface.execute(this.buffer); } catch (error) { throw error; } } cut() { this.append(this.printer.config.CTL_VT); this.append(this.printer.config.CTL_VT); this.append(this.printer.config.PAPER_FULL_CUT); this.append(this.printer.config.HW_INIT); } partialCut() { this.append(this.printer.config.CTL_VT); this.append(this.printer.config.CTL_VT); this.append(this.printer.config.PAPER_PART_CUT); this.append(this.printer.config.HW_INIT); } getWidth(): number { return this.config.width; } getText(): string { return this.buffer?.toString() || ""; } getBuffer() { return this.buffer; } setBuffer(newBuffer: Buffer) { this.buffer = Buffer.from(newBuffer); } clear() { this.buffer = null; } add(buffer: Buffer) { this.append(buffer); } print(text: string) { text = text || ""; this.append(text.toString()); } println(text: string) { this.print(text); this.append("\n"); } printVerticalTab() { this.append(this.printer.config.CTL_VT); } bold(enabled: boolean) { if (enabled) this.append(this.printer.config.TXT_BOLD_ON); else this.append(this.printer.config.TXT_BOLD_OFF); } underline(enabled: boolean) { if (enabled) this.append(this.printer.config.TXT_UNDERL_ON); else this.append(this.printer.config.TXT_UNDERL_OFF); } underlineThick(enabled: boolean) { if (enabled) this.append(this.printer.config.TXT_UNDERL2_ON); else this.append(this.printer.config.TXT_UNDERL_OFF); } upsideDown(enabled: boolean) { if (enabled) this.append(this.printer.config.UPSIDE_DOWN_ON); else this.append(this.printer.config.UPSIDE_DOWN_OFF); } invert(enabled: boolean) { if (enabled) this.append(this.printer.config.TXT_INVERT_ON); else this.append(this.printer.config.TXT_INVERT_OFF); } openCashDrawer() { if (this.config.type === "star") { this.append(this.printer.config.CD_KICK); } else { this.append(this.printer.config.CD_KICK_2); this.append(this.printer.config.CD_KICK_5); } } alignCenter() { this.append(this.printer.config.TXT_ALIGN_CT); } alignLeft() { this.append(this.printer.config.TXT_ALIGN_LT); } alignRight() { this.append(this.printer.config.TXT_ALIGN_RT); } setTypeFontA() { this.append(this.printer.config.TXT_FONT_A); } setTypeFontB() { this.append(this.printer.config.TXT_FONT_B); } setTextNormal() { this.append(this.printer.config.TXT_NORMAL); } setTextDoubleHeight() { this.append(this.printer.config.TXT_2HEIGHT); } setTextDoubleWidth() { this.append(this.printer.config.TXT_2WIDTH); } setTextQuadArea() { this.append(this.printer.config.TXT_4SQUARE); } setTextSize(height: number, width: number) { this.append(this.printer.setTextSize(height, width)); } // ----------------------------------------------------- NEW LINE ----------------------------------------------------- newLine() { this.append(this.printer.config.CTL_LF); } // ----------------------------------------------------- DRAW LINE ----------------------------------------------------- drawLine() { let str = '' for (var i = 0; i < this.config.width; i++) { str += this.config.lineCharacter; } this.append(str); this.newLine(); } // ----------------------------------------------------- LEFT RIGHT ----------------------------------------------------- leftRight(left: number, right: number) { this.append(left.toString()); var width = this.config.width - left.toString().length - right.toString().length; for (var i = 0; i < width; i++) { this.append(Buffer.from(" ")); } this.append(right.toString()); this.newLine(); } // ----------------------------------------------------- TABLE ----------------------------------------------------- table(data: any[]) { var cellWidth = this.config.width / data.length; for (var i = 0; i < data.length; i++) { this.append(data[i].toString()); var spaces = cellWidth - data[i].toString().length; for (var j = 0; j < spaces; j++) { this.append(Buffer.from(" ")); } } this.newLine(); } // ----------------------------------------------------- TABLE CUSTOM ----------------------------------------------------- // Options: text, align, width, bold tableCustom(data: any[]) { var cellWidth = this.config.width / data.length; var secondLine = []; var secondLineEnabled = false; for (var i = 0; i < data.length; i++) { var tooLong = false; var obj = data[i]; obj.text = obj.text.toString(); if (obj.width) { cellWidth = this.config.width * obj.width; } else if (obj.cols) { cellWidth = obj.cols; } if (obj.bold) { this.bold(true); } // If text is too wide go to next line if (cellWidth < obj.text.length) { tooLong = true; obj.originalText = obj.text; obj.text = obj.text.substring(0, cellWidth - 1); } if (obj.align === "CENTER") { const spaces = (cellWidth - obj.text.toString().length) / 2; for (let j = 0; j < spaces; j++) { this.append(Buffer.from(" ")); } if (obj.text !== "") this.append(obj.text); for (let j = 0; j < spaces - 1; j++) { this.append(Buffer.from(" ")); } } else if (obj.align === "RIGHT") { const spaces = cellWidth - obj.text.toString().length; for (let j = 0; j < spaces; j++) { this.append(Buffer.from(" ")); } if (obj.text !== "") this.append(obj.text); } else { if (obj.text !== "") this.append(obj.text); const spaces = cellWidth - obj.text.toString().length; for (let j = 0; j < spaces; j++) { this.append(Buffer.from(" ")); } } if (obj.bold) { this.bold(false); } if (tooLong) { secondLineEnabled = true; obj.text = obj.originalText.substring(cellWidth - 1); secondLine.push(obj); } else { obj.text = ""; secondLine.push(obj); } } this.newLine(); // Print the second line if (secondLineEnabled) { this.tableCustom(secondLine); } } // ----------------------------------------------------- IS PRINTER CONNECTED ----------------------------------------------------- async isPrinterConnected() { return await this.interface.isPrinterConnected(); } // ----------------------------------------------------- BEEP ----------------------------------------------------- beep() { this.append(this.printer.beep()); } // ----------------------------------------------------- PRINT QR ----------------------------------------------------- printQR( data: string, settings?: { cellSize?: number; correction?: "L" | "M" | "Q" | "H"; model?: number; } ) { this.append(this.printer.printQR(data, settings)); } // ----------------------------------------------------- PRINT BARCODE ----------------------------------------------------- printBarcode( data: string, type?: number, settings?: { hriPos?: number; hriFont?: number; width?: number; height?: number; } ) { this.append(this.printer.printBarcode(data, type, settings)); } // ----------------------------------------------------- PRINT MAXICODE ----------------------------------------------------- maxiCode( data: string, settings?: { mode?: number; } ) { this.append(this.printer.maxiCode(data, settings)); } // ----------------------------------------------------- PRINT CODE128 ----------------------------------------------------- code128( data: string, settings?: { width?: "LARGE" | "SMALL" | "MEDIUM"; height?: number; text?: number; } ) { this.append(this.printer.code128(data, settings)); } // ----------------------------------------------------- PRINT PDF417 ----------------------------------------------------- pdf417( data: string, settings?: { rowHeight?: number; width?: number; correction?: number; truncated?: boolean; columns?: number; } ) { this.append(this.printer.pdf417(data, settings)); } // ----------------------------------------------------- PRINT IMAGE BUFFER ----------------------------------------------------- async printImageBuffer(buffer: Buffer) { try { var png = PNG.sync.read(buffer); let buff = this.printer.printImageBuffer(png.width, png.height, png.data); this.append(buff); return buff; } catch (error) { throw error; } } // ------------------------------ RAW ------------------------------ async raw(text: Uint8Array) { try { return await this.interface.execute(text); } catch (error) { throw error; } } // ------------------------------ Set character set ------------------------------ setCharacterSet( characterSet: Required["characterSet"] ) { const buffer = this.printer.config[`CODE_PAGE_${characterSet}`]; if (buffer) { this.append(buffer); this.config.characterSet = characterSet; } else { throw new Error(`Code page not recognized: '${characterSet}'`); } } // ------------------------------ Append ------------------------------ append(textOrBuffer: string | Buffer | null) { let buff: Buffer | null = null; if (typeof textOrBuffer === "string") { let text: string = textOrBuffer; // Remove special characters. if (this.config.removeSpecialCharacters) { const combining = /[\u0300-\u036F]/g; text = unormNfkd(text).replace(combining, ""); } let endBuff = null; for (const char of text) { let code: string | Buffer = char; // eslint-disable-next-line no-control-regex if (!/^[\x00-\x7F]$/.test(char)) { // Test if the active code page can print the current character. try { code = encode( char, this.printer.config.CODE_PAGES[this.config.characterSet] ); } catch (e) { // Probably encoding not recognized. console.error(e); code = "?"; } if (code.toString() === "?") { // Character not available in active code page, now try all other code pages. for (const tmpCodePageKey of Object.keys( this.printer.config.CODE_PAGES )) { const tmpCodePage = this.printer.config.CODE_PAGES[tmpCodePageKey]; try { code = encode(char, tmpCodePage); } catch (e) { // Probably encoding not recognized. console.error(e); } if (code.toString() !== "?") { // We found a match, change active code page. this.config.characterSet = tmpCodePageKey as any; code = Buffer.concat([ this.printer.config[`CODE_PAGE_${tmpCodePageKey}`], code, ]); break; } } } } endBuff = endBuff ? Buffer.concat([endBuff, Buffer.from(code)]) : Buffer.from(code); } buff = endBuff; } else { buff = textOrBuffer; } // Append buffer if (buff) { if (this.buffer) { this.buffer = Buffer.concat([this.buffer, buff]); } else { this.buffer = buff; } } } }