Fonts with per-component overrides
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 48/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- python
- Domain
- documentation, tooling
Research direction
Begin by locating the style defaults, core slide builders, rich-layout text paths, the README style-keys table, and the existing style-defaults key-set test named in the issue. Run that test before changes, then trace the current font-size assignments and rich-layout font handling. Done means the specified family resolution works across components, existing output is unchanged without font keys, tests pass, and the font-availability limitations are documented.
Written by the indexing model from the issue text.
Description
Fonts with per-component overrides
Summary
Today an author can change font sizes but not the typeface — the font family is
either inherited from the blank template (core slides) or hardcoded to Segoe UI
(rich layouts). This enhancement adds a global font_family plus per-component
overrides (title_font_family, body_font_family, …) to the front-matter style:
block, and optional per-component weight/style flags. Builders set each run's font name
alongside the size they already set. It also documents — honestly — the PowerPoint
font-availability limitation (python-pptx cannot embed fonts) and recommends a
safe-fallback strategy. With no font keys set, every existing deck renders identically.
Value & Motivation
Typography is core to brand identity, yet ms-presentations gives authors no control
over the typeface at all. The style block exposes only sizes — every value is a point
size, a colour, or a number; there is no font-family field. Concretely:
- Core slides set only the font size, never the typeface name, so they inherit
whatever font the blank template ships (Calibri). - The rich (16:9) layouts hardcode the family to
Segoe UI.
The result: a company whose brand font is "Poppins for headings, Inter for body" cannot
express that, and a deck mixes Calibri (core slides) with Segoe UI (rich slides) with no
way to unify them. Authors already tune sizes through style:; the natural, expected
next step is to tune the typeface, globally and per component.
Proposed Solution / Detailed Design
Add font-family resolution to the existing style object with a clean
global → per-component → leave-untouched fallback, and have every slide apply a run's
font name wherever it already applies a font size. Parameterise the rich layouts'
hardcoded Segoe UI through the same style object.
Spec syntax
Nested under the existing front-matter style: block:
---
title: Brand Deck
style:
font_family: Inter # global default for every component
title_font_family: Poppins # per-component overrides
heading_font_family: Poppins
body_font_family: Inter
rich_font_family: Inter # text drawn by the 16:9 rich layouts
# weight / style (optional)
title_font_bold: true
subtitle_font_italic: true
---
Per-component family keys (each falls back to font_family, then to "untouched"):
| Key | Component(s) | Today |
|---|---|---|
font_family |
global default for all below | none |
title_font_family |
title-slide & section-header title | Calibri (template) |
heading_font_family |
content / two-column title bar | Calibri (template) |
subtitle_font_family |
subtitle text | Calibri (template) |
body_font_family |
content bullets | Calibri (template) |
column_heading_font_family † |
two-column per-column heading | n/a — not currently rendered |
column_body_font_family |
two-column bullets | Calibri (template) |
name_font_family |
resource-box name | Calibri (template) |
url_font_family |
resource-box URL | Calibri (template) |
badge_font_family |
resource-box badge | Calibri (template) |
rich_font_family |
all rich-layout text | Segoe UI (hardcoded) |
† The two-column layout does not currently render a separate per-column heading — the
existing column_heading_font_size key is defined but unused by any builder.
column_heading_font_family is included only for parity with that key and would take
effect if/when such a heading is rendered; it is otherwise a no-op today.
Optional weight/style flags follow the same per-component naming:
*_font_bold (bool), *_font_italic (bool).
Per-component font overrides (e.g. **TitleFont**: Poppins) at the per-slide level are
intentionally out of scope for v1 and tracked in Open Questions.
Behaviour & resolution
For each component, the resolved family is:
- the component key (e.g.
title_font_family) if non-empty, else - the global
font_familyif non-empty, else - empty → do not set a font name.
Step 3 is what preserves today's look: when no font keys are set, slides never assign a
name, so core slides keep the template typeface and rich layouts keep Segoe UI via the
existing literal fallback. Resolution happens once when the style is built, yielding a
resolved family per component (a string, possibly empty).
Weight/style flags resolve the same way and map onto the run's bold/italic, which the
rich layouts already apply per run.
Implementation outline
Described by area of change rather than by source location, since exact files and
functions are an implementation-PR concern.
| Area | Change |
|---|---|
| Style defaults | Add the *_font_family keys (default empty) and a global font_family; resolve each component's family with the global → component → empty fallback. Add *_font_bold / *_font_italic if weight/style is in scope. |
| Core slide types | At each point that sets a font size, also set the font name when the resolved family for that component is non-empty. A single small helper keeps that conditional in one place; the line-break text helper gains a family parameter. |
| Rich layouts | Replace the hardcoded Segoe UI literals with the resolved rich_font_family (threaded in from the style object the rich builders already receive), keeping Segoe UI only as the final fallback default. |
| Documentation | Add the new *_font_family rows to the README style-keys table and a clear note about the font-availability limitation and recommended fallbacks. |
python-pptx mechanism
Setting a typeface is straightforward — assign the run's font name:
run.font.name = "Inter" # or paragraph.font.name for placeholder paragraphs
This writes an <a:latin typeface="Inter"/> into the run/paragraph rPr. The
rich-layout text path already does exactly this; the core slides simply don't, yet. For
placeholder-based core slides the same applies to the paragraph's font.
The honest limitation — python-pptx cannot embed fonts. Setting a font name only
writes the name into the file; it does not bundle the font. If the viewing
machine lacks that typeface, PowerPoint substitutes another and the deck looks wrong.
PowerPoint's own "Embed fonts in the file" feature writes <p:embeddedFontLst> font
parts into the package — python-pptx exposes no API for this, and doing it by
hand means adding binary font parts plus relationships, which is brittle and raises font
licensing concerns. Therefore:
- v1 sets font names only. Embedding is explicitly out of scope.
- Recommend a safe-fallback bias toward widely-available fonts: Arial is the
safest truly cross-platform choice; Calibri ships with Microsoft Office on both
Windows and macOS; Segoe UI is reliable only on Windows/Office — it is not a
macOS system font, which is why the rich layouts' current Segoe UI default already
substitutes on a Mac without Office installed. Document that any custom or web/Google
font (Inter, Poppins, …) must be installed on every viewing machine, or the author
must run PowerPoint's File → Options → Save → Embed fonts on the generated deck. - "Weight" is not a numeric axis. OOXML/PowerPoint express weight via the bold
toggle or via a weight-named family (e.g.Inter SemiBold), not a CSS-style numeric
weight. The doc and README should say so to avoid surprise.
Google / web font sourcing guidance
The README note should give authors a short recipe for non-system fonts:
- Download the family (e.g. from Google Fonts) and install it locally before
building, so any preview on the author's machine renders correctly. - Reference it by its exact installed family name in
style:
(body_font_family: Inter). - Ensure viewers also have it installed, or embed fonts via PowerPoint after
generation (and confirm the font's licence permits embedding). - When in doubt, choose a near-equivalent system font to minimise substitution drift.
Rationale
Extending the existing style object — rather than inventing a new mechanism — is the
right call for three reasons that map to the motivation:
- Authors already think in
style:. They tune*_font_sizethere today; adding
*_font_familybeside it is the smallest possible conceptual step and immediately
discoverable. The global→component fallback mirrors how CSS-minded users expect
cascades to work. - It unifies the split typography in one move. Routing both the core slides and the
rich layouts' hardcodedSegoe UIthrough the same style field is what lets a single
font_family: Intermake the whole deck consistent — the exact pain today. - Honesty over magic. The embedding limitation is real and unavoidable in
python-pptx. Documenting it plainly (names-only + fallback strategy) is more
valuable than a fragile, license-risky embedding hack that would surprise users when
it breaks.
Alternatives considered
- OOXML font embedding (
<p:embeddedFontLst>) — rejected for v1. Nopython-pptx
support, binary part wrangling, and font-licensing risk. Listed as possible future
work. - Theme-level fonts (
<a:fontScheme>major/minor) on the slide master — deferred
as a Phase-2 complement. Cleaner for blanket placeholder text and a single global
typeface, but doesn't give per-component control and doesn't reach the rich layouts'
explicitly-built runs. Could later back the globalfont_familywhile per-component
keys stay run-level. - Per-run only, no style-object change — rejected. Gives no single global knob and
forces every slide to grow its own font argument plumbing. - A separate
fonts:top-level block — rejected. Splits styling across two places;
style:already owns typography (sizes), so families belong there too.
Impact on Existing Product
- Backwards compatibility. Fully additive. When every family key is empty, slides
never set a font name, so core slides keep the template typeface and rich layouts keep
Segoe UI(via the literal fallback). Existing decks render identically. - Migration. None required; opt-in via new keys.
- Interaction with existing size keys. Families are orthogonal to the existing
*_font_sizekeys; size behaviour is unchanged. - Performance. Negligible — a few extra attribute assignments per run; no I/O.
- Documentation. The README style-keys table grows; add a "Fonts & availability"
callout covering the embedding limitation, safe fallbacks, and the weight caveat. - Tests. Adding keys to the style defaults will break the existing style-defaults
test that asserts the exact set of known style keys; its expected-key set must be
updated in the same change.
Risks & Mitigations
| Risk | Likelihood / Impact | Mitigation |
|---|---|---|
| Chosen font absent on viewer machine → substitution | High / High | Document plainly; bias defaults to cross-platform fonts; advise install-or-embed; manual verification step. |
| Authors expect numeric font weights | Med / Low | Document that weight = bold flag or weight-named family, not a numeric axis. |
| Forgetting to update the style-defaults key-set test | Med / High (red CI) | Flagged in Impact → Tests and Acceptance Criteria. |
| Per-component plumbing touches many call sites | Med / Med | Centralise in one font-applying helper; cover with the no-font regression test. |
| Font licence forbids embedding | Low / Med | v1 doesn't embed; Phase-2 embedding gated on licence guidance. |
Open Questions
- Should v1 include the weight/style (
*_font_bold/*_font_italic) flags, or ship
family-only first and add weight/style in a follow-up? - Should the global
font_familybe backed by a theme<a:fontScheme>now (cleaner for
placeholders) or kept purely run-level for v1? - Do we want experimental font embedding at all, given the licensing exposure?
- Per-slide font overrides: ship alongside the 0001
slide-level override generalisation, or defer entirely?
- Dominant language
- Python
- Stars
- 11
- Forks
- 3
- PR merge metrics
- No merged PRs in 30d
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/presentations
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
microsoft/presentations#12 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
microsoft/presentations#11 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
microsoft/presentations#10 ·
-
Difficulty 2/5 Half a day Newbie friendliness 82/100
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
microsoft/presentations#17 · 1 comment ·
All issues in microsoft/presentations
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
mikf/gallery-dl#9791 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
fossasia/eventyay#6151 · 1 comment ·
Maintainers usually reply within 1 day
-
P4: low tooling
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
jeffknupp/association#318 ·
-
azure-cost bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
microsoft/GitHub-Copilot-for-Azure#3330 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
raullenchai/Rapid-MLX#4097 ·
Maintainers usually reply within 1 day