refactor
This commit is contained in:
313
core/text_fitting.py
Normal file
313
core/text_fitting.py
Normal file
@@ -0,0 +1,313 @@
|
||||
def calculate_available_text_area(width, height, overlays):
|
||||
"""Calculate available area for text placement, avoiding overlay positions"""
|
||||
try:
|
||||
print(f"[Text Fitting] Calculating available area for {width}x{height} with {len(overlays)} overlays")
|
||||
|
||||
# Create a boolean mask for available areas (True = available, False = blocked)
|
||||
available_mask = [[True for _ in range(width)] for _ in range(height)]
|
||||
|
||||
# Mark overlay positions as unavailable
|
||||
for overlay in overlays:
|
||||
overlay_x = int(overlay.get('x', 0))
|
||||
overlay_y = int(overlay.get('y', 0))
|
||||
overlay_width = int(overlay.get('width', 0))
|
||||
overlay_height = int(overlay.get('height', 0))
|
||||
|
||||
# Add some padding around overlays
|
||||
padding = 10
|
||||
|
||||
start_x = max(0, overlay_x - padding)
|
||||
end_x = min(width, overlay_x + overlay_width + padding)
|
||||
start_y = max(0, overlay_y - padding)
|
||||
end_y = min(height, overlay_y + overlay_height + padding)
|
||||
|
||||
for y in range(start_y, end_y):
|
||||
for x in range(start_x, end_x):
|
||||
if 0 <= y < height and 0 <= x < width:
|
||||
available_mask[y][x] = False
|
||||
|
||||
# Find largest available rectangular area
|
||||
max_area = 0
|
||||
best_rect = (0, 0, 0, 0) # x, y, width, height
|
||||
|
||||
# Use dynamic programming approach to find largest rectangle
|
||||
heights = [0] * width
|
||||
|
||||
for row in range(height):
|
||||
# Update heights array
|
||||
for col in range(width):
|
||||
if available_mask[row][col]:
|
||||
heights[col] += 1
|
||||
else:
|
||||
heights[col] = 0
|
||||
|
||||
# Find largest rectangle in histogram
|
||||
rect = largest_rectangle_in_histogram(heights)
|
||||
area = rect[2] * rect[3] # width * height
|
||||
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
best_rect = (rect[0], row - rect[3] + 1, rect[2], rect[3])
|
||||
|
||||
print(f"[Text Fitting] Found best rectangle: x={best_rect[0]}, y={best_rect[1]}, w={best_rect[2]}, h={best_rect[3]}, area={max_area}")
|
||||
|
||||
return {
|
||||
'x': best_rect[0],
|
||||
'y': best_rect[1],
|
||||
'width': best_rect[2],
|
||||
'height': best_rect[3],
|
||||
'area': max_area
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error calculating available text area: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {'x': 0, 'y': 0, 'width': width, 'height': height, 'area': width * height}
|
||||
|
||||
def largest_rectangle_in_histogram(heights):
|
||||
"""Find the largest rectangle in a histogram using stack-based algorithm"""
|
||||
stack = []
|
||||
max_area = 0
|
||||
best_rect = (0, 0, 0, 0) # x, y, width, height
|
||||
|
||||
for i, h in enumerate(heights):
|
||||
start = i
|
||||
|
||||
while stack and stack[-1][1] > h:
|
||||
idx, height = stack.pop()
|
||||
area = height * (i - idx)
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
best_rect = (idx, 0, i - idx, height)
|
||||
start = idx
|
||||
|
||||
stack.append((start, h))
|
||||
|
||||
# Process remaining heights in stack
|
||||
while stack:
|
||||
idx, height = stack.pop()
|
||||
area = height * (len(heights) - idx)
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
best_rect = (idx, 0, len(heights) - idx, height)
|
||||
|
||||
return best_rect
|
||||
|
||||
def calculate_text_position(text, font, available_area, alignment='center'):
|
||||
"""Calculate optimal text position within available area"""
|
||||
try:
|
||||
from PIL import ImageFont, ImageDraw, Image
|
||||
|
||||
print(f"[Text Fitting] Calculating text position for alignment: {alignment}")
|
||||
|
||||
# Create temporary image to measure text
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
# Get text bounding box
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
area_x = available_area['x']
|
||||
area_y = available_area['y']
|
||||
area_width = available_area['width']
|
||||
area_height = available_area['height']
|
||||
|
||||
print(f"[Text Fitting] Text size: {text_width}x{text_height}, Available area: {area_width}x{area_height}")
|
||||
|
||||
# Calculate position based on alignment
|
||||
if alignment == 'center':
|
||||
x = area_x + (area_width - text_width) // 2
|
||||
y = area_y + (area_height - text_height) // 2
|
||||
elif alignment == 'top-left':
|
||||
x = area_x
|
||||
y = area_y
|
||||
elif alignment == 'top-center':
|
||||
x = area_x + (area_width - text_width) // 2
|
||||
y = area_y
|
||||
elif alignment == 'top-right':
|
||||
x = area_x + area_width - text_width
|
||||
y = area_y
|
||||
elif alignment == 'center-left':
|
||||
x = area_x
|
||||
y = area_y + (area_height - text_height) // 2
|
||||
elif alignment == 'center-right':
|
||||
x = area_x + area_width - text_width
|
||||
y = area_y + (area_height - text_height) // 2
|
||||
elif alignment == 'bottom-left':
|
||||
x = area_x
|
||||
y = area_y + area_height - text_height
|
||||
elif alignment == 'bottom-center':
|
||||
x = area_x + (area_width - text_width) // 2
|
||||
y = area_y + area_height - text_height
|
||||
elif alignment == 'bottom-right':
|
||||
x = area_x + area_width - text_width
|
||||
y = area_y + area_height - text_height
|
||||
else:
|
||||
# Default to center
|
||||
x = area_x + (area_width - text_width) // 2
|
||||
y = area_y + (area_height - text_height) // 2
|
||||
|
||||
# Ensure position is within bounds
|
||||
x = max(area_x, min(x, area_x + area_width - text_width))
|
||||
y = max(area_y, min(y, area_y + area_height - text_height))
|
||||
|
||||
print(f"[Text Fitting] Calculated position: ({x}, {y})")
|
||||
|
||||
return {
|
||||
'x': x,
|
||||
'y': y,
|
||||
'text_width': text_width,
|
||||
'text_height': text_height
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error calculating text position: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
'x': available_area['x'],
|
||||
'y': available_area['y'],
|
||||
'text_width': 0,
|
||||
'text_height': 0
|
||||
}
|
||||
|
||||
def find_optimal_font_size(text, font_path, max_width, max_height, min_size=8, max_size=200):
|
||||
"""Find the largest font size that fits within the given constraints"""
|
||||
try:
|
||||
from PIL import ImageFont, ImageDraw, Image
|
||||
|
||||
print(f"[Text Fitting] Finding optimal font size for constraints: {max_width}x{max_height}")
|
||||
|
||||
# Binary search for optimal font size
|
||||
low, high = min_size, max_size
|
||||
best_size = min_size
|
||||
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
|
||||
while low <= high:
|
||||
mid = (low + high) // 2
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, mid)
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
if text_width <= max_width and text_height <= max_height:
|
||||
best_size = mid
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid - 1
|
||||
|
||||
except Exception as font_error:
|
||||
print(f"[Text Fitting] Font size {mid} failed: {font_error}")
|
||||
high = mid - 1
|
||||
|
||||
print(f"[Text Fitting] Optimal font size: {best_size}")
|
||||
return best_size
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error finding optimal font size: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return min_size
|
||||
|
||||
def calculate_text_scaling(text, font, target_width, target_height):
|
||||
"""Calculate scaling factors to fit text within target dimensions"""
|
||||
try:
|
||||
from PIL import ImageDraw, Image
|
||||
|
||||
print(f"[Text Fitting] Calculating scaling for target: {target_width}x{target_height}")
|
||||
|
||||
# Measure current text size
|
||||
temp_img = Image.new('RGBA', (1, 1))
|
||||
draw = ImageDraw.Draw(temp_img)
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
current_width = bbox[2] - bbox[0]
|
||||
current_height = bbox[3] - bbox[1]
|
||||
|
||||
if current_width == 0 or current_height == 0:
|
||||
return 1.0, 1.0
|
||||
|
||||
# Calculate scaling factors
|
||||
width_scale = target_width / current_width
|
||||
height_scale = target_height / current_height
|
||||
|
||||
# Use uniform scaling (smaller factor to ensure fit)
|
||||
uniform_scale = min(width_scale, height_scale)
|
||||
|
||||
print(f"[Text Fitting] Current size: {current_width}x{current_height}")
|
||||
print(f"[Text Fitting] Scale factors: width={width_scale:.2f}, height={height_scale:.2f}, uniform={uniform_scale:.2f}")
|
||||
|
||||
return width_scale, height_scale, uniform_scale
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error calculating text scaling: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1.0, 1.0, 1.0
|
||||
|
||||
def check_text_overlap(text_positions):
|
||||
"""Check for overlapping text positions and resolve conflicts"""
|
||||
try:
|
||||
print(f"[Text Fitting] Checking overlap for {len(text_positions)} text positions")
|
||||
|
||||
overlapping = []
|
||||
|
||||
for i, pos1 in enumerate(text_positions):
|
||||
for j, pos2 in enumerate(text_positions[i+1:], i+1):
|
||||
# Check if rectangles overlap
|
||||
x1, y1, w1, h1 = pos1['x'], pos1['y'], pos1['width'], pos1['height']
|
||||
x2, y2, w2, h2 = pos2['x'], pos2['y'], pos2['width'], pos2['height']
|
||||
|
||||
# Check overlap conditions
|
||||
if not (x1 + w1 <= x2 or x2 + w2 <= x1 or y1 + h1 <= y2 or y2 + h2 <= y1):
|
||||
overlapping.append((i, j))
|
||||
print(f"[Text Fitting] Overlap detected between positions {i} and {j}")
|
||||
|
||||
return overlapping
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error checking text overlap: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
def resolve_text_conflicts(text_positions, canvas_width, canvas_height):
|
||||
"""Resolve overlapping text positions by repositioning"""
|
||||
try:
|
||||
print(f"[Text Fitting] Resolving text conflicts for {len(text_positions)} positions")
|
||||
|
||||
resolved_positions = text_positions.copy()
|
||||
overlaps = check_text_overlap(resolved_positions)
|
||||
|
||||
# Simple conflict resolution: move overlapping text
|
||||
for i, j in overlaps:
|
||||
pos1 = resolved_positions[i]
|
||||
pos2 = resolved_positions[j]
|
||||
|
||||
# Move the second text down or to the right
|
||||
new_y = pos1['y'] + pos1['height'] + 10
|
||||
if new_y + pos2['height'] <= canvas_height:
|
||||
resolved_positions[j]['y'] = new_y
|
||||
else:
|
||||
# Try moving to the right
|
||||
new_x = pos1['x'] + pos1['width'] + 10
|
||||
if new_x + pos2['width'] <= canvas_width:
|
||||
resolved_positions[j]['x'] = new_x
|
||||
else:
|
||||
# If can't fit, reduce to smaller area
|
||||
print(f"[Text Fitting] Could not resolve conflict for position {j}")
|
||||
|
||||
print(f"[Text Fitting] Resolved {len(overlaps)} conflicts")
|
||||
return resolved_positions
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Text Fitting] Error resolving text conflicts: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return text_positions
|
||||
Reference in New Issue
Block a user