zpl_render
A pure-Ruby ZPL II renderer. Converts Zebra label programs (^XA...^XZ) to
PNG images and print-size PDF documents, rasterizing at true printer
resolution so barcodes come out module-exact and scanner-readable.
Every barcode symbology in this gem is verified in CI by rendering labels and decoding them back with the zxing-cpp scanner engine, at every supported rotation and at 203 and 300 dpi.
Install
gem "zpl_render"
Runtime dependencies are pure Ruby: chunky_png, rqrcode_core, prawn.
No ImageMagick, no Java, no native extensions. Text is rasterized by a
built-in pure-Ruby TrueType engine from bundled Liberation fonts (SIL OFL).
Usage
require "zpl_render"
zpl = File.read("label.zpl")
# PNG (first label). dpmm: 6 = 152 dpi, 8 = 203 dpi, 12 = 300 dpi, 24 = 600 dpi
png = ZplRender.to_png(zpl, dpmm: 8, width_in: 4, height_in: 6)
File.binwrite("label.png", png)
# PDF: one page per ^XA..^XZ format, at exact physical label size
pdf = ZplRender.to_pdf(zpl, dpmm: 8, width_in: 4, height_in: 6)
File.binwrite("labels.pdf", pdf)
# All labels as PNGs, upscaled 2x for screen display
pngs = ZplRender.to_pngs(zpl, scale: 2)
# Collect diagnostics about unsupported commands
warnings = []
ZplRender.to_png(zpl, warnings: warnings)
# Strict mode: raise on the first problem instead of warning
ZplRender.to_png(zpl, strict: true)
# Base64-encoded ZPL (e.g. from a database column or WMS export):
# decoded automatically before rendering
ZplRender.to_png(b64_payload, base64: true)
zpl = ZplRender.decode_base64(b64_payload) # or recover the raw .zpl
Errors
Every intentional failure raises ZplRender::Error (invalid input type,
bad dpmm/dimensions, no ^XA..^XZ formats, oversized barcode data in
strict mode). By default rendering is lenient, like a printer: a field
that cannot render or an unsupported command is recorded in the
warnings: array with its type and position (for example
pdf417 barcode field at (21,433) failed: ...) and rendering continues.
Pass strict: true (CLI: --strict) for pipelines that must not
silently drop label content.
CLI
zpl_render label.zpl -o label.png # PNG at 203 dpi, 4x6"
zpl_render label.zpl -o label.pdf -d 12 # PDF at 300 dpi
cat label.zpl | zpl_render -o out.png -W 4 -H 2
zpl_render --base64 label.b64 -o label.png # Base64-encoded ZPL input
Barcode accuracy
Barcodes are generated from the symbology specifications, not drawn
approximately, and are rendered on a 1-bit dot grid with integer module
widths (^BY), exactly as a Zebra print head would lay them down:
| Symbology | ZPL | Implementation |
|---|---|---|
| Code 128 | ^BC |
Own encoder: ISO/IEC 15417 patterns, mod-103 check, ZPL subset invocation codes (>9 >: >; >5 >6 >7 >8 ...), modes N, A (automatic packing), U (UCC case, SSCC-18 + mod-10) and D (GS1 with application identifiers, FNC1) |
| QR Code | ^BQ |
rqrcode_core (full ISO/IEC 18004: Reed-Solomon ECC, mask scoring); ZPL <ECC><mode>, field-data prefixes (QA, HM, ...) |
| PDF417 | ^B7 |
Own encoder: ISO/IEC 15438 cluster patterns, Reed-Solomon over GF(929), security levels 0-8, columns/rows/truncated; verified byte-exact against FedEx ANSI MH10.8.2 payloads incl. GS/RS/EOT control characters |
| Code 39 | ^B3 |
Full pattern table, optional mod-43 check digit |
| Interleaved 2 of 5 | ^B2 |
Interleaved pairs, optional mod-10 check digit |
GS1-128 output is validated down to the application identifier level: a
scanner decodes the mode-U/D output as (00)61414100... etc.
Supported commands
^XA ^XZ formats (multiple per stream), ^FO ^FT ^FS ^FD ^FV ^SN,
fonts ^A ^A@ ^CF with ^FB field blocks (wrap, \& line breaks, L/C/R
justification), ^FH hex escapes, ^CI, field reverse ^FR,
default orientation ^FW and per-field rotation (N/R/I/B),
barcodes ^BY ^BC ^B3 ^B2 ^B7 ^BQ (with interpretation lines, above/below),
graphics ^GB ^GC ^GD, images ^GF (ASCII hex, Zebra RLE compression,
:B64:/:Z64:) and ~DG/^XG stored graphics,
layout ^PW ^LL ^LH, prefix changes ^CC ~CC ^CT ~CT, comments ^FX.
Print-control commands with no visual effect (^PQ ^MM ^MD ...) are
ignored; anything else is reported through the warnings: array.
Fidelity notes
- Scalable font 0 renders with Liberation Sans Bold (metrics match Helvetica/Arial Bold, close to Zebra's CG Triumvirate Bold Condensed), with advances and cap height calibrated against Labelary reference renders. Bitmap fonts A-H render with Liberation Mono Regular at the ZPL matrix cell sizes and inter-character gaps; font B is uppercase-only like the hardware. On a real FedEx Ground label this reproduces the reference render with ~94% pixel agreement, and Code 128 output is pixel-identical. If the bundled fonts cannot be loaded, an embedded public-domain 8x8 bitmap font is the fallback.
^PWnarrower than the label width is centered on the canvas and never shrinks it, matching printer behavior.^B7PDF417 uses byte + numeric compaction; a Zebra printer may pick text compaction for the same data, producing a denser but identically-decoding symbol.^BQmask selection is delegated to the encoder's automatic mask scoring (always produces a spec-valid, scannable symbol; the printed pixel pattern may differ from a printer forcing a specific mask).- DataMatrix (
^BX) and template recall (^FN/^XF) are not yet implemented and are surfaced as warnings.
Development
bundle install
bundle exec rspec # includes scanner round-trip tests when
# python3 + zxing-cpp + pillow are installed
CI runs the suite (with the real zxing-cpp scanner) on Ruby 3.0 through
3.4 via the version matrix in .github/workflows/ci.yml and
.gitlab-ci.yml.
Releasing
- Update
ZplRender::VERSIONand move the[Unreleased]notes to a new version section inCHANGELOG.md. - Commit, then tag and push:
git tag v0.2.0 && git push origin main v0.2.0.
The Release workflow runs the full test matrix, verifies the tag matches
ZplRender::VERSION, builds the gem, and creates a GitHub release with
the .gem attached and notes taken from the changelog. If a
RUBYGEMS_API_KEY repository secret is configured, it also publishes to
rubygems.org.
License
MIT. Embedded 8x8 font derived from public-domain IBM VGA fonts (via Daniel Hepper's font8x8).