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.
90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""
|
|
Headless test of the two-column GUI.
|
|
|
|
Instantiates the real app class and drives its handlers without opening a window,
|
|
so layout wiring and settings logic are verified automatically rather than by
|
|
looking at the screen.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# Keep Kivy from parsing our argv; the real window backend is used but the app
|
|
# event loop is never started, so nothing is displayed.
|
|
os.environ.setdefault("KIVY_NO_ARGS", "1")
|
|
|
|
|
|
def main() -> int:
|
|
import label_printer_ptouch_gui as gui
|
|
|
|
print("1. Window is a 600 x 300 landscape panel")
|
|
print(f" WINDOW_SIZE : {gui.WINDOW_SIZE}")
|
|
landscape = gui.WINDOW_SIZE[0] > gui.WINDOW_SIZE[1]
|
|
print(f" landscape : {landscape} {'PASS' if landscape else 'FAIL'}")
|
|
|
|
print("\n2. App.config is not shadowed (the bug that crashed the settings panel)")
|
|
app = gui.PtouchPrinterApp()
|
|
has_collision = "config" in vars(app) and not hasattr(vars(app)["config"], "get")
|
|
print(f" app.config : {type(app.config).__name__}")
|
|
print(f" label_cfg : {type(app.label_cfg).__name__}")
|
|
cfg_ok = type(app.label_cfg).__name__ == "LabelConfig"
|
|
print(f" config valid: {cfg_ok} {'PASS' if cfg_ok else 'FAIL'}")
|
|
|
|
print("\n3. Two-column root layout")
|
|
root = app.build()
|
|
print(f" orientation : {root.orientation}")
|
|
print(f" columns : {len(root.children)}")
|
|
cols_ok = root.orientation == "horizontal" and len(root.children) == 2
|
|
print(f" {'PASS' if cols_ok else 'FAIL'}")
|
|
|
|
# Children are in reverse order of addition: preview column first, then controls.
|
|
preview_col, controls = root.children
|
|
print(f" controls col: {controls.orientation}, {len(controls.children)} items")
|
|
print(f" preview col : {preview_col.orientation}, {len(preview_col.children)} items")
|
|
|
|
print("\n4. Required widgets exist")
|
|
for name in (
|
|
"inventory_input",
|
|
"printer_spinner",
|
|
"print_button",
|
|
"preview",
|
|
"status_label",
|
|
):
|
|
present = hasattr(app, name)
|
|
print(f" {name:<18}: {'yes' if present else 'NO'}")
|
|
if not present:
|
|
return 1
|
|
|
|
print("\n5. Settings handlers are reachable (would have raised before)")
|
|
# Accessing these properties is what failed previously.
|
|
_ = app.label_cfg.logo_path
|
|
_ = app.label_cfg.static_lines
|
|
_ = app.label_cfg.qr_prefix
|
|
_ = app.label_cfg.qr_suffix
|
|
_ = app.label_cfg.qr_error_correction
|
|
print(" loaded without error: PASS")
|
|
|
|
print("\n6. Preview render works headless")
|
|
app.update_preview()
|
|
print(f" status text : {app.status_label.text!r}")
|
|
print(f" texture set : {app.preview._texture is not None}")
|
|
|
|
print("\n7. QR payload is built from the inventory field")
|
|
app.inventory_input.text = "INV-42"
|
|
app.update_preview()
|
|
print(f" status text : {app.status_label.text!r}")
|
|
payload_ok = "INV-42" in app.status_label.text
|
|
print(f" reflects field: {payload_ok} {'PASS' if payload_ok else 'FAIL'}")
|
|
|
|
print("\n" + "=" * 62)
|
|
passed = landscape and cfg_ok and cols_ok and payload_ok
|
|
print("RESULT:", "PASS" if passed else "FAIL")
|
|
return 0 if passed else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|