Skip to content

feat(iwork): elementary support for apple iwork, starting with .pages - #732

Open
andiwand wants to merge 7 commits into
mainfrom
feat/iwork-pages
Open

feat(iwork): elementary support for apple iwork, starting with .pages#732
andiwand wants to merge 7 commits into
mainfrom
feat/iwork-pages

Conversation

@andiwand

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Stages 1 and 2 of src/odr/internal/iwork/PLAN.md: the container comes apart, and a .pages opens as a text document and renders its body instead of coming back as the zip it is made of.

What lands

  • iwork_snappy — Apple's own .iwa framing (0x00 plus a little-endian 24-bit compressed length, repeated to EOF) plus Snappy block decompression. Stock Snappy stream framing is not present, so a conan dependency would be the wrong shape as well as an extra one in the wasm/android/apple builds.
  • iwork_protobuf — the wire format only. Apple has never published the .proto schemas, so a code generator would have nothing to generate; archives are read against hand-written accessors by field number.
  • iwork_archive — an .iwa is an object graph, not a tree. The component list comes from Index/Metadata.iwa (file names carry identifier suffixes, so globbing for them does not work), and objects resolve by identifier across components.
  • Three FileTypesiwork_pages, iwork_numbers, iwork_keynote, with rows in file_type_table.cpp and the three bindings that mirror the enum by ordinal. Only .pages detects and opens; the other two are classification-only, because which app wrote a package is read off its root archive type and neither has a fixture to pin that against.
  • Pages body text — paragraphs from the storage's paragraph run table rather than from splitting on \n, U+2028 as a line break, and the run tables' UTF-16 indices translated against the UTF-8 text.

What does not

Styles, page geometry, drawables, images, tables, Numbers, Keynote, encryption — stages 3 to 7 of the plan. src/odr/internal/iwork/AGENTS.md records what the two landed stages decided and why; PLAN.md marks them and the three places plan and code disagreed.

Testing

The container readers are covered by inline byte strings — hand-built Snappy blocks, varint edge cases, an unknown archive type that must be skipped rather than thrown on, truncated framing. The two committed .pages fixtures cover detection, the empty-body regression, the body text, and cross-component object resolution.

Full suite green; the only new reference output is output/pages/, pinned in test/data.cmake.

andiwand and others added 7 commits August 23, 2026 17:53
The framing an iWork package uses is Apple's own — a four-byte header per
block, `0x00` and a little-endian 24-bit compressed length — so stock Snappy
stream decoding does not apply and only the block decoder does. That is a
varint length plus literal and copy tags, which is less code than a dependency
would be and keeps it out of the wasm, android and apple builds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
An iWork archive is protobuf, but Apple has never published the `.proto`
schemas, so there is nothing for a code generator to generate and linking conan
`protobuf` would drag it into every downstream build to replace this. Only the
wire format is needed: varints, the three fixed and length-delimited forms, and
unknown fields carried along rather than dropped. A group means the parse went
wrong, so it throws.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
An `.iwa` is not a tree but a sequence of archived objects, each a
`TSP.ArchiveInfo` naming an identifier and the type of the messages that
follow. Objects reference each other by identifier across components, so the
package reads its component list from `Index/Metadata.iwa` first — the file
names carry identifier suffixes often enough that globbing for them finds
nothing — and decompresses a component when something in it is asked for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
`.pages`, `.numbers` and `.key` are zips, so today they are reported as `[zip]`
and open as an archive rather than a document. Naming them gives a caller the
extensions and MIME types to route one and hand a file picker, which is what
has to be decided before the file is held.

Classification only for now: which app wrote a package is read off its root
archive, and only `.pages` has a fixture to pin that against, so the rows
declare no capabilities and nothing detects or decodes one yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
A `.pages` package now opens as a text document rather than as the zip it is
made of, and its body comes out as paragraphs. Which app wrote the package is
read off the type of the root archive in `Index/Document.iwa` — the extension
is not consulted, since a caller may not have one.

Paragraph boundaries come from the storage's paragraph run table rather than
from splitting the text on `\n`, and `U+2028` inside a paragraph becomes a line
break. The anchor a drawable leaves in the text is dropped: styles, page
geometry, drawables, images and tables are all still to come, so this is the
text and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
`PLAN.md` was written before any of it existed; this is the module's `AGENTS.md`
alongside it — why a fixture is the citation here rather than a spec section,
why snappy and protobuf are in-tree, the `Message` lifetime the whole engine
rests on, and where the run tables sit. `PLAN.md` marks the two landed stages
and the three places the plan and the code disagreed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
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