Add template selection and multiple copy printing features

- Implemented template selection: type 0=OK (green), type 1=NOK (red)
- Added multiple copy printing (1-100 copies)
- Extended file format to 5 fields: ARTICLE;NR_ART;SERIAL;TYPE;COUNT
- Created OK/NOK SVG templates with visual distinction
- Fixed PDF landscape orientation issues
- Updated SumatraPDF to use noscale for exact dimensions
- Auto-generate conf folder with default templates on first run
- Made pystray optional for system tray functionality
- Updated build scripts for Python 3.13 compatibility (Kivy 2.3+, PyInstaller 6.18)
- Added comprehensive build documentation
- Improved printer configuration guidance
This commit is contained in:
NAME
2026-02-13 23:34:59 +02:00
parent 3b23f89cf0
commit 839828340d
13 changed files with 1131 additions and 117 deletions

View File

@@ -3,6 +3,7 @@ import barcode
from barcode.writer import ImageWriter
import time
import os
import sys
import datetime
import platform
import subprocess
@@ -173,7 +174,14 @@ def create_label_image(text):
# 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)
# Use high-quality resampling for crisp barcodes
try:
# Try newer Pillow API first
from PIL.Image import Resampling
barcode_resized = barcode_img.resize((barcode_width, barcode_height), Resampling.LANCZOS)
except (ImportError, AttributeError):
# Fallback for older Pillow versions
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
@@ -194,13 +202,14 @@ def create_label_image(text):
return label_img
def create_label_pdf(text):
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" or single value
svg_template (str): Path to specific SVG template to use (optional)
Returns:
str: Path to the generated PDF file
@@ -221,11 +230,17 @@ def create_label_pdf(text):
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
pdf_filename = os.path.join(pdf_backup_dir, f"final_label_{timestamp}.pdf")
# Check for default SVG template
svg_template = None
default_svg = os.path.join('conf', 'label_template.svg')
if os.path.exists(default_svg):
svg_template = default_svg
# Use SVG template for customizable layout
if svg_template is None or not os.path.exists(svg_template):
# Try default templates
default_svg = os.path.join('conf', 'label_template.svg')
if os.path.exists(default_svg):
svg_template = default_svg
print(f"Using SVG template: {default_svg}")
else:
print("SVG template not found, using fallback PDF generation")
else:
print(f"Using SVG template: {svg_template}")
# Check for default image path
image_path = os.path.join('conf', 'accepted.png')
@@ -233,9 +248,94 @@ def create_label_pdf(text):
return generator.create_label_pdf(article, nr_art, serial, pdf_filename, image_path, svg_template)
def configure_printer_quality(printer_name, width_mm=35, height_mm=25):
"""
Configure printer for high quality label printing (Windows only).
Sets paper size, orientation, and QUALITY settings.
Args:
printer_name (str): Name of the printer
width_mm (int): Label width in millimeters (default 35)
height_mm (int): Label height in millimeters (default 25)
Returns:
bool: True if successful
"""
if SYSTEM != "Windows" or not WIN32_AVAILABLE:
return False
try:
import win32print
import pywintypes
hprinter = win32print.OpenPrinter(printer_name)
try:
# Get current printer properties
props = win32print.GetPrinter(hprinter, 2)
devmode = props.get("pDevMode")
if devmode is None:
print("Could not get printer DEVMODE")
return False
# CRITICAL: Set print quality to HIGHEST
# This prevents dotted/pixelated text
try:
devmode.PrintQuality = 600 # 600 DPI (high quality)
except:
try:
devmode.PrintQuality = 4 # DMRES_HIGH
except:
pass
# Set custom paper size
try:
devmode.PaperSize = 256 # DMPAPER_USER (custom size)
devmode.PaperLength = height_mm * 10 # Height in 0.1mm units
devmode.PaperWidth = width_mm * 10 # Width in 0.1mm units
except:
pass
# Set orientation to landscape
try:
devmode.Orientation = 2 # Landscape
except:
pass
# Set additional quality settings
try:
devmode.Color = 1 # Monochrome for labels
except:
pass
try:
devmode.TTOption = 2 # DMTT_BITMAP - print TrueType as graphics (sharper)
except:
pass
# Apply settings
try:
props["pDevMode"] = devmode
win32print.SetPrinter(hprinter, 2, props, 0)
print(f"Printer configured: {width_mm}x{height_mm}mm @ HIGH QUALITY")
return True
except Exception as set_err:
print(f"Could not apply printer settings: {set_err}")
return False
finally:
win32print.ClosePrinter(hprinter)
except Exception as e:
print(f"Could not configure printer quality: {e}")
return False
def print_to_printer(printer_name, file_path):
"""
Print file to printer (cross-platform).
Uses SumatraPDF for silent printing on Windows.
Args:
printer_name (str): Name of printer or "PDF" for PDF output
@@ -258,66 +358,145 @@ def print_to_printer(printer_name, file_path):
return True
elif SYSTEM == "Windows":
# Windows: Print PDF using various methods
# Windows: Print PDF silently without any viewer opening
try:
if WIN32_AVAILABLE:
import win32print
import win32api
if file_path.endswith('.pdf'):
# Method 1: Try SumatraPDF for best silent printing
sumatra_paths = [
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf', 'SumatraPDF.exe'),
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf', 'SumatraPDF-portable.exe'),
# 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"))
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",
os.path.expandvars(r"%LOCALAPPDATA%\SumatraPDF\SumatraPDF.exe"),
]
])
sumatra_found = False
for sumatra_path in sumatra_paths:
if os.path.exists(sumatra_path):
sumatra_found = True
try:
# Use SumatraPDF silent printing with high quality settings
# noscale = print at actual size without scaling
# landscape = force landscape orientation for 35x25mm labels
# Note: SumatraPDF uses printer driver's quality settings
# Use noscale with paper size specification for thermal printers
# Format: "noscale,paper=<size>" where paper size matches PDF (35mm x 25mm)
# SumatraPDF will use the PDF's page size when noscale is used
subprocess.run([
sumatra_path,
'-print-to', printer_name,
'-silent',
'-print-settings', 'noscale,landscape',
file_path
], check=True, creationflags=subprocess.CREATE_NO_WINDOW)
"-print-to",
printer_name,
file_path,
"-print-settings",
"noscale", # Preserve exact PDF page dimensions
"-silent",
"-exit-when-done"
], check=False, creationflags=subprocess.CREATE_NO_WINDOW)
print(f"Label sent to printer via SumatraPDF: {printer_name}")
return True
except Exception as sumatra_err:
print(f"SumatraPDF print failed: {sumatra_err}")
print(f"Note: Printer '{printer_name}' should be configured for 35mm x 25mm labels")
printed = True
break
except Exception as e:
print(f"SumatraPDF error: {e}")
# Method 2: Use ShellExecute with printto (requires default PDF viewer)
if not sumatra_found or WIN32_AVAILABLE:
try:
import win32api
win32api.ShellExecute(
0, "printto", file_path,
f'"{printer_name}"', ".", 0
)
print(f"Label sent to printer via ShellExecute: {printer_name}")
return True
except Exception as shell_err:
print(f"ShellExecute print failed: {shell_err}")
# Method 3: Open PDF with default viewer (last resort)
# 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:
os.startfile(file_path, "print")
print(f"Opened PDF for printing: {file_path}")
print("Please close the PDF viewer after printing.")
return True
except Exception as startfile_err:
print(f"Could not open PDF: {startfile_err}")
print("PDF saved as backup only")
return True
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("=" * 60)
print(f"PDF saved to: {file_path}")
print("The PDF can be printed manually.")
return True
else:
# Non-PDF files: print silently with notepad
subprocess.run(['notepad', '/p', file_path],
check=False,
# 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
@@ -343,7 +522,7 @@ def print_to_printer(printer_name, file_path):
return True
def print_label_standalone(value, printer, preview=0, use_pdf=True):
def print_label_standalone(value, printer, preview=0, use_pdf=True, svg_template=None):
"""
Print a label with the specified text on the specified printer.
Always generates a PDF backup in pdf_backup and prints that PDF.
@@ -353,6 +532,7 @@ def print_label_standalone(value, printer, preview=0, use_pdf=True):
printer (str): The name of the printer to use
preview (int): 0 = no preview, 1-3 = 3s preview, >3 = 5s preview
use_pdf (bool): False to also generate a PNG if PDF generation fails
svg_template (str): Path to specific SVG template to use (optional)
Returns:
bool: True if printing was successful, False otherwise
@@ -370,7 +550,7 @@ def print_label_standalone(value, printer, preview=0, use_pdf=True):
# Always generate a PDF backup and print that PDF for verification
try:
pdf_file = create_label_pdf(value)
pdf_file = create_label_pdf(value, svg_template)
if pdf_file and os.path.exists(pdf_file):
print(f"PDF label created: {pdf_file}")
print(f"PDF backup saved to: {pdf_file}")