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.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
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())
|
||||
Reference in New Issue
Block a user