Fix printing: use pymupdf+GDI for silent direct printing at correct physical label size - removes Adobe Reader/ShellExecute, prints 35x25mm label 1:1 using printer native DPI

This commit is contained in:
2026-02-25 16:05:33 +02:00
parent b35f278c1e
commit be1b494527
3 changed files with 159 additions and 137 deletions

View File

@@ -59,11 +59,12 @@ a = Analysis(
binaries=gs_binaries,
datas=base_datas + gs_datas,
hiddenimports=[
'kivy', 'PIL', 'barcode', 'reportlab',
'kivy', 'PIL', 'PIL.ImageWin', 'barcode', 'reportlab',
'print_label', 'print_label_pdf',
'fitz', 'pymupdf',
'svglib', 'cairosvg',
'watchdog', 'watchdog.observers', 'watchdog.events',
'pystray', 'win32api', 'win32print', 'win32timezone',
'pystray', 'win32api', 'win32print', 'win32ui', 'win32con', 'win32timezone',
],
hookspath=[],
hooksconfig={},

BIN
dist/LabelPrinter.exe vendored

Binary file not shown.

View File

@@ -6,6 +6,21 @@ import platform
import subprocess
from print_label_pdf import PDFLabelGenerator
def _write_print_log(msg):
"""Write a timestamped debug line to print_debug.log next to the exe / script."""
try:
if getattr(sys, 'frozen', False):
log_dir = os.path.dirname(sys.executable)
else:
log_dir = os.path.dirname(os.path.abspath(__file__))
log_path = os.path.join(log_dir, 'print_debug.log')
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
with open(log_path, 'a', encoding='utf-8') as f:
f.write(f"[{ts}] {msg}\n")
except Exception:
pass
# Cross-platform printer support
try:
import cups
@@ -143,6 +158,139 @@ def create_label_pdf(text, svg_template=None):
return generator.create_label_pdf(article, nr_art, serial, pdf_filename, image_path, selected_template)
def _print_pdf_windows(file_path, printer_name):
"""
Print a PDF on Windows using pymupdf (fitz) to render and win32print/GDI to send.
No external application is launched. Falls back to SumatraPDF if pymupdf
is unavailable.
Returns True on success, False on failure.
"""
_write_print_log(f"_print_pdf_windows called: printer={printer_name!r} file={file_path!r}")
# ── Method 1: pure-Python GDI (pymupdf + win32print) ─────────────────
try:
import fitz # pymupdf
import win32print
import win32ui
import win32con
from PIL import ImageWin, Image as PILImage
_write_print_log("pymupdf + win32 imports OK using GDI method")
doc = fitz.open(file_path)
if doc.page_count == 0:
_write_print_log("PDF has no pages aborting")
return False
page = doc[0]
# Get the printer's native resolution
hdc = win32ui.CreateDC()
hdc.CreatePrinterDC(printer_name)
x_dpi = hdc.GetDeviceCaps(win32con.LOGPIXELSX)
y_dpi = hdc.GetDeviceCaps(win32con.LOGPIXELSY)
_write_print_log(f"Printer DPI={x_dpi}x{y_dpi}")
# Render PDF page at printer DPI (PDF uses 72 pt/inch)
mat = fitz.Matrix(x_dpi / 72.0, y_dpi / 72.0)
pix = page.get_pixmap(matrix=mat, alpha=False)
img = PILImage.frombytes("RGB", [pix.width, pix.height], pix.samples)
# Read physical page size before closing the document
pdf_page_w_pts = page.rect.width # PDF points (1pt = 1/72 inch)
pdf_page_h_pts = page.rect.height
doc.close()
dest_w = int(round(pdf_page_w_pts * x_dpi / 72.0))
dest_h = int(round(pdf_page_h_pts * y_dpi / 72.0))
_write_print_log(
f"PDF page={pdf_page_w_pts:.1f}x{pdf_page_h_pts:.1f}pt "
f"rendered={pix.width}x{pix.height}px dest={dest_w}x{dest_h}px"
)
# Draw at exact physical size NOT stretched to the driver's paper area
hdc.StartDoc(os.path.basename(file_path))
hdc.StartPage()
dib = ImageWin.Dib(img)
dib.draw(hdc.GetHandleOutput(), (0, 0, dest_w, dest_h))
hdc.EndPage()
hdc.EndDoc()
hdc.DeleteDC()
_write_print_log(f"GDI print SUCCESS → {printer_name}")
return True
except ImportError as ie:
_write_print_log(f"GDI method unavailable ({ie}) trying SumatraPDF fallback")
except Exception as e:
_write_print_log(f"GDI print error: {e}")
try:
hdc.DeleteDC()
except Exception:
pass
# ── Method 2: SumatraPDF fallback ────────────────────────────────────
_write_print_log("Attempting SumatraPDF fallback")
sumatra_paths = []
if getattr(sys, 'frozen', False):
if hasattr(sys, '_MEIPASS'):
sumatra_paths.append(os.path.join(sys._MEIPASS, 'SumatraPDF.exe'))
app_dir = os.path.dirname(sys.executable)
sumatra_paths.append(os.path.join(app_dir, 'conf', 'SumatraPDF.exe'))
sumatra_paths.append(os.path.join(app_dir, 'SumatraPDF.exe'))
else:
app_dir = os.path.dirname(os.path.abspath(__file__))
sumatra_paths.append(os.path.join(app_dir, 'conf', 'SumatraPDF.exe'))
sumatra_paths.append(os.path.join(app_dir, 'SumatraPDF.exe'))
sumatra_paths += [
r"C:\Program Files\SumatraPDF\SumatraPDF.exe",
r"C:\Program Files (x86)\SumatraPDF\SumatraPDF.exe",
]
_write_print_log(f"SumatraPDF search paths: {sumatra_paths}")
for sumatra_path in sumatra_paths:
_write_print_log(f"Checking: {sumatra_path} → exists={os.path.exists(sumatra_path)}")
if os.path.exists(sumatra_path):
try:
# Find conf folder with SumatraPDF-settings.txt
settings_candidates = []
if getattr(sys, 'frozen', False):
if hasattr(sys, '_MEIPASS'):
settings_candidates.append(os.path.join(sys._MEIPASS, 'conf'))
settings_candidates.append(os.path.join(os.path.dirname(sys.executable), 'conf'))
else:
settings_candidates.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf'))
settings_candidates.append(os.path.dirname(sumatra_path))
appdata_dir = next(
(d for d in settings_candidates
if os.path.exists(os.path.join(d, 'SumatraPDF-settings.txt'))),
None
)
_write_print_log(f"SumatraPDF settings dir: {appdata_dir}")
cmd = [sumatra_path, "-print-to", printer_name,
"-print-settings", "noscale",
"-silent", "-exit-when-done"]
if appdata_dir:
cmd += ["-appdata-dir", appdata_dir]
cmd.append(file_path)
_write_print_log(f"SumatraPDF cmd: {' '.join(cmd)}")
result = subprocess.run(
cmd, check=False,
creationflags=subprocess.CREATE_NO_WINDOW,
capture_output=True, text=True, timeout=30)
_write_print_log(f"SumatraPDF exit={result.returncode} "
f"stdout={result.stdout.strip()!r} "
f"stderr={result.stderr.strip()!r}")
return True # treat any completed run as spooled
except Exception as se:
_write_print_log(f"SumatraPDF error: {se}")
_write_print_log("All print methods failed")
return False
def print_to_printer(printer_name, file_path):
"""
Print file to printer (cross-platform).
@@ -169,141 +317,14 @@ def print_to_printer(printer_name, file_path):
return True
elif SYSTEM == "Windows":
# Windows: Print PDF silently without any viewer opening
try:
if file_path.endswith('.pdf'):
printed = False
# Method 1: SumatraPDF sends the PDF at its exact page size
# (35 mm × 25 mm) directly to the printer driver with no scaling.
# The printer must already have a 35 mm × 25 mm label stock
# configured in its driver settings.
sumatra_paths = []
# Get the directory where this script/exe is running
if getattr(sys, 'frozen', False):
# Running as compiled executable
# PyInstaller extracts bundled files to sys._MEIPASS temp folder
if hasattr(sys, '_MEIPASS'):
# Check bundled version first (inside the exe)
bundled_sumatra = os.path.join(sys._MEIPASS, 'SumatraPDF.exe')
sumatra_paths.append(bundled_sumatra)
# Check app directory and conf subfolder for external version
app_dir = os.path.dirname(sys.executable)
sumatra_paths.append(os.path.join(app_dir, "SumatraPDF", "SumatraPDF.exe"))
sumatra_paths.append(os.path.join(app_dir, "SumatraPDF.exe"))
sumatra_paths.append(os.path.join(app_dir, "conf", "SumatraPDF.exe")) # conf subfolder next to exe
sumatra_paths.append(os.path.join(os.getcwd(), "conf", "SumatraPDF.exe")) # conf relative to cwd
else:
# Running as script - check local folders
app_dir = os.path.dirname(os.path.abspath(__file__))
sumatra_paths.append(os.path.join(app_dir, "SumatraPDF", "SumatraPDF.exe"))
sumatra_paths.append(os.path.join(app_dir, "SumatraPDF.exe"))
sumatra_paths.append(os.path.join(app_dir, "conf", "SumatraPDF.exe"))
# Then check system installations
sumatra_paths.extend([
r"C:\Program Files\SumatraPDF\SumatraPDF.exe",
r"C:\Program Files (x86)\SumatraPDF\SumatraPDF.exe",
])
for sumatra_path in sumatra_paths:
if os.path.exists(sumatra_path):
try:
print(f"Using SumatraPDF: {sumatra_path}")
print(f"Sending to printer: {printer_name}")
# Locate the conf folder that contains
# SumatraPDF-settings.txt so SumatraPDF reads
# our pre-configured PrintScale = noscale.
sumatra_dir = os.path.dirname(sumatra_path)
settings_candidates = [
# conf\ next to the exe (script mode)
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf'),
# same folder as SumatraPDF.exe
sumatra_dir,
]
if getattr(sys, 'frozen', False):
settings_candidates.insert(0, os.path.join(os.path.dirname(sys.executable), 'conf'))
appdata_dir = next(
(d for d in settings_candidates
if os.path.exists(os.path.join(d, 'SumatraPDF-settings.txt'))),
None
)
# Build command:
# -print-settings noscale : send PDF at exact page size (35x25 mm)
# -appdata-dir : use our settings file (PrintScale=noscale)
# -silent / -exit-when-done : no UI, exit after spooling
# IMPORTANT: file_path must come LAST
cmd = [
sumatra_path,
"-print-to", printer_name,
"-print-settings", "noscale",
"-silent",
"-exit-when-done",
]
if appdata_dir:
cmd += ["-appdata-dir", appdata_dir]
print(f"SumatraPDF appdata-dir: {appdata_dir}")
cmd.append(file_path)
result = subprocess.run(
cmd,
check=False,
creationflags=subprocess.CREATE_NO_WINDOW,
capture_output=True, text=True, timeout=30)
if result.returncode != 0:
print(f"SumatraPDF returned code {result.returncode}")
if result.stderr:
print(f"SumatraPDF stderr: {result.stderr.strip()}")
else:
print(f"Label sent to printer via SumatraPDF: {printer_name}")
printed = True
break
except Exception as e:
print(f"SumatraPDF error: {e}")
# Method 2: win32api ShellExecute "print" verb last resort,
# uses whatever PDF application is registered as the default
# handler. This may briefly open the viewer, but it will
# send the job to the correct physical printer.
if not printed and WIN32_AVAILABLE:
try:
import win32api
print(f"Trying win32api ShellExecute print fallback → {printer_name}")
# Set the target printer as default temporarily is
# unreliable; instead, rely on the user having already
# selected the right default printer in Windows.
win32api.ShellExecute(
0, # hwnd
"print", # operation
file_path, # file
None, # parameters
".", # working dir
0 # show-command (SW_HIDE)
)
print("Label sent via ShellExecute print fallback.")
printed = True
except Exception as se_err:
print(f"win32api ShellExecute fallback failed: {se_err}")
if not printed:
print("All print methods failed. PDF saved as backup only.")
return False
return True
else:
# Non-PDF files
subprocess.run(['notepad', '/p', file_path],
check=False,
creationflags=subprocess.CREATE_NO_WINDOW)
print(f"Label sent to printer: {printer_name}")
return True
except Exception as e:
print(f"Windows print error: {e}")
print("PDF backup saved as fallback")
# Windows: Print PDF using Python GDI (pymupdf + win32print).
# No external viewer is launched at any point.
if file_path.endswith('.pdf'):
return _print_pdf_windows(file_path, printer_name)
else:
subprocess.run(['notepad', '/p', file_path],
check=False,
creationflags=subprocess.CREATE_NO_WINDOW)
return True
elif SYSTEM == "Darwin":