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:
@@ -59,11 +59,12 @@ a = Analysis(
|
|||||||
binaries=gs_binaries,
|
binaries=gs_binaries,
|
||||||
datas=base_datas + gs_datas,
|
datas=base_datas + gs_datas,
|
||||||
hiddenimports=[
|
hiddenimports=[
|
||||||
'kivy', 'PIL', 'barcode', 'reportlab',
|
'kivy', 'PIL', 'PIL.ImageWin', 'barcode', 'reportlab',
|
||||||
'print_label', 'print_label_pdf',
|
'print_label', 'print_label_pdf',
|
||||||
|
'fitz', 'pymupdf',
|
||||||
'svglib', 'cairosvg',
|
'svglib', 'cairosvg',
|
||||||
'watchdog', 'watchdog.observers', 'watchdog.events',
|
'watchdog', 'watchdog.observers', 'watchdog.events',
|
||||||
'pystray', 'win32api', 'win32print', 'win32timezone',
|
'pystray', 'win32api', 'win32print', 'win32ui', 'win32con', 'win32timezone',
|
||||||
],
|
],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
|
|||||||
BIN
dist/LabelPrinter.exe
vendored
BIN
dist/LabelPrinter.exe
vendored
Binary file not shown.
281
print_label.py
281
print_label.py
@@ -6,6 +6,21 @@ import platform
|
|||||||
import subprocess
|
import subprocess
|
||||||
from print_label_pdf import PDFLabelGenerator
|
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
|
# Cross-platform printer support
|
||||||
try:
|
try:
|
||||||
import cups
|
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)
|
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):
|
def print_to_printer(printer_name, file_path):
|
||||||
"""
|
"""
|
||||||
Print file to printer (cross-platform).
|
Print file to printer (cross-platform).
|
||||||
@@ -169,141 +317,14 @@ def print_to_printer(printer_name, file_path):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
elif SYSTEM == "Windows":
|
elif SYSTEM == "Windows":
|
||||||
# Windows: Print PDF silently without any viewer opening
|
# Windows: Print PDF using Python GDI (pymupdf + win32print).
|
||||||
try:
|
# No external viewer is launched at any point.
|
||||||
if file_path.endswith('.pdf'):
|
if file_path.endswith('.pdf'):
|
||||||
printed = False
|
return _print_pdf_windows(file_path, printer_name)
|
||||||
|
|
||||||
# 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:
|
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],
|
subprocess.run(['notepad', '/p', file_path],
|
||||||
check=False,
|
check=False,
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
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")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif SYSTEM == "Darwin":
|
elif SYSTEM == "Darwin":
|
||||||
|
|||||||
Reference in New Issue
Block a user