Pattern fills were approximated (dots, bars or a grid per family, on an
8-point cell), so pct30 looked like sparse dots and wave like a dot grid.
Every one of the 54 DrawingML presets now draws its 8x8 tile, each cell one
96-dpi pixel (0.75 pt), laid out from the slide's origin. The tiles were
read from PowerPoint's own export of each preset at two output pixels per
cell; the pattern probe deck now matches it to a mean pixel difference of
0.2 (from 62).
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>
PowerPoint shows white around a background picture stretched into part of
the slide and under a see-through background colour. DeckCraft left those
areas transparent, which exports and the slide show draw black.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A shape with useBgFill="1" is a window onto the slide's background, laid
out on the slide. DeckCraft drew nothing in it, so a master that covers the
background with a white panel and cuts a background-filled frame into it
(tdf93868) rendered all white.
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>
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>
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>
* 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
- release.yml: new linux-riscv64 job, ported from GridCraft. Cross-compiles on
ubuntu-24.04 against ports.ubuntu.com riscv64 libs (incl. libasound2-dev:riscv64),
packages a tar.gz and smoke-tests deckcraft-cli --version under QEMU. The release
job needs it, so the draft and SHA256SUMS include it.
- packaging/linux/package.sh: CROSS_ARCH/CROSS_TARGET/CROSS_COMPILE/EMULATOR support.
- README Downloads: list the riscv64 tarball.
- Version 0.4.0 -> 0.5.0 (cargo xtask version set).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <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>
- engine: add animation.painter (Alt+Shift+C) and animation.painterApply (Alt+Shift+V) supporting single-use and sticky modes
- tools: apply animation painter on canvas shape click
- edit: disarm animation painter on deselect (Escape)
- ui-egui: add Animation Painter button in Advanced Animation ribbon group
- tests: unit tests for single and sticky animation painting
- docs/parity: mark Animation Painter as done (breadth parity to 80%)
- contributors: add Johannesgauss to contributors/people.toml
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Three causes, each making links look like they "don't save":
- With the caret at a word boundary (end of text, empty paragraph),
insert.hyperlink formatted an empty range and changed nothing. It now
inserts the address (or the slide's title, or `text`) as linked text, as
PowerPoint does. Typing right after a link no longer extends it.
- The slide show ignored hyperlinks and shape actions. A click on one now
follows it (ShowState::follow): slides, next/previous/first/last, Last
Slide Viewed, End Show, and URLs; the pointer turns into a hand over links.
- The desktop app was built without eframe's `links` feature, so
ctx.open_url did nothing natively (also the ribbon's app page link).
Enabled as in DesignCraft, EffectCraft and PrintCraft.
Saving was fine: links round-trip through .deckcraft and .pptx.
generated with Claude Code
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(pictures): animated GIFs play, with pause (media.gifPlay)
GIF pictures used to show only their first frame. The renderer now decodes every frame (cached, capped at 256 MB and 1024 px a side) and draws the frame for each GIF's play time, passed in RenderOpts::gif_times; exports and thumbnails pass none and keep frame 1. The editor canvas and the slide show redraw only when a GIF reaches its next frame. A corner button on hover (or selection) pauses and resumes a GIF from the same frame, through the new media.gifPlay command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(pictures): animated GIFs play, with pause (media.gifPlay)
GIF pictures used to show only their first frame. The renderer now decodes every frame (cached, capped at 256 MB and 1024 px a side) and draws the frame for each GIF's play time, passed in RenderOpts::gif_times; exports and thumbnails pass none and keep frame 1. The editor canvas and the slide show redraw only when a GIF reaches its next frame. A corner button on hover (or selection) pauses and resumes a GIF from the same frame, through the new media.gifPlay command.
Also adds my entry to contributors/people.toml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Animated GIFs: cap frames (1000), canvas (4096²), per-GIF (64 MB) and cache (192 MB) memory
A few MB of hostile GIF could decode to millions of tiny frames or a 100 Mpx canvas per frame,
and eight cached GIFs could hold 2 GB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
It ran file.new, so each click silently added another blank presentation
and looked like it did nothing. Home is now a UI command (app.home) that
shows the start screen over the open presentations, with a "Back to …"
button; Escape or Home again goes back. Creating or opening a presentation
from it leaves it. While it's open, keys don't reach the hidden
presentation (only app shortcuts and Escape). ui.inspect reports `home`.
generated with Claude Code
egui::Popup::menu closes on any click inside the popup, so the "More
Colors… #" field of every colour drop-down closed the menu before a value
could be typed. The colour drop-downs now open through
widgets::color_popup (CloseOnClickOutside): picking a colour still closes
them (color_grid calls ui.close()), and the other buttons in them
(Gradient…, the Weight, Dashes and Arrows submenus of Shape Outline) close
them explicitly.
generated with Claude Code
* Morph: a changed geometry morphs its outline (a circle turns into a star)
Paired shapes that differ only in geometry (another preset, other adjust
values, a custom path) and fill colour no longer cross-fade: one shape moves
and its outline turns from the old geometry into the new one.
- geom::morph aligns two outlines the way manim does: the same number of
contours, the same number of cubic curves per contour pair, then every
point moves in a straight line. Closed contours are turned and reversed so
the points travel least (a triangle does not twist into a circle), extra
points go to the longest curves, and a sub-path without partner grows out
of (or shrinks into) a point, so a cube's faces appear and a donut's hole
closes. Fill mode, outline and fill rule switch at the midpoint.
- anim::morph_frame lists such shapes in MorphFrame::paths; the renderer
draws them with the morphed geometry (PathMorph, a new render_blend
argument next to TextMorph).
Ported from the path morph in alexplain (MIT, same author).
generated with Claude Code
* Morph: differing looks of an outline cross-fade instead of switching
When a sub-path pair differs in fill mode, outline or fill rule, the two
looks no longer switch at the midpoint. A fill or outline only one side has
fades out (or in), so a filled shape turning into a line loses its fill
gradually. When both fill, the even-odd side lies opaque underneath and the
other fades over it, so a donut's hole fills smoothly while it shrinks.
generated with Claude Code
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Flips mirror a shape's geometry, never its text. The text was drawn with the shape's full
transform, so every label on a flipped arrow came out mirrored (flipH) or mirrored upside down
(flipV). As in PowerPoint, the text rect keeps its flipped place and the text is mirrored back
about the rect's centre: flipH text stays upright, flipV (or both flips) turns it 180°, rotation
still applies, and the same holds for shapes inside flipped groups.
render::text_transform is that transform (plus vertical-text rotation). Drawing, the PDF text
layer and link boxes, morph text, the editing caret and selection, and click-to-place-the-caret
all use it, so a click lands on the character drawn under it.
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>
* Add OmaStore manifest
* OmaStore manifest: the release tarball keeps the binary in bin/, not usr/bin/
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Text with a:hlinkClick (to a URL or a slide) looked like the text around it. As in PowerPoint,
a linked run now takes the theme's hlink colour and a single underline, unless the run itself
sets a colour (PowerPoint 2019 and later honour it) or an underline (u="none" removes it).
Colours from list and master styles don't count, so links in titles are blue too.
This is part of run resolution, so links added with insert.hyperlink look the same, Format
shows them underlined, and turning underline off writes u="none". The run's own props are left
alone, so the look follows the theme.
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
tools/list described export as png/jpeg/pptx/outline only, with no PDF parameters, although
file.export (which the tool runs) writes PDF slides, notes pages and handouts. The tool's
description now names PDF, format is an enum of the formats file.export writes, and the schema
lists the PDF options: layout, perPage, dpi, slides, includeHidden, textLayer and frame.
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
On a slide change the show kept a clone of the old slide's texture handle
as the transition's "old" picture, then rendered the new slide into that
same texture with set(). Old and new were the same image, so every
transition (except Morph, which renders into its own texture) blended the
new slide with itself and looked like an instant cut. The old texture is
now taken out of show.tex, so the new slide gets a texture of its own.
generated with Claude Code
The hicolor theme had only PNGs. The SVG writes out the shapes of
icon-source.deckcraft with the same coordinates, colours and corner radii;
rendered at 1024 px it matches deckcraft-1024.png up to anti-aliasing at
the curved edges. The Linux, Flatpak and FreeBSD packages copy hicolor/
as a whole, so they ship it without further changes.
generated with Claude Code
dedup only removes consecutive duplicates, so two equal ids that were not
adjacent would have passed. Follow-up to the review note on #5.
generated with Claude Code
cpal pulls in alsa-sys, whose build script needs alsa.pc from the ALSA
development package; the other Linux -sys crates (wayland-sys with dlopen)
link nothing at build time, so this is the only extra package. CI already
installs libasound2-dev; the README now says so for people building locally.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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>
* Tables: rows grow to fit their text, cell text in the PDF text layer (#13)
A stored row height (a:tr/@h) is a minimum, as in PowerPoint: each row grows so its cells' text fits. Tables from pandoc or python-pptx store h="0" and drew every row on top of each other. Cell editing (click to edit, cell outline) uses the same heights, and the PDF text layer now includes table cell text.
* Tables: a grown table's box covers its drawn rows; resize scales from drawn heights
Handles, hit tests and marquee used the stored frame, so rows grown to fit their text drew
below the selection box (a file with h=0 rows had a 0 pt tall box). Resizing divided by the
stored row total, which is ~0 for those files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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>
eframe's default wgpu instance enables PRIMARY | GL on Windows. Creating
the OpenGL backend can crash inside AMD's atio6axx.dll before the window
appears, so the app exits silently at launch. When WGPU_BACKEND is not
set, the Windows instance now enables only DirectX 12; an explicit
WGPU_BACKEND is still honoured. macOS, Linux and web are unchanged.
Same fix as storytold/designcraft#168 and storytold/filmcraft#179.
Claude-Session: https://claude.ai/code/session_01LyVbT6Vv4BwWZ4hY8973qD
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The desktop app installed no logger, so every log::warn!/log::error! in
the workspace was discarded: the crash guard's panic report, a failed
crash-recovery save, PowerPoint parts skipped on import, audio output
problems. A launch from a desktop menu has no terminal either, so there
was nothing to attach to a bug report.
apps/deckcraft/src/logging.rs (no new dependency):
- standard error plus <settings dir>/logs/deckcraft.log (beside ui.json
and prefs.json); each launch rotates the previous log to
deckcraft.1.log and .2.log, so the log of a run that crashed survives
the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
(bounded) and written once the file is attached
- default levels: info for deckcraft*, warn for everything else;
RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
error as the only sink; a poisoned lock is tolerated
- a record holds the lock for one write of an already formatted line
The cpal error callback runs on the realtime audio thread (on ALSA in a
loop while the device stays broken), so it no longer logs: it keeps the
first error without blocking, and the UI thread logs it when the stream
stops.
The logger is installed first in main (before the engine's panic hook)
and the file is opened after argument parsing (--version leaves no
file). The settings directory moves out of prefs_path into config_dir.
The start-up eprintln!s (sample, files that fail to open, control
server) now go through the logger. Documented in README.md (Getting
started > Logs).
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>
The step clock ran before the frame's input actions were applied. A click
that started a step (PlayStep) was timed against the old step_start (when the
slide was entered), so after waiting longer than the step's duration the step
finished in the same frame: everything appeared at once, without animation.
Input actions are now applied before the clock is read.
generated with Claude Code
Exp-Golomb values read with `read_ue()` can be as large as u32::MAX, so
`read_ue()? + N`, `log2_min + read_ue()?` and the cropping / conformance
window arithmetic overflowed: a panic in debug builds, and in release a
silent wrap (e.g. pic_width_in_mbs_minus1 = u32::MAX became a width of 0,
which passed the `<= 1024` check).
- bitstream: add `BitReader::read_ue_plus(n)`, an overflow-checked
`ue(v) + n` that returns InvalidExpGolomb.
- h264/hevc: use it for every `read_ue()? + N`; saturate the log2 size
sums (the existing range checks then reject them); validate the H.264
crop in u64; make HEVC `crop_rect` saturating; validate the HEVC RPS
delta range before casting to i32.
- Regression tests for each (they fail on the previous code).