Skip to content

productdevbook/portakal

Repository files navigation


portakal — Universal printer language SDK

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.

npm version npm downloads bundle size license

Playground · GitHub · npm

Note

portakal has two ways to print barcodes and QR codes:

  1. Printer-native (.barcode() / .qrcode()) — sends commands to the printer's built-in encoder. Fast, zero dependencies, minimal data transfer. Works for most use cases.
  2. 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 install etiket yourself — portakal stays zero-dependency.

Quick Start

npm install portakal

Product label

import { 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 metrics

Same label → any language

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

Receipt (ESC/POS)

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 SVG

Each module: compile + parse + preview + validate

import { 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

Barcode/QR via etiket

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 |

API

label(config)

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)
});

.text(content, options?)

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
});

.image(bitmap, options?)

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)
}

.box(options) / .line(options) / .circle(options)

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 });

.raw(content)

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 drawer

Language Module Methods

Each 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

Image Processing

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 }));

Receipt Layout

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,
);

Cross-Compiler

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.

Validation

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.

Printer Profiles

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 printers

20 built-in profiles: Epson, Star, Bixolon, Citizen, TSC, Zebra, SATO, Honeywell, Generic.

Supported Printer Languages

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

Transport

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);

Comparison

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.

Features

  • 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 previewlang.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 etiket integration 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

Contributing

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 production

License

Published under the MIT license.

About

Universal printer language SDK — 9 languages, 9 parsers, 63 cross-compilers. TSC, ZPL, EPL, ESC/POS, CPCL, DPL, SBPL, Star PRNT, IPL. Zero dependencies. Pure TypeScript.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors