Skip to content

Getting started

@thermal-label/escpos-core is a pure protocol encoder. You bring the bitmap and the transport; the package produces the bytes.

Install

bash
pnpm add @thermal-label/escpos-core @mbtech-nl/bitmap

(@mbtech-nl/bitmap provides the LabelBitmap type the encoder consumes — 1-bpp packed, MSB-first, 1 = dark.)

Encode a print job

ts
import { encodeEscposJob, type EscposEngine } from '@thermal-label/escpos-core';
import { createBitmap } from '@mbtech-nl/bitmap';

const engine: EscposEngine = { dpi: 203, headDots: 384 };

// Make or load a 1-bpp bitmap. Width should be a multiple of 8 dots
// — the GS v 0 wire format uses truncating row stride. Use
// `padBitmap` from @mbtech-nl/bitmap upstream if your source isn't
// aligned.
const bitmap = createBitmap(384, 100);
// …paint the bitmap (text, barcode, image, …)…

const bytes = encodeEscposJob(engine, { bitmap });

// `bytes` is now the complete ESC/POS print job:
//   1B 40                       ESC @            (initialise)
//   1D 76 30 00 30 00 64 00     GS v 0 m=0
//                                  width-bytes = 48 (LE u16)
//                                  height-dots = 100 (LE u16)
//   <raster payload>            48 * 100 = 4800 bytes
//
// Hand it to your transport (USB, RFCOMM, TCP-9100, BLE, …).

Multi-copy

Pure ESC/POS has no protocol-level multi-copy framing. Print N copies by writing the same payload N times back-to-back:

ts
const job = encodeEscposJob(engine, { bitmap });
for (let i = 0; i < N; i++) {
  await transport.write(job);
}

(Or use a vendor-extending package — e.g. labelife — that implements its own 1F 11 21 N multi-copy header.)

Set density

ts
import { buildEscposDensity, concatBytes } from '@thermal-label/escpos-core';

const job = concatBytes(
  buildEscposDensity(8), // ESC N 7 8
  encodeEscposJob(engine, { bitmap }),
);

ESC N 7 <n> is Epson's per-parameter density setter — the legal range of n is printer-defined, consult the device's datasheet.

Parse realtime-status replies

ts
import { parseRealtimeStatus } from '@thermal-label/escpos-core';

let buffer = new Uint8Array();
transport.onData(chunk => {
  buffer = concat(buffer, chunk);
  while (buffer.length > 0) {
    const { reply, bytesConsumed } = parseRealtimeStatus(buffer);
    if (reply === null) break; // accumulate more bytes
    handleReply(reply);
    buffer = buffer.subarray(bytesConsumed);
  }
});

function handleReply(reply: RealtimeStatusReply): void {
  switch (reply.kind) {
    case 'paper':
      if (reply.paperOut) showLoadPaperUi();
      break;
    case 'cover':
      if (reply.open) showCloseCoverUi();
      break;
    case 'drawer':
      // pin-3 high / low
      break;
    case 'unknown':
      // vendor opcode — let a downstream package handle it
      vendorParser(reply.opcode, reply.payload);
      break;
  }
}

What's not here

This package is intentionally narrow:

  • No receipt-shape ESC/POS (text, fonts, alignment, 1D barcodes, kanji). Use a different library for receipt printing — this one targets bitmap label output only.
  • No vendor 1F 11 xx opcode families, multi-copy framing, compressed raster modes, or vendor status-opcode tables. Those belong in vendor-specific driver packages.
  • No transports, device registry, or platform split. Uint8Array in, Uint8Array out.

For vendor-extended drivers, see e.g. @thermal-label/labelife-core which consumes this package as its spec-aligned foundation.

Released under the MIT License.