445 lines
17 KiB
Python
445 lines
17 KiB
Python
"""
|
|
PDF-based Label Printing Module
|
|
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 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 image and text"""
|
|
|
|
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 1200 for high quality thermal printer)
|
|
"""
|
|
self.label_width = label_width * cm
|
|
self.label_height = label_height * cm
|
|
# label_width (3.5 cm) > label_height (2.5 cm) → page is already landscape
|
|
self.page_size = (self.label_width, self.label_height)
|
|
self.dpi = dpi
|
|
self.margin = 1 * mm # Minimal margin
|
|
|
|
def load_image(self, image_path):
|
|
"""
|
|
Load and prepare image for embedding in PDF.
|
|
|
|
Args:
|
|
image_path (str): Path to image file
|
|
|
|
Returns:
|
|
PIL.Image or None: Loaded image
|
|
"""
|
|
if not image_path or not os.path.exists(image_path):
|
|
print(f"Image not found: {image_path}")
|
|
return None
|
|
|
|
try:
|
|
img = Image.open(image_path)
|
|
# 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}")
|
|
return None
|
|
|
|
def replace_svg_variables(self, svg_content, variables):
|
|
"""
|
|
Replace variables in SVG template with actual values.
|
|
|
|
Args:
|
|
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
|
|
|
|
# Use cairosvg FIRST as it handles fonts and complex SVGs better
|
|
if CAIROSVG_AVAILABLE:
|
|
try:
|
|
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:
|
|
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}, trying svglib...")
|
|
|
|
# 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:
|
|
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 = [
|
|
("Nr. Comanda:", comanda),
|
|
("Nr. Art.:", article),
|
|
("Serial No.:", serial),
|
|
]
|
|
|
|
# Create PDF canvas in memory or to file
|
|
if filename:
|
|
pdf_buffer = filename
|
|
else:
|
|
pdf_buffer = io.BytesIO()
|
|
|
|
# Create canvas with label dimensions - explicitly landscape
|
|
c = canvas.Canvas(pdf_buffer, pagesize=self.page_size)
|
|
|
|
# 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
|
|
usable_height = self.label_height - 2 * self.margin
|
|
|
|
# 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()
|
|
|
|
# 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 with high quality
|
|
c.drawImage(
|
|
temp_img_path,
|
|
image_x,
|
|
image_y,
|
|
width=image_width,
|
|
height=image_height,
|
|
preserveAspectRatio=True,
|
|
anchor='c',
|
|
mask='auto' # Better quality rendering
|
|
)
|
|
|
|
# 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:
|
|
text = f"{label_name} -"
|
|
|
|
# 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:
|
|
print(f"Error drawing text row {idx}: {e}")
|
|
|
|
# Save PDF
|
|
c.save()
|
|
|
|
# Return filename or bytes
|
|
if filename:
|
|
return filename
|
|
else:
|
|
pdf_buffer.seek(0)
|
|
return pdf_buffer.getvalue()
|
|
|
|
def create_label_pdf_file(self, comanda, article, serial, filename=None, image_path=None):
|
|
"""
|
|
Create PDF label file and return the filename.
|
|
|
|
Args:
|
|
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
|
|
"""
|
|
if not filename:
|
|
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
filename = f"label_{timestamp}.pdf"
|
|
|
|
return self.create_label_pdf(comanda, article, serial, filename, image_path)
|
|
|
|
|
|
def create_label_pdf_simple(text, image_path=None, svg_template=None):
|
|
"""
|
|
Simple wrapper to create PDF from combined text (article;nr_art;serial).
|
|
|
|
Args:
|
|
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 - 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(article, nr_art, serial, None, image_path, svg_template)
|
|
|
|
return pdf_bytes
|
|
|
|
|
|
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 "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 - 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(article, nr_art, serial, filename, image_path, svg_template)
|