diff --git a/Archive.zip b/Archive.zip index 35251b6..b5fea3c 100644 Binary files a/Archive.zip and b/Archive.zip differ diff --git a/__init__.py b/__init__.py index 7f1ce13..842490f 100644 --- a/__init__.py +++ b/__init__.py @@ -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') diff --git a/test_preset_unicode.py b/test_preset_unicode.py new file mode 100644 index 0000000..acaea6f --- /dev/null +++ b/test_preset_unicode.py @@ -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) + ("TestPreset", 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) \ No newline at end of file