fix(composition): unify effect pipeline for composition components and fix horizontal layout measurement v1.2.5
This commit is contained in:
@@ -100,7 +100,7 @@ def generate_texture_image(props, width, height):
|
||||
y = max(0, min(max_y, y))
|
||||
return x, y
|
||||
|
||||
def _make_text_overlay(component, base_font_size):
|
||||
def _make_text_overlay(component, base_font_size, stroke_kwargs={}):
|
||||
"""Return (image, baseline_offset, descent, render_meta) for a text component."""
|
||||
text_value = getattr(component, 'text_content', '').strip()
|
||||
if not text_value:
|
||||
@@ -115,6 +115,10 @@ def generate_texture_image(props, width, height):
|
||||
desired_size = max(4, int(round(max(base_font_size, 4) * scale)))
|
||||
text_font = load_font_for_size(desired_size)
|
||||
|
||||
# Prepare stroke measurement for the component
|
||||
comp_stroke_width = stroke_kwargs.get('stroke_width', 0)
|
||||
bbox_stroke_kwargs = {'stroke_width': comp_stroke_width} if comp_stroke_width > 0 else {}
|
||||
|
||||
temp_draw = ImageDraw.Draw(Image.new('RGBA', (1, 1)))
|
||||
|
||||
overlay_img = None
|
||||
@@ -128,7 +132,8 @@ def generate_texture_image(props, width, height):
|
||||
(0, 0),
|
||||
text_value,
|
||||
font=text_font,
|
||||
anchor='ls'
|
||||
anchor='ls',
|
||||
**bbox_stroke_kwargs
|
||||
)
|
||||
except Exception:
|
||||
baseline_bbox = None
|
||||
@@ -144,19 +149,19 @@ def generate_texture_image(props, width, height):
|
||||
baseline_offset = -top
|
||||
draw_x = -left
|
||||
draw_y = -top
|
||||
overlay_draw.text((draw_x, draw_y), text_value, font=text_font, fill=text_color, anchor='ls')
|
||||
overlay_draw.text((draw_x, draw_y), text_value, font=text_font, fill=text_color, anchor='ls', **stroke_kwargs)
|
||||
else:
|
||||
# Fallback for PIL versions without baseline-aware textbbox.
|
||||
try:
|
||||
text_bbox = temp_draw.textbbox((0, 0), text_value, font=text_font)
|
||||
text_bbox = temp_draw.textbbox((0, 0), text_value, font=text_font, **bbox_stroke_kwargs)
|
||||
except Exception:
|
||||
try:
|
||||
font_size_val = int(getattr(props, 'font_size', 100))
|
||||
except (ValueError, TypeError):
|
||||
font_size_val = 100
|
||||
width = font_size_val * len(text_value) * 0.6
|
||||
height = font_size_val
|
||||
text_bbox = (0, 0, width, height)
|
||||
width_est = font_size_val * len(text_value) * 0.6 + comp_stroke_width * 2
|
||||
height_est = font_size_val + comp_stroke_width * 2
|
||||
text_bbox = (0, 0, width_est, height_est)
|
||||
left, top, right, bottom = text_bbox
|
||||
text_width = max(1, int(round(right - left)))
|
||||
text_height = max(1, int(round(bottom - top)))
|
||||
@@ -166,7 +171,7 @@ def generate_texture_image(props, width, height):
|
||||
offset_x = -left
|
||||
offset_y = -top
|
||||
baseline_offset = offset_y
|
||||
overlay_draw.text((offset_x, offset_y), text_value, font=text_font, fill=text_color)
|
||||
overlay_draw.text((offset_x, offset_y), text_value, font=text_font, fill=text_color, **stroke_kwargs)
|
||||
|
||||
content_box = overlay_img.getbbox() if overlay_img else None
|
||||
crop_left = crop_top = 0
|
||||
@@ -237,7 +242,7 @@ def generate_texture_image(props, width, height):
|
||||
overlay_img = overlay_img.resize(new_size, lanczos_filter)
|
||||
return overlay_img
|
||||
|
||||
def _apply_component_layers(pil_canvas, stage, base_text_rect, base_font_size):
|
||||
def _apply_component_layers(pil_canvas, stage, base_text_rect, base_font_size, s_draw=None, g_draw=None, s_dx=0.0, s_dy=0.0, stroke_kwargs={}):
|
||||
"""Composite enabled components according to z-index and placement."""
|
||||
components_data = []
|
||||
for index, component in _iter_enabled_components():
|
||||
@@ -248,7 +253,7 @@ def generate_texture_image(props, width, height):
|
||||
continue
|
||||
component_type = getattr(component, 'component_type', 'IMAGE')
|
||||
if component_type == 'TEXT':
|
||||
overlay_img, baseline_offset, component_descent, render_meta = _make_text_overlay(component, base_font_size)
|
||||
overlay_img, baseline_offset, component_descent, render_meta = _make_text_overlay(component, base_font_size, stroke_kwargs)
|
||||
else:
|
||||
overlay_img = _make_image_overlay(component)
|
||||
baseline_offset = overlay_img.height if overlay_img is not None else None
|
||||
@@ -379,29 +384,58 @@ def generate_texture_image(props, width, height):
|
||||
|
||||
positioned_entries.sort(key=lambda item: item['z'])
|
||||
|
||||
draw_ctx = None
|
||||
# Main canvas drawing context
|
||||
draw_ctx = ImageDraw.Draw(pil_canvas)
|
||||
|
||||
# Effect background/shadow colors
|
||||
s_color = tuple(int(c * 255) for c in getattr(props, 'shadow_color', (0,0,0,1))[:4]) if s_draw else None
|
||||
g_color = tuple(int(c * 255) for c in getattr(props, 'glow_color', (1,1,1,0.8))[:4]) if g_draw else None
|
||||
|
||||
for entry in positioned_entries:
|
||||
x_pos, y_pos = entry.get('position', (0, 0))
|
||||
is_text = (entry['type'] == 'TEXT')
|
||||
|
||||
if entry.get('direct_draw') and entry.get('render_info'):
|
||||
if draw_ctx is None:
|
||||
draw_ctx = ImageDraw.Draw(pil_canvas)
|
||||
info = entry['render_info']
|
||||
draw_pos = (
|
||||
x_pos + info.get('draw_offset_x', 0),
|
||||
y_pos + info.get('draw_offset_y', 0)
|
||||
)
|
||||
anchor = info.get('anchor')
|
||||
text_val = info.get('text', '')
|
||||
text_font = info.get('font')
|
||||
|
||||
# Apply shadow to the shadow layer if drawing context exists
|
||||
if s_draw and is_text:
|
||||
s_pos = (draw_pos[0] + s_dx, draw_pos[1] + s_dy)
|
||||
s_draw.text(s_pos, text_val, font=text_font, fill=s_color, anchor=anchor, **stroke_kwargs)
|
||||
|
||||
# Apply glow to glow layer
|
||||
if g_draw and is_text:
|
||||
g_draw.text(draw_pos, text_val, font=text_font, fill=g_color, anchor=anchor, **stroke_kwargs)
|
||||
|
||||
# Apply main text
|
||||
draw_ctx.text(
|
||||
draw_pos,
|
||||
info.get('text', ''),
|
||||
font=info.get('font'),
|
||||
text_val,
|
||||
font=text_font,
|
||||
fill=info.get('color'),
|
||||
anchor=anchor
|
||||
anchor=anchor,
|
||||
**stroke_kwargs
|
||||
)
|
||||
else:
|
||||
overlay_img = entry.get('image')
|
||||
if overlay_img is None:
|
||||
continue
|
||||
|
||||
# Paste shadow for component? Actually images don't get the same stroke/shadow easily
|
||||
# but we can paste them into shadow/glow layers if they are icons.
|
||||
if is_text: # Only apply shadow/glow to text components for now
|
||||
if s_draw:
|
||||
s_draw.bitmap((x_pos + s_dx, y_pos + s_dy), overlay_img, fill=s_color)
|
||||
if g_draw:
|
||||
g_draw.bitmap((x_pos, y_pos), overlay_img, fill=g_color)
|
||||
|
||||
overlay_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
||||
overlay_layer.paste(overlay_img, (x_pos, y_pos), overlay_img)
|
||||
pil_canvas = Image.alpha_composite(pil_canvas, overlay_layer)
|
||||
@@ -420,6 +454,11 @@ def generate_texture_image(props, width, height):
|
||||
base_text_rect = None
|
||||
final_font_size = props.font_size
|
||||
before_layers_applied = False
|
||||
|
||||
# Initialize effect-related variables early to avoid UnboundLocalError
|
||||
s_draw, g_draw = None, None
|
||||
s_dx, s_dy = 0.0, 0.0
|
||||
stroke_kwargs = {}
|
||||
|
||||
# Draw text if provided (including prepend/append)
|
||||
full_text_parts = []
|
||||
@@ -644,12 +683,9 @@ def generate_texture_image(props, width, height):
|
||||
'draw_y': float(start_y),
|
||||
'ascent': float(base_ascent)
|
||||
}
|
||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size)
|
||||
before_layers_applied = True
|
||||
# Pre-initialize effect layers and drawing contexts
|
||||
from PIL import ImageFilter
|
||||
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
||||
|
||||
# Create effect layers
|
||||
has_shadow = getattr(props, 'enable_shadow', False)
|
||||
has_glow = getattr(props, 'enable_glow', False)
|
||||
has_blur = getattr(props, 'enable_blur', False)
|
||||
@@ -658,18 +694,21 @@ def generate_texture_image(props, width, height):
|
||||
glow_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_glow else None
|
||||
text_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_blur else None
|
||||
|
||||
main_draw = ImageDraw.Draw(pil_img) if not has_blur else ImageDraw.Draw(text_layer)
|
||||
s_draw = ImageDraw.Draw(shadow_layer) if has_shadow else None
|
||||
g_draw = ImageDraw.Draw(glow_layer) if has_glow else None
|
||||
|
||||
s_color = tuple(int(c * 255) for c in getattr(props, 'shadow_color', (0,0,0,1))[:4]) if has_shadow else None
|
||||
g_color = tuple(int(c * 255) for c in getattr(props, 'glow_color', (1,1,1,0.8))[:4]) if has_glow else None
|
||||
|
||||
try:
|
||||
s_dx = float(getattr(props, 'shadow_offset_x', 8.0)) if has_shadow else 0.0
|
||||
s_dy = float(getattr(props, 'shadow_offset_y', 8.0)) if has_shadow else 0.0
|
||||
except (ValueError, TypeError):
|
||||
s_dx, s_dy = 0.0, 0.0
|
||||
|
||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size, s_draw=s_draw, g_draw=g_draw, s_dx=s_dx, s_dy=s_dy, stroke_kwargs=stroke_kwargs)
|
||||
before_layers_applied = True
|
||||
|
||||
main_draw = ImageDraw.Draw(pil_img) if not has_blur else ImageDraw.Draw(text_layer)
|
||||
s_color = tuple(int(c * 255) for c in getattr(props, 'shadow_color', (0,0,0,1))[:4]) if has_shadow else None
|
||||
g_color = tuple(int(c * 255) for c in getattr(props, 'glow_color', (1,1,1,0.8))[:4]) if has_glow else None
|
||||
|
||||
current_y = start_y
|
||||
for i, part in enumerate(full_text_parts):
|
||||
@@ -752,11 +791,10 @@ def generate_texture_image(props, width, height):
|
||||
|
||||
c_width = 0
|
||||
if c_type == 'TEXT' and component.text_content.strip():
|
||||
try:
|
||||
c_bbox = measure_draw.textbbox((0, 0), component.text_content.strip(), font=font, **bbox_stroke_kwargs)
|
||||
c_width = c_bbox[2] - c_bbox[0]
|
||||
except Exception:
|
||||
pass
|
||||
# Use the same helper to get the final (possibly cropped/stroked) width
|
||||
temp_img, _, _, meta = _make_text_overlay(component, final_font_size, stroke_kwargs)
|
||||
if meta:
|
||||
c_width = meta.get('width', 0)
|
||||
elif c_type == 'IMAGE':
|
||||
resolved_path = getattr(component, 'image_path', '')
|
||||
if resolved_path:
|
||||
@@ -794,12 +832,9 @@ def generate_texture_image(props, width, height):
|
||||
'draw_y': float(y),
|
||||
'ascent': float(base_ascent)
|
||||
}
|
||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size)
|
||||
before_layers_applied = True
|
||||
# Pre-initialize effect layers and drawing contexts
|
||||
from PIL import ImageFilter
|
||||
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
||||
|
||||
# Create effect layers
|
||||
has_shadow = getattr(props, 'enable_shadow', False)
|
||||
has_glow = getattr(props, 'enable_glow', False)
|
||||
has_blur = getattr(props, 'enable_blur', False)
|
||||
@@ -808,21 +843,26 @@ def generate_texture_image(props, width, height):
|
||||
glow_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_glow else None
|
||||
text_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_blur else None
|
||||
|
||||
s_draw = ImageDraw.Draw(shadow_layer) if has_shadow else None
|
||||
g_draw = ImageDraw.Draw(glow_layer) if has_glow else None
|
||||
|
||||
try:
|
||||
s_dx = float(getattr(props, 'shadow_offset_x', 8.0)) if has_shadow else 0.0
|
||||
s_dy = float(getattr(props, 'shadow_offset_y', 8.0)) if has_shadow else 0.0
|
||||
except (ValueError, TypeError):
|
||||
s_dx, s_dy = 0.0, 0.0
|
||||
|
||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size, s_draw=s_draw, g_draw=g_draw, s_dx=s_dx, s_dy=s_dy, stroke_kwargs=stroke_kwargs)
|
||||
before_layers_applied = True
|
||||
|
||||
main_draw = ImageDraw.Draw(pil_img) if not has_blur else ImageDraw.Draw(text_layer)
|
||||
s_color = tuple(int(c * 255) for c in getattr(props, 'shadow_color', (0,0,0,1))[:4]) if has_shadow else None
|
||||
g_color = tuple(int(c * 255) for c in getattr(props, 'glow_color', (1,1,1,0.8))[:4]) if has_glow else None
|
||||
|
||||
if has_shadow:
|
||||
s_draw = ImageDraw.Draw(shadow_layer)
|
||||
s_color = tuple(int(c * 255) for c in getattr(props, 'shadow_color', (0,0,0,1))[:4])
|
||||
try:
|
||||
s_dx = float(getattr(props, 'shadow_offset_x', 8.0))
|
||||
s_dy = float(getattr(props, 'shadow_offset_y', 8.0))
|
||||
except (ValueError, TypeError):
|
||||
s_dx, s_dy = 0.0, 0.0
|
||||
s_draw.text((x + s_dx, y + s_dy), full_text, font=font, fill=s_color, **stroke_kwargs)
|
||||
|
||||
if has_glow:
|
||||
g_draw = ImageDraw.Draw(glow_layer)
|
||||
g_color = tuple(int(c * 255) for c in getattr(props, 'glow_color', (1,1,1,0.8))[:4])
|
||||
g_draw.text((x, y), full_text, font=font, fill=g_color, **stroke_kwargs)
|
||||
|
||||
main_draw.text((x, y), full_text, font=font, fill=text_color, **stroke_kwargs)
|
||||
@@ -858,8 +898,8 @@ def generate_texture_image(props, width, height):
|
||||
pass
|
||||
|
||||
if not before_layers_applied:
|
||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size)
|
||||
pil_img = _apply_component_layers(pil_img, 'after', base_text_rect, final_font_size)
|
||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size, s_draw=s_draw, g_draw=g_draw, s_dx=s_dx, s_dy=s_dy, stroke_kwargs=stroke_kwargs)
|
||||
pil_img = _apply_component_layers(pil_img, 'after', base_text_rect, final_font_size, s_draw=s_draw, g_draw=g_draw, s_dx=s_dx, s_dy=s_dy, stroke_kwargs=stroke_kwargs)
|
||||
|
||||
# Convert PIL image to Blender format (flip vertically to fix mirroring)
|
||||
pil_img = pil_img.transpose(Image.FLIP_TOP_BOTTOM) # Fix mirroring issue
|
||||
|
||||
Reference in New Issue
Block a user