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.
  • ^PW narrower than the label width is centered on the canvas and never shrinks it, matching printer behavior.
  • ^B7 PDF417 uses byte + numeric compaction; a Zebra printer may pick text compaction for the same data, producing a denser but identically-decoding symbol.
  • ^BQ mask 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

  1. Update ZplRender::VERSION and move the [Unreleased] notes to a new version section in CHANGELOG.md.
  2. 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).