- Fixed network printer enumeration (PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS) - Added printer name truncation to 20 chars with full name mapping - Implemented silent PDF printing using SumatraPDF with landscape orientation - Added auto-dismiss for success popup (3 seconds) - Bundled SumatraPDF inside executable for portable single-file deployment - Updated build script to embed SumatraPDF - Added setup_sumatra.ps1 for downloading SumatraPDF portable - Added DEPLOYMENT.md documentation
497 lines
19 KiB
Python
Executable File
497 lines
19 KiB
Python
Executable File
from PIL import Image, ImageDraw, ImageFont
|
|
import barcode
|
|
from barcode.writer import ImageWriter
|
|
import time
|
|
import os
|
|
import sys
|
|
import datetime
|
|
import platform
|
|
import subprocess
|
|
from print_label_pdf import PDFLabelGenerator
|
|
|
|
# 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_image(text):
|
|
"""
|
|
Create a label image with 3 rows: label + barcode for each field.
|
|
|
|
Args:
|
|
text (str): Combined text in format "SAP|CANTITATE|LOT" or single value
|
|
|
|
Returns:
|
|
PIL.Image: The generated label image
|
|
"""
|
|
# Parse the text input
|
|
parts = text.split('|') if '|' in text else [text, '', '']
|
|
sap_nr = parts[0].strip() if len(parts) > 0 else ''
|
|
cantitate = parts[1].strip() if len(parts) > 1 else ''
|
|
lot_number = parts[2].strip() if len(parts) > 2 else ''
|
|
|
|
# Label dimensions (narrower, 3 rows)
|
|
label_width = 800 # 8 cm
|
|
label_height = 600 # 6 cm
|
|
|
|
# Create canvas
|
|
label_img = Image.new('RGB', (label_width, label_height), 'white')
|
|
draw = ImageDraw.Draw(label_img)
|
|
|
|
# Row setup - 3 equal rows
|
|
row_height = label_height // 3
|
|
left_margin = 15
|
|
row_spacing = 3
|
|
|
|
# Fonts
|
|
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
|
try:
|
|
label_font = ImageFont.truetype(font_path, 16)
|
|
value_font = ImageFont.truetype(font_path, 14)
|
|
except IOError:
|
|
label_font = ImageFont.load_default()
|
|
value_font = ImageFont.load_default()
|
|
|
|
# Data for 3 rows
|
|
rows_data = [
|
|
("SAP-Nr", sap_nr),
|
|
("Cantitate", cantitate),
|
|
("Lot Nr", lot_number),
|
|
]
|
|
|
|
# Generate barcodes first
|
|
CODE128 = barcode.get_barcode_class('code128')
|
|
writer_options = {
|
|
"write_text": False,
|
|
"module_width": 0.4,
|
|
"module_height": 8,
|
|
"quiet_zone": 2,
|
|
"font_size": 0
|
|
}
|
|
|
|
barcode_images = []
|
|
for _, value in rows_data:
|
|
if value:
|
|
try:
|
|
code = CODE128(value[:25], writer=ImageWriter())
|
|
filename = code.save('temp_barcode', options=writer_options)
|
|
barcode_img = Image.open(filename)
|
|
barcode_images.append(barcode_img)
|
|
except:
|
|
barcode_images.append(None)
|
|
else:
|
|
barcode_images.append(None)
|
|
|
|
# Draw each row with label and barcode
|
|
for idx, ((label_name, value), barcode_img) in enumerate(zip(rows_data, barcode_images)):
|
|
row_y = idx * row_height
|
|
|
|
# Draw label name
|
|
draw.text(
|
|
(left_margin, row_y + 3),
|
|
label_name,
|
|
fill='black',
|
|
font=label_font
|
|
)
|
|
|
|
# Draw barcode if available
|
|
if barcode_img:
|
|
# Resize barcode to fit in row width
|
|
barcode_width = label_width - left_margin - 10
|
|
barcode_height = row_height - 25
|
|
barcode_resized = barcode_img.resize((barcode_width, barcode_height), Image.LANCZOS)
|
|
label_img.paste(barcode_resized, (left_margin, row_y + 20))
|
|
else:
|
|
# Fallback: show value as text
|
|
draw.text(
|
|
(left_margin, row_y + 25),
|
|
value if value else "(empty)",
|
|
fill='black',
|
|
font=value_font
|
|
)
|
|
|
|
# Clean up temporary barcode files
|
|
try:
|
|
if os.path.exists('temp_barcode.png'):
|
|
os.remove('temp_barcode.png')
|
|
except:
|
|
pass
|
|
|
|
return label_img
|
|
|
|
|
|
def create_label_pdf(text):
|
|
"""
|
|
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 "SAP|CANTITATE|LOT" or single value
|
|
|
|
Returns:
|
|
str: Path to the generated PDF file
|
|
"""
|
|
# Parse the text input
|
|
parts = text.split('|') if '|' in text else [text, '', '']
|
|
sap_nr = parts[0].strip() if len(parts) > 0 else ''
|
|
cantitate = parts[1].strip() if len(parts) > 1 else ''
|
|
lot_number = parts[2].strip() if len(parts) > 2 else ''
|
|
|
|
# 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")
|
|
|
|
return generator.create_label_pdf(sap_nr, cantitate, lot_number, pdf_filename)
|
|
|
|
|
|
def print_to_printer(printer_name, file_path):
|
|
"""
|
|
Print file to printer (cross-platform).
|
|
|
|
Args:
|
|
printer_name (str): Name of printer or "PDF" for PDF output
|
|
file_path (str): Path to file to print
|
|
|
|
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 silently without any viewer opening
|
|
try:
|
|
if WIN32_AVAILABLE:
|
|
import win32print
|
|
import win32api
|
|
|
|
if file_path.endswith('.pdf'):
|
|
# Try silent printing methods (no viewer opens)
|
|
import os
|
|
import winreg
|
|
|
|
printed = False
|
|
|
|
# Method 1: SumatraPDF (bundled inside exe or external)
|
|
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)
|
|
|
|
# Also check app directory 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"))
|
|
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"))
|
|
|
|
# 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:
|
|
subprocess.run([
|
|
sumatra_path,
|
|
"-print-to",
|
|
printer_name,
|
|
file_path,
|
|
"-print-settings",
|
|
"fit,landscape",
|
|
"-silent",
|
|
"-exit-when-done"
|
|
], check=False, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
print(f"Label sent to printer via SumatraPDF: {printer_name}")
|
|
printed = True
|
|
break
|
|
except Exception as e:
|
|
print(f"SumatraPDF error: {e}")
|
|
|
|
# Method 2: Adobe Reader silent printing
|
|
if not printed:
|
|
adobe_path = None
|
|
for key_path in [
|
|
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\AcroRd32.exe",
|
|
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Acrobat.exe"
|
|
]:
|
|
try:
|
|
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path)
|
|
adobe_path, _ = winreg.QueryValueEx(key, "")
|
|
winreg.CloseKey(key)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
if adobe_path and os.path.exists(adobe_path):
|
|
try:
|
|
subprocess.run([
|
|
adobe_path,
|
|
"/t", # Print and close
|
|
file_path,
|
|
printer_name
|
|
], check=False, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
print(f"Label sent to printer via Adobe Reader: {printer_name}")
|
|
printed = True
|
|
except:
|
|
pass
|
|
|
|
# Method 3: GhostScript (if installed)
|
|
if not printed:
|
|
gs_paths = [
|
|
r"C:\Program Files\gs\gs10.02.1\bin\gswin64c.exe",
|
|
r"C:\Program Files (x86)\gs\gs10.02.1\bin\gswin32c.exe",
|
|
]
|
|
# Try to find gswin in PATH
|
|
try:
|
|
gs_result = subprocess.run(['where', 'gswin64c'],
|
|
capture_output=True, text=True, check=False)
|
|
if gs_result.returncode == 0:
|
|
gs_paths.insert(0, gs_result.stdout.strip().split('\n')[0])
|
|
except:
|
|
pass
|
|
|
|
for gs_path in gs_paths:
|
|
if os.path.exists(gs_path):
|
|
try:
|
|
subprocess.run([
|
|
gs_path,
|
|
"-dNOPAUSE", "-dBATCH", "-dQUIET",
|
|
f"-sDEVICE=mswinpr2",
|
|
f"-sOutputFile=%printer%{printer_name}",
|
|
file_path
|
|
], check=False, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
print(f"Label sent to printer via GhostScript: {printer_name}")
|
|
printed = True
|
|
break
|
|
except:
|
|
pass
|
|
|
|
if not printed:
|
|
# Fallback: Let user know and save PDF
|
|
print("=" * 60)
|
|
print("NOTICE: Silent PDF printing requires SumatraPDF")
|
|
print("SumatraPDF not found (should be bundled inside the app)")
|
|
print("If you built the app yourself, ensure SumatraPDF.exe is downloaded first.")
|
|
print("Run: setup_sumatra.ps1 before building")
|
|
print("=" * 60)
|
|
print(f"PDF saved to: {file_path}")
|
|
print("The PDF can be printed manually.")
|
|
|
|
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
|
|
else:
|
|
print("win32print not available, PDF saved as backup only")
|
|
return True
|
|
except Exception as e:
|
|
print(f"Windows print error: {e}")
|
|
print("PDF backup saved as fallback")
|
|
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, use_pdf=True):
|
|
"""
|
|
Print a label with the specified text on the specified printer.
|
|
|
|
Args:
|
|
value (str): The text to print on the label
|
|
printer (str): The name of the printer to use
|
|
preview (int): 0 = no preview, 1-3 = 3s preview, >3 = 5s preview
|
|
use_pdf (bool): True to use PDF (recommended for quality), False for PNG
|
|
|
|
Returns:
|
|
bool: True if printing was successful, False otherwise
|
|
"""
|
|
# For tracking if file was created
|
|
file_created = False
|
|
temp_file = None
|
|
|
|
try:
|
|
# Debug output
|
|
print(f"Preview value: {preview}")
|
|
print(f"Preview type: {type(preview)}")
|
|
print(f"Using format: {'PDF' if use_pdf else 'PNG'}")
|
|
|
|
# Create label in selected format
|
|
if use_pdf:
|
|
temp_file = create_label_pdf(value)
|
|
print(f"PDF label created: {temp_file}")
|
|
print(f"PDF backup saved to: {temp_file}")
|
|
else:
|
|
# Create the label image (PNG)
|
|
label_img = create_label_image(value)
|
|
temp_file = 'final_label.png'
|
|
label_img.save(temp_file)
|
|
print(f"PNG label created: {temp_file}")
|
|
|
|
file_created = True
|
|
|
|
# Convert preview to int if it's a string
|
|
if isinstance(preview, str):
|
|
preview = int(preview)
|
|
|
|
if preview > 0: # Any value above 0 shows a preview message
|
|
# Calculate preview duration in seconds
|
|
if 1 <= preview <= 3:
|
|
preview_sec = 3 # 3 seconds
|
|
else: # preview > 3
|
|
preview_sec = 5 # 5 seconds
|
|
|
|
print(f"Printing in {preview_sec} seconds... (Press Ctrl+C to cancel)")
|
|
|
|
# Simple countdown timer using time.sleep
|
|
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
|
|
|
|
# Print after preview
|
|
print("Sending to printer...")
|
|
return print_to_printer(printer, temp_file)
|
|
else:
|
|
print("Direct printing without preview...")
|
|
# Direct printing without preview (preview = 0)
|
|
return print_to_printer(printer, temp_file)
|
|
|
|
except Exception as e:
|
|
print(f"Error printing label: {str(e)}")
|
|
return False
|
|
|
|
finally:
|
|
# This block always executes, ensuring cleanup
|
|
if use_pdf:
|
|
print(f"Cleanup complete - PDF backup saved to pdf_backup folder")
|
|
else:
|
|
print("Cleanup complete - label file retained for reference")
|
|
|
|
|
|
# Main code removed - import this module or run as part of the Kivy GUI application
|