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:
+178
-23
@@ -8,6 +8,16 @@ import datetime
|
||||
import platform
|
||||
import subprocess
|
||||
from print_label_pdf import PDFLabelGenerator
|
||||
from ptouch_label import (
|
||||
LabelConfig,
|
||||
PtouchLabelRenderer,
|
||||
code_density_warning,
|
||||
)
|
||||
|
||||
try:
|
||||
from print_area import apply_to_config as apply_print_area
|
||||
except ImportError: # pragma: no cover - print_area is optional
|
||||
apply_print_area = None
|
||||
|
||||
# Cross-platform printer support
|
||||
try:
|
||||
@@ -118,12 +128,25 @@ def create_label_image(text):
|
||||
left_margin = 15
|
||||
row_spacing = 3
|
||||
|
||||
# Fonts
|
||||
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||
try:
|
||||
label_font = ImageFont.truetype(font_path, 16)
|
||||
value_font = ImageFont.truetype(font_path, 14)
|
||||
except IOError:
|
||||
# Fonts - use whatever bold/regular TrueType pair exists on this platform.
|
||||
# The previous hardcoded Linux path always failed on Windows, silently
|
||||
# falling back to a tiny bitmap font.
|
||||
font_candidates = [
|
||||
(r"C:\Windows\Fonts\arialbd.ttf", r"C:\Windows\Fonts\arial.ttf"),
|
||||
(r"C:\Windows\Fonts\segoeuib.ttf", r"C:\Windows\Fonts\segoeui.ttf"),
|
||||
("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"),
|
||||
]
|
||||
label_font = value_font = None
|
||||
for bold_path, regular_path in font_candidates:
|
||||
if os.path.exists(bold_path) and os.path.exists(regular_path):
|
||||
try:
|
||||
label_font = ImageFont.truetype(bold_path, 16)
|
||||
value_font = ImageFont.truetype(regular_path, 14)
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if label_font is None:
|
||||
label_font = ImageFont.load_default()
|
||||
value_font = ImageFont.load_default()
|
||||
|
||||
@@ -225,7 +248,131 @@ def create_label_pdf(text):
|
||||
return generator.create_label_pdf(sap_nr, cantitate, lot_number, pdf_filename)
|
||||
|
||||
|
||||
def print_to_printer(printer_name, file_path):
|
||||
def create_ptouch_label_pdf(inventory, config=None, filename=None):
|
||||
"""
|
||||
Create a PT-E560BT formatted label PDF (63 x 20 mm, three columns).
|
||||
|
||||
Layout: logo | three static lines + variable inventory line | QR code.
|
||||
|
||||
Args:
|
||||
inventory (str): Value for the variable field and the QR payload
|
||||
config (LabelConfig): Layout settings (defaults used if None)
|
||||
filename (str): Output filename (auto-generated in pdf_backup if None)
|
||||
|
||||
Returns:
|
||||
str: Path to the generated PDF file
|
||||
"""
|
||||
cfg = config or LabelConfig()
|
||||
|
||||
pdf_backup_dir = 'pdf_backup'
|
||||
os.makedirs(pdf_backup_dir, exist_ok=True)
|
||||
|
||||
if not filename:
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||
safe_value = "".join(c for c in (inventory or "empty") if c.isalnum() or c in "-_")[:20]
|
||||
filename = os.path.join(pdf_backup_dir, f"ptouch_{safe_value}_{timestamp}.pdf")
|
||||
|
||||
renderer = PtouchLabelRenderer(cfg)
|
||||
return renderer.render_pdf(inventory, filename)
|
||||
|
||||
|
||||
def print_ptouch_label(inventory, printer, config=None):
|
||||
"""
|
||||
Generate and print a PT-E560BT label.
|
||||
|
||||
Before rendering, the layout is fitted to the printer's actual printable
|
||||
area when that can be queried. The PT-E560BT driver rejects anything drawn
|
||||
outside roughly 61.1 x 18.06 mm, so fitting here prevents the driver from
|
||||
silently cropping the label.
|
||||
|
||||
Args:
|
||||
inventory (str): Variable field / QR payload value
|
||||
printer (str): Target printer name, or "PDF" to only save the file
|
||||
config (LabelConfig): Layout settings
|
||||
|
||||
Returns:
|
||||
tuple: (success (bool), pdf_path (str), warning (str or None))
|
||||
"""
|
||||
cfg = config or LabelConfig()
|
||||
|
||||
# Fit to the real printable area (no-op for the "PDF" pseudo-printer).
|
||||
if apply_print_area is not None and printer and printer != "PDF":
|
||||
if apply_print_area(cfg, printer):
|
||||
print(
|
||||
f"Fitted to printable area: content {cfg.page_width_mm} x "
|
||||
f"{cfg.page_height_mm} mm on {cfg.media_width_mm} x "
|
||||
f"{cfg.media_height_mm} mm media"
|
||||
)
|
||||
|
||||
warning = code_density_warning(cfg, inventory)
|
||||
|
||||
try:
|
||||
pdf_path = create_ptouch_label_pdf(inventory, cfg)
|
||||
print(f"P-touch label PDF created: {pdf_path}")
|
||||
except Exception as e:
|
||||
print(f"Error creating P-touch label: {e}")
|
||||
return False, "", warning
|
||||
|
||||
# The page must be sent at its true size. Any driver-side scaling
|
||||
# would distort the QR module pitch and break scanning.
|
||||
success = print_to_printer(
|
||||
printer, pdf_path, scale=cfg.printer_scale, settings=cfg.sumatra_settings
|
||||
)
|
||||
return success, pdf_path, warning
|
||||
|
||||
|
||||
# SumatraPDF exit codes when printing (from its documented behaviour).
|
||||
SUMATRA_EXIT_CODES = {
|
||||
0: "success",
|
||||
2: "could not open the file (not found or unsupported format)",
|
||||
3: "the document does not allow printing",
|
||||
4: "the printer does not exist",
|
||||
5: "the printer driver / device failed",
|
||||
6: "printing is disabled by restriction policy",
|
||||
}
|
||||
|
||||
|
||||
def _sumatra_print(sumatra_path, printer_name, file_path, settings):
|
||||
"""
|
||||
Send a PDF to a printer through SumatraPDF and report the real outcome.
|
||||
|
||||
Args:
|
||||
sumatra_path (str): Path to SumatraPDF.exe
|
||||
printer_name (str): Target printer
|
||||
file_path (str): PDF to print
|
||||
settings (str): Value for -print-settings, e.g. "noscale,center"
|
||||
|
||||
Returns:
|
||||
tuple: (success (bool), message (str))
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[
|
||||
sumatra_path,
|
||||
"-print-to", printer_name,
|
||||
file_path,
|
||||
"-print-settings", settings,
|
||||
"-silent",
|
||||
"-exit-when-done",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
|
||||
code = result.returncode
|
||||
if code == 0:
|
||||
return True, f"sent to '{printer_name}' (settings: {settings})"
|
||||
|
||||
reason = SUMATRA_EXIT_CODES.get(code, f"unknown exit code {code}")
|
||||
detail = (result.stderr or result.stdout or "").strip()
|
||||
message = f"SumatraPDF failed (exit {code}): {reason}"
|
||||
if detail:
|
||||
message += f" - {detail.splitlines()[-1]}"
|
||||
return False, message
|
||||
|
||||
|
||||
def print_to_printer(printer_name, file_path, scale="noscale", settings=None):
|
||||
"""
|
||||
Print file to printer (cross-platform).
|
||||
|
||||
@@ -294,19 +441,25 @@ def print_to_printer(printer_name, file_path):
|
||||
for sumatra_path in sumatra_paths:
|
||||
if os.path.exists(sumatra_path):
|
||||
try:
|
||||
subprocess.run([
|
||||
sumatra_path,
|
||||
"-print-to",
|
||||
printer_name,
|
||||
file_path,
|
||||
"-print-settings",
|
||||
"fit,landscape",
|
||||
"-silent",
|
||||
"-exit-when-done"
|
||||
], check=False, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
print(f"Label sent to printer via SumatraPDF: {printer_name}")
|
||||
printed = True
|
||||
break
|
||||
# "noscale" keeps the PDF at its true physical
|
||||
# size; the P-touch label layout depends on it.
|
||||
# "center" stops the label hugging one edge when
|
||||
# the page is shorter than the media.
|
||||
if settings:
|
||||
print_settings = settings
|
||||
elif scale == "noscale":
|
||||
print_settings = "noscale"
|
||||
else:
|
||||
print_settings = "fit,landscape"
|
||||
|
||||
ok, message = _sumatra_print(
|
||||
sumatra_path, printer_name, file_path,
|
||||
print_settings
|
||||
)
|
||||
print(f"SumatraPDF: {message}")
|
||||
if ok:
|
||||
printed = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"SumatraPDF error: {e}")
|
||||
|
||||
@@ -370,7 +523,8 @@ def print_to_printer(printer_name, file_path):
|
||||
pass
|
||||
|
||||
if not printed:
|
||||
# Fallback: Let user know and save PDF
|
||||
# No silent-print backend worked. Report failure so the
|
||||
# caller does not claim a label was printed.
|
||||
print("=" * 60)
|
||||
print("NOTICE: Silent PDF printing requires SumatraPDF")
|
||||
print("SumatraPDF not found (should be bundled inside the app)")
|
||||
@@ -379,7 +533,8 @@ def print_to_printer(printer_name, file_path):
|
||||
print("=" * 60)
|
||||
print(f"PDF saved to: {file_path}")
|
||||
print("The PDF can be printed manually.")
|
||||
|
||||
return False
|
||||
|
||||
return True
|
||||
else:
|
||||
# Non-PDF files
|
||||
@@ -394,7 +549,7 @@ def print_to_printer(printer_name, file_path):
|
||||
except Exception as e:
|
||||
print(f"Windows print error: {e}")
|
||||
print("PDF backup saved as fallback")
|
||||
return True
|
||||
return False
|
||||
|
||||
elif SYSTEM == "Darwin":
|
||||
# macOS: Use lp command
|
||||
|
||||
Reference in New Issue
Block a user