You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何使用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实现投影的核心思路

因为你要在同一张图上放置多个带投影的文本,不能直接在原图上画投影再模糊(会污染其他内容),正确的步骤是:

  1. 创建一个和原图尺寸一致的临时透明图像,专门用来绘制投影文本;
  2. 在临时图像上绘制投影文本(先处理Spread的膨胀效果);
  3. 对临时图像应用高斯模糊(对应Size参数);
  4. 把模糊后的临时图像(即投影)粘贴到原图上,最后在原位置绘制正常文本。

三、完整优化后的代码

我用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

火山引擎 最新活动