Files
adaptronic_label-printer/print_label_pdf.py

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)