npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@razmans/thermal-printer-encoder

v1.0.0

Published

Turn a declarative receipt document (text, styling, images) into an ESC/POS byte stream ready to send over any serial or Bluetooth transport.

Readme

thermal-printer-encoder

Turn a plain, declarative description of a receipt — text, styling, images, cuts — into an ESC/POS byte stream that thermal receipt printers understand.

This library does not talk to any printer. It has no dependency on serial ports, Bluetooth, USB, or Node's net/serialport modules, so it works the same in Node, a browser, or React Native. You get a Uint8Array back; how you send it (Web Bluetooth, serialport, a native Bluetooth SPP socket, a network print server, lp, whatever) is entirely up to you.

Install

npm install thermal-printer-encoder

Quick start

import { encodeReceipt } from "thermal-printer-encoder";

const bytes = encodeReceipt({
  nodes: [
    { type: "text", value: "MY STORE", style: { align: "center", bold: true, width: 2, height: 2 } },
    { type: "text", value: "123 Main St", style: { align: "center" } },
    { type: "rule" },
    { type: "text", value: "Coffee            $3.50" },
    { type: "text", value: "Bagel             $2.25" },
    { type: "rule" },
    { type: "text", value: "TOTAL             $5.75", style: { bold: true } },
    { type: "feed", lines: 2 },
    { type: "cut" },
  ],
});

// `bytes` is a Uint8Array of raw ESC/POS commands — send it over whatever
// transport you're using, e.g.:
await bluetoothCharacteristic.writeValue(bytes);
// or
serialPort.write(Buffer.from(bytes));

Document model

A receipt is just { nodes: ReceiptNode[] }. Nodes are processed top to bottom.

text

{
  type: "text",
  value: "Hello world",
  style: {
    align: "left" | "center" | "right",
    bold: boolean,
    underline: boolean,
    invert: boolean,       // white-on-black, if the printer supports it
    width: 1-8,             // horizontal size multiplier
    height: 1-8,            // vertical size multiplier
  },
}

Each text node ends with a line feed and resets style back to normal for the next node, so you never have to manually turn formatting off.

image

{
  type: "image",
  source: pngOrJpegBuffer | { width, height, data: rgbaBytes },
  align: "left" | "center" | "right",
  width: 384,                          // resize to this many dots (aspect-ratio preserved)
  dither: "floyd-steinberg" | "threshold", // default: "floyd-steinberg"
  threshold: 128,                      // only used when dither: "threshold"
}

source accepts either:

  • Already-decoded RGBA pixel data (e.g. ImageData from a <canvas>): { width, height, data } — works everywhere (Node, browser, React Native), no extra setup, or
  • A raw PNG or JPEG file buffer (Uint8Array/ArrayBuffer) — the core package has no bundled decoder for this (see below), so it only works if you pass a decodeImage function in encodeReceipt's options.

Decoding PNG/JPEG buffers

The main thermal-printer-encoder entry point has zero Node-specific dependencies, so it can be bundled for a browser or React Native without pulling in a PNG/JPEG decoder you'll never use there. If you want to hand encodeReceipt a raw file buffer instead of pre-decoded pixels:

  • In Node, pass the bundled decoder (uses pngjs / jpeg-js) via the /node subpath:
    import { encodeReceipt } from "thermal-printer-encoder";
    import { decodeImageBuffer } from "thermal-printer-encoder/node";
    
    const bytes = encodeReceipt(document, { decodeImage: decodeImageBuffer });
  • In a browser, decode via <canvas> yourself and pass the resulting RasterImage directly as the image node's source — no decodeImage option needed:
    async function loadRasterImage(url: string): Promise<RasterImage> {
      const img = await new Promise<HTMLImageElement>((resolve, reject) => {
        const el = new Image();
        el.onload = () => resolve(el);
        el.onerror = reject;
        el.src = url;
      });
      const canvas = document.createElement("canvas");
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      const ctx = canvas.getContext("2d")!;
      ctx.drawImage(img, 0, 0);
      const { data, width, height } = ctx.getImageData(0, 0, canvas.width, canvas.height);
      return { width, height, data: new Uint8Array(data.buffer) };
    }

Images are converted to 1-bit monochrome with Floyd–Steinberg error diffusion by default, which reproduces photos and logos far better than a flat black/white threshold on thermal printers' limited resolution. Pass dither: "threshold" if you'd rather have hard edges (e.g. for line-art logos that are already black and white).

Most 58mm printers print at 384 dots wide; most 80mm printers print at 576. Set width on the image node (or paperWidthDots in encodeReceipt's options) to match your printer, or the image will be clipped to the paper width.

barcode

{
  type: "barcode",
  data: "012345678905",
  barcodeType: "upc-a" | "upc-e" | "ean13" | "ean8" | "code39" | "itf" | "codabar" | "code93" | "code128",
  align: "left" | "center" | "right",
  height: 80,               // bar height in dots
  width: 3,                 // bar module width in dots, 2-6
  textPosition: "below",    // "none" | "above" | "below" | "both"
}

data must satisfy the chosen symbology's character set and length rules (e.g. ean13 needs 12-13 digits, code128 accepts arbitrary ASCII) — this library does not validate or checksum the payload, it passes it straight to the printer's barcode engine.

qrcode

{
  type: "qrcode",
  data: "https://example.com",
  align: "left" | "center" | "right",
  size: 6,                    // module (dot) size, 1-16
  errorCorrection: "M",       // "L" | "M" | "Q" | "H" — higher = more damage-tolerant, less capacity
}

feed, rule, cut

{ type: "feed", lines: 3 }
{ type: "rule", char: "-", length: 32 }   // draws a divider line of repeated characters
{ type: "cut", mode: "full" | "partial" } // default: "full"

Raw ESC/POS commands

These are the raw byte sequences the encoder emits internally (src/escpos/commands.ts). Useful as a reference if you need to append your own bytes to the Uint8Array returned by encodeReceipt (e.g. to kick a cash drawer, which has no dedicated document node yet).

| Command | Bytes | What it does | | --- | --- | --- | | INIT | 1B 40 | Resets the printer to its default state. | | ALIGN_LEFT | 1B 61 00 | Left-aligns subsequent content. | | ALIGN_CENTER | 1B 61 01 | Center-aligns subsequent content. | | ALIGN_RIGHT | 1B 61 02 | Right-aligns subsequent content. | | BOLD_ON / BOLD_OFF | 1B 45 01 / 1B 45 00 | Toggles bold text. | | UNDERLINE_ON / UNDERLINE_OFF | 1B 2D 01 / 1B 2D 00 | Toggles underlined text. | | INVERT_ON / INVERT_OFF | 1D 42 01 / 1D 42 00 | Toggles white-on-black (reverse) printing. | | LF | 0A | Line feed — advances one line. | | CUT_FULL | 1D 56 00 | Fully cuts the paper. | | CUT_PARTIAL | 1D 56 01 | Partially cuts the paper (leaves a tab). | | DRAWER_KICK | 1D 70 00 19 FA | Pulses cash drawer pin 2 (25ms on / 250ms off) to pop it open. |

Options

encodeReceipt(document, {
  paperWidthDots: 576, // 384 (58mm) or 576 (80mm); also caps image width
  charsPerLine: 48,    // used to size `rule` nodes when `length` isn't given
  decodeImage: undefined, // decodes an `image` node's PNG/JPEG buffer `source` — see "Decoding PNG/JPEG buffers" above
});

Why declarative instead of a fluent builder?

Receipts are naturally data: they're built from database rows, templates, or serialized and replayed later. A plain object is easy to construct programmatically, validate, log, snapshot-test, and send over the wire (e.g. render on a server, print on a client) without coupling to a builder API.

What this library intentionally does not do

  • Open, list, or write to serial/Bluetooth/USB ports.
  • Manage printer connection state, retries, or reconnection.
  • Support every ESC/POS extension ever shipped by every vendor — it covers the common subset (text styling, raster images, barcodes, QR codes, feed, cut) that works across the vast majority of receipt printers. More obscure vendor-specific escape sequences (cash drawer kick, NV logo storage, etc.) can be appended manually by concatenating your own bytes with the Uint8Array this library returns.

License

MIT