Remove GhostScript, use SumatraPDF only for printing

- Remove find_ghostscript() and print_pdf_with_ghostscript() from print_label.py
- SumatraPDF is now the primary print method with -print-settings noscale
- Fix SumatraPDF arg order: file_path moved to last position
- Add -appdata-dir so SumatraPDF reads conf/SumatraPDF-settings.txt (PrintScale=noscale)
- win32api ShellExecute kept as last-resort fallback
- Remove GhostScript bundling from LabelPrinter.spec and build_exe.py
- Add conf/ folder pre-flight check to build_exe.py
- Rebuild dist/LabelPrinter.exe (41 MB, SumatraPDF-only)
This commit is contained in:
2026-02-25 10:00:56 +02:00
parent faef90f98b
commit b35f278c1e
71 changed files with 99 additions and 16518 deletions

View File

@@ -143,96 +143,6 @@ def create_label_pdf(text, svg_template=None):
return generator.create_label_pdf(article, nr_art, serial, pdf_filename, image_path, selected_template)
def find_ghostscript():
"""
Find GhostScript executable. Search order:
1. Bundled inside the PyInstaller .exe (ghostscript/bin/ in _MEIPASS)
2. System-level installation (C:\\Program Files\\gs\\...)
Returns the full path to gswin64c.exe / gswin32c.exe, or None.
"""
# 1 ── Bundled location (PyInstaller one-file build)
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
for exe_name in ['gswin64c.exe', 'gswin32c.exe']:
bundled = os.path.join(sys._MEIPASS, 'ghostscript', 'bin', exe_name)
if os.path.exists(bundled):
print(f"Using bundled GhostScript: {bundled}")
return bundled
# 2 ── System installation (newest version first)
for program_files in [r"C:\Program Files", r"C:\Program Files (x86)"]:
gs_base = os.path.join(program_files, "gs")
if os.path.exists(gs_base):
for version_dir in sorted(os.listdir(gs_base), reverse=True):
for exe_name in ["gswin64c.exe", "gswin32c.exe"]:
gs_path = os.path.join(gs_base, version_dir, "bin", exe_name)
if os.path.exists(gs_path):
return gs_path
return None
def print_pdf_with_ghostscript(pdf_path, printer_name):
"""
Print PDF via GhostScript mswinpr2 device for true vector quality.
GhostScript sends native vector data to the printer driver, avoiding
the low-DPI rasterisation that causes dotted/pixelated output.
Returns True on success, False if GhostScript is unavailable or fails.
"""
gs_path = find_ghostscript()
if not gs_path:
print("GhostScript not found skipping to SumatraPDF fallback.")
return False
# Build environment: if running as a bundled exe, point GS_LIB at the
# extracted lib/ folder so GhostScript can find its .ps init files.
env = os.environ.copy()
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
bundled_lib = os.path.join(sys._MEIPASS, 'ghostscript', 'lib')
if os.path.isdir(bundled_lib):
env['GS_LIB'] = bundled_lib
print(f"GS_LIB → {bundled_lib}")
try:
# -sDEVICE=mswinpr2 : native Windows printer device (vector path)
# -dNOPAUSE / -dBATCH : batch mode, no user prompts
# -r600 : 600 DPI matches typical thermal-printer head
# -dTextAlphaBits=4 : anti-aliasing for text
# -dGraphicsAlphaBits=4
# -dNOSAFER : allow file access needed for fonts
# The printer paper size and orientation are already configured
# in the driver do not override them here.
cmd = [
gs_path,
"-dNOPAUSE",
"-dBATCH",
"-sDEVICE=mswinpr2",
"-dNOSAFER",
"-r600",
"-dTextAlphaBits=4",
"-dGraphicsAlphaBits=4",
f"-sOutputFile=%printer%{printer_name}",
pdf_path,
]
print(f"Printing with GhostScript (vector quality) to: {printer_name}")
result = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
timeout=60,
env=env,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
)
print(f"✅ Label sent via GhostScript: {printer_name}")
return True
except subprocess.CalledProcessError as e:
print(f"GhostScript print failed (exit {e.returncode}): {e.stderr.strip() if e.stderr else ''}")
return False
except Exception as e:
print(f"GhostScript error: {e}")
return False
def print_to_printer(printer_name, file_path):
"""
Print file to printer (cross-platform).
@@ -261,28 +171,13 @@ def print_to_printer(printer_name, file_path):
elif SYSTEM == "Windows":
# Windows: Print PDF silently without any viewer opening
try:
if WIN32_AVAILABLE:
import win32print
if file_path.endswith('.pdf'):
# Try silent printing methods (no viewer opens)
import os
printed = False
# Method 1: GhostScript (best quality true vector path).
# Tried first regardless of whether we are running as a
# script or as the packaged .exe, because GhostScript is
# a system-level installation and will be present on the
# machine independently of how this app is launched.
# If GhostScript is not installed it returns False
# immediately and we fall through to SumatraPDF.
printed = print_pdf_with_ghostscript(file_path, printer_name)
if printed:
return True
# Method 2: SumatraPDF (bundled inside exe or external)
# Method 1: SumatraPDF sends the PDF at its exact page size
# (35 mm × 25 mm) directly to the printer driver with no scaling.
# The printer must already have a 35 mm × 25 mm label stock
# configured in its driver settings.
sumatra_paths = []
# Get the directory where this script/exe is running
@@ -318,20 +213,47 @@ def print_to_printer(printer_name, file_path):
try:
print(f"Using SumatraPDF: {sumatra_path}")
print(f"Sending to printer: {printer_name}")
# "noscale" = print the PDF at its exact page size.
# Do NOT add "landscape" the printer driver
# already knows the orientation from its own settings.
result = subprocess.run([
# Locate the conf folder that contains
# SumatraPDF-settings.txt so SumatraPDF reads
# our pre-configured PrintScale = noscale.
sumatra_dir = os.path.dirname(sumatra_path)
settings_candidates = [
# conf\ next to the exe (script mode)
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf'),
# same folder as SumatraPDF.exe
sumatra_dir,
]
if getattr(sys, 'frozen', False):
settings_candidates.insert(0, os.path.join(os.path.dirname(sys.executable), 'conf'))
appdata_dir = next(
(d for d in settings_candidates
if os.path.exists(os.path.join(d, 'SumatraPDF-settings.txt'))),
None
)
# Build command:
# -print-settings noscale : send PDF at exact page size (35x25 mm)
# -appdata-dir : use our settings file (PrintScale=noscale)
# -silent / -exit-when-done : no UI, exit after spooling
# IMPORTANT: file_path must come LAST
cmd = [
sumatra_path,
"-print-to",
printer_name,
file_path,
"-print-settings",
"noscale",
"-print-to", printer_name,
"-print-settings", "noscale",
"-silent",
"-exit-when-done"
], check=False, creationflags=subprocess.CREATE_NO_WINDOW,
capture_output=True, text=True, timeout=30)
"-exit-when-done",
]
if appdata_dir:
cmd += ["-appdata-dir", appdata_dir]
print(f"SumatraPDF appdata-dir: {appdata_dir}")
cmd.append(file_path)
result = subprocess.run(
cmd,
check=False,
creationflags=subprocess.CREATE_NO_WINDOW,
capture_output=True, text=True, timeout=30)
if result.returncode != 0:
print(f"SumatraPDF returned code {result.returncode}")
if result.stderr:
@@ -343,9 +265,32 @@ def print_to_printer(printer_name, file_path):
except Exception as e:
print(f"SumatraPDF error: {e}")
# Do not launch default PDF viewers (Adobe/Edge/etc.) as fallback.
# Method 2: win32api ShellExecute "print" verb last resort,
# uses whatever PDF application is registered as the default
# handler. This may briefly open the viewer, but it will
# send the job to the correct physical printer.
if not printed and WIN32_AVAILABLE:
try:
import win32api
print(f"Trying win32api ShellExecute print fallback → {printer_name}")
# Set the target printer as default temporarily is
# unreliable; instead, rely on the user having already
# selected the right default printer in Windows.
win32api.ShellExecute(
0, # hwnd
"print", # operation
file_path, # file
None, # parameters
".", # working dir
0 # show-command (SW_HIDE)
)
print("Label sent via ShellExecute print fallback.")
printed = True
except Exception as se_err:
print(f"win32api ShellExecute fallback failed: {se_err}")
if not printed:
print("SumatraPDF not found or failed. PDF saved as backup only (no viewer launched).")
print("All print methods failed. PDF saved as backup only.")
return False
return True