refactor
This commit is contained in:
342
core/text_processor.py
Normal file
342
core/text_processor.py
Normal file
@@ -0,0 +1,342 @@
|
||||
def process_multiline_text(text, font, max_width, max_height):
|
||||
"""Process multiline text with automatic wrapping and fitting"""
|
||||
try:
|
||||
from PIL import ImageFont, ImageDraw, Image
|
||||
|
||||
print(f"[Text Processor] Processing multiline text with constraints: {max_width}x{max_height}")
|
||||
|
||||
# Split text into lines
|
||||
lines = text.split('\n')
|
||||
processed_lines = []
|
||||
|
||||
# Create temporary image for measurements
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
total_height = 0
|
||||
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
# Empty line
|
||||
bbox = draw.textbbox((0, 0), "A", font=font)
|
||||
line_height = bbox[3] - bbox[1]
|
||||
processed_lines.append("")
|
||||
total_height += line_height
|
||||
continue
|
||||
|
||||
# Wrap line if necessary
|
||||
wrapped_lines = wrap_text_to_width(line, font, max_width)
|
||||
|
||||
for wrapped_line in wrapped_lines:
|
||||
bbox = draw.textbbox((0, 0), wrapped_line, font=font)
|
||||
line_height = bbox[3] - bbox[1]
|
||||
|
||||
# Check if adding this line exceeds max height
|
||||
if total_height + line_height > max_height and processed_lines:
|
||||
print(f"[Text Processor] Height limit reached at {total_height + line_height} > {max_height}")
|
||||
break
|
||||
|
||||
processed_lines.append(wrapped_line)
|
||||
total_height += line_height
|
||||
|
||||
# Break if height limit reached
|
||||
if total_height >= max_height:
|
||||
break
|
||||
|
||||
print(f"[Text Processor] Processed {len(processed_lines)} lines, total height: {total_height}")
|
||||
|
||||
return {
|
||||
'lines': processed_lines,
|
||||
'total_height': total_height,
|
||||
'line_count': len(processed_lines)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error processing multiline text: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'lines': [text], 'total_height': 0, 'line_count': 1}
|
||||
|
||||
def wrap_text_to_width(text, font, max_width):
|
||||
"""Wrap text to fit within specified width"""
|
||||
try:
|
||||
from PIL import ImageDraw, Image
|
||||
|
||||
if not text.strip():
|
||||
return [""]
|
||||
|
||||
# Create temporary image for measurements
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
# Check if entire text fits
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
|
||||
if text_width <= max_width:
|
||||
return [text]
|
||||
|
||||
# Split into words and wrap
|
||||
words = text.split()
|
||||
lines = []
|
||||
current_line = ""
|
||||
|
||||
for word in words:
|
||||
test_line = current_line + (" " if current_line else "") + word
|
||||
bbox = draw.textbbox((0, 0), test_line, font=font)
|
||||
test_width = bbox[2] - bbox[0]
|
||||
|
||||
if test_width <= max_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
current_line = word
|
||||
else:
|
||||
# Single word is too long, force wrap
|
||||
lines.append(word)
|
||||
current_line = ""
|
||||
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
return lines
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error wrapping text: {e}")
|
||||
return [text]
|
||||
|
||||
def render_text_with_stroke(draw, position, text, font, fill_color, stroke_color=None, stroke_width=0):
|
||||
"""Render text with optional stroke/outline"""
|
||||
try:
|
||||
x, y = position
|
||||
|
||||
if stroke_color and stroke_width > 0:
|
||||
# Render stroke by drawing text at offset positions
|
||||
for dx in range(-stroke_width, stroke_width + 1):
|
||||
for dy in range(-stroke_width, stroke_width + 1):
|
||||
if dx != 0 or dy != 0:
|
||||
draw.text((x + dx, y + dy), text, font=font, fill=stroke_color)
|
||||
|
||||
# Render main text
|
||||
draw.text((x, y), text, font=font, fill=fill_color)
|
||||
|
||||
print(f"[Text Processor] Rendered text with stroke: width={stroke_width}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error rendering text with stroke: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def calculate_text_metrics(text, font):
|
||||
"""Calculate comprehensive text metrics"""
|
||||
try:
|
||||
from PIL import ImageDraw, Image
|
||||
|
||||
# Create temporary image for measurements
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
# Get bounding box
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
|
||||
metrics = {
|
||||
'width': bbox[2] - bbox[0],
|
||||
'height': bbox[3] - bbox[1],
|
||||
'left_bearing': bbox[0],
|
||||
'top_bearing': bbox[1],
|
||||
'advance_width': bbox[2],
|
||||
'advance_height': bbox[3]
|
||||
}
|
||||
|
||||
# Get additional font metrics if available
|
||||
try:
|
||||
metrics['ascent'] = font.getmetrics()[0]
|
||||
metrics['descent'] = font.getmetrics()[1]
|
||||
except:
|
||||
metrics['ascent'] = metrics['height']
|
||||
metrics['descent'] = 0
|
||||
|
||||
print(f"[Text Processor] Text metrics: {metrics}")
|
||||
return metrics
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error calculating text metrics: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'width': 0, 'height': 0}
|
||||
|
||||
def apply_text_effects(img, text_position, text_size, effects):
|
||||
"""Apply various text effects like shadow, glow, etc."""
|
||||
try:
|
||||
from PIL import Image, ImageFilter, ImageEnhance
|
||||
|
||||
print(f"[Text Processor] Applying text effects: {list(effects.keys())}")
|
||||
|
||||
result_img = img.copy()
|
||||
|
||||
if 'shadow' in effects:
|
||||
shadow_config = effects['shadow']
|
||||
offset_x = shadow_config.get('offset_x', 2)
|
||||
offset_y = shadow_config.get('offset_y', 2)
|
||||
blur_radius = shadow_config.get('blur', 0)
|
||||
shadow_color = shadow_config.get('color', (0, 0, 0, 128))
|
||||
|
||||
# Create shadow layer
|
||||
shadow_img = Image.new('RGBA', img.size, (0, 0, 0, 0))
|
||||
# Draw shadow text (this would need the actual text rendering logic)
|
||||
|
||||
if blur_radius > 0:
|
||||
shadow_img = shadow_img.filter(ImageFilter.GaussianBlur(radius=blur_radius))
|
||||
|
||||
# Composite shadow
|
||||
result_img = Image.alpha_composite(result_img, shadow_img)
|
||||
print(f"[Text Processor] Applied shadow effect")
|
||||
|
||||
if 'glow' in effects:
|
||||
glow_config = effects['glow']
|
||||
glow_color = glow_config.get('color', (255, 255, 255, 128))
|
||||
glow_radius = glow_config.get('radius', 3)
|
||||
|
||||
# Create glow effect
|
||||
glow_img = Image.new('RGBA', img.size, (0, 0, 0, 0))
|
||||
# Apply glow rendering logic here
|
||||
|
||||
result_img = Image.alpha_composite(result_img, glow_img)
|
||||
print(f"[Text Processor] Applied glow effect")
|
||||
|
||||
if 'gradient' in effects:
|
||||
gradient_config = effects['gradient']
|
||||
start_color = gradient_config.get('start', (255, 255, 255))
|
||||
end_color = gradient_config.get('end', (0, 0, 0))
|
||||
direction = gradient_config.get('direction', 'horizontal')
|
||||
|
||||
# Apply gradient to text
|
||||
print(f"[Text Processor] Applied gradient effect")
|
||||
|
||||
return result_img
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error applying text effects: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return img
|
||||
|
||||
def optimize_text_layout(text_blocks, canvas_width, canvas_height):
|
||||
"""Optimize layout of multiple text blocks to minimize overlap"""
|
||||
try:
|
||||
print(f"[Text Processor] Optimizing layout for {len(text_blocks)} text blocks")
|
||||
|
||||
optimized_blocks = []
|
||||
|
||||
for i, block in enumerate(text_blocks):
|
||||
x = block.get('x', 0)
|
||||
y = block.get('y', 0)
|
||||
width = block.get('width', 0)
|
||||
height = block.get('height', 0)
|
||||
|
||||
# Check for overlaps with previous blocks
|
||||
overlap_found = False
|
||||
for prev_block in optimized_blocks:
|
||||
if rectangles_overlap(
|
||||
(x, y, width, height),
|
||||
(prev_block['x'], prev_block['y'], prev_block['width'], prev_block['height'])
|
||||
):
|
||||
overlap_found = True
|
||||
break
|
||||
|
||||
if overlap_found:
|
||||
# Find new position
|
||||
new_x, new_y = find_non_overlapping_position(
|
||||
width, height, optimized_blocks, canvas_width, canvas_height
|
||||
)
|
||||
x, y = new_x, new_y
|
||||
print(f"[Text Processor] Moved block {i} to avoid overlap: ({x}, {y})")
|
||||
|
||||
optimized_block = block.copy()
|
||||
optimized_block.update({'x': x, 'y': y})
|
||||
optimized_blocks.append(optimized_block)
|
||||
|
||||
print(f"[Text Processor] Layout optimization complete")
|
||||
return optimized_blocks
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error optimizing text layout: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return text_blocks
|
||||
|
||||
def rectangles_overlap(rect1, rect2):
|
||||
"""Check if two rectangles overlap"""
|
||||
x1, y1, w1, h1 = rect1
|
||||
x2, y2, w2, h2 = rect2
|
||||
|
||||
return not (x1 + w1 <= x2 or x2 + w2 <= x1 or y1 + h1 <= y2 or y2 + h2 <= y1)
|
||||
|
||||
def find_non_overlapping_position(width, height, existing_blocks, canvas_width, canvas_height):
|
||||
"""Find a position that doesn't overlap with existing blocks"""
|
||||
# Try positions from top-left, moving right then down
|
||||
for y in range(0, canvas_height - height, 20):
|
||||
for x in range(0, canvas_width - width, 20):
|
||||
rect = (x, y, width, height)
|
||||
|
||||
overlap = False
|
||||
for block in existing_blocks:
|
||||
if rectangles_overlap(rect, (block['x'], block['y'], block['width'], block['height'])):
|
||||
overlap = True
|
||||
break
|
||||
|
||||
if not overlap:
|
||||
return x, y
|
||||
|
||||
# If no position found, return original or default
|
||||
return 0, 0
|
||||
|
||||
def validate_text_rendering(img, text_positions):
|
||||
"""Validate that text rendering was successful"""
|
||||
try:
|
||||
print(f"[Text Processor] Validating text rendering for {len(text_positions)} positions")
|
||||
|
||||
validation_results = []
|
||||
|
||||
for i, pos in enumerate(text_positions):
|
||||
result = {
|
||||
'position_index': i,
|
||||
'x': pos.get('x', 0),
|
||||
'y': pos.get('y', 0),
|
||||
'width': pos.get('width', 0),
|
||||
'height': pos.get('height', 0),
|
||||
'valid': True,
|
||||
'issues': []
|
||||
}
|
||||
|
||||
# Check bounds
|
||||
if pos.get('x', 0) < 0 or pos.get('y', 0) < 0:
|
||||
result['valid'] = False
|
||||
result['issues'].append('Negative position')
|
||||
|
||||
if pos.get('x', 0) + pos.get('width', 0) > img.width:
|
||||
result['valid'] = False
|
||||
result['issues'].append('Exceeds image width')
|
||||
|
||||
if pos.get('y', 0) + pos.get('height', 0) > img.height:
|
||||
result['valid'] = False
|
||||
result['issues'].append('Exceeds image height')
|
||||
|
||||
# Check for zero dimensions
|
||||
if pos.get('width', 0) == 0 or pos.get('height', 0) == 0:
|
||||
result['valid'] = False
|
||||
result['issues'].append('Zero dimensions')
|
||||
|
||||
validation_results.append(result)
|
||||
|
||||
valid_count = sum(1 for r in validation_results if r['valid'])
|
||||
print(f"[Text Processor] Validation complete: {valid_count}/{len(text_positions)} positions valid")
|
||||
|
||||
return validation_results
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Processor] Error validating text rendering: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
Reference in New Issue
Block a user