updated
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@ label/
|
|||||||
build/
|
build/
|
||||||
logs/
|
logs/
|
||||||
pdf_backup/
|
pdf_backup/
|
||||||
|
conf/ghostscript/
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
.venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -1,12 +1,70 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
# -*- 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(
|
a = Analysis(
|
||||||
['label_printer_gui.py'],
|
['label_printer_gui.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=gs_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')],
|
datas=base_datas + gs_datas,
|
||||||
hiddenimports=['kivy', 'PIL', 'barcode', 'reportlab', 'print_label', 'print_label_pdf', 'svglib', 'cairosvg', 'watchdog', 'watchdog.observers', 'watchdog.events', 'pystray', 'win32timezone'],
|
hiddenimports=[
|
||||||
|
'kivy', 'PIL', 'barcode', 'reportlab',
|
||||||
|
'print_label', 'print_label_pdf',
|
||||||
|
'svglib', 'cairosvg',
|
||||||
|
'watchdog', 'watchdog.observers', 'watchdog.events',
|
||||||
|
'pystray', 'win32timezone',
|
||||||
|
],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
|
|||||||
149
build_exe.py
149
build_exe.py
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
PyInstaller build script for Label Printer GUI
|
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.
|
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.
|
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
|
2. Install dependencies: pip install -r requirements_windows.txt
|
||||||
3. Run this script: python build_exe.py
|
3. Run this script: python build_exe.py
|
||||||
4. The Windows .exe will be created in the dist/ folder
|
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 os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
|
||||||
# Get the current directory
|
# Get the current directory
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
# PyInstaller arguments
|
|
||||||
args = [
|
def prepare_ghostscript():
|
||||||
'label_printer_gui.py',
|
"""
|
||||||
'--onefile', # Create a single executable
|
Find the system GhostScript installation and copy the minimum files
|
||||||
'--windowed', # Don't show console window
|
needed for bundling into conf\\ghostscript\\.
|
||||||
'--name=LabelPrinter', # Executable name
|
|
||||||
'--distpath=./dist', # Output directory
|
Files copied:
|
||||||
'--workpath=./build', # Work directory (was --buildpath)
|
conf\\ghostscript\\bin\\gswin64c.exe (or 32-bit variant)
|
||||||
'--hidden-import=kivy',
|
conf\\ghostscript\\bin\\gsdll64.dll
|
||||||
'--hidden-import=kivy.core.window',
|
conf\\ghostscript\\lib\\*.ps (PostScript init files)
|
||||||
'--hidden-import=kivy.core.text',
|
|
||||||
'--hidden-import=kivy.core.image',
|
Returns True if GhostScript was found and prepared, False otherwise.
|
||||||
'--hidden-import=kivy.uix.boxlayout',
|
"""
|
||||||
'--hidden-import=kivy.uix.gridlayout',
|
import glob
|
||||||
'--hidden-import=kivy.uix.label',
|
|
||||||
'--hidden-import=kivy.uix.textinput',
|
gs_exe = None
|
||||||
'--hidden-import=kivy.uix.button',
|
gs_dll = None
|
||||||
'--hidden-import=kivy.uix.spinner',
|
gs_lib = None
|
||||||
'--hidden-import=kivy.uix.scrollview',
|
|
||||||
'--hidden-import=kivy.uix.popup',
|
for pf in [r"C:\Program Files", r"C:\Program Files (x86)"]:
|
||||||
'--hidden-import=kivy.clock',
|
gs_base = os.path.join(pf, "gs")
|
||||||
'--hidden-import=kivy.graphics',
|
if not os.path.isdir(gs_base):
|
||||||
'--hidden-import=PIL',
|
continue
|
||||||
'--hidden-import=barcode',
|
# Iterate versions newest-first
|
||||||
'--hidden-import=reportlab',
|
for ver_dir in sorted(os.listdir(gs_base), reverse=True):
|
||||||
'--hidden-import=print_label',
|
bin_dir = os.path.join(gs_base, ver_dir, "bin")
|
||||||
'--hidden-import=print_label_pdf',
|
lib_dir = os.path.join(gs_base, ver_dir, "lib")
|
||||||
'--hidden-import=svglib',
|
if os.path.exists(os.path.join(bin_dir, "gswin64c.exe")):
|
||||||
'--hidden-import=cairosvg',
|
gs_exe = os.path.join(bin_dir, "gswin64c.exe")
|
||||||
'--hidden-import=watchdog',
|
gs_dll = os.path.join(bin_dir, "gsdll64.dll")
|
||||||
'--hidden-import=watchdog.observers',
|
gs_lib = lib_dir
|
||||||
'--hidden-import=watchdog.events',
|
break
|
||||||
'--hidden-import=pystray',
|
if os.path.exists(os.path.join(bin_dir, "gswin32c.exe")):
|
||||||
'--hidden-import=win32timezone',
|
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__':
|
if __name__ == '__main__':
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("Label Printer GUI - PyInstaller Build")
|
print("Label Printer GUI - PyInstaller Build")
|
||||||
print("=" * 60)
|
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)
|
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:
|
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("\n" + "=" * 60)
|
||||||
print("Build Complete!")
|
print("Build Complete!")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("\nExecutable location: ./dist/LabelPrinter.exe")
|
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("\nYou can now:")
|
||||||
print("1. Double-click LabelPrinter.exe to run")
|
print(" 1. Double-click LabelPrinter.exe to run")
|
||||||
print("2. Share the exe with others")
|
print(" 2. Copy the dist\\ folder to target machines")
|
||||||
print("3. Create a shortcut on desktop")
|
print(" 3. No extra software installation required on target machines")
|
||||||
print("\nNote: First run may take a moment as Kivy initializes")
|
print("\nNote: First run may take a moment as Kivy initializes")
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
@@ -88,3 +148,4 @@ if __name__ == '__main__':
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\nFatal error: {e}")
|
print(f"\nFatal error: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ if errorlevel 1 (
|
|||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Install dependencies
|
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...
|
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
|
pip install python-barcode pillow reportlab kivy pyinstaller pywin32 wmi watchdog svglib cairosvg pystray
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -48,6 +48,11 @@ if errorlevel 1 (
|
|||||||
)
|
)
|
||||||
echo.
|
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
|
REM Check that SumatraPDF.exe exists before building
|
||||||
if not exist "conf\SumatraPDF.exe" (
|
if not exist "conf\SumatraPDF.exe" (
|
||||||
echo ERROR: conf\SumatraPDF.exe not found!
|
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 conf\SumatraPDF.exe found - will be bundled into executable.
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Clean old build
|
REM Clean old build (keep the spec file - it's handcrafted)
|
||||||
echo [4/5] Cleaning old build artifacts...
|
echo [5/6] Cleaning old build artifacts...
|
||||||
if exist "dist" rmdir /s /q dist
|
if exist "dist" rmdir /s /q dist
|
||||||
if exist "build" rmdir /s /q build
|
if exist "build" rmdir /s /q build
|
||||||
if exist "*.spec" del *.spec
|
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Build with PyInstaller
|
REM Build with PyInstaller using the spec file.
|
||||||
echo [5/5] Building executable with PyInstaller...
|
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 This may take 5-15 minutes, please wait...
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
pyinstaller label_printer_gui.py ^
|
pyinstaller LabelPrinter.spec --distpath=./dist --workpath=./build -y
|
||||||
--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
|
|
||||||
|
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo.
|
echo.
|
||||||
@@ -129,7 +111,8 @@ echo Next steps:
|
|||||||
echo 1. Copy the entire dist\ folder to the target machine
|
echo 1. Copy the entire dist\ folder to the target machine
|
||||||
echo (it contains LabelPrinter.exe AND the conf\ folder)
|
echo (it contains LabelPrinter.exe AND the conf\ folder)
|
||||||
echo 2. Run LabelPrinter.exe from the dist 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.
|
||||||
echo Note: First run may take a moment as Kivy initializes
|
echo Note: First run may take a moment as Kivy initializes
|
||||||
echo.
|
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:
|
except:
|
||||||
pass
|
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:
|
try:
|
||||||
devmode.Orientation = 2 # Landscape
|
devmode.Orientation = 1 # Portrait = no rotation
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -343,6 +347,101 @@ def configure_printer_quality(printer_name, width_mm=35, height_mm=25):
|
|||||||
return False
|
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):
|
def print_to_printer(printer_name, file_path):
|
||||||
"""
|
"""
|
||||||
Print file to printer (cross-platform).
|
Print file to printer (cross-platform).
|
||||||
@@ -375,6 +474,13 @@ def print_to_printer(printer_name, file_path):
|
|||||||
import win32print
|
import win32print
|
||||||
import win32api
|
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'):
|
if file_path.endswith('.pdf'):
|
||||||
# Try silent printing methods (no viewer opens)
|
# Try silent printing methods (no viewer opens)
|
||||||
import os
|
import os
|
||||||
@@ -382,7 +488,19 @@ def print_to_printer(printer_name, file_path):
|
|||||||
|
|
||||||
printed = False
|
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 = []
|
sumatra_paths = []
|
||||||
|
|
||||||
# Get the directory where this script/exe is running
|
# Get the directory where this script/exe is running
|
||||||
@@ -418,11 +536,14 @@ def print_to_printer(printer_name, file_path):
|
|||||||
try:
|
try:
|
||||||
print(f"Using SumatraPDF: {sumatra_path}")
|
print(f"Using SumatraPDF: {sumatra_path}")
|
||||||
print(f"Sending to printer: {printer_name}")
|
print(f"Sending to printer: {printer_name}")
|
||||||
# IMPORTANT: The PDF is already landscape (35mm wide x 25mm tall).
|
# The printer DEVMODE has already been configured
|
||||||
# Do NOT add "landscape" to print-settings — it would rotate an
|
# above (Portrait, 35mm×25mm, high quality).
|
||||||
# already-landscape PDF 90° again, printing it portrait.
|
# "noscale" tells SumatraPDF to send the PDF
|
||||||
# "noscale" alone tells SumatraPDF to honour the PDF page size
|
# at its exact size without any shrink/fit.
|
||||||
# exactly and not fit/stretch it to the printer paper.
|
# 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([
|
result = subprocess.run([
|
||||||
sumatra_path,
|
sumatra_path,
|
||||||
"-print-to",
|
"-print-to",
|
||||||
|
|||||||
Reference in New Issue
Block a user