""" 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 # 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 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)