An owned corpus slice checks combo charts with data labels, merged
tables with diagonal borders, and text columns, autofit, vertical text
and a WordArt warp. Trendlines (type, order, period, forecast,
intercept, equation and R²) are now read and written.
New owned corpus test on PowerPoint-shaped XML: a custom-theme deck with
shadow, glow, soft-edge and reflection effects and every common text and
shape hyperlink, and a two-master deck with its own themes and layouts.
The one loss it found: a:hlinkMouseOver on text runs was dropped; it is
now read into RunProps::hover and written back.
An owned corpus opens, saves and reopens SmartArt and OLE kept as XML, audio and video bookmarks and trims, a section with a hidden slide, slide-number and date fields, ink, .glb models and animation repeat, bounce end and triggers. Relationship-bearing SmartArt and OLE, playback options and rewind stay with their open PRs.
An owned corpus of those kinds survives open, save and reopen. A run's
inner shadow, reflection and soft edge were dropped, and a bevel preset
with no kept 3-D XML was not written back, so import and export now keep
them.
An owned corpus of those element kinds survives open, save and reopen.
Paragraph default run properties, including a link, and action sounds
were dropped on save, so import and export now keep them.
Open, save and reopen decks we author, and compare package parts and
text. The check showed a notes master's header and footer were replaced
by an empty notes master on save.
Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up to the transition sound picker. The Sound box now lists
[Stop Previous Sound] and six built-in sounds (Chime, Click, Camera,
Whoosh, Drum Roll, Laser) that are synthesised as small WAVs when
chosen, so no sound files ship with the app; the same built-in sound is
embedded once however many slides use it. Loop Until Next Sound repeats
a sound in the show until another slide's sound or Stop Previous Sound.
PPTX reads and writes p:stSnd loop="1" and p:endSnd, keeps the sound's
file name in p:snd/@name, and setting a slide's transition to None no
longer drops its sound.
Co-authored-by: Cursor <cursoragent@cursor.com>
Transition sounds were read from and written to .pptx but could not be
set and never played. Transitions › Timing gets a Sound box ([No Sound]
or Other Sound…), backed by a new transition.sound command that embeds
an audio file on the selected slides (or removes it with none). In the
slide show the sound plays as its slide comes in and, as in PowerPoint,
keeps playing into the next slide unless that slide has a sound of its
own. Built-in sounds, Stop Previous Sound and Loop Until Next Sound are
still missing; the parity docs say so.
Co-authored-by: Cursor <cursoragent@cursor.com>
There was no way to make text see-through: the only Transparency
slider was the shape fill's. The new format.textTransparency command
keeps the text's colour and sets its alpha (replacing any it had, and
dropping it at 0). A run without its own colour takes the colour it
shows now, so inherited placeholder or theme colours stay as they are.
Gradient text gets the alpha on every stop. The Format Shape pane's
Text Options page gains a Text Fill section (colour and Transparency),
and the Shape Format › Text Fill dropdown has the same slider.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Charts: every category/value/size cache draws from one chart-wide
budget of 1M decoded slots, so sparse ptCount/idx declarations across
many series can't expand far beyond the XML.
- Chart parts go through the same 96 MiB XML size guard as other XML.
- Package::open charges every inflated byte, including parts it then
discards, against a 4 GiB decompression budget and stops reading
entries once it is spent.
- Legacy OLE previews index each VML drawing once (shape id -> picture
part) instead of reparsing it and its rels for every object.
Co-authored-by: Cursor <cursoragent@cursor.com>
A deck that embeds a TrueType or OpenType face, including one wrapped as Embedded OpenType, now draws with that face and saves it back. MicroType Express subsets stay in the file and still draw in a stand-in.
Co-authored-by: Cursor <cursoragent@cursor.com>
The writer emitted the legacy comment part only: replies were flattened
into sibling comments, the resolved flag was dropped. Modern PowerPoint
decks carry a second, threaded part (p188) as well - and DeckCraft read
both, importing every comment twice.
Export now writes the modern threaded part (p188:cmLst, with the slide's
commentRel anchor ext and the modern authors part) next to the flattened
legacy part, the same pair of parts PowerPoint itself writes: older
readers keep the legacy view, PowerPoint 2021+ and DeckCraft read
threads, replies and resolved state. The reader prefers the modern part
when both are present and scales modern positions as EMU instead of
legacy 1/8 pt, so real PowerPoint decks stop double-importing their
comments and keep their marker positions.
Comment ids are deterministic GUIDs from the existing tables::guid, so
exports stay reproducible without a new dependency.
Known limitation for a follow-up: comments anchor with
p188:unknownAnchor (schema-valid) rather than a slide moniker, which
needs part identity the model does not have yet.
Tests: threads plus resolved state round-trip on top-level comments and
replies, both parts and the anchor-rel wiring asserted in the raw
package, part numbering with comment-free slides skipped, legacy-only
and modern-only packages import each with their own position units, and
XML-special characters survive the trip.
A shape's picture fill marked not to rotate with the shape turned and
flipped with it (rotWithShape was neither kept nor written: always 1).
PowerPoint keeps such a picture upright, stretched (or tiled) over the
box the rotated shape covers on the slide and clipped to the shape. Now
read, written (PictureFill::upright) and drawn so.
Wild synthetic set: picture fills on rotated/flipped shapes 12.89 ->
3.87.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- a:biLevel (Color > Black and White) was ignored. It is now read, written
and drawn: white where the luminance reaches the threshold, else black
(observed thresholds at 30/50/70% on colour swatches).
- a:alphaInv was ignored; an opaque picture with it is invisible in
PowerPoint. Now read, written and drawn.
- Grayscale, duotone and saturation weighed channels 0.299/0.587/0.114;
PowerPoint's export greys pure red to 54 and green to 182: Rec. 709
weights (0.2126/0.7152/0.0722).
Wild synthetic set: recolour slide 7.60 -> 1.15, picture corrections
13.36 -> 7.36.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A table whose tblPr names no tableStyleId drew without any lines.
PowerPoint draws such a table with thin dark grid lines (as its "No Style,
Table Grid"); observed on a synthetic deck with and without the explicit
"No Style, No Grid" GUID. Such tables now read as the grid style.
Wild synthetic set: tables with cell margins/fills/styles 4.80 -> 2.82,
11.28 -> 10.15, 6.86 -> 3.16.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Gradient tile rectangles were dropped on import. PowerPoint's "from
corner" radial presets depend on them: fillToRect puts the focus on a
corner and tileRect stretches the gradient's box a whole size past it, so
the circle is centred on that corner and reaches the far one. Without it
(after the two-point radial change) such fills washed out. A linear
gradient now spans its tile rectangle; a radial one ends on the circle
around it, with the focus still placed in the shape's own box (fitted to
PowerPoint's export of the corpus deck tdf114848: mean diff over the
filled box 0.77). Round-trips through pptx.
Wild synthetic set: tile-rect gradients 16.74 -> 2.48.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Observed on PowerPoint's export (w14-xml s12):
- a two-column body with rtlCol="1" starts its first column on the right; DeckCraft read and
saved the attribute but laid columns out left to right;
- anchor="just" and anchor="dist" place the text at the bottom of the box, like anchor="b";
DeckCraft drew them at the top;
- wordArtVertRtl stacks characters like wordArtVert but runs its lines from right to left; it was
read as wordArtVert (and saved back as it). It is now its own TextDir, kept on save.
w14-xml s12 9.47 -> 5.39.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
mongolianVert was read as vert (and saved back as vert). PowerPoint turns it like vert but runs
its lines from left to right, the anchor mirrored with them (bottom-anchored text sits on the
right). It is now its own TextDir, kept on save; its lines' places are mirrored across the box.
wordArtVert (Stacked) was turned like vert. PowerPoint stands every character upright, one
under the other (Latin letters about one em apart, spaces their own width), with lines running
left to right. Stacked text now stands each character upright in an em of its own and mirrors
the line order.
Also: a bullet in the second or later column of a multi-column body is drawn on its line's
baseline (it kept the first column's).
w01-vertical s4 (mongolianVert) 11.54 -> 9.38, s5 (wordArtVert) 13.84 -> 13.56; w14-xml s13
6.78 -> 6.60.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every transition and Effect Option was built in PowerPoint (COM), exported as
video and compared frame by frame with DeckCraft at the same progress
(mean abs diff over 150 variants 35.7 -> 15.0, 0-255).
- anim: transition_frame(kind, option, t, aspect) with measured curves
(smoothstep fades, ease-in-out moves, Cover/Uncover/Pan curves), directions
and option meanings, soft edges as per-corner opacity, 3-D effects as
perspective-projected cards, Dynamic Content moving slide contents over
cross-fading backgrounds (Source::OldBack/NewBack/OldContent/NewContent);
layer_mesh subdivides perspective quads so textures stay straight.
- New options: Page Curl Single, Glitter Hexagons, Shred In/Out, Zoom and
Rotate; PowerPoint-style Effect Options labels.
- render: composite_transition (CPU twin of the show), render_slide_part,
render_transition; deckcraft-cli render-transition FILE --slide N --t T.
- show: per-vertex opacity meshes, slide parts rendered once per transition.
- pptx: PowerPoint's elements and defaults (p:strips diagonal wipes, p14:warp
and p:newsflash for Zoom, p:zoom for Shape In/Out, glitter pattern, shred
rectangle/dir, prstTrans invX per preset, pageCurlSingle, defaults for
randomBar/doors/window/pan/zoom); PowerPoint reopens DeckCraft's files with
the same effects.
- Tests: measured curves, probes of which slide shows where at t, option
coverage, per-frame triangle budget, PowerPoint XML and defaults round trip.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A SmartArt drawing shape keeps its text rectangle in dsp:txXfrm, apart
from the shape's own box. The importer ignored it and laid the text out
in the shape itself. In list layouts the shape is a tall, narrow box
turned by 90 degrees, so the text came out as narrow vertical columns,
in the editor and in the exported PPTX.
The text rectangle uses the shape's coordinate space and is turned by
the shape's rotation plus its own:
- When it is the shape's text area shrunk by some margin, with the same
orientation, the shape keeps its text and the margin is added to its
insets.
- Otherwise (a rectangle reaching outside the shape, or turned against
it) the text moves to a new shape without fill or outline, placed on
the rectangle right above its shape in the diagram's group.
Both export as plain DrawingML (positive insets, an ordinary rotated
shape), which other readers lay out the same way.
Test: a SmartArt drawing built in the test, with a text rectangle wider
than its shape, a turned shape with upright text, and a rectangle
inside its shape; the laid-out text box is checked on import and after
a PPTX round trip.
Value axes showed raw numbers ("800000") with a fixed ~5-interval scale and
fixed label gutters, deleted axes were still drawn, and data labels ignored
their number format, so Russian business charts read "516600000" where
PowerPoint shows "516 600 000 ₽".
- New deckcraft_model::numfmt: Excel number format codes (sections for
positive/negative/zero, quoted and escaped literals, _x padding, *x fill,
[$sym-lcid] currency, [colour]/conditions dropped, 0/#/? placeholders,
grouping with ',' or the locale's literal space, trailing-comma scaling,
%, General) with digit grouping and decimal mark from the chart's
language (c:lang: ru-RU -> "800 000", "12,5%").
- Reading: each value axis' numFmt, c:delete, scaling min/max and
majorUnit, for the primary and a combo chart's secondary axis; data
labels' numFmt (series, then group, else the source data's formatCode);
the chart's c:lang. The primary value axis is the one on the left
(bottom) and the secondary the one on the right/top or crossing at max,
whatever the plot groups' order.
- Scale (Chart::axis_scale): fixed bounds and unit where given, else the
data plus 5% headroom rounded out to the smallest 1-2-5 unit that leaves
as many intervals as the axis has room for labels, as PowerPoint does
(0-800 000 by 100 000, 0-7 000 by 1 000 on the sample deck).
- Rendering: labels in the axis format, gutters sized to the longest label,
deleted axes draw no labels and take no room, data labels in their own
format.
A combo chart (columns plus lines, often with the lines on a second value
axis) was read with each series' type but drawn as lines only, on one
scale, so a spend-and-clicks chart showed the clicks as a flat line along
the bottom and no columns.
- Reading: a plot group whose value axis isn't the first group's is on the
secondary axis (Series::secondary_axis, now set).
- Model: Chart::series_kind, on_secondary and axis_range (per axis; a combo
stacks only its stacked series).
- Rendering: bar-like series are drawn as bars (clustered or stacked among
themselves), the others as lines/areas over them, each against its own
axis; the secondary axis' labels go on the right. Line series use their
line's colour and width (where PowerPoint keeps them), also in the legend.
Long category labels wrap onto two lines.
RUSTSEC-2026-0194 (quadratic duplicate-attribute check) and RUSTSEC-2026-0195 (unbounded NsReader
namespace allocation) are fixed in quick-xml 0.41. deckcraft-pptx already reads attributes with
with_checks(false) and does not use NsReader, so neither is reachable today; the bump keeps the
workspace on a supported quick-xml and leaves one copy (wayland-scanner already locks 0.41.0).
decode_and_unescape_value() is deprecated in 0.41 (clippy -D warnings); decoded_and_normalized_value
(Implicit1_0, ...) is what the deprecated method now does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>