#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Business风格(商务风格)海报模板 """ import logging import math import random from collections import Counter from typing import Dict, Any, Optional, List, Tuple import numpy as np from PIL import Image, ImageDraw, ImageFont import colorsys from .base_template import BaseTemplate logger = logging.getLogger(__name__) class BusinessTemplate(BaseTemplate): """ 商务风格模板,适用于酒店、房地产、高端服务等场景。 特点是上下图片融合,中间为信息展示区,整体风格沉稳、专业。 """ def __init__(self, size: Tuple[int, int] = (900, 1200)): super().__init__(size) self.config = { 'total_parts': 4.0, 'transparent_ratio': 0.5, 'dynamic_spacing': { 'min_line_spacing': 8, 'max_line_spacing': 25, 'content_margin': 20, 'section_spacing': 35, 'bottom_reserve': 40 } } self.color_themes = { "modern_blue": [(25, 52, 85), (65, 120, 180)], "warm_sunset": [(45, 25, 20), (180, 100, 60)], "fresh_green": [(15, 45, 25), (90, 140, 80)], "deep_ocean": [(20, 40, 70), (70, 140, 200)], "elegant_purple": [(35, 25, 55), (120, 90, 160)], "classic_gray": [(30, 35, 40), (120, 130, 140)], "premium_gold": [(60, 50, 30), (160, 140, 100)], "tech_gradient": [(20, 30, 50), (80, 100, 140)] } self._current_background_colors = None def generate(self, top_image_path: str, bottom_image_path: str, content: Dict[str, Any], small_image_paths: Optional[List[str]] = None, color_theme: Optional[str] = None, **kwargs) -> Image.Image: """ 生成Business风格海报 Args: top_image_path (str): 顶部图像路径 bottom_image_path (str): 底部图像路径 content (Dict[str, Any]): 包含所有文本信息的字典 small_image_paths (Optional[List[str]]): 中间小图路径列表 (最多3张) color_theme (Optional[str]): 预设颜色主题名称 Returns: Image.Image: 生成的海报图像 """ top_img = self.image_processor.load_image(top_image_path) bottom_img = self.image_processor.load_image(bottom_image_path) if not top_img or not bottom_img: return None top_img = self.image_processor.resize_and_crop(top_img, (self.width, top_img.height)) bottom_img = self.image_processor.resize_and_crop(bottom_img, (self.width, bottom_img.height)) if color_theme and color_theme in self.color_themes: top_color, bottom_color = self.color_themes[color_theme] else: top_color = self._extract_dominant_color_edge(top_img) bottom_color = self._extract_dominant_color_edge(bottom_img) top_color, bottom_color = self._ensure_colors_harmony(top_color, bottom_color) self._current_background_colors = (top_color, bottom_color) canvas = self.create_gradient_background(top_color, bottom_color) top_img_trans = self._apply_top_transparency(top_img) bottom_img_trans = self._apply_bottom_transparency(bottom_img) section_heights = self._calculate_section_heights() canvas = self._compose_images(canvas, top_img_trans, bottom_img_trans) if small_image_paths: canvas = self._add_small_images(canvas, small_image_paths, section_heights) canvas = self._add_decorative_elements(canvas) canvas = self._render_texts(canvas, content, section_heights) return canvas def _calculate_section_heights(self) -> Dict[str, int]: """计算各区域高度(1:2:1的比例)""" total_height = self.height total_parts = self.config['total_parts'] top_h = int(total_height / total_parts) middle_h = int(total_height * 2 / total_parts) bottom_h = total_height - top_h - middle_h return { 'top_height': top_h, 'middle_height': middle_h, 'bottom_height': bottom_h, 'middle_start': top_h, 'middle_end': top_h + middle_h } def _apply_top_transparency(self, image: Image.Image) -> Image.Image: """对上部图像应用从不透明到透明的渐变""" img_rgba = self.image_processor.ensure_rgba(image) img_array = np.array(img_rgba) h = img_array.shape[0] start_y = int(h * (1 - self.config['transparent_ratio'])) for y in range(start_y, h): ratio = ((y - start_y) / (h - start_y)) ** 2 img_array[y, :, 3] = np.clip(img_array[y, :, 3] * (1 - ratio), 0, 255) return Image.fromarray(img_array) def _apply_bottom_transparency(self, image: Image.Image) -> Image.Image: """对下部图像应用从透明到不透明的渐变""" img_rgba = self.image_processor.ensure_rgba(image) img_array = np.array(img_rgba) h = img_array.shape[0] end_y = int(h * self.config['transparent_ratio']) for y in range(end_y): ratio = (1 - y / end_y) ** 2 img_array[y, :, 3] = np.clip(img_array[y, :, 3] * (1 - ratio), 0, 255) return Image.fromarray(img_array) def _compose_images(self, canvas: Image.Image, top_img: Image.Image, bottom_img: Image.Image) -> Image.Image: """合成背景和上下图像""" canvas.paste(top_img, ((self.width - top_img.width) // 2, 0), top_img) canvas.paste(bottom_img, ((self.width - bottom_img.width) // 2, self.height - bottom_img.height), bottom_img) return canvas def _render_texts(self, canvas: Image.Image, content: Dict[str, Any], heights: Dict[str, int]) -> Image.Image: """渲染所有文本内容""" draw = ImageDraw.Draw(canvas) center_x = self.width // 2 text_start_y, text_end_y = heights['middle_start'], heights['middle_end'] available_height = text_end_y - text_start_y - self.config['dynamic_spacing']['content_margin'] * 2 estimated_height = self._estimate_content_height(content, self.width) layout_params = self._calculate_dynamic_layout_params(estimated_height, available_height) current_y = text_start_y + self.config['dynamic_spacing']['content_margin'] available_width = int(self.width * 0.9) margin_x = (self.width - available_width) // 2 current_y = self._render_title(draw, content.get("name", ""), current_y, center_x, self.width, layout_params) current_y = self._render_feature(draw, content.get("feature", ""), current_y, center_x, available_width, layout_params) self._render_info_section(draw, content, current_y, margin_x, available_width, layout_params, text_end_y) return canvas def _add_small_images(self, canvas: Image.Image, image_paths: List[str], heights: Dict[str, int]) -> Image.Image: """在中间区域添加最多三张小图""" if not image_paths: return canvas num_images = min(len(image_paths), 3) padding = 10 total_img_width = int(self.width * 0.8) img_width = (total_img_width - (num_images - 1) * padding) // num_images img_height = int(img_width * 0.75) start_x = (self.width - total_img_width) // 2 y_pos = heights['middle_start'] + (heights['middle_height'] - img_height) // 2 for i, path in enumerate(image_paths[:num_images]): if img := self.image_processor.load_image(path): img = self.image_processor.resize_and_crop(img, (img_width, img_height)) x_pos = start_x + i * (img_width + padding) canvas.paste(img, (x_pos, y_pos)) return canvas def _add_decorative_elements(self, canvas: Image.Image) -> Image.Image: """添加装饰性元素增强设计感""" overlay = Image.new('RGBA', self.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) # 顶部装饰线条 line_y = self.height // 4 - 20 for i in range(w := self.width // 3): alpha = int(255 * (1 - abs(i - w//2) / (w//2)) * 0.3) draw.line([((self.width-w)//2 + i, line_y), ((self.width-w)//2 + i, line_y + 2)], fill=(255,255,255,alpha), width=1) # 角落装饰 s, a = 80, 40 draw.arc([20, 20, 20+s, 20+s], 180, 270, fill=(255,255,255,a), width=3) draw.arc([self.width-s-20, self.height-s-20, self.width-20, self.height-20], 0, 90, fill=(255,255,255,a), width=3) return Image.alpha_composite(canvas, overlay) def _estimate_content_height(self, content: Dict[str, Any], width: int) -> int: """预估内容总高度""" spacing = self.config['dynamic_spacing']['section_spacing'] title_h = int(self.text_renderer.load_font(60).getbbox("T")[3] * 1.2) feature_h = int(self.text_renderer.load_font(30).getbbox("T")[3] * 1.5) + 50 info_h = len(content.get("info_list", [])) * 35 price_h = 80 return 20 + title_h + spacing + feature_h + spacing + info_h + price_h + self.config['dynamic_spacing']['bottom_reserve'] def _calculate_dynamic_layout_params(self, estimated_height: int, available_height: int) -> Dict[str, Any]: """根据空间利用率计算动态布局参数""" ratio = estimated_height / available_height if available_height > 0 else 1.0 factor = 1.2 if ratio <= 0.8 else (0.8 if ratio > 1.0 else 1.0) spacing = self.config['dynamic_spacing'] return { 'line_spacing': int(spacing['min_line_spacing'] + (spacing['max_line_spacing'] - spacing['min_line_spacing']) * factor), 'section_spacing': int(spacing['section_spacing'] * factor) } def _render_title(self, draw: ImageDraw.Draw, text: str, y: int, center_x: int, width: int, params: Dict[str, Any]) -> int: """渲染主标题""" size, calculated_width = self.text_renderer.calculate_font_size_and_width(text, int(width * 0.85), max_size=80) font = self.text_renderer.load_font(size) text_w, text_h = self.text_renderer.get_text_size(text, font) self.text_renderer.draw_text_with_outline(draw, (center_x - text_w // 2, y), text, font, text_color=(255, 240, 200), outline_width=2) return y + text_h + params['section_spacing'] def _render_feature(self, draw: ImageDraw.Draw, text: str, y: int, center_x: int, width: int, params: Dict[str, Any]) -> int: """渲染特色描述及其背景""" size, calculated_width = self.text_renderer.calculate_font_size_and_width(text, int(width * 0.8), max_size=40) font = self.text_renderer.load_font(size) text_w, text_h = self.text_renderer.get_text_size(text, font) bg_h = 50 bg_w = min(text_w + 30, width) bg_x = center_x - bg_w // 2 self.text_renderer.draw_rounded_rectangle(draw, (bg_x, y-5), (bg_w, bg_h), 20, fill=(50, 50, 50, 180)) color = self._get_smart_feature_color() draw.text((center_x - text_w // 2, y), text, font=font, fill=color) return y + bg_h + params['section_spacing'] def _render_info_section(self, draw, content, y, x, width, params, max_y): """渲染信息列表和价格区域""" info_texts = content.get("info_list", []) if not info_texts: return y # 为信息列表找到合适的字体 longest_info_line = max([f"{item.get('title', '')}: {item.get('desc', '')}" for item in info_texts], key=len) if info_texts else "" info_size, _ = self.text_renderer.calculate_font_size_and_width(longest_info_line, int(width*0.55), max_size=30) info_font = self.text_renderer.load_font(info_size) _, line_h = self.text_renderer.get_text_size("T", info_font) # 为价格找到合适的字体 price_text = f"¥{content.get('price', '')}" price_size, price_w = self.text_renderer.calculate_font_size_and_width(price_text, int(width*0.35), max_size=200, min_size=60) price_font = self.text_renderer.load_font(price_size) # 渲染信息列表 for i, info in enumerate(info_texts): info_str = f"{info.get('title', '')}: {info.get('desc', '')}" if isinstance(info, dict) else str(info) draw.text((x, y + i * (line_h + params['line_spacing'])), info_str, font=info_font, fill=(255, 255, 255)) # 渲染价格 self.text_renderer.draw_text_with_shadow(draw, (x + width - price_w, y), price_text, price_font) # --- Color Extraction Logic (from demo) --- def _extract_dominant_color_edge(self, image: Image.Image) -> Tuple[int, int, int]: """从图像边缘提取主要颜色""" if image.mode != 'RGB': image = image.convert('RGB') w, h = image.size pixels = [] edge_w = min(w, h) // 4 points = [(x,y) for y in range(h) for x in range(w) if xw-edge_w or yh-edge_w] sample_points = random.sample(points, min(len(points), 200)) for p in sample_points: pixel = image.getpixel(p) if sum(pixel) > 50 and sum(pixel) < 700: pixels.append(pixel) if not pixels: return (80, 120, 160) best_color = self._select_best_color(Counter(pixels).most_common(5)) return self._adjust_color_for_background(best_color) def _select_best_color(self, candidates: List[Tuple[Tuple[int, int, int], int]]) -> Tuple[int, int, int]: """从候选颜色中选择最合适的""" best_score, best_color = -1, None for color, count in candidates: r,g,b = color brightness = (r*299 + g*587 + b*114) / 1000 saturation = (max(r,g,b) - min(r,g,b)) / max(r,g,b) if max(r,g,b)>0 else 0 b_score = 1.0 - abs((brightness - 130) / 130) s_score = 1.0 - abs(saturation - 0.5) / 0.5 score = b_score * 0.4 + s_score * 0.6 if score > best_score: best_score, best_color = score, color return best_color or candidates[0][0] def _adjust_color_for_background(self, color: Tuple[int, int, int]) -> Tuple[int, int, int]: """调整颜色使其更适合作为背景""" r, g, b = color h, s, v = colorsys.rgb_to_hsv(r/255.0, g/255.0, b/255.0) v = max(0.3, min(0.7, v)) # 限制亮度 s = max(0.4, min(0.8, s)) # 限制饱和度 r,g,b = [int(c*255) for c in colorsys.hsv_to_rgb(h,s,v)] return (r, g, b) def _ensure_colors_harmony(self, top: Tuple[int, int, int], bottom: Tuple[int, int, int]) -> Tuple[Tuple[int, int, int], Tuple[int, int, int]]: """确保颜色和谐""" dist = sum(abs(a-b) for a,b in zip(top,bottom)) if dist < 30: factor = 1.1 if sum(top)/3 > 128 else 0.9 top = tuple(max(0,min(255,int(c*factor))) for c in top) elif dist > 150: mid = tuple((a+b)//2 for a,b in zip(top,bottom)) top = tuple(int(c*0.8+m*0.2) for c,m in zip(top,mid)) bottom = tuple(int(c*0.8+m*0.2) for c,m in zip(bottom,mid)) return top, bottom def _get_smart_feature_color(self) -> Tuple[int, int, int]: """为feature文本智能选择颜色""" if not self._current_background_colors: return (255, 255, 255) top, bottom = self._current_background_colors avg_bright = (sum(top)+sum(bottom))/6 def get_tone(c): r,g,b=c return 'w' if r > g and r > b else ('f' if g > r and g > b else 'c') tone = get_tone(tuple((a+b)//2 for a,b in zip(top,bottom))) if avg_bright > 120: return {'c':(65,105,155), 'w':(155,85,65)}.get(tone, (85,125,85)) else: return {'c':(135,185,235), 'w':(255,195,135)}.get(tone, (155,215,155))