From a46be309c0f07497b502861069da9aa030bb50d8 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 11 Aug 2025 14:40:14 +0700 Subject: [PATCH] prepend append --- Archive.zip | Bin 18883 -> 20675 bytes __init__.py | 309 ++++++++++++++++++++++++++++++++++------- test_preset_unicode.py | 150 ++++++++++++++++++++ 3 files changed, 408 insertions(+), 51 deletions(-) create mode 100644 test_preset_unicode.py diff --git a/Archive.zip b/Archive.zip index 35251b6204a78eef196d7023a07f36bc21bceb7a..b5fea3c6f6c5a09e673c5db73a60f0c8391ff85e 100644 GIT binary patch delta 20296 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 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