portakal
Universal printer language SDK — 9 languages, one API.
Text, barcodes, QR codes, images, shapes — anything you can print.
One API, every thermal printer. Pure TypeScript, zero dependencies.
Playground · GitHub · npm
Important
We need your help testing! If you have a thermal printer, please try portakal and report any differences between our preview and your actual printed output. Even "looks correct" confirmations help. See Help Us Test below.
Note
portakal has two ways to print barcodes and QR codes:
- Printer-native (
.barcode()/.qrcode()) — sends commands to the printer's built-in encoder. Fast, zero dependencies, minimal data transfer. Works for most use cases. - Software-rendered (
.image()+etiket) — renders barcodes/QR codes as images on the host, sends pixels to the printer. Pixel-perfect output, 40+ formats, styled QR codes, guaranteed consistency across all printers. You installetiketyourself — portakal stays zero-dependency.
npm install portakalimport { label } from "portakal/core";
import { tsc } from "portakal/lang/tsc";
const myLabel = label({ width: 40, height: 30, unit: "mm" })
.text("ACME Corp", { x: 10, y: 10, size: 2 })
.text("SKU: PRD-00123", { x: 10, y: 35 })
.line({ x1: 5, y1: 55, x2: 310, y2: 55 })
.box({ x: 5, y: 5, width: 310, height: 230, thickness: 2 });
const code = tsc.compile(myLabel); // TSC/TSPL2 commands
const svg = tsc.preview(myLabel); // SVG preview with TSC font metricsimport { label } from "portakal/core";
import { tsc } from "portakal/lang/tsc";
import { zpl } from "portakal/lang/zpl";
import { epl } from "portakal/lang/epl";
import { escpos } from "portakal/lang/escpos";
const myLabel = label({ width: 40, height: 30, unit: "mm" }).text("Hello World", {
x: 10,
y: 10,
size: 2,
});
tsc.compile(myLabel); // TSC/TSPL2 — TSC, Gprinter, Xprinter, iDPRT
zpl.compile(myLabel); // Zebra ZPL II — GK420, ZT410, ZD620
epl.compile(myLabel); // Eltron EPL2 — LP/TLP 2824, GX420, ZD220
escpos.compile(myLabel); // ESC/POS — Epson, Bixolon, Star, Citizen (Uint8Array)Only the imported languages enter your bundle — 100% tree-shakeable.
import { label } from "portakal/core";
import { escpos } from "portakal/lang/escpos";
const receipt = label({ width: 80, unit: "mm" })
.text("MY STORE", { align: "center", bold: true, size: 2 })
.text("123 Market St", { align: "center" })
.text("================================")
.text("Hamburger x2 $25.98")
.text("Cola x1 $3.50")
.text("================================")
.text("TOTAL $29.48", { bold: true, size: 2 });
const bytes = escpos.compile(receipt); // Uint8Array
const svg = escpos.preview(receipt); // Receipt-style SVGimport { tsc } from "portakal/lang/tsc";
// Compile: label → printer commands
tsc.compile(myLabel);
// Preview: label → SVG (per-language font metrics)
tsc.preview(myLabel);
// Parse: printer commands → structured data
tsc.parse(tscCode); // { commands, elements, widthDots, ... }
// Validate: check for errors
tsc.validate(tscCode); // { valid, errors, issues }Available: tsc, zpl, epl, cpcl, dpl, sbpl, escpos, starprnt, ipl
Use etiket for barcode/QR generation, then embed as image:
import { label } from "portakal/core";
import { tsc } from "portakal/lang/tsc";
import { barcodePNG, qrcodePNG } from "etiket";
const myLabel = label({ width: 40, height: 30, unit: "mm" })
.text("Product Label", { x: 10, y: 5 })
.image(barcodePNG("123456789", { type: "code128" }), { x: 10, y: 40, width: 200 })
.image(qrcodePNG("https://example.com"), { x: 220, y: 40, width: 80 });
const code = tsc.compile(myLabel);| Best for | Simple labels, fast printing | Pixel-perfect, guaranteed output |
Creates a new label builder.
const builder = label({
width: 40, // Label width
height: 30, // Label height (omit for receipt/continuous)
unit: "mm", // "mm" | "inch" | "dot" (default: "mm")
dpi: 203, // Printer DPI (default: 203)
gap: 3, // Gap between labels in mm (default: 3)
speed: 4, // Print speed 1-10 (default: 4)
density: 8, // Darkness 0-15 (default: 8)
copies: 1, // Number of copies (default: 1)
});builder.text("Hello", {
x: 10, // X position in dots
y: 20, // Y position in dots
font: "2", // Font name/ID (printer-specific)
size: 2, // Magnification (1-10)
rotation: 0, // 0 | 90 | 180 | 270
bold: true, // Bold (ESC/POS only)
underline: true, // Underline (ESC/POS only)
reverse: false, // White on black
align: "center", // "left" | "center" | "right"
maxWidth: 300, // Word-wrap width in dots
});builder.image(monochromeBitmap, {
x: 10,
y: 10, // Position
width: 200, // Target width in dots
height: 100, // Target height in dots
});The bitmap must be a MonochromeBitmap:
interface MonochromeBitmap {
data: Uint8Array; // 1-bit packed, row-major, MSB-first
width: number; // Width in pixels
height: number; // Height in pixels
bytesPerRow: number; // ceil(width / 8)
}builder.box({ x: 0, y: 0, width: 200, height: 100, thickness: 2, radius: 5 });
builder.line({ x1: 0, y1: 50, x2: 300, y2: 50, thickness: 1 });
builder.circle({ x: 100, y: 100, diameter: 60, thickness: 2 });Escape hatch for printer-specific commands:
builder.raw("SET CUTTER ON"); // TSC
builder.raw("^FO10,10^FDCustom^FS"); // ZPL
builder.raw(new Uint8Array([0x1b, 0x70, 0x00, 0x32, 0x32])); // ESC/POS cash drawerEach language module (tsc, zpl, epl, cpcl, dpl, sbpl, escpos, starprnt, ipl) has:
| Method | Output | Description |
|---|---|---|
lang.compile(label) |
string or Uint8Array |
Compile to printer commands |
lang.preview(label) |
string |
SVG preview with language-specific fonts |
lang.parse(code) |
object |
Parse printer commands → structured data |
lang.validate(code) |
object |
Validate commands for errors/warnings |
Convert any RGBA image to monochrome bitmap with dithering:
import { imageToMonochrome } from "portakal";
const bitmap = imageToMonochrome(rgbaPixels, width, height, {
dither: "floyd-steinberg", // "threshold" | "floyd-steinberg" | "atkinson" | "ordered"
});
tsc.compile(label({ width: 40, height: 30 }).image(bitmap, { x: 10, y: 10 }));import { formatPair, separator, formatTable } from "portakal";
// Same-line left+right alignment
formatPair("Hamburger x2", "$25.98", 48);
// → "Hamburger x2 $25.98"
// Separator line
separator("=", 48);
// → "================================================"
// Multi-column table
formatTable(
[
{ width: 30, align: "left" },
{ width: 5, align: "center" },
{ width: 13, align: "right" },
],
[
["Item", "Qty", "Price"],
["Hamburger", "2", "$25.98"],
],
48,
);Convert between any printer languages — world's first thermal printer translator:
import { convert } from "portakal";
// TSC → ZPL
const { output } = convert(tscCode, "tsc", "zpl");
// ZPL → ESC/POS
const { output } = convert(zplCode, "zpl", "escpos");
// EPL → CPCL
const { output } = convert(eplCode, "epl", "cpcl");7 source × 9 target = 63 conversion paths.
Check printer commands for errors before sending to printer:
import { validate } from "portakal";
const result = validate(code, "tsc");
// { valid: false, errors: 1, warnings: 2, issues: [
// { level: "error", message: "CLS must appear before label elements" },
// { level: "warning", message: "No PRINT command found" },
// ]}TSC validation: SIZE order, CLS before elements, PRINT required, DENSITY 0-15, SPEED 1-18. ZPL validation: ^XA/^XZ required, ^FD without ^FO, ^PW range.
Auto-configure DPI, paper width, and capabilities based on printer model:
import { label, getProfile, findByVendorId } from "portakal";
// Auto-DPI from profile
label({ width: 40, height: 30, printer: "tsc-te310" }); // 300 DPI
label({ width: 80, printer: "epson-tm-t88vi" }); // 203 DPI
// Lookup profiles
getProfile("zebra-zd420"); // { name, dpi, paperWidth, ... }
findByVendorId(0x04b8); // All Epson printers20 built-in profiles: Epson, Star, Bixolon, Citizen, TSC, Zebra, SATO, Honeywell, Generic.
| Language | Printers | Status |
|---|---|---|
| TSC/TSPL2 | TSC, Gprinter, Xprinter, iDPRT, Munbyn, Polono | ✅ |
| ZPL II | Zebra GK420, ZT410, ZD620, ZQ series | ✅ |
| EPL2 | Zebra LP/TLP 2824, GX420, ZD220, ZD420 | ✅ |
| CPCL | Zebra QLn, ZQ mobile printers | ✅ |
| DPL | Honeywell/Datamax label printers | ✅ |
| SBPL | SATO label printers | ✅ |
| ESC/POS | Epson, Bixolon, Citizen, Star (compat mode) | ✅ |
| Star PRNT | Star TSP100/143/600/700 (native mode) | ✅ |
| IPL | Intermec/Honeywell printers | ✅ |
| PPLA/PPLB | Argox (use DPL/EPL/ZPL) | ✅ |
| Fingerprint | Honeywell Smart Printers | Planned |
portakal generates commands only — it does not handle printer connections. Send the output over any transport you choose:
import { label } from "portakal/core";
import { tsc } from "portakal/lang/tsc";
import { escpos } from "portakal/lang/escpos";
import net from "node:net";
const myLabel = label({ width: 40, height: 30 }).text("Hello", { x: 10, y: 10 });
const commands = tsc.compile(myLabel);
// TCP (port 9100)
const socket = net.createConnection({ host: "192.168.1.100", port: 9100 });
socket.write(commands);
socket.end();
// ESC/POS (binary) over WebUSB
const receipt = label({ width: 80 }).text("Receipt");
const bytes = escpos.compile(receipt);
await usbDevice.transferOut(endpointNumber, bytes);| Feature | portakal | node-thermal-printer | escpos | jszpl |
|---|---|---|---|---|
| Zero dependencies | ✅ | ❌ (pngjs, iconv-lite) | ❌ (get-pixels, jimp) | ✅ |
| TypeScript-first | ✅ | Partial | Partial | ✅ |
| Multi-language output | ✅ 9 languages | ❌ ESC/POS only | ❌ ESC/POS only | ❌ ZPL only |
| Transport-agnostic | ✅ | ❌ (coupled) | ❌ (coupled) | ✅ |
| Label printers (TSC/ZPL/EPL) | ✅ | ❌ | ❌ | ZPL only |
| Receipt printers (ESC/POS) | ✅ | ✅ | ✅ | ❌ |
| Image support | ✅ | ✅ | ✅ | ✅ |
| Barcode/QR (via etiket) | ✅ | ✅ | ✅ | ✅ |
| Image dithering | ✅ | ❌ | ❌ | ❌ |
| Receipt layout engine | ✅ | Partial | ❌ | ❌ |
| SVG preview | ✅ | ❌ | ❌ | ❌ |
| Command parser (reverse) | ✅ 9 parsers | ❌ | ❌ | ❌ |
| Cross-compiler (translate) | ✅ 63 paths | ❌ | ❌ | ❌ |
| Command validation | ✅ | ❌ | ❌ | ❌ |
| Printer profiles | ✅ 20 | ❌ | ❌ | ❌ |
| Works in browser | ✅ | ❌ | ❌ | ✅ |
| No native modules (no gyp) | ✅ | ❌ | ❌ | ✅ |
| Pure ESM | ✅ | ❌ (CJS) | ❌ (CJS) | ❌ (CJS) |
portakal is the only library that generates 9 printer languages from a single API with zero dependencies.
- Zero dependencies — pure computation, no native modules, no node-gyp
- 9 printer languages — TSC, ZPL, EPL, CPCL, DPL, SBPL, ESC/POS, Star PRNT, IPL
- Tree-shakeable — sub-path exports for every module (
portakal/tsc,portakal/image, etc.) - Pure ESM, edge-runtime compatible (Cloudflare Workers, Deno, Bun)
- TypeScript-first with strict types (tsgo)
- Transport-agnostic — generates commands, you handle the connection
- Fluent builder API — one label definition compiles to any language
- Image processing — RGBA → monochrome with 4 dithering algorithms (Floyd-Steinberg, Atkinson, ordered, threshold)
- Receipt layout engine — same-line left+right alignment, tables, word-wrap, separators
- SVG preview —
lang.preview(label)renders labels without a physical printer - 9 parsers — reverse-parse printer commands back to structured data (TSC, ZPL, EPL, CPCL, ESC/POS, DPL, SBPL, Star PRNT, IPL)
- Drawing primitives — box, line, circle, diagonal
- Raw command passthrough for advanced/unsupported features
- Optional
etiketintegration for barcode/QR images (40+ formats) - Works in browser, Node.js, Deno, Bun, Electron
- UTF-8 encoding engine — auto code page selection (CP437, CP858, CP1252, CP866, CP857)
- Cross-compiler — convert between any languages (63 paths: TSC↔ZPL↔EPL↔CPCL↔DPL↔SBPL↔IPL↔ESC/POS↔Star)
- Real validation — parameter ranges, command order, structure checks
- 20 printer profiles — auto-DPI, auto-width by model (Epson, Star, Zebra, TSC, SATO, etc.)
- Language modules — each language is a standalone module (compile + parse + preview + validate)
- Per-language SVG preview — TSC fonts differ from ZPL fonts, ESC/POS renders receipt-style
- 447 tests across 28 test files
We're building the most accurate open-source printer language SDK, but we need real-world validation. If you have access to a thermal printer, we'd love your help comparing portakal's output against actual printed labels.
- Pick a language — ZPL, TSC, EPL, CPCL, or any supported language
- Write a test label — use portakal to generate commands, or paste raw printer code into the Playground
- Print it — send the commands to a real printer
- Compare — take a photo of the printed label and a screenshot of portakal's SVG preview
- Report — open an issue with:
- The printer code (ZPL, TSC, etc.)
- Screenshot of portakal's preview
- Photo of the actual printed label
- What's different (position, font size, spacing, barcode, etc.)
^XA
^CF0,60
^FO50,50^GB100,100,100^FS
^FO75,75^FR^GB100,100,100^FS
^FO93,93^GB40,40,40^FS
^FO220,50^FDIntershipping, Inc.^FS
^CF0,30
^FO220,115^FD1000 Shipping Lane^FS
^FO220,155^FDShelbyville TN 38102^FS
^FO50,250^GB700,3,3^FS
^CFA,30
^FO50,300^FDJohn Doe^FS
^FO50,340^FD100 Main Street^FS
^FO50,380^FDSpringfield TN 39021^FS
^BY5,2,270
^FO100,550^BC^FD12345678^FS
^FO50,900^GB700,250,3^FS
^FO400,900^GB3,250,3^FS
^CF0,40
^FO100,960^FDCtr. X34B-1^FS
^CF0,190
^FO470,955^FDCA^FS
^XZ
Paste this into the Playground (Validate tab, select ZPL) and compare with Labelary or your real printer.
| Area | What to check |
|---|---|
| Text positioning | Are texts at the correct x,y? Do they overflow? |
| Font sizes | Does ^CF0,60 look the same size as the real printer? |
| Font rendering | Font 0 should be proportional (Helvetica-like), Font A should be monospace |
| ^FR reverse (XOR) | The Intershipping logo (3 overlapping boxes) should show black L-shape + white area + small black square |
| Barcodes | Correct height from ^BY? Correct width? Readable interpretation line? |
| Box/line thickness | Does ^GB draw borders inward? Correct corner radius? |
| ^LH, ^LS, ^LT offsets | Do label home / shift / top offsets apply correctly? |
| ESC/POS receipts | Alignment, bold, text sizing on Epson/Star/Bixolon |
Even a "looks correct" confirmation is helpful. Every report makes the SDK more reliable for everyone.
Tip
No printer? You can still help by comparing our preview against Labelary (ZPL) or other online viewers and reporting any visual differences.
Contributions are welcome! Here are areas where help is especially appreciated:
- Arabic/Hebrew RTL support (bidi algorithm + Arabic shaping)
- GS1/UDI label standards (SSCC, GTIN, FMD, DSCSA templates)
- Star TSP100 raster-only text rendering
- CJK encoding (GB18030, Shift_JIS, Big5, EUC-KR)
- Fingerprint (Honeywell BASIC-like) compiler
- WebUSB/WebSerial/Web Bluetooth transport adapters
- Additional printer profiles
- Parser validation rules for more languages
pnpm install # Install dependencies
pnpm dev # Run tests in watch mode
pnpm test # Lint + typecheck + test
pnpm build # Build for productionPublished under the MIT license.