Render shape/text shadows and gradient strokes

- soft offset shadow (shadowDistance/Blur/Angle/Color) for shape and
  rasterized text output + previews
- gradient shape strokes (ring-clipped gradient) instead of black fallback
(Note: tl-mode TypeLayer shadows need LayerEffects - not implemented yet)
This commit is contained in:
AskaEth committed 2026-08-28 09:33:47 +08:00
1 parent 7ee5f6e6a8
commit 724449f3f0
1 file changed
+46 -2
+46 -2
View File
@@ -262,13 +262,48 @@ def render_shape(ly: dict, cw: int, ch: int) -> Optional[Tuple[Image.Image, Tupl
draw.rounded_rectangle(bbox, radius=r_, outline=stroke["colors"][0][1],
width=thickness)
else:
draw.rounded_rectangle(bbox, radius=r_, outline=(0, 0, 0, 255),
width=thickness)
# gradient stroke: paste gradient clipped to a rounded-rect ring
stroke_img = make_linear_gradient(ww, hh, stroke)
ring = Image.new("L", (ww, hh), 0)
ImageDraw.Draw(ring).rounded_rectangle(bbox, radius=r_,
outline=255, width=thickness)
img.paste(stroke_img, (0, 0), ring)
apply_opacity(img, ly.get("alpha", 255))
# shadow (ppl renders a soft offset shadow per layer)
sd = float(ly.get("shadowDistance", "0") or 0)
sb = float(ly.get("shadowBlur", "0") or 0)
if (sd > 0 or sb > 0) and ly.get("_no_shadow") != "1":
img, ox, oy = _add_shadow(img, sd, sb,
float(ly.get("shadowAngle", "0") or 0),
ly.get("shadowColor", "-16777216"))
return img, (ix0 - ox, iy0 - oy)
return img, (ix0, iy0)
def _add_shadow(img, distance, blur, angle_deg, color_spec):
"""Render a soft offset shadow under the layer content. Returns
(expanded_img, offset_x, offset_y)."""
import math
from PIL import ImageFilter
rgba = argb_to_rgba(int(color_spec or "-16777216"))
rad = math.radians(angle_deg)
dx = int(round(distance * math.cos(rad)))
dy = int(round(distance * math.sin(rad)))
pad = max(int(blur) * 2 + abs(dx) + abs(dy) + 4, 8)
w, h = img.size
canvas = Image.new("RGBA", (w + 2 * pad, h + 2 * pad), (0, 0, 0, 0))
mask = img.getchannel("A")
shadow_fill = Image.new("RGBA", (w, h), rgba)
shadow_fill.putalpha(mask)
sh_layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
sh_layer.paste(shadow_fill, (pad + dx, pad + dy), mask)
sh_layer = sh_layer.filter(ImageFilter.GaussianBlur(blur))
canvas.alpha_composite(sh_layer)
canvas.alpha_composite(img, (pad, pad))
return canvas, pad, pad
def _font_path_for(ly: dict) -> str:
spec = ly.get("textFont", "")
return resolve_font(spec)
@@ -394,6 +429,15 @@ def render_text(ly: dict, cw: int, ch: int) -> Optional[Tuple[Image.Image, Tuple
img = img.crop((ix0 - left, iy0 - top, ix1 - left, iy1 - top))
apply_opacity(img, ly.get("alpha", 255))
# soft shadow (used by rasterized output / previews)
sd = float(ly.get("shadowDistance", "0") or 0)
sb = float(ly.get("shadowBlur", "0") or 0)
if (sd > 0 or sb > 0) and ly.get("_no_shadow") != "1":
img, ox, oy = _add_shadow(img, sd, sb,
float(ly.get("shadowAngle", "0") or 0),
ly.get("shadowColor", "-16777216"))
return img, (ix0 - ox, iy0 - oy)
return img, (ix0, iy0)