Skip to content

feat(ooxml.js): record a pptx slide's reading order as a rank per shape - #1199

Open
fcbwilliams wants to merge 2 commits into
mainfrom
feat/pptx-reading-order
Open

feat(ooxml.js): record a pptx slide's reading order as a rank per shape#1199
fcbwilliams wants to merge 2 commits into
mainfrom
feat/pptx-reading-order

Conversation

@fcbwilliams

Copy link
Copy Markdown
Member

`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.
@fcbwilliams

Copy link
Copy Markdown
Member Author

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 sourcePath after the sort, and sourcePath then stops being the document-order-derived path its own comment promises. That trade only pays off if most consumers want a reordered array — and we are one consumer, calling it explicitly, which the export already serves.

So: no follow-up from us unless you would prefer the default. Entirely your call on what sourcePath means; this PR is complete either way.

`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.
@fcbwilliams
fcbwilliams force-pushed the feat/pptx-reading-order branch from ae24338 to ce6b282 Compare September 10, 2026 15:21
@fcbwilliams fcbwilliams changed the title feat(ooxml.js): opt-in reading-order projection for pptx slides feat(ooxml.js): record a pptx slide's reading order as a rank per shape Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant