fix(text): constant word space for ragged alignment, exact justification (S13)
LeftAlignmentHandler spread each line's residual space across its word gaps,
clamped to max_spacing. A line whose residual divided to under max_spacing was
stretched flush, one that exceeded it was not, so left-aligned text was
justified sometimes, by a different amount per line - which reads as a wobbling
right edge rather than as ragged-right. Centre/right did the same, and computed
their start position from a different spacing than the one they returned, so
centred lines were not centred.
Ragged alignments now use a constant word space - the font's own space advance,
clamped to the style's bounds - and report overflow instead of tightening, so
line breaking decides what fits rather than rendering squeezing it.
Justification kept two further defects:
- the final line of a paragraph was stretched across the measure, so a
three-word tail was spread edge to edge. Line now carries is_paragraph_end,
set on the line holding the last word, and renders flush left. A paragraph
continued on the next page is not marked, so it stays justified.
- gaps were floored per gap with a truncated remainder, discarding the
fractional part of both. Lines stopped one or two pixels short, differently
each time. Distributing by cumulative rounding makes the gaps sum to the
residual exactly; advance ends now land identically on every line.
Alignment is configurable rather than hardcoded: PageStyle.default_alignment,
defaulting to JUSTIFY for body text. text_align on abstract and concrete styles
defaults to None meaning "unspecified", so HTML without text-align inherits the
page default while explicit CSS still wins. Headings are never justified.
This commit is contained in:
@@ -8,7 +8,7 @@ from pyWebLayout.concrete.image import RenderableImage
|
||||
from pyWebLayout.concrete.functional import ButtonText, FormFieldText
|
||||
from pyWebLayout.concrete.table import TableRenderer, TableStyle
|
||||
from pyWebLayout.abstract import Paragraph, Word
|
||||
from pyWebLayout.abstract.block import Image as AbstractImage, PageBreak, Table
|
||||
from pyWebLayout.abstract.block import Image as AbstractImage, Heading, PageBreak, Table
|
||||
from pyWebLayout.abstract.functional import Button, Form, FormField
|
||||
from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
|
||||
from pyWebLayout.style import Font, Alignment
|
||||
@@ -51,6 +51,15 @@ def paragraph_layouter(paragraph: Paragraph,
|
||||
# We need to get word spacing constraints from the Font's abstract style if available
|
||||
# For now, use reasonable defaults based on font size
|
||||
|
||||
# Alignment for text that does not specify its own. Headings are never
|
||||
# justified - stretching a two-word title across the measure is always wrong -
|
||||
# so they fall back to flush left.
|
||||
default_alignment = getattr(page.style, 'default_alignment', None)
|
||||
if not isinstance(default_alignment, Alignment):
|
||||
default_alignment = Alignment.JUSTIFY
|
||||
if isinstance(paragraph, Heading):
|
||||
default_alignment = Alignment.LEFT
|
||||
|
||||
if isinstance(paragraph.style, Font):
|
||||
# paragraph.style is already a Font (concrete style)
|
||||
font = paragraph.style
|
||||
@@ -59,7 +68,7 @@ def paragraph_layouter(paragraph: Paragraph,
|
||||
min_spacing = float(font.font_size) * 0.25 # 25% of font size
|
||||
max_spacing = float(font.font_size) * 0.5 # 50% of font size
|
||||
word_spacing_constraints = (int(min_spacing), int(max_spacing))
|
||||
text_align = Alignment.LEFT # Default alignment
|
||||
text_align = default_alignment
|
||||
else:
|
||||
# paragraph.style is an AbstractStyle, resolve it
|
||||
# Ensure font_size is an int (it could be a FontSize enum)
|
||||
@@ -79,7 +88,8 @@ def paragraph_layouter(paragraph: Paragraph,
|
||||
int(concrete_style.word_spacing_min),
|
||||
int(concrete_style.word_spacing_max)
|
||||
)
|
||||
text_align = concrete_style.text_align
|
||||
# text_align is None when the source did not specify one.
|
||||
text_align = concrete_style.text_align or default_alignment
|
||||
|
||||
# Apply page-level word spacing override if specified
|
||||
if hasattr(
|
||||
@@ -260,7 +270,13 @@ def paragraph_layouter(paragraph: Paragraph,
|
||||
else:
|
||||
current_pretext = overflow_text # May be None or hyphenated remainder
|
||||
|
||||
# All words processed successfully
|
||||
# All words processed successfully. The line holding the final word is the
|
||||
# end of the paragraph, so it is rendered at its natural width rather than
|
||||
# justified to the full column. A paragraph continued on the next page does
|
||||
# not reach here, so its lines stay justified - which is correct.
|
||||
if current_line is not None:
|
||||
current_line.is_paragraph_end = True
|
||||
|
||||
return True, None, None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user