- 支持四种图片适配模式:stretch、contain、cover、center - 支持背景色填充功能(contain 和 center 模式) - 支持文档级 DPI 配置(metadata.dpi) - PPTX 渲染器集成 Pillow 实现高质量图片处理 - HTML 渲染器使用 CSS object-fit 实现相同效果 - 添加完整的单元测试、集成测试和端到端测试 - 更新 README 文档和架构文档 - 模块化设计:utils/image_utils.py 图片处理工具模块 - 添加图片配置验证器:validators/image_config.py - 向后兼容:未指定 fit 时默认使用 stretch 模式
150 lines
4.7 KiB
Python
150 lines
4.7 KiB
Python
"""
|
||
图片处理工具模块
|
||
|
||
提供图片适配模式处理、像素与英寸转换等功能。
|
||
"""
|
||
|
||
from PIL import Image, ImageOps
|
||
from typing import Tuple, Optional
|
||
|
||
|
||
def inches_to_pixels(inches: float, dpi: int = 96) -> float:
|
||
"""
|
||
将英寸转换为像素
|
||
|
||
Args:
|
||
inches: 英寸值
|
||
dpi: DPI(每英寸像素数),默认 96
|
||
|
||
Returns:
|
||
像素值
|
||
"""
|
||
return inches * dpi
|
||
|
||
|
||
def pixels_to_inches(pixels: float, dpi: int = 96) -> float:
|
||
"""
|
||
将像素转换为英寸
|
||
|
||
Args:
|
||
pixels: 像素值
|
||
dpi: DPI(每英寸像素数),默认 96
|
||
|
||
Returns:
|
||
英寸值
|
||
"""
|
||
return pixels / dpi
|
||
|
||
|
||
def create_canvas_with_background(size: Tuple[int, int], background_color: str) -> Image.Image:
|
||
"""
|
||
创建带背景色的画布
|
||
|
||
Args:
|
||
size: 画布尺寸 (width, height)
|
||
background_color: 背景颜色(#RRGGBB 或 #RGB 格式)
|
||
|
||
Returns:
|
||
PIL Image 对象
|
||
"""
|
||
# 验证颜色格式
|
||
import re
|
||
if not isinstance(background_color, str):
|
||
raise ValueError(f"无效的颜色格式: {background_color}")
|
||
pattern = r'^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$'
|
||
if not re.match(pattern, background_color):
|
||
raise ValueError(f"无效的颜色格式: {background_color}")
|
||
|
||
# 创建 RGB 模式的画布
|
||
canvas = Image.new('RGB', size, background_color)
|
||
return canvas
|
||
|
||
|
||
def apply_fit_mode(
|
||
img: Image.Image,
|
||
box_size: Tuple[int, int],
|
||
fit: Optional[str] = None,
|
||
background: Optional[str] = None
|
||
) -> Image.Image:
|
||
"""
|
||
应用图片适配模式
|
||
|
||
Args:
|
||
img: PIL Image 对象
|
||
box_size: 目标尺寸 (width, height) 像素
|
||
fit: 适配模式(stretch, contain, cover, center),默认 stretch
|
||
background: 背景颜色(#RRGGBB 或 #RGB 格式),仅对 contain 和 center 模式有效
|
||
|
||
Returns:
|
||
处理后的 PIL Image 对象
|
||
"""
|
||
if fit is None or fit == 'stretch':
|
||
# stretch 模式:直接拉伸到目标尺寸
|
||
return img.resize(box_size, Image.Resampling.LANCZOS)
|
||
|
||
elif fit == 'contain':
|
||
# contain 模式:保持宽高比,完整显示在 box 内
|
||
# 如果图片比 box 小,保持原始尺寸;如果比 box 大,等比缩小
|
||
img_width, img_height = img.size
|
||
box_width, box_height = box_size
|
||
|
||
# 检查图片是否需要缩小
|
||
if img_width <= box_width and img_height <= box_height:
|
||
# 图片比 box 小,保持原始尺寸
|
||
result = img
|
||
else:
|
||
# 图片比 box 大,使用 contain 缩小
|
||
result = ImageOps.contain(img, box_size, Image.Resampling.LANCZOS)
|
||
|
||
# 如果指定了背景色,创建画布并居中粘贴
|
||
if background:
|
||
canvas = create_canvas_with_background(box_size, background)
|
||
# 计算居中位置
|
||
offset_x = (box_size[0] - result.width) // 2
|
||
offset_y = (box_size[1] - result.height) // 2
|
||
canvas.paste(result, (offset_x, offset_y))
|
||
return canvas
|
||
|
||
return result
|
||
|
||
elif fit == 'cover':
|
||
# cover 模式:保持宽高比,填满 box,裁剪超出部分
|
||
result = ImageOps.cover(img, box_size, Image.Resampling.LANCZOS)
|
||
# ImageOps.cover 可能返回比 box_size 大的图片,需要裁剪到精确尺寸
|
||
if result.size != box_size:
|
||
# 从中心裁剪到目标尺寸
|
||
left = (result.width - box_size[0]) // 2
|
||
top = (result.height - box_size[1]) // 2
|
||
result = result.crop((left, top, left + box_size[0], top + box_size[1]))
|
||
return result
|
||
|
||
elif fit == 'center':
|
||
# center 模式:不缩放,居中显示,超出部分裁剪
|
||
img_width, img_height = img.size
|
||
box_width, box_height = box_size
|
||
|
||
# 如果图片比 box 大,需要裁剪
|
||
if img_width > box_width or img_height > box_height:
|
||
# 计算裁剪区域(从中心裁剪)
|
||
left = max(0, (img_width - box_width) // 2)
|
||
top = max(0, (img_height - box_height) // 2)
|
||
right = min(img_width, left + box_width)
|
||
bottom = min(img_height, top + box_height)
|
||
result = img.crop((left, top, right, bottom))
|
||
else:
|
||
result = img
|
||
|
||
# 如果指定了背景色,创建画布并居中粘贴
|
||
if background:
|
||
canvas = create_canvas_with_background(box_size, background)
|
||
# 计算居中位置
|
||
offset_x = (box_size[0] - result.width) // 2
|
||
offset_y = (box_size[1] - result.height) // 2
|
||
canvas.paste(result, (offset_x, offset_y))
|
||
return canvas
|
||
|
||
return result
|
||
|
||
else:
|
||
raise ValueError(f"不支持的 fit 模式: {fit}")
|