* Text: a missing font is drawn in Calibri, as PowerPoint does
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fonts: Windows family names find their faces; PowerPoint's twins for missing fonts
"Segoe UI Semibold", "Arial Black" and the like are the faces' Windows (GDI) family
names; a family's installed files now load together; Liberation Sans and Serif draw as
Arial and Times New Roman when missing, as PowerPoint draws them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fonts: Office cloud fonts (Aptos…) keep their open substitute
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Text warps: draw WordArt prstTxWarp presets, round-trip their adjusts
All 40 DrawingML text-warp presets now render instead of drawing flat text.
A warp is a list of bands over the shape's text rectangle (insets ignored):
fill bands stretch the ink of their lines between a top and a bottom guide
(positions by arc length, one horizontal scale for the whole text), follow
bands (arch, circle, button) set lines at natural size along an ellipse arc,
lines stacking outwards on parallel curves, with no wrapping. Lines are
shared over multi-band presets in order (ceil(n/k) per band).
The guide curves were fitted to PowerPoint renders of synthetic decks of
"I" bars (black-box observation, defaults and changed adjusts, 1-3 lines);
most presets match within about a point on a 420 x 200 pt box.
- geom: warp module (guides, presets, band mapping) with unit tests
- render: glyph outlines and underlines flattened, subdivided and mapped
- text: follow warps lay out without wrapping
- model/pptx: BodyProps.warp_adj read from and written to prstTxWarp/avLst
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Text warps: bands with fewer lines keep the fuller band's text height
Multi-band fill warps (Deflate-Inflate, Button Pour) map a box as tall as
the tallest band's ink onto every band, centred on the band's own ink, as
PowerPoint does: a band holding one line next to a band of two keeps the
line at the same size instead of stretching it over the whole band.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PowerPoint keeps fi, ffi and Calibri's ti apart and kerns a run only when
a:rPr/@kern is set and the size reaches it; DeckCraft shaped with the font's
defaults (ligatures and kerning always on), so Calibri text read differently
and kern="0" text came out narrower.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bullets in Wingdings, Wingdings 2/3, Symbol and other symbol fonts were
always swapped for a look-alike from one Wingdings table, so a Wingdings 2
star (0xEA) became a round dot. When the deck's symbol font is installed,
its glyph is drawn (symbol fonts map character codes at U+F000 + code), as
PowerPoint does; otherwise Wingdings 2 and 3 codes get their own
look-alikes.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Circled-number lists (circleNumDbPlain) and character bullets whose glyph
isn't in the paragraph's font were drawn as missing-glyph boxes (numbers)
or always from Inter (characters). Both now go through the usual font
fallback, as PowerPoint draws them.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Run shadows and glows (a:rPr/a:effectLst outerShdw, glow) were read but
never drawn. The text layout now carries them on each glyph run (colours
resolved, sizes scaled with autofit) and the renderer draws every run's
shadow, then every glow, blurred, under the text. A text glow is solid to
about a third of its radius past the letters and fades out by two thirds,
as measured on PowerPoint's export. needs_filters also looks at text
effects (runs, list styles, master text styles), so they render with the
blur filter.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A gradient on text was read but drawn in its first colour. Each line is now
filled with the whole gradient, spanning that line's text from its top to
its bottom and its first to its last character, as PowerPoint does. The
gradient builder moves out of fill_path into paint::gradient so fills and
text share it (and PowerPoint's two-colour ramp). Gradient colours are
resolved when the text style is made.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Emoji: colour glyphs, and emoji take the colour emoji font
Emoji were drawn as black outlines from a symbol font, with ZWJ sequences
(families, flags) falling apart into their parts. Now:
- an emoji cluster (emoji presentation, U+FE0F, ZWJ between pictographs,
skin tones, regional indicators, keycaps) takes an installed colour font
(COLR, CBDT, sbix or SVG tables) before the document's fonts, so its
sequences ligate as in PowerPoint; U+FE0E keeps a plain face;
- COLR v0 and v1 glyphs are painted in their CPAL colours (solid fills,
linear, radial and sweep gradients, transforms, composite modes), with
palette index 0xFFFF in the text colour.
A tiny COLRv0 test font (two squares, CC0, regenerated by
make_colr_font.py) backs the render test.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Render: lock skrifa for colour glyphs
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Text: wrap lines where PowerPoint does
- Without latinLnBrk (PowerPoint's default) a word doesn't break after '/'
or '\': URLs and paths fill the line and break at its edge.
- Korean wraps between words, not between syllables.
- With hangingPunct, closing East Asian punctuation hangs past the end of
the line instead of dragging the previous character down.
ParaProps gains latin_line_break and hanging_punct, read and written as
latinLnBrk and hangingPunct.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Text: simpler break guard (clippy)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Text: single-spaced lines are 1.2 times the font size apart, as in PowerPoint
PowerPoint spaces single-spaced lines 1.2 x the font size apart in every
font (measured on twenty Latin, CJK, Thai and Indic fonts at 18 and 40 pt
with PowerPoint's slide export) and puts the baseline where the font's
Windows ascent/descent split it. DeckCraft used each font's own ascent plus
descent, so Calibri lines were 15% too tight and Segoe UI ones too loose.
FontFace gains line_split (usWinAscent / (usWinAscent + usWinDescent));
line_metrics uses it with LINE_PITCH = 1.2.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Text: tidy the line pitch test
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fonts: line_split guard reads plainly (clippy)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Text: note that the 1.2 line pitch was measured on one Windows install
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When marL + indent is negative, PowerPoint pins the bullet to the box's edge
and starts the first line's text at -indent; DeckCraft put the text right
after the number, so numbered lists written with indent=-457200 and no marL
lost their gap.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PowerPoint draws buChar char="••" (seen in a LibreOffice test deck) as a
single dot; DeckCraft drew both, pushing the text right.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Superscript and subscript glyphs were drawn at two thirds of the run's size but
advanced by the full size, so the letters spread apart ("s u p e r") and the
line ran wider than in PowerPoint. The cluster advance now uses the same scale
as the glyphs.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every datetime field showed the deck-wide fixed date text instead of a date: empty when no slide
had a fixed date, and that text on every slide when one did. A fixed date is plain text in its
slide's placeholder; a field is an automatic date, so it now shows the local date and time in the
format of its type (ECMA-376 datetime1 to datetime13 as PowerPoint lists them for English,
datetime and datetimeFigureOut the short date). Numeric dates follow the run's language
(en-GB 09/10/2026, de-DE 09.10.2026...); formats with names stay English-only, so other languages
keep the field's saved text, as do unknown types and builds without a clock (wasm). The saved
text is kept on save.
- text: datetime module (field types, formats, local clock via chrono, not on wasm);
Fields::field_in passes the run's language.
- render: RenderOpts::now injects the clock (tests, deterministic output); the PDF text layer
uses the same fields.
- engine: Header & Footer turns existing date placeholders into fixed text or back into a
field; new date fields are saved with the date they show (local, not UTC).
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Sprint 1: bidi and Arabic typography engine
fonts: ISO 15924 script and a:latin/a:ea/a:cs slot classification;
shape_range shapes an item with the whole paragraph as HarfRust
pre/post context, explicit script/language/direction, glyphs kept in
visual order; per-grapheme-cluster fallback (slot face, theme script
font, OS cascade families, bundled craft-fonts face); GDEF ligature
carets.
text: UAX #9 levels for the whole paragraph (a:pPr/@rtl), itemization
by level, script, face and shaping style (paint-only changes no longer
split words), logical line breaking at cluster boundaries, per-line
L1/L2 reordering (L4 mirroring by the shaper); RTL start edge and
bullets, justLow kashida; caret Affinity, logical/visual movement,
bidi hit testing and selection as the union of visual boxes.
model/pptx: theme a:font script fonts and algn="justLow" round-trip.
engine: format.rtl (Right-to-Left Text Direction), justifyLow align.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fonts: the bundled script face comes before installed fonts; no Microsoft family names
Arabic, Hebrew, Thai and Devanagari clusters try the craft-fonts face first, so every machine
draws the same, then openly licensed or OS-native installed families, then any installed face.
Installed Microsoft faces are still used through that scan, but not named (AGENTS.md §1.1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tayeb <tayeb@192.168.11.179>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Morph: shapes move, resize and blend in the slide show
Morph used to fall back to a plain crossfade in the show: morph_pairs and
morph_xfrm existed, but nothing rendered frames with them.
- anim: morph_frame builds a frame from two slides. Paired shapes move along
morph_xfrm (eased); a pure fill colour change blends, other content changes
cross-fade on the same moving box. Unpaired shapes fade out/in, shapes with
entrance animations or hidden media stay out of the frame.
- render: render_blend draws such a frame in one pass: both backdrops
(background, master and layout graphics) cross-fade, every shape resolves
against the slide it comes from, so placeholders keep their own layout.
Renderer::slide is split into canvas/fields/backdrop to share the code.
- ui-egui: the show renders morph frames with these, crossfade otherwise.
generated with Claude Code
* Contributors: add my own entry (alexanderalber)
generated with Claude Code
* Morph: Words and Characters options morph text token by token
The transition option (objects/words/characters) was stored and round-tripped
but ignored by the show. With Words or Characters, paired shapes whose text
differs now morph their text instead of cross-fading it:
- text: GlyphRun records per glyph the paragraph character it draws, so
glyphs can be grouped into words or characters.
- anim: morph_frame reads the option and lists such pairs in MorphFrame::text.
- render: morph_text draws them. Shared words (or characters) travel from their
place in the old text to their place in the new one and grow or shrink to the
new size; a different look cross-fades on the way. The remaining tokens fade
while following a scale and shift fitted to the shared ones, so a line moves
and rearranges as one. Rotated text falls back to a cross-fade.
draw_layout_with's glyph drawing moved into draw_glyph for reuse.
- ui-egui: the show passes the pairs to render_blend.
generated with Claude Code
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>