5 Commits
v1.2.5 ... main

37 changed files with 149 additions and 51 deletions

19
.gitignore vendored
View File

@@ -68,10 +68,21 @@ desktop.ini
# Project-specific directories and files
.benchmarks/
# Marketing - Large binary files
marketing/screenrecords/*.mp4
marketing/voice/*.wav
marketing/*.psd
# Marketing - Large binary files and assets
marketing/**/*.mp4
marketing/**/*.mov
marketing/**/*.wav
marketing/**/*.psd
marketing/**/*.blend
marketing/**/*.blend[0-9]
marketing/screenrecords/
marketing/voice/
marketing/thumbnails/
# Build and Distribution
dist/
*.whl
src/blender_manifest.toml
# Temporary files
*.tmp

BIN
dist/text-texture-generator_1.0.0.zip vendored Normal file

Binary file not shown.

BIN
dist/text_texture_generator_v1.2.5.zip vendored Normal file

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.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

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.

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

38
src/blender_manifest.toml Normal file
View File

@@ -0,0 +1,38 @@
schema_version = "1.0.0"
# Module ID
id = "text_texture_generator"
# Module version
version = "1.2.5"
# Display name
name = "Text Texture Generator Pro"
# Short description
tagline = "Generate image textures from text with accurate dimensions and instant live preview"
# Maintainer
maintainer = "Marc Mintel <marc@mintel.me>"
# Version: standard or community
type = "add-on"
# Support: Community, Official or Core
support = "COMMUNITY"
# Website URI
website = "https://mintel.me"
# Common tags
tags = ["Material", "Shader", "Texture", "Text", "Image"]
# Minimum Blender version
blender_version_min = "4.0.0"
# License
license = ["GPL-3.0-or-later"]
[permissions]
# files = "Read and write access to files"
# network = "Access to the Internet"

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,16 +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:
font_size_val = getattr(props, 'font_size', 100)
width = font_size_val * len(text_value) * 0.6
height = font_size_val
text_bbox = (0, 0, width, height)
try:
font_size_val = int(getattr(props, 'font_size', 100))
except (ValueError, TypeError):
font_size_val = 100
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)))
@@ -163,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
@@ -234,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():
@@ -245,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
@@ -376,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)
@@ -417,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 = []
@@ -641,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)
@@ -655,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):
@@ -749,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:
@@ -791,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)
@@ -805,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)
@@ -855,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
@@ -903,9 +946,15 @@ def generate_preview(props, preview_width=None, preview_height=None):
pass
# Use full texture resolution when not explicitly provided
if not preview_width or preview_width <= 0:
preview_width = getattr(props, 'texture_width', 512) or 512
try:
preview_width = int(getattr(props, 'texture_width', 512))
except (ValueError, TypeError):
preview_width = 512
if not preview_height or preview_height <= 0:
preview_height = getattr(props, 'texture_height', 512) or 512
try:
preview_height = int(getattr(props, 'texture_height', 512))
except (ValueError, TypeError):
preview_height = 512
# Generate at preview resolution
result = generate_texture_image(props, preview_width, preview_height)