import time import os import sys import datetime 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 CUPS_AVAILABLE = True except ImportError: CUPS_AVAILABLE = False try: import win32api import win32print WIN32_AVAILABLE = True except ImportError: WIN32_AVAILABLE = False SYSTEM = platform.system() # 'Linux', 'Windows', 'Darwin' def get_available_printers(): """ Get list of available printers (cross-platform). Includes both local and network printers on Windows. Returns: list: List of available printer names, with "PDF" as fallback """ try: if SYSTEM == "Linux" and CUPS_AVAILABLE: # Linux: Use CUPS conn = cups.Connection() printers = conn.getPrinters() return list(printers.keys()) if printers else ["PDF"] elif SYSTEM == "Windows": # Windows: Get local + connected printers (includes print server connections) try: printers = [] # PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS captures: # - Locally installed printers # - Printers connected from a print server (e.g. \\server\printer) try: flags = win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS for printer_info in win32print.EnumPrinters(flags): printer_name = printer_info[2] if printer_name and printer_name not in printers: printers.append(printer_name) except Exception as e: print(f"Error enumerating printers: {e}") # Add PDF as fallback option if "PDF" not in printers: printers.append("PDF") return printers if printers else ["PDF"] except Exception as e: print(f"Error getting Windows printers: {e}") return ["PDF"] elif SYSTEM == "Darwin": # macOS: Use lpstat command try: result = subprocess.run(["lpstat", "-p", "-d"], capture_output=True, text=True) printers = [] for line in result.stdout.split('\n'): if line.startswith('printer'): printer_name = line.split()[1] printers.append(printer_name) return printers if printers else ["PDF"] except: return ["PDF"] else: return ["PDF"] except Exception as e: print(f"Error getting printers: {e}") return ["PDF"] def create_label_pdf(text, svg_template=None): """ Create a high-quality PDF label with 3 rows: label + barcode for each field. PDFs are saved to the pdf_backup folder. Args: text (str): Combined text in format "article;nr_art;serial;status" or single value status: 1 = OK label, 0 = NOK label svg_template (str): Path to specific SVG template to use (optional) Returns: str: Path to the generated PDF file """ # Parse the text input - using semicolon separator parts = text.split(';') if ';' in text else [text, '', ''] article = parts[0].strip() if len(parts) > 0 else '' nr_art = parts[1].strip() if len(parts) > 1 else '' serial = parts[2].strip() if len(parts) > 2 else '' status_flag = parts[3].strip() if len(parts) > 3 else '1' # Create PDF using high-quality generator generator = PDFLabelGenerator() # Ensure pdf_backup folder exists pdf_backup_dir = 'pdf_backup' os.makedirs(pdf_backup_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") pdf_filename = os.path.join(pdf_backup_dir, f"final_label_{timestamp}.pdf") # Select template/image based on status flag # 1 = OK label, 0 = NOK label selected_template = svg_template if svg_template and os.path.exists(svg_template) else None default_svg = os.path.join('conf', 'label_template.svg') ok_svg = os.path.join('conf', 'label_template_ok.svg') nok_svg = os.path.join('conf', 'label_template_nok.svg') if selected_template: image_path = os.path.join('conf', 'accepted.png') if status_flag != '0' else os.path.join('conf', 'refused.png') elif status_flag == '0': # NOK label: prefer dedicated NOK SVG template, otherwise use refused image in standard layout if os.path.exists(nok_svg): selected_template = nok_svg elif os.path.exists(default_svg): selected_template = default_svg image_path = os.path.join('conf', 'refused.png') else: # OK label (default): prefer dedicated OK SVG template, fallback to default SVG template if os.path.exists(ok_svg): selected_template = ok_svg elif os.path.exists(default_svg): selected_template = default_svg image_path = os.path.join('conf', 'accepted.png') return generator.create_label_pdf(article, nr_art, serial, pdf_filename, image_path, selected_template) def _print_pdf_windows(file_path, printer_name, copies=1): """ Print a PDF on Windows using pymupdf (fitz) to render and win32print/GDI to send. All copies are sent as multiple pages within ONE print job so the printer never sees a gap (which causes blank labels on thermal printers). No external application is launched. Falls back to SumatraPDF if pymupdf is unavailable. Returns True on success, False on failure. """ copies = max(1, int(copies)) _write_print_log(f"_print_pdf_windows called: printer={printer_name!r} copies={copies} 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. # All copies go into ONE print job to prevent blank labels between jobs. hdc.StartDoc(os.path.basename(file_path)) dib = ImageWin.Dib(img) for copy_idx in range(copies): hdc.StartPage() 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} ({copies} cop{'y' if copies==1 else 'ies'} in 1 job)") 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, copies=1): """ Print file to printer (cross-platform). Uses pymupdf+GDI for silent printing on Windows with all copies in one job. Args: printer_name (str): Name of printer or "PDF" for PDF output file_path (str): Path to file to print copies (int): Number of copies to print in a single print job Returns: bool: True if successful """ try: if printer_name == "PDF": # PDF output - file is already saved print(f"PDF output: {file_path}") return True elif SYSTEM == "Linux" and CUPS_AVAILABLE: # Linux: Use CUPS conn = cups.Connection() conn.printFile(printer_name, file_path, "Label Print", {}) print(f"Label sent to printer: {printer_name}") return True elif SYSTEM == "Windows": # 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, copies=copies) else: subprocess.run(['notepad', '/p', file_path], check=False, creationflags=subprocess.CREATE_NO_WINDOW) return True elif SYSTEM == "Darwin": # macOS: Use lp command subprocess.run(["lp", "-d", printer_name, file_path], check=True) print(f"Label sent to printer: {printer_name}") return True else: print(f"Unsupported system: {SYSTEM}") return False except Exception as e: print(f"Printer error: {str(e)}") print("Label already saved to file as fallback...") print(f"Label file: {file_path}") return True def print_label_standalone(value, printer, preview=0, svg_template=None, copies=1): """ Generate a PDF label, save it to pdf_backup/, and print it on the printer. All copies are sent in a single print job to avoid blank labels on thermal printers that eject a label between separate jobs. Args: value (str): Label data in format "article;nr_art;serial;status" printer (str): Printer name (or "PDF" to skip physical printing) preview (int): 0 = print immediately; 1-3 = 3 s delay; >3 = 5 s delay svg_template (str): Path to SVG template (optional; auto-selected if None) copies (int): Number of copies to print in one job (default 1) Returns: bool: True if sending to printer succeeded, False otherwise """ copies = max(1, int(copies)) pdf_file = None try: # Step 1 – Generate and save PDF to pdf_backup/ try: pdf_file = create_label_pdf(value, svg_template) if pdf_file and os.path.exists(pdf_file): print(f"PDF label created: {pdf_file}") else: print("PDF generation failed – no output file") return False except Exception as pdf_err: print(f"PDF generation error: {pdf_err}") return False # Step 2 – Optional countdown before printing if isinstance(preview, str): preview = int(preview) if preview > 0: preview_sec = 3 if 1 <= preview <= 3 else 5 print(f"Printing in {preview_sec} seconds… (Ctrl+C to cancel)") try: for i in range(preview_sec, 0, -1): print(f" {i}...", end=" ", flush=True) time.sleep(1) print("\nPrinting now...") except KeyboardInterrupt: print("\nCancelled by user") return False # Step 3 – Send to printer (all copies in one job) print(f"Sending to printer ({copies} cop{'y' if copies==1 else 'ies'})...") return print_to_printer(printer, pdf_file, copies=copies) except Exception as e: print(f"Error printing label: {str(e)}") return False finally: if pdf_file and os.path.exists(pdf_file): print("Cleanup complete – PDF backup saved to pdf_backup/") else: print("Cleanup complete") # Main code removed - import this module or run as part of the Kivy GUI application