yourovo/mac/patch_ppd.py
type-two 3fa41ddbf7 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>
2026-07-24 13:40:30 +10:00

101 lines
4.0 KiB
Python

#!/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}')