Compare commits
10 Commits
7437c547ec
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 71ccdb7b96 | |||
| be1b494527 | |||
| b35f278c1e | |||
| faef90f98b | |||
| 7197df9f5c | |||
| f67e1fb0da | |||
| f7833ed4b9 | |||
| 7a77199fcf | |||
| cbf324eb76 | |||
| 71558cc983 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,10 +2,13 @@ label/
|
||||
build/
|
||||
logs/
|
||||
pdf_backup/
|
||||
conf/ghostscript/
|
||||
venv/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
build_output.log
|
||||
.vscode/
|
||||
|
||||
|
||||
@@ -1,12 +1,71 @@
|
||||
# -*- 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', 'PIL.ImageWin', 'barcode', 'reportlab',
|
||||
'print_label', 'print_label_pdf',
|
||||
'fitz', 'pymupdf',
|
||||
'svglib', 'cairosvg',
|
||||
'watchdog', 'watchdog.observers', 'watchdog.events',
|
||||
'pystray', 'win32api', 'win32print', 'win32ui', 'win32con', 'win32timezone',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
|
||||
103
build_exe.py
103
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,6 +10,12 @@ 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
|
||||
|
||||
Printing method
|
||||
---------------
|
||||
SumatraPDF is bundled inside the exe and used for all printing.
|
||||
It sends the PDF at its exact 35x25 mm page size via -print-settings noscale.
|
||||
No GhostScript installation required on the build or target machine.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -19,64 +25,66 @@ import subprocess
|
||||
# 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 check_conf_folder():
|
||||
"""
|
||||
Verify that required files are present in the conf\ folder before building.
|
||||
Returns True if all required files exist, False otherwise.
|
||||
"""
|
||||
required = [
|
||||
os.path.join('conf', 'SumatraPDF.exe'),
|
||||
os.path.join('conf', 'SumatraPDF-settings.txt'),
|
||||
os.path.join('conf', 'label_template_ok.svg'),
|
||||
os.path.join('conf', 'label_template_nok.svg'),
|
||||
os.path.join('conf', 'label_template.svg'),
|
||||
]
|
||||
all_ok = True
|
||||
for f in required:
|
||||
if os.path.exists(f):
|
||||
print(f" OK: {f}")
|
||||
else:
|
||||
print(f" MISSING: {f}")
|
||||
all_ok = False
|
||||
return all_ok
|
||||
|
||||
|
||||
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: Verify conf\ folder contents
|
||||
print("\n[1/2] Checking conf\\ folder...")
|
||||
if not check_conf_folder():
|
||||
print("\nERROR: One or more required conf\\ files are missing.")
|
||||
print("Place SumatraPDF.exe and the SVG templates in the conf\\ folder, then retry.")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 2: Build with PyInstaller using the handcrafted spec file.
|
||||
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(" - SumatraPDF (primary printing, noscale 35x25 mm)")
|
||||
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(" 1. Double-click LabelPrinter.exe to run")
|
||||
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 +96,4 @@ if __name__ == '__main__':
|
||||
except Exception as e:
|
||||
print(f"\nFatal error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ if errorlevel 1 (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [1/5] Checking Python installation...
|
||||
echo [1/6] Checking Python installation...
|
||||
python --version
|
||||
echo.
|
||||
|
||||
REM Upgrade pip
|
||||
echo [2/5] Upgrading pip, setuptools, and wheel...
|
||||
echo [2/6] Upgrading pip, setuptools, and wheel...
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Failed to upgrade pip
|
||||
@@ -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...
|
||||
python prepare_ghostscript.py
|
||||
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.
|
||||
|
||||
@@ -1,20 +1,110 @@
|
||||
from reportlab.lib.pagesizes import landscape
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfgen import canvas
|
||||
"""
|
||||
check_pdf_size.py – verify that a PDF's page dimensions match 35 mm × 25 mm.
|
||||
|
||||
Usage:
|
||||
python check_pdf_size.py [path/to/label.pdf]
|
||||
|
||||
If no path is given the script operates on every PDF found in pdf_backup/.
|
||||
Exit code 0 = all OK, 1 = mismatch or error.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Check the test PDF file
|
||||
if os.path.exists('test_label.pdf'):
|
||||
file_size = os.path.getsize('test_label.pdf')
|
||||
print(f'test_label.pdf exists ({file_size} bytes)')
|
||||
print(f'Expected: 35mm x 25mm landscape (99.2 x 70.9 points)')
|
||||
print(f'')
|
||||
print(f'Open test_label.pdf in a PDF viewer to verify:')
|
||||
print(f' - Page size should be wider than tall')
|
||||
print(f' - Content should be correctly oriented')
|
||||
print(f'')
|
||||
print(f'In Adobe Reader: File > Properties > Description')
|
||||
print(f' Page size should show: 3.5 x 2.5 cm or 1.38 x 0.98 in')
|
||||
else:
|
||||
print('test_label.pdf not found')
|
||||
# ── Target dimensions ────────────────────────────────────────────────────────
|
||||
TARGET_W_MM = 35.0 # width (landscape, wider side)
|
||||
TARGET_H_MM = 25.0 # height
|
||||
TOLERANCE_MM = 0.5 # ± 0.5 mm is acceptable rounding from PDF viewers
|
||||
|
||||
PT_PER_MM = 72.0 / 25.4 # 1 mm in points
|
||||
|
||||
|
||||
def read_page_size_pt(pdf_path):
|
||||
"""
|
||||
Return (width_pt, height_pt) of the first page of *pdf_path*.
|
||||
Tries pypdf first, then pymupdf (fitz) as a fallback.
|
||||
Raises RuntimeError if neither library is available.
|
||||
"""
|
||||
# ── pypdf ────────────────────────────────────────────────────────────────
|
||||
try:
|
||||
from pypdf import PdfReader # type: ignore
|
||||
reader = PdfReader(pdf_path)
|
||||
page = reader.pages[0]
|
||||
w = float(page.mediabox.width)
|
||||
h = float(page.mediabox.height)
|
||||
return w, h
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# ── pymupdf (fitz) ───────────────────────────────────────────────────────
|
||||
try:
|
||||
import fitz # type: ignore
|
||||
doc = fitz.open(pdf_path)
|
||||
rect = doc[0].rect
|
||||
return rect.width, rect.height
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
raise RuntimeError(
|
||||
"Install pypdf or pymupdf:\n"
|
||||
" pip install pypdf\n"
|
||||
" pip install pymupdf"
|
||||
)
|
||||
|
||||
|
||||
def check_file(pdf_path):
|
||||
"""Print a pass/fail line for one PDF. Returns True if dimensions match."""
|
||||
if not os.path.exists(pdf_path):
|
||||
print(f" MISS {pdf_path} (file not found)")
|
||||
return False
|
||||
|
||||
try:
|
||||
w_pt, h_pt = read_page_size_pt(pdf_path)
|
||||
except Exception as e:
|
||||
print(f" ERR {pdf_path} ({e})")
|
||||
return False
|
||||
|
||||
w_mm = w_pt / PT_PER_MM
|
||||
h_mm = h_pt / PT_PER_MM
|
||||
|
||||
w_ok = abs(w_mm - TARGET_W_MM) <= TOLERANCE_MM
|
||||
h_ok = abs(h_mm - TARGET_H_MM) <= TOLERANCE_MM
|
||||
ok = w_ok and h_ok
|
||||
|
||||
status = "PASS" if ok else "FAIL"
|
||||
print(
|
||||
f" {status} {os.path.basename(pdf_path)}"
|
||||
f" {w_mm:.2f}×{h_mm:.2f} mm"
|
||||
f" (target {TARGET_W_MM}×{TARGET_H_MM} mm ±{TOLERANCE_MM} mm)"
|
||||
)
|
||||
return ok
|
||||
|
||||
|
||||
def main():
|
||||
targets = sys.argv[1:]
|
||||
|
||||
if not targets:
|
||||
backup_dir = os.path.join(os.path.dirname(__file__), "pdf_backup")
|
||||
if os.path.isdir(backup_dir):
|
||||
targets = [
|
||||
os.path.join(backup_dir, f)
|
||||
for f in sorted(os.listdir(backup_dir))
|
||||
if f.lower().endswith(".pdf")
|
||||
]
|
||||
if not targets:
|
||||
# fall back to test_label.pdf in cwd
|
||||
targets = ["test_label.pdf"]
|
||||
|
||||
print(f"Checking {len(targets)} PDF(s)…")
|
||||
results = [check_file(p) for p in targets]
|
||||
|
||||
total = len(results)
|
||||
passed = sum(results)
|
||||
failed = total - passed
|
||||
print(f"\n {passed}/{total} passed" + (f", {failed} FAILED" if failed else ""))
|
||||
sys.exit(0 if failed == 0 else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
BIN
dist/LabelPrinter.exe
vendored
BIN
dist/LabelPrinter.exe
vendored
Binary file not shown.
@@ -982,23 +982,13 @@ printer = PDF
|
||||
success = False
|
||||
all_success = True
|
||||
try:
|
||||
# Print multiple copies if count > 1
|
||||
for i in range(count):
|
||||
if count > 1:
|
||||
Clock.schedule_once(lambda dt, idx=i: popup.content.children[0].text.replace(
|
||||
f'Processing {count} label(s)...',
|
||||
f'Printing {idx+1} of {count}...'
|
||||
), 0)
|
||||
|
||||
success = print_label_standalone(label_text, printer, preview=0, use_pdf=True, svg_template=template_path)
|
||||
|
||||
if not success:
|
||||
all_success = False
|
||||
break
|
||||
|
||||
# Small delay between prints for multiple copies
|
||||
if i < count - 1:
|
||||
time.sleep(0.5)
|
||||
# Send all copies in ONE print job to prevent blank labels
|
||||
# being ejected between separate jobs on thermal printers.
|
||||
success = print_label_standalone(
|
||||
label_text, printer, preview=0,
|
||||
svg_template=template_path, copies=count
|
||||
)
|
||||
all_success = success
|
||||
|
||||
# Get the PDF filename that was created
|
||||
# Files are saved to pdf_backup/ with timestamp
|
||||
|
||||
121
prepare_ghostscript.bat
Normal file
121
prepare_ghostscript.bat
Normal file
@@ -0,0 +1,121 @@
|
||||
@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=
|
||||
|
||||
REM Pre-assign ProgramFiles(x86) to avoid batch parser choking on the parentheses
|
||||
set "PF64=%ProgramFiles%"
|
||||
set "PF32=%ProgramFiles(x86)%"
|
||||
|
||||
echo.
|
||||
echo ============================================================
|
||||
echo GhostScript Bundle Preparation
|
||||
echo ============================================================
|
||||
echo.
|
||||
|
||||
REM ---- Search for GhostScript in both Program Files locations ----
|
||||
for %%P in ("%PF64%" "%PF32%") 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
|
||||
115
prepare_ghostscript.py
Normal file
115
prepare_ghostscript.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
prepare_ghostscript.py
|
||||
Finds the system GhostScript installation and copies the files needed for
|
||||
bundling into conf/ghostscript/
|
||||
|
||||
Files copied:
|
||||
conf/ghostscript/bin/gswin64c.exe (or gswin32c.exe)
|
||||
conf/ghostscript/bin/gsdll64.dll (or gsdll32.dll)
|
||||
conf/ghostscript/lib/*.ps (PostScript init files)
|
||||
|
||||
Run this ONCE before building the exe with build_windows.bat.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import glob
|
||||
import sys
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DEST_BIN = os.path.join(SCRIPT_DIR, "conf", "ghostscript", "bin")
|
||||
DEST_LIB = os.path.join(SCRIPT_DIR, "conf", "ghostscript", "lib")
|
||||
|
||||
SEARCH_ROOTS = [
|
||||
os.environ.get("ProgramFiles", r"C:\Program Files"),
|
||||
os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"),
|
||||
os.environ.get("ProgramW6432", r"C:\Program Files"),
|
||||
]
|
||||
|
||||
FLAVOURS = [
|
||||
("gswin64c.exe", "gsdll64.dll"),
|
||||
("gswin32c.exe", "gsdll32.dll"),
|
||||
]
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(" GhostScript Bundle Preparation")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
|
||||
def find_ghostscript():
|
||||
"""Return (bin_dir, lib_dir, exe_name, dll_name) or None."""
|
||||
for root in dict.fromkeys(r for r in SEARCH_ROOTS if r): # unique, preserve order
|
||||
gs_root = os.path.join(root, "gs")
|
||||
if not os.path.isdir(gs_root):
|
||||
continue
|
||||
# Each versioned sub-folder, newest first
|
||||
versions = sorted(
|
||||
[d for d in glob.glob(os.path.join(gs_root, "gs*")) if os.path.isdir(d)],
|
||||
reverse=True,
|
||||
)
|
||||
for ver_dir in versions:
|
||||
bin_dir = os.path.join(ver_dir, "bin")
|
||||
lib_dir = os.path.join(ver_dir, "lib")
|
||||
for exe, dll in FLAVOURS:
|
||||
if os.path.isfile(os.path.join(bin_dir, exe)):
|
||||
print(f"Found GhostScript: {ver_dir}")
|
||||
return bin_dir, lib_dir, exe, dll
|
||||
return None
|
||||
|
||||
|
||||
result = find_ghostscript()
|
||||
|
||||
if result is None:
|
||||
print("WARNING: GhostScript is NOT installed on this machine.")
|
||||
print()
|
||||
print(" The exe will still build successfully, but GhostScript will")
|
||||
print(" NOT be bundled. On the target machine the app will fall back")
|
||||
print(" to SumatraPDF for printing (which may produce dotted output).")
|
||||
print()
|
||||
print(" To get sharp vector-quality printing, install GhostScript:")
|
||||
print(" https://ghostscript.com/releases/gsdnld.html")
|
||||
print(" Then re-run this script before building.")
|
||||
print()
|
||||
sys.exit(0)
|
||||
|
||||
src_bin, src_lib, exe_name, dll_name = result
|
||||
|
||||
# Create destination folders
|
||||
os.makedirs(DEST_BIN, exist_ok=True)
|
||||
os.makedirs(DEST_LIB, exist_ok=True)
|
||||
|
||||
# Copy executable and DLL
|
||||
print("Copying GhostScript binaries...")
|
||||
for fname in (exe_name, dll_name):
|
||||
src = os.path.join(src_bin, fname)
|
||||
dst = os.path.join(DEST_BIN, fname)
|
||||
if os.path.isfile(src):
|
||||
shutil.copy2(src, dst)
|
||||
print(f" + {fname}")
|
||||
else:
|
||||
print(f" WARNING: {fname} not found in {src_bin}")
|
||||
|
||||
# Copy lib/ (PostScript init files .ps)
|
||||
print()
|
||||
print("Copying GhostScript lib (PostScript init files)...")
|
||||
ps_files = glob.glob(os.path.join(src_lib, "*.ps"))
|
||||
for ps in ps_files:
|
||||
shutil.copy2(ps, DEST_LIB)
|
||||
print(f" + {len(ps_files)} .ps files copied from {src_lib}")
|
||||
|
||||
# Report
|
||||
bin_count = len(os.listdir(DEST_BIN))
|
||||
lib_count = len(os.listdir(DEST_LIB))
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(" GhostScript prepared successfully in conf\\ghostscript\\")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(f" bin\\ : {bin_count} file(s)")
|
||||
print(f" lib\\ : {lib_count} file(s)")
|
||||
print()
|
||||
print("GhostScript will be embedded into LabelPrinter.exe at build time.")
|
||||
print()
|
||||
558
print_label.py
558
print_label.py
@@ -1,6 +1,3 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import barcode
|
||||
from barcode.writer import ImageWriter
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
@@ -9,6 +6,21 @@ import platform
|
||||
import subprocess
|
||||
from print_label_pdf import PDFLabelGenerator
|
||||
|
||||
|
||||
def _write_print_log(msg):
|
||||
"""Write a timestamped debug line to print_debug.log next to the exe / script."""
|
||||
try:
|
||||
if getattr(sys, 'frozen', False):
|
||||
log_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
log_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
log_path = os.path.join(log_dir, 'print_debug.log')
|
||||
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
with open(log_path, 'a', encoding='utf-8') as f:
|
||||
f.write(f"[{ts}] {msg}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Cross-platform printer support
|
||||
try:
|
||||
import cups
|
||||
@@ -89,119 +101,6 @@ def get_available_printers():
|
||||
return ["PDF"]
|
||||
|
||||
|
||||
def create_label_image(text):
|
||||
"""
|
||||
Create a label image with 3 rows: label + barcode for each field.
|
||||
|
||||
Args:
|
||||
text (str): Combined text in format "SAP|CANTITATE|LOT" or single value
|
||||
|
||||
Returns:
|
||||
PIL.Image: The generated label image
|
||||
"""
|
||||
# Parse the text input
|
||||
parts = text.split('|') if '|' in text else [text, '', '']
|
||||
sap_nr = parts[0].strip() if len(parts) > 0 else ''
|
||||
cantitate = parts[1].strip() if len(parts) > 1 else ''
|
||||
lot_number = parts[2].strip() if len(parts) > 2 else ''
|
||||
|
||||
# Label dimensions (narrower, 3 rows)
|
||||
label_width = 800 # 8 cm
|
||||
label_height = 600 # 6 cm
|
||||
|
||||
# Create canvas
|
||||
label_img = Image.new('RGB', (label_width, label_height), 'white')
|
||||
draw = ImageDraw.Draw(label_img)
|
||||
|
||||
# Row setup - 3 equal rows
|
||||
row_height = label_height // 3
|
||||
left_margin = 15
|
||||
row_spacing = 3
|
||||
|
||||
# Fonts
|
||||
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||
try:
|
||||
label_font = ImageFont.truetype(font_path, 16)
|
||||
value_font = ImageFont.truetype(font_path, 14)
|
||||
except IOError:
|
||||
label_font = ImageFont.load_default()
|
||||
value_font = ImageFont.load_default()
|
||||
|
||||
# Data for 3 rows
|
||||
rows_data = [
|
||||
("SAP-Nr", sap_nr),
|
||||
("Cantitate", cantitate),
|
||||
("Lot Nr", lot_number),
|
||||
]
|
||||
|
||||
# Generate barcodes first
|
||||
CODE128 = barcode.get_barcode_class('code128')
|
||||
writer_options = {
|
||||
"write_text": False,
|
||||
"module_width": 0.4,
|
||||
"module_height": 8,
|
||||
"quiet_zone": 2,
|
||||
"font_size": 0
|
||||
}
|
||||
|
||||
barcode_images = []
|
||||
for _, value in rows_data:
|
||||
if value:
|
||||
try:
|
||||
code = CODE128(value[:25], writer=ImageWriter())
|
||||
filename = code.save('temp_barcode', options=writer_options)
|
||||
barcode_img = Image.open(filename)
|
||||
barcode_images.append(barcode_img)
|
||||
except:
|
||||
barcode_images.append(None)
|
||||
else:
|
||||
barcode_images.append(None)
|
||||
|
||||
# Draw each row with label and barcode
|
||||
for idx, ((label_name, value), barcode_img) in enumerate(zip(rows_data, barcode_images)):
|
||||
row_y = idx * row_height
|
||||
|
||||
# Draw label name
|
||||
draw.text(
|
||||
(left_margin, row_y + 3),
|
||||
label_name,
|
||||
fill='black',
|
||||
font=label_font
|
||||
)
|
||||
|
||||
# Draw barcode if available
|
||||
if barcode_img:
|
||||
# Resize barcode to fit in row width
|
||||
barcode_width = label_width - left_margin - 10
|
||||
barcode_height = row_height - 25
|
||||
# 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
|
||||
draw.text(
|
||||
(left_margin, row_y + 25),
|
||||
value if value else "(empty)",
|
||||
fill='black',
|
||||
font=value_font
|
||||
)
|
||||
|
||||
# Clean up temporary barcode files
|
||||
try:
|
||||
if os.path.exists('temp_barcode.png'):
|
||||
os.remove('temp_barcode.png')
|
||||
except:
|
||||
pass
|
||||
|
||||
return label_img
|
||||
|
||||
|
||||
def create_label_pdf(text, svg_template=None):
|
||||
"""
|
||||
Create a high-quality PDF label with 3 rows: label + barcode for each field.
|
||||
@@ -259,99 +158,154 @@ def create_label_pdf(text, svg_template=None):
|
||||
return generator.create_label_pdf(article, nr_art, serial, pdf_filename, image_path, selected_template)
|
||||
|
||||
|
||||
def configure_printer_quality(printer_name, width_mm=35, height_mm=25):
|
||||
def _print_pdf_windows(file_path, printer_name, copies=1):
|
||||
"""
|
||||
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
|
||||
Print a PDF on Windows using pymupdf (fitz) to render and win32print/GDI to send.
|
||||
All copies are sent as multiple pages within ONE print job so the printer
|
||||
never sees a gap (which causes blank labels on thermal printers).
|
||||
No external application is launched. Falls back to SumatraPDF if pymupdf
|
||||
is unavailable.
|
||||
|
||||
Returns True on success, False on failure.
|
||||
"""
|
||||
if SYSTEM != "Windows" or not WIN32_AVAILABLE:
|
||||
return False
|
||||
|
||||
copies = max(1, int(copies))
|
||||
_write_print_log(f"_print_pdf_windows called: printer={printer_name!r} copies={copies} file={file_path!r}")
|
||||
|
||||
# ── Method 1: pure-Python GDI (pymupdf + win32print) ─────────────────
|
||||
try:
|
||||
import fitz # pymupdf
|
||||
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)
|
||||
|
||||
import win32ui
|
||||
import win32con
|
||||
from PIL import ImageWin, Image as PILImage
|
||||
|
||||
_write_print_log("pymupdf + win32 imports OK – using GDI method")
|
||||
|
||||
doc = fitz.open(file_path)
|
||||
if doc.page_count == 0:
|
||||
_write_print_log("PDF has no pages – aborting")
|
||||
return False
|
||||
page = doc[0]
|
||||
|
||||
# Get the printer's native resolution
|
||||
hdc = win32ui.CreateDC()
|
||||
hdc.CreatePrinterDC(printer_name)
|
||||
x_dpi = hdc.GetDeviceCaps(win32con.LOGPIXELSX)
|
||||
y_dpi = hdc.GetDeviceCaps(win32con.LOGPIXELSY)
|
||||
_write_print_log(f"Printer DPI={x_dpi}x{y_dpi}")
|
||||
|
||||
# Render PDF page at printer DPI (PDF uses 72 pt/inch)
|
||||
mat = fitz.Matrix(x_dpi / 72.0, y_dpi / 72.0)
|
||||
pix = page.get_pixmap(matrix=mat, alpha=False)
|
||||
img = PILImage.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
||||
|
||||
# Read physical page size before closing the document
|
||||
pdf_page_w_pts = page.rect.width # PDF points (1pt = 1/72 inch)
|
||||
pdf_page_h_pts = page.rect.height
|
||||
doc.close()
|
||||
dest_w = int(round(pdf_page_w_pts * x_dpi / 72.0))
|
||||
dest_h = int(round(pdf_page_h_pts * y_dpi / 72.0))
|
||||
_write_print_log(
|
||||
f"PDF page={pdf_page_w_pts:.1f}x{pdf_page_h_pts:.1f}pt "
|
||||
f"rendered={pix.width}x{pix.height}px dest={dest_w}x{dest_h}px"
|
||||
)
|
||||
|
||||
# Draw at exact physical size – NOT stretched to the driver's paper area.
|
||||
# All copies go into ONE print job to prevent blank labels between jobs.
|
||||
hdc.StartDoc(os.path.basename(file_path))
|
||||
dib = ImageWin.Dib(img)
|
||||
for copy_idx in range(copies):
|
||||
hdc.StartPage()
|
||||
dib.draw(hdc.GetHandleOutput(), (0, 0, dest_w, dest_h))
|
||||
hdc.EndPage()
|
||||
hdc.EndDoc()
|
||||
hdc.DeleteDC()
|
||||
|
||||
_write_print_log(f"GDI print SUCCESS → {printer_name} ({copies} cop{'y' if copies==1 else 'ies'} in 1 job)")
|
||||
return True
|
||||
|
||||
except ImportError as ie:
|
||||
_write_print_log(f"GDI method unavailable ({ie}) – trying SumatraPDF fallback")
|
||||
except Exception as e:
|
||||
print(f"Could not configure printer quality: {e}")
|
||||
return False
|
||||
_write_print_log(f"GDI print error: {e}")
|
||||
try:
|
||||
hdc.DeleteDC()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Method 2: SumatraPDF fallback ────────────────────────────────────
|
||||
_write_print_log("Attempting SumatraPDF fallback")
|
||||
sumatra_paths = []
|
||||
if getattr(sys, 'frozen', False):
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
sumatra_paths.append(os.path.join(sys._MEIPASS, 'SumatraPDF.exe'))
|
||||
app_dir = os.path.dirname(sys.executable)
|
||||
sumatra_paths.append(os.path.join(app_dir, 'conf', 'SumatraPDF.exe'))
|
||||
sumatra_paths.append(os.path.join(app_dir, 'SumatraPDF.exe'))
|
||||
else:
|
||||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sumatra_paths.append(os.path.join(app_dir, 'conf', 'SumatraPDF.exe'))
|
||||
sumatra_paths.append(os.path.join(app_dir, 'SumatraPDF.exe'))
|
||||
sumatra_paths += [
|
||||
r"C:\Program Files\SumatraPDF\SumatraPDF.exe",
|
||||
r"C:\Program Files (x86)\SumatraPDF\SumatraPDF.exe",
|
||||
]
|
||||
_write_print_log(f"SumatraPDF search paths: {sumatra_paths}")
|
||||
|
||||
for sumatra_path in sumatra_paths:
|
||||
_write_print_log(f"Checking: {sumatra_path} → exists={os.path.exists(sumatra_path)}")
|
||||
if os.path.exists(sumatra_path):
|
||||
try:
|
||||
# Find conf folder with SumatraPDF-settings.txt
|
||||
settings_candidates = []
|
||||
if getattr(sys, 'frozen', False):
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
settings_candidates.append(os.path.join(sys._MEIPASS, 'conf'))
|
||||
settings_candidates.append(os.path.join(os.path.dirname(sys.executable), 'conf'))
|
||||
else:
|
||||
settings_candidates.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf'))
|
||||
settings_candidates.append(os.path.dirname(sumatra_path))
|
||||
|
||||
appdata_dir = next(
|
||||
(d for d in settings_candidates
|
||||
if os.path.exists(os.path.join(d, 'SumatraPDF-settings.txt'))),
|
||||
None
|
||||
)
|
||||
_write_print_log(f"SumatraPDF settings dir: {appdata_dir}")
|
||||
|
||||
cmd = [sumatra_path, "-print-to", printer_name,
|
||||
"-print-settings", "noscale",
|
||||
"-silent", "-exit-when-done"]
|
||||
if appdata_dir:
|
||||
cmd += ["-appdata-dir", appdata_dir]
|
||||
cmd.append(file_path)
|
||||
|
||||
_write_print_log(f"SumatraPDF cmd: {' '.join(cmd)}")
|
||||
result = subprocess.run(
|
||||
cmd, check=False,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
capture_output=True, text=True, timeout=30)
|
||||
_write_print_log(f"SumatraPDF exit={result.returncode} "
|
||||
f"stdout={result.stdout.strip()!r} "
|
||||
f"stderr={result.stderr.strip()!r}")
|
||||
return True # treat any completed run as spooled
|
||||
except Exception as se:
|
||||
_write_print_log(f"SumatraPDF error: {se}")
|
||||
|
||||
_write_print_log("All print methods failed")
|
||||
return False
|
||||
|
||||
|
||||
def print_to_printer(printer_name, file_path):
|
||||
def print_to_printer(printer_name, file_path, copies=1):
|
||||
"""
|
||||
Print file to printer (cross-platform).
|
||||
Uses SumatraPDF for silent printing on Windows.
|
||||
|
||||
Uses pymupdf+GDI for silent printing on Windows with all copies in one job.
|
||||
|
||||
Args:
|
||||
printer_name (str): Name of printer or "PDF" for PDF output
|
||||
file_path (str): Path to file to print
|
||||
|
||||
copies (int): Number of copies to print in a single print job
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
@@ -369,112 +323,14 @@ def print_to_printer(printer_name, file_path):
|
||||
return True
|
||||
|
||||
elif SYSTEM == "Windows":
|
||||
# Windows: Print PDF silently without any viewer opening
|
||||
try:
|
||||
if WIN32_AVAILABLE:
|
||||
import win32print
|
||||
import win32api
|
||||
|
||||
if file_path.endswith('.pdf'):
|
||||
# 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)
|
||||
|
||||
# Check app directory and conf subfolder 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"))
|
||||
sumatra_paths.append(os.path.join(app_dir, "conf", "SumatraPDF.exe")) # conf subfolder next to exe
|
||||
sumatra_paths.append(os.path.join(os.getcwd(), "conf", "SumatraPDF.exe")) # conf relative to cwd
|
||||
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",
|
||||
])
|
||||
|
||||
# Set printer to highest quality / landscape before sending job
|
||||
if WIN32_AVAILABLE:
|
||||
try:
|
||||
import win32print
|
||||
hPrinter = win32print.OpenPrinter(printer_name)
|
||||
try:
|
||||
props = win32print.GetPrinter(hPrinter, 2)
|
||||
dm = props['pDevMode']
|
||||
if dm:
|
||||
dm.Orientation = 2 # DMORIENT_LANDSCAPE
|
||||
dm.PrintQuality = -4 # DMRES_HIGH (highest DPI)
|
||||
dm.YResolution = -4 # also set Y res
|
||||
win32print.SetPrinter(hPrinter, 2, props, 0)
|
||||
print("Printer set to landscape + high quality")
|
||||
finally:
|
||||
win32print.ClosePrinter(hPrinter)
|
||||
except Exception as qe:
|
||||
print(f"Could not configure printer quality/orientation: {qe}")
|
||||
|
||||
for sumatra_path in sumatra_paths:
|
||||
if os.path.exists(sumatra_path):
|
||||
try:
|
||||
print(f"Using SumatraPDF: {sumatra_path}")
|
||||
print(f"Sending to printer: {printer_name}")
|
||||
result = subprocess.run([
|
||||
sumatra_path,
|
||||
"-print-to",
|
||||
printer_name,
|
||||
file_path,
|
||||
"-print-settings",
|
||||
"noscale,landscape", # preserve dimensions + force landscape
|
||||
"-silent",
|
||||
"-exit-when-done"
|
||||
], 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:
|
||||
print(f"SumatraPDF stderr: {result.stderr.strip()}")
|
||||
else:
|
||||
print(f"Label sent to printer via SumatraPDF: {printer_name}")
|
||||
printed = True
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"SumatraPDF error: {e}")
|
||||
|
||||
# Do not launch default PDF viewers (Adobe/Edge/etc.) as fallback.
|
||||
if not printed:
|
||||
print("SumatraPDF not found or failed. PDF saved as backup only (no viewer launched).")
|
||||
return False
|
||||
|
||||
return True
|
||||
else:
|
||||
# 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
|
||||
except Exception as e:
|
||||
print(f"Windows print error: {e}")
|
||||
print("PDF backup saved as fallback")
|
||||
# Windows: Print PDF using Python GDI (pymupdf + win32print).
|
||||
# No external viewer is launched at any point.
|
||||
if file_path.endswith('.pdf'):
|
||||
return _print_pdf_windows(file_path, printer_name, copies=copies)
|
||||
else:
|
||||
subprocess.run(['notepad', '/p', file_path],
|
||||
check=False,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
return True
|
||||
|
||||
elif SYSTEM == "Darwin":
|
||||
@@ -494,73 +350,44 @@ def print_to_printer(printer_name, file_path):
|
||||
return True
|
||||
|
||||
|
||||
def print_label_standalone(value, printer, preview=0, use_pdf=True, svg_template=None):
|
||||
def print_label_standalone(value, printer, preview=0, svg_template=None, copies=1):
|
||||
"""
|
||||
Print a label with the specified text on the specified printer.
|
||||
Always generates a PDF backup in pdf_backup and prints that PDF.
|
||||
|
||||
Generate a PDF label, save it to pdf_backup/, and print it on the printer.
|
||||
All copies are sent in a single print job to avoid blank labels on thermal
|
||||
printers that eject a label between separate jobs.
|
||||
|
||||
Args:
|
||||
value (str): The text to print on the label
|
||||
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)
|
||||
|
||||
value (str): Label data in format "article;nr_art;serial;status"
|
||||
printer (str): Printer name (or "PDF" to skip physical printing)
|
||||
preview (int): 0 = print immediately; 1-3 = 3 s delay; >3 = 5 s delay
|
||||
svg_template (str): Path to SVG template (optional; auto-selected if None)
|
||||
copies (int): Number of copies to print in one job (default 1)
|
||||
|
||||
Returns:
|
||||
bool: True if printing was successful, False otherwise
|
||||
bool: True if sending to printer succeeded, False otherwise
|
||||
"""
|
||||
# Track generated files
|
||||
file_created = False
|
||||
temp_file = None
|
||||
copies = max(1, int(copies))
|
||||
pdf_file = None
|
||||
|
||||
try:
|
||||
# Debug output
|
||||
print(f"Preview value: {preview}")
|
||||
print(f"Preview type: {type(preview)}")
|
||||
print(f"Using format: {'PDF' if use_pdf else 'PNG'}")
|
||||
|
||||
# Always generate a PDF backup and print that PDF for verification
|
||||
# Step 1 – Generate and save PDF to pdf_backup/
|
||||
try:
|
||||
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}")
|
||||
else:
|
||||
print("PDF generation returned no file path")
|
||||
print("PDF generation failed – no output file")
|
||||
return False
|
||||
except Exception as pdf_err:
|
||||
print(f"PDF generation failed: {pdf_err}")
|
||||
|
||||
# Optionally also create the label image (PNG)
|
||||
if not pdf_file or not os.path.exists(pdf_file):
|
||||
if not use_pdf:
|
||||
label_img = create_label_image(value)
|
||||
temp_file = 'final_label.png'
|
||||
label_img.save(temp_file)
|
||||
print(f"PNG label created: {temp_file}")
|
||||
else:
|
||||
temp_file = pdf_file
|
||||
|
||||
file_created = True
|
||||
|
||||
if not temp_file or not os.path.exists(temp_file):
|
||||
print("No label file created for printing")
|
||||
print(f"PDF generation error: {pdf_err}")
|
||||
return False
|
||||
|
||||
# Convert preview to int if it's a string
|
||||
|
||||
# Step 2 – Optional countdown before printing
|
||||
if isinstance(preview, str):
|
||||
preview = int(preview)
|
||||
|
||||
if preview > 0: # Any value above 0 shows a preview message
|
||||
# Calculate preview duration in seconds
|
||||
if 1 <= preview <= 3:
|
||||
preview_sec = 3 # 3 seconds
|
||||
else: # preview > 3
|
||||
preview_sec = 5 # 5 seconds
|
||||
|
||||
print(f"Printing in {preview_sec} seconds... (Press Ctrl+C to cancel)")
|
||||
|
||||
# Simple countdown timer using time.sleep
|
||||
|
||||
if preview > 0:
|
||||
preview_sec = 3 if 1 <= preview <= 3 else 5
|
||||
print(f"Printing in {preview_sec} seconds… (Ctrl+C to cancel)")
|
||||
try:
|
||||
for i in range(preview_sec, 0, -1):
|
||||
print(f" {i}...", end=" ", flush=True)
|
||||
@@ -569,25 +396,20 @@ def print_label_standalone(value, printer, preview=0, use_pdf=True, svg_template
|
||||
except KeyboardInterrupt:
|
||||
print("\nCancelled by user")
|
||||
return False
|
||||
|
||||
# Print after preview
|
||||
print("Sending to printer...")
|
||||
return print_to_printer(printer, temp_file)
|
||||
else:
|
||||
print("Direct printing without preview...")
|
||||
# Direct printing without preview (preview = 0)
|
||||
return print_to_printer(printer, temp_file)
|
||||
|
||||
|
||||
# Step 3 – Send to printer (all copies in one job)
|
||||
print(f"Sending to printer ({copies} cop{'y' if copies==1 else 'ies'})...")
|
||||
return print_to_printer(printer, pdf_file, copies=copies)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error printing label: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
finally:
|
||||
# This block always executes, ensuring cleanup
|
||||
if pdf_file and os.path.exists(pdf_file):
|
||||
print("Cleanup complete - PDF backup saved to pdf_backup folder")
|
||||
print("Cleanup complete – PDF backup saved to pdf_backup/")
|
||||
else:
|
||||
print("Cleanup complete - label file retained for reference")
|
||||
print("Cleanup complete")
|
||||
|
||||
|
||||
# Main code removed - import this module or run as part of the Kivy GUI application
|
||||
|
||||
@@ -49,8 +49,8 @@ class PDFLabelGenerator:
|
||||
"""
|
||||
self.label_width = label_width * cm
|
||||
self.label_height = label_height * cm
|
||||
# Force landscape: ensure width > height
|
||||
self.page_size = landscape((self.label_height, self.label_width)) if self.label_width > self.label_height else (self.label_width, self.label_height)
|
||||
# label_width (3.5 cm) > label_height (2.5 cm) → page is already landscape
|
||||
self.page_size = (self.label_width, self.label_height)
|
||||
self.dpi = dpi
|
||||
self.margin = 1 * mm # Minimal margin
|
||||
|
||||
|
||||
Reference in New Issue
Block a user