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,106 @@
|
||||
"""
|
||||
Native Windows file/folder picker, used by the Kivy GUI.
|
||||
|
||||
Kivy's built-in FileChooserListView has no address bar, no Quick Access and
|
||||
awkward keyboard navigation, which makes finding a file on a real PC painful.
|
||||
This shells out to the standard Windows dialog instead.
|
||||
|
||||
It runs in a **separate process** on purpose: Tk wants to own the main event
|
||||
loop, and Kivy's SDL2 loop is already running in the parent. Isolating Tk in a
|
||||
child process avoids that conflict entirely, and the result comes back on stdout.
|
||||
|
||||
Usage:
|
||||
python native_file_dialog.py [--dir START_DIR] [--save] [--folder] [--title T]
|
||||
|
||||
Output (one line on stdout):
|
||||
SELECTED:<abs path> on success
|
||||
CANCELLED if the user dismissed the dialog
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
IMAGE_TYPES = [
|
||||
("Image files", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tif;*.tiff;*.webp"),
|
||||
("PNG", "*.png"),
|
||||
("JPEG", "*.jpg;*.jpeg"),
|
||||
("Bitmap", "*.bmp"),
|
||||
("All files", "*.*"),
|
||||
]
|
||||
|
||||
|
||||
def _default_dir(start: str | None) -> str:
|
||||
"""Pick a sensible starting folder."""
|
||||
if start and os.path.isdir(start):
|
||||
return start
|
||||
if start and os.path.isfile(start):
|
||||
return os.path.dirname(start)
|
||||
|
||||
# Prefer the user's Pictures folder, then their home directory.
|
||||
home = os.path.expanduser("~")
|
||||
pictures = os.path.join(home, "Pictures")
|
||||
if os.path.isdir(pictures):
|
||||
return pictures
|
||||
return home if os.path.isdir(home) else os.getcwd()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Native file picker")
|
||||
parser.add_argument("--dir", default=None, help="initial directory or file")
|
||||
parser.add_argument("--title", default="Select a file")
|
||||
parser.add_argument(
|
||||
"--folder", action="store_true", help="pick a folder instead of a file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--save", action="store_true", help="use the save-as dialog"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
import tkinter
|
||||
from tkinter import filedialog
|
||||
except ImportError:
|
||||
print("ERROR:tkinter is not available in this Python installation", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
root = tkinter.Tk()
|
||||
root.withdraw()
|
||||
# Keep the dialog on top of the Kivy window rather than behind it.
|
||||
root.attributes("-topmost", True)
|
||||
root.update()
|
||||
|
||||
start = _default_dir(args.dir)
|
||||
try:
|
||||
if args.folder:
|
||||
result = filedialog.askdirectory(
|
||||
title=args.title, initialdir=start, mustexist=True
|
||||
)
|
||||
elif args.save:
|
||||
result = filedialog.asksaveasfilename(
|
||||
title=args.title,
|
||||
initialdir=start,
|
||||
defaultextension=".png",
|
||||
filetypes=IMAGE_TYPES,
|
||||
)
|
||||
else:
|
||||
result = filedialog.askopenfilename(
|
||||
title=args.title, initialdir=start, filetypes=IMAGE_TYPES
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
root.destroy()
|
||||
except tkinter.TclError:
|
||||
pass
|
||||
|
||||
if result:
|
||||
print(f"SELECTED:{os.path.normpath(result)}")
|
||||
else:
|
||||
print("CANCELLED")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user