updated
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@ label/
|
||||
build/
|
||||
logs/
|
||||
pdf_backup/
|
||||
conf/ghostscript/
|
||||
venv/
|
||||
.venv/
|
||||
__pycache__/
|
||||
|
||||
@@ -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=[],
|
||||
|
||||
147
build_exe.py
147
build_exe.py
@@ -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)
|
||||
|
||||
|
||||
@@ -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
BIN
dist/LabelPrinter.exe
vendored
Binary file not shown.
117
prepare_ghostscript.bat
Normal file
117
prepare_ghostscript.bat
Normal 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
|
||||
137
print_label.py
137
print_label.py
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user