This commit is contained in:
2026-02-23 17:07:00 +02:00
parent cbf324eb76
commit 7a77199fcf
7 changed files with 433 additions and 92 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ label/
build/
logs/
pdf_backup/
conf/ghostscript/
venv/
.venv/
__pycache__/

View File

@@ -1,12 +1,70 @@
# -*- mode: python ; coding: utf-8 -*-
#
# LabelPrinter PyInstaller spec file.
#
# GhostScript bundling
# --------------------
# Run build_windows.bat (or prepare_ghostscript.bat) BEFORE pyinstaller.
# That script copies the following directories from the system
# GhostScript installation into conf\ghostscript\:
#
# conf\ghostscript\bin\ <- gswin64c.exe + gsdll64.dll
# conf\ghostscript\lib\ <- all .ps init files
#
# This spec detects those directories automatically and adds them to the
# bundle under ghostscript\bin\ and ghostscript\lib\ inside _MEIPASS so
# that print_label.py can find and launch the bundled executable.
# If the directories are absent the build still succeeds (prints will fall
# back to SumatraPDF on the target machine).
import os
import glob as _glob
# ── GhostScript: collect binaries and lib init files ──────────────────────
_gs_bin_src = os.path.join('conf', 'ghostscript', 'bin')
_gs_lib_src = os.path.join('conf', 'ghostscript', 'lib')
gs_binaries = [] # (src_path, dest_folder_in_bundle)
gs_datas = [] # (src_path, dest_folder_in_bundle)
if os.path.isdir(_gs_bin_src):
for _fn in ['gswin64c.exe', 'gsdll64.dll', 'gswin32c.exe', 'gsdll32.dll']:
_fp = os.path.join(_gs_bin_src, _fn)
if os.path.exists(_fp):
# Put executables/DLLs in binaries so PyInstaller handles deps
gs_binaries.append((_fp, 'ghostscript/bin'))
print(f'[spec] Bundling GhostScript binaries from: {_gs_bin_src}')
else:
print('[spec] WARNING: conf/ghostscript/bin not found.'
' GhostScript will NOT be bundled in the exe.')
if os.path.isdir(_gs_lib_src):
for _fp in _glob.glob(os.path.join(_gs_lib_src, '*')):
if os.path.isfile(_fp):
gs_datas.append((_fp, 'ghostscript/lib'))
print(f'[spec] Bundling GhostScript lib from: {_gs_lib_src}')
# ── Base data files ────────────────────────────────────────────────────────
base_datas = [
('conf\\SumatraPDF.exe', '.'),
('conf\\SumatraPDF-settings.txt', 'conf'),
('conf\\label_template_ok.svg', 'conf'),
('conf\\label_template_nok.svg', 'conf'),
('conf\\label_template.svg', 'conf'),
]
a = Analysis(
['label_printer_gui.py'],
pathex=[],
binaries=[],
datas=[('conf\\SumatraPDF.exe', '.'), ('conf\\SumatraPDF-settings.txt', 'conf'), ('conf\\label_template_ok.svg', 'conf'), ('conf\\label_template_nok.svg', 'conf'), ('conf\\label_template.svg', 'conf')],
hiddenimports=['kivy', 'PIL', 'barcode', 'reportlab', 'print_label', 'print_label_pdf', 'svglib', 'cairosvg', 'watchdog', 'watchdog.observers', 'watchdog.events', 'pystray', 'win32timezone'],
binaries=gs_binaries,
datas=base_datas + gs_datas,
hiddenimports=[
'kivy', 'PIL', 'barcode', 'reportlab',
'print_label', 'print_label_pdf',
'svglib', 'cairosvg',
'watchdog', 'watchdog.observers', 'watchdog.events',
'pystray', 'win32timezone',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],

View File

@@ -1,6 +1,6 @@
"""
PyInstaller build script for Label Printer GUI
Run this to create a standalone Windows executable
Run this to create a standalone Windows executable.
IMPORTANT: This script MUST be run on Windows to generate a Windows .exe file.
If run on Linux/macOS, it will create a Linux/macOS binary that won't work on Windows.
@@ -10,73 +10,133 @@ To build for Windows:
2. Install dependencies: pip install -r requirements_windows.txt
3. Run this script: python build_exe.py
4. The Windows .exe will be created in the dist/ folder
GhostScript bundling
--------------------
If GhostScript is installed on this build machine the script will
automatically copy the required files into conf\\ghostscript\\ before
calling PyInstaller so they are embedded in LabelPrinter.exe.
The target machine then needs NO separate GhostScript install.
"""
import os
import sys
import subprocess
import shutil
# Get the current directory
script_dir = os.path.dirname(os.path.abspath(__file__))
# PyInstaller arguments
args = [
'label_printer_gui.py',
'--onefile', # Create a single executable
'--windowed', # Don't show console window
'--name=LabelPrinter', # Executable name
'--distpath=./dist', # Output directory
'--workpath=./build', # Work directory (was --buildpath)
'--hidden-import=kivy',
'--hidden-import=kivy.core.window',
'--hidden-import=kivy.core.text',
'--hidden-import=kivy.core.image',
'--hidden-import=kivy.uix.boxlayout',
'--hidden-import=kivy.uix.gridlayout',
'--hidden-import=kivy.uix.label',
'--hidden-import=kivy.uix.textinput',
'--hidden-import=kivy.uix.button',
'--hidden-import=kivy.uix.spinner',
'--hidden-import=kivy.uix.scrollview',
'--hidden-import=kivy.uix.popup',
'--hidden-import=kivy.clock',
'--hidden-import=kivy.graphics',
'--hidden-import=PIL',
'--hidden-import=barcode',
'--hidden-import=reportlab',
'--hidden-import=print_label',
'--hidden-import=print_label_pdf',
'--hidden-import=svglib',
'--hidden-import=cairosvg',
'--hidden-import=watchdog',
'--hidden-import=watchdog.observers',
'--hidden-import=watchdog.events',
'--hidden-import=pystray',
'--hidden-import=win32timezone',
]
def prepare_ghostscript():
"""
Find the system GhostScript installation and copy the minimum files
needed for bundling into conf\\ghostscript\\.
Files copied:
conf\\ghostscript\\bin\\gswin64c.exe (or 32-bit variant)
conf\\ghostscript\\bin\\gsdll64.dll
conf\\ghostscript\\lib\\*.ps (PostScript init files)
Returns True if GhostScript was found and prepared, False otherwise.
"""
import glob
gs_exe = None
gs_dll = None
gs_lib = None
for pf in [r"C:\Program Files", r"C:\Program Files (x86)"]:
gs_base = os.path.join(pf, "gs")
if not os.path.isdir(gs_base):
continue
# Iterate versions newest-first
for ver_dir in sorted(os.listdir(gs_base), reverse=True):
bin_dir = os.path.join(gs_base, ver_dir, "bin")
lib_dir = os.path.join(gs_base, ver_dir, "lib")
if os.path.exists(os.path.join(bin_dir, "gswin64c.exe")):
gs_exe = os.path.join(bin_dir, "gswin64c.exe")
gs_dll = os.path.join(bin_dir, "gsdll64.dll")
gs_lib = lib_dir
break
if os.path.exists(os.path.join(bin_dir, "gswin32c.exe")):
gs_exe = os.path.join(bin_dir, "gswin32c.exe")
gs_dll = os.path.join(bin_dir, "gsdll32.dll")
gs_lib = lib_dir
break
if gs_exe:
break
if not gs_exe:
print(" WARNING: GhostScript not found on this machine.")
print(" The exe will still build but GhostScript will NOT be bundled.")
print(" Install GhostScript from https://ghostscript.com/releases/gsdnld.html")
print(" then rebuild for sharp vector-quality printing.")
return False
dest_bin = os.path.join("conf", "ghostscript", "bin")
dest_lib = os.path.join("conf", "ghostscript", "lib")
os.makedirs(dest_bin, exist_ok=True)
os.makedirs(dest_lib, exist_ok=True)
print(f" GhostScript found: {os.path.dirname(gs_exe)}")
shutil.copy2(gs_exe, dest_bin)
print(f" Copied: {os.path.basename(gs_exe)}")
if os.path.exists(gs_dll):
shutil.copy2(gs_dll, dest_bin)
print(f" Copied: {os.path.basename(gs_dll)}")
count = 0
for ps_file in glob.glob(os.path.join(gs_lib, "*.ps")):
shutil.copy2(ps_file, dest_lib)
count += 1
print(f" Copied {count} .ps init files from lib/")
print(f" GhostScript prepared in conf\\ghostscript\\")
return True
if __name__ == '__main__':
print("=" * 60)
print("Label Printer GUI - PyInstaller Build")
print("=" * 60)
print("\nBuilding standalone executable...")
print("This may take a few minutes...\n")
# Change to script directory
# Change to script directory so relative paths work
os.chdir(script_dir)
# Run PyInstaller directly with subprocess for better error reporting
# Step 1: Prepare GhostScript for bundling (Windows only)
if sys.platform == "win32":
print("\n[1/2] Preparing GhostScript for bundling...")
prepare_ghostscript()
else:
print("\n[1/2] Skipping GhostScript prep (non-Windows build machine)")
# Step 2: Build with PyInstaller using the handcrafted spec file.
# The spec's Python code auto-detects conf\\ghostscript\\ and includes it.
print("\n[2/2] Building standalone executable via LabelPrinter.spec...")
print("This may take a few minutes...\n")
try:
result = subprocess.run(['pyinstaller'] + args, check=True)
result = subprocess.run(
['pyinstaller', 'LabelPrinter.spec',
'--distpath=./dist', '--workpath=./build', '-y'],
check=True,
)
print("\n" + "=" * 60)
print("Build Complete!")
print("=" * 60)
print("\nExecutable location: ./dist/LabelPrinter.exe")
print("\nBundled components:")
print(" - GhostScript (vector-quality printing)")
print(" - SumatraPDF (fallback printing)")
print(" - SVG templates, conf files")
print("\nYou can now:")
print(" 1. Double-click LabelPrinter.exe to run")
print("2. Share the exe with others")
print("3. Create a shortcut on desktop")
print(" 2. Copy the dist\\ folder to target machines")
print(" 3. No extra software installation required on target machines")
print("\nNote: First run may take a moment as Kivy initializes")
except subprocess.CalledProcessError as e:
print("\n" + "=" * 60)
@@ -88,3 +148,4 @@ if __name__ == '__main__':
except Exception as e:
print(f"\nFatal error: {e}")
sys.exit(1)

View File

@@ -38,7 +38,7 @@ if errorlevel 1 (
echo.
REM Install dependencies
echo [3/5] Installing dependencies...
echo [3/6] Installing dependencies...
echo Installing: python-barcode, pillow, reportlab, kivy, pyinstaller, pywin32, wmi, watchdog, svglib, cairosvg, pystray...
pip install python-barcode pillow reportlab kivy pyinstaller pywin32 wmi watchdog svglib cairosvg pystray
if errorlevel 1 (
@@ -48,6 +48,11 @@ if errorlevel 1 (
)
echo.
REM Copy GhostScript binaries for bundling (optional but strongly recommended)
echo [4/6] Preparing GhostScript for bundling...
call prepare_ghostscript.bat
echo.
REM Check that SumatraPDF.exe exists before building
if not exist "conf\SumatraPDF.exe" (
echo ERROR: conf\SumatraPDF.exe not found!
@@ -59,43 +64,20 @@ if not exist "conf\SumatraPDF.exe" (
echo conf\SumatraPDF.exe found - will be bundled into executable.
echo.
REM Clean old build
echo [4/5] Cleaning old build artifacts...
REM Clean old build (keep the spec file - it's handcrafted)
echo [5/6] Cleaning old build artifacts...
if exist "dist" rmdir /s /q dist
if exist "build" rmdir /s /q build
if exist "*.spec" del *.spec
echo.
REM Build with PyInstaller
echo [5/5] Building executable with PyInstaller...
REM Build with PyInstaller using the spec file.
REM The spec (LabelPrinter.spec) contains Python code that detects whether
REM conf\ghostscript\ was prepared and includes those files automatically.
echo [6/6] Building executable with PyInstaller...
echo This may take 5-15 minutes, please wait...
echo.
pyinstaller label_printer_gui.py ^
--onefile ^
--windowed ^
--name=LabelPrinter ^
--distpath=./dist ^
--workpath=./build ^
--hidden-import=kivy ^
--hidden-import=PIL ^
--hidden-import=barcode ^
--hidden-import=reportlab ^
--hidden-import=print_label ^
--hidden-import=print_label_pdf ^
--hidden-import=svglib ^
--hidden-import=cairosvg ^
--hidden-import=watchdog ^
--hidden-import=watchdog.observers ^
--hidden-import=watchdog.events ^
--hidden-import=pystray ^
--hidden-import=win32timezone ^
--add-data "conf\SumatraPDF.exe;." ^
--add-data "conf\SumatraPDF-settings.txt;conf" ^
--add-data "conf\label_template_ok.svg;conf" ^
--add-data "conf\label_template_nok.svg;conf" ^
--add-data "conf\label_template.svg;conf" ^
-y
pyinstaller LabelPrinter.spec --distpath=./dist --workpath=./build -y
if errorlevel 1 (
echo.
@@ -129,7 +111,8 @@ echo Next steps:
echo 1. Copy the entire dist\ folder to the target machine
echo (it contains LabelPrinter.exe AND the conf\ folder)
echo 2. Run LabelPrinter.exe from the dist folder
echo 3. Ensure conf\SumatraPDF.exe is present for printing to work
echo 3. GhostScript is bundled inside the exe for sharp vector printing
echo 4. SumatraPDF (conf\SumatraPDF.exe) is also bundled as fallback
echo.
echo Note: First run may take a moment as Kivy initializes
echo.

BIN
dist/LabelPrinter.exe vendored

Binary file not shown.

117
prepare_ghostscript.bat Normal file
View File

@@ -0,0 +1,117 @@
@echo off
REM ============================================================
REM prepare_ghostscript.bat
REM Finds the system GhostScript installation and copies the
REM files needed for bundling into conf\ghostscript\
REM
REM Files copied:
REM conf\ghostscript\bin\gswin64c.exe (or gswin32c.exe)
REM conf\ghostscript\bin\gsdll64.dll (or gsdll32.dll)
REM conf\ghostscript\lib\*.ps (PostScript init files)
REM
REM Run this ONCE before building the exe with build_windows.bat.
REM Only the two executables + the lib/ folder are needed (~15-20 MB).
REM The full GhostScript fonts/Resources are NOT bundled to keep
REM the exe size manageable; Windows system fonts are used instead.
REM ============================================================
setlocal enabledelayedexpansion
set GS_FOUND=0
set GS_BIN_SRC=
set GS_LIB_SRC=
set GS_EXE=
echo.
echo ============================================================
echo GhostScript Bundle Preparation
echo ============================================================
echo.
REM ---- Search for GhostScript in both Program Files locations ----
for %%P in ("%ProgramFiles%" "%ProgramFiles(x86)%") do (
if exist "%%~P\gs" (
for /d %%V in ("%%~P\gs\gs*") do (
if exist "%%~V\bin\gswin64c.exe" (
set GS_BIN_SRC=%%~V\bin
set GS_LIB_SRC=%%~V\lib
set GS_EXE=gswin64c.exe
set GS_DLL=gsdll64.dll
set GS_FOUND=1
echo Found GhostScript (64-bit): %%~V
)
if !GS_FOUND!==0 (
if exist "%%~V\bin\gswin32c.exe" (
set GS_BIN_SRC=%%~V\bin
set GS_LIB_SRC=%%~V\lib
set GS_EXE=gswin32c.exe
set GS_DLL=gsdll32.dll
set GS_FOUND=1
echo Found GhostScript (32-bit): %%~V
)
)
)
)
)
if %GS_FOUND%==0 (
echo.
echo WARNING: GhostScript is NOT installed on this machine.
echo.
echo The exe will still build successfully, but GhostScript will
echo NOT be bundled. On the target machine the app will fall back
echo to SumatraPDF for printing (which may produce dotted output).
echo.
echo To get sharp vector-quality printing, install GhostScript:
echo https://ghostscript.com/releases/gsdnld.html
echo Then re-run this script before building.
echo.
exit /b 0
)
REM ---- Create destination folders ----
set GS_DEST_BIN=conf\ghostscript\bin
set GS_DEST_LIB=conf\ghostscript\lib
if not exist "%GS_DEST_BIN%" mkdir "%GS_DEST_BIN%"
if not exist "%GS_DEST_LIB%" mkdir "%GS_DEST_LIB%"
REM ---- Copy executable and DLL ----
echo.
echo Copying GhostScript binaries...
copy /y "%GS_BIN_SRC%\%GS_EXE%" "%GS_DEST_BIN%\%GS_EXE%" >nul
if errorlevel 1 ( echo ERROR copying %GS_EXE% & exit /b 1 )
echo + %GS_EXE%
copy /y "%GS_BIN_SRC%\%GS_DLL%" "%GS_DEST_BIN%\%GS_DLL%" >nul
if errorlevel 1 ( echo ERROR copying %GS_DLL% & exit /b 1 )
echo + %GS_DLL%
REM ---- Copy lib/ (PostScript init files .ps) ----
echo.
echo Copying GhostScript lib (PostScript init files)...
for %%F in ("%GS_LIB_SRC%\*.ps") do (
copy /y "%%F" "%GS_DEST_LIB%\" >nul
)
echo + *.ps copied from %GS_LIB_SRC%
REM ---- Report ----
echo.
echo ============================================================
echo GhostScript prepared successfully in conf\ghostscript\
echo ============================================================
echo.
REM Show size summary
set BIN_COUNT=0
set LIB_COUNT=0
for %%F in ("%GS_DEST_BIN%\*") do set /a BIN_COUNT+=1
for %%F in ("%GS_DEST_LIB%\*") do set /a LIB_COUNT+=1
echo bin\ : %BIN_COUNT% file(s)
echo lib\ : %LIB_COUNT% file(s)
echo.
echo GhostScript will be embedded into LabelPrinter.exe at build time.
echo.
endlocal
exit /b 0

View File

@@ -308,9 +308,13 @@ def configure_printer_quality(printer_name, width_mm=35, height_mm=25):
except:
pass
# Set orientation to landscape
# Set orientation to PORTRAIT (1 = no rotation).
# For a 35mm × 25mm thermal label, Portrait means "print across the
# 35mm print-head width without rotating". Landscape (2) would
# rotate the output 90° CCW, which is exactly the reported
# "rotated-left" symptom so we must NOT use Landscape here.
try:
devmode.Orientation = 2 # Landscape
devmode.Orientation = 1 # Portrait = no rotation
except:
pass
@@ -343,6 +347,101 @@ def configure_printer_quality(printer_name, width_mm=35, height_mm=25):
return False
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
# -dDEVICEWIDTHPOINTS \
# -dDEVICEHEIGHTPOINTS : explicit label size (35mm x 25mm in pts)
# -dFIXEDMEDIA : do not auto-scale/rotate to a different size
# 35mm = 35/25.4*72 ≈ 99.21 pt, 25mm = 25/25.4*72 ≈ 70.87 pt
cmd = [
gs_path,
"-dNOPAUSE",
"-dBATCH",
"-sDEVICE=mswinpr2",
"-dNOSAFER",
"-r600",
"-dTextAlphaBits=4",
"-dGraphicsAlphaBits=4",
"-dDEVICEWIDTHPOINTS=99.21", # 35 mm
"-dDEVICEHEIGHTPOINTS=70.87", # 25 mm
"-dFIXEDMEDIA",
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).
@@ -375,6 +474,13 @@ def print_to_printer(printer_name, file_path):
import win32print
import win32api
# Configure printer DEVMODE BEFORE sending any job.
# This is critical: it sets Portrait orientation (no rotation)
# and maximum print quality so the 35mm×25mm PDF maps
# directly to the physical label without being auto-rotated
# by the driver (which caused the 90° "rotated left" symptom).
configure_printer_quality(printer_name)
if file_path.endswith('.pdf'):
# Try silent printing methods (no viewer opens)
import os
@@ -382,7 +488,19 @@ def print_to_printer(printer_name, file_path):
printed = False
# Method 1: SumatraPDF (bundled inside exe or external)
# 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)
sumatra_paths = []
# Get the directory where this script/exe is running
@@ -418,11 +536,14 @@ def print_to_printer(printer_name, file_path):
try:
print(f"Using SumatraPDF: {sumatra_path}")
print(f"Sending to printer: {printer_name}")
# IMPORTANT: The PDF is already landscape (35mm wide x 25mm tall).
# Do NOT add "landscape" to print-settings — it would rotate an
# already-landscape PDF 90° again, printing it portrait.
# "noscale" alone tells SumatraPDF to honour the PDF page size
# exactly and not fit/stretch it to the printer paper.
# The printer DEVMODE has already been configured
# above (Portrait, 35mm×25mm, high quality).
# "noscale" tells SumatraPDF to send the PDF
# at its exact size without any shrink/fit.
# Do NOT add "landscape" here: the DEVMODE
# Portrait setting already matches the label
# orientation; adding landscape would tell the
# driver to rotate 90° again and undo the fix.
result = subprocess.run([
sumatra_path,
"-print-to",