Skip to content

Keynote Reader and Writer : Read and write the text and the images of a .key - #949

Merged
Progi1984 merged 1 commit into
PHPOffice:masterfrom
businessarshgoyal:devin/1788018267-keynote-reader-writer
Sep 2, 2026
Merged

Progi1984 merged 1 commit into
PHPOffice:masterfrom
businessarshgoyal:devin/1788018267-keynote-reader-writer

Conversation

@businessarshgoyal

Copy link
Copy Markdown
Contributor

Description

/claim #48

Adds Reader\Keynote and Writer\Keynote, and registers the reader with the automatic resolution of IOFactory. Scope is what the issue asks for: the text of every slide, its speaker note and the images the slide uses.

The reader reads both shapes an Apple .key package comes in:

  • Keynote '13 and later store the deck as Index/*.iwa components: Snappy compressed frames of Protobuf messages. Three small helpers read that container with no new dependency and no Apple schema:

    • Reader\Keynote\Snappy — Apple's own framing (a one byte type, a three byte little endian length, then a Snappy block; no stream identifier and no checksums), plus the block format itself (literals, copy-1/2/4, overlapping copies).
    • Reader\Keynote\Protobuf — the wire format only. A message is kept field number by field number, a varint as an int, anything length delimited as its bytes. That is all the text and the image file names need, and it avoids pulling in hundreds of generated Apple messages.
    • Reader\Keynote\IwaStream — the TSP.ArchiveInfo header of every archive of a component and the messages it announces, handed back as ['type' => int, 'payload' => string].

    Out of a slide component, the text of every TSWP.StorageArchive (type 2001) becomes a Shape\RichText, a storage of kind NOTE goes to $oSlide->getNote(), and a field whose bytes name a file the Data folder of the package actually holds becomes an image — which is what keeps a style name or a digest out of the result. Index.zip, which some versions wrap the components in, is read too.

  • Keynote '09 stores it as index.apxl (or index.apxl.gz), read with XPath: the text bodies of the drawables, sf:data sf:path for the images, key:notes for the note, and sf:geometry for the position and the size.

Images load as Shape\Drawing\Base64 and are skipped on SKIP_IMAGES. A .key which is a Zip archive but is of neither shape, a component whose frames are not Snappy, a malformed block, a truncated message, an index.apxl.gz that is not gzip: each is refused with an InvalidFileFormatException naming what was found, rather than read as an empty presentation.

The writer writes the same set as the index.apxl package Keynote '09 reads — writing the IWA shape would mean writing Apple's Protobuf schemas, which is far past text and images — so what this library writes, this reader reads back (Writer\KeynoteTest::testSaveAndLoad).

Compared with the two open PRs for this issue. #860 (@sparkybug) takes the IWA route with generated Protobuf classes and a Snappy dependency; #891 (@yasumorishima) takes the index.apxl route only, so a deck from any Keynote of the last decade is not read. This PR reads both shapes in one reader, adds no dependency (the ~400 lines of Snappy/Protobuf/IWA here replace both a Snappy extension and a generated schema), and covers the container itself with unit tests (SnappyTest, ProtobufTest, IwaStreamTest) next to the reader and writer tests, which is where CI went red on #860. Happy to close this in favour of either if the maintainers prefer their approach, or to fold the container helpers into #860.

Fixes #48

Checklist:

  • My CI is 🟢
  • I have covered by unit tests my new code (check build/coverage for coverage report)
  • I have updated the documentation to describe the changes
  • I have updated the changelog

Locally: vendor/bin/phpunit (1058 tests, 49 of them Keynote), phpstan level 6, php-cs-fixer --dry-run, phpmd src/,tests/ all clean.

Note on the fixtures: tests/resources/files/Keynote_09.key and Keynote_13.key are minimal packages written by hand for the tests (two slides, a note, two images) rather than files exported by Keynote, so that the repository carries no third-party document; the IWA one is built with real Snappy frames and real TSP.ArchiveInfo/TSWP.StorageArchive messages. Glad to swap in a genuine export if you would rather test against one.

… a .key

A `.key` could be neither read nor written, so a Keynote deck had to be exported to
PPTX by hand before this library could see it.

Added `Reader\Keynote` and `Writer\Keynote`, and registered the Reader with the
automatic resolution of `IOFactory`.

The Reader reads both shapes of the format :

- Keynote '13 and later store the deck as `Index/*.iwa` components, which are Snappy
  compressed frames of Protobuf messages. `Reader\Keynote\Snappy`,
  `Reader\Keynote\Protobuf` and `Reader\Keynote\IwaStream` read that container without
  a Protobuf dependency and without Apple's schemas : a message is kept field by field,
  which is all the text and the names of the image files ask for.
- Keynote '09 stores it as an `index.apxl` XML document (or `index.apxl.gz`), read with
  XPath.

Read out of a slide : the text of every text storage, the speaker note, and the images
the slide references, loaded as `Shape\Drawing\Base64` and skipped on `SKIP_IMAGES`. The
Writer writes the same set, as the `index.apxl` package Keynote '09 reads. A `.key` of
neither shape, or a component whose frames are not Snappy, is refused with an exception
naming what was found.

Co-Authored-By: business.arshgoyal@gmail.com <business.arshgoyal@gmail.com>
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 93.544% (+0.05%) from 93.494% — businessarshgoyal:devin/1788018267-keynote-reader-writer into PHPOffice:master

@Progi1984 Progi1984 added this to the 1.3.0 milestone Sep 2, 2026
@Progi1984
Progi1984 merged commit 44ebfc0 into PHPOffice:master Sep 2, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Reader / Writer for Keynote

3 participants