Files
label_printer/check_print_area.py
ske087 93afa3c417 Add P-touch Labels for Asset: 63x20 mm label format for PT-E560BT
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.
2026-09-25 17:30:11 +03:00

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