feat(ooxml.js): record a pptx slide's reading order as a rank per shape - #1199
Conversation
fcbwilliams
commented
Sep 10, 2026
|
For what it is worth, our own preference is to leave this opt-in rather than follow up by making it the default. The reason is the one the PR describes: making it default means reassigning So: no follow-up from us unless you would prefer the default. Entirely your call on what |
ae24338 to
ce6b282
Compare
`readPptxContent` returns `ContentSlide.shapes` in `p:spTree` order, which is z-order -- roughly creation order -- and bears no relation to layout. That is fine for a consumer rendering the shapes, since each carries its own frame and is positioned independently. It stops being fine the moment a consumer reads a slide as prose: concatenating `shapes` in array order puts a column of bullets ahead of the heading that owns them, which the same deck exported to PDF does not do (a PDF renderer has already resolved layout to reading order). `orderShapesForReading` recovers that order by recursive XY-cut over the frames already on every shape: find a band of empty space no shape straddles, take the groups either side in order, recurse, and fall back to topmost-then-leftmost where a set overlaps on both axes. The axis is chosen per cut rather than always cutting rows first, which is what keeps a two-column slide readable -- and the gap is compared *relative* to the extent the shapes occupy on each axis, because a 16:9 slide is twice as wide as it is tall, so an absolute comparison reads a four-box grid down its columns instead of across its rows. Both layouts are covered by tests. Deliberately NOT applied inside `readPptxContent`. `ContentShape`'s `sourcePath` is assigned during the shape-tree walk as `slides[N].shapes[N]`, and the existing sourcePath tests assert it matches the shape's own array position -- so reordering the array in place would either desynchronise those paths from the positions they name, or redefine sourcePath away from the document order its comment promises. Exported instead, so a consumer reading a slide as prose can sort while one correlating by sourcePath keeps the order it has. Happy to make it the default and reassign sourcePath afterwards if you would rather -- that is your call about what sourcePath means, which is why this PR does not make it. No behaviour change for any existing consumer.
`readPptxContent` returns `ContentSlide.shapes` in `p:spTree` order, which is z-order -- roughly creation order -- and bears no relation to layout. That is fine for a consumer rendering the shapes, since each carries its own frame. It stops being fine for one reading a slide as prose: in spTree order a column of bullets can precede the heading that owns them, which the same deck exported to PDF does not do, because a PDF renderer has already resolved layout to reading order. `ContentShape` gains an optional `readingOrder`, recovered from the shapes' own geometry by recursive XY-cut: find a band of empty space no shape straddles, take the groups either side in order, recurse, and fall back to topmost-then-leftmost where a set overlaps on both axes. **A rank on the shape, not a reordered array**, expressed exactly as `paintOrder` already is -- including the same plain `z.number()`, for the same reason its comment gives: a fractional value can be inserted between two existing ones later. `sourcePath` is assigned as `slides[N].shapes[N]` and has to keep naming the position it names, so sorting the array would either desynchronise every path or redefine sourcePath away from the document order its own comment promises. The array is returned untouched; a consumer wanting reading order sorts by the rank, and one that does not is unaffected. Two details that took measuring, both covered by tests: The axis is chosen per cut rather than always cutting rows first, which is what keeps a two-column slide readable -- where each column is a heading above its own list, cutting rows yields both headings then both lists. On a title-above-body slide the same comparison comes out the other way round. The gap is compared *relative* to the extent the shapes occupy on each axis, because a 16:9 slide is nearly twice as wide as it is tall: an absolute comparison reads a four-box grid down its columns instead of across its rows. Additive and optional throughout, so no existing consumer changes behaviour. `ShapeDescriptor`'s hand-authored JSON Schema fragment is updated to match; the `ContentVector` variants deliberately are not, since they carry `paintOrder` but have no reading order of their own.
ce6b282 to
e5d2153
Compare
|
🎉 This PR is included in version 7.10.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
|
🎉 This PR is included in version 8.12.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |