From Driver to Image Commands: Mastering Thermal Printer Integration in Electron Apps
This article walks through the evolution of thermal printer solutions—from basic driver printing to command‑based and image‑based approaches—detailing hardware principles, Electron architecture, USB communication, Rust‑based monitoring, and practical code snippets for reliable store‑level receipt printing.
Thermal Printer Principle
A thermal printer works by heating a line of semiconductor elements that quickly raise the temperature of a chemically coated paper, causing a color change. The coating mainly contains BPA (2,2‑di(4‑hydroxyphenyl)propane). The paper consists of a protective layer, a color‑changing layer, and the base.
Quick Overview Guide
Background
Printing effects and conclusions for each era
Personal industry observations
Background
Store POS systems need to print receipts and cup stickers.
Device
POS: Windows system with pre‑installed receipt and sticker printer drivers.
Printers: Mostly USB‑connected, a few network‑connected.
System
Development framework: Electron.
Printing Solution Implementation
Stone Age (Driver Printing)
Requirement
Just print.
Solution
Use Electron to call the driver directly, prepare React class component templates, bundle with UMD, render in the renderer process.
Flow
Print message → printer process fetches template → combines socket data → generates real template → calls print method.
Practice
1. Send print data to printer process
this.window.webContents.send(ipcEvent.SET_PRINT_DATA, printData, traceId);2. Render template and trigger print
import { useEffect, useState } from 'react';
import AsyncComponent from './AsyncComponent';
interface PrintData {
templateUrl: string // template
data: PrintDataInfo // data
deviceInfo: PrintDeviceInfo // printer info
}
export default () => {
const [templateUrl, setTemplateUrl] = useState('');
const [templateProps, setTemplateProps] = useState({});
useEffect(() => {
ipcRenderer.on(ipcEvent.SET_PRINT_DATA, (
_e: Electron.IpcRendererEvent,
printData: PrintData,
traceId: TraceID
) => {
const { templateUrl, data, deviceInfo } = printData;
setTemplateUrl(templateUrl);
setTemplateProps(() => data);
setTimeout(() => {
ipcRenderer.send(
ipcEvent.EXEC_PRINT,
deviceInfo.deviceName,
traceId
);
}, 200);
});
}, []);
return templateUrl ? (
<AsyncComponent data={data} templateUrl={templateUrl} />
) : null;
};3. Execute print in main process
ipcMain.on(ipcEvent.EXEC_PRINT, (_, deviceName, traceId) => {
this.window.webContents.print(
{
silent: true,
printBackground: true,
copies: 1,
deviceName
},
(success, failReason) => {
logger.info(Lable.打印, traceId, deviceName, '打印结果', success, failReason);
if (success !== true) {
logger.error(Lable.打印, '', '打印失败', '驱动打印异常', failReason);
}
}
);
});Result
Problems
Cup‑sticker printer offset, incomplete prints; driver parameters need tweaking.
Blurry output.
Slow printing speed (several seconds delay).
Conclusion
Driver printing is unreliable and slow for large‑scale store deployment.
Bronze Age (Text Command Printing)
Requirement
No manual driver configuration, faster printing.
Solution
Use node-escpos-win to generate ESC/POS hex data, prepare CJS templates, generate data in the Node layer.
Flow
Print message → printer process fetches CJS template → combines socket data → generates hex data → sends to printer.
Knowledge
Receipt ESC/POS commands, barcode TSPL commands.
Practice
1. Generate buffer from template
const getPrinterBuffer = async (printData, traceId) => {
const { templateUrl, data } = printData;
const command = require(templateUrl);
const buffer = await command({ data });
return buffer;
};2. ESC/POS command example
const command = async (data => {
const cmd = require('escpos');
const p = new cmd.Printer('', { encoding: 'gbk', width: 48 });
p.newLine();
p.size(2, 2).align('lt').text(`${data.number}号 ${data.type}`);
p.size(1, 1);
p.text(data.shopName).text(data.time);
// ... many template lines ...
p.cut();
return p.buffer._buffer;
});
module.exports = command;3. Send buffer to USB device
const print = async (buffer, deviceName, traceId) => {
const printers = await getPrinter();
const portMap = await printPortMap();
const escpos = require('node-escpos-win');
const usb = escpos.GetDeviceList('USB');
const usbList = usb.list.filter(item => item.service === 'usbprint' || item.name === 'USB 打印支持');
printers.forEach(item => {
if (item.name === deviceName) {
const usbDevice = usbList.find(i => i.path.indexOf(portMap[i.portName]) !== -1);
const res = escpos.Print(usbDevice.path, buffer);
logger.info(String(res), traceId);
escpos.Disconnect(usbDevice.path);
}
});
};Result
Conclusion
Command printing is clear, fast, and does not require driver configuration, though font size is fixed.
Agriculture Age (Image Command Printing)
Background
Command printing looks ugly; marketing wants custom images for promotions.
Solution
Use ESC/POS/TSPL image commands: render canvas to PNG, convert to printer‑compatible bitmap.
Flow
Print message → render process fetches CJS template → canvas draws image → converts to printer format → sends to printer.
Practice
1. Draw a 1×1 black pixel
// target buffer: 1D 76 30 00 01 00 01 00 80
const escpos = require('node-escpos-win');
const getPixel = require('get-pixels');
const usb = escpos.GetDeviceList('USB');
const printer = usb.list.filter(i => i.service === 'usbprint' || i.name === 'USB 打印支持')[0];
getPixel('./dot.png', (err, { data, shape }) => {
const imgData = rgba2hex(data, shape);
const width = shape[0];
const height = shape[1];
const xL = Math.ceil((width / 8) % 256);
const xH = Math.floor((width / 8) / 256);
const yL = height % 256;
const yH = Math.floor(height / 256);
const buffer = Buffer.from([0x1d, 0x76, 0x30, 0, xL, xH, yL, yH, ...imgData]);
escpos.Print(printer.path, buffer);
});
function rgba2hex(arr, shape) {
const bitArr = [];
for (let i = 0; i < arr.length; i += 4) {
if (arr[i + 3] === 0) { bitArr.push(0); continue; }
const bit = (arr[i] + arr[i + 1] + arr[i + 2]) / 3 > 160 ? 0 : 1;
bitArr.push(bit);
}
const width = shape[0];
const needPad = width % 8 !== 0;
const newBitArr = [];
if (needPad) {
for (let r = 0; r < shape[1]; r++) {
const row = bitArr.slice(r * width, (r + 1) * width);
newBitArr.push(...row);
for (let p = 0; p < 8 - (width % 8); p++) newBitArr.push(0);
}
} else {
newBitArr.push(...bitArr);
}
const byteArr = [];
for (let i = 0; i < newBitArr.length; i += 8) {
const byte = (newBitArr[i] << 7) + (newBitArr[i + 1] << 6) + (newBitArr[i + 2] << 5) + (newBitArr[i + 3] << 4) + (newBitArr[i + 4] << 3) + (newBitArr[i + 5] << 2) + (newBitArr[i + 6] << 1) + newBitArr[i + 7];
byteArr.push(byte);
}
return new Uint8Array(byteArr);
}2. Canvas template generation
module.exports = (data) => {
const canvas = document.createElement('canvas');
canvas.width = 576;
let y = 0;
const ctx = canvas.getContext('2d');
ctx.font = '40px sans-bold';
ctx.fillStyle = '#231815';
ctx.fillText(data.number, 0, y + 52);
y += 64;
ctx.font = '24px sans-bold';
ctx.fillText(data.shopName, 0, y + 24);
y += 24;
// ... more drawing ...
canvas.height = y;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, 576, y);
return canvas.toDataURL();
};3. Send rendered image to printer
const command = require(templateUrl);
const buffer = command(data);
getPixel(buffer, (err, { data, shape }) => {
const imgData = rgba2hex(data, shape);
const width = shape[0];
const height = shape[1];
const xL = Math.ceil((width / 8) % 256);
const xH = Math.floor((width / 8) / 256);
const yL = height % 256;
const yH = Math.floor(height / 256);
const buf = Buffer.from([0x1d, 0x76, 0x30, 0, xL, xH, yL, yH, ...imgData]);
escpos.Print(printer.path, buf);
});Result
Conclusion
Image command printing offers full control over the receipt appearance, with only a slight speed penalty due to larger data size.
Industrial Age (Full‑Configurable Printing)
Solution
Build a web‑based template platform (similar to an electronic menu) that compiles templates into JS bundles, uploads to OSS, and distributes to stores.
Related Code
import replace from "@rollup/plugin-replace";
const templateMap = { LABEL: "BiaoQian", TICKET: "XiaoPiao" };
const template = process.env.template;
const templateConfig = process.env.templateConfig || "null";
const buildEnv = process.env.ENV || "dev";
const token = process.env.Token;
const buildTemplate = template?.split(",")?.map(item => templateMap[item])?.join("|");
if (!buildTemplate?.length) { throw new Error("请指定打包模板"); }
const files = glob.sync(`./src/package/@(${buildTemplate})**/index.@(tsx|ts)`);
const hash = `/${Math.random().toString(36).substring(2, 20)}/`;
export default {
input: { [entryName]: file },
output: {
sourcemap: true,
format: file.includes("tsx") ? "umd" : "cjs",
dir: `dist${hash}${entryName}`,
entryFileNames: `${entryName}.js`,
name: `Micro_${entryName}`,
},
plugins: [replace({ "process.env.templateConfig": templateConfig })]
};Conclusion
The template platform lets operations staff modify receipts without developer involvement, reducing deployment effort across many stores.
Information Age (Print Monitoring)
Solution
Develop a Rust‑based NAPI module to communicate over USB, send data, and read completion messages (e.g., after a cut command) to monitor print success.
Rust Module
#![deny(clippy::all)]
use std::ffi::CString;
use napi::bindgen_prelude::Buffer;
use winapi::um::fileapi::{CreateFileA, READ, WRITE, OPEN_EXISTING};
// ... omitted for brevity ...
#[napi]
pub fn send_usb(path: String, buffer: Buffer) -> String {
// open device, write buffer, read response, return string
}Usage in Electron
const { sendUsb } = require('./index.js');
const escpos = require('node-escpos-win');
const usb = escpos.GetDeviceList('USB');
const printer = usb.list.find(i => i.service === 'usbprint');
const buffer = Buffer.from([0x1d, 0x76, 0x30, 0, /* ... */]);
const res = sendUsb(printer.path, buffer);
console.log(res); // "complete"Conclusion
Using a native module enables reliable USB communication and allows the system to detect print completion, supporting retry logic and overall monitoring.
Reflections
Implemented an auto‑detect process for USB plug‑and‑play.
napi sometimes requires Windows patches on older versions.
Magnetic fields can cause signal loss, leading to missing or duplicated prints.
Ensuring correct hex data and reliable transmission is the core of successful printing.
Overall, mastering the hardware interface and data format makes thermal printer integration straightforward.
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
