docs(mac): patched CUPS PPD for the DYMO 450's 24x55mm price stock

macOS auto-adds a USB LabelWriter 450 with the GENERIC CUPS driver (dymo.ppd,
ModelName "DYMO Label Printer"), which has 10 fixed sizes and no CustomPageSize
block — so a custom paper size made in the page-setup dialog can never reach the
printer. CUPS falls back to w81h252 (Address, 28.6x88.9mm) and the 54x24mm design
prints rotated 90 degrees and shrunk. Cost a bench session to diagnose; the extension's
labels.js was never at fault.

Adds the stock in both feed orientations (pg55x25 / pg55x25L — John's price roll is
the landscape-feed one) plus patch_ppd.py to regenerate. Lives here because mac/dymo.py
drives the same printer.

Must be re-applied after any macOS update or after re-adding the printer, so the file
and the lpadmin invocation are both written down in the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-24 13:40:30 +10:00
parent e792e8315e
commit 3fa41ddbf7
3 changed files with 1192 additions and 0 deletions

View File

@ -45,6 +45,50 @@ is the **only** thing that needs to change if it must match a different (Mac/Cha
nothing else depends on the byte order. Confirm by reading an existing tag: put a
Chafon-written record at the antenna and `GET /read-tag`.
## DYMO LabelWriter 450 on macOS — the PPD you will need again
Not the daemon, but it lives here because `mac/dymo.py` drives the same printer.
macOS auto-adds a USB LabelWriter 450 with the **generic** CUPS driver
(`*PCFileName: "dymo.ppd"`, `*ModelName: "DYMO Label Printer"`). That PPD ships 10 fixed
label sizes and **no** `CustomPageSize`/`VariablePaperSize` block — so a custom paper size
made in the macOS page-setup dialog can never reach the printer. CUPS silently falls back
to `w81h252` (Address, 28.6 × 88.9 mm), and a 54×24 mm label design comes out **rotated 90°
and shrunk**, with a big blank area. Nothing is wrong with the extension's `labels.js`.
`mac/dymo-24x55.ppd` is the generic PPD patched to add the shop's 24×55 mm price stock, in
both feed orientations, so the right one can be picked in the print dialog:
| PageSize | pt | mm | roll |
|---|---|---|---|
| `pg55x25` | 72 × 155 | 25.4 × 54.7 | ~25 mm wide, label runs 55 mm along the feed |
| `pg55x25L` | 155 × 72 | 54.7 × 25.4 | ~55 mm wide, label runs 25 mm along the feed |
**John's price roll is the landscape-feed kind → use `pg55x25L`.**
```sh
sudo lpadmin -p <queue> -P mac/dymo-24x55.ppd # apply (queue on JING5: make_it_soso)
sudo lpadmin -p <queue> -m drv:///sample.drv/dymo.ppd # revert to stock
```
**Re-apply after any macOS update or after re-adding the printer** — both revert the queue to
the generic driver and the symptom comes straight back.
Regenerate from the **pristine** driver PPD — once applied, the queue's own PPD is already
patched and `patch_ppd.py` will refuse it:
```sh
ppdc -d /tmp/ppdout /usr/share/cups/drv/sample.drv
python3 mac/patch_ppd.py mac/dymo-24x55.ppd /tmp/ppdout/dymo.ppd # byte-identical, PASSes cupstestppd
```
Two traps it handles: margins are set to 2 pt sides / 4 pt ends instead of the driver's
stock 14.9 pt, which would eat 10.5 mm of a 55 mm label; and every one of the 31 languages
in `*cupsLanguages` needs a translation string per new choice or `cupstestppd` FAILs.
Dead end for next time: `LabelPrinter-*.pkg` in `~/Documents` is **not** the DYMO driver —
it's the generic bundle for the PM-246S/Wagtail printers.
## Notes / open items
- Default label geometry is `65×37 mm`, continuous (`GAP 0`). Adjust to your actual RFID
roll via the `w`/`h`/`gap` fields, or change the `DEF_*` constants.

1048
mac/dymo-24x55.ppd Normal file

File diff suppressed because it is too large Load Diff

100
mac/patch_ppd.py Normal file
View File

@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Add John's 24x55mm label stock to the generic CUPS DYMO PPD.
The macOS-supplied generic dymo.ppd (ModelName "DYMO Label Printer") ships 10 fixed
label sizes and NO CustomPageSize block -- so a "custom paper size" made in the macOS
page-setup dialog can never reach the printer, and CUPS silently falls back to
w81h252 (Address, 28.6 x 88.9 mm). That is why the label prints rotated and shrunk.
We add BOTH orientations so the correct one can be picked in the print dialog:
pg55x25 72 x 155 pt = 25.4 x 54.7 mm -- roll ~25 mm wide, label runs 55 mm along the feed
pg55x25L 155 x 72 pt = 54.7 x 25.4 mm -- roll ~55 mm wide, label runs 25 mm along the feed
Margins: 2 pt sides, 4 pt ends (~0.7 / 1.4 mm) -- tighter than the driver's stock
14.9 pt end margin, which would eat 10.5 mm of a 55 mm label.
Regenerate from the PRISTINE driver PPD, not from the live queue -- once you have applied
this, /etc/cups/ppd/<queue>.ppd IS the patched file and the script will refuse it:
ppdc -d /tmp/ppdout /usr/share/cups/drv/sample.drv
python3 patch_ppd.py dymo-24x55.ppd /tmp/ppdout/dymo.ppd
(Verified: that route reproduces the committed dymo-24x55.ppd byte-for-byte and PASSes
cupstestppd.) Source defaults to JING5's queue for a first run on a stock install.
"""
import re
import shutil
import sys
DST = sys.argv[1] if len(sys.argv) > 1 else 'dymo-24x55.ppd'
SRC = sys.argv[2] if len(sys.argv) > 2 else '/etc/cups/ppd/make_it_soso.ppd'
# name, width_pt, height_pt, human label
NEW = [
('pg55x25', 72, 155, '24x55mm Price Label (portrait feed)'),
('pg55x25L', 155, 72, '24x55mm Price Label (landscape feed)'),
]
shutil.copyfile(SRC, DST)
with open(DST, encoding='utf-8', errors='surrogateescape') as f:
ppd = f.read()
if 'pg55x25' in ppd:
sys.exit('already patched')
def area(w, h):
"""left bottom right top -- 2pt sides, 4pt ends"""
return f'2 4 {w - 2} {h - 4}'
# 1. *PageSize entries -- insert after the last existing *PageSize line
def insert_after_last(text, prefix, lines):
idx = [m.start() for m in re.finditer(rf'^\*{prefix} ', text, re.M)]
end = text.index('\n', idx[-1]) + 1
return text[:end] + ''.join(lines) + text[end:]
ppd = insert_after_last(ppd, 'PageSize', [
f'*PageSize {n}/{lab}: "<</PageSize[{w} {h}]/ImagingBBox null>>setpagedevice"\n'
for n, w, h, lab in NEW])
ppd = insert_after_last(ppd, 'PageRegion', [
f'*PageRegion {n}/{lab}: "<</PageSize[{w} {h}]/ImagingBBox null>>setpagedevice"\n'
for n, w, h, lab in NEW])
ppd = insert_after_last(ppd, 'ImageableArea', [
f'*ImageableArea {n}/{lab}: "{area(w, h)}"\n'
for n, w, h, lab in NEW])
ppd = insert_after_last(ppd, 'PaperDimension', [
f'*PaperDimension {n}/{lab}: "{w} {h}"\n'
for n, w, h, lab in NEW])
# 1b. per-language translation strings. The PPD declares 31 languages in *cupsLanguages
# and cupstestppd FAILs if a PageSize choice is missing any of them.
langs = re.search(r'^\*cupsLanguages:\s*"([^"]+)"', ppd, re.M).group(1).split()
for lg in langs:
if lg == 'en':
continue
for opt in ('PageSize', 'PageRegion'):
pat = rf'^\*{lg}\.{opt} \S+.*$'
hits = [m for m in re.finditer(pat, ppd, re.M)]
if not hits:
continue
end = hits[-1].end() + 1
add = ''.join(f'*{lg}.{opt} {n}/{lab}: ""\n' for n, w, h, lab in NEW)
ppd = ppd[:end] + add + ppd[end:]
# 2. make the portrait one the default so a plain "print" already lands right
ppd = ppd.replace('*DefaultPageSize: w81h252', '*DefaultPageSize: pg55x25')
ppd = ppd.replace('*DefaultPageRegion: w81h252', '*DefaultPageRegion: pg55x25')
ppd = ppd.replace('*DefaultImageableArea: w81h252', '*DefaultImageableArea: pg55x25')
ppd = ppd.replace('*DefaultPaperDimension: w81h252', '*DefaultPaperDimension: pg55x25')
with open(DST, 'w', encoding='utf-8', errors='surrogateescape') as f:
f.write(ppd)
print(f'wrote {DST}')
for n, w, h, lab in NEW:
print(f' {n:9s} {w:3d} x {h:3d} pt = {w/72*25.4:5.1f} x {h/72*25.4:5.1f} mm {lab}')