Compare commits

...

5 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
1cf4482914 updated printer list to show local networ printers 2026-02-06 10:48:19 +02:00
b9025fcabe printing 2026-02-06 10:42:11 +02:00
12 changed files with 412 additions and 49 deletions

3
.gitignore vendored
View File

@@ -1 +1,4 @@
label/ label/
build/
logs/
pdf_backup/

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

@@ -39,7 +39,7 @@ Write-Host ""
Write-Host "[3/5] Installing dependencies..." -ForegroundColor Cyan Write-Host "[3/5] Installing dependencies..." -ForegroundColor Cyan
Write-Host "Installing: python-barcode, pillow, reportlab, kivy, pyinstaller, pywin32, wmi..." Write-Host "Installing: python-barcode, pillow, reportlab, kivy, pyinstaller, pywin32, wmi..."
pip install python-barcode pillow reportlab kivy==2.2.1 pyinstaller==6.1.0 pywin32 wmi pip install python-barcode pillow reportlab kivy pyinstaller pywin32 wmi
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Failed to install dependencies" -ForegroundColor Red Write-Host "ERROR: Failed to install dependencies" -ForegroundColor Red
Read-Host "Press Enter to exit" Read-Host "Press Enter to exit"
@@ -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

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
@@ -41,38 +42,21 @@ def get_available_printers():
return list(printers.keys()) if printers else ["PDF"] return list(printers.keys()) if printers else ["PDF"]
elif SYSTEM == "Windows": elif SYSTEM == "Windows":
# Windows: Get both local and network printers # Windows: Get local + connected printers (includes print server connections)
try: try:
printers = [] printers = []
# Get local printers # PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS captures:
# - Locally installed printers
# - Printers connected from a print server (e.g. \\server\printer)
try: try:
for printer_info in win32print.EnumPrinters(win32print.PRINTER_ENUM_LOCAL): flags = win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS
for printer_info in win32print.EnumPrinters(flags):
printer_name = printer_info[2] printer_name = printer_info[2]
if printer_name and printer_name not in printers: if printer_name and printer_name not in printers:
printers.append(printer_name) printers.append(printer_name)
except: except Exception as e:
pass print(f"Error enumerating printers: {e}")
# Get network printers from print server
try:
for printer_info in win32print.EnumPrinters(win32print.PRINTER_ENUM_NETWORK):
printer_name = printer_info[2]
if printer_name and printer_name not in printers:
printers.append(printer_name)
except:
pass
# Get connected printers (alternative method using WMI)
try:
import wmi
c = wmi.WMI()
for printer in c.Win32_Printer():
printer_name = printer.Name
if printer_name and printer_name not in printers:
printers.append(printer_name)
except:
pass
# Add PDF as fallback option # Add PDF as fallback option
if "PDF" not in printers: if "PDF" not in printers:
@@ -266,23 +250,146 @@ def print_to_printer(printer_name, file_path):
return True return True
elif SYSTEM == "Windows": elif SYSTEM == "Windows":
# Windows: Use win32print or open with default printer # Windows: Print PDF silently without any viewer opening
try: try:
if WIN32_AVAILABLE: if WIN32_AVAILABLE:
import win32print import win32print
import win32api import win32api
# Print using the Windows API
win32api.ShellExecute(0, "print", file_path, f'/d:"{printer_name}"', ".", 0) 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)
# 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:
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:
pass
for gs_path in gs_paths:
if os.path.exists(gs_path):
try:
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:
# Non-PDF files
subprocess.run(['notepad', '/p', file_path],
check=False,
creationflags=subprocess.CREATE_NO_WINDOW)
print(f"Label sent to printer: {printer_name}") print(f"Label sent to printer: {printer_name}")
return True return True
else: else:
# Fallback: Open with default printer print("win32print not available, PDF saved as backup only")
if file_path.endswith('.pdf'):
os.startfile(file_path, "print")
else:
# For images, use default print application
subprocess.run([f'notepad', '/p', file_path], check=False)
print(f"Label sent to default printer")
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"