Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da7cdc6e28 | |||
| b0a86ee059 | |||
| 2df6d3bdc2 | |||
| edda76f519 | |||
| 09da031cd4 |
19
.gitignore
vendored
19
.gitignore
vendored
@@ -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
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
BIN
dist/text_texture_generator_v1.2.5.zip
vendored
Normal file
Binary file not shown.
BIN
dist/text_texture_generator_v1.2.5_free.zip
vendored
Normal file
BIN
dist/text_texture_generator_v1.2.5_free.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.
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
38
src/blender_manifest.toml
Normal 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"
|
||||
@@ -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)
|
||||
@@ -418,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():
|
||||
@@ -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,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]
|
||||
@@ -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
|
||||
|
||||
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)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user