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>
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>
PowerPoint blends a gradient of exactly two stops at 0% and 100% in linear
light, with the second colour's share following an S-shaped curve (black to
white is 105/186/238 at a quarter/half/three quarters, not 64/128/191).
Gradients with more stops, or a stop moved off an end, stay linear in sRGB.
Measured with PowerPoint's slide export on shapes and backgrounds, in both
directions. The two-colour case is expanded into sixteen pieces per
segment so vello's sRGB interpolation draws the same ramp.
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>
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>
A --cmd without a value now asks for COMMAND[=PARAMS]. The params field
says how describe reads it, table.split's row and col are optional as
the command reads them, and the MCP instructions name only shape
commands for `ids`.
[claude-opus-5-5] on behalf of hoblin
Same fix as storytold/gridcraft#117 (storytold/gridcraft#316): eframe drops
the window while AppKit's run loop still runs, AccessKit restores the
content view's class and AppKit's Touch Bar KVO observer throws on the
next display cycle. on_exit now ends the process on macOS once the
preferences are saved.
`deckcraft-cli help` and each subcommand's `--help` say what it does,
with a working example; `--help` never runs the subcommand. `describe`
spells out a command's parameters and what it acts on, `commands` lists
one command per line, and the MCP instructions say what format.* acts on.
[claude-opus-5-5] on behalf of hoblin
`deckcraft-cli render` took any argument it didn't recognise as the output
path, so `render deck.pptx --help` rendered the slide into a file named
`--help`, and a mistyped option was silently ignored. An argument starting
with `-` that render doesn't know is now an error naming it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous release build failed in craft-common-actions, not in this
repo: riscv64 packaging got --formats deb,rpm,tar (fixed in
craft-common-actions#3), Linux jobs lacked the ALSA headers
(craft-common-actions#5), and Windows signing needed the AZURE_* secrets
listed in the caller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the release workflow moved to storytold/craft-common-actions, it has
passed only the Apple and WINDOWS_CERTIFICATE secrets. The signing job reads
values from the release environment, but only for names the caller lists, so
the AZURE_* names that Trusted Signing uses came through empty and Windows
builds would ship unsigned. Same fix as storytold/craft-launcher#174.
Co-authored-by: Claude Opus 5.5 <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's lnSpcReduction takes percentage points off a paragraph's line
spacing (90 % less 20 % is 70 %) and leaves the space before and after
paragraphs alone. We multiplied (90 % x 0.8 = 72 %) and shrank the
paragraph spacing too, so shrunk text came out with different line and
paragraph pitches than PowerPoint's: measured on PowerPoint's PDF of a real
deck, a body at 90 % spacing, 10 pt before paragraphs and a 20 % reduction
runs at 20.2 pt lines with 9.9 pt between paragraphs.
PowerPoint fits shrink-on-overflow text again only when the text changes,
never when a file opens: it draws the saved fontScale/lnSpcReduction even
where our metrics would call the text overflowing (trailing empty
paragraphs at full size, substituted fonts). Shrinking further on open made
saved decks text smaller than PowerPoints (74 % instead of 85 % on a real
deck). With no saved scale, or once an edit has reset it, the text is
fitted as before.
With exact line spacing (a:lnSpc/a:spcPts) PowerPoint puts the baseline at
80 % of the line pitch and ends the text at the last line's descent, not a
full pitch below it. We drew every line a full pitch tall, so table rows
whose cells use exact spacing grew taller than PowerPoint's (35.6 pt
instead of 33 pt for 12 pt type at 26.59 pt), pushing tables and text
boxes down and tripping shrink-on-overflow early.
Measured on PowerPoint's own PDF of a real deck: rows now come out at
33.5 pt against PowerPoint's 32.9-33.8 pt.
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>
PowerPoint draws normAutofit text at its saved fontScale/lnSpcReduction;
we ignored them and only shrank when our own layout overflowed, so saved
decks showed larger text and wider line spacing than PowerPoint, with text
running past placeholders. The layout now starts from the saved scale
(nonsense values ignored) and still shrinks further when the text doesn't
fit. Editing the text resets the saved scale so the new text is fitted
afresh, as PowerPoint refits on edit.
A glow always filled the shape's whole silhouette, so a frame with no fill
(a rounded-rectangle border with a soft glow, common in corporate decks)
came out as a solid tinted panel over the slide. PowerPoint glows such a
shape along its outline only and leaves the inside clear; a filled shape's
glow is unchanged.
Checked against PowerPoint's own PDF export of 31 real decks (597 slides):
the affected slides' mean pixel difference drops from ~29 to ~4 (of 255).
Long speaker notes grew out of the notes pane and over the slide: the
editor now scrolls inside the pane. In Presenter View the Previous, Next
and End Show buttons drew dark text on the dark window, so they looked
missing; they are now light on a visible fill, and the timer gets Pause /
Resume and Reset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AppKit draws the traffic lights 16 pt from the top of the window on macOS 27,
but the title bar was 38 pt, so they sat above its centre line. On macOS the
bar is now 32 pt, centred on them: content starts after the lights with the gap
they keep from the top, and the Present button shrinks to 22 pt to keep a
margin. Same approach as wordcraft #255 and gridcraft #323, with the light
metrics measured on macOS 27. Linux, Windows and web are unchanged.
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>
File ▸ Open's Outline filter accepts `.md`, but a `.md` was read with the
plain outline format (unindented line = slide, indent = bullet), so a
Markdown outline written the usual way kept `#` in its titles and made
every top-level list item a slide of its own.
`deckcraft_format::markdown_to_slides` reads ATX headings (`#`–`######`,
closing `#`s dropped) as slide titles, list items (`-`, `*`, `+`, `1.`,
`1)`) as bullets nested by their indentation, and other lines under a
heading as body paragraphs. A `.md` without headings is still read as a
plain outline, so existing indented `.md` outlines open as before; `.txt`
is unchanged. Opening a `.md` and dropping one on the window
(`slide.fromOutline {markdown: true}`) use it. Both readers now share the
slide builder.
Tests: the outline from the issue (2 slides, nested bullets), numbered and
`*`/`+` lists with text under a heading, `slide.fromOutline` with
`markdown`, plain `.txt`/`.md` outlines unchanged, and heading / list-item
edge cases (not-a-heading forms, closing hashes, Unicode, long digit runs).
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>
The Selection Pane was only under Home ▸ Arrange, which is disabled until
a shape is selected, so it couldn't be opened on an empty selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Picture Format ▸ Change Picture button ran the insert-picture dialog,
so it added a second picture instead of replacing the selected one. A new
app.changePictureDialog picks a file and runs picture.change, which keeps
the picture's position, size and formatting; the canvas right-click menu
offers it on pictures too. On the web, where the file arrives later
through the inbox, the pending change is remembered for that pick.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Slide Size dialog gets an Orientation choice that swaps width and
height, and design.slideSize takes orientation: portrait|landscape so a
preset can be made portrait from the CLI and MCP too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI remembers whether the last press landed in the thumbnail pane (the sorter always counts) and passes scope slides to edit.selectAll in that case.
The current slide stays put and any shape or text selection is cleared. A scope other than slides is an error; without a scope the command still selects shapes.
egui-winit drops Ctrl+V when the system clipboard is empty. Copying a
rectangle or picture puts no text on it, so the shortcut never reached
the session and only the context menu could paste objects. Copy and cut
now leave an invisible word joiner on the system clipboard in that case,
and paste maps it back to the session's own clipboard.
There was no way to turn a presentation into an animated GIF. file.export now writes one: format
`gif` (or a .gif path), one frame per slide drawn by the slide renderer and shown for
secondsPerSlide, encoded with the `image` crate's GIF encoder (already a dependency, no new
crates) with a 256-colour palette per frame: exact when a slide has 256 colours or fewer,
NeuQuant otherwise. Slides are composited onto white.
Options: width (small 640, medium 960 (default), large 1280, extraLarge 1920, or 16-3840 px; the
height follows the slide's aspect ratio, no side over 3840), secondsPerSlide (0.1-600, default 5,
written in GIF hundredths), loop (forever by default, or once: no NETSCAPE2.0 block), slides (a
list of indices, in play order) and includeHidden (hidden slides are skipped by default).
Out-of-range or wrongly typed values, NaN and infinities are BadParams errors, never panics; at
most 500 frames and 200 million pixels in total, checked before anything is rendered.
- Engine: crates/engine/src/gif_export.rs (options, frame list, size, encoder);
cmd::file::gif_bytes, save_bytes("gif"), format_for_path(".gif").
- UI: the Export… dialog (Command palette; the app has no File menu yet) gets "Animated GIF (.gif)" with Size, Seconds per slide, From/To slide,
Loop forever and Include hidden slides; an invalid range or value keeps the dialog open with a
message. The web build downloads the bytes (image/gif).
- CLI: convert IN OUT.gif, run --export OUT.gif --export-params JSON; usage lists the format and
options.
- MCP: the export tool lists gif and width/secondsPerSlide/loop (slides and includeHidden now
apply to PDF and GIF).
- Docs: file-format parity GIF row, gaps (video export remains), parity checklist row M -> P
(P2 breadth 36% -> 37%, overall 79%), control protocol, MCP, README, ROADMAP progress log.
Transitions and build animations are not animated in the GIF (tracked with video export).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wgpu's default DX12 compiler (Auto) loads whichever dxcompiler.dll the DLL
search path finds. DeckCraft ships none, and without a matching dxil.dll the
unsigned-DXIL warning fails wgpu's indirect-validation pipeline, the device
is lost and the app exits before the first frame. Pin the compiler to FXC,
which is part of Windows; WGPU_DX12_COMPILER still overrides it.
Fixes#153
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When typing in Kurdish, Arabic or Hebrew in a new document or text shape, default
paragraph direction (props.rtl: None) was resolving to LTR. In an LTR
paragraph, visual reordering pinned newly typed characters and the caret to the
left margin, resetting to the start of the line with every keystroke and jumping on
space characters due to UAX #9 rule L1.
- Add first_strong_is_rtl() in deckcraft-model following UAX #9 rules P2/P3.
- Auto-resolve unassigned paragraph direction to RTL in shape_para layout when the
paragraph starts with an RTL script, defaulting alignment to Align::Right.
- Auto-promote paragraph to RTL (props.rtl = Some(true), props.align = Some(Align::Right))
when typing or inserting text starting with an RTL character.
- Add regression tests in deckcraft-model, deckcraft-text, and deckcraft-engine.
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>
* ci: refactor workflows to use reusable artcraft-common-actions
* ci: point reusable workflows to storytold/craft-common-actions
* ci: pin storytold/craft-common-actions to commit 27a978f and pass secrets explicitly
* ci: use @main (unpinned) for craft-common-actions with explanation comment