prepend append

This commit is contained in:
2025-08-11 14:40:14 +07:00
parent 4b39131efa
commit a46be309c0
3 changed files with 408 additions and 51 deletions

Binary file not shown.

View File

@@ -495,12 +495,57 @@ def generate_texture_image(props, width, height):
print("WARNING: Using default font - this may not support mixed-case properly!")
print("WARNING: Consider installing additional fonts for proper mixed-case support")
# Calculate text position
text = props.text if props.text and props.text.strip() else "Hello World"
# ============================================================================
# PREPEND/APPEND TEXT PROCESSING
# ============================================================================
# Get the main text
main_text = props.text if props.text and props.text.strip() else "Hello World"
# Get prepend and append text
prepend_text = props.prepend_text.strip() if props.prepend_text else ""
append_text = props.append_text.strip() if props.append_text else ""
# Calculate margins as pixels (percentage of font size)
prepend_margin_px = int((props.prepend_margin / 100.0) * font_size_pixels) if prepend_text else 0
append_margin_px = int((props.append_margin / 100.0) * font_size_pixels) if append_text else 0
# Combine text based on layout
if props.prepend_append_layout == 'HORIZONTAL':
# Horizontal layout: [prepend] [margin] [main] [margin] [append]
text_parts = []
if prepend_text:
text_parts.append(prepend_text)
# Add proper spacing based on margin percentage
if prepend_margin_px > 0:
# Convert margin pixels to approximate space characters
space_count = max(1, int(prepend_margin_px / (font_size_pixels * 0.3)))
text_parts.append(" " * space_count)
else:
text_parts.append(" ") # Default single space
text_parts.append(main_text)
if append_text:
# Add proper spacing based on margin percentage
if append_margin_px > 0:
space_count = max(1, int(append_margin_px / (font_size_pixels * 0.3)))
text_parts.append(" " * space_count)
else:
text_parts.append(" ") # Default single space
text_parts.append(append_text)
text = "".join(text_parts)
else:
# Vertical layout will be handled separately - for now use main text
text = main_text
# ENHANCED DIAGNOSTIC LOGGING for uppercase text issue
print(f"DEBUG UPPERCASE ISSUE - Original text input: '{props.text}'")
print(f"DEBUG UPPERCASE ISSUE - Processed text: '{text}'")
print(f"DEBUG PREPEND/APPEND - Original main text: '{props.text}'")
print(f"DEBUG PREPEND/APPEND - Prepend text: '{prepend_text}'")
print(f"DEBUG PREPEND/APPEND - Append text: '{append_text}'")
print(f"DEBUG PREPEND/APPEND - Layout: '{props.prepend_append_layout}'")
print(f"DEBUG PREPEND/APPEND - Final combined text: '{text}'")
print(f"DEBUG UPPERCASE ISSUE - Text type: {type(text)}")
print(f"DEBUG UPPERCASE ISSUE - Has upper chars: {any(c.isupper() for c in text)}")
print(f"DEBUG UPPERCASE ISSUE - Has lower chars: {any(c.islower() for c in text)}")
@@ -516,14 +561,52 @@ def generate_texture_image(props, width, height):
print("WARNING: Current font may not support mixed-case rendering properly!")
print("This could be the cause of uppercase-only text rendering")
try:
bbox = draw.textbbox((0, 0), text, font=font)
text_width = max(1, bbox[2] - bbox[0])
text_height = max(1, bbox[3] - bbox[1])
except (OSError, ZeroDivisionError, ValueError) as e:
print(f"Error getting text bbox: {e}")
text_width = int(len(text) * font_size_pixels * 0.6)
text_height = font_size_pixels
# Handle vertical layout separately if needed
if props.prepend_append_layout == 'VERTICAL' and (prepend_text or append_text):
# For vertical layout, we need to render each text part separately
# and position them vertically with proper margins
# Calculate dimensions for each text part
main_bbox = draw.textbbox((0, 0), main_text, font=font)
main_width = max(1, main_bbox[2] - main_bbox[0])
main_height = max(1, main_bbox[3] - main_bbox[1])
total_width = main_width
total_height = main_height
text_elements = [(main_text, main_width, main_height)]
if prepend_text:
prepend_bbox = draw.textbbox((0, 0), prepend_text, font=font)
prepend_width = max(1, prepend_bbox[2] - prepend_bbox[0])
prepend_height = max(1, prepend_bbox[3] - prepend_bbox[1])
total_width = max(total_width, prepend_width)
total_height += prepend_height + prepend_margin_px
text_elements.insert(0, (prepend_text, prepend_width, prepend_height))
if append_text:
append_bbox = draw.textbbox((0, 0), append_text, font=font)
append_width = max(1, append_bbox[2] - append_bbox[0])
append_height = max(1, append_bbox[3] - append_bbox[1])
total_width = max(total_width, append_width)
total_height += append_height + append_margin_px
text_elements.append((append_text, append_width, append_height))
text_width = total_width
text_height = total_height
print(f"DEBUG VERTICAL LAYOUT - Total dimensions: {text_width}x{text_height}")
print(f"DEBUG VERTICAL LAYOUT - Text elements: {len(text_elements)}")
else:
# Standard bbox calculation for horizontal layout or single text
try:
bbox = draw.textbbox((0, 0), text, font=font)
text_width = max(1, bbox[2] - bbox[0])
text_height = max(1, bbox[3] - bbox[1])
except (OSError, ZeroDivisionError, ValueError) as e:
print(f"Error getting text bbox: {e}")
text_width = int(len(text) * font_size_pixels * 0.6)
text_height = font_size_pixels
# ============================================================================
# ENHANCED POSITIONING SYSTEM - Replaces hardcoded vertical centering
@@ -550,10 +633,41 @@ def generate_texture_image(props, width, height):
if len(props.text_color) > 3:
text_color += (int(props.text_color[3] * 255),)
print(f"DEBUG UPPERCASE ISSUE - About to draw text: '{text}' with font: {font}")
draw.text((x, y), text, fill=text_color, font=font)
print(f"Text drawn at ({x}, {y})")
print(f"DEBUG UPPERCASE ISSUE - Text drawing completed")
# Handle vertical layout rendering
if props.prepend_append_layout == 'VERTICAL' and (prepend_text or append_text):
print(f"DEBUG VERTICAL LAYOUT - Rendering vertical text layout")
# Calculate starting Y position for vertical stack
current_y = y
# Render prepend text if exists
if prepend_text:
print(f"DEBUG VERTICAL LAYOUT - Drawing prepend text: '{prepend_text}' at y={current_y}")
draw.text((x, current_y), prepend_text, fill=text_color, font=font)
prepend_bbox = draw.textbbox((0, 0), prepend_text, font=font)
prepend_height = prepend_bbox[3] - prepend_bbox[1]
current_y += prepend_height + prepend_margin_px
# Render main text
print(f"DEBUG VERTICAL LAYOUT - Drawing main text: '{main_text}' at y={current_y}")
draw.text((x, current_y), main_text, fill=text_color, font=font)
main_bbox = draw.textbbox((0, 0), main_text, font=font)
main_height = main_bbox[3] - main_bbox[1]
current_y += main_height + append_margin_px
# Render append text if exists
if append_text:
print(f"DEBUG VERTICAL LAYOUT - Drawing append text: '{append_text}' at y={current_y}")
draw.text((x, current_y), append_text, fill=text_color, font=font)
print(f"DEBUG VERTICAL LAYOUT - Vertical text rendering completed")
else:
# Standard horizontal rendering
print(f"DEBUG UPPERCASE ISSUE - About to draw text: '{text}' with font: {font}")
draw.text((x, y), text, fill=text_color, font=font)
print(f"Text drawn at ({x}, {y})")
print(f"DEBUG UPPERCASE ISSUE - Text drawing completed")
# Add text to elements list
elements.append((0, 'text', text_img, 0, 0))
@@ -851,6 +965,12 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
default=False
)
expand_colors: BoolProperty(
name="Colors",
description="Show/hide Colors section",
default=False
)
text: StringProperty(
name="Text",
description="Text to render as texture",
@@ -859,6 +979,54 @@ class TEXT_TEXTURE_Properties(PropertyGroup):
update=update_live_preview
)
# Prepend/Append Text Properties
prepend_text: StringProperty(
name="Prepend Text",
description="Text to prepend before main text",
default="",
maxlen=512,
update=update_live_preview
)
append_text: StringProperty(
name="Append Text",
description="Text to append after main text",
default="",
maxlen=512,
update=update_live_preview
)
prepend_margin: FloatProperty(
name="Prepend Margin",
description="Margin between prepend text and main text as percentage of font size",
default=50.0,
min=0.0,
max=200.0,
precision=1,
update=update_live_preview
)
append_margin: FloatProperty(
name="Append Margin",
description="Margin between main text and append text as percentage of font size",
default=50.0,
min=0.0,
max=200.0,
precision=1,
update=update_live_preview
)
prepend_append_layout: EnumProperty(
name="Layout",
description="Layout arrangement for prepend/main/append text",
items=[
('HORIZONTAL', 'Horizontal', 'Arrange text horizontally: [prepend] [main] [append]'),
('VERTICAL', 'Vertical', 'Arrange text vertically: prepend above, main center, append below'),
],
default='HORIZONTAL',
update=update_live_preview
)
texture_width: IntProperty(
name="Width",
description="Texture width in pixels",
@@ -1442,8 +1610,16 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
# Validate preset name characters (avoid filesystem issues)
import re
if not re.match(r'^[a-zA-Z0-9_\-\s]+$', preset_name):
self.report({'ERROR'}, "Preset name can only contain letters, numbers, spaces, hyphens, and underscores")
# Allow Unicode letters, numbers, spaces, hyphens, underscores, and common accented characters
# This pattern supports international characters like ö, ü, ñ, etc.
if not re.match(r'^[\w\s\-]+$', preset_name, re.UNICODE):
self.report({'ERROR'}, "Preset name contains invalid characters for filesystem compatibility")
return {'CANCELLED'}
# Additional check for filesystem-unsafe characters
invalid_chars = ['<', '>', ':', '"', '|', '?', '*', '/', '\\']
if any(char in preset_name for char in invalid_chars):
self.report({'ERROR'}, f"Preset name cannot contain: {' '.join(invalid_chars)}")
return {'CANCELLED'}
# Check for duplicate names
@@ -1473,6 +1649,11 @@ class TEXT_TEXTURE_OT_save_preset(Operator):
'custom_font_path': props.custom_font_path,
'use_custom_font': props.use_custom_font,
'text_align': props.text_align,
'prepend_text': props.prepend_text,
'append_text': props.append_text,
'prepend_margin': props.prepend_margin,
'append_margin': props.append_margin,
'prepend_append_layout': props.prepend_append_layout,
'image_overlays': []
}
@@ -1587,6 +1768,11 @@ class TEXT_TEXTURE_OT_load_preset(Operator):
props.custom_font_path = preset_data.get('custom_font_path', props.custom_font_path)
props.use_custom_font = preset_data.get('use_custom_font', props.use_custom_font)
props.text_align = preset_data.get('text_align', props.text_align)
props.prepend_text = preset_data.get('prepend_text', props.prepend_text)
props.append_text = preset_data.get('append_text', props.append_text)
props.prepend_margin = preset_data.get('prepend_margin', props.prepend_margin)
props.append_margin = preset_data.get('append_margin', props.append_margin)
props.prepend_append_layout = preset_data.get('prepend_append_layout', props.prepend_append_layout)
# Load overlay data
props.image_overlays.clear()
@@ -1859,6 +2045,25 @@ def draw_positioning_section(layout, props):
row.prop(props, "texture_width", text="Width")
row.prop(props, "texture_height", text="Height")
layout.separator()
# Prepend/Append Text Section
layout.label(text="Additional Text:", icon='TEXT')
# Compact layout selector
layout.prop(props, "prepend_append_layout", text="Layout")
# Minimal prepend/append controls
row = layout.row(align=True)
row.prop(props, "prepend_text", text="Prepend")
if props.prepend_text.strip():
row.prop(props, "prepend_margin", text="", slider=True)
row = layout.row(align=True)
row.prop(props, "append_text", text="Append")
if props.append_text.strip():
row.prop(props, "append_margin", text="", slider=True)
layout.separator()
layout.prop(props, "position_mode", text="Mode")
@@ -1927,20 +2132,26 @@ def draw_font_settings_section(layout, props):
row = layout.row(align=True)
row.prop(props, "padding")
row.prop(props, "text_align")
def draw_colors_section(layout, props):
"""Draw color controls for text and background"""
layout.label(text="Text Color:", icon='COLOR')
layout.prop(props, "text_color", text="")
layout.separator()
layout.label(text="Colors:")
layout.prop(props, "text_color")
layout.label(text="Background Color:", icon='IMAGE_PLANE')
layout.prop(props, "background_transparent")
if not props.background_transparent:
layout.prop(props, "background_color")
layout.prop(props, "background_color", text="")
def draw_preview_options_section(layout, props):
"""Draw preview options"""
"""Draw preview options (preview-specific settings only)"""
layout.prop(props, "preview_size", text="Preview Size")
layout.separator()
layout.label(text="Preview Background Display:", icon='SCENE')
layout.prop(props, "preview_bg_type", text="Background")
if props.preview_bg_type == 'color':
layout.prop(props, "preview_bg_color", text="Background Color")
layout.prop(props, "preview_bg_color", text="Preview Display Color")
elif props.preview_bg_type == 'gradient':
layout.prop(props, "preview_bg_color", text="Color 1")
layout.prop(props, "preview_bg_color2", text="Color 2")
@@ -2048,6 +2259,10 @@ class TEXT_TEXTURE_PT_panel(Panel):
layout.separator()
draw_generate_button(layout)
# Preview Button at top level
layout.separator()
layout.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
layout.separator()
# COLLAPSIBLE SECTIONS
@@ -2067,16 +2282,10 @@ class TEXT_TEXTURE_PT_panel(Panel):
if font_section:
draw_font_settings_section(font_section, props)
# Preview Options Section (collapsed by default)
preview_section = draw_collapsible_header(layout, "expand_preview_options", "Preview Options", 'IMAGE_DATA')
if preview_section:
draw_preview_options_section(preview_section, props)
# Preview Button in expanded section
layout.separator()
preview_section.separator()
button_row = preview_section.row()
button_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
# Colors Section (collapsed by default)
colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
if colors_section:
draw_colors_section(colors_section, props)
# Presets Section (collapsed by default)
presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
@@ -2108,6 +2317,10 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
layout.separator()
draw_generate_button(layout)
# Preview Button at top level
layout.separator()
layout.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
layout.separator()
# COLLAPSIBLE SECTIONS
@@ -2127,15 +2340,10 @@ class TEXT_TEXTURE_PT_panel_3d(Panel):
if font_section:
draw_font_settings_section(font_section, props)
# Preview Options Section (collapsed by default)
preview_section = draw_collapsible_header(layout, "expand_preview_options", "Preview Options", 'IMAGE_DATA')
if preview_section:
draw_preview_options_section(preview_section, props)
# Preview Button in expanded section
preview_section.separator()
button_row = preview_section.row()
button_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
# Colors Section (collapsed by default)
colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
if colors_section:
draw_colors_section(colors_section, props)
# Presets Section (collapsed by default)
presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
@@ -2167,6 +2375,10 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
layout.separator()
draw_generate_button(layout)
# Preview Button at top level
layout.separator()
layout.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
layout.separator()
# COLLAPSIBLE SECTIONS
@@ -2186,15 +2398,10 @@ class TEXT_TEXTURE_PT_panel_properties(Panel):
if font_section:
draw_font_settings_section(font_section, props)
# Preview Options Section (collapsed by default)
preview_section = draw_collapsible_header(layout, "expand_preview_options", "Preview Options", 'IMAGE_DATA')
if preview_section:
draw_preview_options_section(preview_section, props)
# Preview Button in expanded section
preview_section.separator()
button_row = preview_section.row()
button_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
# Colors Section (collapsed by default)
colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
if colors_section:
draw_colors_section(colors_section, props)
# Presets Section (collapsed by default)
presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')

150
test_preset_unicode.py Normal file
View File

@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""
Test script to verify that preset names with Unicode characters like 'ö' work correctly.
This script tests the validation logic outside of Blender.
"""
import re
import json
import os
import tempfile
def test_preset_name_validation():
"""Test the new preset name validation regex"""
# Test cases with expected results
test_cases = [
# Valid names (should pass)
("Test Preset", True),
("Mein schönes Preset", True), # German with ö
("Préférence française", True), # French with accents
("Configuración español", True), # Spanish with ñ
("Test_123", True),
("Hello-World", True),
("Тест", True), # Cyrillic
("テスト", True), # Japanese
("测试", True), # Chinese
# Invalid names (should fail)
("Test<Preset", False), # Contains <
("Test>Preset", False), # Contains >
("Test:Preset", False), # Contains :
("Test\"Preset", False), # Contains "
("Test|Preset", False), # Contains |
("Test?Preset", False), # Contains ?
("Test*Preset", False), # Contains *
("Test/Preset", False), # Contains /
("Test\\Preset", False), # Contains \
("", False), # Empty string
]
print("Testing preset name validation...")
for preset_name, should_pass in test_cases:
# Test the Unicode regex pattern
unicode_pattern_match = bool(re.match(r'^[\w\s\-]+$', preset_name, re.UNICODE))
# Test filesystem-unsafe characters
invalid_chars = ['<', '>', ':', '"', '|', '?', '*', '/', '\\']
has_invalid_chars = any(char in preset_name for char in invalid_chars)
# Combined validation (same as in the actual code)
validation_passes = unicode_pattern_match and not has_invalid_chars and preset_name.strip()
status = "✅ PASS" if validation_passes == should_pass else "❌ FAIL"
print(f"{status}: '{preset_name}' -> Expected: {should_pass}, Got: {validation_passes}")
if validation_passes != should_pass:
print(f" Unicode match: {unicode_pattern_match}, Has invalid chars: {has_invalid_chars}")
def test_json_serialization():
"""Test that Unicode characters work correctly with JSON serialization/deserialization"""
print("\nTesting JSON serialization with Unicode characters...")
test_data = {
'name': 'Schönes Preset',
'text': 'Höllö Wörld',
'texture_width': 1024,
'texture_height': 1024,
'font_size': 96
}
try:
# Test JSON encoding/decoding
json_str = json.dumps(test_data, indent=2, ensure_ascii=False)
decoded_data = json.loads(json_str)
print("✅ JSON serialization successful")
print(f"Original name: {test_data['name']}")
print(f"Decoded name: {decoded_data['name']}")
print(f"Names match: {test_data['name'] == decoded_data['name']}")
# Test file I/O
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump(test_data, f, indent=2, ensure_ascii=False)
temp_file = f.name
with open(temp_file, 'r', encoding='utf-8') as f:
file_data = json.load(f)
os.unlink(temp_file)
print("✅ File I/O successful")
print(f"File data name: {file_data['name']}")
print(f"File names match: {test_data['name'] == file_data['name']}")
except Exception as e:
print(f"❌ JSON serialization failed: {e}")
def test_filesystem_compatibility():
"""Test creating files with Unicode names"""
print("\nTesting filesystem compatibility...")
test_names = [
"schönes_preset",
"café_preset",
"naïve_preset",
"test_ñ_preset"
]
temp_dir = tempfile.mkdtemp()
print(f"Testing in directory: {temp_dir}")
for name in test_names:
try:
filename = f"{name}.json"
filepath = os.path.join(temp_dir, filename)
# Create file
with open(filepath, 'w', encoding='utf-8') as f:
json.dump({'name': name}, f, ensure_ascii=False)
# Read file back
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
# Clean up
os.unlink(filepath)
print(f"{name}: File created and read successfully")
except Exception as e:
print(f"{name}: Failed - {e}")
# Clean up temp directory
os.rmdir(temp_dir)
if __name__ == "__main__":
print("=" * 60)
print("Testing Text Texture Generator Preset Unicode Support")
print("=" * 60)
test_preset_name_validation()
test_json_serialization()
test_filesystem_compatibility()
print("\n" + "=" * 60)
print("Test completed! Check results above.")
print("=" * 60)