PowerPoint stores chart data in ppt/embeddings. Saving used to drop that part and the chart relationship, so the deck still rendered but the workbook was gone. An edit still omits the workbook rather than writing numbers that no longer match it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ActiveX controls show their pictures
A slide's p:controls are read: each control with a p:pic (the image PowerPoint shows for it,
often a WMF) becomes a picture above the slide's shapes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Metafiles draw their text, pattern and hatch brushes
deckcraft-metafile (still kurbo + thiserror only, so it builds for wasm):
- Text: WMF EXTTEXTOUT/TEXTOUT and EMF EXTTEXTOUTW/EXTTEXTOUTA/SMALLTEXTOUT with the
selected font (CREATEFONTINDIRECT, EXTCREATEFONTINDIRECTW: face, signed LOGFONT height,
width, weight, italic, underline, strike-out, escapement, charset), text and background
colour, background mode, TA_* alignment and TA_UPDATECP, the dx array (ETO_PDY pairs too),
ETO_OPAQUE fills and ETO_CLIPPED clips. 8-bit strings decode through their charset
(Windows-1252/1250/1251; symbol fonts keep their codes at U+F000; double-byte sets keep
their advances). The neutral Item::Text carries a text-space-to-picture transform, so the
mapping and escapement are folded in and glyphs are never mirrored.
- Brushes: WMF DIBCREATEPATTERNBRUSH, EMF CREATEDIBPATTERNBRUSHPT and CREATEMONOBRUSH, and
BS_HATCHED, as Item::Pattern tiles in 96-dpi pixels; WMF PATBLT. The XOR / AND-pattern /
XOR see-through shadow of old PowerPoint WMFs draws its pattern in the XOR colour.
- MFCOMMENT escapes with an embedded EMF are ignored: PowerPoint draws the WMF's records.
- Pattern pixels and text count against the byte budget; strings are capped.
deckcraft-render draws text through deckcraft-fonts with the face slide text would use
(document_face), symbol fonts mapped as bullets map them (deckcraft_text::symbol_char, now
public, with Webdings), per-character fallback, the file's advances, GDI cell metrics for
top/bottom alignment, underline/strike-out and synthetic bold/italic. Tiles below 1.5 output
pixels per cell are drawn as their average colour.
On the 24 corpus slides that show a metafile, mean abs diff against PowerPoint goes from
28.31 to 5.06 (with the OLE/ActiveX preview commits).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #133: Selection pane engine: restack inside groups, Show/Hide All, Lock like PowerPoint
- arrange.bringForward/sendBackward/bringToFront/sendToBack and arrange.reorder work on a
group's children, which stay in their group (as PowerPoint's pane arrows and drag do);
steps that change nothing leave no undo step.
- selection.select (pane click: Ctrl toggles, children and top-level objects don't mix,
picking a hidden object shows it), selection.showAll (everything, children too) and
selection.hideAll (top level).
- shape.lock cascades through groups; locked objects can be selected, formatted and typed
in, but not moved, sized, rotated, flipped, reshaped or cropped (commands and canvas).
- PPTX: Lock reads and writes PowerPoint's full locking attribute set per element kind
(observed by saving locked objects from PowerPoint); partial locks aren't Lock.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Front-most first with groups folding; drag a row to restack among its siblings (accent
insertion line); Bring Forward / Send Backward arrows; Show All / Hide All; per-row eye and
padlock; double-click or F2 renames in place; Ctrl+click toggles, Shift+click selects a run;
arrows, Home, End and Esc in the list; right-click menu. Lock/Unlock on the canvas menu,
greyed handles on locked objects. egui_kittest tests drive the pane.
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>
* Tables: styles the file defines are read, drawn and kept
PowerPoint writes the definition of every table style a deck uses into
ppt/tableStyles.xml. DeckCraft mapped a handful of GUIDs to its own looks
and drew any other style as Medium Style 2, so themed and light styles came
out in the wrong colours, and dropped the definitions on save.
The reader now keeps each definition (whole table, bands, first/last
rows and columns, corner cells, table background; fills, theme fill and
line references, borders, text colour and bold) on the presentation, and
tables using one keep its GUID. The renderer applies the parts that cover
each cell in the spec's order, a part's edge lines on its region's edges
and its inside lines within it, and paints fills as real fills (gradients
and see-through colours). The writer puts the definitions back as read.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Tables: DeckCraft's own table styles still map back to their ids
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fix a brace lost in the rebase
---------
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>
PowerPoint leaves an inheriting placeholder's spPr without a geometry and
draws it with its layout (or master) placeholder's: a picture placeholder
cut into a shape, or with holes, keeps that shape on every slide. DeckCraft
read such placeholders as rectangles. They're now marked while reading
and given their parent's geometry once the layouts are read; anything
without a parent stays a rectangle. DeckCraft itself already writes
inheriting placeholders this way.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A shape or background blipFill with neither a:stretch nor a:tile (as some
tools write it, tdf#128596) was stretched across the shape; PowerPoint
tiles the picture at its own size.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Theme background pictures (Office 2007-era themes such as Treck) carry an
a:duotone recolour in phClr; DeckCraft read it but drew the picture
unchanged, so those backgrounds came out grey. The painter now resolves the
duotone's colours against the slide's colours and placeholder colour, and
the decoded-image cache applies them (luminance picks a colour between dark
and light) and keys on them.
Layouts and slides can replace the master's colour map
(p:clrMapOvr/a:overrideClrMapping, e.g. dark section layouts mapping bg1 to
dk1). They were read as the master's map; now Layout and Slide carry an
optional colour map, resolve::Ctx uses slide > layout > master, and the
writer keeps them.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bodyPr spcFirstLastPara="1" makes the first paragraph's space before and
the last paragraph's space after count, which PowerPoint (and Google
Slides exports) use; we always dropped them, so such text sat a paragraph
gap too high (9.5 pt on a real deck measured against PowerPoint's PDF).
Read, written, inherited from the layout like the other body properties,
and applied in the layout.
PowerPoint writes Float In / Float Out with the Float Down option as its
own preset, 47 (Descend), not as preset 42 with a direction. DeckCraft
only knew 42, so a Float Down entrance or exit imported as a plain fade,
which is what the issue's video shows: the buildings fade out in place
instead of sinking. Preset 47 now imports as float / floatOut with the
"d" option and is written back as 47.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exit effects DeckCraft had no entry for (Peek Out, Strips, Blinds, Box,
Checkerboard, Diamond, Sink Down, Spinner, Contract) imported as a plain
Fade and lost their direction, so a Peek Out or Sink Down didn't move.
Each mirrors an entrance DeckCraft already plays, so it is played as that
entrance in reverse (Sink Down as Rise Up, leaving downward); the import
keeps PowerPoint's direction and saving writes a matching filter or
motion. Float Out now also writes its motion.
The preset ids and directions come from PowerPoint: every effect, as
entrance and exit, in every direction it accepts, saved and read back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The writer named a preset's only guide `adj` whenever the preset has one
adjust value, but bentConnector3, curvedConnector3, frame, mathPlus,
mathMinus and mathMultiply call it `adj1`, and PowerPoint refuses to open
a file that renames it. The names come from PowerPoint itself: every
autoshape and connector created with an adjusted first value and saved;
these six are the only single-guide presets named `adj1`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving with a .potx or .ppsx name wrote a byte-identical .pptx: the
main part was always presentationml.presentation.main+xml, because
format_for_path folded all three extensions into "pptx". It now keeps
potx/ppsx, and deckcraft_pptx::export_as picks the template or
slideshow content type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A legacy OLE object finds its preview in the slide's VML drawing through
spid; DeckCraft doesn't write that drawing, and PowerPoint refused to
open a file whose oleObj has a spid but no VML shape. The reader now
takes the preview picture from the VML drawing (so DeckCraft shows it
too), and the writer drops spid and adds that preview as a p:pic, the
form PowerPoint itself writes as the fallback.
Checked in PowerPoint: the issue's 45545_Comment.pptx saved by DeckCraft
opens with its three embedded objects and draws slide 8's table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object DeckCraft doesn't model kept its XML, but the parts that XML
points to (diagram data, layout, style and colours; an OLE embedding)
were not kept, so a save wrote it as an empty rectangle or its preview.
Opaque now carries those parts (relationship id, full relationship type,
the part as a media item) and the writer re-adds them under the slide's
new relationship ids. A part with relationships of its own, or an
external link, still falls back to the preview with the existing
warning. A diagram's data part loses its link to the cached drawing,
which is a slide relationship id a save renumbers; only diagrams whose
drawing is missing or empty reach this path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Notes from a .pptx were read and written with their formatting, but the notes pane and Notes
Page drew them as plain text (Slide::notes_text in a plain TextEdit), and any edit there saved
the text back through slide.notes as TextBody::from_text, dropping every bold run, level,
indent and bullet of the slide's notes.
- ui-egui: a notes editor (notes.rs) shared by the notes pane and Notes Page. It draws bold,
italic, underline, strikethrough and size from the runs, and each paragraph's margin, hanging
indent and bullet or number (Wingdings/Symbol bullets mapped as on slides); paragraphs are
laid out one by one so wrapped lines align with the text, not the bullet. Each edit is applied
to a formatted draft, so typed text takes the formatting where it lands (Enter in a bulleted
point starts another), and the draft is saved once the field loses focus or the slide changes.
- engine: slide.notes takes `body` (formatted notes, as slide.inspect's new `notesBody`), and
`text` now changes only what was retyped (model edit::replace_text); text sharing nothing
with the old notes is still plain.
- text: map_symbol_bullet is public for the notes editor.
PDF notes pages still print the notes as plain text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PowerPoint 2021/2024 write a 0.5 s transition with neither spd nor
p14:dur, since "fast" is the schema default for spd. The reader fell back
to the effect's own default instead (Morph 2 s, Fade 0.7 s, most 1 s).
generated with Claude Code
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>
`sniff` searched only the leading 4096 bytes for the content-types part, but
OOXML writers may place that part anywhere in the archive. Recent PowerPoint
builds (16.x) store it as the last zip entry, so every deck saved by them was
rejected with "not a presentation DeckCraft can read" although it imports fine.
In one real folder 169 of 300 decks were affected.
Look the part up in the zip's central directory instead. The regression test
re-packs the sample deck with the part last behind 64 KB of padding and
checks that it is both recognised and imported.
Co-authored-by: Matthias Weber <webermatt@me.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>