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 xxopcode 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.
Uint8Arrayin,Uint8Arrayout.
For vendor-extended drivers, see e.g. @thermal-label/labelife-core which consumes this package as its spec-aligned foundation.