From 839828340dfdd20ebc8420eae3d3dfb64b0a1679 Mon Sep 17 00:00:00 2001 From: NAME Date: Fri, 13 Feb 2026 23:34:59 +0200 Subject: [PATCH] Add template selection and multiple copy printing features - Implemented template selection: type 0=OK (green), type 1=NOK (red) - Added multiple copy printing (1-100 copies) - Extended file format to 5 fields: ARTICLE;NR_ART;SERIAL;TYPE;COUNT - Created OK/NOK SVG templates with visual distinction - Fixed PDF landscape orientation issues - Updated SumatraPDF to use noscale for exact dimensions - Auto-generate conf folder with default templates on first run - Made pystray optional for system tray functionality - Updated build scripts for Python 3.13 compatibility (Kivy 2.3+, PyInstaller 6.18) - Added comprehensive build documentation - Improved printer configuration guidance --- .gitignore | 8 +- BUILD_EXECUTABLE.md | 303 +++++++++++++++++++++++++++++++ build_exe.py | 6 + build_windows.bat | 10 +- check_pdf_size.py | 20 ++ conf/label_template_nok.svg | 64 +++++++ conf/label_template_ok.svg | 63 +++++++ configure_printer_paper_size.ps1 | 83 +++++++++ label_printer_gui.py | 206 +++++++++++++++++++-- print_label.py | 288 +++++++++++++++++++++++------ print_label_pdf.py | 132 +++++++++----- requirements_windows.txt | 2 +- test_conf_generation.py | 63 +++++++ 13 files changed, 1131 insertions(+), 117 deletions(-) create mode 100644 BUILD_EXECUTABLE.md create mode 100644 check_pdf_size.py create mode 100644 conf/label_template_nok.svg create mode 100644 conf/label_template_ok.svg create mode 100644 configure_printer_paper_size.ps1 create mode 100644 test_conf_generation.py diff --git a/.gitignore b/.gitignore index 96a9dad..7edc1e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ label/ build/ +dist/ logs/ pdf_backup/ venv/ - +__pycache__/ +*.pyc +*.pyo +*.spec +*.egg-info/ +.eggs/ diff --git a/BUILD_EXECUTABLE.md b/BUILD_EXECUTABLE.md new file mode 100644 index 0000000..0f34560 --- /dev/null +++ b/BUILD_EXECUTABLE.md @@ -0,0 +1,303 @@ +# Build Windows Executable (.exe) + +## Quick Start + +**Option 1: One Command (Recommended)** +```bash +build_windows.bat +``` + +**Option 2: Python Script** +```bash +python build_exe.py +``` + +**Option 3: Direct PyInstaller** +```bash +pyinstaller LabelPrinter.spec +``` + +--- + +## Requirements + +- **Windows** 10/11 +- **Python** 3.10 - 3.13 (Python 3.14+ may have compatibility issues) +- **Dependencies** installed (see below) + +--- + +## Step-by-Step Build Process + +### 1. Install Python Dependencies + +If not already installed: +```bash +pip install -r requirements_windows.txt +``` + +Or install manually: +```bash +pip install python-barcode pillow reportlab kivy==2.2.1 pyinstaller==6.1.0 pywin32 wmi watchdog svglib cairosvg +``` + +### 2. Build the Executable + +Run the build script: +```bash +build_windows.bat +``` + +This will: +- ✅ Install/upgrade dependencies +- ✅ Clean old build artifacts +- ✅ Create `LabelPrinter.exe` in `dist/` folder +- ⏱️ Takes 5-15 minutes + +### 3. Test the Executable + +```bash +cd dist +LabelPrinter.exe +``` + +--- + +## What Gets Included + +The executable is **self-contained** and includes: +- ✅ All Python code +- ✅ All dependencies (Kivy, ReportLab, svglib, cairosvg, etc.) +- ✅ Auto-generates `conf/` folder on first run +- ✅ Auto-generates `pdf_backup/` and `logs/` folders + +**On first run, the app automatically creates:** +``` +LabelPrinter.exe location/ +├── conf/ +│ ├── app.conf (default settings) +│ ├── label_template.svg (default template) +│ ├── label_template_ok.svg (OK labels - green) +│ └── label_template_nok.svg (NOK labels - red) +├── pdf_backup/ (generated PDFs) +└── logs/ (print logs) +``` + +--- + +## Output + +After successful build: + +``` +📁 dist/ + └── LabelPrinter.exe (Single file, ~80-120 MB) +``` + +**File size**: 80-120 MB (includes Python runtime + all libraries) + +--- + +## Distribution + +### To Deploy on Other Windows Machines: + +**Simply copy `LabelPrinter.exe` to the target machine!** + +The executable is **100% self-contained**: +- ✅ No Python installation needed +- ✅ No dependencies needed +- ✅ Auto-creates all required folders on first run +- ✅ Generates default templates automatically + +**First run will create:** +``` +[wherever you put LabelPrinter.exe]/ +├── LabelPrinter.exe ← Copy just this file! +├── conf/ ← Auto-generated on first run +│ ├── app.conf +│ ├── label_template.svg +│ ├── label_template_ok.svg +│ └── label_template_nok.svg +├── pdf_backup/ ← Auto-generated +└── logs/ ← Auto-generated +``` + +**To customize templates on target machine:** +1. Run `LabelPrinter.exe` once (generates conf folder) +2. Edit SVG files in the `conf/` folder +3. Restart the application + +**Requirements on target machine:** +- ✅ Windows 10/11 +- ✅ Thermal printer configured (35mm x 25mm paper size) +- ⚠️ No other software required! + +--- + +## Troubleshooting + +### Build Fails - Missing Dependencies +```bash +pip install --upgrade pip setuptools wheel +pip install -r requirements_windows.txt +``` + +### Build Fails - PyInstaller Error +```bash +pip install --upgrade pyinstaller==6.1.0 +``` + +### Executable Won't Start +- Check Windows Defender didn't quarantine it +- Run as Administrator once to register +- Check Event Viewer for errors + +### Executable is Huge (>150 MB) +This is normal! It includes: +- Python runtime (~40 MB) +- Kivy framework (~30 MB) +- ReportLab, PIL, etc. (~20 MB) +- Your code + data (~10 MB) + +### First Run is Slow +- First launch takes 10-30 seconds (Kivy initialization) +- Subsequent launches are faster (~2-5 seconds) +- This is normal behavior + +--- + +## Advanced Options + +### Build with Console (for debugging) +Edit `build_windows.bat` and remove `--windowed`: +```bat +pyinstaller label_printer_gui.py ^ + --onefile ^ + --name=LabelPrinter ^ + ... +``` + +### Add Custom Icon +```bat +pyinstaller label_printer_gui.py ^ + --onefile ^ + --windowed ^ + --icon=icon.ico ^ + ... +``` + +### Reduce File Size +Not recommended, but possible: +```bat +pyinstaller label_printer_gui.py ^ + --onefile ^ + --windowed ^ + --strip ^ + --exclude-module=tkinter ^ + --exclude-module=matplotlib ^ + ... +``` + +--- + +## What's Built + +**Source Files Compiled:** +- `label_printer_gui.py` (Main GUI) +- `print_label.py` (Printing logic) +- `print_label_pdf.py` (PDF generation) + +**Data Files Included:** +- `conf/app.conf` (Settings) +- `conf/label_template.svg` (Default template) +- `conf/label_template_ok.svg` (OK labels - green) +- `conf/label_template_nok.svg` (NOK labels - red) +- `conf/accepted.png` (Checkmark image) + +**Output Folders:** +- `pdf_backup/` - Stores generated PDFs +- `logs/` - Stores print logs + +--- + +## Build Times + +- **First build**: 10-15 minutes (PyInstaller analyzes all dependencies) +- **Rebuild**: 5-10 minutes (cached analysis) +- **Clean build**: 10-15 minutes (removed cache) + +--- + +## File Structure After Build + +``` +📁 Project Root/ +├── 📄 label_printer_gui.py +├── 📄 print_label.py +├── 📄 print_label_pdf.py +├── 📄 build_windows.bat ← Run this +├── 📄 build_exe.py +├── 📄 LabelPrinter.spec +│ +├── 📁 dist/ +│ ├── 📄 LabelPrinter.exe ← Your executable! +│ ├── 📁 conf/ +│ ├── 📁 pdf_backup/ +│ └── 📁 logs/ +│ +├── 📁 build/ (temp files, can delete) +└── 📁 conf/ + ├── app.conf + ├── label_template.svg + ├── label_template_ok.svg + └── label_template_nok.svg +``` + +--- + +## Clean Build + +To remove all build artifacts: +```bash +rmdir /s /q build +rmdir /s /q dist +del *.spec +``` + +Then rebuild: +```bash +build_windows.bat +``` + +--- + +## Testing + +After building, test with: + +1. **Run the executable**: + ```bash + dist\LabelPrinter.exe + ``` + +2. **Configure monitoring**: + - File: `C:\Users\Public\Documents\check.txt` + +3. **Test print**: + - Add to check.txt: `COM-001;ART-123;SN-001;0;1` + - Should print 1 OK label + +4. **Test template switching**: + - `COM-002;ART-456;SN-002;1;1` → NOK label (red) + +--- + +## Support + +If build fails, check: +1. Python version (3.10-3.13 required) +2. All dependencies installed +3. Windows 10/11 (not Windows 7/8) +4. Disk space (need ~500MB for build) +5. Antivirus not blocking PyInstaller diff --git a/build_exe.py b/build_exe.py index 79d271c..1cda591 100644 --- a/build_exe.py +++ b/build_exe.py @@ -46,6 +46,12 @@ args = [ '--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', ] if __name__ == '__main__': diff --git a/build_windows.bat b/build_windows.bat index 8fdb3fc..b33edf8 100644 --- a/build_windows.bat +++ b/build_windows.bat @@ -39,8 +39,8 @@ echo. REM Install dependencies echo [3/5] Installing dependencies... -echo 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 +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 ( echo ERROR: Failed to install dependencies pause @@ -72,6 +72,12 @@ pyinstaller label_printer_gui.py ^ --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 ^ -y if errorlevel 1 ( diff --git a/check_pdf_size.py b/check_pdf_size.py new file mode 100644 index 0000000..3966087 --- /dev/null +++ b/check_pdf_size.py @@ -0,0 +1,20 @@ +from reportlab.lib.pagesizes import landscape +from reportlab.lib.utils import ImageReader +from reportlab.pdfgen import canvas +import os + +# 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') + diff --git a/conf/label_template_nok.svg b/conf/label_template_nok.svg new file mode 100644 index 0000000..9ab1d55 --- /dev/null +++ b/conf/label_template_nok.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nr. Comanda:{Article} + + + Nr. Art.:{NrArt} + + + Serial No.:{Serial} + + + + + + + + diff --git a/conf/label_template_ok.svg b/conf/label_template_ok.svg new file mode 100644 index 0000000..843c10f --- /dev/null +++ b/conf/label_template_ok.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Nr. Comanda:{Article} + + + Nr. Art.:{NrArt} + + + Serial No.:{Serial} + + + + + + + + diff --git a/configure_printer_paper_size.ps1 b/configure_printer_paper_size.ps1 new file mode 100644 index 0000000..225b045 --- /dev/null +++ b/configure_printer_paper_size.ps1 @@ -0,0 +1,83 @@ +# Configure Thermal Printer for 35mm x 25mm Labels +# Run as Administrator if needed + +Write-Host "====================================" -ForegroundColor Cyan +Write-Host "Label Printer Paper Size Setup" -ForegroundColor Cyan +Write-Host "Target: 35mm x 25mm (Landscape)" -ForegroundColor Cyan +Write-Host "====================================" -ForegroundColor Cyan +Write-Host "" + +# Get list of printers +$printers = Get-Printer | Select-Object Name, DriverName, PortName +Write-Host "Available Printers:" -ForegroundColor Yellow +$printers | Format-Table -AutoSize + +Write-Host "" +Write-Host "MANUAL CONFIGURATION STEPS:" -ForegroundColor Green +Write-Host "===========================" -ForegroundColor Green +Write-Host "" +Write-Host "1. Open Control Panel > Devices and Printers" -ForegroundColor White +Write-Host "2. Right-click your thermal printer (e.g., Citizen CTS-310 or Zebra ZD420)" -ForegroundColor White +Write-Host "3. Select 'Printing Preferences'" -ForegroundColor White +Write-Host "4. Look for 'Paper Size' or 'Media' settings:" -ForegroundColor White +Write-Host " - Size: Custom or 35mm x 25mm" -ForegroundColor Cyan +Write-Host " - Width: 35mm (1.38 inches)" -ForegroundColor Cyan +Write-Host " - Height: 25mm (0.98 inches)" -ForegroundColor Cyan +Write-Host " - Orientation: Landscape" -ForegroundColor Cyan +Write-Host " - Media Type: LABELS (not Continuous)" -ForegroundColor Cyan +Write-Host "5. Click 'Apply' and 'OK'" -ForegroundColor White +Write-Host "" +Write-Host "IMPORTANT FOR ZEBRA PRINTERS:" -ForegroundColor Red +Write-Host "- Must set Media Type = 'Labels' (enables gap sensor)" -ForegroundColor Yellow +Write-Host "- If set to 'Continuous', printer will print on entire roll" -ForegroundColor Yellow +Write-Host "" +Write-Host "CRITICAL PRINTER SETTINGS:" -ForegroundColor Magenta +Write-Host "- Darkness: 10-15 (thermal printers)" -ForegroundColor White +Write-Host "- Speed: Medium" -ForegroundColor White +Write-Host "- Print Mode: Thermal Transfer or Direct Thermal" -ForegroundColor White +Write-Host "" + +# Show printer properties access +Write-Host "To open printer properties directly, run:" -ForegroundColor Green +Write-Host 'control printers' -ForegroundColor Cyan +Write-Host "" + +# Check if running as admin +$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if ($isAdmin) { + Write-Host "✓ Running as Administrator" -ForegroundColor Green +} else { + Write-Host "⚠ Not running as Administrator (some settings may require elevation)" -ForegroundColor Yellow +} + +Write-Host "" +Write-Host "For Citizen CTS-310 printers:" -ForegroundColor Cyan +Write-Host " 1. Open Devices and Printers" -ForegroundColor White +Write-Host " 2. Right-click 'Citizen CTS-310II' > Printer Properties" -ForegroundColor White +Write-Host " 3. Device Settings tab > Form to Tray Assignment" -ForegroundColor White +Write-Host " 4. Click 'Create Form' button" -ForegroundColor White +Write-Host " 5. Form name: Label35x25" -ForegroundColor White +Write-Host " 6. Width: 35mm, Height: 25mm" -ForegroundColor White +Write-Host " 7. Save and select this form in Printing Preferences" -ForegroundColor White +Write-Host "" + +Write-Host "For Zebra ZD420/ZD421 printers:" -ForegroundColor Cyan +Write-Host " 1. Open Zebra Setup Utility (ZSU)" -ForegroundColor White +Write-Host " 2. Select your printer" -ForegroundColor White +Write-Host " 3. Click 'Printer Configuration'" -ForegroundColor White +Write-Host " 4. Media section:" -ForegroundColor White +Write-Host " - Media Type: Label Stock" -ForegroundColor Cyan +Write-Host " - Label Width: 35mm" -ForegroundColor Cyan +Write-Host " - Label Height: 25mm" -ForegroundColor Cyan +Write-Host " 5. Print section:" -ForegroundColor White +Write-Host " - Darkness: 10-15" -ForegroundColor Cyan +Write-Host " - Print Speed: 4 ips (medium)" -ForegroundColor Cyan +Write-Host " 6. Click 'Send to Printer'" -ForegroundColor White +Write-Host "" + +Write-Host "After configuration, test print with:" -ForegroundColor Green +Write-Host " python label_printer_gui.py" -ForegroundColor Cyan +Write-Host "" +Write-Host "Press any key to open Printer settings..." -ForegroundColor Yellow +$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") +control printers diff --git a/label_printer_gui.py b/label_printer_gui.py index 6920277..8fa91e1 100644 --- a/label_printer_gui.py +++ b/label_printer_gui.py @@ -28,10 +28,17 @@ from print_label import print_label_standalone, get_available_printers from kivy.clock import Clock from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler -import pystray -from pystray import MenuItem as item from PIL import Image, ImageDraw +# Optional system tray support +try: + import pystray + from pystray import MenuItem as item + PYSTRAY_AVAILABLE = True +except ImportError: + PYSTRAY_AVAILABLE = False + print("Warning: pystray not available. System tray functionality disabled.") + # Set window size - portrait/phone dimensions (375x667 like iPhone) # Adjusted to be slightly wider for touch-friendly UI Window.size = (420, 700) @@ -63,6 +70,8 @@ class LabelPrinterApp(App): def __init__(self, **kwargs): super().__init__(**kwargs) + # Initialize conf folder with default templates + self.initialize_conf_folder() # Build printer display names and mapping to full names full_printers = get_available_printers() self.printer_display_map = {} # display_name -> full_name @@ -109,6 +118,107 @@ class LabelPrinterApp(App): """Resolve display name back to full printer name for printing.""" return self.printer_display_map.get(display_name, display_name) + def initialize_conf_folder(self): + """ + Initialize conf folder with default templates and configuration. + Creates folder and files if they don't exist. + """ + conf_dir = 'conf' + + # Create conf folder if it doesn't exist + if not os.path.exists(conf_dir): + os.makedirs(conf_dir, exist_ok=True) + print(f"Created conf folder: {conf_dir}") + + # Define default SVG template (minimal 35mm x 25mm with placeholders) + default_svg_template = ''' + + + + + + Nr. Comanda: {Article} + Nr. Art.: {NrArt} + Serial No.: {Serial} + + + +''' + + # OK template (green checkmark) + ok_svg_template = ''' + + + + + + Nr. Comanda: {Article} + Nr. Art.: {NrArt} + Serial No.: {Serial} + OK + + + +''' + + # NOK template (red text and checkmark) + nok_svg_template = ''' + + + + + + Nr. Comanda: {Article} + Nr. Art.: {NrArt} + Serial No.: {Serial} + NOK + + + +''' + + # Create SVG template files if they don't exist + templates = { + 'label_template.svg': default_svg_template, + 'label_template_ok.svg': ok_svg_template, + 'label_template_nok.svg': nok_svg_template, + } + + for filename, content in templates.items(): + file_path = os.path.join(conf_dir, filename) + if not os.path.exists(file_path): + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + print(f"Created template: {file_path}") + except Exception as e: + print(f"Failed to create {file_path}: {e}") + + # Create default app.conf if it doesn't exist + config_path = os.path.join(conf_dir, 'app.conf') + if not os.path.exists(config_path): + default_config = '''[Settings] +file_path = C:\\Users\\Public\\Documents\\check.txt +printer = PDF +''' + try: + with open(config_path, 'w', encoding='utf-8') as f: + f.write(default_config) + print(f"Created config: {config_path}") + except Exception as e: + print(f"Failed to create {config_path}: {e}") + def cleanup_old_pdfs(self, days=5): """ Delete PDF files older than specified days from pdf_backup folder. @@ -469,6 +579,10 @@ class LabelPrinterApp(App): """ Minimize the application to system tray. """ + if not PYSTRAY_AVAILABLE: + print("System tray not available - window will minimize normally") + return + if self.is_minimized: return @@ -652,9 +766,13 @@ class LabelPrinterApp(App): print(f"Error clearing file: {e}") def read_file_variables(self): - """Read variables from monitored file""" + """Read variables from monitored file + Expected format: article;nr_art;serial;template_type;count + template_type: 0=OK, 1=NOK + count: number of labels to print (default=1) + """ if not self.monitored_file or not os.path.exists(self.monitored_file): - return None, None, None + return None, None, None, 0, 1 try: with open(self.monitored_file, 'r', encoding='utf-8') as f: @@ -662,14 +780,16 @@ class LabelPrinterApp(App): # Skip if file is empty or only contains "-" (cleared marker) if not content or content == '-': - return None, None, None + return None, None, None, 0, 1 - # Parse file content - expecting format: article;nr_art;serial + # Parse file content - expecting format: article;nr_art;serial;template_type;count # or key=value pairs on separate lines lines = content.split('\n') article = "" nr_art = "" serial = "" + template_type = 0 # Default to OK template + count = 1 # Default to 1 label # Try semicolon-separated format first if ';' in content: @@ -680,6 +800,20 @@ class LabelPrinterApp(App): nr_art = parts[1].strip() if len(parts) >= 3: serial = parts[2].strip() + if len(parts) >= 4: + try: + template_type = int(parts[3].strip()) + except ValueError: + template_type = 0 + if len(parts) >= 5: + try: + count = int(parts[4].strip()) + if count < 1: + count = 1 + if count > 100: # Safety limit + count = 100 + except ValueError: + count = 1 else: # Try key=value format for line in lines: @@ -694,16 +828,30 @@ class LabelPrinterApp(App): nr_art = value elif key in ['serial', 'serial_no', 'serial-no', 'serialno']: serial = value + elif key in ['template', 'template_type', 'type']: + try: + template_type = int(value) + except ValueError: + template_type = 0 + elif key in ['count', 'quantity', 'copies']: + try: + count = int(value) + if count < 1: + count = 1 + if count > 100: + count = 100 + except ValueError: + count = 1 - return article, nr_art, serial + return article, nr_art, serial, template_type, count except Exception as e: print(f"Error reading file: {e}") return None, None, None def print_label(self, instance): """Handle print button press - read from file and print""" - # Read variables from file - article, nr_art, serial = self.read_file_variables() + # Read variables from file including template type and count + article, nr_art, serial, template_type, count = self.read_file_variables() # Resolve display name to full printer name printer = self._get_full_printer_name(self.printer_spinner.text) @@ -713,10 +861,23 @@ class LabelPrinterApp(App): self.show_popup("Error", "No data in file or file not set", auto_dismiss_after=3) return + # Select template based on template_type + if template_type == 1: + template_path = os.path.join('conf', 'label_template_nok.svg') + template_name = "NOK" + else: + template_path = os.path.join('conf', 'label_template_ok.svg') + template_name = "OK" + + # Verify template exists, fallback to default if not + if not os.path.exists(template_path): + template_path = os.path.join('conf', 'label_template.svg') + template_name = "DEFAULT" + # Create combined label text using semicolon separator label_text = f"{article};{nr_art};{serial}" - # Show loading popup + # Show loading popup with count info popup = Popup( title='Printing', content=BoxLayout( @@ -727,15 +888,32 @@ class LabelPrinterApp(App): size_hint=(0.8, 0.3) ) - popup.content.add_widget(Label(text='Processing label...\nPlease wait')) + popup.content.add_widget(Label(text=f'Processing {count} label(s)...\nTemplate: {template_name}\nPlease wait')) popup.open() # Print in background thread (using PDF by default) def print_thread(): pdf_filename = None success = False + all_success = True try: - success = print_label_standalone(label_text, printer, preview=0, use_pdf=True) + # 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) # Get the PDF filename that was created # Files are saved to pdf_backup/ with timestamp @@ -744,7 +922,7 @@ class LabelPrinterApp(App): # Get the most recently created PDF file pdf_filename = max(pdf_files, key=os.path.getctime) - if success: + if all_success: # Log the successful print action self.log_print_action(article, nr_art, serial, printer, pdf_filename or "unknown", True) @@ -753,7 +931,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!", auto_dismiss_after=3), 0.1) + Clock.schedule_once(lambda dt: self.show_popup("Success", f"{count} label(s) printed successfully!", auto_dismiss_after=3), 0.1) else: # Log the failed print action self.log_print_action(article, nr_art, serial, printer, pdf_filename or "unknown", False) diff --git a/print_label.py b/print_label.py index 3cf6c17..7e8d57f 100644 --- a/print_label.py +++ b/print_label.py @@ -3,6 +3,7 @@ import barcode from barcode.writer import ImageWriter import time import os +import sys import datetime import platform import subprocess @@ -173,7 +174,14 @@ def create_label_image(text): # Resize barcode to fit in row width barcode_width = label_width - left_margin - 10 barcode_height = row_height - 25 - barcode_resized = barcode_img.resize((barcode_width, barcode_height), Image.LANCZOS) + # 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 @@ -194,13 +202,14 @@ def create_label_image(text): return label_img -def create_label_pdf(text): +def create_label_pdf(text, svg_template=None): """ Create a high-quality PDF label with 3 rows: label + barcode for each field. PDFs are saved to the pdf_backup folder. Args: text (str): Combined text in format "article;nr_art;serial" or single value + svg_template (str): Path to specific SVG template to use (optional) Returns: str: Path to the generated PDF file @@ -221,11 +230,17 @@ def create_label_pdf(text): timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") pdf_filename = os.path.join(pdf_backup_dir, f"final_label_{timestamp}.pdf") - # Check for default SVG template - svg_template = None - default_svg = os.path.join('conf', 'label_template.svg') - if os.path.exists(default_svg): - svg_template = default_svg + # Use SVG template for customizable layout + if svg_template is None or not os.path.exists(svg_template): + # Try default templates + default_svg = os.path.join('conf', 'label_template.svg') + if os.path.exists(default_svg): + svg_template = default_svg + print(f"Using SVG template: {default_svg}") + else: + print("SVG template not found, using fallback PDF generation") + else: + print(f"Using SVG template: {svg_template}") # Check for default image path image_path = os.path.join('conf', 'accepted.png') @@ -233,9 +248,94 @@ def create_label_pdf(text): return generator.create_label_pdf(article, nr_art, serial, pdf_filename, image_path, svg_template) +def configure_printer_quality(printer_name, width_mm=35, height_mm=25): + """ + 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 + """ + if SYSTEM != "Windows" or not WIN32_AVAILABLE: + return False + + try: + 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) + + except Exception as e: + print(f"Could not configure printer quality: {e}") + return False + + def print_to_printer(printer_name, file_path): """ Print file to printer (cross-platform). + Uses SumatraPDF for silent printing on Windows. Args: printer_name (str): Name of printer or "PDF" for PDF output @@ -258,66 +358,145 @@ def print_to_printer(printer_name, file_path): return True elif SYSTEM == "Windows": - # Windows: Print PDF using various methods + # Windows: Print PDF silently without any viewer opening try: + if WIN32_AVAILABLE: + import win32print + import win32api + if file_path.endswith('.pdf'): - # Method 1: Try SumatraPDF for best silent printing - sumatra_paths = [ - os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf', 'SumatraPDF.exe'), - os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf', 'SumatraPDF-portable.exe'), + # 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")) + 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", - os.path.expandvars(r"%LOCALAPPDATA%\SumatraPDF\SumatraPDF.exe"), - ] + ]) - sumatra_found = False for sumatra_path in sumatra_paths: if os.path.exists(sumatra_path): - sumatra_found = True try: - # Use SumatraPDF silent printing with high quality settings - # noscale = print at actual size without scaling - # landscape = force landscape orientation for 35x25mm labels - # Note: SumatraPDF uses printer driver's quality settings + # Use noscale with paper size specification for thermal printers + # Format: "noscale,paper=" where paper size matches PDF (35mm x 25mm) + # SumatraPDF will use the PDF's page size when noscale is used subprocess.run([ sumatra_path, - '-print-to', printer_name, - '-silent', - '-print-settings', 'noscale,landscape', - file_path - ], check=True, creationflags=subprocess.CREATE_NO_WINDOW) + "-print-to", + printer_name, + file_path, + "-print-settings", + "noscale", # Preserve exact PDF page dimensions + "-silent", + "-exit-when-done" + ], check=False, creationflags=subprocess.CREATE_NO_WINDOW) print(f"Label sent to printer via SumatraPDF: {printer_name}") - return True - except Exception as sumatra_err: - print(f"SumatraPDF print failed: {sumatra_err}") + print(f"Note: Printer '{printer_name}' should be configured for 35mm x 25mm labels") + printed = True break + except Exception as e: + print(f"SumatraPDF error: {e}") - # Method 2: Use ShellExecute with printto (requires default PDF viewer) - if not sumatra_found or WIN32_AVAILABLE: - try: - import win32api - win32api.ShellExecute( - 0, "printto", file_path, - f'"{printer_name}"', ".", 0 - ) - print(f"Label sent to printer via ShellExecute: {printer_name}") - return True - except Exception as shell_err: - print(f"ShellExecute print failed: {shell_err}") - # Method 3: Open PDF with default viewer (last resort) + # 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: - os.startfile(file_path, "print") - print(f"Opened PDF for printing: {file_path}") - print("Please close the PDF viewer after printing.") - return True - except Exception as startfile_err: - print(f"Could not open PDF: {startfile_err}") - print("PDF saved as backup only") - return True + 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("=" * 60) + print(f"PDF saved to: {file_path}") + print("The PDF can be printed manually.") + + return True else: - # Non-PDF files: print silently with notepad - subprocess.run(['notepad', '/p', file_path], - check=False, + # 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 @@ -343,7 +522,7 @@ def print_to_printer(printer_name, file_path): return True -def print_label_standalone(value, printer, preview=0, use_pdf=True): +def print_label_standalone(value, printer, preview=0, use_pdf=True, svg_template=None): """ Print a label with the specified text on the specified printer. Always generates a PDF backup in pdf_backup and prints that PDF. @@ -353,6 +532,7 @@ def print_label_standalone(value, printer, preview=0, use_pdf=True): 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) Returns: bool: True if printing was successful, False otherwise @@ -370,7 +550,7 @@ def print_label_standalone(value, printer, preview=0, use_pdf=True): # Always generate a PDF backup and print that PDF for verification try: - pdf_file = create_label_pdf(value) + 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}") diff --git a/print_label_pdf.py b/print_label_pdf.py index 75481a9..d93caf2 100644 --- a/print_label_pdf.py +++ b/print_label_pdf.py @@ -38,17 +38,19 @@ except (ImportError, OSError) as e: class PDFLabelGenerator: """Generate high-quality PDF labels with image and text""" - def __init__(self, label_width=3.5, label_height=2.5, dpi=600): + def __init__(self, label_width=3.5, label_height=2.5, dpi=1200): """ Initialize PDF label generator. Args: label_width (float): Width in cm (default 3.5 cm = 35mm) label_height (float): Height in cm (default 2.5 cm = 25mm) - dpi (int): DPI for image rendering (default 600 for high quality print) + dpi (int): DPI for image rendering (default 1200 for high quality thermal printer) """ 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) self.dpi = dpi self.margin = 1 * mm # Minimal margin @@ -68,9 +70,11 @@ class PDFLabelGenerator: try: img = Image.open(image_path) - # Convert to RGB if needed - if img.mode not in ['RGB', 'L']: + # Convert to RGB for best quality (don't use grayscale) + if img.mode != 'RGB': img = img.convert('RGB') + # Set DPI information for high-quality output + img.info['dpi'] = (self.dpi, self.dpi) return img except Exception as e: print(f"Image loading error: {e}") @@ -130,42 +134,25 @@ class PDFLabelGenerator: else: pdf_output = tempfile.NamedTemporaryFile(suffix='.pdf', delete=False).name - # Try svglib first (more portable, no external dependencies) - if SVG_AVAILABLE: - try: - drawing = svg2rlg(temp_svg_path) - if drawing: - # Render at original size - quality depends on PDF rendering - # The PDF will contain vector graphics for sharp output - renderPDF.drawToFile(drawing, pdf_output) - - # Clean up temp SVG - try: - os.remove(temp_svg_path) - except: - pass - - if filename: - return pdf_output - else: - with open(pdf_output, 'rb') as f: - pdf_bytes = f.read() - os.remove(pdf_output) - return pdf_bytes - except Exception as svg_err: - print(f"svglib conversion failed: {svg_err}, trying cairosvg...") - - # Fallback: Try cairosvg (requires system Cairo library) + # Use cairosvg FIRST as it handles fonts and complex SVGs better if CAIROSVG_AVAILABLE: try: - # Render at high DPI for sharp output - cairosvg.svg2pdf(url=temp_svg_path, write_to=pdf_output, dpi=self.dpi) + print("Converting SVG to PDF using CairoSVG (high quality)...") + # CRITICAL: Let CairoSVG read dimensions from SVG file (width="35mm" height="25mm") + # DO NOT specify output_width/output_height as they control raster size, not PDF page size + # The SVG already has the correct dimensions, just render at high DPI + cairosvg.svg2pdf( + url=temp_svg_path, + write_to=pdf_output, + dpi=300 # High DPI for sharp output, page size comes from SVG + ) # Clean up temp SVG try: os.remove(temp_svg_path) except: pass + print(f"✅ PDF created from SVG template: {pdf_output}") if filename: return pdf_output else: @@ -174,9 +161,54 @@ class PDFLabelGenerator: os.remove(pdf_output) return pdf_bytes except Exception as cairo_err: - print(f"CairoSVG conversion failed: {cairo_err}") + print(f"CairoSVG conversion failed: {cairo_err}, trying svglib...") - print("SVG conversion failed. svglib and cairosvg both unavailable or failed.") + # Fallback: Try svglib (generates many warnings but works) + if SVG_AVAILABLE: + try: + print("Converting SVG to PDF using svglib (fallback)...") + drawing = svg2rlg(temp_svg_path) + if drawing: + # CRITICAL: Force exact label dimensions (35mm x 25mm landscape) + # Convert to points: 1mm = 2.834645669 points + from reportlab.lib.units import mm + from reportlab.pdfgen import canvas as pdf_canvas + + target_width = 35 * mm + target_height = 25 * mm + + # Scale drawing to exact size + if drawing.width > 0 and drawing.height > 0: + scale_x = target_width / drawing.width + scale_y = target_height / drawing.height + drawing.width = target_width + drawing.height = target_height + drawing.scale(scale_x, scale_y) + + # Create PDF with explicit landscape page size + c = pdf_canvas.Canvas(pdf_output, pagesize=(target_width, target_height)) + c.setPageCompression(0) # No compression for quality + renderPDF.draw(drawing, c, 0, 0) + c.save() + + # Clean up temp SVG + try: + os.remove(temp_svg_path) + except: + pass + + print(f"✅ PDF created from SVG template: {pdf_output}") + if filename: + return pdf_output + else: + with open(pdf_output, 'rb') as f: + pdf_bytes = f.read() + os.remove(pdf_output) + return pdf_bytes + except Exception as svg_err: + print(f"svglib conversion failed: {svg_err}") + + print("❌ SVG conversion failed. Both cairosvg and svglib unavailable or failed.") return None except Exception as e: @@ -226,11 +258,15 @@ class PDFLabelGenerator: else: pdf_buffer = io.BytesIO() - # Create canvas with label dimensions - c = canvas.Canvas(pdf_buffer, pagesize=(self.label_width, self.label_height)) + # Create canvas with label dimensions - explicitly landscape + c = canvas.Canvas(pdf_buffer, pagesize=self.page_size) - # Set higher resolution for better quality - c.setPageCompression(1) # Enable compression + # CRITICAL: Disable compression for maximum print quality + c.setPageCompression(0) # Disable compression for best quality + + # Set high resolution for crisp output on thermal printers + # Page size already set to landscape orientation + c._pagesize = self.page_size # Calculate dimensions usable_width = self.label_width - 2 * self.margin @@ -257,11 +293,11 @@ class PDFLabelGenerator: temp_img_path = temp_img_file.name temp_img_file.close() - # Convert to grayscale for black and white - img_bw = img.convert('L') - img_bw.save(temp_img_path, 'PNG') + # Keep as RGB for better quality (thermal printers handle conversion) + # Save at high DPI for sharp output + img.save(temp_img_path, 'PNG', dpi=(self.dpi, self.dpi), optimize=False) - # Draw image maintaining aspect ratio + # Draw image maintaining aspect ratio with high quality c.drawImage( temp_img_path, image_x, @@ -269,7 +305,8 @@ class PDFLabelGenerator: width=image_width, height=image_height, preserveAspectRatio=True, - anchor='c' + anchor='c', + mask='auto' # Better quality rendering ) # Clean up @@ -291,10 +328,15 @@ class PDFLabelGenerator: else: text = f"{label_name} -" - # Use appropriate font size to fit (6pt = ~2.1mm height) - font_size = 6 + # IMPROVED: Larger font size for better readability (8pt = ~2.8mm height) + # This is critical for thermal printers - text must be crisp and readable + font_size = 8 c.setFont("Helvetica-Bold", font_size) + # Enable text rendering mode for crisp output + c.setStrokeColorRGB(0, 0, 0) + c.setFillColorRGB(0, 0, 0) + try: c.drawString(text_area_x, y_position, text) except Exception as e: diff --git a/requirements_windows.txt b/requirements_windows.txt index 9d16dae..dc20ee8 100644 --- a/requirements_windows.txt +++ b/requirements_windows.txt @@ -1,6 +1,6 @@ python-barcode pillow -kivy>=2.1.0 +kivy>=2.3.0 reportlab pyinstaller>=6.0.0 pywin32 diff --git a/test_conf_generation.py b/test_conf_generation.py new file mode 100644 index 0000000..cd0325a --- /dev/null +++ b/test_conf_generation.py @@ -0,0 +1,63 @@ +""" +Test script to verify conf folder auto-generation +Run this to test that conf/ folder is created with default templates +""" + +import os +import shutil + +# Temporarily rename existing conf folder to test auto-generation +if os.path.exists('conf'): + print("Backing up existing conf folder to conf_backup...") + if os.path.exists('conf_backup'): + shutil.rmtree('conf_backup') + shutil.move('conf', 'conf_backup') + print("✓ Existing conf folder backed up") + +print("\nTesting conf folder auto-generation...") +print("Starting label_printer_gui initialization...") + +# Import the app (this will trigger initialization) +from label_printer_gui import LabelPrinterApp + +# Create app instance (will auto-create conf folder) +app = LabelPrinterApp() + +# Check if conf folder was created +if os.path.exists('conf'): + print("\n✓ SUCCESS: conf folder created") + + # Check for required files + required_files = [ + 'conf/app.conf', + 'conf/label_template.svg', + 'conf/label_template_ok.svg', + 'conf/label_template_nok.svg' + ] + + missing_files = [] + for file in required_files: + if os.path.exists(file): + print(f" ✓ {file}") + else: + print(f" ✗ {file} MISSING") + missing_files.append(file) + + if not missing_files: + print("\n✓ All required files created successfully!") + else: + print(f"\n✗ Missing files: {missing_files}") +else: + print("\n✗ FAILED: conf folder not created") + +# Restore original conf folder +print("\nRestoring original conf folder...") +if os.path.exists('conf_backup'): + if os.path.exists('conf'): + shutil.rmtree('conf') + shutil.move('conf_backup', 'conf') + print("✓ Original conf folder restored") + +print("\n" + "="*50) +print("Test complete!") +print("="*50)