Compare commits
4 Commits
7197df9f5c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 71ccdb7b96 | |||
| be1b494527 | |||
| b35f278c1e | |||
| faef90f98b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,5 @@
|
|||||||
label/
|
label/
|
||||||
build/
|
build/
|
||||||
dist/
|
|
||||||
logs/
|
logs/
|
||||||
pdf_backup/
|
pdf_backup/
|
||||||
conf/ghostscript/
|
conf/ghostscript/
|
||||||
|
|||||||
@@ -59,11 +59,12 @@ a = Analysis(
|
|||||||
binaries=gs_binaries,
|
binaries=gs_binaries,
|
||||||
datas=base_datas + gs_datas,
|
datas=base_datas + gs_datas,
|
||||||
hiddenimports=[
|
hiddenimports=[
|
||||||
'kivy', 'PIL', 'barcode', 'reportlab',
|
'kivy', 'PIL', 'PIL.ImageWin', 'barcode', 'reportlab',
|
||||||
'print_label', 'print_label_pdf',
|
'print_label', 'print_label_pdf',
|
||||||
|
'fitz', 'pymupdf',
|
||||||
'svglib', 'cairosvg',
|
'svglib', 'cairosvg',
|
||||||
'watchdog', 'watchdog.observers', 'watchdog.events',
|
'watchdog', 'watchdog.observers', 'watchdog.events',
|
||||||
'pystray', 'win32timezone',
|
'pystray', 'win32api', 'win32print', 'win32ui', 'win32con', 'win32timezone',
|
||||||
],
|
],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
|
|||||||
112
build_exe.py
112
build_exe.py
@@ -11,91 +11,41 @@ To build for Windows:
|
|||||||
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
|
Printing method
|
||||||
--------------------
|
---------------
|
||||||
If GhostScript is installed on this build machine the script will
|
SumatraPDF is bundled inside the exe and used for all printing.
|
||||||
automatically copy the required files into conf\\ghostscript\\ before
|
It sends the PDF at its exact 35x25 mm page size via -print-settings noscale.
|
||||||
calling PyInstaller so they are embedded in LabelPrinter.exe.
|
No GhostScript installation required on the build or target machine.
|
||||||
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__))
|
||||||
|
|
||||||
|
|
||||||
def prepare_ghostscript():
|
def check_conf_folder():
|
||||||
"""
|
"""
|
||||||
Find the system GhostScript installation and copy the minimum files
|
Verify that required files are present in the conf\ folder before building.
|
||||||
needed for bundling into conf\\ghostscript\\.
|
Returns True if all required files exist, False otherwise.
|
||||||
|
|
||||||
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
|
required = [
|
||||||
|
os.path.join('conf', 'SumatraPDF.exe'),
|
||||||
gs_exe = None
|
os.path.join('conf', 'SumatraPDF-settings.txt'),
|
||||||
gs_dll = None
|
os.path.join('conf', 'label_template_ok.svg'),
|
||||||
gs_lib = None
|
os.path.join('conf', 'label_template_nok.svg'),
|
||||||
|
os.path.join('conf', 'label_template.svg'),
|
||||||
for pf in [r"C:\Program Files", r"C:\Program Files (x86)"]:
|
]
|
||||||
gs_base = os.path.join(pf, "gs")
|
all_ok = True
|
||||||
if not os.path.isdir(gs_base):
|
for f in required:
|
||||||
continue
|
if os.path.exists(f):
|
||||||
# Iterate versions newest-first
|
print(f" OK: {f}")
|
||||||
for ver_dir in sorted(os.listdir(gs_base), reverse=True):
|
else:
|
||||||
bin_dir = os.path.join(gs_base, ver_dir, "bin")
|
print(f" MISSING: {f}")
|
||||||
lib_dir = os.path.join(gs_base, ver_dir, "lib")
|
all_ok = False
|
||||||
if os.path.exists(os.path.join(bin_dir, "gswin64c.exe")):
|
return all_ok
|
||||||
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__':
|
if __name__ == '__main__':
|
||||||
@@ -106,15 +56,14 @@ if __name__ == '__main__':
|
|||||||
# Change to script directory so relative paths work
|
# Change to script directory so relative paths work
|
||||||
os.chdir(script_dir)
|
os.chdir(script_dir)
|
||||||
|
|
||||||
# Step 1: Prepare GhostScript for bundling (Windows only)
|
# Step 1: Verify conf\ folder contents
|
||||||
if sys.platform == "win32":
|
print("\n[1/2] Checking conf\\ folder...")
|
||||||
print("\n[1/2] Preparing GhostScript for bundling...")
|
if not check_conf_folder():
|
||||||
prepare_ghostscript()
|
print("\nERROR: One or more required conf\\ files are missing.")
|
||||||
else:
|
print("Place SumatraPDF.exe and the SVG templates in the conf\\ folder, then retry.")
|
||||||
print("\n[1/2] Skipping GhostScript prep (non-Windows build machine)")
|
sys.exit(1)
|
||||||
|
|
||||||
# Step 2: Build with PyInstaller using the handcrafted spec file.
|
# 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("\n[2/2] Building standalone executable via LabelPrinter.spec...")
|
||||||
print("This may take a few minutes...\n")
|
print("This may take a few minutes...\n")
|
||||||
|
|
||||||
@@ -130,8 +79,7 @@ if __name__ == '__main__':
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("\nExecutable location: ./dist/LabelPrinter.exe")
|
print("\nExecutable location: ./dist/LabelPrinter.exe")
|
||||||
print("\nBundled components:")
|
print("\nBundled components:")
|
||||||
print(" - GhostScript (vector-quality printing)")
|
print(" - SumatraPDF (primary printing, noscale 35x25 mm)")
|
||||||
print(" - SumatraPDF (fallback printing)")
|
|
||||||
print(" - SVG templates, conf files")
|
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")
|
||||||
|
|||||||
BIN
dist/LabelPrinter.exe
vendored
Normal file
BIN
dist/LabelPrinter.exe
vendored
Normal file
Binary file not shown.
@@ -982,23 +982,13 @@ printer = PDF
|
|||||||
success = False
|
success = False
|
||||||
all_success = True
|
all_success = True
|
||||||
try:
|
try:
|
||||||
# Print multiple copies if count > 1
|
# Send all copies in ONE print job to prevent blank labels
|
||||||
for i in range(count):
|
# being ejected between separate jobs on thermal printers.
|
||||||
if count > 1:
|
success = print_label_standalone(
|
||||||
Clock.schedule_once(lambda dt, idx=i: popup.content.children[0].text.replace(
|
label_text, printer, preview=0,
|
||||||
f'Processing {count} label(s)...',
|
svg_template=template_path, copies=count
|
||||||
f'Printing {idx+1} of {count}...'
|
)
|
||||||
), 0)
|
all_success = success
|
||||||
|
|
||||||
success = print_label_standalone(label_text, printer, preview=0, 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)
|
|
||||||
|
|
||||||
# Get the PDF filename that was created
|
# Get the PDF filename that was created
|
||||||
# Files are saved to pdf_backup/ with timestamp
|
# Files are saved to pdf_backup/ with timestamp
|
||||||
|
|||||||
351
print_label.py
351
print_label.py
@@ -6,6 +6,21 @@ import platform
|
|||||||
import subprocess
|
import subprocess
|
||||||
from print_label_pdf import PDFLabelGenerator
|
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
|
# Cross-platform printer support
|
||||||
try:
|
try:
|
||||||
import cups
|
import cups
|
||||||
@@ -143,104 +158,153 @@ def create_label_pdf(text, svg_template=None):
|
|||||||
return generator.create_label_pdf(article, nr_art, serial, pdf_filename, image_path, selected_template)
|
return generator.create_label_pdf(article, nr_art, serial, pdf_filename, image_path, selected_template)
|
||||||
|
|
||||||
|
|
||||||
def find_ghostscript():
|
def _print_pdf_windows(file_path, printer_name, copies=1):
|
||||||
"""
|
"""
|
||||||
Find GhostScript executable. Search order:
|
Print a PDF on Windows using pymupdf (fitz) to render and win32print/GDI to send.
|
||||||
1. Bundled inside the PyInstaller .exe (ghostscript/bin/ in _MEIPASS)
|
All copies are sent as multiple pages within ONE print job so the printer
|
||||||
2. System-level installation (C:\\Program Files\\gs\\...)
|
never sees a gap (which causes blank labels on thermal printers).
|
||||||
Returns the full path to gswin64c.exe / gswin32c.exe, or None.
|
No external application is launched. Falls back to SumatraPDF if pymupdf
|
||||||
|
is unavailable.
|
||||||
|
|
||||||
|
Returns True on success, False on failure.
|
||||||
"""
|
"""
|
||||||
# 1 ── Bundled location (PyInstaller one-file build)
|
copies = max(1, int(copies))
|
||||||
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
_write_print_log(f"_print_pdf_windows called: printer={printer_name!r} copies={copies} file={file_path!r}")
|
||||||
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}")
|
|
||||||
|
|
||||||
|
# ── Method 1: pure-Python GDI (pymupdf + win32print) ─────────────────
|
||||||
try:
|
try:
|
||||||
# -sDEVICE=mswinpr2 : native Windows printer device (vector path)
|
import fitz # pymupdf
|
||||||
# -dNOPAUSE / -dBATCH : batch mode, no user prompts
|
import win32print
|
||||||
# -r600 : 600 DPI matches typical thermal-printer head
|
import win32ui
|
||||||
# -dTextAlphaBits=4 : anti-aliasing for text
|
import win32con
|
||||||
# -dGraphicsAlphaBits=4
|
from PIL import ImageWin, Image as PILImage
|
||||||
# -dNOSAFER : allow file access needed for fonts
|
|
||||||
# The printer paper size and orientation are already configured
|
_write_print_log("pymupdf + win32 imports OK – using GDI method")
|
||||||
# in the driver – do not override them here.
|
|
||||||
cmd = [
|
doc = fitz.open(file_path)
|
||||||
gs_path,
|
if doc.page_count == 0:
|
||||||
"-dNOPAUSE",
|
_write_print_log("PDF has no pages – aborting")
|
||||||
"-dBATCH",
|
return False
|
||||||
"-sDEVICE=mswinpr2",
|
page = doc[0]
|
||||||
"-dNOSAFER",
|
|
||||||
"-r600",
|
# Get the printer's native resolution
|
||||||
"-dTextAlphaBits=4",
|
hdc = win32ui.CreateDC()
|
||||||
"-dGraphicsAlphaBits=4",
|
hdc.CreatePrinterDC(printer_name)
|
||||||
f"-sOutputFile=%printer%{printer_name}",
|
x_dpi = hdc.GetDeviceCaps(win32con.LOGPIXELSX)
|
||||||
pdf_path,
|
y_dpi = hdc.GetDeviceCaps(win32con.LOGPIXELSY)
|
||||||
]
|
_write_print_log(f"Printer DPI={x_dpi}x{y_dpi}")
|
||||||
print(f"Printing with GhostScript (vector quality) to: {printer_name}")
|
|
||||||
result = subprocess.run(
|
# Render PDF page at printer DPI (PDF uses 72 pt/inch)
|
||||||
cmd,
|
mat = fitz.Matrix(x_dpi / 72.0, y_dpi / 72.0)
|
||||||
check=True,
|
pix = page.get_pixmap(matrix=mat, alpha=False)
|
||||||
capture_output=True,
|
img = PILImage.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
||||||
text=True,
|
|
||||||
timeout=60,
|
# Read physical page size before closing the document
|
||||||
env=env,
|
pdf_page_w_pts = page.rect.width # PDF points (1pt = 1/72 inch)
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
|
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"
|
||||||
)
|
)
|
||||||
print(f"✅ Label sent via GhostScript: {printer_name}")
|
|
||||||
|
# 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
|
return True
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(f"GhostScript print failed (exit {e.returncode}): {e.stderr.strip() if e.stderr else ''}")
|
except ImportError as ie:
|
||||||
return False
|
_write_print_log(f"GDI method unavailable ({ie}) – trying SumatraPDF fallback")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"GhostScript error: {e}")
|
_write_print_log(f"GDI print error: {e}")
|
||||||
return False
|
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).
|
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:
|
Args:
|
||||||
printer_name (str): Name of printer or "PDF" for PDF output
|
printer_name (str): Name of printer or "PDF" for PDF output
|
||||||
file_path (str): Path to file to print
|
file_path (str): Path to file to print
|
||||||
|
copies (int): Number of copies to print in a single print job
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if successful
|
bool: True if successful
|
||||||
@@ -259,106 +323,14 @@ def print_to_printer(printer_name, file_path):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
elif SYSTEM == "Windows":
|
elif SYSTEM == "Windows":
|
||||||
# Windows: Print PDF silently without any viewer opening
|
# Windows: Print PDF using Python GDI (pymupdf + win32print).
|
||||||
try:
|
# No external viewer is launched at any point.
|
||||||
if WIN32_AVAILABLE:
|
if file_path.endswith('.pdf'):
|
||||||
import win32print
|
return _print_pdf_windows(file_path, printer_name, copies=copies)
|
||||||
|
else:
|
||||||
if file_path.endswith('.pdf'):
|
subprocess.run(['notepad', '/p', file_path],
|
||||||
# Try silent printing methods (no viewer opens)
|
check=False,
|
||||||
import os
|
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||||
|
|
||||||
printed = False
|
|
||||||
|
|
||||||
# Method 1: GhostScript (best quality – true vector path).
|
|
||||||
# Tried first regardless of whether we are running as a
|
|
||||||
# script or as the packaged .exe, because GhostScript is
|
|
||||||
# a system-level installation and will be present on the
|
|
||||||
# machine independently of how this app is launched.
|
|
||||||
# If GhostScript is not installed it returns False
|
|
||||||
# immediately and we fall through to SumatraPDF.
|
|
||||||
printed = print_pdf_with_ghostscript(file_path, printer_name)
|
|
||||||
|
|
||||||
if printed:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Method 2: SumatraPDF (bundled inside exe or external)
|
|
||||||
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",
|
|
||||||
])
|
|
||||||
|
|
||||||
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}")
|
|
||||||
# "noscale" = print the PDF at its exact page size.
|
|
||||||
# Do NOT add "landscape" – the printer driver
|
|
||||||
# already knows the orientation from its own settings.
|
|
||||||
result = subprocess.run([
|
|
||||||
sumatra_path,
|
|
||||||
"-print-to",
|
|
||||||
printer_name,
|
|
||||||
file_path,
|
|
||||||
"-print-settings",
|
|
||||||
"noscale",
|
|
||||||
"-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")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif SYSTEM == "Darwin":
|
elif SYSTEM == "Darwin":
|
||||||
@@ -378,20 +350,23 @@ def print_to_printer(printer_name, file_path):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def print_label_standalone(value, printer, preview=0, svg_template=None):
|
def print_label_standalone(value, printer, preview=0, svg_template=None, copies=1):
|
||||||
"""
|
"""
|
||||||
Generate a PDF label, save it to pdf_backup/, and print it on the printer.
|
Generate a PDF label, save it to pdf_backup/, and print it on the printer.
|
||||||
The printer's own paper size and orientation settings are used as-is.
|
All copies are sent in a single print job to avoid blank labels on thermal
|
||||||
|
printers that eject a label between separate jobs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value (str): Label data in format "article;nr_art;serial;status"
|
value (str): Label data in format "article;nr_art;serial;status"
|
||||||
printer (str): Printer name (or "PDF" to skip physical printing)
|
printer (str): Printer name (or "PDF" to skip physical printing)
|
||||||
preview (int): 0 = print immediately; 1-3 = 3 s delay; >3 = 5 s delay
|
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)
|
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:
|
Returns:
|
||||||
bool: True if sending to printer succeeded, False otherwise
|
bool: True if sending to printer succeeded, False otherwise
|
||||||
"""
|
"""
|
||||||
|
copies = max(1, int(copies))
|
||||||
pdf_file = None
|
pdf_file = None
|
||||||
try:
|
try:
|
||||||
# Step 1 – Generate and save PDF to pdf_backup/
|
# Step 1 – Generate and save PDF to pdf_backup/
|
||||||
@@ -422,9 +397,9 @@ def print_label_standalone(value, printer, preview=0, svg_template=None):
|
|||||||
print("\nCancelled by user")
|
print("\nCancelled by user")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Step 3 – Send to printer
|
# Step 3 – Send to printer (all copies in one job)
|
||||||
print("Sending to printer...")
|
print(f"Sending to printer ({copies} cop{'y' if copies==1 else 'ies'})...")
|
||||||
return print_to_printer(printer, pdf_file)
|
return print_to_printer(printer, pdf_file, copies=copies)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error printing label: {str(e)}")
|
print(f"Error printing label: {str(e)}")
|
||||||
|
|||||||
Reference in New Issue
Block a user