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/
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(
['label_printer_gui.py'],
pathex=[],
binaries=[],
binaries=[('SumatraPDF\\SumatraPDF.exe', '.')],
datas=[],
hiddenimports=['kivy', 'PIL', 'barcode', 'reportlab', 'print_label', 'print_label_pdf'],
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 "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) {
Write-Host "ERROR: Failed to install dependencies" -ForegroundColor Red
Read-Host "Press Enter to exit"
@@ -47,13 +47,39 @@ if ($LASTEXITCODE -ne 0) {
}
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 "build") { Remove-Item -Recurse -Force "build" }
Remove-Item -Force "*.spec" -ErrorAction SilentlyContinue
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 ""
@@ -73,6 +99,11 @@ $pyinstallerArgs = @(
"-y"
)
# Add SumatraPDF binary if available (bundles inside the exe)
if ($addBinaryArg) {
$pyinstallerArgs += $addBinaryArg
}
pyinstaller @pyinstallerArgs
if ($LASTEXITCODE -ne 0) {
Write-Host ""

BIN
dist/LabelPrinter.exe vendored

Binary file not shown.

View File

@@ -38,15 +38,42 @@ class LabelPrinterApp(App):
def __init__(self, **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
self.cleanup_old_pdfs()
# Clean old log files on startup
self.cleanup_old_logs()
def get_available_printers(self):
"""Get list of available printers (cross-platform)"""
return get_available_printers()
def _shorten_printer_name(self, name, max_len=20):
"""Shorten printer name for display (max 20 chars).
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):
"""
@@ -278,7 +305,8 @@ class LabelPrinterApp(App):
values=self.available_printers,
size_hint_y=None,
height=45,
font_size='12sp'
font_size='12sp',
sync_height=True,
)
self.printer_spinner = printer_spinner
form_layout.add_widget(printer_spinner)
@@ -320,7 +348,8 @@ class LabelPrinterApp(App):
sap_nr = self.sap_input.text.strip()
quantity = self.qty_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
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
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)
Clock.schedule_once(lambda dt: self.clear_inputs(), 0.2)
else:
@@ -391,8 +420,14 @@ class LabelPrinterApp(App):
self.cable_id_input.text = ''
# Printer selection is NOT cleared - it persists until user changes it
def show_popup(self, title, message):
"""Show a popup message"""
def show_popup(self, title, message, auto_dismiss=False):
"""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(
title=title,
content=BoxLayout(
@@ -411,6 +446,10 @@ class LabelPrinterApp(App):
popup.open()
# Auto-dismiss after 3 seconds if requested
if auto_dismiss:
Clock.schedule_once(lambda dt: popup.dismiss(), 3)
if __name__ == '__main__':
app = LabelPrinterApp()

View File

@@ -3,6 +3,7 @@ import barcode
from barcode.writer import ImageWriter
import time
import os
import sys
import datetime
import platform
import subprocess
@@ -41,38 +42,21 @@ def get_available_printers():
return list(printers.keys()) if printers else ["PDF"]
elif SYSTEM == "Windows":
# Windows: Get both local and network printers
# Windows: Get local + connected printers (includes print server connections)
try:
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:
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]
if printer_name and printer_name not in printers:
printers.append(printer_name)
except:
pass
# 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
except Exception as e:
print(f"Error enumerating printers: {e}")
# Add PDF as fallback option
if "PDF" not in printers:
@@ -266,23 +250,146 @@ def print_to_printer(printer_name, file_path):
return True
elif SYSTEM == "Windows":
# Windows: Use win32print or open with default printer
# Windows: Print PDF silently without any viewer opening
try:
if WIN32_AVAILABLE:
import win32print
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}")
return True
else:
# Fallback: Open with default printer
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")
print("win32print not available, PDF saved as backup only")
return True
except Exception as 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"