Skip to content

PhysicalCollectionQRLabelPipeline

Dennis Lee edited this page Jul 4, 2026 · 2 revisions

title: Physical Collection QR Label Pipeline radar_quadrant: Techniques radar_ring: Trial radar_position: inner

Physical Collection QR Label Pipeline

When a physical card collection grows past 50–100 items, visual identification becomes a bottleneck — pulling a card from a top loader to read it defeats the purpose of organised storage. This technique bridges a digital catalog with physical labels, making each card self-identifying without internet access.

What This Technique Does

A pipeline script reads a card catalog export, encodes identity and pricing data into a QR code per card, and sends the result directly to a thermal label printer — no manual steps, no UI. Scanning a printed label with a phone camera shows the card's identity offline; tapping the embedded link opens a live pricing page when online.

Pipeline Stages

Stage 1 — Export: Coda.io, via a CLI export command, produces a CSV filtered to rows where a printed boolean column is false.

Stage 2 — Encode: A Python script reads each CSV row and builds a QR payload combining human-readable identity fields with obfuscated pricing fields and a live pricing URL. A deterministic QR code library (Segno) generates an image sized to the target label dimensions at 300 DPI. Because QR generation is deterministic, any label can be regenerated from source data at any time without storing the image file.

Stage 3 — Print: Four paths are available to drive the printer from a script. Path A — dymon: A community wrapper (dymon) sends each image via TCP/IP (dymon_pbm --net <IP>) for LAN-connected models. On macOS, Path A carries significant setup friction: no pre-built binary exists (must compile from source using Xcode CLT, Homebrew, and cmake); the --usb backend is Linux-only (a LAN-connected printer is required); Segno's PNG output must be converted to PBM with pixel calibration before printing; and 550 Turbo support is unconfirmed in the README. Once running, dymon operates as a launchd background service with no per-job startup step. Path B — DYMO Connect Web Service: The DYMO Connect Web Service is a local REST API (port ~41951) exposed by DYMO Connect for Desktop (free, macOS Mojave and later). It can be called directly with Python requests or the lightweight dymopy wrapper (pip install dymopy) -- no .NET required. Labels are submitted as DYMO XML rather than raw images, which requires an additional formatting step beyond Segno's PNG output. Path B works with both the 450 and 550 Turbo on any macOS without compilation. Path C — HP + Avery sheet: WeasyPrint (brew install weasyprint) renders an HTML/CSS template sized to Avery 6572 dimensions (2⅝" × 2", landscape) into a letter-sized PDF containing 15 labels per sheet arranged in three columns. The label area (66.7mm × 50.8mm) accommodates a vertical stack — QR code (~40mm square) on top, identity and pricing text below. The PDF is sent to any CUPS-registered HP inkjet or laser printer with lp -d <printer> -o media=letter output.pdf. No dedicated label printer is required; any HP printer already registered in macOS handles the job. Path C requires a minimum batch of one full sheet (15 cards) to avoid wasting stock. Path D — Brother QL: The brother_ql Python package sends PNG output from Segno directly to a Brother QL-series thermal printer via USB (pyusb backend, brew install libusb) or network, with no driver or intermediate format conversion required. Third-party label stock is viable: Brother QL printers do not enforce NFC DRM, making ~$10/1,000 third-party stock available. A 62mm continuous roll accommodates the QR payload with more layout margin than DYMO 30334. Hardware is required: QL-700 (USB only, ~$80) or QL-720NW (network, ~$130). Path D is documented from the brother_ql README and community reports; it has not been validated against physical hardware by the author.

Stage 4 — Mark printed: After successful printing, the pipeline calls the Coda CLI to set printed = true per card, making subsequent runs idempotent.

Path C Template Calibration

No official HTML/CSS template exists for Avery 6572 -- Avery publishes Word and PDF formats only. The layout below is estimated from the Avery 6490 glabels definition (the closest size in the open-source glabels database: 2.6875" × 2", same 3×5 layout) and requires a plain-paper calibration cycle before committing to label stock.

Prerequisites

  • brew install weasyprint and pip install weasyprint -- WeasyPrint renders HTML/CSS to PDF
  • Segno already in the pipeline for QR image generation
  • Any CUPS-registered HP printer (lpstat -p to list available printers)
  • Jinja2 for template rendering (pip install jinja2)

Layout Specifications

Parameter Value Source
Label size 2.625" × 2" (66.7 × 50.8mm) Avery confirmed
Layout 3 columns × 5 rows Calculated
Left/right margin ~0.21875" (5.56mm) Calculated from label width
Top/bottom margin 0.5" (12.7mm) Avery 6490 glabels reference
Column gap ~0.09375" (2.38mm) Avery 6490 glabels reference
Row gap 0 Avery 6490 glabels reference

Template

templates/label_6572.html

<!DOCTYPE html>
<html>
<head>
<style>
@page {
  size: 8.5in 11in;
  margin: 0.5in 0.21875in;
}
body { margin: 0; padding: 0; }
.sheet {
  display: grid;
  grid-template-columns: repeat(3, 2.625in);
  grid-template-rows: repeat(5, 2in);
  column-gap: 0.09375in;
  row-gap: 0;
}
.label {
  width: 2.625in;
  height: 2in;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 2mm;
  box-sizing: border-box;
  overflow: hidden;
}
.qr  { width: 40mm; height: 40mm; }
.text { font-size: 7pt; text-align: center; margin-top: 1mm; line-height: 1.3; }
</style>
</head>
<body>
<div class="sheet">
  {% for card in cards %}
  <div class="label">
    <img class="qr" src="{{ card.qr_path }}">
    <div class="text">
      <strong>{{ card.set }} | {{ card.rarity }}</strong><br>
      {{ card.character }} #{{ card.number }}<br>
      {{ card.prices_obfuscated }}
    </div>
  </div>
  {% endfor %}
</div>
</body>
</html>

generate_labels.py

from jinja2 import Template
import weasyprint, subprocess

def render_labels(cards, template_path="templates/label_6572.html", out="labels.pdf"):
    html = Template(open(template_path).read()).render(cards=cards)
    weasyprint.HTML(string=html, base_url=".").write_pdf(out)
    subprocess.run(["lp", "-d", "<printer>", "-o", "media=letter", out])

Calibration Cycle

This is a one-time step per printer model. It does not repeat per job.

  1. Run generate_labels.py with dummy data; print output on plain paper.
  2. Overlay the plain paper on an Avery 6572 sheet against a light source.
  3. Measure X and Y shift in mm.
  4. Adjust @page margin to absorb the offset -- e.g. a 2mm rightward shift means increase left margin by 2mm and decrease right margin by 2mm.
  5. Reprint on plain paper; confirm alignment; then run on label stock.

Payload Design

The QR payload for a 57mm × 32mm label stays under 200 characters:

Owner: dennislwm
Set: VIV | Rarity: RR | Type: Holo
Character: Pikachu VMAX | Card#: 044/185
Prices: 2250|3500|4000
https://www.pricecharting.com/game/pokemon-vivid-voltage/pikachu-vmax-44

The offline text block satisfies the first priority — card identity without internet. The PriceCharting URL satisfies the second — live market prices with one tap. Margin data (cost, menu, and maximum prices) is encoded as integers offset by a personal constant known only to the owner; the values read as arbitrary numbers to a casual observer. The offset is documented privately, not on the label or in the pipeline code.

Design Choices

Offline-first payload. The QR text is readable by any phone camera without internet. The URL is secondary — useful when online, irrelevant when not.

Obfuscation over omission for pricing. Pricing fields are included in encoded form rather than excluded, so the label remains complete as a standalone artifact. The encoding is not cryptographic; it deters casual reading, not a determined one. Private margin data (the Coda catalog) stays behind authentication and is never encoded into the label.

Pre-cut labels over continuous roll. DYMO 30334 (57mm × 32mm, 1,000 per roll) fit the back of an Ultra Pro standard top loader with comfortable margin on both sides of the 76.2mm outer width. The 550 Turbo includes Automatic Label Recognition (ALR) that reads roll size and remaining count from an NFC chip on the roll -- and rejects rolls that do not carry a recognised chip. This DRM blocks third-party stock at the hardware level. Genuine DYMO 30334 stock costs approximately $42 per 1,000; third-party RFID-compatible alternatives exist but are unverified against the 550's DRM check. Standard thermal stock is adequate for collection storage; durable polypropylene stock (approximately $80 per 1,000) is justified only for labels subject to regular handling or moisture exposure.

Composite key from existing columns. No new identifier column is needed. Set code, card number, and condition form a composite key (e.g. VIV-044-NM) that is human-readable, short, and derivable from data already in the catalog. Duplicate copies of the same card add a suffix counter.

Wired connection over Bluetooth. A wired printer connection eliminates the open-source Bluetooth driver dependency that community-maintained alternatives carry. Two hardware options are available: the DYMO LabelWriter 450 (USB only, no NFC DRM, third-party label stock viable at ~$10/1,000) and the DYMO LabelWriter 550 Turbo (USB + wired LAN, NFC DRM enforces genuine DYMO stock at ~$42/1,000). On macOS, dymon's USB backend does not function -- the 450 is only usable via Path B (DYMO Connect Web Service). The 550 Turbo's LAN port is accessible via dymon --net on macOS following the build fix merged in May 2026. DYMO Connect for Desktop (free, macOS Mojave and later) supports both models without this restriction.

Known Frictions

Ranked by severity:

Already-printed tracking (high). Without a printed boolean in the catalog, the pipeline reprints every card on each run. The flag must be maintained in Coda and flipped via CLI after each successful print job.

NFC label DRM (high). The 550 series reads NFC chips on label rolls and rejects non-genuine DYMO stock at the hardware level. Third-party labels at $10 per 1,000 -- viable on the older 450 series -- are blocked on the 550 Turbo unless they carry a compatible chip. Genuine DYMO 30334 stock ($42 per 1,000) is the safe baseline. RFID-compatible third-party alternatives exist but are unverified against the 550 Turbo's DRM check.

dymon macOS friction (high). Path A on macOS requires resolving five compounding issues before a single label prints: (1) no pre-built binary -- dymon must be compiled from source; Apple Clang compatibility was only fixed in May 2026 (issue #13); build deps are Xcode CLT, Homebrew, and cmake; (2) --usb is Linux-only -- the LabelWriter 450 cannot be used with dymon on macOS; a LAN-connected printer (550 Turbo) is required; (3) PNG-to-PBM conversion -- dymon expects PBM input; Segno produces PNG; Pillow converts in one line but pixel dimensions must be calibrated or prints are blurred or clipped (issue #10); (4) launchd service registration -- dymon must be running before the pipeline executes; without a launchd entry every job requires a manual server start; (5) 550 Turbo support unconfirmed -- the dymon README lists "550" but does not name the Turbo variant. Path B (DYMO Connect Web Service via dymopy) avoids all five.

DYMO Connect Web Service label format (medium). Path B submits labels as DYMO XML rather than raw images. The pipeline generates PNGs via Segno; an additional step to embed the QR image into a DYMO XML label definition is required before calling the Web Service.

Path C sheet minimum run (medium). Path C prints one full sheet of 15 Avery 6572 labels per job. Runs of fewer than 15 cards waste stock. The pipeline should be batched to near-full sheets; ad-hoc single-card reprints are uneconomical.

Path C template calibration (medium). No official HTML/CSS template exists for Avery 6572 -- Avery publishes Word and PDF formats only. Initial layout dimensions are estimated from the Avery 6490 glabels reference and require a plain-paper calibration cycle before printing on label stock. One-time effort per printer model; does not recur per job. See the Path C Template Calibration section above for the starting layout, sample template, and calibration steps.

Path D hardware unverified (medium). Path D is documented from the brother_ql README and community reports. The author has not validated it against physical hardware; hardware-specific surprises analogous to the dymon 550 Turbo uncertainty in Path A are possible on first run.

Obfuscation offset documentation (medium). The personal offset that encodes pricing must be documented somewhere private and durable. If forgotten, printed labels cannot be decoded. The pipeline code must not contain the offset value.

QR image scaling calibration (medium). Segno generates QR images at a scale specified in pixels per module. A 57mm × 32mm label at 300 DPI is 674 × 378 pixels. This calibration is a one-time step but must be correct before batch printing begins; a mismatched scale produces a cropped or padded label.

Column filtering in the export script (low). The Coda CSV export includes all columns, including raw unobfuscated pricing. The encoding script must explicitly select only the public identity columns for plain text and apply the offset transformation to pricing columns before building the payload.

Path C inkjet durability (low). Inkjet-printed Avery labels are susceptible to smearing under moisture and abrasion. For labels stored in binders or boxes with minimal handling, this is background risk. Laser printing on the same Avery 6572 stock mitigates it; durable inkjet label stock is a further option.

Label durability (low). Standard DYMO thermal labels fade over five to ten years under ambient light and heat. For a long-term collection stored in binders or boxes, this is a background risk rather than an immediate blocker. Durable polypropylene stock (approximately $80 per 1,000) mitigates it at roughly double the label cost.

What It Does Not Solve

The pipeline does not manage label placement consistency on top loaders — that requires a physical jig or careful manual alignment. The Coda catalog URL is intentionally excluded from the QR payload: the catalog is behind a login wall and its URL adds length without offline value.

Radar Assessment

Promoted to Trial on the strength of Path C: the pipeline has been assembled and validated end-to-end on dbmacm3 running macOS. The implementation (pylabel) ships as a standalone Python project with a Makefile, test suite, two calibrated templates (Avery 6572 letter and Avery L7161 A4), and a confirmed default CUPS printer. make run exports the card catalog via Coda CLI, generates QR codes with Segno, renders the label sheet via WeasyPrint, and submits to the HP LaserJet M211dw in one command.

Path A (dymon) remains unsuitable as the primary macOS print path due to five compounding setup issues. Path B (DYMO Connect Web Service via dymopy) is the lower-friction DYMO alternative but requires the desktop service running and an additional DYMO XML formatting step. Path D (Brother QL) is the simplest thermal alternative -- direct PNG input, no DRM, cheaper stock -- but requires dedicated hardware and has not been validated by the author. The batch minimum of 15 cards per sheet run is the primary operational constraint on Path C; ad-hoc single-card reprints are uneconomical.

References

Clone this wiki locally