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):
"""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:

View File

@ -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))