feat: enhance generation engine and preset management system
This commit is contained in:
@@ -331,8 +331,8 @@ def generate_texture_image(props, width, height):
|
||||
entry_height = entry.get('height', entry['image'].height if entry['image'] else 0)
|
||||
spacing_factor = float(getattr(component, 'text_spacing', 0.0) or 0.0) / 100.0
|
||||
inter_spacing_factor = float(getattr(component, 'image_spacing', 0.0) or 0.0) / 100.0
|
||||
spacing_pixels = spacing_factor * width
|
||||
inter_pixels = inter_spacing_factor * width
|
||||
spacing_pixels = spacing_factor * base_font_size
|
||||
inter_pixels = inter_spacing_factor * base_font_size
|
||||
|
||||
if placement == 'PREPEND':
|
||||
cursor -= spacing_pixels
|
||||
@@ -353,8 +353,11 @@ def generate_texture_image(props, width, height):
|
||||
center_y = base_center_y
|
||||
y_pos = int(round(center_y - entry_height / 2.0))
|
||||
x_pos = int(round(x_pos))
|
||||
y_pos = max(0, min(height - entry_height, y_pos))
|
||||
x_pos = max(0, min(width - entry_width, x_pos))
|
||||
# For sequential layouts (PREPEND/APPEND), we don't strictly clamp.
|
||||
# If the spacing is huge, it should render off-screen, not clump together at x=0
|
||||
if placement == 'ABSOLUTE':
|
||||
y_pos = max(0, min(height - entry_height, y_pos))
|
||||
x_pos = max(0, min(width - entry_width, x_pos))
|
||||
entry['position'] = (x_pos, y_pos)
|
||||
assigned.append(entry)
|
||||
return assigned
|
||||
@@ -423,6 +426,26 @@ def generate_texture_image(props, width, height):
|
||||
pass
|
||||
measure_draw = ImageDraw.Draw(pil_img)
|
||||
|
||||
# Gather static widths (Images) and prepare dynamic components for fitting loop
|
||||
static_overlay_width = 0.0
|
||||
dynamic_components = []
|
||||
|
||||
if hasattr(props, "image_overlays"):
|
||||
for component in props.image_overlays:
|
||||
if component.enabled and getattr(component, 'placement_mode', 'ABSOLUTE') in ['PREPEND', 'APPEND']:
|
||||
c_type = getattr(component, 'component_type', 'IMAGE')
|
||||
dynamic_components.append(component)
|
||||
|
||||
if c_type == 'IMAGE':
|
||||
# Image width is static to font scaling
|
||||
resolved_path = getattr(component, 'image_path', '')
|
||||
if resolved_path:
|
||||
try:
|
||||
with Image.open(resolved_path) as raw:
|
||||
static_overlay_width += raw.width * float(getattr(component, 'scale', 1.0) or 1.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Combine text based on layout mode
|
||||
if props.prepend_append_layout == 'HORIZONTAL':
|
||||
pass
|
||||
@@ -452,11 +475,12 @@ def generate_texture_image(props, width, height):
|
||||
text_parts_with_spacing.append(part)
|
||||
|
||||
full_text = " ".join(text_parts_with_spacing)
|
||||
|
||||
layout_mode = "horizontal"
|
||||
else:
|
||||
pass
|
||||
# Vertical layout - keep parts separate for individual positioning
|
||||
full_text = "\n".join(full_text_parts)
|
||||
|
||||
layout_mode = "vertical"
|
||||
|
||||
|
||||
@@ -497,6 +521,11 @@ def generate_texture_image(props, width, height):
|
||||
|
||||
# Determine font size (with text fitting if enabled)
|
||||
font_size = props.font_size
|
||||
stroke_width = getattr(props, 'stroke_width', 0) if getattr(props, 'enable_stroke', False) else 0
|
||||
stroke_color = tuple(int(c * 255) for c in getattr(props, 'stroke_color', (0, 0, 0, 1))[:4])
|
||||
stroke_kwargs = {'stroke_width': stroke_width, 'stroke_fill': stroke_color} if stroke_width > 0 else {}
|
||||
bbox_stroke_kwargs = {'stroke_width': stroke_width} if stroke_width > 0 else {}
|
||||
|
||||
if props.enable_text_fitting and has_text_fitting():
|
||||
pass
|
||||
|
||||
@@ -509,22 +538,34 @@ def generate_texture_image(props, width, height):
|
||||
|
||||
optimal_size = min_size
|
||||
for test_size in range(min_size, max_size + 1, 2): # Step by 2 for performance
|
||||
pass
|
||||
test_font = load_font_for_size(test_size)
|
||||
|
||||
try:
|
||||
bbox = measure_draw.textbbox((0, 0), full_text, font=test_font)
|
||||
bbox = measure_draw.textbbox((0, 0), full_text, font=test_font, **bbox_stroke_kwargs)
|
||||
test_width = bbox[2] - bbox[0]
|
||||
test_height = bbox[3] - bbox[1]
|
||||
except (ValueError, AttributeError):
|
||||
# Fallback for bitmap fonts
|
||||
test_width, test_height = measure_draw.textsize(full_text, font=test_font)
|
||||
except Exception:
|
||||
# Fallback gracefully if font measurement fails
|
||||
test_width = test_size * len(full_text) * 0.6 + stroke_width * 2
|
||||
test_height = test_size + stroke_width * 2
|
||||
|
||||
if test_width <= target_width and test_height <= target_height:
|
||||
pass
|
||||
# Add dynamic components width + spacing mathematically
|
||||
for component in dynamic_components:
|
||||
c_type = getattr(component, 'component_type', 'IMAGE')
|
||||
c_spacing = (float(getattr(component, 'text_spacing', 0.0) or 0.0) / 100.0) * test_size
|
||||
c_inter_spacing = (float(getattr(component, 'image_spacing', 0.0) or 0.0) / 100.0) * test_size
|
||||
test_width += c_spacing + c_inter_spacing
|
||||
|
||||
if c_type == 'TEXT' and component.text_content.strip():
|
||||
try:
|
||||
c_bbox = measure_draw.textbbox((0, 0), component.text_content.strip(), font=test_font, **bbox_stroke_kwargs)
|
||||
test_width += (c_bbox[2] - c_bbox[0]) * float(getattr(component, 'scale', 1.0) or 1.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if test_width + static_overlay_width <= target_width and test_height <= target_height:
|
||||
optimal_size = test_size
|
||||
else:
|
||||
pass
|
||||
break
|
||||
|
||||
font_size = optimal_size
|
||||
@@ -553,13 +594,14 @@ def generate_texture_image(props, width, height):
|
||||
for part in full_text_parts:
|
||||
pass
|
||||
try:
|
||||
bbox = measure_draw.textbbox((0, 0), part, font=font)
|
||||
bbox = measure_draw.textbbox((0, 0), part, font=font, **bbox_stroke_kwargs)
|
||||
part_height = bbox[3] - bbox[1]
|
||||
part_width = bbox[2] - bbox[0]
|
||||
baseline = base_ascent
|
||||
except (ValueError, AttributeError):
|
||||
# Fallback for bitmap fonts
|
||||
part_width, part_height = measure_draw.textsize(part, font=font)
|
||||
except Exception:
|
||||
# Fallback if font measurement fails
|
||||
part_width = font_size * len(part) * 0.6 + stroke_width * 2
|
||||
part_height = font_size + stroke_width * 2
|
||||
baseline = base_ascent
|
||||
part_heights.append(part_height)
|
||||
part_widths.append(part_width)
|
||||
@@ -590,22 +632,61 @@ def generate_texture_image(props, width, height):
|
||||
}
|
||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size)
|
||||
before_layers_applied = True
|
||||
draw = ImageDraw.Draw(pil_img)
|
||||
|
||||
# Draw each part
|
||||
current_y = start_y
|
||||
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)
|
||||
|
||||
shadow_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_shadow else None
|
||||
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
|
||||
|
||||
s_dx = getattr(props, 'shadow_offset_x', 8.0) if has_shadow else 0
|
||||
s_dy = getattr(props, 'shadow_offset_y', 8.0) if has_shadow else 0
|
||||
|
||||
for i, part in enumerate(full_text_parts):
|
||||
pass
|
||||
part_width = part_widths[i]
|
||||
x = (width - part_width) // 2 # Center horizontally
|
||||
|
||||
draw.text((x, current_y), part, font=font, fill=text_color)
|
||||
if has_shadow:
|
||||
s_draw.text((x + s_dx, current_y + s_dy), part, font=font, fill=s_color, **stroke_kwargs)
|
||||
if has_glow:
|
||||
g_draw.text((x, current_y), part, font=font, fill=g_color, **stroke_kwargs)
|
||||
|
||||
main_draw.text((x, current_y), part, font=font, fill=text_color, **stroke_kwargs)
|
||||
|
||||
current_y += part_heights[i]
|
||||
if i < len(full_text_parts) - 1: # Add spacing except after last part
|
||||
current_y += font_size // 4
|
||||
|
||||
# Apply effects and composite
|
||||
if has_shadow:
|
||||
s_blur = getattr(props, 'shadow_blur', 6.0)
|
||||
if s_blur > 0:
|
||||
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(radius=s_blur))
|
||||
pil_img = Image.alpha_composite(pil_img, shadow_layer)
|
||||
|
||||
if has_glow:
|
||||
g_blur = getattr(props, 'glow_blur', 10.0)
|
||||
if g_blur > 0:
|
||||
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(radius=g_blur))
|
||||
pil_img = Image.alpha_composite(pil_img, glow_layer)
|
||||
|
||||
if has_blur:
|
||||
t_blur = getattr(props, 'blur_radius', 2.0)
|
||||
if t_blur > 0:
|
||||
text_layer = text_layer.filter(ImageFilter.GaussianBlur(radius=t_blur))
|
||||
pil_img = Image.alpha_composite(pil_img, text_layer)
|
||||
|
||||
else:
|
||||
pass
|
||||
@@ -616,22 +697,60 @@ def generate_texture_image(props, width, height):
|
||||
bottom_offset = 0
|
||||
|
||||
try:
|
||||
text_bbox = measure_draw.textbbox((0, 0), full_text, font=font)
|
||||
text_bbox = measure_draw.textbbox((0, 0), full_text, font=font, **bbox_stroke_kwargs)
|
||||
text_width = text_bbox[2] - text_bbox[0]
|
||||
text_height = text_bbox[3] - text_bbox[1]
|
||||
left_offset = text_bbox[0]
|
||||
right_offset = text_bbox[2]
|
||||
top_offset = text_bbox[1]
|
||||
bottom_offset = text_bbox[3]
|
||||
except (ValueError, AttributeError):
|
||||
except Exception:
|
||||
# Fallback for bitmap fonts that don't support textbbox
|
||||
text_width, text_height = measure_draw.textsize(full_text, font=font)
|
||||
text_width = font_size * len(full_text) * 0.6 + stroke_width * 2
|
||||
text_height = font_size + stroke_width * 2
|
||||
right_offset = text_width
|
||||
bottom_offset = text_height
|
||||
|
||||
# Center text
|
||||
x = (width - text_width) // 2
|
||||
# Calculate total widths for PREPEND and APPEND components
|
||||
# to properly center the entire logical sequence.
|
||||
total_prepend_width = 0.0
|
||||
total_append_width = 0.0
|
||||
|
||||
if hasattr(props, "image_overlays"):
|
||||
for component in props.image_overlays:
|
||||
if component.enabled and getattr(component, 'placement_mode', 'ABSOLUTE') in ['PREPEND', 'APPEND']:
|
||||
c_type = getattr(component, 'component_type', 'IMAGE')
|
||||
c_spacing = float(getattr(component, 'text_spacing', 0.0) or 0.0) / 100.0 * final_font_size
|
||||
c_inter_spacing = float(getattr(component, 'image_spacing', 0.0) or 0.0) / 100.0 * final_font_size
|
||||
|
||||
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
|
||||
elif c_type == 'IMAGE':
|
||||
resolved_path = getattr(component, 'image_path', '')
|
||||
if resolved_path:
|
||||
try:
|
||||
with Image.open(resolved_path) as raw:
|
||||
c_width = raw.width * float(getattr(component, 'scale', 1.0) or 1.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if getattr(component, 'placement_mode', 'ABSOLUTE') == 'PREPEND':
|
||||
total_prepend_width += (c_width + c_spacing + c_inter_spacing)
|
||||
else:
|
||||
total_append_width += (c_width + c_spacing + c_inter_spacing)
|
||||
|
||||
# Center the entire logical block
|
||||
total_logical_width = total_prepend_width + text_width + total_append_width
|
||||
logical_start_x = (width - total_logical_width) // 2
|
||||
|
||||
x = logical_start_x + int(total_prepend_width)
|
||||
y = (height - text_height) // 2
|
||||
|
||||
left = x + left_offset
|
||||
right = x + right_offset
|
||||
top = y + top_offset
|
||||
@@ -650,11 +769,52 @@ def generate_texture_image(props, width, height):
|
||||
}
|
||||
pil_img = _apply_component_layers(pil_img, 'before', base_text_rect, final_font_size)
|
||||
before_layers_applied = True
|
||||
draw = ImageDraw.Draw(pil_img)
|
||||
|
||||
# Draw text
|
||||
from PIL import ImageFilter
|
||||
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
||||
draw.text((x, y), full_text, font=font, fill=text_color)
|
||||
|
||||
# Create effect layers
|
||||
has_shadow = getattr(props, 'enable_shadow', False)
|
||||
has_glow = getattr(props, 'enable_glow', False)
|
||||
has_blur = getattr(props, 'enable_blur', False)
|
||||
|
||||
shadow_layer = Image.new('RGBA', (width, height), (0, 0, 0, 0)) if has_shadow else None
|
||||
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)
|
||||
|
||||
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])
|
||||
s_dx = getattr(props, 'shadow_offset_x', 8.0)
|
||||
s_dy = getattr(props, 'shadow_offset_y', 8.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)
|
||||
|
||||
# Apply effects and composite
|
||||
if has_shadow:
|
||||
s_blur = getattr(props, 'shadow_blur', 6.0)
|
||||
if s_blur > 0:
|
||||
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(radius=s_blur))
|
||||
pil_img = Image.alpha_composite(pil_img, shadow_layer)
|
||||
|
||||
if has_glow:
|
||||
g_blur = getattr(props, 'glow_blur', 10.0)
|
||||
if g_blur > 0:
|
||||
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(radius=g_blur))
|
||||
pil_img = Image.alpha_composite(pil_img, glow_layer)
|
||||
|
||||
if has_blur:
|
||||
t_blur = getattr(props, 'blur_radius', 2.0)
|
||||
if t_blur > 0:
|
||||
text_layer = text_layer.filter(ImageFilter.GaussianBlur(radius=t_blur))
|
||||
pil_img = Image.alpha_composite(pil_img, text_layer)
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user