#!/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/.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}: "<>setpagedevice"\n' for n, w, h, lab in NEW]) ppd = insert_after_last(ppd, 'PageRegion', [ f'*PageRegion {n}/{lab}: "<>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}')