Operations 26 min read

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.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
From Driver to Image Commands: Mastering Thermal Printer Integration in Electron Apps

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.

ElectronNode.jsescposrust napithermal printerusb communication
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.