93afa3c417
New three-column label format for Brother P-touch EDGE PT-E560BT asset tags:
logo | three static lines + variable field | QR code.
Layout and rendering
- ptouch_label.py: geometry engine with PDF and preview renderers. Both
consume the same geometry, so the preview matches the print exactly.
- Code symbols are snapped to whole printer dots per module. Fractional
modules are the usual reason a small QR code will not scan.
- QR is maximised to the printable height by default (16.4 mm, 4 dots per
module) instead of a fixed 16 mm box (12.3 mm, 3 dots per module).
- Code payload is composed as prefix + inventory + suffix. A legacy
{inventory} template is migrated on first save.
Hardware handling
- print_area.py reads the driver's real printable area via GetDeviceCaps.
The PT-E560BT head is 128 dots at 180 dpi, so the maximum print height is
18.06 mm even on 24 mm tape; the driver reports media as 65x24 mm but only
about 61x18 mm is reachable. Content is fitted to the reachable area,
otherwise the driver silently crops it.
- set_printer_resolution.py reads/switches the driver's resolution mode. The
driver offers 180x180 (symmetric) and 180x360 (anisotropic, 2x across the
tape); the anisotropic mode scaled output incorrectly.
GUI
- label_printer_ptouch_gui.py: 600x300 two-column panel - controls on the
left, live preview on the right. Advanced settings for logo, static lines,
code prefix/suffix and error correction.
- native_file_dialog.py: the standard Windows file picker, run in a
subprocess because Tk and Kivy both want the main event loop.
Printing
- Print failures are reported honestly. print_to_printer previously returned
True unconditionally, so a failed print claimed success and was logged as
SUCCESS.
- SumatraPDF exit codes are now checked (4 = no printer, 5 = driver error).
- The PDF is sent with noscale so driver-side scaling cannot distort the QR
module pitch.
Tests
- validate_ptouch_layout.py decodes the QR back out of the rendered preview
module by module, proving the preview and PDF are the same layout.
- test_ptouch_pipeline.py writes to a temp config so it cannot overwrite
operator settings.
- test_gui_layout.py exercises the real app class headlessly.
- test_print_reporting.py covers the failure paths.
Diagnostics: check_print_area.py, check_print_geometry.py, check_devmode.py,
print_calibration.py, print_measure_scale.py, capture_print_job.py.
143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
"""
|
|
Measure the PT-E560BT driver's page and printable area.
|
|
|
|
Context: the PDF we send is verified to be exactly 65 x 24 mm, but the label
|
|
prints at roughly 25 x 17 mm. That points at the driver rather than our PDF, so
|
|
this reads the driver's own coordinate system instead of guessing.
|
|
|
|
GetDeviceCaps() is authoritative:
|
|
HORZSIZE / VERTSIZE -> physical page size, in millimetres
|
|
HORZRES / VERTRES -> printable area, in device dots
|
|
PHYSICALWIDTH / HEIGHT -> full page, in device dots
|
|
PHYSICALOFFSETX / Y -> where the printable area starts (unprintable margin)
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
import win32ui
|
|
|
|
PRINTER = "Brother PT-E560BT"
|
|
CONTROL = "Microsoft Print to PDF" # control: a normal printer, to sanity-check the method
|
|
|
|
HORZSIZE, VERTSIZE = 4, 6
|
|
HORZRES, VERTRES = 8, 10
|
|
LOGPIXELSX, LOGPIXELSY = 88, 90
|
|
PHYSICALWIDTH, PHYSICALHEIGHT = 110, 111
|
|
PHYSICALOFFSETX, PHYSICALOFFSETY = 112, 113
|
|
|
|
CAP_INDICES = {
|
|
"HORZSIZE": HORZSIZE,
|
|
"VERTSIZE": VERTSIZE,
|
|
"HORZRES": HORZRES,
|
|
"VERTRES": VERTRES,
|
|
"LOGPIXELSX": LOGPIXELSX,
|
|
"LOGPIXELSY": LOGPIXELSY,
|
|
"PHYSICALWIDTH": PHYSICALWIDTH,
|
|
"PHYSICALHEIGHT": PHYSICALHEIGHT,
|
|
"PHYSICALOFFSETX": PHYSICALOFFSETX,
|
|
"PHYSICALOFFSETY": PHYSICALOFFSETY,
|
|
}
|
|
|
|
# What we intend to print (our PDF page, and the label content inside it).
|
|
PAGE_MM = (65.0, 24.0)
|
|
CONTENT_MM = (63.0, 20.0)
|
|
|
|
|
|
def dots_to_mm(dots, dpi):
|
|
return dots / dpi * 25.4 if dpi else 0.0
|
|
|
|
|
|
def measure(printer, devmode=None):
|
|
"""Return the driver's caps dict, or None if the DC could not be created."""
|
|
try:
|
|
hdc = win32ui.CreateDC()
|
|
hdc.CreatePrinterDC(printer)
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f" CreatePrinterDC failed: {exc}")
|
|
return None
|
|
|
|
try:
|
|
caps = {name: hdc.GetDeviceCaps(idx) for name, idx in CAP_INDICES.items()}
|
|
finally:
|
|
try:
|
|
hdc.DeleteDC()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
dpix = caps["LOGPIXELSX"] or 1
|
|
dpiy = caps["LOGPIXELSY"] or 1
|
|
|
|
print(f"\n{printer}")
|
|
print(f" resolution : {dpix} x {dpiy} dpi")
|
|
print(f" physical page : {caps['HORZSIZE']} x {caps['VERTSIZE']} mm")
|
|
print(
|
|
f" printable area : {caps['HORZRES']} x {caps['VERTRES']} dots = "
|
|
f"{dots_to_mm(caps['HORZRES'], dpix):.2f} x {dots_to_mm(caps['VERTRES'], dpiy):.2f} mm"
|
|
)
|
|
print(
|
|
f" physical (dots) : {caps['PHYSICALWIDTH']} x {caps['PHYSICALHEIGHT']} = "
|
|
f"{dots_to_mm(caps['PHYSICALWIDTH'], dpix):.2f} x "
|
|
f"{dots_to_mm(caps['PHYSICALHEIGHT'], dpiy):.2f} mm"
|
|
)
|
|
print(
|
|
f" printable origin : {caps['PHYSICALOFFSETX']} x {caps['PHYSICALOFFSETY']} dots = "
|
|
f"{dots_to_mm(caps['PHYSICALOFFSETX'], dpix):.2f} x "
|
|
f"{dots_to_mm(caps['PHYSICALOFFSETY'], dpiy):.2f} mm"
|
|
)
|
|
return caps
|
|
|
|
|
|
def verdict(caps):
|
|
"""Explain whether the driver's page matches what we send."""
|
|
dpix = caps["LOGPIXELSX"] or 1
|
|
dpiy = caps["LOGPIXELSY"] or 1
|
|
w = dots_to_mm(caps["PHYSICALWIDTH"], dpix)
|
|
h = dots_to_mm(caps["PHYSICALHEIGHT"], dpiy)
|
|
|
|
print(f"\n VERDICT for {PAGE_MM[0]:.0f} x {PAGE_MM[1]:.0f} mm page:")
|
|
if abs(w - PAGE_MM[0]) < 1.0 and abs(h - PAGE_MM[1]) < 1.0:
|
|
print(" driver page matches our PDF -> driver has nothing to rescale")
|
|
else:
|
|
print(f" driver page is {w:.2f} x {h:.2f} mm, NOT {PAGE_MM[0]} x {PAGE_MM[1]} mm")
|
|
print(f" implied scale: x{PAGE_MM[0] / w:.3f} by y{PAGE_MM[1] / h:.3f}")
|
|
|
|
# Distinguish 'fit' (uniform) from 'stretch' (non-uniform).
|
|
sx, sy = PAGE_MM[0] / w, PAGE_MM[1] / h
|
|
if abs(sx - sy) < 0.02:
|
|
print(" uniform scale -> driver is using 'fit'")
|
|
else:
|
|
print(" NON-UNIFORM scale -> driver is using 'stretch'; this distorts")
|
|
|
|
# Does the reported printable area match the 25 x 17 mm that actually printed?
|
|
pw = dots_to_mm(caps["HORZRES"], dpix)
|
|
ph = dots_to_mm(caps["VERTRES"], dpiy)
|
|
print(f" printable area {pw:.2f} x {ph:.2f} mm")
|
|
if abs(pw - 25.0) < 3.0 or abs(ph - 17.0) < 3.0:
|
|
print(" ^ this is close to the 25 x 17 mm actually observed on tape")
|
|
|
|
|
|
def main() -> int:
|
|
print("=" * 72)
|
|
print("DRIVER PRINT AREA")
|
|
print("=" * 72)
|
|
print(f"Our PDF page : {PAGE_MM[0]:.1f} x {PAGE_MM[1]:.1f} mm")
|
|
print(f"Label content : {CONTENT_MM[0]:.1f} x {CONTENT_MM[1]:.1f} mm")
|
|
|
|
caps = measure(PRINTER)
|
|
if caps is None:
|
|
return 1
|
|
verdict(caps)
|
|
|
|
print("\n" + "-" * 72)
|
|
print("CONTROL (method sanity check)")
|
|
print("-" * 72)
|
|
measure(CONTROL)
|
|
|
|
print("\n" + "=" * 72)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|