Spaces:
Runtime error
Runtime error
| """ | |
| Flyer builder module - assembles the final adoption flyer. | |
| """ | |
| from PIL import Image, ImageDraw, ImageFont | |
| import textwrap | |
| import os | |
| from pathlib import Path | |
| def build_flyer(image, bio_text, flyer_data): | |
| """ | |
| Build the final adoption flyer with image, text, and shelter info. | |
| Args: | |
| image: PIL Image of the composited animal photo | |
| bio_text: str, the generated bio | |
| flyer_data: dict with animal and shelter information | |
| Returns: | |
| tuple: (flyer_image, png_path, pdf_path) | |
| """ | |
| # Create flyer canvas (8.5" x 11" at 300 DPI = 2550 x 3300, but we'll use 1200x1600 for digital) | |
| canvas_width = 1200 | |
| canvas_height = 1600 | |
| # Create white canvas | |
| canvas = Image.new('RGB', (canvas_width, canvas_height), 'white') | |
| draw = ImageDraw.Draw(canvas) | |
| # Define layout areas | |
| header_height = 120 | |
| photo_top = header_height + 20 | |
| photo_height = 700 | |
| details_top = photo_top + photo_height + 30 | |
| bio_top = details_top + 120 | |
| footer_top = canvas_height - 100 | |
| # Try to load fonts, fall back to default if not available | |
| try: | |
| # These are common system fonts | |
| title_font = _get_font(size=60, bold=True) | |
| heading_font = _get_font(size=28, bold=True) | |
| body_font = _get_font(size=22) | |
| detail_font = _get_font(size=20) | |
| footer_font = _get_font(size=18) | |
| except Exception as e: | |
| print(f"Font loading issue: {e}") | |
| title_font = heading_font = body_font = detail_font = footer_font = None | |
| # 1. Header - "ADOPT ME!" | |
| header_text = "🐾 ADOPT ME! 🐾" | |
| _draw_centered_text(draw, header_text, canvas_width // 2, 60, title_font, fill='#2E86AB') | |
| # 2. Animal Photo (centered) | |
| photo_width = 900 | |
| photo = image.copy() | |
| photo.thumbnail((photo_width, photo_height), Image.Resampling.LANCZOS) | |
| photo_x = (canvas_width - photo.width) // 2 | |
| photo_y = photo_top + (photo_height - photo.height) // 2 | |
| # Add subtle border around photo | |
| border_padding = 10 | |
| draw.rectangle( | |
| [photo_x - border_padding, photo_y - border_padding, | |
| photo_x + photo.width + border_padding, photo_y + photo.height + border_padding], | |
| outline='#cccccc', | |
| width=3 | |
| ) | |
| canvas.paste(photo, (photo_x, photo_y)) | |
| # 3. Animal Details (name, breed, age, gender) | |
| name = flyer_data.get("name", "Unknown") | |
| breed = flyer_data.get("breed", "Mixed Breed") | |
| age = flyer_data.get("age", "Unknown") | |
| gender = flyer_data.get("gender", "") | |
| species = flyer_data.get("species", "Pet") | |
| # Name (large and centered) | |
| _draw_centered_text(draw, name, canvas_width // 2, details_top, heading_font, fill='#333333') | |
| # Details line | |
| details = f"{species} • {breed} • {age} • {gender}" | |
| _draw_centered_text(draw, details, canvas_width // 2, details_top + 40, detail_font, fill='#666666') | |
| # 4. Bio Text (wrapped) | |
| bio_y = bio_top | |
| bio_margin = 80 | |
| bio_width = canvas_width - (bio_margin * 2) | |
| wrapped_bio = textwrap.fill(bio_text, width=60) | |
| for line in wrapped_bio.split('\n'): | |
| if bio_y > footer_top - 50: # Don't overflow into footer | |
| break | |
| _draw_centered_text(draw, line, canvas_width // 2, bio_y, body_font, fill='#444444') | |
| bio_y += 35 | |
| # 5. Footer - Shelter Contact Info | |
| draw.line([(100, footer_top - 30), (canvas_width - 100, footer_top - 30)], fill='#cccccc', width=2) | |
| shelter_name = flyer_data.get("shelter_name", "Animal Shelter") | |
| shelter_phone = flyer_data.get("shelter_phone", "") | |
| shelter_website = flyer_data.get("shelter_website", "") | |
| _draw_centered_text(draw, shelter_name, canvas_width // 2, footer_top, footer_font, fill='#2E86AB') | |
| contact_text = f"📞 {shelter_phone} • 🌐 {shelter_website}" | |
| _draw_centered_text(draw, contact_text, canvas_width // 2, footer_top + 35, footer_font, fill='#666666') | |
| # Save outputs | |
| output_dir = Path(__file__).parent.parent / "output" | |
| output_dir.mkdir(exist_ok=True) | |
| # Generate filename from animal name | |
| safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_')).strip() | |
| safe_name = safe_name.replace(' ', '_') | |
| png_path = output_dir / f"{safe_name}_flyer.png" | |
| pdf_path = output_dir / f"{safe_name}_flyer.pdf" | |
| # Save PNG | |
| canvas.save(str(png_path), 'PNG', quality=95) | |
| print(f"Saved PNG to {png_path}") | |
| # Save PDF using reportlab | |
| try: | |
| _save_as_pdf(canvas, str(pdf_path)) | |
| print(f"Saved PDF to {pdf_path}") | |
| except Exception as e: | |
| print(f"PDF generation failed: {e}") | |
| pdf_path = None | |
| return canvas, str(png_path), str(pdf_path) if pdf_path else None | |
| def _get_font(size=20, bold=False): | |
| """ | |
| Try to load a system font, fall back to default if unavailable. | |
| Args: | |
| size: int, font size | |
| bold: bool, whether to use bold variant | |
| Returns: | |
| ImageFont object | |
| """ | |
| # Common font paths by OS | |
| font_options = [ | |
| # Windows | |
| "C:/Windows/Fonts/arial.ttf", | |
| "C:/Windows/Fonts/arialbd.ttf" if bold else "C:/Windows/Fonts/arial.ttf", | |
| # macOS | |
| "/System/Library/Fonts/Helvetica.ttc", | |
| "/Library/Fonts/Arial.ttf", | |
| # Linux | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", | |
| ] | |
| for font_path in font_options: | |
| if os.path.exists(font_path): | |
| try: | |
| return ImageFont.truetype(font_path, size) | |
| except Exception: | |
| continue | |
| # Fall back to default font | |
| try: | |
| return ImageFont.load_default() | |
| except Exception: | |
| return None | |
| def _draw_centered_text(draw, text, x, y, font, fill='black'): | |
| """ | |
| Draw text centered at the given x, y position. | |
| Args: | |
| draw: ImageDraw object | |
| text: str, text to draw | |
| x: int, center x position | |
| y: int, top y position | |
| font: ImageFont object | |
| fill: color for text | |
| """ | |
| if font: | |
| bbox = draw.textbbox((0, 0), text, font=font) | |
| text_width = bbox[2] - bbox[0] | |
| text_x = x - text_width // 2 | |
| draw.text((text_x, y), text, font=font, fill=fill) | |
| else: | |
| # Fallback without font (approximation) | |
| text_width = len(text) * 8 # Rough approximation | |
| text_x = x - text_width // 2 | |
| draw.text((text_x, y), text, fill=fill) | |
| def _save_as_pdf(image, pdf_path): | |
| """ | |
| Save the flyer image as a PDF. | |
| Args: | |
| image: PIL Image | |
| pdf_path: str, output path for PDF | |
| """ | |
| from reportlab.pdfgen import canvas as pdf_canvas | |
| from reportlab.lib.pagesizes import letter | |
| from reportlab.lib.units import inch | |
| import io | |
| # Create PDF | |
| c = pdf_canvas.Canvas(pdf_path, pagesize=letter) | |
| width, height = letter | |
| # Convert PIL image to bytes | |
| img_buffer = io.BytesIO() | |
| image.save(img_buffer, format='PNG') | |
| img_buffer.seek(0) | |
| # Calculate dimensions to fit letter size while maintaining aspect ratio | |
| img_width, img_height = image.size | |
| aspect = img_width / img_height | |
| target_width = width - (1 * inch) | |
| target_height = target_width / aspect | |
| if target_height > height - (1 * inch): | |
| target_height = height - (1 * inch) | |
| target_width = target_height * aspect | |
| # Center on page | |
| x = (width - target_width) / 2 | |
| y = (height - target_height) / 2 | |
| # Draw image | |
| c.drawImage(img_buffer, x, y, width=target_width, height=target_height) | |
| # Save PDF | |
| c.save() | |