diff --git a/ppl2psd.py b/ppl2psd.py index 25a1cce..34dcae0 100644 --- a/ppl2psd.py +++ b/ppl2psd.py @@ -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)