Shipping labels and barcodes in generated PDFs
Shipping labels look simple until you have to generate one. Then you discover that a 4x6 label needs exact dimensions, carriers reject anything with a smudged barcode, and your PDF viewer renders the Code 128 at a resolution the warehouse scanner can't read. This post walks through what actually matters when you generate shipping labels and barcodes as PDFs — the formats, the sizing, the gotchas, and how to wire it up.
The two formats that matter: 4x6 and A4
Most thermal label printers (Zebra, DYMO, Brother QL) expect a 4x6 inch label — exactly 288x432 points in PDF units (72 points per inch). If you render an 8.5x11 page and expect the printer to scale it, you'll get clipped labels and misaligned barcodes.
Set the page size explicitly:
tsimport PDFDocument from "pdfkit"; const doc = new PDFDocument({ size: [288, 432], // 4x6 inches in points margin: 0, });
If your labels go on A4 or Letter paper with multiple labels per sheet (think Avery templates), you need the opposite: a full-size page with precise cell positioning. Avery 5163, for example, is 10 labels per sheet, 2 columns x 5 rows, each 4x2 inches.
Barcode symbologies: pick the right one
Not all barcodes are equal. Here's what carriers actually use:
- Code 128 — Most common. Supports full ASCII, high density. Used by UPS, FedEx tracking numbers, and most internal warehouse systems.
- Code 39 — Older, less dense, uppercase + digits + a few symbols. Still common in healthcare and automotive.
- GS1-128 (UCC/EAN-128) — Code 128 with structured Application Identifiers. Required for shipments into most retail distribution centers.
- PDF417 — 2D stacked barcode. USPS IMpb labels require this.
- Data Matrix / QR — For small items and dense data. UPS Mail Innovations and some international carriers use Data Matrix.
Pick based on the carrier's spec sheet, not what looks cool.
Generating barcodes as vector, not raster
This is the single most important rule: render barcodes as vector paths (SVG or PDF drawing commands), not PNG images. A rasterized barcode at 150 DPI looks fine on screen and fails at the scanner because the thin bars pick up anti-aliasing artifacts.
Using bwip-js to generate a vector barcode and embed it in a PDF:
tsimport bwipjs from "bwip-js"; import PDFDocument from "pdfkit"; import fs from "fs"; const svg = bwipjs.toSVG({ bcid: "code128", text: "1Z999AA10123456784", height: 15, includetext: true, textxalign: "center", }); const doc = new PDFDocument({ size: [288, 432], margin: 0 }); doc.pipe(fs.createWriteStream("label.pdf")); // SVGtoPDF is a separate helper, e.g. svg-to-pdfkit SVGtoPDF(doc, svg, 20, 200, { width: 248 }); doc.end();
The barcode now scales cleanly at any zoom level and prints crisply on both thermal and laser printers.
Minimum bar width (X-dimension)
Scanners need a minimum bar width to read reliably. For Code 128 on a thermal printer, the X-dimension should be at least 0.5 mm (0.02 inches). For label stock printed at 203 DPI, that's about 4 pixels per narrow bar. Below that, you'll get intermittent scan failures that are miserable to debug because they only show up in the warehouse.
Also respect the quiet zone: at least 10x the X-dimension of blank space on each side of the barcode. Don't butt text or borders against it.
A realistic label layout
A typical carrier label has:
- Carrier logo and service class (top)
- Ship-from address (small, top-left)
- Ship-to address (large, bold, center)
- Routing barcode (middle)
- Tracking number barcode (bottom)
- Human-readable tracking number below the barcode
Here's a trimmed example:
tsconst doc = new PDFDocument({ size: [288, 432], margin: 0 }); doc.fontSize(10).text("FROM:", 10, 10); doc.fontSize(9).text("Acme Co\n123 Warehouse Ln\nAustin, TX 78701", 10, 24); doc.fontSize(14).font("Helvetica-Bold") .text("SHIP TO:", 10, 90); doc.fontSize(16) .text("Jane Doe\n500 Market St\nSan Francisco, CA 94105", 10, 110); // Routing barcode SVGtoPDF(doc, routingSvg, 20, 230, { width: 248 }); // Tracking barcode + text SVGtoPDF(doc, trackingSvg, 20, 330, { width: 248 }); doc.fontSize(12).font("Courier") .text("1Z999AA10123456784", 0, 400, { width: 288, align: "center" }); doc.end();
Letting a rendering service do the heavy lifting
If you don't want to maintain PDFKit layout code, SVG barcode libraries, and per-carrier templates in your service, you can offload the rendering. With Kamy, you define the label as an HTML template once and post the shipment data:
tsimport { kamy } from "@kamydev/sdk"; const pdf