Shipping Labels and Barcodes in Generated PDFs

A practical guide to generating shipping labels with barcodes in PDFs, covering layout sizing, barcode formats, carrier requirements, and rendering pipelines.

pdfshippingbarcodeslogisticsnode.js

Shipping Labels and Barcodes in Generated PDFs

Shipping labels look simple — a few addresses, a barcode, maybe a logo — but they're one of the easiest documents to get wrong. A label that scans cleanly in your test scanner can be rejected at a carrier sortation facility because the barcode is 2mm too narrow, the quiet zone is missing, or the PDF was scaled at print time. This post walks through what actually matters when generating shipping labels as PDFs, and how to do it without fighting your rendering pipeline.

The constraints you're actually working with

Shipping labels are physical artifacts first, PDFs second. Before writing a line of code, lock down:

  • Label size. 4×6 inches (101.6×152.4 mm) is the de facto standard for thermal printers. UPS, FedEx, DHL, and USPS all accept it. Some warehouse setups still use 4×8.
  • DPI. Thermal printers run at 203 DPI (8 dots/mm) or 300 DPI. Barcodes must align to whole dots, or you'll get edge artifacts.
  • Barcode symbology. USPS uses IMpb (a Code 128 variant), UPS uses MaxiCode + Code 128, FedEx uses Code 128 with specific Application Identifiers. If you're generating labels for a specific carrier, their spec is non-negotiable.
  • Quiet zone. Every linear barcode needs blank space (typically 10x the narrowest bar width) on both sides. Skipping this is the #1 reason labels fail to scan.

Page setup

Generate the PDF at the exact physical size. Don't generate Letter and hope the printer scales correctly — it won't.

js
// Using pdf-lib for a 4x6 thermal label import { PDFDocument } from 'pdf-lib'; const doc = await PDFDocument.create(); // 4x6 inches in points (72 pt = 1 inch) const page = doc.addPage([288, 432]);

If you're sending this to a Zebra or similar thermal printer, also disable any "fit to page" behavior in your print job. Generate at exact size, print at exact size.

Generating the barcode

You have three reasonable options:

  1. Render the barcode as an SVG or PNG and embed it. Most flexible.
  2. Draw the bars directly using PDF primitives. Smallest file size, sharpest output.
  3. Use a barcode font. Works, but font handling across PDF viewers is unreliable for production label printing.

Option 1 with bwip-js is the most pragmatic for Node:

js
import bwipjs from 'bwip-js'; import { PDFDocument } from 'pdf-lib'; const png = await bwipjs.toBuffer({ bcid: 'code128', text: '1Z999AA10123456784', scale: 3, // controls module width height: 15, // mm includetext: true, textxalign: 'center', }); const doc = await PDFDocument.create(); const page = doc.addPage([288, 432]); const image = await doc.embedPng(png); page.drawImage(image, { x: 24, y: 60, width: 240, height: 80, });

A few things to watch:

  • Don't resample. If you generate the barcode at 200×80 pixels and stretch it to 240×80 points, the bar widths will alias. Generate at the target size or use SVG.
  • Pick the right symbology. code128 for most carrier tracking numbers, gs1-128 for SSCC/AI-encoded data, datamatrix for compact 2D, pdf417 for high-density.
  • Test by scanning. Always. Use a real scanner, not your phone camera. Phone cameras are too forgiving and will scan things that warehouse scanners won't.

2D barcodes and Data Matrix

For pack-level labels, returns, and most modern carrier integrations, you'll want a 2D barcode. Data Matrix and PDF417 are common:

js
const dataMatrix = await bwipjs.toBuffer({ bcid: 'datamatrix', text: '(00)106141412345678908', scale: 4, padding: 5, });

2D codes are more tolerant of damage but still need a clear quiet zone of at least one module on every side.

Laying out the rest of the label

Beyond the barcode, a typical carrier label has:

  • Sender address (small, top-left)
  • Recipient address (large, center)
  • Service type and routing code (large, often bold)
  • Tracking number (under the barcode, human-readable)
  • A 2D code for sortation

Use a monospaced or condensed sans-serif for addresses. Helvetica Bold at 10–12pt scans well at 203 DPI. Keep text away from the barcode quiet zone.

Generating labels at scale

If you're producing one label per order, on-the-fly generation is fine. If you're batch-generating thousands per hour, two things matter:

  1. Concatenate, don't create new docs. Reuse the PDF document and append pages. Embedding the same logo or font across thousands of new documents wastes CPU.
  2. Stream the output. Don't buffer 10,000 labels in memory before responding.
js
const doc = await PDFDocument.create(); const logo = await doc.embedPng(logoBytes); // once for (const shipment of shipments) { const page = doc.addPage([288, 432]); page.drawImage(logo, { x: 12, y: 400, width: 60, height: 20 }); // ...draw barcode and addresses } const bytes = await doc.save();

Offloading the whole pipeline

If you'd rather work in