Shipping Labels and Barcodes in Generated PDFs

A practical guide to generating shipping labels with scannable barcodes in PDFs, covering label sizing, barcode encoding, and common pitfalls that cause scanner failures.

pdfbarcodesshippingnode.jslogistics

Shipping Labels and Barcodes in Generated PDFs

Shipping labels look simple — a sender, a recipient, a tracking number, a barcode — but generating them correctly is deceptively tricky. Carriers reject labels with the wrong dimensions. Scanners fail on barcodes rendered at the wrong DPI. PDFs that look fine on screen come out unreadable when printed on a 4×6 thermal printer. This post covers the practical details of generating shipping labels with scannable barcodes in PDFs.

Label dimensions and DPI

Almost every carrier (UPS, FedEx, USPS, DHL) accepts the 4×6 inch label format. That's 288 × 432 points in PDF units (72 points per inch), or 812 × 1218 pixels at 203 DPI — the native resolution of most thermal label printers.

The key constraint: the PDF page must match the physical label exactly. If you generate an 8.5×11 page with a 4×6 label drawn in a corner, thermal printers will either scale it (blurring the barcode) or clip it.

ts
// Correct: page size matches label size const page = doc.addPage({ size: [288, 432] }); // 4×6 inches in points // Wrong: drawing a label on a letter-size page const page = doc.addPage({ size: "Letter" });

If you control the printer, target 203 DPI. For higher-end laser printing, 300 DPI is standard. Either way, render barcodes as vectors when possible — they stay sharp at any resolution.

Choosing the right barcode symbology

Don't pick a barcode format because it "looks nice." Pick it because the receiving scanner expects it.

  • Code 128 — General-purpose 1D barcode. Used for internal SKUs, batch numbers, custom tracking IDs. Encodes alphanumeric data.
  • Code 39 — Older, less dense, but still common for industrial use and military shipments.
  • PDF417 — 2D stacked barcode. USPS Intelligent Mail Package Barcode (IMpb) and many shipping manifests use it.
  • QR Code — High-density 2D. Good for customer-facing tracking URLs, less common on carrier labels.
  • DataMatrix — Compact 2D barcode used heavily in pharma and parts labeling.
  • GS1-128 — Code 128 with a specific data structure (Application Identifiers). Required for many retail and logistics workflows.

If you're integrating with a carrier API, they'll usually return a pre-rendered barcode image or specify the exact symbology and data format. Don't improvise.

Generating barcodes with bwip-js

For Node.js, bwip-js is the most complete barcode library. It supports nearly every symbology and outputs PNG, SVG, or a canvas.

bash
npm install bwip-js pdfkit

Here's a minimal example that renders a Code 128 barcode and embeds it in a PDF:

ts
import PDFDocument from "pdfkit"; import bwipjs from "bwip-js"; import fs from "fs"; async function makeLabel(trackingNumber: string) { const barcodePng = await bwipjs.toBuffer({ bcid: "code128", text: trackingNumber, scale: 3, height: 12, includetext: true, textxalign: "center", }); const doc = new PDFDocument({ size: [288, 432], margin: 12 }); doc.pipe(fs.createWriteStream("label.pdf")); doc.fontSize(10).text("FROM: Warehouse 4", { align: "left" }); doc.moveDown(); doc.fontSize(12).text("TO: Jane Doe\n123 Main St\nPortland, OR 97201"); doc.moveDown(2); doc.image(barcodePng, { fit: [264, 100], align: "center" }); doc.end(); } makeLabel("1Z999AA10123456784");

The scale parameter is critical. At scale: 1, the barcode is rendered at 1 pixel per module — far too small for most scanners. A scale of 2 or 3 produces module widths that scan reliably at 203 DPI.

Quiet zones and the "white space" trap

Every barcode needs a quiet zone — empty space on both sides (and top/bottom for 2D codes). The standard is 10× the module width. Skip this, and scanners fail intermittently in ways that are hard to debug.

bwip-js adds quiet zones by default, but if you crop the image or place it flush against other elements, you'll break them. Always leave at least 5mm of whitespace around any barcode.

ts
// Reserve quiet zone explicitly doc.image(barcodePng, doc.x + 20, doc.y, { width: 240 });

Testing barcodes before you ship

Don't trust visual inspection. A barcode that looks perfect on screen can fail a real scanner. Three things to do:

  1. Print at actual size on a thermal or laser printer, then scan with a real handheld or a phone app like ZXing.
  2. Validate the encoded data with a verifier — zbarimg (from the ZBar project) is free and reliable.
  3. Test with the carrier's sandbox if you're integrating with UPS, FedEx, etc. They'll reject malformed labels at the API level.
bash
zbarimg --raw label-rendered.png

If the decoded string matches what you encoded, you're good.

Using Kamy for template-driven labels

If you'd rather not maintain barcode and layout code yourself, Kamy supports shipping label templates with built-in barcode rendering. You define the label as an HTML