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
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
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.