Barcode Generation with Python, HTML, and CSS
Here’s a complete MDX article you can drop in as-is. It focuses on generating QR codes with segno
and laying out printable, professional-looking barcode/QR labels using plain HTML + CSS. It also includes a small Python script that reads a CSV (or JSON) and outputs both image files and a printable labels page.
How it works
Modern teams can generate and print their own labels, no SaaS required.
- Create error-corrected QR codes in seconds using
segno
. - Export SVG/PNG for crisp printing.
- Build a print-ready labels sheet with plain HTML/CSS.
- Batch-generate from a CSV (e.g., SKU -> product page URL).
Note: segno generates QR codes (and Micro QR). For linear barcodes (Code128/EAN/UPC), pair this with a library like python-barcode. This article focuses on QR codes with segno.
Prerequisites
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install segno pandas
Quick start: one QR in 3 lines
import segno
qr = segno.make('https://example.com/products/ABC-123', error='h') # 'h' = high error correction
qr.save('qr-ABC-123.svg', scale=10, border=2) # SVG scales perfectly for print
error='h'
increases scan reliability if your label might be scuffed or partially covered.- For PNG instead:
qr.save('qr-ABC-123.png', scale=8, border=2)
Styling: colors, quiet zone, and size
import segno
qr = segno.make('INV-000456', error='m')
qr.save(
'invoice-qr.svg',
scale=12, # module pixel size (if saving raster, this is pixels; SVG scales infinitely)
border=4, # quiet zone (light margin) in modules
dark='#111111', # QR “ink” color
light='#ffffff' # QR background
)
- For light-on-dark labels (e.g., thermal printers on black stock), keep
light
very light anddark
very dark for contrast.
Embedding as a data URI (no image files!)
You can inline the QR as an SVG data URI—great for single-file HTML labels.
import segno
qr = segno.make('https://example.com/p/sku123')
data_uri = qr.svg_data_uri(scale=5) # 'scale' sets module size
print(data_uri) # paste into <img src="...">
Batch: from CSV to images + printable labels page
This script:
- Reads
labels.csv
with columns like:sku,title,url
. - Generates SVG and PNG for each row.
- Builds a printable HTML grid with embedded data URIs (so it’s one self-contained file).
- Gives you CSS tuned for common Avery-style label sheets (you can tweak sizes).
import csv
import html
import segno
from pathlib import Path
INPUT_CSV = Path("labels.csv") # sku,title,url
OUT_DIR = Path("out")
OUT_DIR.mkdir(parents=True, exist_ok=True)
rows = []
with INPUT_CSV.open(newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for r in reader:
sku = (r.get("sku") or "").strip()
title = (r.get("title") or "").strip()
url = (r.get("url") or "").strip()
if not sku or not url:
continue
qr = segno.make(url, error="h", micro=False)
# Save discrete assets if you want files
qr.save(OUT_DIR / f"{sku}.svg", scale=10, border=2)
qr.save(OUT_DIR / f"{sku}.png", scale=8, border=2)
# Build data URI for single-file printable HTML
data_uri = qr.svg_data_uri(scale=5) # scale => module size inside label
rows.append({
"sku": sku,
"title": title,
"url": url,
"data_uri": data_uri
})
# Create printable labels page
html_items = []
for r in rows:
safe_title = html.escape(r["title"] or r["sku"])
safe_sku = html.escape(r["sku"])
safe_url = html.escape(r["url"])
img_tag = f'<img alt="QR for {safe_sku}" src="{r["data_uri"]}" />'
html_items.append(f"""
<div class="label">
<div class="qr">{img_tag}</div>
<div class="meta">
<div class="title">{safe_title}</div>
<div class="sku">{safe_sku}</div>
<div class="url">{safe_url}</div>
</div>
</div>
""")
html_doc = f"""<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Printable Labels</title>
<style>
@page {{
size: A4; /* change to 'Letter' if in the US */
margin: 10mm; /* tune for your stock */
}}
:root {{
--label-w: 64mm; /* label width */
--label-h: 34mm; /* label height */
--gap-x: 4mm; /* horizontal gap between labels */
--gap-y: 4mm; /* vertical gap between labels */
}}
* {{ box-sizing: border-box; }}
body {{
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial;
}}
.sheet {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--label-w),1fr));
gap: var(--gap-y) var(--gap-x);
padding: 8mm;
}}
.label {{
width: var(--label-w);
height: var(--label-h);
border: 0.2mm solid #ddd;
border-radius: 2mm;
padding: 3mm;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 1fr;
align-items: center;
overflow: hidden;
}}
.label .qr {{
display: flex;
align-items: center;
justify-content: center;
padding-right: 3mm;
}}
.label img {{
display: block;
width: 28mm; /* size of QR inside label; adjust to fit */
height: 28mm;
}}
.label .meta {{
display: grid;
gap: 1mm;
min-width: 0;
}}
.label .title {{
font-size: 3.4mm;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}}
.label .sku {{
font-size: 3.2mm;
letter-spacing: 0.1mm;
}}
.label .url {{
font-size: 2.6mm;
color: #555;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}}
@media print {{
.label {{ border-color: transparent; }} /* hide borders for final print */
.sheet {{ padding: 0; }}
}}
</style>
</head>
<body>
<div class="sheet">
{''.join(html_items)}
</div>
</body>
</html>
"""
outfile = OUT_DIR / "labels.html"
outfile.write_text(html_doc, encoding="utf-8")
print(f"Wrote {outfile.resolve()}")
Sample labels.csv
sku,title,url
SKU-001,Widget Alpha,https://example.com/products/widget-alpha
SKU-002,Widget Beta,https://example.com/products/widget-beta
SKU-003,Widget Gamma,https://example.com/products/widget-gamma
Run the generator:
python generate_labels.py
# => out/SKU-001.svg, out/SKU-001.png, ..., and out/labels.html
Open out/labels.html
in your browser and Print (⌘/Ctrl + P). If you’re using pre-cut label sheets:
- Set Scale: 100% (no “Fit to page”).
- Disable any header/footer printing.
- Nudge
@page margin
and--gap-*
values until the grid lines up perfectly with your stock (keep a “calibration” copy of the CSS per label SKU like Avery 5160/5163, etc.).
Tips for resilient QR labels
- Error correction: Use
error='h'
for physical labels; it costs a bit more dense modules but scans better when damaged. - Quiet zone: Keep
border ≥ 2
modules; many scanners require clear margins. - Data length: Shorter payloads -> larger modules -> easier scans. Prefer short URLs/IDs or use a redirect short-link domain.
- Contrast: Dark “ink” on a light background. Avoid low-contrast color pairs.
Micro QR Codes (very small labels)
When space is tight, try Micro QR. It encodes less data but fits tiny labels:
import segno
qr = segno.make_micro('INV-457') # segno chooses the smallest micro version that fits
qr.save('micro-invoice.svg', scale=14, border=2)
Embedding QR into your app workflow
- Packing slips: Print QR that points to your order detail page.
- Inventory bins: Encode
SKU
orsku:<value>
and make your app route on scan. - Returns: Encode a one-time token, e.g.,
https://your.app/rma?t=...
.
Example tokenized payload with high error correction:
import secrets, segno
token = secrets.token_urlsafe(16)
qr = segno.make(f"https://your.app/rma?t={token}", error='h')
qr.save("rma.svg", scale=10, border=3)
Verifying scan quality
-
Test with multiple devices (iOS/Android defaults + an app like “SCANDIT”).
-
Print a test page at your chosen DPI and check from the expected distance.
-
If scans struggle, try:
- Increase
scale
- Increase
border
- Shorten payload (use redirects)
- Switch to SVG for vector-sharp print paths
- Increase
Bonus: mixing QR with linear barcodes
If you need both QR and a linear barcode (e.g., Code128 for scanners, QR for phones), add python-barcode
and render beneath the QR:
pip install python-barcode pillow
import segno, barcode
from barcode.writer import ImageWriter
from pathlib import Path
payload = "SKU-001"
segno.make(payload, error='m').save("out/sku-001-qr.svg", scale=8, border=2)
Path("out").mkdir(exist_ok=True, parents=True)
code128 = barcode.get("code128", payload, writer=ImageWriter())
code128.save("out/sku-001-code128", options={"module_height": 12.0, "quiet_zone": 2.0})
Then position both in your label HTML with simple CSS.
Troubleshooting
- QR looks tiny on print -> increase
scale
(SVG) orscale
/DPI (PNG), and ensure the<img>
width/height in CSS is large enough. - Misaligned to pre-cut sheets -> adjust
@page margin
and--gap-x/--gap-y
to match your stock’s template; keep a per-stock CSS. - Scanner fails on glossy paper -> reduce glare; try matte stock or darker
dark
color (near-black).
Wrap-up
You now have a reproducible pipeline to generate, layout, and print professional QR labels with open tools. Tweak the CSS to your exact paper stock, and hook the Python scripts into your ops flow (CI, cron, or a button in your internal tools).