如何使用Python Pillow库为文本添加可自定义的投影效果?
如何使用Python Pillow库为文本添加可自定义的投影效果?
我来帮你一步步搞定这个问题!先澄清你对Photoshop投影参数的假设,再把Pillow里的实现逻辑理清楚,最后优化你的代码和类型提示~
一、Photoshop投影参数与Pillow的对应关系
先确认你的几个假设,再补充关键细节:
- Opacity:没错!它对应投影颜色的Alpha通道值。注意Photoshop里是0-100的百分比,要转成Pillow用的0-255范围,比如65%就是
int(65 * 255 / 100)≈ 166。 - Angle:完全正确,这个角度用来计算投影相对于原文本的偏移方向。需要注意Photoshop的角度是从右上0°顺时针增加,而Pillow的y轴是向下的,计算偏移量时要对应调整坐标系。
- Distance:就是投影和原文本的像素偏移距离,结合Angle就能算出x、y方向的具体偏移量。
- Spread:这个不是高斯模糊半径哦!Photoshop里的Spread是在模糊之前扩展投影的边缘(类似“膨胀”文本形状),百分比代表边缘扩展的比例。在Pillow里我们可以用膨胀滤镜模拟这个效果。
- Size:这个才是高斯模糊的半径!Photoshop里的Size数值直接对应Pillow的
GaussianBlur滤镜的radius参数。
二、Pillow实现投影的核心思路
因为你要在同一张图上放置多个带投影的文本,不能直接在原图上画投影再模糊(会污染其他内容),正确的步骤是:
- 创建一个和原图尺寸一致的临时透明图像,专门用来绘制投影文本;
- 在临时图像上绘制投影文本(先处理Spread的膨胀效果);
- 对临时图像应用高斯模糊(对应Size参数);
- 把模糊后的临时图像(即投影)粘贴到原图上,最后在原位置绘制正常文本。
三、完整优化后的代码
我用TypedDict重构了投影参数的类型定义(比你之前的Dict更清晰,编辑器还能自动补全),同时实现了所有Photoshop投影参数的对应逻辑:
from PIL import Image, ImageDraw, ImageFont, ImageFilter from typing import Optional, Tuple, Union, TypedDict import math # 用TypedDict明确投影参数的类型,比模糊的Dict更易读且类型提示准确 class DropShadowParams(TypedDict, total=False): opacity: int # 0-100的百分比,默认65 angle: int # 0-360的角度,默认30 distance: int # 投影偏移的像素距离,默认15 spread: int # 0-100的百分比,默认0 size: int # 模糊半径(像素),默认15 color: Tuple[int, int, int] # 投影RGB颜色,默认黑色 class Text: def __init__(self, image: Image.Image): self._draw = ImageDraw.Draw(image) self._image = image def text( self, position: Tuple[int, int], text: str, font: ImageFont.FreeTypeFont, color: Union[Tuple[int, int, int], Tuple[int, int, int, int]], drop_shadow: Optional[DropShadowParams] = None ): if drop_shadow is not None: self._add_drop_shadow(position, text, font, drop_shadow) self._draw.text(position, text, font=font, fill=color) def _add_drop_shadow( self, original_position: Tuple[int, int], text: str, font: ImageFont.FreeTypeFont, drop_shadow: DropShadowParams ): # 提取投影参数,同时设置合理默认值 opacity = drop_shadow.get("opacity", 65) angle = drop_shadow.get("angle", 30) distance = drop_shadow.get("distance", 15) spread = drop_shadow.get("spread", 0) blur_size = drop_shadow.get("size", 15) shadow_color = drop_shadow.get("color", (0, 0, 0)) # 1. 将百分比透明度转换为Pillow用的0-255 Alpha值 alpha = int(opacity * 255 / 100) full_shadow_color = (*shadow_color, alpha) # 2. 根据角度和距离计算投影的偏移坐标 angle_rad = math.radians(angle) # 适配Pillow的y轴向下的坐标系,对应Photoshop顺时针角度 dx = round(distance * math.cos(angle_rad)) dy = round(distance * math.sin(angle_rad)) shadow_position = (original_position[0] + dx, original_position[1] + dy) # 3. 创建临时透明图像绘制投影,避免污染原图其他内容 temp_image = Image.new("RGBA", self._image.size, (0, 0, 0, 0)) temp_draw = ImageDraw.Draw(temp_image) # 4. 模拟Spread扩展效果:先绘制文本再用膨胀滤镜处理 spread_pixels = int(spread * blur_size / 100) if blur_size > 0 else 0 temp_draw.text(shadow_position, text, font=font, fill=full_shadow_color) if spread_pixels > 0: # 用MaxFilter实现膨胀,滤镜尺寸需为奇数 temp_image = temp_image.filter(ImageFilter.MaxFilter(size=spread_pixels * 2 + 1)) # 5. 应用高斯模糊(对应Photoshop的Size参数) if blur_size > 0: temp_image = temp_image.filter(ImageFilter.GaussianBlur(radius=blur_size)) # 6. 将模糊后的投影粘贴到原图上,用自身Alpha通道做蒙版 self._image.paste(temp_image, (0, 0), mask=temp_image) if __name__ == "__main__": # 完全对应你提供的Photoshop参数 shadow_args: DropShadowParams = { "opacity": 65, "angle": 30, "distance": 15, "spread": 29, "size": 15, "color": (0, 0, 0) } img = Image.new("RGB", (1920, 1080), (255, 255, 255)) text_handler = Text(image=img) # 加载字体,找不到指定字体时用默认字体兜底 try: font = ImageFont.truetype("./assets/Hiragino.ttf", 200) except IOError: font = ImageFont.load_default(size=200) text_handler.text( (200, 400), "ダミーテキスト", font=font, color=(0, 153, 255, 255), drop_shadow=shadow_args ) img.show()
四、类型提示的优化建议
你之前觉得类型提示繁琐,主要是因为用了模糊的Dict来定义投影参数,改用TypedDict后不仅类型更明确,编辑器还能自动补全参数,代码可读性也更高。另外还有几个小优化:
- 把
ImageFont改为ImageFont.FreeTypeFont,类型提示更精准; color参数同时支持RGB和RGBA,用Union覆盖两种情况;- 避免使用
Union[int, Tuple[int,int,int]]这种模糊的类型,明确每个参数的类型后,代码维护起来更轻松。
备注:内容来源于stack exchange,提问作者woowaaahehahah




