Parse and render ZPL / EPL labels to PNG & PDF — fast, offline, open source.
🌐 Playground · 📚 Tutorials · 📜 Changelog · 🚀 Releases
Labelize is a Rust engine that parses ZPL (Zebra Programming Language) and EPL (Eltron Programming Language) label data and renders it to PNG or PDF — no printer hardware required. The same engine ships as a CLI tool, an HTTP microservice, a WebAssembly package, an Android library, and a plain Rust crate, producing identical output on every surface.
A free public playground is hosted on Cloudflare Workers at https://labelize.764629910.workers.dev — paste a label, see it render, download PNG or PDF. No install, no signup, no data leaving the browser tab beyond the render call.
Previewing thermal labels today usually means one of three compromises: send your label data to the Labelary web API (third-party service, data-privacy concerns, ~400 ms per render), pay for a commercial SDK (often hundreds of dollars), or keep a physical printer around just to see what would print. Labelize is the self-hosted alternative:
| Labelize | Labelary (web) | Zebra Printer | |
|---|---|---|---|
| Offline / self-hosted | ✅ | ❌ | ✅ |
| No hardware needed | ✅ | ✅ | ❌ |
| Open source | ✅ | ❌ | ❌ |
| EPL support | ✅ | ❌ | ✅ |
| PDF output | ✅ | ❌ | ❌ |
| Embeddable library | ✅ | ❌ | ❌ |
| REST API | ✅ | ✅ | ❌ |
| Cost | Free | Free / paid tiers | Hardware cost |
Features at a glance
- 🖨️ 38+ ZPL commands — text & fonts, 12 barcode symbologies (Code 128, EAN-13/8, UPC-A/E, Code 39, Interleaved 2-of-5, PDF417, Aztec, DataMatrix, QR, MaxiCode), boxes/circles/diagonals/ellipses, graphic fields, stored formats (
^DF/^XF), label rotation & inversion - 🏷️ EPL2 support — text, the full 1D/2D barcode command set (
B/b), lines, diagonals, boxes, binary graphics (GW) - 🖼️ PNG & PDF output — thermal-faithful 1-bit monochrome by default, optional antialiased greyscale
- 🔤 Embedded fonts — zero runtime font dependencies (Roboto Condensed, DejaVu Sans Mono, ZPL GS — all permissively licensed)
- ⚡ ~5 ms per render — no network, no interpreter, no printer
- 🧪 124 golden-file E2E tests against Labelary reference renders on every push
Pick the surface that fits your stack — every path below produces the same pixels.
# Homebrew (macOS / Linux)
brew tap GOODBOY008/homebrew-labelize && brew install labelize
# Or from source / crates.io (requires a Rust toolchain)
cargo install labelize --features cli
# Windows: download labelize-x86_64-pc-windows-msvc.zip from Releaseslabelize convert label.zpl # → label.png (format auto-detected)
labelize convert label.epl -t pdf # EPL in, PDF out
labelize convert label.zpl --width 100 --height 62 --dpmm 12Pre-built multi-arch images (linux/amd64 + linux/arm64) on every release:
docker run -p 8080:8080 goodboy008/labelize:latest # or ghcr.io/goodboy008/labelize
curl -X POST http://localhost:8080/convert \
-H "Content-Type: application/zpl" \
-d '^XA^FO50,50^A0N,40,40^FDHello Docker^FS^XZ' \
-o label.pngOpen http://localhost:8080/ for the interactive playground. Tags: latest, 1.6.0 / 1.6 / 1, edge (current main).
serve mode reads two optional flags; both default to true:
| Variable | Effect when false |
|---|---|
LABELIZE_PLAYGROUND_ENABLED |
API-only container — / returns an endpoint listing instead of the web UI |
LABELIZE_PLAYGROUND_LABELARY_COMPARE |
Hides the playground's "Compare with Labelary" button, which POSTs the current ZPL to the external api.labelary.com service |
# Privacy-sensitive deployment: keep label data in-house
docker run -p 8080:8080 \
-e LABELIZE_PLAYGROUND_LABELARY_COMPARE=false \
goodboy008/labelize:latestRendering itself (/convert, the CLI, the library) never makes outbound requests.
The engine compiled to WebAssembly — renders in browsers, Node.js, and bundlers with no server:
npm install @goodboy008/labelize-wasmimport { lz_render } from "@goodboy008/labelize-wasm/init"; // Node ≥ 20
const zpl = Buffer.from("^XA^FO50,50^A0N,40,40^FDHello World^FS^XZ", "ascii");
const png = lz_render(zpl, 102.0, 152.0, 8, false, false, false); // Uint8ArrayBundlers (webpack 5, Vite + vite-plugin-wasm) import from the package root instead. Full API in the JavaScript tutorial.
Self-contained AAR (com.goodboy008.labelize) with native libraries for arm64-v8a, armeabi-v7a, x86_64, x86 (minSdk 24) — bit-identical output to the desktop builds. Download labelize-android-aar.zip from Releases:
// app/build.gradle.kts
repositories {
flatDir { dirs("libs") }
}
dependencies {
implementation(name = "labelize-android-release", ext = "aar")
}
// then, from Kotlin or Java:
val png = Labelize.renderZplToPng(zpl.toByteArray(), widthMm = 102.0, heightMm = 152.0)More in the Android tutorial and android/README.md.
cargo add labelizeuse std::io::Cursor;
use labelize::{ZplParser, Renderer, DrawerOptions};
let mut parser = ZplParser::new();
let labels = parser.parse(b"^XA^FO50,50^A0N,40,40^FDHello^FS^XZ")?;
let mut buf = Cursor::new(Vec::new());
Renderer::new().draw_label_as_png(&labels[0], &mut buf, DrawerOptions::default())?;
std::fs::write("label.png", buf.into_inner())?;A complete runnable example lives at examples/render_label.rs
(cargo run --example render_label -- label.zpl), with a walkthrough in the
Rust tutorial.
Every Labelize server serves an interactive UI at GET / — the same page running
at the public instance:
- Paste ZPL/EPL or open a
.zpl/.eplfile, pick a label size (4×6, 4×4, …) - Live preview with debounced auto-render, zoom fit / percent, dark & light themes, English / 简体中文
- One-click PNG / PDF download, copy-PNG to clipboard,
Ctrl+S - Compare with Labelary (ZPL) — fetches the reference render and scores the diff on the same scale as CI
- Shareable permalinks that encode the label + settings in the URL hash
labelize serve --port 8080 # or: docker run -p 8080:8080 goodboy008/labelizeLabelize is calibrated against the Labelary reference renderer with per-label pixel-diff tolerance thresholds — 128 real-world carrier and synthetic labels are compared on every push. Left = Labelary reference, right = Labelize.
| Label | Diff | Preview |
|---|---|---|
| amazon | 1.08% | ![]() |
| dhlpaket | 1.48% | ![]() |
| ups | 2.98% | ![]() |
| fedex | 4.85% | ![]() |
128 labels tested — 8 perfect · 61 good (<1%) · 52 minor (<5%) · 7 moderate (<15%) · 0 high
All side-by-side images: testdata/diffs/ ·
full reports: labels · unit ·
thresholds: docs/DIFF_THRESHOLDS.md
Step-by-step tutorials for every platform live in docs/tutorials/:
| Guide | For |
|---|---|
| Command Line | Converting files from a shell |
| Docker | Running the service in a container |
| HTTP Service | REST API & playground integration |
| JavaScript / WebAssembly | Client-side rendering |
| Android | Rendering inside an app |
| Rust Library | Embedding the engine in Rust |
Reference docs: Usage · ZPL command matrix · Diff thresholds · Changelog
CLI reference
Usage: labelize <COMMAND>
Commands:
convert Convert a ZPL/EPL file to PNG or PDF
serve Start HTTP server for label conversion
Convert Options:
<INPUT> Input file path (.zpl or .epl)
-o, --output <PATH> Output file path (default: input stem + .png/.pdf)
-f, --format <FMT> Input format override: zpl | epl
-t, --type <TYPE> Output type: png | pdf [default: png]
--width <MM> Label width in mm [default: 102]
--height <MM> Label height in mm [default: 152]
--dpmm <N> Dots per mm: 6, 8, 12, or 24 [default: 8]
--antialias 8-bit grayscale output (default: 1-bit)
Serve Options:
--host <HOST> Bind address [default: 0.0.0.0]
-p, --port <PORT> Listen port [default: 8080]
HTTP API
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | Interactive web playground |
/health |
GET | Health check → {"status":"ok"} |
/convert |
POST | Convert label data → PNG or PDF |
POST /convert — the parser is selected by Content-Type (application/zpl / application/epl):
| Parameter | Default | Description |
|---|---|---|
width |
102 | Label width in mm |
height |
152 | Label height in mm |
dpmm |
8 | Dots per mm |
output |
png | Output format: png/pdf |
antialias |
false | Preserve antialiased greys instead of 1-bit |
Status codes: 200 success (body is PNG/PDF bytes) · 400 parse error (bad label data) · 500 render error. Only the first label in the body is rendered — split multi-label files, or use the CLI.
Supported commands
DataMatrix ECC 200 field-data escapes and firmware defaults are described in DataMatrix field data.
| Category | Commands |
|---|---|
| Text & Font | ^FO ^FT ^FD ^FS ^A ^A@ (named font) ^CF ^CW (font identifier) ^FB ^FR ^FH ^FN ^FW ^FV |
| Barcodes | ^BC (Code 128) ^BE (EAN-13) ^B8 (EAN-8) ^B9 (UPC-E) ^BU (UPC-A) ^B2 (Interleaved 2-of-5) ^B3 (Code 39) ^B7 (PDF417) ^BO (Aztec) ^BX (DataMatrix) ^BQ (QR Code) ^BD (MaxiCode) ^BY (defaults) |
| Graphics | ^GB (box) ^GC (circle) ^GD (diagonal) ^GE (ellipse) ^GF (graphic field) ^GS (symbol) ~DG (download graphic) ^IL ^XG ^ID ^IM ^IS ~EG |
| Label Control | ^XA ^XZ ^PW ^PO ^PM (persistent mirror image) ^LH ^LR ^LT (label top) ^LS (label shift) ^LL (label length) ^CI ^MU (units) ^PQ (print quantity) ^FX (comment) ^SN/^SF (serial state) |
| Stored Formats | ^DF ^XF |
DataMatrix rendering supports ECC 000, 050, 080, 100, 140 and 200. Omitted or empty ZPL
^BX quality defaults to ECC 000, as specified by Zebra; use ^BXN,4,200
for modern ECC 200. The Legacy path supports six encodation formats,
CRC, convolutional protection, randomization and square symbols up to 49 modules.
Legacy ZPL preserves raw field bytes and ^FH bytes, including stored-format
recalls. Backslashes and pipes remain literal, matching the recorded CI13/CI27
printer probes; control bytes can be supplied through ^FH.
Numeric records above 511 remain explicitly unsupported.
They never silently become ECC 200. The raw Legacy encoder API accepts bytes.
EPL DataMatrix continues to use ECC 200. See Legacy scope and evidence
for the norm-based implementation, printer observations, limitations and source
attribution. No independent overall validation is claimed.
N (new label) · A (text) · B (1D barcodes — Code 128, Code 39, EAN-13/8, UPC-A/E, 2-of-5, Codabar, …) · b (2D: QR, DataMatrix, Aztec, PDF417, MaxiCode) · LO/LW (black/white line) · LS (diagonal) · X (box) · GW (graphic write) · R (reference point) · P (print)
Architecture
ZPL/EPL input
│
▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│ Parser │ ──▶ │ Renderer │ ──▶ │ Encoder │
└─────────┘ └──────────┘ └─────────┘
│ │ │
LabelInfo RgbaImage PNG / PDF
- Parser (
src/parsers/) — tokenizes input, maintainsVirtualPrinterstate, produces typedLabelElements - Renderer (
src/drawers/) — rasterizes elements onto anRgbaImagecanvas (text, graphics, barcodes, reverse print, label inversion) - Encoder (
src/images/) — emits 1-bit monochrome PNG or a single-page PDF
cargo build --release # binary: target/release/labelize
cargo test # all tests (124 golden E2E + unit)
cargo test --test 'e2e_*' # golden-file tests vs Labelary references
cargo test --test 'unit_*' # unit tests
PATH="$PATH:target/debug" bash e2e/http/test_http.sh # HTTP integration
PATH="$PATH:target/debug" bash e2e/cli/test_cli.sh # CLI integrationAfter any rendering change, regenerate and commit the diff artifacts
(cargo test --test e2e_diff_report -- --nocapture) — CI enforces this via the
golden-staleness check. See AGENTS.md for the full workflow.
Building a Windows binary without a Windows toolchain:
tools/build/build-windows.sh # → target/windows-release/labelize.exe (via Docker + mingw-w64)- Shipping label preview — see exactly what prints before it hits the printer
- Warehouse management — batch-convert label templates to PDF for archival
- E-commerce integrations — generate label PNGs on the fly from a microservice
- Automated QA — validate label content in CI/CD with golden-file tests
- Label design tools — add real-time ZPL preview with the library or wasm API
Contributions are welcome! Bug reports, new ZPL/EPL commands, rendering improvements, and docs fixes all count — see AGENTS.md for the development workflow, then open an issue or submit a pull request.
MIT AND BSD-3-Clause — see THIRD_PARTY_LICENSES.md for embedded assets.
Looking for a ZPL renderer, ZPL to PNG converter, ZPL to PDF, EPL parser, Zebra label preview, thermal label rendering, or a Labelary alternative? Labelize covers all of these.



