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.
194 lines
6.2 KiB
Python
194 lines
6.2 KiB
Python
"""
|
|
Read or change the PT-E560BT driver's print resolution.
|
|
|
|
Why this exists: the Brother driver offers two resolution modes
|
|
|
|
180 x 180 dpi balanced, matches the 128-dot head in both axes
|
|
180 x 360 dpi anisotropic - 2x the resolution along the tape feed
|
|
|
|
In the anisotropic mode the driver reports the media as 24 x 65 mm but rasters
|
|
along the tape at 360 dpi, so a page authored for the media dimensions can be
|
|
scaled by the wrong factor and printed short. This tool lets the mode be switched
|
|
from the command line so the effect can be tested without clicking through the
|
|
printer properties dialog.
|
|
|
|
Changing the resolution modifies a system printer default, so it is reversible:
|
|
use --restore, or simply re-run with the other value. Administrator rights are
|
|
required; without them the script reports what to change by hand.
|
|
|
|
Usage:
|
|
python set_printer_resolution.py # show current setting
|
|
python set_printer_resolution.py --mode 180 # symmetric 180x180
|
|
python set_printer_resolution.py --mode 360 # anisotropic 180x360
|
|
python set_printer_resolution.py --restore # put back the old value
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
PRINTER = "Brother PT-E560BT"
|
|
STATE_FILE = ".printer_resolution_backup.json"
|
|
|
|
# DEVMODE field flags
|
|
DM_PRINTQUALITY = 0x00000400
|
|
DM_YRESOLUTION = 0x00040000
|
|
DM_MODIFY = 0x00000008
|
|
DM_IN_BUFFER = 0x00000008
|
|
DM_OUT_BUFFER = 0x00000002
|
|
|
|
|
|
def _devmode_field_bits():
|
|
return DM_PRINTQUALITY | DM_YRESOLUTION
|
|
|
|
|
|
def current_settings():
|
|
"""Return the printer's current resolution-related DEVMODE values."""
|
|
import win32print
|
|
|
|
handle = win32print.OpenPrinter(PRINTER)
|
|
try:
|
|
info = win32print.GetPrinter(handle, 2)
|
|
dm = info["pDevMode"]
|
|
return {
|
|
"port": info.get("pPortName"),
|
|
"paper_size": dm.PaperSize,
|
|
"paper_width": dm.PaperWidth,
|
|
"paper_length": dm.PaperLength,
|
|
"orientation": dm.Orientation,
|
|
"print_quality": dm.PrintQuality,
|
|
"y_resolution": dm.YResolution,
|
|
"fields": dm.Fields,
|
|
}
|
|
finally:
|
|
win32print.ClosePrinter(handle)
|
|
|
|
|
|
def measure_dc():
|
|
"""Read the resolution and paper dimensions the driver reports via a DC."""
|
|
try:
|
|
import win32ui
|
|
except ImportError:
|
|
return None
|
|
|
|
try:
|
|
dc = win32ui.CreateDC()
|
|
dc.CreatePrinterDC(PRINTER)
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f" Could not create DC: {exc}")
|
|
return None
|
|
|
|
try:
|
|
return {
|
|
"logpixelsx": dc.GetDeviceCaps(88),
|
|
"logpixelsy": dc.GetDeviceCaps(90),
|
|
"physicalwidth": dc.GetDeviceCaps(110),
|
|
"physicalheight": dc.GetDeviceCaps(111),
|
|
"horzres": dc.GetDeviceCaps(8),
|
|
"vertres": dc.GetDeviceCaps(10),
|
|
}
|
|
finally:
|
|
try:
|
|
dc.DeleteDC()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
|
|
def show(label=""):
|
|
settings = current_settings()
|
|
caps = measure_dc()
|
|
|
|
print(f"\n{label}")
|
|
print(f" DEVMODE PrintQuality : {settings['print_quality']}")
|
|
print(f" DEVMODE YResolution : {settings['y_resolution']}")
|
|
print(f" DEVMODE PaperSize : {settings['paper_size']} "
|
|
f"({settings['paper_width']/10:.1f} x {settings['paper_length']/10:.1f} mm)")
|
|
if caps:
|
|
x, y = caps["logpixelsx"], caps["logpixelsy"]
|
|
if x and y:
|
|
print(f" DC resolution : {x} x {y} dpi", end="")
|
|
if max(x, y) / min(x, y) >= 1.9:
|
|
print(" <-- ANISOTROPIC (2x mismatch between axes)")
|
|
else:
|
|
print(" (symmetric)")
|
|
print(f" Physical page : {caps['physicalwidth']} x "
|
|
f"{caps['physicalheight']} dots = "
|
|
f"{caps['physicalwidth']/max(x,1)*25.4:.2f} x "
|
|
f"{caps['physicalheight']/max(y,1)*25.4:.2f} mm")
|
|
print(f" Printable area : {caps['horzres']} x {caps['vertres']} dots")
|
|
return settings
|
|
|
|
|
|
def apply_mode(mode: str) -> bool:
|
|
"""Set the driver resolution mode. mode is '180' or '360'."""
|
|
import win32print
|
|
|
|
y_res = 360 if mode == "360" else 0
|
|
try:
|
|
handle = win32print.OpenPrinter(
|
|
PRINTER, {"DesiredAccess": win32print.PRINTER_ALL_ACCESS}
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f"Cannot open printer for writing: {exc}")
|
|
print("Re-run this from an elevated (Administrator) terminal.")
|
|
return False
|
|
|
|
try:
|
|
info = win32print.GetPrinter(handle, 2)
|
|
dm = info["pDevMode"]
|
|
dm.PrintQuality = 180
|
|
dm.YResolution = y_res
|
|
dm.Fields |= _devmode_field_bits()
|
|
|
|
info["pDevMode"] = dm
|
|
win32print.SetPrinter(handle, 2, info, 0)
|
|
print(f"Applied resolution mode {mode} (YResolution={y_res}).")
|
|
return True
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f"Failed to set resolution: {exc}")
|
|
print("This normally needs Administrator rights.")
|
|
return False
|
|
finally:
|
|
win32print.ClosePrinter(handle)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--mode", choices=["180", "360"], help="resolution mode to set")
|
|
parser.add_argument("--restore", action="store_true", help="restore the saved value")
|
|
args = parser.parse_args()
|
|
|
|
before = show("BEFORE")
|
|
|
|
if args.restore:
|
|
if not os.path.exists(STATE_FILE):
|
|
print(f"\nNo backup found at {STATE_FILE}; nothing to restore.")
|
|
return 1
|
|
with open(STATE_FILE) as handle:
|
|
saved = json.load(handle)
|
|
mode = "360" if saved.get("y_resolution") else "180"
|
|
print(f"\nRestoring YResolution={saved.get('y_resolution')} (mode {mode})")
|
|
if apply_mode(mode):
|
|
show("AFTER")
|
|
return 0
|
|
|
|
if not args.mode:
|
|
print("\n(no --mode given; nothing changed)")
|
|
return 0
|
|
|
|
# Save the previous value so the change can be undone.
|
|
with open(STATE_FILE, "w") as handle:
|
|
json.dump(before, handle, indent=2)
|
|
print(f"\nSaved previous setting to {STATE_FILE}")
|
|
|
|
if apply_mode(args.mode):
|
|
show("AFTER")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|