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

@point-of-sale/receipt-printer-decoder

v1.1.0

Published

Decode the raw data sent to a receipt printer, ESC/POS, StarPRNT, Star Line or Star Graphics, into its commands with their parameters and meanings

Readme

ReceiptPrinterDecoder

Decode the raw data sent to a receipt printer, ESC/POS, StarPRNT, Star Line or Star Graphics, into its commands with their parameters and meanings.

This library is part of @point-of-sale, a collection of libraries for interfacing browsers and Node with Point of Sale devices such as receipt printers, barcode scanners and customer facing displays.

About ReceiptPrinterDecoder

This library reads the bytes an application sends to a receipt printer and says what they are. Give it a stream in ESC/POS, StarPRNT, Star Line or Star Graphics, whatever produced it, and it comes back with the commands in it, each with its mnemonic, its name, the parameters it carries and what their values mean, with the runs of text between them decoded to strings in the codepage the stream itself selected. It is the other half of an encoder: where ReceiptPrinterEncoder writes those bytes and ReceiptPrinterRenderer draws them as paper, this one reads them back as a list of what was asked of the printer.

That makes it two tools at once. It is a viewer, for the moment a receipt comes out wrong and the only evidence is a capture of the bytes that produced it, where a list of commands with their meanings is the fastest way to see which one is at fault. And it is a test's tool, for asserting that the stream an application produced holds the cut, the codepage or the barcode it was supposed to hold, without rendering an image and comparing pixels.

Underneath the meanings sits the syntax of the four languages, which is a package of its own inside this one: a tokenizer entry that cuts a stream into its commands, its runs of text and its control bytes and says nothing about what any of them means. It has no dependency and carries none of the tables of names, so a renderer, a proxy or a log viewer can take the syntax alone. It is written for ReceiptPrinterRenderer to parse with, in place of the parsers of its own it carries today, so that there is one answer about where a command ends rather than two.

import { readFileSync } from 'node:fs';
import { decode } from '@point-of-sale/receipt-printer-decoder';

const tokens = decode(readFileSync('receipt.bin'), 'esc-pos');

for (const token of tokens) {
    const what = token.mnemonic ?? token.type;
    console.log(String(token.offset).padStart(4), what.padEnd(9), token.summary ?? token.text ?? token.name);
}
   0 ESC @     Initialize the printer
   2 FS .      Cancel Kanji mode
   4 ESC M     Font A
   7 text                      
  23 ESC E     Bold on
  26 ESC t     Select codepage 0, cp437
  29 text      THE CORNER STORE
  45 ESC E     Bold off
  48 control   Line feed
  49 control   Carriage return
  50 text                  Kerkstraat 1, Amsterdam
  85 control   Line feed
  ...

Using the library

decode() is the whole library in one call: bytes and a language in, a list of tokens out. Every token carries the offset and the length of the bytes it covers and a view of those bytes, and the tokens of a stream cover it exactly, every byte in one token and one only, control bytes the printer ignores included. A command token gains its mnemonic, a label of a name, its category, the command it belongs to, its parameters with the meaning of each value and the bytes each was read from, and a one line summary; a run of text gains the codepage it was printed in and the string it spells; a control byte gains its name. Nothing is skipped and nothing is copied, so the list can be laid beside a hex dump of the stream and nothing will be missing from it.

describe() is that same knowledge for one command, for an application that has a token already and wants to know what it means. It answers from the tables alone, which is everything except the name of a codepage: a number in an ESC t becomes a codepage through the mapping the stream was written with, and a mapping is an option of decode() rather than a fact about the command, so only decode() fills that in.

The tokenizer is a second entry, @point-of-sale/receipt-printer-decoder/tokenizer, with tokenize(), the Tokenizer class behind it and detect(). It is separate because the two halves have different weights. The syntax is small, it has no dependency at all, and it is what a renderer needs to walk a stream, what a proxy needs to know where one command ends and the next begins, and what a log viewer needs to show a capture as a list. The meanings are tables of a few hundred commands and the codepages behind them, and the main entry carries them and the codepage encoder with them. Whoever needs only the first imports the tokenizer entry and takes none of the second; whoever imports both holds one tokenizer, because the main entry imports the tokenizer entry itself rather than a copy of it. Tokenizer is the class for a stream that arrives in pieces: push() gives back the tokens that completed and keeps the rest, and the tokens of a stream pushed in pieces are the tokens of the whole stream, offsets and all.

detect() answers the question a caller has to answer before it can tokenize anything, which is which language a stream is written in. It votes on the commands that exist in only one of the families, so it needs no parse to give a first answer, and it gives one of three: esc-pos, star-prnt or star-graphics. The handful of commands both families have, which only the argument behind them tells apart, are counted as ambiguities: they break a tie between the commands that belong to one family and never outvote one, so an ESC/POS job that feeds its sheets with ESC d 2, the Star cut, is still decided by the GS V that cuts them, which Star has no command for. It never answers star-line, because StarPRNT and Star Line are one command set to everything that reads the answer and a detector should not claim a distinction nobody acts on. What it cannot tell you is the language of a stream that holds no command at all: plain text with nothing but line feeds in it is the same stream in every language, and it is read as ESC/POS, which is the default of this family of packages and renders the same paper either way. Nor can it survive a payload that is a dense run of well formed commands of the other language, which has to be built by hand and which the suite pins as a known limit rather than leaves to be discovered.

The reason it survives an image, which is the case that does happen, is a second pass. A single dithered raster image holds something like a hundred and twenty byte pairs that read as FS p, FS q or GS V, and a receipt casts one or two real votes, so a first pass over every byte of a Star stream with one picture in it elects ESC/POS by a distance of a hundred. So the passes behind the first read the stream with the tokenizer as the candidate language and count only over the header of each command, its prefix, its command byte and the two argument bytes the table can reach. The dots of a picture then sit inside the arguments of the command that carries them and are never scanned, and if the candidate was the wrong language the parse desynchronises in those dots and the accidents no longer fall on a token boundary, so they stop counting. A wrong candidate corrects itself, because a Star stream read as ESC/POS still shows its ESC GS pairs as two byte commands and two bytes is a header, and the stream is read again as the winner.

See Usage and installation for the options, the fields of every kind of token, the tokenizer entry in full and what is deliberately not decoded.

Testing

The streams the tests read are ReceiptPrinterRenderer's, frozen here: 256 of them, copied at a named commit, the 147 that came from other projects with their provenance beside them and the licence of the project they came from, because those are other people's samples. They are copies and never a link into the renderer's working tree, so the two suites can move apart without either breaking the other.

The tokenizer was not argued into agreement with those parsers, it was measured against them. A copy of each of the renderer's two parsers, with a few lines added that record where every command starts and how long it is, read all 256 streams and every one of them cut short, a quarter of a million streams in all, and the spans the renderer walked are the spans the tokenizer reports, some thirty five million of them, with the position the renderer stops at being the position of the incomplete token. On top of that the suite asserts for every stream that its tokens cover it exactly with no gap and no overlap, that a stream cut at any position still tokenizes to what the whole stream did up to the cut, and that a stream pushed in pieces at a hundred split points gives the tokens of the whole stream field for field.

The descriptions are held to the tokenizer's tables in both directions, so that every command the tokenizer knows has a meaning and every meaning names a command that can be tokenized, and a command added to one side has to be added to the other. Every command of every fixture is described, some twenty six thousand of them, and none of them falls back to its mnemonic. And because a summary is a sentence and a sentence is not something anybody proofreads at scale, a sweep walks every leaf of both tables and every one of the 256 values of every byte parameter of it, seventy two thousand summaries in all, asserting that each is a sentence with no empty value, no dangling comma and no double space in it.

The ESC/POS tables carry every command of Epson's ESC/POS reference, the length of each in the tokenizer and its meaning in the descriptions, down to the function of every GS ( group and the five symbologies of GS ( k the renderer does not draw. The tables started as the renderer's, and the commands the renderer had no handler for were completed from the reference afterwards: a Windows BMP of GS D ends where the size in its own header says, the counter of GS C ; is five decimal strings each ended by a semicolon, and the real time buzzer of DLE DC4 3 is six bytes. A test decodes one instance of every command and every function, some three hundred of them, and asserts that none falls back to its mnemonic, so a command of that reference that reads as unknown here is a bug and not a gap.

Beside Epson's own there are the commands another manufacturer added to ESC/POS and that collide with nothing of Epson's: Bixolon's BS M, BS V and BS ^ P, the font, the cut and the power saving mode under the backspace byte, and the ESC B buzzer and the ESC SO and ESC DC4 double width of the Xprinter XP-58 series. They are read from those manufacturers' manuals, they say whose they are in the vendor field of the token, and a backspace in front of anything but an M, a V or a ^ is the control byte it has always been. Where two manufacturers wrote two different commands in the same bytes one of them has to be picked, and every such case is written down in Known incompatibilities.

The Star tables carry every command of both Star references, the Star Line Mode reference (Rev 1.80) and the StarPRNT reference (Rev 4.20), the length of each in the tokenizer and its meaning in the descriptions, down to every function of every group: the sixty eight network settings of ESC GS ) N, the six magnetic card tracks of ESC M and the graphics group Star shares with Epson. These tables started as the renderer's too, and its readings were made from captures and the Star Graphic Mode specification, without the two command manuals, so completing them from the manuals corrected five lengths that were cut wrong and six names that were the capture's guess. The count test holds the table at 97 Star commands beside the 99 of ESC/POS, and a test of its own decodes one instance of every command and every function of both manuals, 301 of them, and holds each to one token with a name and a summary of its own. One table reads all three Star names, because the two manuals give the same bytes two meanings exactly once: ESC M is the twelve dot character pitch under star-line and star-graphics, two bytes, and under star-prnt a magnetic card read of three when one of the six track letters follows it.


This library has been created by Niels Leenheer under the MIT license. Feel free to use it in your products. The development of this library is sponsored by Salonhub.