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,142 @@
|
||||
"""
|
||||
Print a measuring label to determine the printer's actual scale factors.
|
||||
|
||||
The problem: we render a correct 61 x 18 mm label, but it prints around 25 x 17 mm.
|
||||
Two candidate causes predict different numbers, and both look similar at that size:
|
||||
|
||||
(a) Resolution mismatch - the driver reports LOGPIXELSX=360 but the device is
|
||||
180 dpi, so content is compressed 2x along the tape. Predicts marks 10 mm
|
||||
apart appearing ~5 mm apart in X only.
|
||||
(b) Driver shrink-to-fit across the 24 mm media width. Predicts an equal
|
||||
shrink in X and Y: scale 24/65.05 = 0.37 -> marks 10 mm apart appear 3.7 mm.
|
||||
|
||||
This label makes the answer unambiguous: it draws ruler ticks at known spacings
|
||||
in both axes, so reading one number off the tape identifies the cause.
|
||||
|
||||
Reading the print:
|
||||
* X ticks (along the tape) are laid out every 10 mm from 0 to 60.
|
||||
* Y ticks (across the tape) are laid out every 2 mm from 0 to 18.
|
||||
* A 10 x 10 mm reference square sits at a known position.
|
||||
|
||||
Measure the distance between tick "0" and tick "60" (intended 60 mm):
|
||||
~60 mm -> geometry correct; the problem lies elsewhere
|
||||
~30 mm -> case (a), 2x compression along the tape
|
||||
~22 mm -> case (b), shrink-to-fit on width
|
||||
other -> report the measured value
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
from print_area import query_print_area
|
||||
from ptouch_label import LabelConfig
|
||||
|
||||
PRINTER = "Brother PT-E560BT"
|
||||
|
||||
|
||||
def build_measure_pdf(cfg: LabelConfig, path: str) -> str:
|
||||
"""Draw the measuring label at the configured content size."""
|
||||
page_w, page_h = cfg.page_size_mm
|
||||
ox, oy = cfg.origin_mm
|
||||
content_w, content_h = cfg.page_width_mm, cfg.page_height_mm
|
||||
|
||||
c = canvas.Canvas(path, pagesize=(page_w * mm, page_h * mm))
|
||||
c.setTitle("PT-E560BT scale measurement")
|
||||
|
||||
def X(v: float) -> float:
|
||||
return (ox + v) * mm
|
||||
|
||||
def Y(v: float) -> float:
|
||||
return (oy + v) * mm
|
||||
|
||||
# Outer frame exactly at the content bounds.
|
||||
c.setStrokeColorRGB(0, 0, 0)
|
||||
c.setLineWidth(0.4)
|
||||
c.rect(X(0), Y(0), content_w * mm, content_h * mm)
|
||||
|
||||
# X ruler: a tick every 10 mm along the bottom, labelled.
|
||||
c.setFont("Helvetica", 5)
|
||||
for i in range(0, int(content_w) + 1, 10):
|
||||
tall = i % 20 == 0
|
||||
c.setLineWidth(0.6 if tall else 0.3)
|
||||
c.line(X(i), Y(0), X(i), Y(3.5 if tall else 1.8))
|
||||
if tall:
|
||||
c.drawString(X(i + 0.4), Y(3.8), str(i))
|
||||
|
||||
# Y ruler: a tick every 2 mm up the left edge, labelled every 6 mm.
|
||||
for j in range(0, int(content_h) + 1, 2):
|
||||
tall = j % 6 == 0
|
||||
c.setLineWidth(0.6 if tall else 0.3)
|
||||
c.line(X(0), Y(j), X(2.5 if tall else 1.4), Y(j))
|
||||
if tall:
|
||||
c.drawString(X(2.8), Y(j + 0.3), str(j))
|
||||
|
||||
# 10 x 10 mm reference square, for an independent scale check.
|
||||
c.setLineWidth(0.6)
|
||||
c.rect(X(30), Y(4), 10 * mm, 10 * mm)
|
||||
c.setFont("Helvetica-Bold", 4)
|
||||
c.drawString(X(30.6), Y(8.4), "10x10")
|
||||
c.drawString(X(30.6), Y(6.0), "mm")
|
||||
|
||||
# Explanation printed on the label itself.
|
||||
c.setFont("Helvetica-Bold", 5)
|
||||
c.drawString(X(44), Y(15.4), "MEASURE:")
|
||||
c.setFont("Helvetica", 4)
|
||||
c.drawString(X(44), Y(13.4), "tick 0 to 60")
|
||||
c.drawString(X(44), Y(11.6), "should be 60 mm")
|
||||
c.drawString(X(44), Y(9.8), "square = 10 mm")
|
||||
c.drawString(X(44), Y(8.0), "frame =")
|
||||
c.drawString(X(44), Y(6.4), f"{content_w:.0f} x {content_h:.0f} mm")
|
||||
c.drawString(X(44), Y(4.8), "tape = 24 mm")
|
||||
|
||||
c.showPage()
|
||||
c.save()
|
||||
return path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
cfg = load_config()
|
||||
|
||||
# Use the printer's real printable area for this measurement.
|
||||
area = query_print_area(PRINTER)
|
||||
if area:
|
||||
print(f"Printable area: {area.printable_width_mm:.2f} x "
|
||||
f"{area.printable_height_mm:.2f} mm")
|
||||
cfg.media_width_mm = round(area.media_width_mm, 2)
|
||||
cfg.media_height_mm = round(area.media_height_mm, 2)
|
||||
cfg.page_width_mm = round(area.printable_width_mm - 0.1, 2)
|
||||
cfg.page_height_mm = round(area.printable_height_mm - 0.1, 2)
|
||||
|
||||
os.makedirs("pdf_backup", exist_ok=True)
|
||||
out = os.path.join("pdf_backup", "measure_scale.pdf")
|
||||
build_measure_pdf(cfg, out)
|
||||
|
||||
print(f"Content bounds : {cfg.page_width_mm:.2f} x {cfg.page_height_mm:.2f} mm")
|
||||
print(f"PDF page : {cfg.page_size_mm[0]:.2f} x {cfg.page_size_mm[1]:.2f} mm")
|
||||
print(f"Origin : {cfg.origin_mm}")
|
||||
print(f"Written : {out}")
|
||||
|
||||
if "--no-print" in sys.argv:
|
||||
print("(--no-print: not sending to printer)")
|
||||
return 0
|
||||
|
||||
from print_label import print_to_printer
|
||||
|
||||
settings = "noscale,paperkind=261"
|
||||
print(f"\nPrinting with -print-settings '{settings}' ...")
|
||||
ok = print_to_printer(PRINTER, out, settings=settings)
|
||||
print(f"Result: {'SENT' if ok else 'FAILED'}")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user