Files
adaptronic_label-printer/print_label.py

416 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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