working
This commit is contained in:
@@ -53,16 +53,16 @@ def generate_texture_image(props, width, height):
|
||||
bicubic_filter = getattr(resampling_module, 'BICUBIC', getattr(Image, 'BICUBIC', Image.BILINEAR))
|
||||
|
||||
def _iter_enabled_components():
|
||||
"""Yield enabled composition components."""
|
||||
"""Yield enabled composition components with their index."""
|
||||
overlays = getattr(props, 'image_overlays', [])
|
||||
try:
|
||||
overlay_list = list(overlays)
|
||||
except TypeError:
|
||||
overlay_list = []
|
||||
for overlay in overlay_list:
|
||||
for index, overlay in enumerate(overlay_list):
|
||||
if not getattr(overlay, 'enabled', True):
|
||||
continue
|
||||
yield overlay
|
||||
yield index, overlay
|
||||
|
||||
def _calculate_overlay_position(overlay, overlay_size):
|
||||
"""Calculate top-left pixel position for an overlay image."""
|
||||
@@ -91,102 +91,305 @@ def generate_texture_image(props, width, height):
|
||||
x = max(0, min(max_x, x))
|
||||
y = max(0, min(max_y, y))
|
||||
return x, y
|
||||
|
||||
def _make_text_overlay(component, base_font_size):
|
||||
"""Return (image, baseline_offset, descent, render_meta) for a text component."""
|
||||
text_value = getattr(component, 'text_content', '').strip()
|
||||
if not text_value:
|
||||
return None, None, None, None
|
||||
scale = getattr(component, 'scale', 1.0)
|
||||
try:
|
||||
scale = float(scale if scale is not None else 1.0)
|
||||
except (TypeError, ValueError):
|
||||
scale = 1.0
|
||||
if not math.isfinite(scale) or scale <= 0:
|
||||
return None, None, None, None
|
||||
desired_size = max(4, int(round(max(base_font_size, 4) * scale)))
|
||||
text_font = load_font_for_size(desired_size)
|
||||
|
||||
def _apply_composition_components(pil_canvas):
|
||||
"""Composite enabled components (text or image) onto the base canvas."""
|
||||
components = sorted(
|
||||
_iter_enabled_components(),
|
||||
key=lambda comp: getattr(comp, 'z_index', 0)
|
||||
)
|
||||
temp_draw = ImageDraw.Draw(Image.new('RGBA', (1, 1)))
|
||||
|
||||
for component in components:
|
||||
if getattr(component, 'component_type', 'IMAGE') == 'TEXT':
|
||||
text_value = getattr(component, 'text_content', '').strip()
|
||||
if not text_value:
|
||||
overlay_img = None
|
||||
baseline_offset = None
|
||||
component_descent = None
|
||||
render_meta = None
|
||||
|
||||
# Prefer anchoring to the left baseline so we can align with the main text baseline.
|
||||
try:
|
||||
baseline_bbox = temp_draw.textbbox(
|
||||
(0, 0),
|
||||
text_value,
|
||||
font=text_font,
|
||||
anchor='ls'
|
||||
)
|
||||
except Exception:
|
||||
baseline_bbox = None
|
||||
|
||||
if baseline_bbox:
|
||||
left, top, right, bottom = baseline_bbox
|
||||
text_width = max(1, int(round(right - left)))
|
||||
text_height = max(1, int(round(bottom - top)))
|
||||
overlay_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0))
|
||||
overlay_draw = ImageDraw.Draw(overlay_img)
|
||||
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
||||
# Place the baseline at a consistent offset from the top of the overlay.
|
||||
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')
|
||||
else:
|
||||
# Fallback for PIL versions without baseline-aware textbbox.
|
||||
try:
|
||||
text_bbox = temp_draw.textbbox((0, 0), text_value, font=text_font)
|
||||
except Exception:
|
||||
width, height = temp_draw.textsize(text_value, font=text_font)
|
||||
text_bbox = (0, 0, width, height)
|
||||
left, top, right, bottom = text_bbox
|
||||
text_width = max(1, int(round(right - left)))
|
||||
text_height = max(1, int(round(bottom - top)))
|
||||
overlay_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0))
|
||||
overlay_draw = ImageDraw.Draw(overlay_img)
|
||||
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
||||
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)
|
||||
|
||||
content_box = overlay_img.getbbox() if overlay_img else None
|
||||
crop_left = crop_top = 0
|
||||
if content_box:
|
||||
crop_left, crop_top, crop_right, crop_bottom = content_box
|
||||
overlay_img = overlay_img.crop(content_box)
|
||||
if baseline_offset is not None:
|
||||
baseline_offset -= crop_top
|
||||
component_descent = (crop_bottom - crop_top) - (baseline_offset or 0)
|
||||
else:
|
||||
component_descent = (overlay_img.height if overlay_img else 0) - (baseline_offset or 0)
|
||||
|
||||
if overlay_img and (baseline_offset is None or baseline_offset < 0 or baseline_offset > overlay_img.height):
|
||||
try:
|
||||
ascent, descent = text_font.getmetrics()
|
||||
except Exception:
|
||||
ascent, descent = overlay_img.height, 0
|
||||
baseline_offset = min(max(0, int(round(ascent))), overlay_img.height)
|
||||
component_descent = max(0, overlay_img.height - baseline_offset)
|
||||
|
||||
baseline_offset = max(0, int(round(baseline_offset or 0)))
|
||||
component_descent = max(0, int(round(component_descent or 0)))
|
||||
render_meta = {
|
||||
'text': text_value,
|
||||
'font': text_font,
|
||||
'color': tuple(int(c * 255) for c in props.text_color[:4]),
|
||||
'draw_offset_x': (draw_x - crop_left) if baseline_bbox else (offset_x - crop_left),
|
||||
'draw_offset_y': (draw_y - crop_top) if baseline_bbox else (offset_y - crop_top),
|
||||
'anchor': 'ls' if baseline_bbox else None,
|
||||
'width': overlay_img.width if overlay_img else 0,
|
||||
'height': overlay_img.height if overlay_img else 0
|
||||
}
|
||||
return overlay_img, baseline_offset, component_descent, render_meta
|
||||
|
||||
def _make_image_overlay(component):
|
||||
"""Return RGBA image for an image component or None."""
|
||||
resolved_path = getattr(component, 'image_path', '')
|
||||
if not resolved_path:
|
||||
return None
|
||||
scale = getattr(component, 'scale', 1.0)
|
||||
try:
|
||||
scale = float(scale if scale is not None else 1.0)
|
||||
except (TypeError, ValueError):
|
||||
scale = 1.0
|
||||
if not math.isfinite(scale) or scale <= 0:
|
||||
return None
|
||||
scale_to_apply = scale
|
||||
if is_svg_path(resolved_path):
|
||||
try:
|
||||
raster_info = ensure_svg_raster(resolved_path, scale)
|
||||
resolved_path = raster_info.path
|
||||
scale_to_apply = raster_info.applied_scale
|
||||
except SVGConversionError as svg_error:
|
||||
print(f"WARNING: Failed to rasterize SVG overlay '{component.image_path}': {svg_error}")
|
||||
return None
|
||||
try:
|
||||
with Image.open(resolved_path) as raw_overlay:
|
||||
overlay_img = raw_overlay.convert('RGBA')
|
||||
except Exception as overlay_error:
|
||||
print(f"WARNING: Failed to load overlay image '{resolved_path}': {overlay_error}")
|
||||
return None
|
||||
if scale_to_apply != 1.0:
|
||||
new_size = (
|
||||
max(1, int(round(overlay_img.width * scale_to_apply))),
|
||||
max(1, int(round(overlay_img.height * scale_to_apply)))
|
||||
)
|
||||
if new_size != overlay_img.size:
|
||||
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):
|
||||
"""Composite enabled components according to z-index and placement."""
|
||||
components_data = []
|
||||
for index, component in _iter_enabled_components():
|
||||
z_value = getattr(component, 'z_index', 0)
|
||||
if stage == 'before' and z_value >= 0:
|
||||
continue
|
||||
if stage == 'after' and z_value < 0:
|
||||
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)
|
||||
else:
|
||||
overlay_img = _make_image_overlay(component)
|
||||
baseline_offset = overlay_img.height if overlay_img is not None else None
|
||||
component_descent = overlay_img.height if overlay_img is not None else None
|
||||
render_meta = None
|
||||
if overlay_img is None and not render_meta:
|
||||
continue
|
||||
rotation = float(getattr(component, 'rotation', 0.0) or 0.0)
|
||||
direct_draw = False
|
||||
width_from_image = overlay_img.width if overlay_img is not None else 0
|
||||
height_from_image = overlay_img.height if overlay_img is not None else 0
|
||||
if component_type == 'TEXT' and render_meta and not (rotation % 360):
|
||||
direct_draw = True
|
||||
else:
|
||||
if overlay_img is None:
|
||||
continue
|
||||
scale = getattr(component, 'scale', 1.0)
|
||||
try:
|
||||
scale = float(scale if scale is not None else 1.0)
|
||||
except (TypeError, ValueError):
|
||||
scale = 1.0
|
||||
text_size = max(8, int(round(props.font_size * max(scale, 0.1))))
|
||||
text_font = load_font_for_size(text_size)
|
||||
try:
|
||||
text_bbox = text_font.getbbox(text_value)
|
||||
text_width = text_bbox[2] - text_bbox[0]
|
||||
text_height = text_bbox[3] - text_bbox[1]
|
||||
except AttributeError:
|
||||
try:
|
||||
text_width, text_height = text_font.getsize(text_value)
|
||||
except AttributeError:
|
||||
# Fallback to a temporary draw context
|
||||
tmp_draw = ImageDraw.Draw(Image.new('RGBA', (1, 1)))
|
||||
text_width, text_height = tmp_draw.textsize(text_value, font=text_font)
|
||||
|
||||
overlay_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0))
|
||||
overlay_draw = ImageDraw.Draw(overlay_img)
|
||||
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
||||
overlay_draw.text((0, 0), text_value, font=text_font, fill=text_color)
|
||||
|
||||
rotation = float(getattr(component, 'rotation', 0.0) or 0.0)
|
||||
if rotation % 360:
|
||||
overlay_img = overlay_img.rotate(-rotation, expand=True, resample=bicubic_filter)
|
||||
width_from_image = overlay_img.width
|
||||
height_from_image = overlay_img.height
|
||||
width_value = 0
|
||||
height_value = 0
|
||||
if component_type == 'TEXT' and render_meta:
|
||||
if direct_draw:
|
||||
width_value = render_meta.get('width', width_from_image)
|
||||
height_value = render_meta.get('height', height_from_image)
|
||||
else:
|
||||
width_value = width_from_image
|
||||
height_value = height_from_image
|
||||
elif overlay_img is not None:
|
||||
width_value = width_from_image
|
||||
height_value = height_from_image
|
||||
components_data.append({
|
||||
'index': index,
|
||||
'component': component,
|
||||
'image': overlay_img,
|
||||
'z': z_value,
|
||||
'type': component_type,
|
||||
'baseline_offset': baseline_offset,
|
||||
'descent': component_descent,
|
||||
'render_info': render_meta,
|
||||
'direct_draw': direct_draw,
|
||||
'rotation': rotation,
|
||||
'width': width_value,
|
||||
'height': height_value
|
||||
})
|
||||
|
||||
if not components_data:
|
||||
return pil_canvas
|
||||
|
||||
sequential_groups = {'PREPEND': [], 'APPEND': []}
|
||||
absolute_entries = []
|
||||
for entry in components_data:
|
||||
placement = getattr(entry['component'], 'placement_mode', 'ABSOLUTE') or 'ABSOLUTE'
|
||||
if placement in sequential_groups:
|
||||
sequential_groups[placement].append(entry)
|
||||
else:
|
||||
x_pos, y_pos = _calculate_overlay_position(entry['component'], entry['image'].size)
|
||||
entry['position'] = (x_pos, y_pos)
|
||||
absolute_entries.append(entry)
|
||||
|
||||
base_center_x = width * 0.5
|
||||
base_center_y = height * 0.5
|
||||
base_left = base_center_x
|
||||
base_right = base_center_x
|
||||
base_top = base_center_y - (base_font_size / 2.0)
|
||||
base_bottom = base_center_y + (base_font_size / 2.0)
|
||||
base_baseline = base_bottom
|
||||
if base_text_rect:
|
||||
base_center_x = base_text_rect['center_x']
|
||||
base_center_y = base_text_rect['center_y']
|
||||
base_left = base_text_rect['left']
|
||||
base_right = base_text_rect['right']
|
||||
base_top = base_text_rect.get('top', base_top)
|
||||
base_bottom = base_text_rect.get('bottom', base_bottom)
|
||||
base_baseline = base_text_rect.get('baseline', base_bottom)
|
||||
else:
|
||||
base_top = max(0.0, min(float(height - base_font_size), base_top))
|
||||
base_bottom = max(base_top + 1.0, min(float(height), base_bottom))
|
||||
base_baseline = base_bottom
|
||||
|
||||
def assign_sequential_positions(entries, placement):
|
||||
if not entries:
|
||||
return []
|
||||
sorted_entries = sorted(entries, key=lambda item: item['index'])
|
||||
cursor = base_left if placement == 'PREPEND' else base_right
|
||||
assigned = []
|
||||
for entry in sorted_entries:
|
||||
component = entry['component']
|
||||
entry_width = entry.get('width', entry['image'].width if entry['image'] else 0)
|
||||
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
|
||||
|
||||
x_pos, y_pos = _calculate_overlay_position(component, overlay_img.size)
|
||||
|
||||
if placement == 'PREPEND':
|
||||
cursor -= spacing_pixels
|
||||
cursor -= entry_width
|
||||
x_pos = cursor
|
||||
cursor -= inter_pixels
|
||||
else:
|
||||
cursor += spacing_pixels
|
||||
x_pos = cursor
|
||||
cursor += entry_width
|
||||
cursor += inter_pixels
|
||||
if base_text_rect:
|
||||
if entry['type'] == 'TEXT' and entry['baseline_offset'] is not None:
|
||||
y_pos = int(round(base_baseline - entry['baseline_offset']))
|
||||
else:
|
||||
y_pos = int(round(base_bottom - entry_height))
|
||||
else:
|
||||
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))
|
||||
entry['position'] = (x_pos, y_pos)
|
||||
assigned.append(entry)
|
||||
return assigned
|
||||
|
||||
positioned_entries = absolute_entries
|
||||
positioned_entries.extend(assign_sequential_positions(sequential_groups['PREPEND'], 'PREPEND'))
|
||||
positioned_entries.extend(assign_sequential_positions(sequential_groups['APPEND'], 'APPEND'))
|
||||
|
||||
positioned_entries.sort(key=lambda item: item['z'])
|
||||
|
||||
draw_ctx = None
|
||||
for entry in positioned_entries:
|
||||
x_pos, y_pos = entry.get('position', (0, 0))
|
||||
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')
|
||||
draw_ctx.text(
|
||||
draw_pos,
|
||||
info.get('text', ''),
|
||||
font=info.get('font'),
|
||||
fill=info.get('color'),
|
||||
anchor=anchor
|
||||
)
|
||||
else:
|
||||
overlay_img = entry.get('image')
|
||||
if overlay_img is None:
|
||||
continue
|
||||
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)
|
||||
continue
|
||||
|
||||
resolved_path = getattr(component, 'image_path', '')
|
||||
if not resolved_path:
|
||||
continue
|
||||
scale = getattr(component, 'scale', 1.0)
|
||||
try:
|
||||
scale = float(scale if scale is not None else 1.0)
|
||||
except (TypeError, ValueError):
|
||||
scale = 1.0
|
||||
|
||||
if not math.isfinite(scale) or scale <= 0:
|
||||
continue
|
||||
|
||||
scale_to_apply = scale
|
||||
|
||||
if is_svg_path(resolved_path):
|
||||
try:
|
||||
raster_info = ensure_svg_raster(resolved_path, scale)
|
||||
resolved_path = raster_info.path
|
||||
scale_to_apply = raster_info.applied_scale
|
||||
except SVGConversionError as svg_error:
|
||||
print(f"WARNING: Failed to rasterize SVG overlay '{component.image_path}': {svg_error}")
|
||||
continue
|
||||
|
||||
try:
|
||||
with Image.open(resolved_path) as raw_overlay:
|
||||
overlay_img = raw_overlay.convert('RGBA')
|
||||
except Exception as overlay_error:
|
||||
print(f"WARNING: Failed to load overlay image '{resolved_path}': {overlay_error}")
|
||||
continue
|
||||
|
||||
if scale_to_apply != 1.0:
|
||||
new_size = (
|
||||
max(1, int(round(overlay_img.width * scale_to_apply))),
|
||||
max(1, int(round(overlay_img.height * scale_to_apply)))
|
||||
)
|
||||
if new_size != overlay_img.size:
|
||||
overlay_img = overlay_img.resize(new_size, lanczos_filter)
|
||||
|
||||
rotation = float(getattr(component, 'rotation', 0.0) or 0.0)
|
||||
if rotation % 360:
|
||||
overlay_img = overlay_img.rotate(-rotation, expand=True, resample=bicubic_filter)
|
||||
|
||||
x_pos, y_pos = _calculate_overlay_position(component, overlay_img.size)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
return pil_canvas
|
||||
|
||||
# Create PIL image with transparent background
|
||||
@@ -198,6 +401,10 @@ def generate_texture_image(props, width, height):
|
||||
bg_color = tuple(int(c * 255) for c in props.background_color[:3]) + (255,)
|
||||
pil_img = Image.new('RGBA', (width, height), bg_color)
|
||||
|
||||
base_text_rect = None
|
||||
final_font_size = props.font_size
|
||||
before_layers_applied = False
|
||||
|
||||
# Draw text if provided (including prepend/append)
|
||||
full_text_parts = []
|
||||
if props.prepend_text.strip():
|
||||
@@ -212,7 +419,7 @@ def generate_texture_image(props, width, height):
|
||||
|
||||
if full_text_parts:
|
||||
pass
|
||||
draw = ImageDraw.Draw(pil_img)
|
||||
measure_draw = ImageDraw.Draw(pil_img)
|
||||
|
||||
# Combine text based on layout mode
|
||||
if props.prepend_append_layout == 'HORIZONTAL':
|
||||
@@ -304,12 +511,12 @@ def generate_texture_image(props, width, height):
|
||||
test_font = load_font_for_size(test_size)
|
||||
|
||||
try:
|
||||
bbox = draw.textbbox((0, 0), full_text, font=test_font)
|
||||
bbox = measure_draw.textbbox((0, 0), full_text, font=test_font)
|
||||
test_width = bbox[2] - bbox[0]
|
||||
test_height = bbox[3] - bbox[1]
|
||||
except (ValueError, AttributeError):
|
||||
# Fallback for bitmap fonts
|
||||
test_width, test_height = draw.textsize(full_text, font=test_font)
|
||||
test_width, test_height = measure_draw.textsize(full_text, font=test_font)
|
||||
|
||||
if test_width <= target_width and test_height <= target_height:
|
||||
pass
|
||||
@@ -323,7 +530,12 @@ def generate_texture_image(props, width, height):
|
||||
pass
|
||||
font_size = props.font_size
|
||||
|
||||
final_font_size = font_size
|
||||
font = load_font_for_size(font_size)
|
||||
try:
|
||||
base_ascent, base_descent = font.getmetrics()
|
||||
except Exception:
|
||||
base_ascent, base_descent = (final_font_size, 0)
|
||||
|
||||
# Calculate text position and draw
|
||||
if layout_mode == "vertical" and props.prepend_append_layout == 'VERTICAL':
|
||||
@@ -331,18 +543,25 @@ def generate_texture_image(props, width, height):
|
||||
# Draw each part separately for vertical layout
|
||||
y_offset = 0
|
||||
total_height = 0
|
||||
|
||||
part_widths = []
|
||||
part_baselines = []
|
||||
|
||||
# Calculate total height first
|
||||
part_heights = []
|
||||
for part in full_text_parts:
|
||||
pass
|
||||
try:
|
||||
bbox = draw.textbbox((0, 0), part, font=font)
|
||||
bbox = measure_draw.textbbox((0, 0), part, font=font)
|
||||
part_height = bbox[3] - bbox[1]
|
||||
part_width = bbox[2] - bbox[0]
|
||||
baseline = base_ascent
|
||||
except (ValueError, AttributeError):
|
||||
# Fallback for bitmap fonts
|
||||
_, part_height = draw.textsize(part, font=font)
|
||||
part_width, part_height = measure_draw.textsize(part, font=font)
|
||||
baseline = base_ascent
|
||||
part_heights.append(part_height)
|
||||
part_widths.append(part_width)
|
||||
part_baselines.append(baseline)
|
||||
total_height += part_height
|
||||
|
||||
# Add spacing between parts
|
||||
@@ -353,6 +572,23 @@ def generate_texture_image(props, width, height):
|
||||
|
||||
# Center vertically
|
||||
start_y = (height - total_height) // 2
|
||||
max_width = max(part_widths) if part_widths else 0
|
||||
left_x = (width - max_width) // 2
|
||||
base_text_rect = {
|
||||
'left': float(left_x),
|
||||
'right': float(left_x + max_width),
|
||||
'top': float(start_y),
|
||||
'bottom': float(start_y + total_height),
|
||||
'center_x': float(left_x + max_width / 2.0),
|
||||
'center_y': float(start_y + total_height / 2.0),
|
||||
'baseline': float(start_y + base_ascent),
|
||||
'descent': float(base_descent),
|
||||
'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
|
||||
draw = ImageDraw.Draw(pil_img)
|
||||
|
||||
# Draw each part
|
||||
current_y = start_y
|
||||
@@ -360,12 +596,7 @@ def generate_texture_image(props, width, height):
|
||||
|
||||
for i, part in enumerate(full_text_parts):
|
||||
pass
|
||||
try:
|
||||
bbox = draw.textbbox((0, 0), part, font=font)
|
||||
part_width = bbox[2] - bbox[0]
|
||||
except (ValueError, AttributeError):
|
||||
# Fallback for bitmap fonts
|
||||
part_width, _ = draw.textsize(part, font=font)
|
||||
part_width = part_widths[i]
|
||||
x = (width - part_width) // 2 # Center horizontally
|
||||
|
||||
draw.text((x, current_y), part, font=font, fill=text_color)
|
||||
@@ -377,17 +608,47 @@ def generate_texture_image(props, width, height):
|
||||
else:
|
||||
pass
|
||||
# Horizontal layout or single text
|
||||
left_offset = 0
|
||||
right_offset = 0
|
||||
top_offset = 0
|
||||
bottom_offset = 0
|
||||
|
||||
try:
|
||||
text_bbox = draw.textbbox((0, 0), full_text, font=font)
|
||||
text_bbox = measure_draw.textbbox((0, 0), full_text, font=font)
|
||||
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):
|
||||
# Fallback for bitmap fonts that don't support textbbox
|
||||
text_width, text_height = draw.textsize(full_text, font=font)
|
||||
text_width, text_height = measure_draw.textsize(full_text, font=font)
|
||||
right_offset = text_width
|
||||
bottom_offset = text_height
|
||||
|
||||
# Center text
|
||||
x = (width - text_width) // 2
|
||||
y = (height - text_height) // 2
|
||||
left = x + left_offset
|
||||
right = x + right_offset
|
||||
top = y + top_offset
|
||||
bottom = y + bottom_offset
|
||||
base_text_rect = {
|
||||
'left': float(left),
|
||||
'right': float(right),
|
||||
'top': float(top),
|
||||
'bottom': float(bottom),
|
||||
'center_x': float((left + right) / 2.0),
|
||||
'center_y': float((top + bottom) / 2.0),
|
||||
'baseline': float(y + base_ascent),
|
||||
'descent': float(base_descent),
|
||||
'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
|
||||
draw = ImageDraw.Draw(pil_img)
|
||||
|
||||
# Draw text
|
||||
text_color = tuple(int(c * 255) for c in props.text_color[:4])
|
||||
@@ -395,8 +656,9 @@ def generate_texture_image(props, width, height):
|
||||
else:
|
||||
pass
|
||||
|
||||
# Apply any configured image overlays before converting to Blender pixels
|
||||
pil_img = _apply_composition_components(pil_img)
|
||||
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)
|
||||
|
||||
# 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