Fonts and themes¶
Why fonts matter more here than usual¶
Justified text places each word individually, at coordinates svg-plus computes. If the width it computed is off by a percent, the line reaches the margin a percent early or late, and every gap in it is wrong. Measurement is not a detail of this library; it is the thing the library rests on.
There are two sources of metrics.
reportlab's tables for the standard PDF families — Helvetica, Times, Courier. No font file needed, works on any machine including CI. Approximate: they can run several percent away from the face a browser resolves for font-family: Helvetica, which shows up as loose justification.
Loading a family¶
from svg_plus import Theme, find_font, load_font
FAMILY = "Source Sans Pro, Helvetica Neue, Helvetica, sans-serif"
THEME = Theme(
body=load_font(find_font("SourceSansPro-Regular"), family=FAMILY),
bold=load_font(find_font("SourceSansPro-Bold"), family=FAMILY, weight=700),
italic=load_font(find_font("SourceSansPro-It"), family=FAMILY, italic=True),
)
find_font takes a filename stem and searches the usual directories on macOS, Linux and Windows, raising FileNotFoundError with the list it looked in. family= is the string written into the SVG, so you can measure with one specific file and still name a whole fallback stack for the renderer.
Fall back gracefully when the font may be missing:
def theme() -> Theme:
try:
regular = find_font("SourceSansPro-Regular")
except FileNotFoundError:
return Theme() # built-in metrics; approximate but portable
return Theme(body=load_font(regular, family=FAMILY), ...)
Collections¶
Helvetica Neue and Menlo ship several faces in one .ttc. index= picks one:
neue = find_font("HelveticaNeue")
load_font(neue, weight=700, index=1) # 0 regular, 1 bold, 2 italic
A face the theme does not carry¶
A theme has three faces. When you need a fourth — a monospace terminal block, a display face for one headline — pass it directly:
MONO = load_font(find_font("Menlo"), family="Menlo, monospace")
Text("# apt install postgresql", size=14.0, face=MONO, fill="#ffffff")
face overrides the theme entirely; weight and italic then only describe what that face already is, for the SVG attributes.
No bold italic
A theme carries body, bold and italic. Ask for both bold and italic and bold wins. If you need a bold-italic face, pass it with face=.
The theme¶
Theme(
body=…, bold=…, italic=…, # the three faces
size=11.0, # default text size
leading=1.38, # multiple of size
ink="#1a1a1a", # body text
muted="#5f5f5f", # secondary text
accent="#1f4e5f", # titles, emphasis
surface="#f4f3f0", # panel grounds
rule="#e0ded9", # borders and hairlines
page="#ffffff", # the canvas itself
radius=5.0, pad=14.0, gap=12.0,
)
The theme is frozen. To vary one thing, build a new one — dataclasses.replace(THEME, page="#0b1020") works.
Naming colours¶
Blocks take colour names, not spellings:
Frame(child, fill="surface", stroke="rule") # palette roles
Text("locked", fill="red") # status colours
Frame(child, fill="#f4e0dc") # a literal, when you mean it
Theme.color() resolves in this order: a # literal or "none" passes through; then the six palette roles; then the five status colours red, amber, green, blue, grey. Anything else raises KeyError — a typo is a crash, not a silently grey box.
This is what makes a figure restylable. examples/hop3/ defines a light theme and a dark one with the same role names, and the posters are written once.
Status colours and their tints¶
The status colours are meant as redundant reinforcement of what the text already says — a figure has to survive being printed in black and white. For the pale ground that goes with a status stroke, tint() mixes toward white:
A worked example¶
from dataclasses import replace
from svg_plus import DEFAULT, Doc, Text, card
DAY = DEFAULT
NIGHT = replace(
DEFAULT,
ink="#ffffff", muted="#8b94a7", accent="#7c8dff",
surface="#151d33", rule="#232b40", page="#0b1020",
)
def build(theme):
return Doc(600, theme=theme).add(
card("Networks", "Fibre, mobile and exchange points.", accent="green"),
Text("Colours are named, so this tree does not know which theme it is in."),
)
build(DAY).save("light.svg")
build(NIGHT).save("dark.svg")
The same block tree, two themes, two figures. Nothing in the tree mentions a colour: