Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50c65497d0 | ||
|
|
da7cdc6e28 | ||
|
|
b0a86ee059 |
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user