To add a barcode to a PDF built from HTML, generate the barcode as a base64 data URI or inline SVG in your backend code, drop it into an <img> tag in your template, and render the HTML to PDF. The barcode becomes part of the page and prints at full resolution. Do not point an <img> at an external barcode service: the network request can finish after the page snapshot fires and leave a blank box on a label you already shipped.
This guide shows the data-URI method in Node.js, Python, and PHP, which barcode symbology to pick, the quiet-zone and sizing rules that keep it scannable, and how to put a dynamic barcode on every invoice or shipping label with PDF4.dev.
What is the most reliable way to put a barcode in a PDF?
The most reliable method is to generate the barcode as a base64 data URI or inline SVG and embed it directly in the HTML before rendering. A data URI carries the image bytes inside the src attribute, so there is nothing to fetch. The renderer already has the pixels when it snapshots the page.
The alternative, pointing an <img> at a remote barcode API, adds a network round trip during rendering. If the request is slow or the service rate-limits you, the PDF engine can capture the page before the image loads and produce a missing-image box. On a warehouse label or a courier manifest, that failure ships out the door and cannot be recalled.
| Method | Renders reliably? | Works offline? | Print quality | Best for |
|---|---|---|---|---|
| Inline SVG | Yes | Yes | Sharp at any size | Labels, high-resolution print |
| Base64 data URI (PNG) | Yes | Yes | Good if rendered large | Most documents |
| Barcode font | Partly | Yes | Fragile, mapping errors | Avoid for PDFs |
| External image URL | No, network-dependent | No | Varies | Avoid for PDFs |
Barcode fonts look convenient, but they skip the check digit and quiet zone, need exact character mapping, and break silently when a glyph is missing. Generating the barcode with a real library removes that class of breakage and computes the check digit for you.
Which barcode symbology should you choose?
Pick the symbology that matches your data and your reader. Code 128 is the default for developer use because it encodes the full ASCII set, from order IDs to alphanumeric SKUs, at high density. Retail product codes (EAN-13, UPC-A) are only valid if the product owns a real GTIN, and 2D codes hold far more data than any 1D barcode.
A barcode symbology is the encoding standard that defines how data maps to bars and spaces. Code 128 is standardized as ISO/IEC 15417 and is the workhorse for logistics and internal identifiers.
| Symbology | Encodes | Typical use | Check digit |
|---|---|---|---|
| Code 128 | Full ASCII | Order IDs, SKUs, shipping | Automatic |
| Code 39 | A-Z, 0-9, some symbols | Older industrial, badges | Optional |
| EAN-13 | 13 digits (GTIN) | Retail products (world) | Required |
| UPC-A | 12 digits (GTIN) | Retail products (US, Canada) | Required |
| ITF-14 | 14 digits | Shipping cartons, cases | Required |
| QR code (2D) | URLs, long text | Payment links, rich payloads | Built-in |
If you need to encode a URL, a payment link, or more than about 20 characters, use a 2D code instead. See how to add a QR code to a PDF for that workflow, which uses the same data-URI pattern shown here.
How do you generate a barcode as a data URI?
Use a barcode library to encode your value as Code 128, then inject the result into your HTML. The bwip-js package for Node.js, python-barcode for Python, and picqer/php-barcode-generator for PHP each produce an embeddable image in a few lines.
import bwipjs from 'bwip-js';
// Returns a PNG Buffer for the Code 128 barcode
const png = await bwipjs.toBuffer({
bcid: 'code128', // barcode type
text: 'INV-2026-0042', // value to encode
scale: 3, // 3x pixel density (crisp at print)
height: 12, // bar height in mm
includetext: true, // print the human-readable value
textxalign: 'center',
});
const dataUri = `data:image/png;base64,${png.toString('base64')}`;
const html = `
<div style="text-align:center">
<img src="${dataUri}" alt="Order INV-2026-0042"
style="height:16mm" />
<p style="font-family:monospace">INV-2026-0042</p>
</div>
`;Each snippet returns a self-contained image string. Because the bytes live inside the HTML, the barcode renders whether or not the machine generating the PDF has network access.
Why use inline SVG for barcodes that get printed?
Use inline SVG when the label will be printed and scanned, because vector bars stay sharp at any size. A barcode scanner reads the ratio between narrow and wide bars, so soft or blurred edges from an upscaled raster image are the top cause of failed reads. SVG has no pixels to blur.
The python-barcode example above already emits SVG. In Node.js, bwip-js exposes toSVG for the same result:
import bwipjs from 'bwip-js';
const svg = bwipjs.toSVG({
bcid: 'code128',
text: 'INV-2026-0042',
height: 12,
includetext: true,
textxalign: 'center',
});
// Inline the SVG straight into the template, no img tag needed
const html = `<div class="label">${svg}</div>`;Inlining the SVG markup directly, rather than base64-encoding it into an <img>, keeps the file smaller and lets you style the bars with CSS if needed. Both approaches render reliably because nothing is fetched at snapshot time.
What sizing and quiet-zone rules keep a barcode scannable?
Keep the narrowest bar at or above 0.25 mm (roughly 10 mil), the bar height at 10 mm or more, and leave a quiet zone of at least 10 times the narrowest bar width on both sides. These three rules cover most Code 128 scanning failures.
A quiet zone is the blank margin before the first bar and after the last one that lets the scanner find the code boundaries. GS1's barcode guidance treats the quiet zone as part of the symbol, not optional padding. If your CSS crops it, many handheld scanners silently refuse the read.
| Rule | Recommended value | Why it matters |
|---|---|---|
| Narrowest bar (X dimension) | 0.25 mm or more | Below this, print bleed merges bars |
| Bar height | 10 mm or more | Allows scanning at a shallow angle |
| Quiet zone | 10x narrowest bar, each side | Scanner detects start and stop |
| Contrast | Black on white | Color or gray reduces read rate |
| Aspect ratio | Do not stretch one axis | Distorted bar ratios fail to decode |
Values are typical minimums for Code 128 in general retail and logistics. Verify against your scanner hardware and any carrier or retailer label spec, which may require larger dimensions.
Never scale a barcode by setting only width in CSS. Stretching one axis changes the bar-to-space ratio and breaks decoding. Set the height and let the width follow the encoded data, or size the whole image proportionally.
How do you put a dynamic barcode on every invoice or label?
Generate the barcode per render from the record data, then pass the data URI into your template as a variable. For an invoice, encode the invoice number; for a shipping label, encode the tracking or order ID. The template stays static while the barcode changes on every document.
This is the same pattern as generating PDF invoices programmatically: the layout is a reusable template and the per-document values, including the barcode image, come from your data. Handlebars variables make the wiring explicit, covered in the Handlebars templates guide.
// Build the barcode for this specific order, then render
import bwipjs from 'bwip-js';
async function renderLabel(order) {
const png = await bwipjs.toBuffer({
bcid: 'code128',
text: order.trackingId,
scale: 3,
height: 12,
includetext: true,
});
const barcode = `data:image/png;base64,${png.toString('base64')}`;
return fetch('https://api.pdf4.dev/v1/render', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PDF4_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
template_id: 'shipping-label',
data: { barcode, tracking: order.trackingId, to: order.address },
}),
});
}In the template, reference the variable as an image source: <img src="{{barcode}}" style="height:16mm">. The API compiles the template with your data and returns the PDF, so the barcode is baked into every label.
When the DIY route starts to hurt
Generating the barcode is the easy part. Rendering thousands of labels to PDF reliably is where a headless browser turns into an operations problem. This is true for barcodes, invoices, and any HTML-to-PDF workload.
Running Playwright or Puppeteer yourself means shipping a browser in your image (a Chromium install adds around 300 MB), keeping a warm browser pool so cold starts do not add seconds per render, and handling crashes when a spike of label prints exhausts memory. Serverless functions add their own limits: bundle size caps and short timeouts that a cold Chromium launch can blow through. See PDF generation in serverless environments for the specific limits.
None of this is impossible. The question is whether you want to operate a browser fleet or call an endpoint. The rendering engine is the same Chromium either way; the difference is who runs it.
With PDF4.dev you keep the barcode generation in your code and send the data URI in the render request. No Chromium to install, no browser pool to babysit. Try the free HTML to PDF tool to test a label layout, then move to the API when you are ready to automate.
Here is the difference in code. The DIY path launches a browser, sets the content, and manages the lifecycle:
import { chromium } from 'playwright';
const browser = await chromium.launch(); // cold start cost
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'load' });
const pdf = await page.pdf({ printBackground: true, format: 'A6' });
await browser.close(); // must not leakThe API path is one request with no infrastructure to maintain:
const res = await fetch('https://api.pdf4.dev/v1/render', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PDF4_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, format: { preset: 'custom', width: '105mm', height: '148mm' } }),
});Key takeaways
Add a barcode to a PDF by generating it as a base64 data URI or inline SVG in your backend, embedding it in an <img> tag or inline, and rendering the HTML to PDF. Use Code 128 for general data, reserve EAN-13 and UPC-A for products with a real GTIN, and prefer SVG for anything printed. Respect the quiet zone and minimum bar dimensions or scanners will reject an otherwise perfect code. For dynamic labels at volume, generate the barcode in your code and hand the rendering to PDF4.dev so you are not operating a browser fleet.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



