594 lines
24 KiB
Python
594 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
海报生成相关的工具类模块
|
||
"""
|
||
|
||
import os
|
||
import random
|
||
import logging
|
||
import colorsys
|
||
from abc import ABC, abstractmethod
|
||
from typing import List, Tuple, Optional, Dict, Any, Union
|
||
|
||
import numpy as np
|
||
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance, ExifTags
|
||
from sklearn.cluster import KMeans
|
||
from collections import Counter
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# --- 从 demo_refactored_templates.py 提取 ---
|
||
|
||
class TextRenderer:
|
||
"""统一的文本渲染类"""
|
||
|
||
def __init__(self, font_dir: str = "/root/autodl-tmp/TCC_RESTRUCT/assets"):
|
||
self.font_dir = font_dir
|
||
self.fonts = {}
|
||
self.available_fonts = self._get_available_fonts()
|
||
if not self.available_fonts:
|
||
logger.warning(f"在目录 {self.font_dir} 中没有找到字体文件。")
|
||
self.default_font_name = None
|
||
else:
|
||
self.default_font_name = list(self.available_fonts.keys())[0]
|
||
logger.info(f"可用字体: {list(self.available_fonts.keys())}")
|
||
|
||
def _get_available_fonts(self) -> Dict[str, str]:
|
||
"""获取所有可用字体"""
|
||
fonts = {}
|
||
if not os.path.isdir(self.font_dir):
|
||
return fonts
|
||
for f in os.listdir(self.font_dir):
|
||
if f.lower().endswith(('.ttf', '.otf')):
|
||
font_name = os.path.splitext(f)[0]
|
||
fonts[font_name] = os.path.join(self.font_dir, f)
|
||
return fonts
|
||
|
||
def get_font_path(self, font_name: Optional[str] = None) -> str:
|
||
"""获取字体路径,如果未指定则返回默认字体"""
|
||
if font_name and font_name in self.available_fonts:
|
||
return self.available_fonts[font_name]
|
||
if self.default_font_name:
|
||
return self.available_fonts[self.default_font_name]
|
||
# 如果没有任何字体,尝试一个通用系统路径作为后备
|
||
logger.warning(f"字体 '{font_name}' 不可用, 将尝试系统后备字体。")
|
||
# 这是一个常见的 linux 系统路径
|
||
return "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||
|
||
def load_font(self, size: int, font_name: Optional[str] = None) -> ImageFont.FreeTypeFont:
|
||
"""加载指定大小和名称的字体"""
|
||
font_path = self.get_font_path(font_name)
|
||
try:
|
||
return ImageFont.truetype(font_path, size)
|
||
except IOError:
|
||
logger.error(f"无法加载字体: {font_path}。将使用Pillow默认字体。")
|
||
return ImageFont.load_default()
|
||
|
||
def calculate_optimal_font_size(self, text: str,
|
||
target_width: int,
|
||
font_name: Optional[str] = None,
|
||
max_size: int = 120,
|
||
min_size: int = 10) -> int:
|
||
"""二分法计算最佳字体大小以适应目标宽度"""
|
||
low, high = min_size, max_size
|
||
optimal_size = min_size
|
||
|
||
font_path = self.get_font_path(font_name)
|
||
|
||
while low <= high:
|
||
mid = (low + high) // 2
|
||
font = self.load_font(mid, font_name)
|
||
|
||
# 使用 getbbox 来获取更精确的宽度
|
||
try:
|
||
text_bbox = font.getbbox(text)
|
||
text_width = text_bbox[2] - text_bbox[0]
|
||
except AttributeError: # 兼容旧版 Pillow
|
||
text_width, _ = font.getsize(text)
|
||
|
||
if text_width <= target_width:
|
||
optimal_size = mid
|
||
low = mid + 1
|
||
else:
|
||
high = mid - 1
|
||
return optimal_size
|
||
|
||
def get_text_size(self, text: str, font: ImageFont.FreeTypeFont) -> Tuple[int, int]:
|
||
"""
|
||
获取文字的尺寸
|
||
|
||
Args:
|
||
text: 文字内容
|
||
font: 字体对象
|
||
|
||
Returns:
|
||
文字尺寸 (width, height)
|
||
"""
|
||
if not text:
|
||
return 0, 0
|
||
bbox = font.getbbox(text)
|
||
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||
|
||
def calculate_font_size_and_width(self, text: str,
|
||
target_width: int,
|
||
font_name: Optional[str] = None,
|
||
max_size: int = 120,
|
||
min_size: int = 10) -> Tuple[int, int]:
|
||
"""
|
||
计算文本的最佳字体大小,使其宽度接近目标宽度
|
||
|
||
Args:
|
||
text: 文字内容
|
||
target_width: 目标宽度
|
||
font_name: 字体文件名
|
||
max_size: 最大字体大小
|
||
min_size: 最小字体大小
|
||
|
||
Returns:
|
||
元组 (最佳字体大小, 实际文本宽度)
|
||
"""
|
||
if not text.strip():
|
||
return min_size, 0
|
||
|
||
font_path = self.get_font_path(font_name)
|
||
|
||
low = min_size
|
||
high = max_size
|
||
best_size = min_size
|
||
best_width = 0
|
||
tolerance = 0.08
|
||
|
||
try:
|
||
font = self.load_font(max_size, font_name)
|
||
max_width = self.get_text_size(text, font)[0]
|
||
except Exception:
|
||
max_width = target_width * 2
|
||
|
||
if max_width < target_width * (1 + tolerance):
|
||
best_size = max_size
|
||
best_width = max_width
|
||
else:
|
||
closest_size = min_size
|
||
closest_diff = target_width
|
||
|
||
while low <= high:
|
||
mid = (low + high) // 2
|
||
try:
|
||
font = self.load_font(mid, font_name)
|
||
width = self.get_text_size(text, font)[0]
|
||
except Exception:
|
||
width = target_width * 2
|
||
|
||
diff = abs(width - target_width)
|
||
|
||
if diff < closest_diff:
|
||
closest_diff = diff
|
||
closest_size = mid
|
||
|
||
if target_width * (1 - tolerance) <= width <= target_width * (1 + tolerance):
|
||
best_size = mid
|
||
best_width = width
|
||
break
|
||
|
||
if width < target_width:
|
||
if width > best_width:
|
||
best_width = width
|
||
best_size = mid
|
||
low = mid + 1
|
||
else:
|
||
high = mid - 1
|
||
|
||
if best_width == 0:
|
||
best_size = closest_size
|
||
|
||
final_font = self.load_font(best_size, font_name)
|
||
final_width = self.get_text_size(text, final_font)[0]
|
||
|
||
return best_size, final_width
|
||
|
||
def draw_multiline_text(self, draw: ImageDraw.Draw,
|
||
position: Tuple[int, int],
|
||
text: str,
|
||
font: ImageFont.FreeTypeFont,
|
||
max_width: int,
|
||
line_spacing: int = 5,
|
||
align: str = "left",
|
||
text_color: Tuple[int, int, int, int] = (255, 255, 255, 255),
|
||
outline_color: Optional[Tuple[int, int, int, int]] = None,
|
||
outline_width: int = 1):
|
||
"""绘制多行文本,支持自动换行和对齐"""
|
||
lines = self.wrap_text(text, font, max_width)
|
||
x, y = position
|
||
|
||
for line in lines:
|
||
try:
|
||
line_bbox = font.getbbox(line)
|
||
line_width = line_bbox[2] - line_bbox[0]
|
||
line_height = line_bbox[3] - line_bbox[1]
|
||
except AttributeError:
|
||
line_width, line_height = font.getsize(line)
|
||
|
||
if align == "center":
|
||
draw_x = x - line_width / 2
|
||
elif align == "right":
|
||
draw_x = x - line_width
|
||
else: # left
|
||
draw_x = x
|
||
|
||
if outline_color:
|
||
self.draw_text_with_outline(draw, (draw_x, y), line, font, text_color, outline_color, outline_width)
|
||
else:
|
||
draw.text((draw_x, y), line, font=font, fill=text_color)
|
||
|
||
y += line_height + line_spacing
|
||
|
||
def wrap_text(self, text: str, font: ImageFont.FreeTypeFont, max_width: int) -> List[str]:
|
||
"""将文本按最大宽度换行"""
|
||
lines = []
|
||
words = text.split(' ')
|
||
|
||
current_line = ""
|
||
for word in words:
|
||
try:
|
||
test_line_bbox = font.getbbox(current_line + " " + word)
|
||
test_line_width = test_line_bbox[2] - test_line_bbox[0]
|
||
except AttributeError:
|
||
test_line_width, _ = font.getsize(current_line + " " + word)
|
||
|
||
if test_line_width <= max_width:
|
||
current_line += " " + word
|
||
else:
|
||
lines.append(current_line.strip())
|
||
current_line = word
|
||
|
||
lines.append(current_line.strip())
|
||
return lines
|
||
|
||
def draw_text_with_outline(self, draw: ImageDraw.Draw,
|
||
position: Tuple[int, int],
|
||
text: str,
|
||
font: ImageFont.FreeTypeFont,
|
||
text_color: Tuple[int, int, int, int] = (255, 255, 255, 255),
|
||
outline_color: Tuple[int, int, int, int] = (0, 0, 0, 255),
|
||
outline_width: int = 2):
|
||
"""绘制带描边的文本"""
|
||
x, y = position
|
||
# 绘制描边
|
||
for i in range(-outline_width, outline_width + 1):
|
||
for j in range(-outline_width, outline_width + 1):
|
||
if i != 0 or j != 0:
|
||
draw.text((x + i, y + j), text, font=font, fill=outline_color)
|
||
# 绘制文本本身
|
||
draw.text((x, y), text, font=font, fill=text_color)
|
||
|
||
def draw_text_with_shadow(self, draw: ImageDraw.Draw,
|
||
position: Tuple[int, int],
|
||
text: str,
|
||
font: ImageFont.FreeTypeFont,
|
||
text_color: Tuple[int, int, int, int] = (255, 255, 255, 255),
|
||
shadow_color: Tuple[int, int, int, int] = (0, 0, 0, 128),
|
||
shadow_offset: Tuple[int, int] = (2, 2)):
|
||
"""
|
||
绘制带阴影的文字
|
||
|
||
Args:
|
||
draw: PIL绘图对象
|
||
position: 文字位置
|
||
text: 文字内容
|
||
font: 字体对象
|
||
text_color: 文字颜色
|
||
shadow_color: 阴影颜色
|
||
shadow_offset: 阴影偏移
|
||
"""
|
||
x, y = position
|
||
shadow_x, shadow_y = shadow_offset
|
||
|
||
# 绘制阴影
|
||
draw.text((x + shadow_x, y + shadow_y), text, font=font, fill=shadow_color)
|
||
|
||
# 绘制文字
|
||
draw.text(position, text, font=font, fill=text_color)
|
||
|
||
def draw_rounded_rectangle(self, draw: ImageDraw.Draw,
|
||
position: Tuple[int, int],
|
||
size: Tuple[int, int],
|
||
radius: int,
|
||
fill: Optional[Tuple[int, int, int, int]] = None,
|
||
outline: Optional[Tuple[int, int, int, int]] = None,
|
||
width: int = 1):
|
||
"""
|
||
绘制圆角矩形。Pillow的内置方法在某些版本有bug,这里是兼容实现。
|
||
"""
|
||
x1, y1 = position
|
||
x2, y2 = x1 + size[0], y1 + size[1]
|
||
|
||
# 确保Pillow版本支持rounded_rectangle
|
||
if hasattr(draw, 'rounded_rectangle'):
|
||
try:
|
||
draw.rounded_rectangle([x1, y1, x2, y2], radius=radius, fill=fill, outline=outline, width=width)
|
||
return
|
||
except Exception:
|
||
# 如果新版方法失败,则回退到旧版手动绘制
|
||
pass
|
||
|
||
# 手动绘制,作为旧版Pillow的回退方案
|
||
if fill:
|
||
# 绘制中心矩形
|
||
draw.rectangle([x1 + radius, y1, x2 - radius, y2], fill=fill)
|
||
draw.rectangle([x1, y1 + radius, x2, y2 - radius], fill=fill)
|
||
# 绘制四个角
|
||
draw.pieslice([x1, y1, x1 + 2*radius, y1 + 2*radius], 180, 270, fill=fill)
|
||
draw.pieslice([x2 - 2*radius, y1, x2, y1 + 2*radius], 270, 360, fill=fill)
|
||
draw.pieslice([x1, y2 - 2*radius, x1 + 2*radius, y2], 90, 180, fill=fill)
|
||
draw.pieslice([x2 - 2*radius, y2 - 2*radius, x2, y2], 0, 90, fill=fill)
|
||
|
||
if outline and width > 0:
|
||
# 简化的边框绘制
|
||
for i in range(width):
|
||
draw.arc([x1 + i, y1 + i, x1 + 2*radius - i, y1 + 2*radius - i], 180, 270, fill=outline)
|
||
draw.arc([x2 - 2*radius + i, y1 + i, x2 - i, y1 + 2*radius - i], 270, 360, fill=outline)
|
||
draw.arc([x1 + i, y2 - 2*radius + i, x1 + 2*radius - i, y2 - i], 90, 180, fill=outline)
|
||
draw.arc([x2 - 2*radius + i, y2 - 2*radius + i, x2 - i, y2 - i], 0, 90, fill=outline)
|
||
draw.line([x1 + radius, y1 + i, x2 - radius, y1 + i], fill=outline)
|
||
draw.line([x1 + radius, y2 - i, x2 - radius, y2 - i], fill=outline)
|
||
draw.line([x1 + i, y1 + radius, x1 + i, y2 - radius], fill=outline)
|
||
draw.line([x2 - i, y1 + radius, x2 - i, y2 - radius], fill=outline)
|
||
|
||
|
||
class ColorExtractor:
|
||
"""统一的颜色处理类"""
|
||
|
||
@staticmethod
|
||
def extract_dominant_colors(image: Image.Image, k: int = 5, sample_size: int = 200) -> List[Tuple[int, int, int]]:
|
||
"""使用K-Means提取主色调"""
|
||
image_rgb = image.convert("RGB")
|
||
# 调整采样大小以提高性能
|
||
if sample_size:
|
||
thumb = image_rgb.copy()
|
||
thumb.thumbnail((sample_size, sample_size))
|
||
pixels = list(thumb.getdata())
|
||
else:
|
||
pixels = list(image_rgb.getdata())
|
||
|
||
# 过滤掉近似白色和黑色的像素
|
||
pixels = [p for p in pixels if (sum(p) > 30 and sum(p) < 700)]
|
||
if not pixels:
|
||
return [(128, 128, 128)] # 返回默认灰色
|
||
|
||
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10).fit(pixels)
|
||
dominant_colors = kmeans.cluster_centers_.astype(int).tolist()
|
||
|
||
# 按颜色在图像中的流行度排序
|
||
labels = kmeans.labels_
|
||
counts = Counter(labels)
|
||
sorted_colors = sorted(dominant_colors, key=lambda color: counts[dominant_colors.index(color)], reverse=True)
|
||
|
||
return [tuple(c) for c in sorted_colors]
|
||
|
||
@staticmethod
|
||
def get_complementary_color(color: Tuple[int, int, int]) -> Tuple[int, int, int]:
|
||
"""获取互补色"""
|
||
h, l, s = colorsys.rgb_to_hls(color[0]/255.0, color[1]/255.0, color[2]/255.0)
|
||
h = (h + 0.5) % 1.0
|
||
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
||
return (int(r*255), int(g*255), int(b*255))
|
||
|
||
@staticmethod
|
||
def create_gradient_colors(base_color: Tuple[int, int, int],
|
||
variation: float = 0.3) -> Tuple[Tuple[int, int, int], Tuple[int, int, int]]:
|
||
"""基于基色创建和谐的渐变色对"""
|
||
h, l, s = colorsys.rgb_to_hls(base_color[0]/255.0, base_color[1]/255.0, base_color[2]/255.0)
|
||
|
||
# 创建更亮和更暗的变体
|
||
light_l = min(1.0, l + variation / 2)
|
||
dark_l = max(0.0, l - variation / 2)
|
||
|
||
top_r, top_g, top_b = colorsys.hls_to_rgb(h, light_l, s)
|
||
bottom_r, bottom_g, bottom_b = colorsys.hls_to_rgb(h, dark_l, s)
|
||
|
||
return ((int(top_r*255), int(top_g*255), int(top_b*255)),
|
||
(int(bottom_r*255), int(bottom_g*255), int(bottom_b*255)))
|
||
|
||
|
||
class ImageProcessor:
|
||
"""统一的图像处理类"""
|
||
|
||
@staticmethod
|
||
def ensure_rgba(image: Image.Image) -> Image.Image:
|
||
"""
|
||
确保图像是RGBA模式
|
||
|
||
Args:
|
||
image: PIL Image对象
|
||
|
||
Returns:
|
||
RGBA模式的PIL Image对象
|
||
"""
|
||
if image.mode == 'RGBA':
|
||
return image
|
||
return image.convert('RGBA')
|
||
|
||
@staticmethod
|
||
def resize_image(image: Image.Image, target_width: int) -> Image.Image:
|
||
"""调整图像大小,保持原始高宽比"""
|
||
if image.width == target_width:
|
||
return image
|
||
orig_aspect = image.height / image.width
|
||
target_height = int(target_width * orig_aspect)
|
||
return image.resize((target_width, target_height), Image.LANCZOS)
|
||
|
||
@staticmethod
|
||
def load_image(image_path: str) -> Optional[Image.Image]:
|
||
"""
|
||
安全地加载图像文件
|
||
"""
|
||
if not os.path.exists(image_path):
|
||
logger.error(f"图像文件不存在: {image_path}")
|
||
return None
|
||
try:
|
||
image = Image.open(image_path)
|
||
image.load() # 强制加载图像数据,验证文件完整性
|
||
logger.info(f"成功加载图像: {os.path.basename(image_path)}, 尺寸: {image.size}")
|
||
return image
|
||
except (IOError, OSError) as e:
|
||
logger.error(f"加载或验证图像失败: {image_path}, 错误: {e}")
|
||
return None
|
||
|
||
@staticmethod
|
||
def resize_and_crop(image: Image.Image, target_size: Tuple[int, int]) -> Image.Image:
|
||
"""
|
||
调整图像大小并居中裁剪到目标尺寸
|
||
"""
|
||
target_width, target_height = target_size
|
||
img_width, img_height = image.size
|
||
|
||
if img_width == target_width and img_height == target_height:
|
||
return image
|
||
|
||
scale = max(target_width / image.width, target_height / image.height)
|
||
new_width = int(image.width * scale)
|
||
new_height = int(image.height * scale)
|
||
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
|
||
|
||
left = (new_width - target_width) // 2
|
||
top = (new_height - target_height) // 2
|
||
right = left + target_width
|
||
bottom = top + target_height
|
||
|
||
return resized_image.crop((left, top, right, bottom))
|
||
|
||
@staticmethod
|
||
def save_image(image: Image.Image, output_path: str, quality: int = 95):
|
||
"""保存图片, 如果是JPG格式且有Alpha通道,则自动处理"""
|
||
try:
|
||
# 确保输出目录存在
|
||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||
|
||
# 如果是RGBA模式且要保存为JPEG,需要转换
|
||
if image.mode == 'RGBA' and output_path.lower().endswith(('.jpg', '.jpeg')):
|
||
logger.info(f"图像为RGBA模式,将转换为RGB以保存为JPEG: {output_path}")
|
||
# 创建一个白色背景
|
||
background = Image.new('RGB', image.size, (255, 255, 255))
|
||
# 将原图粘贴到背景上,使用alpha通道作为蒙版
|
||
background.paste(image, mask=image.split()[-1])
|
||
background.save(output_path, 'JPEG', quality=quality)
|
||
else:
|
||
image.save(output_path, quality=quality)
|
||
|
||
logger.info(f"图片成功保存至: {output_path}")
|
||
except Exception as e:
|
||
logger.error(f"保存图片失败: {output_path}, 错误: {e}", exc_info=True)
|
||
|
||
# --- 图像效果与增强 ---
|
||
|
||
@staticmethod
|
||
def enhance_image(image: Image.Image,
|
||
contrast: float = 1.0,
|
||
brightness: float = 1.0,
|
||
saturation: float = 1.0) -> Image.Image:
|
||
"""增强图片效果"""
|
||
enhanced_image = image
|
||
if brightness != 1.0:
|
||
enhancer = ImageEnhance.Brightness(enhanced_image)
|
||
enhanced_image = enhancer.enhance(brightness)
|
||
if contrast != 1.0:
|
||
enhancer = ImageEnhance.Contrast(enhanced_image)
|
||
enhanced_image = enhancer.enhance(contrast)
|
||
if saturation != 1.0:
|
||
enhancer = ImageEnhance.Color(enhanced_image)
|
||
enhanced_image = enhancer.enhance(saturation)
|
||
return enhanced_image
|
||
|
||
@staticmethod
|
||
def apply_blur(image: Image.Image, radius: float = 2.0) -> Image.Image:
|
||
"""应用高斯模糊效果"""
|
||
return image.filter(ImageFilter.GaussianBlur(radius=radius))
|
||
|
||
# --- 哈希干扰方法 (来自 PosterNotesCreator) ---
|
||
|
||
@staticmethod
|
||
def strip_metadata(image: Image.Image) -> Image.Image:
|
||
"""移除图像的EXIF等元数据"""
|
||
if not image.info:
|
||
return image
|
||
|
||
# 创建一个没有元数据的新图像副本
|
||
image_without_exif = Image.new(image.mode, image.size)
|
||
image_without_exif.putdata(list(image.getdata()))
|
||
|
||
# 保留 ICC profile, 因为它影响颜色显示
|
||
if 'icc_profile' in image.info:
|
||
image_without_exif.info['icc_profile'] = image.info['icc_profile']
|
||
|
||
return image_without_exif
|
||
|
||
@staticmethod
|
||
def apply_strategic_hash_disruption(image: Image.Image, strength: str = "medium") -> Image.Image:
|
||
"""
|
||
智能地应用一系列哈希干扰方法,以在保持视觉质量的同时最大化改变图像哈希值。
|
||
"""
|
||
logger.debug(f"开始应用策略性哈希干扰,强度: {strength}")
|
||
|
||
# 1. 预处理:移除元数据
|
||
disrupted_image = ImageProcessor.strip_metadata(image)
|
||
|
||
# 2. 颜色空间微调
|
||
if strength == "low":
|
||
disrupted_image = ImageProcessor.enhance_image(disrupted_image, saturation=1.01)
|
||
elif strength == "medium":
|
||
disrupted_image = ImageProcessor.enhance_image(disrupted_image, brightness=1.01, saturation=1.02)
|
||
else: # high
|
||
disrupted_image = ImageProcessor.enhance_image(disrupted_image, brightness=0.99, contrast=1.01, saturation=1.03)
|
||
|
||
# 3. 添加难以察觉的噪声
|
||
if strength == "low":
|
||
noise_intensity = 5
|
||
elif strength == "medium":
|
||
noise_intensity = 10
|
||
else: # high
|
||
noise_intensity = 15
|
||
|
||
np_image = np.array(disrupted_image.convert("RGB")).astype(np.int16)
|
||
noise = np.random.randint(-noise_intensity, noise_intensity, np_image.shape, dtype=np.int16)
|
||
np_image += noise
|
||
np_image = np.clip(np_image, 0, 255).astype(np.uint8)
|
||
disrupted_image = Image.fromarray(np_image, "RGB")
|
||
|
||
# 4. 轻微的几何变换
|
||
if strength != "low":
|
||
w, h = disrupted_image.size
|
||
if strength == "medium":
|
||
# 裁剪掉1个像素的边框
|
||
disrupted_image = disrupted_image.crop((1, 1, w - 1, h - 1))
|
||
# 再缩放回去
|
||
disrupted_image = disrupted_image.resize((w, h), Image.LANCZOS)
|
||
else: # high
|
||
# 细微的透视变换
|
||
coeffs = [1, 0.001, 0, 0.001, 1, 0, 0, 0]
|
||
disrupted_image = disrupted_image.transform((w, h), Image.PERSPECTIVE, coeffs, Image.BICUBIC)
|
||
|
||
logger.debug("策略性哈希干扰应用完成。")
|
||
return disrupted_image.convert("RGBA")
|
||
|
||
@staticmethod
|
||
def create_canvas(size: Tuple[int, int], color: Tuple[int, int, int, int] = (255, 255, 255, 255)) -> Image.Image:
|
||
"""创建指定尺寸和颜色的画布"""
|
||
return Image.new('RGBA', size, color)
|
||
|
||
@staticmethod
|
||
def paste_image(canvas: Image.Image, image: Image.Image, position: Tuple[int, int], mask: Optional[Image.Image] = None) -> Image.Image:
|
||
"""将图像粘贴到画布上"""
|
||
canvas.paste(image, position, mask)
|
||
return canvas
|
||
|
||
@staticmethod
|
||
def alpha_composite(base: Image.Image, overlay: Image.Image) -> Image.Image:
|
||
"""Alpha合成两个图像"""
|
||
base = ImageProcessor.ensure_rgba(base)
|
||
overlay = ImageProcessor.ensure_rgba(overlay)
|
||
if base.size != overlay.size:
|
||
overlay = overlay.resize(base.size, Image.LANCZOS)
|
||
return Image.alpha_composite(base, overlay) |