Commit Graph
858 Commits
Author SHA1 Message Date
Nikita RakitinandCursor 07dd513c54 Show: render the next slide in the background while the current one rests (#240)
Advancing rendered the new slide on the UI thread at full screen size
before anything changed on screen. While a slide rests (no animation or
transition playing) the show now renders the next slide's first frame on
a background thread; entering that slide uploads the ready image instead
of rendering, waiting for the render if it is still running. Slides with
animated GIFs and the web build keep rendering on entry.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:11:38 +03:00
Nikita RakitinandCursor ede2ca69b6 Designer: layout ideas that rearrange the current slide (#314)
Design Ideas showed the current slide once per built-in theme, and picking
one ran design.theme on the whole presentation; with a large picture every
idea looked the same. The pane also re-rendered eight slides every frame.

- New commands design.ideas (query) and design.applyIdea {id, slide?}
  (cmd/ideas.rs). A slide with a picture gets full-bleed with the title on a
  band, picture left or right with the text beside it, and a framed picture;
  a text slide gets a title column, a centred title and a title at the foot.
  An idea moves the slide's own shapes, crops the picture to cover its new
  box, sends it to the back, and changes only that slide (one undo step).
- The pane shows the layout ideas first, then the themes under a "whole
  presentation" heading, and caches the thumbnails until the deck revision,
  slide or pane width changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:10:59 +03:00
Nikita RakitinandCursor c7a4930c4c UI: open presentations in the background with a progress window (#350)
Opening from the file picker or the Backstage recent list read and parsed
the file on the UI thread, so a large PPTX froze the window. The read now
runs on a thread (engine: read_presentation_file / add_presentation_file),
and a modal shows the file name, elapsed time and Cancel; Escape cancels
too. A cancelled read is dropped when it finishes. Scripted opens (control,
MCP, CLI) and the web build stay synchronous.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:08:47 +03:00
Nikita RakitinandCursor 7d23e4670b Charts: pie and doughnut data labels use the number format (#324)
Pie and doughnut data labels still went through fmt_num, which rounded
anything non-integral to one decimal (999.99 drew as "1000.0"). They now
use Chart::data_label like the other label paths (series format, else
axis format, else General), and fmt_num is gone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:01:43 +03:00
Nikita RakitinandCursor 6b071aa053 Video: check the whole picture store against a memory budget (#398)
Per-axis size caps let AV1 accept 65536x65536 frames (over 8 GiB of luma)
and did not count references or pictures in flight. A shared budget module
in deckcraft-bitstream sizes a picture with checked arithmetic, and the
AV1, VP9, HEVC and H.264 decoders refuse a picture size whose current
picture, reference slots and in-flight pictures would exceed 4 GiB (1 GiB
on 32-bit), before allocating. set_memory_budget lowers it for hosts and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:58:31 +03:00
Nikita RakitinandCursor 5d4bfc0846 Media: keep playback with the document and the clip
Shape ids are local to one presentation, and decoded audio was cached by the
address of a buffer the cache did not keep. Switching decks, closing the last
one, or replacing a clip could keep the previous sound. Playback now stops
when the document changes or closes, a new source replaces the voice's audio,
and the decode and duration caches hold the bytes they are keyed by.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:57:08 +03:00
Nikita RakitinandCursor 241ecd59d7 Recovery: unique uids, per-instance ownership, keep snapshots until replaced (#390)
Document uids started at 1 in every process, so two instances sharing
the recovery folder overwrote and deleted each other's entries, and
file.recovery.open deleted each entry as soon as it was loaded, leaving
nothing if the app died before the next checkpoint.

- Uids are a random per-process base plus a counter (below 2^53).
- Each instance holds a lock on owner-<id>.lock in the folder; entries
  record their owner, and recovery open skips a running owner's entries
  and removes stale lock files.
- Opening an entry no longer deletes it: the recovered presentation keeps
  the entry's uid and takes ownership, so the snapshot stays until the
  next recovery save replaces it or a save/close removes it.
- Entry files and metadata are both written via temp file + rename.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:56:21 +03:00
Nikita RakitinandCursor 1fe638bcf8 AV1: reject duplicate film-grain scaling points; a failed job still publishes (#394)
The film-grain header accepted scaling points whose values didn't strictly
increase, and building the scaling lookup then divided by zero. In threaded
decoding the panic left the frame's reference and picture slots unset, so
the output and dependent frames waited forever.

- The header rejects Y/Cb/Cr point values that don't strictly increase
  (spec 6.8.20).
- The scaling lookup moves into scaling_lut, which skips a non-increasing
  pair and uses checked indexing (defence in depth).
- run_job decodes inside guarded (catch_unwind), so a panic becomes an error
  published to the picture and reference slots.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:55:12 +03:00
Nikita RakitinandCursor d2066a1f07 Pptx: budget chart points, chart XML size, inflate work and VML lookups (#401)
- 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>
2026-10-13 20:53:49 +03:00
Nikita RakitinandCursor 8bab5c2053 Web: keep a picked file on the document and slide that asked for it
The browser file picker only queued a name and bytes. When the read
finished, DeckCraft guessed the command and ran it on whichever
document and slide were active, so a picture, sound or outline could
land in the wrong place, and a later pick could finish first.

The request now keeps the document, the slide or master, the operation
and a cancellation id. A completion applies there, or is dropped when
that target is gone or the picker was dismissed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:53:10 +03:00
Nikita RakitinandCursor 201ab63c48 Render: cap RGBA conversion and bound the failed-image cache (#402)
decode_at accepted up to 400 million pixels and then called to_rgba8, which
the decoder's allocation limit doesn't cover (a grey image converts to four
times its decoded size), and every picture that failed to decode was kept,
bytes and all, for the life of the process.

- Raster decodes check the RGBA size (512 MiB cap) from the header before
  decoding and again before the conversion.
- The failed-source cache keeps at most 256 entries and 64 MiB of encoded
  bytes, oldest forgotten first; a larger failed input isn't kept, and a hit
  must be the same Arc.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:52:20 +03:00
Nikita RakitinandCursor cb26e7a04f Text: bidi levels for language runs in linear time (#403)
Currency and percent signs in right-to-left runs looked ahead through the
whole ET run for every sign, and each neutral span searched both ways for
the nearest strong direction. Both are now precomputed or carried along, so
a long run of signs or many short neutral spans cost linear work. The old
code stays in the tests as a reference and 4000 random paragraphs match it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:52:15 +03:00
Nikita RakitinandCursor ce878ef74e Render: budget shape path-gradient work on dense outlines (#404)
A shape path gradient tested every raster pixel against every flattened
outline edge, so an outline with tens of thousands of segments cost
billions of tests and stalled rendering. The outline is now simplified
before rasterising so pixels x edges stays under 64M: points closer than
a raster pixel are dropped first, then every k-th point is kept, with
each polygon left closed and at least 8 edges.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:49:02 +03:00
Nikita RakitinandCursor 272ab5f96f Edit: copies remap media, connector ends, animation targets and triggers (#406)
Paste added clashing media under a fresh id but left the pasted shapes on the
old id, reid did not touch connector ends despite its comment, pasted slides
kept animations on the original shape ids, and slide.duplicate remapped
animation targets but not triggers.

- reid returns the old-to-new shape map; connector ends on a copied shape
  follow the copy, ends on a shape outside the copied set are detached.
- remap_animations points targets and triggers at the copies (an animation
  whose target wasn't copied is dropped, an uncopied trigger cleared); used
  by slide paste and slide.duplicate.
- bring_media returns the media ids it had to change and paste rewrites the
  shapes (and a pasted slide's picture background) to use them; slide copy
  now also carries a picture background's media.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:47:27 +03:00
Nikita RakitinandCursor fb2a83edb6 Table: inserting and deleting rows and columns keeps merges consistent (#408)
Row and column insert/delete ignored merged cells, leaving covered cells
with no anchor or new cells overlapping an existing span. These edits now
read the merged areas first, shift or resize each one (a row or column
inserted inside a merge widens it, one deleted from it narrows it), move
the anchor and its text to the next covered cell when the anchor row or
column is deleted, and rebuild spans and merge flags from the result.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:44:37 +03:00
Nikita RakitinandCursor de098cfa52 Anim: group animations by sequence in one pass (#410)
Timeline::with found trigger sequences with Vec::contains and then rescanned
every animation once per sequence, and checked MAX_EFFECTS only after a
match, so a slide with many distinct triggers cost quadratic work before the
effect cap could stop it.

Animations are now grouped once (an index by sequence, first-appearance order
kept, main first), the build stops at MAX_EFFECTS before the next sequence,
and paragraph groups are deduplicated with a set instead of Vec::contains.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:39:57 +03:00
Nikita RakitinandCursor 952ebad3cf Media: a failed or stalled audio output hands the clock back to wall time (#417)
A cpal stream error was only kept and logged on stop, and the player kept
`streaming` true, so a device that died mid-play froze the voice and video.

AudioOut gains `failure()` (CpalOut reports DeviceNotAvailable), and
Player::tick also treats a stream with no callbacks for a second while a
voice plays as stalled. Either way the stream is stopped, voices continue on
the wall clock, and the next play retries the device; after three failures
the player gives up on the device and plays silently.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:27:21 +03:00
Nikita 6873a25635 Codecs: a hostile number is a format error, not an overflow
H.264 and HEVC quantiser offsets, Matroska block times, Opus granule timing and QuickTime PCM frame sizes are range-checked before the arithmetic.
2026-10-13 20:16:51 +03:00
Nikita 1be57fee4b H.264: bound frame_num gap work and reject gaps the SPS forbids (#427) 2026-10-13 19:59:48 +03:00
ZetenypappandClaude Opus 5.5 1baf1f5569 Copy and Cut buttons and menus put the text on the system clipboard (#431)
Ctrl+C and Ctrl+X ran edit.copy / edit.cut and handed the returned text
to egui's clipboard; the ribbon's Copy and Cut, the slide's right-click
menu, the menus, the palette and the control channel ran the same
commands through SlideApp::run, which dropped the text, so it never left
DeckCraft. SlideApp::run now keeps the text of a successful Copy or Cut
and hands it to the clipboard on the next frame, like pending URLs.

Regression test: copy_and_cut_commands_reach_the_system_clipboard runs
both commands and checks the frame's CopyText output (it fails without
the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:45:38 +02:00
Sazid Al Bayazid 8ba97e61be Slide show: Honeycomb's hexagons cover the whole slide again (#420)
The row count came from 1/(1.5*rh) - the row spacing alone - and
ignored how far a settled hexagon actually reaches. On a 16:9 slide
the eleven rows stopped at ~75% of the height: from t~0.9, when every
cell has settled, the bottom quarter was plain black until the t=1
endpoint snapped to the full new slide.

The count now derives from the real geometry: rows sit 0.866*rh apart
and the flat bottom zone of a settled hexagon reaches 0.289*rh past
its centre, so counting to the flat-zone reach covers the slide for
every aspect ratio - counting to the vertex reach still left small
V-notches of black above the bottom edge on about a third of them
(US Letter, square, ...). 16:9 gains four rows (15, was 11), 4:3
seven (20, was 13).

Tests: at t=0.99, when every cell is settled and the seams are nearly
closed, a 0.02-step grid over the slide must sit inside settled
incoming cells (padded 1% past the hairline seams) on five aspect
ratios including US Letter and square. Fails on the old count at
(0.2, 0.74); before/after frames rendered with render-transition.
2026-10-13 21:58:25 +06:00
MarekSpodBiedry 007741718e Slide: leading tabs in a new slide's body set the outline level (#370) 2026-10-13 17:52:00 +02:00
MarekSpodBiedry 97855c70a0 File: save through a unique, exclusively created temporary file (#391) 2026-10-13 17:48:28 +02:00
MarekSpodBiedry 09b72e4b70 Pptx: keep the math namespace equations inherit from the slide root or a wrapper (#412) 2026-10-13 17:41:28 +02:00
MarekSpodBiedry 902edb3c85 UI: notes, alt text and rename drafts no longer carry over to another document (#413) 2026-10-13 17:40:31 +02:00
MarekSpodBiedry d4cf50dd3d MCP: do not resend a command whose reply was lost (#409) 2026-10-13 17:38:28 +02:00
MarekSpodBiedry 33ade63687 Table: split and cell fill stay inside the table (#400) 2026-10-13 17:38:20 +02:00
MarekSpodBiedry 45b8dce220 Info: keep a property draft when another field or the side menu is clicked (#422) 2026-10-13 17:38:13 +02:00
MarekSpodBiedry 27d73db0d9 Engine: a command that fails after editing puts the presentation back (#405) 2026-10-13 17:38:10 +02:00
MarekSpodBiedry 863f7cbd87 Engine: moving the selection to another text body settles the composition (#421) 2026-10-13 17:37:44 +02:00
MarekSpodBiedry a4a7ebe658 Bitstream: reject bad NAL length widths, check NAL endpoints and signed Exp-Golomb limits (#397) 2026-10-13 17:37:25 +02:00
MarekSpodBiedry 9aebc8ba51 AV1: reject tile groups that repeat, skip or reorder tiles (#395) 2026-10-13 17:37:24 +02:00
MarekSpodBiedry efb2127e4e Format scientific number formats (0.00E+00) in chart labels (#419) 2026-10-13 17:36:03 +02:00
MarekSpodBiedry c623b4f555 Matroska: stop following chapter and tag nesting past 32 levels (#396) 2026-10-13 17:35:58 +02:00
NikitaandCursor 0b35b74c4d Show: a laser pointer that leaves no ink
Cmd/Ctrl+L and the on-screen button draw the show's laser colour at the pointer. The pen and the laser are not on together, and a click does not advance the slide while the laser is up.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 18:35:32 +03:00
MarekSpodBiedry 3ed12de918 Opus: validate the header in Decoder::from_head (#425) 2026-10-13 17:33:29 +02:00
NikitaandCursor 525b7ed3f9 Palette: skip commands that need a value
Searching for Export ran file.export with an empty object and reported a missing path. The palette now offers only commands that accept that call, so Export opens the dialog.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 18:31:02 +03:00
NikitaandCursor 82dcd86c88 Pptx: read and write embedded fonts
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>
2026-10-13 18:22:48 +03:00
Sazid Al Bayazid e70fce996b Model: an exhausted id space refuses growth instead of duplicating ids (#407)
alloc_id bumped next_id with saturating_add: a presentation whose
next_id reached u32::MAX (a hostile file can arrive that way;
fix_next_id imports it) handed out that same id to every new slide,
shape, layout and media - duplicate ids, ambiguous references, silent
corruption on every later insert.

alloc_id now returns Option<u32> via checked_add, and every caller
refuses: the engine's insert/duplicate/media/layout commands answer
with a clear error naming the command, paste and duplicate re-id
helpers propagate it, media addition is fallible, and a fresh slide is
Option. The pptx export's synthesized blank layout is guarded the same
way. Loading keeps such a deck viewable: repair re-ids duplicate
slides while it can, and a duplicate it cannot re-id stays as it is -
growth is refused, and only that duplicate keeps its ambiguous id.

Renumbering the loaded graph was rejected: it needs the
cross-document reference remapping that issue #406 tracks.

Tests: model exhaustion (alloc_id None, fresh media refused, a new
slide refused), engine (slide.new, shape.insert, slide.duplicate and
edit.duplicate all refuse; the document holds no duplicate ids),
format repair (an unre-id-able duplicate stays visible).
2026-10-13 21:13:25 +06:00
NikitaandCursor 4fee02636a Metafile: clip EMF and WMF drawing to rectangle regions
INTERSECTCLIPRECT, EXCLUDECLIPRECT and EXTSELECTCLIPRGN limit what later records paint, including a region made of several rectangles. A rotated map stores the bounds of the clip rectangle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 17:57:34 +03:00
NetterSilver 7887a13868 Colour menus: an Eyedropper that takes a colour from the slide (#367)
Every colour menu ends with Eyedropper. Choosing it closes the menu; the pointer is a
crosshair over the slide with the colour under it and its hex beside it, a click takes
the colour (slide.colorAt) and Esc, a right click or a click off the slide cancels.
The colour comes back through the menu that asked, which opens for a frame and returns
it as if its swatch had been clicked, so Shape Fill, Shape Outline, Font Color, the
Format pane and the other callers all apply it their own way without changes.
2026-10-13 16:41:57 +02:00
NetterSilver 35312529e1 slide.colorAt: the colour the slide is drawn with at a point (#367)
A read-only command, {slide?, x, y (slide units), scale?}: the slide is rendered and
the pixel at the point is returned as {color: "RRGGBB", alpha, x, y}. This is what the
Eyedropper takes, and what scripts and MCP use to read a colour off a slide. A point
outside the slide, a missing or non-numeric coordinate or an unknown slide is an error.
2026-10-13 16:41:57 +02:00
Sazid Al Bayazid 78a99ad1cf Start screen: a missing Recent file is marked, its error shows, and entries can be removed (#172)
The start screen reported errors through set_status, but the status bar
is only drawn while a presentation is open: clicking a Recent entry
whose file had moved or been deleted did nothing visible, and the same
went for every other start-screen error, like a failed sample open.

The start screen now draws the transient status message itself: a real
label, so screen readers see it too, wrapped so a long OS error does
not clip at the panel edge. Recent entries whose file no longer exists
are dimmed with a "(missing)" mark before anything is clicked, each
entry has a remove button, and a successful open clears a stale error.

Existence is probed once per path per session; a stat on a dead network
share can block for the OS timeout, so UNC paths and URLs are never
probed and are judged when opened instead.

The remove button is a plain multiplication sign: U+2715 has no glyph
in the UI font stack and egui would draw the replacement character.

Tests: the missing mark and the drawn error (kittest, clicked through
the real UI), removal from the list, and the negative case - an
existing file is not marked.
2026-10-13 19:43:15 +06:00
Sazid Al Bayazid 69d4893689 Comments: threads and resolved state survive a .pptx round trip (#70)
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.
2026-10-13 19:25:04 +06:00
Brandon ThomasandClaude Opus 5.5 8ee082f257 Render: 3-D shapes' faces are lit even without a bevel
A shape with scene3d and sp3d but no bevelT was drawn in its flat fill
colour. PowerPoint lights the front face from the light rig whether or
not there is a bevel, so a plain 3-D rotated rectangle is lighter than
its fill. Model the face with the same threePt approximation as the
bevel (no rim), for solid fills.

Wild 3-D slides: s8 6.35 -> 4.26, s9 19.74 -> 17.68. Corpus unchanged
except lo tdf114848 s4 2.13 -> 1.94.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 08:16:41 -04:00
Brandon ThomasandClaude Opus 5.5 238157d98e Render: 3-D cameras turn shapes (affine approximation)
A shape seen through a 3-D camera preset (isometric, perspective...) or
turned with a:camera/a:rot drew flat and front-on. PowerPoint turns the
shape's plane in space and projects it. Fitted black-box to its exports
(a picture with coloured corner markers under every preset and several
rotations; every fit within half a point on a 100 x 75 pt shape): points
about the centre are turned Rz(-rev) Rx(lat) Ry(lon), perspective cameras
then dividing by D / (D - z) with D about 5-16 shape sizes; a:rot turns
the other way about the horizontal and vertical axes. DeckCraft now draws
the shape under the affine transform that best fits its turned corners:
exact for orthographic cameras, within 2% of the size for perspective
ones (an affine map can't foreshorten). Oblique cameras leave a flat
shape as it is; depth (extrusion) is not drawn.

Wild synthetic set: camera presets 15.02 -> 6.35, 3-D picture styles
16.00 -> 7.72, extruded cameras 21.41 -> 19.74.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 08:16:41 -04:00
Brandon ThomasandClaude Opus 5.5 4e2cbaed79 Geom: moon, magnetic disk and drum by the standard
- The magnetic disk's and drum's end ellipses were half the size of
  PowerPoint's: ECMA-376 makes them h/6 high (disk) and w/6 across
  (drum); measured on PowerPoint's outlines.
- The moon's inner edge was a half ellipse of the box's height; the
  standard's is an arc of a larger ellipse through the right corners and
  (adj * w, middle) whose centre lies right of that point (radius g18w
  across, dy1 down), and so it is fuller. Text rectangle from the standard
  too.

Wild synthetic set: preset galleries 3.76 -> 3.47, 3.45 -> 3.21,
4.31 -> 4.00 and others slightly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 08:16:41 -04:00
Brandon ThomasandClaude Opus 5.5 ee71879f80 Render: pictures reflect their pixels
A reflection on a picture (PowerPoint's "reflected" picture styles) drew
nothing: the reflection painted the shape's fill, and a picture has none.
It now mirrors the picture itself, clipped to its outline, under the same
fade.

Wild synthetic set: picture effects slide 16.77 -> 16.00 (the rest is
3-D camera rotation, not modelled).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 08:16:41 -04:00
Brandon ThomasandClaude Opus 5.5 7cb62d5197 Pptx/Render: picture fills with rotWithShape=0 stay upright
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>
2026-10-13 08:16:41 -04:00
Brandon ThomasandClaude Opus 5.5 c0dcfc1db5 Geom: stars stretched by the standard's hf/vf factors
Five-, six-, seven- and ten-pointed stars used their own normalisation
and came out narrower or shifted against PowerPoint's. ECMA-376 places a
star's points on an ellipse around (hc, vc * vf) with radii half the box
times hf / vf (star5 1.05146 / 1.10557, star6 1.15470, star7 1.02572 /
1.05210, star10 1.05146), so the points touch the box's edges; now done
for all stars (checked exactly against PowerPoint's outlines at three
sizes and inner-radius adjusts).

Wild synthetic set: star galleries 4.08 -> 3.76, 3.77 -> 3.45,
4.62 -> 4.31 and the adjust galleries slightly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 08:16:41 -04:00