Adsditional fix
This commit is contained in:
+125
-40
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user