Compare commits

...

3 Commits

Author SHA1 Message Date
58082ed171 final doc 2026-02-13 15:30:00 +02:00
f09c365384 Fix printer detection, implement portable deployment with SumatraPDF
- Fixed network printer enumeration (PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS)
- Added printer name truncation to 20 chars with full name mapping
- Implemented silent PDF printing using SumatraPDF with landscape orientation
- Added auto-dismiss for success popup (3 seconds)
- Bundled SumatraPDF inside executable for portable single-file deployment
- Updated build script to embed SumatraPDF
- Added setup_sumatra.ps1 for downloading SumatraPDF portable
- Added DEPLOYMENT.md documentation
2026-02-06 14:00:17 +02:00
b204ce38fc Fix silent printing and shorten printer display names
- Print directly via win32print API without opening PDF viewer
- Shorten printer names to 20 chars in dropdown (strip server prefix for network printers)
- Map display names back to full names for printing
- PDF backup saved silently without launching viewer
2026-02-06 11:18:27 +02:00
10 changed files with 397 additions and 35 deletions

88
DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,88 @@
# Label Printer - Portable Deployment Guide
## Deployment Structure
The app is now **fully self-contained** with SumatraPDF embedded inside:
```
LabelPrinter/
├── LabelPrinter.exe # Main application (includes SumatraPDF inside)
├── pdf_backup/ # Auto-created: PDF backups
└── logs/ # Auto-created: Print logs
```
**No visible folders!** SumatraPDF is bundled inside LabelPrinter.exe and extracted to a temporary location at runtime.
## Setup Instructions
### 1. Download SumatraPDF (For Building Only)
**This step is only needed when building the app.** SumatraPDF will be embedded inside the executable.
```powershell
# PowerShell command to download SumatraPDF
powershell -ExecutionPolicy Bypass -File setup_sumatra.ps1
```
This downloads SumatraPDF portable (~5 MB) to the `SumatraPDF` folder.
### 2. Build the Application
```powershell
.\build_windows.ps1
```
The build script will:
- Check for SumatraPDF
- Bundle it inside the executable
- Create `dist\LabelPrinter.exe` (~80 MB including all dependencies)
### 3. Deploy
**Simply copy `LabelPrinter.exe` to any Windows machine!**
```
📁 Deployment (any folder)
└── LabelPrinter.exe ← Just this one file!
```
- No installation needed
- No additional files or folders
- Double-click to run
- Works on any Windows 10/11 machine
## Features
-**Single Executable** - Everything bundled in one .exe file (~80 MB)
-**Fully Portable** - No installation needed, no external dependencies
-**Silent Printing** - No PDF viewer windows pop up
-**Network Printers** - Supports printers from print servers (e.g. `\\server\printer`)
-**PDF Backup** - All labels saved to `pdf_backup/` folder
-**Print Logging** - CSV logs in `logs/` folder
-**SumatraPDF Hidden** - Embedded inside, not visible to users
## Printer Name Display
Network printer names (e.g. `\\filesibiusb05\ZDesigner_ZQ630`) are automatically shortened to 20 characters in the dropdown for better display. The full printer name is used for actual printing.
## Troubleshooting
### Printing Not Working
1. **Check Printer Connection**: Verify printer is online and accessible
2. **Check PDF Backup**: Labels are always saved to `pdf_backup/` folder even if printing fails
3. **Check Logs**: View print logs in `logs/` folder for error messages
4. **Rebuild App**: If you built the app yourself, ensure `setup_sumatra.ps1` was run first to download SumatraPDF before building
### Network Printers Not Showing
- Network printers must be installed/connected on the machine before running the app
- For print server printers like `\\filesibiusb05\printer`, ensure the share is accessible
- Run as administrator if printer enumeration fails
## Notes
- First run may be slower (Kivy initialization)
- PDF backups are auto-deleted after 5 days
- Log files are auto-deleted after 5 days
- Supports Python 3.10-3.13 (Python 3.14+ may have issues with Kivy)

View File

@@ -4,7 +4,7 @@
a = Analysis( a = Analysis(
['label_printer_gui.py'], ['label_printer_gui.py'],
pathex=[], pathex=[],
binaries=[], binaries=[('SumatraPDF\\SumatraPDF.exe', '.')],
datas=[], datas=[],
hiddenimports=['kivy', 'PIL', 'barcode', 'reportlab', 'print_label', 'print_label_pdf'], hiddenimports=['kivy', 'PIL', 'barcode', 'reportlab', 'print_label', 'print_label_pdf'],
hookspath=[], hookspath=[],

BIN
SumatraPDF/SumatraPDF.exe Normal file

Binary file not shown.

View File

@@ -47,13 +47,39 @@ if ($LASTEXITCODE -ne 0) {
} }
Write-Host "" Write-Host ""
Write-Host "[4/5] Cleaning old build artifacts..." -ForegroundColor Cyan Write-Host "[4/6] Checking for SumatraPDF..." -ForegroundColor Cyan
$sumatraPath = "SumatraPDF\SumatraPDF.exe"
if (-not (Test-Path $sumatraPath)) {
Write-Host ""
Write-Host "WARNING: SumatraPDF not found!" -ForegroundColor Yellow
Write-Host "SumatraPDF is required for silent PDF printing." -ForegroundColor Yellow
Write-Host ""
Write-Host "Run the setup script first:" -ForegroundColor Yellow
Write-Host " powershell -ExecutionPolicy Bypass -File setup_sumatra.ps1" -ForegroundColor Cyan
Write-Host ""
$response = Read-Host "Continue building without SumatraPDF? (y/n)"
if ($response -ne "y") {
Write-Host "Build cancelled."
Read-Host "Press Enter to exit"
exit 1
}
Write-Host ""
Write-Host "Building without SumatraPDF (PDF printing will not work)..." -ForegroundColor Yellow
$addBinaryArg = @()
} else {
Write-Host "Found: $sumatraPath" -ForegroundColor Green
# Add SumatraPDF as bundled binary (will be embedded inside the exe)
$addBinaryArg = @("--add-binary", "$sumatraPath;.")
}
Write-Host ""
Write-Host "[5/6] Cleaning old build artifacts..." -ForegroundColor Cyan
if (Test-Path "dist") { Remove-Item -Recurse -Force "dist" } if (Test-Path "dist") { Remove-Item -Recurse -Force "dist" }
if (Test-Path "build") { Remove-Item -Recurse -Force "build" } if (Test-Path "build") { Remove-Item -Recurse -Force "build" }
Remove-Item -Force "*.spec" -ErrorAction SilentlyContinue Remove-Item -Force "*.spec" -ErrorAction SilentlyContinue
Write-Host "" Write-Host ""
Write-Host "[5/5] Building executable with PyInstaller..." -ForegroundColor Cyan Write-Host "[6/6] Building executable with PyInstaller..." -ForegroundColor Cyan
Write-Host "This may take 5-15 minutes, please wait..." Write-Host "This may take 5-15 minutes, please wait..."
Write-Host "" Write-Host ""
@@ -73,6 +99,11 @@ $pyinstallerArgs = @(
"-y" "-y"
) )
# Add SumatraPDF binary if available (bundles inside the exe)
if ($addBinaryArg) {
$pyinstallerArgs += $addBinaryArg
}
pyinstaller @pyinstallerArgs pyinstaller @pyinstallerArgs
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Host "" Write-Host ""

BIN
dist/LabelPrinter.exe vendored Normal file

Binary file not shown.

View File

@@ -38,15 +38,42 @@ class LabelPrinterApp(App):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.available_printers = self.get_available_printers() # Build printer display names and mapping to full names
full_printers = get_available_printers()
self.printer_display_map = {} # display_name -> full_name
self.available_printers = []
for full_name in full_printers:
display_name = self._shorten_printer_name(full_name)
# Ensure unique display names
if display_name in self.printer_display_map:
display_name = full_name[:20]
self.printer_display_map[display_name] = full_name
self.available_printers.append(display_name)
# Clean old PDF backup files on startup # Clean old PDF backup files on startup
self.cleanup_old_pdfs() self.cleanup_old_pdfs()
# Clean old log files on startup # Clean old log files on startup
self.cleanup_old_logs() self.cleanup_old_logs()
def get_available_printers(self): def _shorten_printer_name(self, name, max_len=20):
"""Get list of available printers (cross-platform)""" """Shorten printer name for display (max 20 chars).
return get_available_printers() For network printers like \\\\server\\printer, show just the printer part."""
if name.startswith('\\\\'):
# Network printer: \\server\printer -> extract printer name
parts = name.strip('\\').split('\\')
if len(parts) >= 2:
short = parts[-1] # Just the printer name
else:
short = name
else:
short = name
# Truncate to max_len
if len(short) > max_len:
short = short[:max_len]
return short
def _get_full_printer_name(self, display_name):
"""Resolve display name back to full printer name for printing."""
return self.printer_display_map.get(display_name, display_name)
def cleanup_old_pdfs(self, days=5): def cleanup_old_pdfs(self, days=5):
""" """
@@ -278,7 +305,8 @@ class LabelPrinterApp(App):
values=self.available_printers, values=self.available_printers,
size_hint_y=None, size_hint_y=None,
height=45, height=45,
font_size='12sp' font_size='12sp',
sync_height=True,
) )
self.printer_spinner = printer_spinner self.printer_spinner = printer_spinner
form_layout.add_widget(printer_spinner) form_layout.add_widget(printer_spinner)
@@ -320,7 +348,8 @@ class LabelPrinterApp(App):
sap_nr = self.sap_input.text.strip() sap_nr = self.sap_input.text.strip()
quantity = self.qty_input.text.strip() quantity = self.qty_input.text.strip()
cable_id = self.cable_id_input.text.strip() cable_id = self.cable_id_input.text.strip()
printer = self.printer_spinner.text # Resolve display name to full printer name
printer = self._get_full_printer_name(self.printer_spinner.text)
# Validate input # Validate input
if not sap_nr and not quantity and not cable_id: if not sap_nr and not quantity and not cable_id:
@@ -364,7 +393,7 @@ class LabelPrinterApp(App):
# Use Clock.schedule_once to update UI from main thread # Use Clock.schedule_once to update UI from main thread
Clock.schedule_once(lambda dt: popup.dismiss(), 0) Clock.schedule_once(lambda dt: popup.dismiss(), 0)
Clock.schedule_once(lambda dt: self.show_popup("Success", "Label printed successfully!"), 0.1) Clock.schedule_once(lambda dt: self.show_popup("Success", "Label printed successfully!", auto_dismiss=True), 0.1)
# Clear inputs after successful print (but keep printer selection) # Clear inputs after successful print (but keep printer selection)
Clock.schedule_once(lambda dt: self.clear_inputs(), 0.2) Clock.schedule_once(lambda dt: self.clear_inputs(), 0.2)
else: else:
@@ -391,8 +420,14 @@ class LabelPrinterApp(App):
self.cable_id_input.text = '' self.cable_id_input.text = ''
# Printer selection is NOT cleared - it persists until user changes it # Printer selection is NOT cleared - it persists until user changes it
def show_popup(self, title, message): def show_popup(self, title, message, auto_dismiss=False):
"""Show a popup message""" """Show a popup message
Args:
title (str): Popup title
message (str): Popup message
auto_dismiss (bool): If True, popup will auto-dismiss after 3 seconds
"""
popup = Popup( popup = Popup(
title=title, title=title,
content=BoxLayout( content=BoxLayout(
@@ -411,6 +446,10 @@ class LabelPrinterApp(App):
popup.open() popup.open()
# Auto-dismiss after 3 seconds if requested
if auto_dismiss:
Clock.schedule_once(lambda dt: popup.dismiss(), 3)
if __name__ == '__main__': if __name__ == '__main__':
app = LabelPrinterApp() app = LabelPrinterApp()

View File

@@ -3,6 +3,7 @@ import barcode
from barcode.writer import ImageWriter from barcode.writer import ImageWriter
import time import time
import os import os
import sys
import datetime import datetime
import platform import platform
import subprocess import subprocess
@@ -249,38 +250,146 @@ def print_to_printer(printer_name, file_path):
return True return True
elif SYSTEM == "Windows": elif SYSTEM == "Windows":
# Windows: Use win32print API for reliable printing (supports UNC paths) # Windows: Print PDF silently without any viewer opening
try: try:
if WIN32_AVAILABLE: if WIN32_AVAILABLE:
import win32print import win32print
import win32api import win32api
# Set the target printer as default temporarily, then print if file_path.endswith('.pdf'):
# This approach works reliably with both local and UNC printer paths # Try silent printing methods (no viewer opens)
try: import os
old_default = win32print.GetDefaultPrinter() import winreg
except:
old_default = None
try: printed = False
win32print.SetDefaultPrinter(printer_name)
win32api.ShellExecute(0, "print", file_path, None, ".", 0) # Method 1: SumatraPDF (bundled inside exe or external)
print(f"Label sent to printer: {printer_name}") sumatra_paths = []
finally:
# Restore original default printer # Get the directory where this script/exe is running
if old_default: 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)
# Also check app directory 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"))
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"))
# 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:
subprocess.run([
sumatra_path,
"-print-to",
printer_name,
file_path,
"-print-settings",
"fit,landscape",
"-silent",
"-exit-when-done"
], check=False, creationflags=subprocess.CREATE_NO_WINDOW)
print(f"Label sent to printer via SumatraPDF: {printer_name}")
printed = True
break
except Exception as e:
print(f"SumatraPDF error: {e}")
# Method 2: Adobe Reader silent printing
if not printed:
adobe_path = None
for key_path in [
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\AcroRd32.exe",
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Acrobat.exe"
]:
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path)
adobe_path, _ = winreg.QueryValueEx(key, "")
winreg.CloseKey(key)
break
except:
pass
if adobe_path and os.path.exists(adobe_path):
try:
subprocess.run([
adobe_path,
"/t", # Print and close
file_path,
printer_name
], check=False, creationflags=subprocess.CREATE_NO_WINDOW)
print(f"Label sent to printer via Adobe Reader: {printer_name}")
printed = True
except:
pass
# Method 3: GhostScript (if installed)
if not printed:
gs_paths = [
r"C:\Program Files\gs\gs10.02.1\bin\gswin64c.exe",
r"C:\Program Files (x86)\gs\gs10.02.1\bin\gswin32c.exe",
]
# Try to find gswin in PATH
try: try:
win32print.SetDefaultPrinter(old_default) gs_result = subprocess.run(['where', 'gswin64c'],
capture_output=True, text=True, check=False)
if gs_result.returncode == 0:
gs_paths.insert(0, gs_result.stdout.strip().split('\n')[0])
except: except:
pass pass
return True
else: for gs_path in gs_paths:
# Fallback: Open with default printer if os.path.exists(gs_path):
if file_path.endswith('.pdf'): try:
os.startfile(file_path, "print") subprocess.run([
gs_path,
"-dNOPAUSE", "-dBATCH", "-dQUIET",
f"-sDEVICE=mswinpr2",
f"-sOutputFile=%printer%{printer_name}",
file_path
], check=False, creationflags=subprocess.CREATE_NO_WINDOW)
print(f"Label sent to printer via GhostScript: {printer_name}")
printed = True
break
except:
pass
if not printed:
# Fallback: Let user know and save PDF
print("=" * 60)
print("NOTICE: Silent PDF printing requires SumatraPDF")
print("SumatraPDF not found (should be bundled inside the app)")
print("If you built the app yourself, ensure SumatraPDF.exe is downloaded first.")
print("Run: setup_sumatra.ps1 before building")
print("=" * 60)
print(f"PDF saved to: {file_path}")
print("The PDF can be printed manually.")
return True
else: else:
subprocess.run(['notepad', '/p', file_path], check=False) # Non-PDF files
print(f"Label sent to default printer") subprocess.run(['notepad', '/p', file_path],
check=False,
creationflags=subprocess.CREATE_NO_WINDOW)
print(f"Label sent to printer: {printer_name}")
return True
else:
print("win32print not available, PDF saved as backup only")
return True return True
except Exception as e: except Exception as e:
print(f"Windows print error: {e}") print(f"Windows print error: {e}")

95
setup_sumatra.ps1 Normal file
View File

@@ -0,0 +1,95 @@
# Download and Setup SumatraPDF Portable for Label Printer
# This script downloads SumatraPDF portable and sets up the deployment structure
Write-Host ""
Write-Host "========================================================"
Write-Host " Label Printer - SumatraPDF Setup"
Write-Host "========================================================"
Write-Host ""
# Create SumatraPDF folder if it doesn't exist
$sumatraFolder = "SumatraPDF"
if (-not (Test-Path $sumatraFolder)) {
New-Item -ItemType Directory -Path $sumatraFolder -Force | Out-Null
}
# Check if SumatraPDF.exe already exists
$sumatraExe = Join-Path $sumatraFolder "SumatraPDF.exe"
if (Test-Path $sumatraExe) {
Write-Host "[OK] SumatraPDF.exe already exists at: $sumatraExe" -ForegroundColor Green
Write-Host ""
Read-Host "Press Enter to exit"
exit 0
}
Write-Host "[1/3] Downloading SumatraPDF portable (64-bit, ~5 MB)..." -ForegroundColor Cyan
# SumatraPDF download URL (latest stable version)
$url = "https://www.sumatrapdfreader.org/dl/rel/3.5.2/SumatraPDF-3.5.2-64.zip"
$zipFile = "SumatraPDF-temp.zip"
try {
# Download with progress
$progressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $url -OutFile $zipFile -ErrorAction Stop
Write-Host "[OK] Download complete" -ForegroundColor Green
} catch {
Write-Host "[ERROR] Failed to download SumatraPDF" -ForegroundColor Red
Write-Host "Error: $_" -ForegroundColor Red
Write-Host ""
Write-Host "Please download manually from:" -ForegroundColor Yellow
Write-Host " https://www.sumatrapdfreader.org/download-free-pdf-viewer" -ForegroundColor Yellow
Write-Host " Extract SumatraPDF.exe to the 'SumatraPDF' folder" -ForegroundColor Yellow
Write-Host ""
Read-Host "Press Enter to exit"
exit 1
}
Write-Host ""
Write-Host "[2/3] Extracting..." -ForegroundColor Cyan
try {
# Extract ZIP file
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($zipFile, $sumatraFolder)
Write-Host "[OK] Extraction complete" -ForegroundColor Green
} catch {
Write-Host "[ERROR] Failed to extract ZIP file" -ForegroundColor Red
Write-Host "Error: $_" -ForegroundColor Red
Read-Host "Press Enter to exit"
exit 1
}
Write-Host ""
Write-Host "[3/3] Cleaning up..." -ForegroundColor Cyan
# Remove temporary ZIP file
if (Test-Path $zipFile) {
Remove-Item $zipFile -Force
}
Write-Host "[OK] Cleanup complete" -ForegroundColor Green
Write-Host ""
# Verify installation
if (Test-Path $sumatraExe) {
Write-Host "========================================================"
Write-Host " SETUP SUCCESSFUL!" -ForegroundColor Green
Write-Host "========================================================"
Write-Host ""
Write-Host "SumatraPDF portable is now installed at:" -ForegroundColor Green
Write-Host " $sumatraExe" -ForegroundColor Green
Write-Host ""
Write-Host "The Label Printer app will now be able to print PDFs silently." -ForegroundColor Green
Write-Host ""
} else {
Write-Host "========================================================"
Write-Host " SETUP INCOMPLETE" -ForegroundColor Yellow
Write-Host "========================================================"
Write-Host ""
Write-Host "Could not find SumatraPDF.exe after extraction." -ForegroundColor Yellow
Write-Host "Please check the SumatraPDF folder and ensure SumatraPDF.exe is present." -ForegroundColor Yellow
Write-Host ""
}
Read-Host "Press Enter to exit"