From d254d16278b6d01b45855bd66e80e8053c8f57a2 Mon Sep 17 00:00:00 2001 From: type-two Date: Wed, 22 Jul 2026 16:39:58 +1000 Subject: [PATCH] Fix Dymo label geometry: rotation and 300x600 dpi halving Two bugs found on the bench, both from wrong assumptions about the LW450: 1. Orientation. I assumed the 57mm head meant the label feeds landscape. It doesn't -- the Dymo takes the SAME 24mm narrow-edge-first stock as the Urovo, so the landscape design needs the identical 90-degree rotation. Rendering 656 dots onto a ~288-dot label also silently clipped the price and QR off the right-hand edge. Now rotates by default, overridable per request via "rotate", and the daemon rejects an image wider than the head instead of clipping it. 2. Half-length labels. print_image() sent ESC i (300x600 Barcode/Graphics mode), where the head stays 300 dpi across but each raster line becomes 1/600" in the travel direction -- so a 650-line label printed at 27.5mm instead of 55mm, with the across-head dimension correct. Default is now ESC h (300x300), matching our square 300 dpi bitmap. graphics_mode=True remains available and now stretches the image 2x vertically so physical size stays correct. Co-Authored-By: Claude Opus 4.8 --- mac/daemon.py | 13 +++++++++++-- mac/dymo.py | 12 +++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/mac/daemon.py b/mac/daemon.py index 1dde3cc..639befd 100644 --- a/mac/daemon.py +++ b/mac/daemon.py @@ -152,8 +152,17 @@ def do_encode(body): def do_print_paper(body): """Print the paper label on the Dymo LabelWriter 450 (raw USB, no CUPS driver).""" prof = label.profile(body.get('size'), dpmm=label.DPMM_DYMO, - art_mm=body.get('art_mm'), margin_mm=body.get('margin')) - img = label.render(body, prof, rotate=False) # 57 mm head: no rotation needed + web_mm=body.get('w'), pitch_mm=body.get('h'), + margin_mm=body.get('margin')) + # The Dymo takes the SAME 24 mm stock as the Urovo, fed narrow-edge-first, so the + # landscape design has to be rotated 90 degrees here too. (The 57 mm head is wider + # than the label; that does NOT mean the label feeds landscape.) Overridable per + # request via "rotate" in case a wider Dymo roll is loaded. + rotate = body.get('rotate', True) + img = label.render(body, prof, rotate=rotate) + if img.width > dymo_mod.HEAD_BYTES * 8: + return {'ok': False, 'error': 'label is %d dots wide; Dymo head is %d' + % (img.width, dymo_mod.HEAD_BYTES * 8)} with LOCK, dymo_mod.Dymo() as d: before = d.status() if before is not None and not before & 0x01: diff --git a/mac/dymo.py b/mac/dymo.py index b6bffca..bad8bd4 100644 --- a/mac/dymo.py +++ b/mac/dymo.py @@ -131,13 +131,23 @@ class Dymo(object): def form_feed(self): self.write(b'\x1bE') - def print_image(self, img, density='normal', graphics_mode=True, feed=True): + def print_image(self, img, density='normal', graphics_mode=False, feed=True): """Send a 1-bit PIL image, already oriented for the head (width = across head). + The bitmap is assumed to be 300 dpi in BOTH axes, so the default is `ESC h` + (300x300 Text mode). `ESC i` selects 300x600 Barcode/Graphics mode, where the + head is still 300 dpi across but each raster line becomes 1/600" in the travel + direction -- printing the label at HALF length unless the image is doubled up. + So when graphics_mode is on we stretch the image 2x vertically to keep the + physical size right. + Returns the number of raster lines sent. """ if img.mode != '1': img = img.convert('1') + if graphics_mode: + from PIL import Image as _I + img = img.resize((img.width, img.height * 2), _I.NEAREST) if img.width > HEAD_BYTES * 8: raise DymoError('image is %d dots wide; head is %d' % (img.width, HEAD_BYTES * 8))