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>
101 lines
4.0 KiB
Python
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}')
|