diff --git a/LabelPrinter.spec b/LabelPrinter.spec index b550edb..c22cb31 100644 --- a/LabelPrinter.spec +++ b/LabelPrinter.spec @@ -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={}, diff --git a/dist/LabelPrinter.exe b/dist/LabelPrinter.exe index 0a5b48b..a80fa6c 100644 Binary files a/dist/LabelPrinter.exe and b/dist/LabelPrinter.exe differ diff --git a/print_label.py b/print_label.py index 576ef01..d5cfba7 100644 --- a/print_label.py +++ b/print_label.py @@ -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":