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:
1 parent
7ee5f6e6a8
commit
724449f3f0
1 file changed
+46
-2
+46
-2
@@ -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],
|
draw.rounded_rectangle(bbox, radius=r_, outline=stroke["colors"][0][1],
|
||||||
width=thickness)
|
width=thickness)
|
||||||
else:
|
else:
|
||||||
draw.rounded_rectangle(bbox, radius=r_, outline=(0, 0, 0, 255),
|
# gradient stroke: paste gradient clipped to a rounded-rect ring
|
||||||
width=thickness)
|
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))
|
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)
|
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:
|
def _font_path_for(ly: dict) -> str:
|
||||||
spec = ly.get("textFont", "")
|
spec = ly.get("textFont", "")
|
||||||
return resolve_font(spec)
|
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))
|
img = img.crop((ix0 - left, iy0 - top, ix1 - left, iy1 - top))
|
||||||
|
|
||||||
apply_opacity(img, ly.get("alpha", 255))
|
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)
|
return img, (ix0, iy0)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user