9 changed files with 87 additions and 44 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+83 -43
View File
@@ -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)
@@ -421,6 +455,11 @@ def generate_texture_image(props, width, height):
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 = []
if props.prepend_text.strip():
@@ -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,19 +694,22 @@ 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):
part_width = part_widths[i]
@@ -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
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
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))
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.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
+3
View File
@@ -109,6 +109,7 @@ def get_system_fonts():
elif system == "Darwin": # macOS
font_dirs = [
"/System/Library/Fonts/",
"/System/Library/Fonts/Supplemental/",
"/Library/Fonts/",
os.path.expanduser("~/Library/Fonts/")
]
@@ -189,6 +190,8 @@ def find_default_truetype_font():
]
elif system == "darwin":
preferred_fonts = [
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/System/Library/Fonts/Supplemental/Courier New.ttf",
"/System/Library/Fonts/SFNS.ttf",
"/System/Library/Fonts/SFNSDisplay.ttf",
"/System/Library/Fonts/Helvetica.ttc",