Repository navigation
Keynote Reader and Writer : Read and write the text and the images of a .key - #949
Merged
Progi1984 merged 1 commit intoSep 2, 2026
Conversation
… 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>
Progi1984
approved these changes
Aug 31, 2026
This was referenced Sep 2, 2026
3 of 4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
/claim #48Adds
Reader\KeynoteandWriter\Keynote, and registers the reader with the automatic resolution ofIOFactory. 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
.keypackage comes in:Keynote '13 and later store the deck as
Index/*.iwacomponents: 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 anint, 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— theTSP.ArchiveInfoheader 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(type2001) becomes aShape\RichText, a storage of kindNOTEgoes to$oSlide->getNote(), and a field whose bytes name a file theDatafolder 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(orindex.apxl.gz), read with XPath: the text bodies of the drawables,sf:data sf:pathfor the images,key:notesfor the note, andsf:geometryfor the position and the size.Images load as
Shape\Drawing\Base64and are skipped onSKIP_IMAGES. A.keywhich is a Zip archive but is of neither shape, a component whose frames are not Snappy, a malformed block, a truncated message, anindex.apxl.gzthat is not gzip: each is refused with anInvalidFileFormatExceptionnaming what was found, rather than read as an empty presentation.The writer writes the same set as the
index.apxlpackage 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.apxlroute 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:
Locally:
vendor/bin/phpunit(1058 tests, 49 of them Keynote),phpstanlevel 6,php-cs-fixer --dry-run,phpmd src/,tests/all clean.Note on the fixtures:
tests/resources/files/Keynote_09.keyandKeynote_13.keyare 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 realTSP.ArchiveInfo/TSWP.StorageArchivemessages. Glad to swap in a genuine export if you would rather test against one.