Files
label_printer/print_calibration.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

135 lines
4.4 KiB
Python

"""
Print a calibration label to verify physical geometry on the PT-E560BT.
The label carries four things that can be measured with a ruler on the tape:
1. A hairline box exactly at the 63 x 20 mm content bounds
2. Corner ticks at 10 mm from each corner, for checking scale (a 10 mm tick
measures 10 mm only if the driver did not rescale)
3. A QR symbol with a known 12.28 mm size and 3 dots per module
4. Ruler marks every 10 mm along the bottom edge
Whether the driver is scaling, rotating or offsetting the page can be read
directly off the print, instead of being inferred.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
os.chdir(os.path.dirname(os.path.abspath(__file__)))
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from label_config import load_config, save_config
from ptouch_label import PRINTER_DPI, LabelConfig, build_code_bitmap
PRINTER = "Brother PT-E560BT"
# One module of the QR at 3 dots/module, i.e. what 0.4233 mm should measure.
MODULE_MM = 3.0 / PRINTER_DPI * 25.4
def build_calibration_pdf(cfg: LabelConfig, path: str) -> str:
page_w, page_h = cfg.page_size_mm
ox, oy = cfg.origin_mm
c = canvas.Canvas(path, pagesize=(page_w * mm, page_h * mm))
c.setTitle("PT-E560BT calibration")
def X(label_mm: float) -> float:
return (ox + label_mm) * mm
def Y(label_mm: float) -> float:
return (oy + label_mm) * mm
# 1. Content bounds, hairline
c.setStrokeColorRGB(0, 0, 0)
c.setLineWidth(0.25)
c.rect(X(0), Y(0), cfg.page_width_mm * mm, cfg.page_height_mm * mm)
# 2. Corner ticks: 10 mm legs just inside the box
c.setLineWidth(0.5)
for cx, cy, dx, dy in [
(0, 0, 1, 1),
(cfg.page_width_mm, 0, -1, 1),
(0, 20, 1, -1),
(cfg.page_width_mm, 20, -1, -1),
]:
c.line(X(cx), Y(cy), X(cx + 10 * dx), Y(cy))
c.line(X(cx), Y(cy), X(cx), Y(cy + 10 * dy))
# 3. Ruler marks every 10 mm along the bottom, labelled
c.setFont("Helvetica", 4)
for i in range(0, 61, 10):
tick_h = 2.0 if i % 20 == 0 else 1.0
c.setLineWidth(0.4)
c.line(X(i + 1), Y(1), X(i + 1), Y(1 + tick_h))
if i % 20 == 0:
c.drawString(X(i + 2), Y(1.2), str(i))
# 4. Column guides at the real boundaries: 20 mm and 45 mm
c.setLineWidth(0.25)
for x in (cfg.col_logo_mm, cfg.col_logo_mm + cfg.col_text_mm):
c.line(X(x), Y(1), X(x), Y(cfg.page_height_mm - 1))
# 5. Labels so the print can be interpreted without this file
c.setFont("Helvetica-Bold", 5)
c.drawString(X(20.8), Y(16.2), "CALIBRATION")
c.setFont("Helvetica", 4)
c.drawString(X(20.8), Y(13.0), "box = 63 x 20 mm")
c.drawString(X(20.8), Y(9.5), "ticks = 10 mm")
c.drawString(X(20.8), Y(6.5), "columns at")
c.drawString(X(20.8), Y(3.5), "20 / 45 mm")
c.drawString(X(20.8), Y(1.6), f"qr mod {MODULE_MM:.3f}mm")
# 6. The QR itself, with a known module size
bitmap = build_code_bitmap(cfg, "CAL", 112)
if bitmap is not None:
temp = os.path.join(os.path.dirname(path), "_cal_qr.png")
bitmap.image.save(temp, "PNG")
geo_box_w = bitmap.width_mm
c.drawImage(
temp,
X(cfg.page_width_mm - 1 - geo_box_w),
Y((cfg.page_height_mm - geo_box_w) / 2),
width=geo_box_w * mm,
height=geo_box_w * mm,
mask=None,
)
os.remove(temp)
print(f"QR: {bitmap.modules} modules, {bitmap.dots_per_module:.0f} dots/module, "
f"{bitmap.width_mm:.2f} mm across")
c.showPage()
c.save()
return path
def main() -> int:
cfg = load_config()
if not os.path.exists("label_config.json"):
save_config(cfg)
out = os.path.join("pdf_backup", "calibration_63x20.pdf")
os.makedirs("pdf_backup", exist_ok=True)
build_calibration_pdf(cfg, out)
print(f"Page : {cfg.page_size_mm[0]:.1f} x {cfg.page_size_mm[1]:.1f} mm")
print(f"Content : {cfg.page_width_mm:.1f} x {cfg.page_height_mm:.1f} mm "
f"at origin {cfg.origin_mm}")
print(f"Written : {out}")
if "--no-print" in sys.argv:
print("(--no-print given, not sending to printer)")
return 0
from print_label import print_to_printer
ok = print_to_printer(PRINTER, out, settings=cfg.sumatra_settings)
print(f"Printed : {'SUCCESS' if ok else 'FAILED'}")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())