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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-22 16:39:58 +10:00
parent 2727545ac7
commit d254d16278
2 changed files with 22 additions and 3 deletions

View File

@ -152,8 +152,17 @@ def do_encode(body):
def do_print_paper(body): def do_print_paper(body):
"""Print the paper label on the Dymo LabelWriter 450 (raw USB, no CUPS driver).""" """Print the paper label on the Dymo LabelWriter 450 (raw USB, no CUPS driver)."""
prof = label.profile(body.get('size'), dpmm=label.DPMM_DYMO, prof = label.profile(body.get('size'), dpmm=label.DPMM_DYMO,
art_mm=body.get('art_mm'), margin_mm=body.get('margin')) web_mm=body.get('w'), pitch_mm=body.get('h'),
img = label.render(body, prof, rotate=False) # 57 mm head: no rotation needed 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: with LOCK, dymo_mod.Dymo() as d:
before = d.status() before = d.status()
if before is not None and not before & 0x01: if before is not None and not before & 0x01:

View File

@ -131,13 +131,23 @@ class Dymo(object):
def form_feed(self): def form_feed(self):
self.write(b'\x1bE') 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). """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. Returns the number of raster lines sent.
""" """
if img.mode != '1': if img.mode != '1':
img = img.convert('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: if img.width > HEAD_BYTES * 8:
raise DymoError('image is %d dots wide; head is %d' raise DymoError('image is %d dots wide; head is %d'
% (img.width, HEAD_BYTES * 8)) % (img.width, HEAD_BYTES * 8))