TravelContentCreator/poster/templates/business_template.py

350 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 x<edge_w or x>w-edge_w or y<edge_w or y>h-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))