Complete label printer redesign: file monitoring, SVG templates, sharp print quality

- Redesigned GUI for automatic file monitoring workflow
- Changed label format to 35x25mm landscape
- Implemented SVG template support with variable substitution
- Added configuration auto-save/load (conf/app.conf)
- Added system tray minimize functionality
- Fixed print quality: landscape orientation, vector fonts, 600 DPI
- Auto-clear file after print to prevent duplicates
- All popups auto-dismiss after 2-3 seconds
- Semicolon separator for data format (article;nr_art;serial)
- SumatraPDF integration with noscale settings
- Printer configured for outline fonts (sharp output)
- Reorganized documentation into documentation/ folder
This commit is contained in:
NAME
2026-02-12 22:25:51 +02:00
parent 0743c44051
commit 8954135f93
51 changed files with 1209 additions and 6396 deletions

View File

@@ -1,116 +1,223 @@
"""
PDF-based Label Printing Module
Generates high-quality PDF labels with barcodes for printing.
Uses reportlab for superior PDF generation compared to PNG rasterization.
Simplified layout: no borders, just field names and barcodes.
Generates high-quality PDF labels with image and text.
Uses reportlab for superior PDF generation.
Layout: 35mm x 25mm landscape - supports SVG templates with variable substitution.
"""
from reportlab.lib.pagesizes import landscape
from reportlab.lib.units import cm, mm
from reportlab.pdfgen import canvas
from barcode import Code128
from barcode.writer import ImageWriter
from reportlab.graphics import renderPDF
import io
from PIL import Image
import os
import tempfile
import datetime
import re
import traceback
# SVG support
try:
from svglib.svglib import svg2rlg
SVG_AVAILABLE = True
except ImportError:
SVG_AVAILABLE = False
print("Warning: svglib not available. Install with: pip install svglib")
try:
import cairosvg
CAIROSVG_AVAILABLE = True
except (ImportError, OSError) as e:
CAIROSVG_AVAILABLE = False
# Only show warning if it's an import error, not missing Cairo library
if isinstance(e, ImportError):
print("Warning: cairosvg not available. Install with: pip install cairosvg")
class PDFLabelGenerator:
"""Generate high-quality PDF labels with barcodes"""
"""Generate high-quality PDF labels with image and text"""
def __init__(self, label_width=11.5, label_height=8, dpi=300):
def __init__(self, label_width=3.5, label_height=2.5, dpi=600):
"""
Initialize PDF label generator.
Args:
label_width (float): Width in cm (default 11.5 cm)
label_height (float): Height in cm (default 8 cm)
dpi (int): DPI for barcode generation (default 300 for print quality)
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)
"""
self.label_width = label_width * cm
self.label_height = label_height * cm
self.dpi = dpi
self.margin = 3 * mm # Minimal margin
self.margin = 1 * mm # Minimal margin
def generate_barcode_image(self, value, height_mm=18):
def load_image(self, image_path):
"""
Generate barcode image from text value.
Load and prepare image for embedding in PDF.
Args:
value (str): Text to encode in barcode (max 25 chars)
height_mm (int): Barcode height in mm (default 18mm for 1.8cm)
image_path (str): Path to image file
Returns:
PIL.Image or None: Generated barcode image
PIL.Image or None: Loaded image
"""
if not value or not value.strip():
if not image_path or not os.path.exists(image_path):
print(f"Image not found: {image_path}")
return None
try:
# Truncate to 25 characters (Code128 limitation)
value_truncated = value.strip()[:25]
# Create barcode
barcode_instance = Code128(value_truncated, writer=ImageWriter())
# Generate in memory using a temporary directory
temp_dir = tempfile.gettempdir()
temp_name = f"barcode_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S_%f')}"
temp_base_path = os.path.join(temp_dir, temp_name)
# Barcode options - generate at high DPI for quality
options = {
'write_text': False,
'module_width': 0.5, # Width of each bar in mm
'module_height': 8, # Height in mm
'quiet_zone': 2,
'font_size': 0
}
barcode_instance.save(temp_base_path, options=options)
# The barcode.save() adds .png extension automatically
temp_path = temp_base_path + '.png'
# Load and return image
img = Image.open(temp_path)
img = Image.open(image_path)
# Convert to RGB if needed
if img.mode != 'RGB':
if img.mode not in ['RGB', 'L']:
img = img.convert('RGB')
# Clean up temp file
try:
os.remove(temp_path)
except:
pass
return img
except Exception as e:
# Log error but don't fail silently
print(f"Barcode generation error for '{value}': {e}")
print(f"Image loading error: {e}")
return None
def create_label_pdf(self, sap_nr, cantitate, lot_number, filename=None):
def replace_svg_variables(self, svg_content, variables):
"""
Create a PDF label with three rows of data and barcodes.
Each row shows label name, barcode, and value text.
Replace variables in SVG template with actual values.
Args:
sap_nr (str): SAP article number
cantitate (str): Quantity value
lot_number (str): Lot/Cable ID
svg_content (str): SVG file content as string
variables (dict): Dictionary of variable replacements
Returns:
str: SVG content with replaced variables
"""
# Replace placeholders like {Article}, {NrArt}, {Serial}
for key, value in variables.items():
placeholder = f"{{{key}}}"
svg_content = svg_content.replace(placeholder, str(value or ''))
return svg_content
def create_label_from_svg_template(self, template_path, variables, filename=None):
"""
Create PDF label from SVG template with variable substitution.
Args:
template_path (str): Path to SVG template file
variables (dict): Dictionary with keys: Article, NrArt, Serial
filename (str): Output filename (if None, returns bytes)
Returns:
bytes or str: PDF content as bytes or filename if saved
"""
if not os.path.exists(template_path):
print(f"SVG template not found: {template_path}")
return None
try:
# Read SVG template
with open(template_path, 'r', encoding='utf-8') as f:
svg_content = f.read()
# Replace variables
svg_content = self.replace_svg_variables(svg_content, variables)
# Save modified SVG to temp file
temp_svg = tempfile.NamedTemporaryFile(mode='w', suffix='.svg', delete=False, encoding='utf-8')
temp_svg.write(svg_content)
temp_svg.close()
temp_svg_path = temp_svg.name
# Convert SVG to PDF
if filename:
pdf_output = filename
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)
if CAIROSVG_AVAILABLE:
try:
# Render at high DPI for sharp output
cairosvg.svg2pdf(url=temp_svg_path, write_to=pdf_output, dpi=self.dpi)
# 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 cairo_err:
print(f"CairoSVG conversion failed: {cairo_err}")
print("SVG conversion failed. svglib and cairosvg both unavailable or failed.")
return None
except Exception as e:
print(f"SVG template error: {e}")
traceback.print_exc()
return None
def create_label_pdf(self, comanda, article, serial, filename=None, image_path=None, svg_template=None):
"""
Create a PDF label with image on left and text on right.
Label: 35mm x 25mm landscape
Layout: 1/3 left = image, 2/3 right = 3 rows of text
Or use SVG template if provided.
Args:
comanda (str): Nr. Comanda value
article (str): Nr. Art. value
serial (str): Serial No. value
filename (str): Output filename (if None, returns bytes)
image_path (str): Path to accepted.png image
svg_template (str): Path to SVG template file (optional)
Returns:
bytes or str: PDF content as bytes or filename if saved
"""
# If SVG template is provided, use it instead
if svg_template and os.path.exists(svg_template):
variables = {
'Article': comanda,
'NrArt': article,
'Serial': serial,
'Comanda': comanda, # Alternative name
}
return self.create_label_from_svg_template(svg_template, variables, filename)
# Fallback to standard layout
# Prepare data for rows
rows_data = [
("SAP-Nr", sap_nr),
("Cantitate", cantitate),
("Lot Nr", lot_number),
("Nr. Comanda:", comanda),
("Nr. Art.:", article),
("Serial No.:", serial),
]
# Create PDF canvas in memory or to file
@@ -122,101 +229,76 @@ class PDFLabelGenerator:
# Create canvas with label dimensions
c = canvas.Canvas(pdf_buffer, pagesize=(self.label_width, self.label_height))
# Set higher resolution for better quality
c.setPageCompression(1) # Enable compression
# Calculate dimensions
usable_width = self.label_width - 2 * self.margin
row_height = (self.label_height - 2 * self.margin) / 3
usable_height = self.label_height - 2 * self.margin
# Draw each row - label name, barcode, and value text
for idx, (label_name, value) in enumerate(rows_data):
y_position = self.label_height - self.margin - (idx + 1) * row_height
# Draw label name (small, at top of row)
c.setFont("Helvetica-Bold", 8)
c.drawString(
self.margin,
y_position + row_height - 3 * mm,
label_name
)
# Generate and draw barcode if value exists
if value and value.strip():
barcode_value = value.strip()[:25]
# Fixed barcode height: 1.6 cm (16mm) for optimal readability
barcode_height_mm = 16
barcode_height = barcode_height_mm * mm
try:
barcode_img = self.generate_barcode_image(barcode_value, height_mm=barcode_height_mm)
# Image area: 1/3 of width on the left
image_width = usable_width / 3
image_x = self.margin
image_y = self.margin
image_height = usable_height
# Text area: 2/3 of width on the right
text_area_x = self.margin + image_width + 1 * mm # Small gap
text_area_width = usable_width - image_width - 1 * mm
row_height = usable_height / 3
# Draw image on left if available
if image_path and os.path.exists(image_path):
try:
img = self.load_image(image_path)
if img:
# Save temp file for reportlab
temp_img_file = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
temp_img_path = temp_img_file.name
temp_img_file.close()
if barcode_img:
# Calculate barcode dimensions
max_barcode_width = usable_width - 2 * mm
aspect_ratio = barcode_img.width / barcode_img.height
barcode_width = barcode_height * aspect_ratio
# Constrain width to fit in label
if barcode_width > max_barcode_width:
barcode_width = max_barcode_width
# Save barcode temporarily and draw on PDF
barcode_temp = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
barcode_path = barcode_temp.name
barcode_temp.close()
barcode_img.save(barcode_path, 'PNG')
# Position barcode vertically centered in middle of row
barcode_y = y_position + (row_height - barcode_height) / 2
# Draw barcode image
c.drawImage(
barcode_path,
self.margin,
barcode_y,
width=barcode_width,
height=barcode_height,
preserveAspectRatio=True
)
# Draw small text below barcode showing the value
c.setFont("Helvetica", 6)
c.drawString(
self.margin,
barcode_y - 3 * mm,
f"({barcode_value})"
)
# Clean up
try:
os.remove(barcode_path)
except:
pass
else:
# If barcode generation failed, show text
c.setFont("Helvetica", 10)
c.drawString(
self.margin,
y_position + row_height / 2,
f"[No Barcode: {barcode_value}]"
)
except Exception as e:
# Fallback: draw value as text with error indicator
print(f"PDF barcode error: {e}")
c.setFont("Helvetica", 10)
c.drawString(
self.margin,
y_position + row_height / 2,
f"[Text: {barcode_value}]"
# Convert to grayscale for black and white
img_bw = img.convert('L')
img_bw.save(temp_img_path, 'PNG')
# Draw image maintaining aspect ratio
c.drawImage(
temp_img_path,
image_x,
image_y,
width=image_width,
height=image_height,
preserveAspectRatio=True,
anchor='c'
)
# Clean up
try:
os.remove(temp_img_path)
except:
pass
except Exception as e:
print(f"Error drawing image: {e}")
# Draw text rows on right
for idx, (label_name, value) in enumerate(rows_data):
# Calculate y position for this row (top to bottom)
y_position = self.label_height - self.margin - (idx * row_height) - row_height/2
# Draw text with better quality
if value and value.strip():
text = f"{label_name} {value.strip()}"
else:
# Empty value - show placeholder
c.setFont("Helvetica", 8)
c.drawString(
self.margin,
y_position + row_height / 2,
"(empty)"
)
text = f"{label_name} -"
# Use appropriate font size to fit (6pt = ~2.1mm height)
font_size = 6
c.setFont("Helvetica-Bold", font_size)
try:
c.drawString(text_area_x, y_position, text)
except Exception as e:
print(f"Error drawing text row {idx}: {e}")
# Save PDF
c.save()
@@ -228,15 +310,16 @@ class PDFLabelGenerator:
pdf_buffer.seek(0)
return pdf_buffer.getvalue()
def create_label_pdf_file(self, sap_nr, cantitate, lot_number, filename=None):
def create_label_pdf_file(self, comanda, article, serial, filename=None, image_path=None):
"""
Create PDF label file and return the filename.
Args:
sap_nr (str): SAP article number
cantitate (str): Quantity value
lot_number (str): Lot/Cable ID
comanda (str): Nr. Comanda value
article (str): Nr. Art. value
serial (str): Serial No. value
filename (str): Output filename (if None, auto-generates)
image_path (str): Path to accepted.png image
Returns:
str: Path to created PDF file
@@ -245,51 +328,75 @@ class PDFLabelGenerator:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"label_{timestamp}.pdf"
return self.create_label_pdf(sap_nr, cantitate, lot_number, filename)
return self.create_label_pdf(comanda, article, serial, filename, image_path)
def create_label_pdf_simple(text):
def create_label_pdf_simple(text, image_path=None, svg_template=None):
"""
Simple wrapper to create PDF from combined text (SAP|CANTITATE|LOT).
Simple wrapper to create PDF from combined text (article;nr_art;serial).
Args:
text (str): Combined text in format "SAP|CANTITATE|LOT"
text (str): Combined text in format "article;nr_art;serial"
image_path (str): Path to accepted.png image
svg_template (str): Path to SVG template file (optional, checks conf/label_template.svg by default)
Returns:
bytes: PDF content
"""
# Parse text
parts = text.split('|') if '|' in text else [text, '', '']
sap_nr = parts[0].strip() if len(parts) > 0 else ''
cantitate = parts[1].strip() if len(parts) > 1 else ''
lot_number = parts[2].strip() if len(parts) > 2 else ''
# Parse text - using semicolon separator
parts = text.split(';') if ';' in text else [text, '', '']
article = parts[0].strip() if len(parts) > 0 else ''
nr_art = parts[1].strip() if len(parts) > 1 else ''
serial = parts[2].strip() if len(parts) > 2 else ''
# Use default image path if not provided
if not image_path:
image_path = os.path.join('conf', 'accepted.png')
# Check for default SVG template if not provided
if not svg_template:
default_svg = os.path.join('conf', 'label_template.svg')
if os.path.exists(default_svg):
svg_template = default_svg
generator = PDFLabelGenerator()
pdf_bytes = generator.create_label_pdf(sap_nr, cantitate, lot_number)
pdf_bytes = generator.create_label_pdf(article, nr_art, serial, None, image_path, svg_template)
return pdf_bytes
def create_label_pdf_file(text, filename=None):
def create_label_pdf_file(text, filename=None, image_path=None, svg_template=None):
"""
Create PDF label file from combined text.
Args:
text (str): Combined text in format "SAP|CANTITATE|LOT"
text (str): Combined text in format "article;nr_art;serial"
filename (str): Output filename (auto-generates if None)
image_path (str): Path to accepted.png image
svg_template (str): Path to SVG template file (optional, checks conf/label_template.svg by default)
Returns:
str: Path to created PDF file
"""
# Parse text
parts = text.split('|') if '|' in text else [text, '', '']
sap_nr = parts[0].strip() if len(parts) > 0 else ''
cantitate = parts[1].strip() if len(parts) > 1 else ''
lot_number = parts[2].strip() if len(parts) > 2 else ''
# Parse text - using semicolon separator
parts = text.split(';') if ';' in text else [text, '', '']
article = parts[0].strip() if len(parts) > 0 else ''
nr_art = parts[1].strip() if len(parts) > 1 else ''
serial = parts[2].strip() if len(parts) > 2 else ''
if not filename:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"label_{timestamp}.pdf"
# Use default image path if not provided
if not image_path:
image_path = os.path.join('conf', 'accepted.png')
# Check for default SVG template if not provided
if not svg_template:
default_svg = os.path.join('conf', 'label_template.svg')
if os.path.exists(default_svg):
svg_template = default_svg
generator = PDFLabelGenerator()
return generator.create_label_pdf(sap_nr, cantitate, lot_number, filename)
return generator.create_label_pdf(article, nr_art, serial, filename, image_path, svg_template)