Adsditional fix

This commit is contained in:
2025-06-08 15:11:35 +02:00
parent 8d892bfe28
commit 57d1b5f75c
9 changed files with 1559 additions and 905 deletions
+125 -40
View File
@@ -36,7 +36,7 @@ class AlignmentHandler(ABC):
@abstractmethod
def should_try_hyphenation(self, text_objects: List['Text'], word_width: int,
available_width: int, spacing: int) -> bool:
available_width: int, spacing: int, font: 'Font') -> bool:
"""
Determine if hyphenation should be attempted for better spacing.
@@ -45,6 +45,7 @@ class AlignmentHandler(ABC):
word_width: Width of the word trying to be added
available_width: Available width remaining
spacing: Current minimum spacing being used
font: Font object containing hyphenation settings
Returns:
True if hyphenation should be attempted
@@ -62,9 +63,11 @@ class LeftAlignmentHandler(AlignmentHandler):
return min_spacing, 0
def should_try_hyphenation(self, text_objects: List['Text'], word_width: int,
available_width: int, spacing: int) -> bool:
"""For left alignment, hyphenate only if the word doesn't fit."""
return word_width > available_width
available_width: int, spacing: int, font: 'Font') -> bool:
"""For left alignment, hyphenate only if the word doesn't fit and there's reasonable space."""
# Only hyphenate if word doesn't fit AND we have reasonable space for hyphenation
# Don't hyphenate in extremely narrow spaces where it won't be meaningful
return word_width > available_width and available_width >= font.min_hyphenation_width
class CenterRightAlignmentHandler(AlignmentHandler):
@@ -92,9 +95,9 @@ class CenterRightAlignmentHandler(AlignmentHandler):
return spacing, max(0, x_pos)
def should_try_hyphenation(self, text_objects: List['Text'], word_width: int,
available_width: int, spacing: int) -> bool:
"""For center/right alignment, hyphenate only if the word doesn't fit."""
return word_width > available_width
available_width: int, spacing: int, font: 'Font') -> bool:
"""For center/right alignment, hyphenate only if the word doesn't fit and there's reasonable space."""
return word_width > available_width and available_width >= font.min_hyphenation_width
class JustifyAlignmentHandler(AlignmentHandler):
@@ -122,13 +125,14 @@ class JustifyAlignmentHandler(AlignmentHandler):
return spacing, 0
def should_try_hyphenation(self, text_objects: List['Text'], word_width: int,
available_width: int, spacing: int) -> bool:
available_width: int, spacing: int, font: 'Font') -> bool:
"""
For justified text, consider hyphenation if it would improve spacing quality.
This includes cases where the word fits but would create poor spacing.
"""
if word_width > available_width:
return True
# Only hyphenate if we have reasonable space for hyphenation
return available_width >= font.min_hyphenation_width
# Calculate what the spacing would be with this word added
if not text_objects:
@@ -140,9 +144,12 @@ class JustifyAlignmentHandler(AlignmentHandler):
if num_spaces > 0:
projected_spacing = available_space // num_spaces
# If spacing would be too large, consider hyphenation for better distribution
max_acceptable_spacing = spacing * 2 # Allow up to 2x normal spacing
return projected_spacing > max_acceptable_spacing
# Be more conservative about hyphenation - only suggest it if spacing would be very large
# Use a higher threshold to avoid unnecessary hyphenation
max_acceptable_spacing = spacing * 3 # Allow up to 3x normal spacing before hyphenating
# Also ensure we have a minimum threshold to avoid hyphenating for tiny improvements
min_threshold_for_hyphenation = spacing + 10 # At least 10 pixels above min spacing
return projected_spacing > max(max_acceptable_spacing, min_threshold_for_hyphenation)
return False
@@ -395,6 +402,45 @@ class Line(Box):
"""Set the next line in sequence"""
self._next = line
def _try_reduced_spacing_fit(self, text: str, font: Font, word_width: int, safety_margin: int) -> Union[None, str]:
"""
Try to fit the word by reducing spacing between existing words.
Args:
text: The text to fit
font: The font to use
word_width: Width of the word
safety_margin: Safety margin for fitting
Returns:
None if the word fits with reduced spacing, or the text if it doesn't
"""
if not self._text_objects:
return text # No existing words to reduce spacing between
min_spacing, max_spacing = self._spacing
# Calculate minimum possible spacing (could be even less than min_spacing for edge cases)
emergency_spacing = max(1, min_spacing // 2) # At least 1 pixel spacing
# Calculate current used width without spacing
total_text_width = sum(obj.width for obj in self._text_objects) + word_width
# Calculate available space for spacing
available_space_for_spacing = self._size[0] - total_text_width - safety_margin
num_spaces_needed = len(self._text_objects) # Will be this many spaces after adding the word
if num_spaces_needed > 0 and available_space_for_spacing >= emergency_spacing * num_spaces_needed:
# We can fit the word with reduced spacing
text_obj = Text(text, font)
text_obj.add_to_line(self)
self._text_objects.append(text_obj)
# Update current width calculation (spacing will be calculated during render)
self._current_width = total_text_width + (emergency_spacing * num_spaces_needed)
return None
return text # Can't fit even with minimal spacing
def _force_fit_long_word(self, text: str, font: Font, max_width: int) -> Union[None, str]:
"""
Force-fit a long word by breaking it at character boundaries if necessary.
@@ -444,7 +490,14 @@ class Line(Box):
def add_word(self, text: str, font: Optional[Font] = None) -> Union[None, str]:
"""
Add a word to this line as a Text object using intelligent hyphenation decisions.
Add a word to this line as a Text object using intelligent word fitting strategies.
This method implements a comprehensive word fitting algorithm that:
1. First tries to fit the word with normal spacing
2. If that fails, tries reducing spacing to minimize gaps
3. Uses hyphenation when beneficial for spacing quality
4. Falls back to moving the word to the next line
5. As a last resort, force-fits long words
Args:
text: The text content of the word
@@ -470,27 +523,49 @@ class Line(Box):
# Check if word fits in the line with safety margin
available_width = self._size[0] - self._current_width - spacing_needed - safety_margin
# Use the alignment handler to decide if hyphenation should be attempted
should_hyphenate = self._alignment_handler.should_try_hyphenation(
self._text_objects, word_width, available_width, min_spacing)
if word_width <= available_width and not should_hyphenate:
# Word fits and alignment handler doesn't suggest hyphenation - add it to the line
text_obj.add_to_line(self)
self._text_objects.append(text_obj)
self._current_width += spacing_needed + word_width
return None
elif should_hyphenate:
# Try hyphenation for better spacing
return self._try_hyphenation_or_fit(text, font, available_width, spacing_needed, safety_margin)
else:
# Word doesn't fit and no hyphenation recommended
if self._text_objects:
# Line already has words, can't fit this one at all
return text
# Strategy 1: Try to fit with normal spacing
if word_width <= available_width:
# Check if alignment handler suggests hyphenation for better spacing quality
should_hyphenate = self._alignment_handler.should_try_hyphenation(
self._text_objects, word_width, available_width, min_spacing, font)
if not should_hyphenate:
# Word fits with normal spacing and no hyphenation needed - add it
text_obj.add_to_line(self)
self._text_objects.append(text_obj)
self._current_width += spacing_needed + word_width
return None
else:
# Empty line with word that's too long - force fit
return self._force_fit_long_word(text, font, available_width + safety_margin)
# Word fits but hyphenation might improve spacing - try it
hyphen_result = self._try_hyphenation_or_fit(text, font, available_width, spacing_needed, safety_margin)
if hyphen_result is None:
# Hyphenation worked and improved spacing
return None
# If hyphenation didn't work or didn't improve things, fall through to add the whole word
text_obj.add_to_line(self)
self._text_objects.append(text_obj)
self._current_width += spacing_needed + word_width
return None
# Strategy 2: Try reducing spacing to maximize fit
if self._text_objects and word_width > available_width:
reduced_spacing_result = self._try_reduced_spacing_fit(text, font, word_width, safety_margin)
if reduced_spacing_result is None:
# Word fitted by reducing spacing
return None
# Strategy 3: Try hyphenation to fit part of the word
hyphen_result = self._try_hyphenation_or_fit(text, font, available_width, spacing_needed, safety_margin)
if hyphen_result != text: # Some progress was made with hyphenation
return hyphen_result
# Strategy 4: Word doesn't fit and no hyphenation helped
if self._text_objects:
# Line already has words, move this word to the next line
return text
else:
# Empty line with word that's too long - force fit as last resort
return self._force_fit_long_word(text, font, available_width + safety_margin)
def _try_hyphenation_or_fit(self, text: str, font: Font, available_width: int,
spacing_needed: int, safety_margin: int) -> Union[None, str]:
@@ -507,6 +582,20 @@ class Line(Box):
Returns:
None if the word fits, or remaining text if it doesn't fit
"""
# First check if the alignment handler recommends hyphenation
text_obj = Text(text, font)
word_width = text_obj.width
should_hyphenate = self._alignment_handler.should_try_hyphenation(
self._text_objects, word_width, available_width, self._spacing[0], font)
if not should_hyphenate:
# Alignment handler doesn't recommend hyphenation
if self._text_objects:
return text # Line already has words, return the word
else:
# Empty line with word that's too long - force fit as last resort
return self._force_fit_long_word(text, font, available_width + safety_margin)
abstract_word = Word(text, font)
if abstract_word.hyphenate():
@@ -547,18 +636,14 @@ class Line(Box):
else:
return None
else:
# No hyphenation part fits
if self._text_objects:
return text # Line already has words, can't fit this one
else:
# Empty line - must fit something
return self._force_fit_long_word(text, font, available_width + safety_margin)
# No hyphenation part fits - return the original word
return text
else:
# Word cannot be hyphenated
if self._text_objects:
return text # Line already has words, can't fit this unhyphenatable word
else:
# Empty line with unhyphenatable word that's too long
# Empty line with unhyphenatable word that's too long - force fit as last resort
return self._force_fit_long_word(text, font, available_width + safety_margin)
def render(self) -> Image.Image:
+10 -1
View File
@@ -34,7 +34,8 @@ class Font:
style: FontStyle = FontStyle.NORMAL,
decoration: TextDecoration = TextDecoration.NONE,
background: Optional[Tuple[int, int, int, int]] = None,
language = "en_EN"):
language = "en_EN",
min_hyphenation_width: Optional[int] = None):
"""
Initialize a Font object with the specified properties.
@@ -47,6 +48,8 @@ class Font:
decoration: Text decoration (none, underline, or strikethrough).
background: RGBA background color for the text. If None, transparent background.
language: Language code for hyphenation and text processing.
min_hyphenation_width: Minimum width in pixels required for hyphenation to be considered.
If None, defaults to 4 times the font size.
"""
self._font_path = font_path
self._font_size = font_size
@@ -56,6 +59,7 @@ class Font:
self._decoration = decoration
self._background = background if background else (255, 255, 255, 0)
self.language = language
self._min_hyphenation_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4
# Load the font file or use default
self._load_font()
@@ -141,6 +145,11 @@ class Font:
"""Get the text decoration"""
return self._decoration
@property
def min_hyphenation_width(self):
"""Get the minimum width required for hyphenation to be considered"""
return self._min_hyphenation_width
def with_size(self, size: int):
"""Create a new Font object with modified size"""
return Font(