A Swift package for generating, previewing, and printing ZPL labels on Zebra thermal printers.
LabelKit models the two things you have to get right before sending ZPL to a
label printer: the physical media (Stock — width, height, gap, die-cut vs.
continuous, in inches) and the printer's capabilities (Device — native DPI,
printable limits, in dots). From those it derives render geometry, runs your
label source through a chain of composable processors (Stencil templating,
automatic ^LL length injection, pretty-printing), and delivers the result to
a Target — a network printer, a file, stdout, or an inline iTerm2 image
preview. Because every payload carries its render DPI, sending 203 dpi ZPL to
a 300 dpi printer fails loudly instead of printing tiny.
macOS 14+ only; presets currently cover one printer (Zebra ZD620) and two stocks, though both types are trivially constructible for other hardware.
- Features
- Prerequisites
- Installation
- Quick Start
- Examples
- Configuration
- Documentation
- Known Limitations
- License
- Media-first modeling —
Stock(inches) andDevice(dots @ DPI) are separate types;RenderGeometryis derived, so inch→dot math lives in one place. - Composable ZPL processors —
ZPLLabelfolds its source through an ordered[ZPLProcessor]chain:ResolveTemplates— Stencil templating with aKeyValueContextof variablesInjectLength— estimates label length by walking ZPL commands (ZPLLengthEstimator) and injects a^LLPrettyPrint/ZPLFormatter— pretty-print or minify ZPL
- Pluggable delivery targets —
NetworkTarget(raw TCP, port 9100),FileTarget,StdoutTarget, andITerm2Target(renders PNG previews inline in your terminal). - Two PNG preview renderers —
LabelaryRenderer(Labelary web API) andZPL2PNGRenderer(offline, via azpl2pnghelper binary bundled as a package resource). - DPI safety — payloads are tagged with their render DPI; targets reject
mismatches with the device's native DPI unless you pass
strict: false. - Named template store —
StencilTemplateStoreloads reusable label templates from~/Library/Application Support/LabelKit/templates.json.
- macOS 14 (Sonoma) or later
- Swift 6.0 toolchain (Xcode 16+)
- A Zebra ZPL printer on your network (optional — previews work without one)
- iTerm2 for inline image previews (optional)
- jazzy only if you want to regenerate docs
Add LabelKit to your Package.swift:
dependencies: [
.package(url: "https://github.com/PeteRichardson/LabelKit.git", branch: "main")
]git clone git@github.com:PeteRichardson/LabelKit.git
cd LabelKit
swift build
swift testimport LabelKit
// 1. Describe the media and the printer
let env = ZPLEnvironment(
context: KeyValueContext(["name": "World"]),
stock: Stock.Preset.label2x1, // 2"x1" die-cut labels, 1/8" gap
device: Device.Preset.ZD620 // 300 dpi Zebra ZD620
)
// 2. Build a label: source + processor chain + environment
let label = ZPLLabel(
"^XA^FO50,50^A0N,40,40^FDHello {{ name }}^FS^XZ",
processors: [ResolveTemplates()!, InjectLength(), PrettyPrint()],
environment: env
)
// 3. Deliver it — to a printer...
let printer = NetworkTarget(device: Device.Preset.ZD620, host: "192.168.0.133", port: 9100)
try await printer.send(.zpl(label.zpl(), dpi: .dpi300))
// ...or preview it inline in iTerm2 without a printer
let renderer = try ZPL2PNGRenderer()
let png = try await renderer.render(
from: label.zpl(),
options: ImageRenderOptions(geometry: env.options.geometry, timeout: 2.0)
)
try await ITerm2Target(device: Device.Preset.ZD620).send(.png(png, dpi: .dpi300))Examples are organized in three tiers, from smallest to most complete:
Tier 1 — tutorial snippets (Snippets/): single-file, single-concept
programs. Read the source; each one demonstrates exactly one idea.
| Snippet | Demonstrates |
|---|---|
PrintToStdout |
The smallest end-to-end pipeline: build a label, pick a target, deliver it |
ChooseStockAndDevice |
Combining Stock (inches) and Device (DPI) into render geometry |
PrintAList |
Laying a sequence of ListLine values out with ListLayout |
RenderATemplate |
Filling a Stencil template with values via StencilTemplateStore |
PreviewInITerm2 |
Rendering ZPL to PNG and showing it inline in iTerm2 |
swift run PrintToStdout
swift run ChooseStockAndDevice
swift run PrintAList
swift run RenderATemplate
swift run PreviewInITerm2Tier 2 — labelprint (Examples/labelprint/): reads lines of text on
stdin and prints them as one long list label via ListLayout; lines ending in
: become section headers and blank lines become gaps.
cat list.txt | swift run labelprint # send to network printer (default)
cat list.txt | swift run labelprint preview # PNG preview inline in iTerm2
cat list.txt | swift run labelprint zpl # generated ZPL to stdout
cat list.txt | swift run labelprint list # echo parsed lines, for debugging headers/gapslabelprint defaults to its print subcommand, so a bare pipe goes straight
to the printer. Every subcommand takes -d/--debug to enable OSLog output
to Console.
Tier 3 — advanced examples (Examples/Advanced/): multi-feature programs
that combine several parts of LabelKit.
| Executable | Description |
|---|---|
batch-badges |
Renders one badge label per row of a CSV, from a single Stencil template |
compare-renderers |
Renders ZPL from stdin through both Labelary and zpl2png, side by side |
| LabelGUI | The Tier 3 GUI example — a SwiftUI app in Apps/LabelGUI, built via Apps/LabelGUI/LabelGUI.xcodeproj or Workspace/LabelWorkspace.xcworkspace in Xcode |
swift run batch-badges # template + CSV -> N labels
cat list.txt | swift run labelprint zpl | swift run compare-renderersThere is also a standalone example not part of the tiers above:
| Executable | Description |
|---|---|
example-reminderlist |
Prints your uncompleted Reminders.app items as a label on 4" continuous stock |
swift run example-reminderlist list # reminder titles as text
swift run example-reminderlist zpl # generated ZPL to stdout
swift run example-reminderlist preview # PNG preview inline in iTerm2
swift run example-reminderlist print # send to network printer
swift run example-reminderlist print -d # ...with debug logging to ConsoleExecutables that talk to a printer default to 192.168.0.133:9100. Override
it with --host/--port:
cat list.txt | swift run labelprint print --host 10.0.1.42 --port 9100
swift run example-reminderlist print --host 10.0.1.42or set the environment variables once:
export LABELKIT_PRINTER_HOST=10.0.1.42
export LABELKIT_PRINTER_PORT=9100Precedence is flag → environment variable → built-in default. No source edit
or rebuild required. example-reminderlist will prompt for Reminders access
on first run.
StencilTemplateStore (used by the RenderATemplate snippet, batch-badges,
and available to your own code) reads named templates from:
~/Library/Application Support/LabelKit/templates.json
The folder and file are created on first use. Templates are named Stencil
strings rendered against a [String: Any] context, e.g.:
{
"label": "^XA^FO50,50^A0N,40,40^FD{{ name }}^FS^XZ"
}- API reference lives in the DocC catalog at
Sources/LabelKit/LabelKit.docc/— browse it in Xcode (Product ▸ Build Documentation). - HTML docs can be generated with
./Scripts/generate_docs.sh(requires jazzy).
- macOS only (14+). Uses
Securityand a bundled macOS helper binary — no Linux support. - One device preset — only the ZD620 (300 dpi) ships as a preset;
construct your own
Devicefor other printers. LabelaryRendererneeds internet — it calls the Labelary web API. UseZPL2PNGRendererfor offline previews, except for list labels (see below).- The bundled
zpl2pngdoes not implement^FB— it renders only the first line of a field block and silently drops the rest.ListLayoutlays lists out with^FB, solabelprint previewandexample-reminderlist previewuseLabelaryRendererand therefore need network access. Printing is unaffected; the ZD620 renders^FBcorrectly.compare-renderersshows the divergence. InjectLengthchecks the device but not the stock, and its check ignores padding — the estimate is validated againstDevice.maxLengthDots, but the value actually injected is the estimate plus 150 dots. A label landing within 150 dots of the device maximum passes the check and still emits an over-long^LL. Stock height is not checked at all.- Length estimation parses a subset of ZPL —
ZPLLengthEstimatoraccounts for^FDtext,^FBfield blocks,^BCbarcodes and^GBboxes. Other barcode types and graphics contribute nothing to the estimate, so^LLcan come out short for labels that use them. ^FBwrapping is estimated, not measured — the estimator has no font metrics, so it charges each character an average fraction of the declared cell width. The fraction is deliberately generous, so a wrapped block usually reports a line or so more than the printer lays out:^LLruns slightly long and feeds a little extra media rather than clipping the print. Text set in unusually wide glyphs can still exceed the estimate.- No print-job feedback —
NetworkTargetfires raw TCP at port 9100 and does not read printer status back. It surfaces connection failures and timeouts, but never a paper-out, head-open, or job-rejected condition. - Sandboxed apps must ship the helper — a sandboxed process cannot execute
binaries outside its container, so
ZPL2PNGRendererskips the$PATHand/usr/local/binsearch steps when sandboxed. Bundlezpl2pnginContents/Helpers, or pass it explicitly toZPL2PNGRenderer(helperURL:).
Bugs are tracked as
GitHub issues — that
tracker, not this list, is the current source of truth. docs/reviews/PROJECT_REVIEW.md
records the audit those issues were filed from.
The crash and correctness bugs raised by earlier review rounds are fixed: the
force-unwrap on continuous stock (previews now throw
PreviewError.missingGeometry), ^FD content corruption in the formatter, the
pipe deadlock on previews larger than the OS pipe buffer, and the defeated
NetworkTarget connect timeout.
License not yet specified.