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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user