Skip to content

ESC/POS bitmap-label wire protocol

This page documents the driver-complete bitmap-label subset of ESC/POS — every directive needed to drive a bitmap-rasterised print job end-to-end on a spec-compliant Epson printer, plus paper handling, plus status. Native text, native barcodes, page-mode buffering, cash-drawer kick, buzzer, kanji, and macros are out of scope by design.

Source

Bytes below are documented from Epson's ESC/POS Application Programming Guide — the canonical ESC/POS reference, published by Seiko Epson Corporation. See INTEROPERABILITY.md for the full statement of intent.

Design rule

Bitmap in, printer code out. The host renders content (text, barcodes, graphics) into a 1-bpp bitmap upstream; this package emits the wire bytes that drive the printer to render that bitmap. Commands that exist solely to make the printer render content the host could have rendered are not in scope.

A typical job is three parts back-to-back:

1B 40                       ESC @                       initialise
1D 76 30 00 xL xH yL yH     GS v 0 m=0 + 4-byte header  raster command
<raster bytes>              widthBytes × heightDots     1-bpp packed payload

Drivers needing paper handling (feeds / cuts) or print-area control compose the directives below alongside the raster.

For multiple copies, write the payload N times back-to-back. Pure ESC/POS has no protocol-level multi-copy framing.

Initialisation

BuilderBytesPurpose
buildEscposReset()1B 40ESC @ — reset to power-on state

Always the first command in a job.

Bitmap raster

BuilderBytesPurpose
buildGsV0Raster(widthBytes, heightDots)1D 76 30 00 xL xH yL yHModern raster (GS v 0) — preferred on recent Epson chassis
buildEscStarRaster(mode, widthDots, heightDots)1B 2A m xL xHLegacy raster (ESC *) — modes 0 / 1 / 32 / 33 for older chassis
buildEscposDensity(level)1B 4E 07 nnESC N 7 — set print density

ESC * modes

  • 0 — 8-dot single-density (60 × 90 dpi)
  • 1 — 8-dot double-density (60 × 180 dpi)
  • 32 — 24-dot single-density (180 × 90 dpi)
  • 33 — 24-dot double-density (180 × 180 dpi)

ESC * and GS v 0 overlap; both are provided so this package can drive either chassis family. JSDoc on each builder describes the trade-off.

Bit polarity

Bit 1 in the raster payload denotes a printed dot. This matches the 1 = dark invariant used by LabelBitmap from @mbtech-nl/bitmap — no flip needed.

Width must be a multiple of 8 dots

GS v 0 uses truncating row stride: the firmware reads exactly widthBytes × heightDots bytes, and any pixels past the last 8-dot boundary are silently dropped. Pad upstream:

ts
import { padBitmap } from '@mbtech-nl/bitmap';
const aligned = padBitmap(source, { right: 8 - (source.widthPx % 8) });

Paper handling

BuilderBytesPurpose
buildFeedLines(n)1B 64 nnESC d n — feed n lines at current line spacing
buildFeedDots(n)1B 4A nnESC J n — feed n dots forward
buildReverseFeed(n)1B 4B nnESC K n — reverse-feed n dots (where supported)
buildCut(mode, feed?)1D 56 …GS V — full / partial / feed-then-cut
buildPrintAndFeed(n?)1B 64 nnConvenience alias of buildFeedLines

GS V cut variants:

1D 56 00      full cut
1D 56 01      partial cut
1D 56 41 nn   feed nn dots, then full cut
1D 56 42 nn   feed nn dots, then partial cut

The decimal duplicates (0 / 48, 1 / 49, 65 / 97, 66 / 98) are interchangeable per the spec; the builder emits the lower of each pair.

BuilderBytesPurpose
buildLeftMargin(dots)1D 4C nL nHGS L — set left margin in dots (LE u16)
buildPrintWidth(dots)1D 57 nL nHGS W — set print-area width in dots (LE u16)
buildPanelButtons(state)1B 63 35 nnESC c 5 — enable / disable front panel buttons
buildLineSpacing(dots)1B 33 nnESC 3 — set line-spacing slot in dots
buildDefaultLineSpacing()1B 32ESC 2 — reset line-spacing slot to printer default

ESC 2 / ESC 3 modulate the dot count consumed by ESC d (line feed) — they affect bitmap-mode paper handling and so are in scope. The text-mode right-spacing setter ESC SP is not in scope.

ESC c 5 accepts any byte; only bit 0 is significant per the spec (0 enabled, 1 disabled). buildPanelButtons accepts a boolean and encodes the spec-canonical values.

Status query

BuilderBytesPurpose
buildRealtimeStatusQuery(kind)10 04 nnDLE EOT n — real-time status (replies interleave with print data)
buildTransmitStatus(kind)1D 72 nnGS r n — legacy "transmit status"
buildAutoStatusBack(kind)1D 61 nnGS a n — Automatic Status Back (printer pushes status without query)

DLE EOT n argument values:

kindnReply
'printer'1Printer status (online / cover / panel-button / error)
'offline-cause'2Cause of an offline condition
'error-cause'3Cause of an error condition
'paper-sensor'4Paper sensor status

GS r n argument values:

kindnReply
'paper-sensor'1Paper sensor status
'drawer'2Drawer-kick connector pin-3 status

GS a n accepts a string from 'off' | 'drawer' | 'online' | 'error' | 'paper' | 'all'; the bitmask is composed spec-canonically.

Realtime-status replies

Reply frames arrive byte-oriented:

[1A]?  <opcode>  <payload>

The 0x1A lead byte ("transmit status") is optional but commonly present.

The Epson realtime-status reply opcodes parsed by this package:

OpcodeReplyPayloadEncoding
0x10printer1 bytebit 3 = online, bit 5 = cover, bit 6 = paper-feed button, bit 7 = error
0x12cover1 bytebit 2 = cover open
0x14drawer1 bytebit 2 = drawer-kick connector pin 3 high
0x16paper1 bytebit 2 = paper near-end, bit 5 = paper-out
0x18autocutter1 bytebit 3 = autocutter error
0x60error-info4 bytesbyte 0: bit 2 = paper jam, bit 3 = mechanical, bit 4 = autocutter; byte 1: bit 0 = unrecoverable, bit 6 = temperature
0x70offline-cause4 bytesbyte 0: bit 2 = cover, bit 3 = paper-feed button, bit 5 = paper end, bit 6 = error
0x80printer-star1 byteStar-compatible: bit 3 = online, bit 5 = cover, bit 2 = paper out

Bit numbering

Epson's spec uses 1..8 bit numbering in its reply tables; this package documents zero-indexed bit positions (so the spec's "bit 8" = 0x80 in our notation). Both numbering conventions appear in the parser source comments to keep the mapping unambiguous.

Anything else — including vendor opcode-table extensions — surfaces as { kind: 'unknown', opcode, payload } with payload = remainder-of-buffer. Downstream packages that know their vendor's opcode table consume the unknown reply and decode it themselves.

Parser: parseRealtimeStatus(bytes, offset?).

Released under the MIT License.