Shipping Labels and Barcodes in Generated PDFs

A practical guide to generating shipping labels with barcodes in PDFs — covering label sizing, barcode encoding, print quality, and how to avoid the common mistakes that cause scanners to fail.

pdfshippingbarcodesnode.jslogistics

Shipping Labels and Barcodes in Generated PDFs

Shipping labels look simple until you try to print one that a warehouse scanner will actually read. Between carrier requirements, thermal printer quirks, and the fact that barcodes are surprisingly fragile at low DPI, there's more to it than dropping an image into an HTML template. This post walks through what actually matters when generating shipping label PDFs programmatically.

The label anatomy

A typical shipping label has four zones:

  • Carrier branding + service level (top)
  • Ship-from and ship-to blocks
  • Tracking / routing barcode (usually Code 128 or GS1-128)
  • 2D barcode (usually PDF417 or QR) with structured data for automation

Every zone has scanner requirements. If you compress the barcode zone to fit a nicer layout, you'll get rejected packages. Design around the barcode, not the other way around.

Standard label sizes

Most carriers accept one of these formats:

txt
4x6 in (288x432 pt) — thermal printers, most common 4x6.75 (288x486 pt) — UPS/FedEx with return label attached A6 (105x148 mm) — international carriers

At 4×6 inches, plan your PDF as exactly 288×432 PostScript points. Do not scale later — scaling a barcode after rasterization destroys the quiet zones and module widths.

Generating a Code 128 barcode

Code 128 is the workhorse for tracking numbers. In Node, bwip-js is reliable and supports GS1-128, PDF417, DataMatrix, and QR:

bash
npm install bwip-js pdfkit

Generate a barcode as a PNG buffer, then embed it:

ts
import bwipjs from "bwip-js"; import PDFDocument from "pdfkit"; import fs from "node:fs"; const barcodePng = await bwipjs.toBuffer({ bcid: "code128", text: "1Z999AA10123456784", scale: 3, height: 20, includetext: true, textxalign: "center", }); const doc = new PDFDocument({ size: [288, 432], margin: 0 }); doc.pipe(fs.createWriteStream("label.pdf")); doc.image(barcodePng, 20, 260, { width: 248 }); doc.end();

The important knob is scale. A scale of 3 at 96 DPI yields roughly 300 DPI when the label is printed at nominal size. Anything below scale: 2 will produce narrow bars that thermal printers can't render cleanly.

PDF417 for carrier-required data blobs

UPS, FedEx, and USPS all require a PDF417 symbol encoding structured shipment metadata (Maxicode for UPS ground, but PDF417 is common elsewhere):

ts
const pdf417 = await bwipjs.toBuffer({ bcid: "pdf417", text: "[)>\x1E01\x1D96...", // carrier-specific format columns: 10, rows: 8, scale: 2, }); doc.image(pdf417, 20, 340, { width: 248, height: 60 });

The payload format is dictated by the carrier's label spec — don't invent your own. USPS IMpb, UPS MaxiCode, and FedEx Ground 96 barcodes all have documented byte layouts.

Quiet zones matter more than you think

Every barcode needs a blank margin around it (the "quiet zone"). For Code 128, that's a minimum of 10× the narrow bar width. If you place a border, background color, or adjacent text inside that zone, scanners will fail intermittently — the worst kind of bug, because it works fine on your desk and breaks in the warehouse.

Rule of thumb: leave at least 3mm of pure white on all four sides of any barcode.

Rendering with HTML templates

If you're generating labels from HTML (which is often easier for layout), use a base64 data URL for the barcode image:

ts
const png = await bwipjs.toBuffer({ bcid: "code128", text: trackingNumber, scale: 3, height: 20 }); const dataUrl = `data:image/png;base64,${png.toString("base64")}`; const html = ` <div class="label"> <div class="ship-to">${shipTo.name}<br>${shipTo.address}</div> <img src="${dataUrl}" class="barcode" /> <div class="tracking">${trackingNumber}</div> </div> `;

The critical CSS:

css
@page { size: 4in 6in; margin: 0; } .label { width: 4in; height: 6in; } .barcode { width: 3.4in; image-rendering: pixelated; }

image-rendering: pixelated prevents the browser from smoothing barcode edges, which would otherwise blur the bar boundaries.

Doing this with Kamy

If you'd rather not run a headless browser or manage PDF layout code, Kamy handles this with a template + data model. Barcodes are a first-class field type:

ts
import { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "shipping-label-4x6", data: { shipTo: { name: "Acme Corp", address: "123 Main St" }, tracking: "1Z999AA10123456784", service: "GROUND", barcodes: { tracking: { type: "code128", value: "1Z999AA10123456784" }, routing: { type: "pdf417", value: routingPayload }, }, }, });

Kamy renders barcodes at 300