Skip to content

Setting text

Text

Text(
    "…",
    size=11.0,        # inherits theme.size when None
    weight=400,       # 600 and above picks the bold face
    italic=False,
    fill="ink",       # a theme name or a #rrggbb literal
    align="left",     # left | center | right | justify
    leading=None,     # absolute line height; theme.leading * size when None
    tracking=0.0,     # letter-spacing, in units
    wrap=True,
    hyphenate=None,
    face=None,        # an explicit Font, overriding the theme
)

The block's height is len(lines) * leading, and the lines come out of the breaker, so a Text is exactly as tall as its content at the width it was given.

Alignment, and what it changes

align decides more than the anchor.

  • justify places each word individually so the line reaches the measure exactly. Non-final lines are set to the full width; the last line keeps natural spacing.
  • Everything else sets the line as one run, anchored left, centre or right.

It also changes how the paragraph is broken. Justified text may shrink its spaces slightly to pull a word up; ragged text may not, because there is nothing to shrink — so a ragged line is never broken as though it could squeeze. Get this wrong and lines come out a fraction past the measure; svg-plus handles it for you off the align you asked for.

Why justified lines are one <text> per word

SVG has textLength and word-spacing, and svglib supports neither — so a PDF or PNG export would lose the justification. Placing each word means the output is exact in every renderer, and since each word is its own run there is no cross-word kerning to disagree with our measurement.

Emphasis inside a paragraph

A Text is normally one style throughout. Pass a sequence instead, and mark the pieces that differ with a Span:

from svg_plus import Span, Text

Text([
    "The market grew ",
    Span("sixfold", weight=700),
    ", while suppliers grew only ",
    Span("threefold", weight=700, fill="red"),
    ".",
], align="justify")

A span says only what it changes — size, weight, italic, fill, tracking or an explicit face; everything left as None is inherited from the paragraph.

Each piece is measured in its own style, so justification stays exact. Two details worth knowing:

  • A run boundary need not be a word boundary. Span("sixfold", weight=700) followed by "." produces two pieces of one word with nothing between them — the full stop stays welded to the bold word, and the line still breaks as though it were one word.
  • Ordinary prose is unaffected. A Text with a plain string is still emitted as a single <text> element; emphasis does not fragment text that has none.

line.uniform tells you whether a line came out in one style, and line.styles gives the style of each piece.

Hyphenation and spans

A word already cut by a change of style is not hyphenated as well. In practice a hyphenation point and an emphasis boundary rarely land in the same word.

Line breaking

break_paragraph implements the Knuth-Plass total-fit algorithm: the paragraph becomes a stream of boxes (words), glue (spaces that can stretch and shrink) and penalties (optional hyphens, forced breaks), and every possible set of breakpoints is scored. The cheapest wins.

Greedy breaking fills each line to the brim and pays for it on the next. Total fit accepts a slightly fuller line here to avoid a badly stretched one there. In a figure — where columns are narrow — the difference is large:

Measure Greedy Total fit
145 units 11.1 4.6
165 units 17.4 6.9
195 units 18.3 3.6

(Extra space the loosest line has to invent, per gap. Lower is tighter.)

It also stops a paragraph stranding one word alone on the last line, without the "shrink the measure until it looks right" hack that greedy breaking needs.

The second pass

In a narrow column, often no breaking fits within tolerance — every candidate line is either too loose or too tight. TeX runs a second pass rather than let a line run past the measure, and so does svg-plus: it re-breaks accepting any amount of stretch. A loose line is always better than text spilling out of its box.

You will hit this constantly at card width. It is handled; there is nothing to configure.

Using the breaker directly

from svg_plus import DEFAULT, break_paragraph

for line in break_paragraph(text, DEFAULT.style(11.0), measure=260.0):
    line.words           # ('The', 'market', 'grew')
    line.gaps            # adjusted space between them
    line.natural_width   # width at natural spacing
    line.justified_width # width with the gaps applied
    line.last            # is this the final line?

Not wrapping

wrap=False sets the string as one line, whatever its width. Use it for headings, labels and anything whose spacing is meaningful — the string is kept verbatim, so a list that lines up on two spaces still lines up when drawn.

Text("1.  Install PHP and its extensions", size=15.0, wrap=False)

There is no truncation and no ellipsis: an over-long unwrapped line simply runs on, which is loud enough to notice.

Leading and tracking

leading is the absolute distance between baselines. Left as None it is theme.leading * size. Text sits vertically centred in its line box, with half the leading above and half below, so a block of text is optically centred in whatever space it was given.

tracking adds letter-spacing. It is accounted for in measurement — including in the space width, so a tracked line still justifies exactly. It is what makes the small tracked-out capitals of eyebrow() come out right.

Hyphenation

Hyphenation is a hook, not a bundled dictionary. Pass any callable that splits a word into pieces:

import pyphen

splitter = pyphen.Pyphen(lang="fr_FR")

Text(
    body,
    align="justify",
    hyphenate=lambda word: splitter.inserted(word).split("-"),
)

The breaker inserts a flagged penalty between the pieces, charges for breaking there, and charges again when two consecutive lines both end on a hyphen — the same rules TeX uses. Without it, narrow French columns stretch badly; with it they close up.

Non-breaking spaces

The breaker splits on ordinary spaces and tabs only. U+00A0 and U+202F stay inside the word, which is how French typography keeps a guillemet or a percent sign attached to what it belongs to:

"« Nos serveurs sont en Europe »"            # ordinary spaces: » can fall alone
\u202fNos serveurs sont en Europe\u202f»"  # narrow no-break: stays attached

U+202F is the character French typography wants; U+00A0 is the safer choice when the font lacks it.