416 lines
16 KiB
Python
416 lines
16 KiB
Python
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
|