Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changes/1.3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
## Enhancements
- `phpoffice/phpspreadsheet`: Allow version 5.0 by [@seanlynchwv](http://github.com/seanlynchwv) in [#879](https://github.com/PHPOffice/PHPPresentation/pull/879)
- Raised the minimum PHP version to 7.4 (dropped 7.1, 7.2 and 7.3) by [@slayerfx](http://github.com/slayerfx) in [#894](https://github.com/PHPOffice/PHPPresentation/pull/894)
- Keynote: Add basic reader and writer for the Apple Keynote (.key) format by [@yasumorishima](https://github.com/yasumorishima) in [#891](https://github.com/PHPOffice/PHPPresentation/pull/891)

## Bug fixes
- Guarded `imagedestroy()` calls behind a PHP version check (no-op since PHP 8.0, deprecated in PHP 8.5) by [@slayerfx](http://github.com/slayerfx) in [#893](https://github.com/PHPOffice/PHPPresentation/pull/893)
Expand Down
16 changes: 16 additions & 0 deletions docs/usage/readers.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ $reader = new PowerPoint2007();
$reader->load(__DIR__ . '/sample.pptx', PowerPoint2007::SKIP_IMAGES);
```

## Keynote
The name of the reader is `Keynote`.

This reader provides basic support for the legacy iWork '09 Keynote format, where
the presentation is stored as an `index.apxl` XML document inside the `.key`
package. It reads slides, text placeholders (paragraphs and runs), shape geometry
and speaker notes. The modern IWA binary format used by Keynote '13 and later is
not supported and is rejected by `canRead()`.

``` php
<?php

$reader = IOFactory::createReader('Keynote');
$reader->load(__DIR__ . '/sample.key');
```

## Serialized
The name of the reader is `Serialized`.

Expand Down
16 changes: 16 additions & 0 deletions docs/usage/writers.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ $writer->setZipAdapter(new PclZipAdapter());
$writer->save(__DIR__ . '/sample.pptx');
```

## Keynote
The name of the writer is `Keynote`.

The writer produces a `.key` package following the legacy iWork '09 structure
(an `index.apxl` document). It writes slides, text placeholders (paragraphs and
runs), shape geometry and speaker notes. The output is round-trippable with the
`Keynote` reader; opening it directly in a current Keynote.app is not guaranteed
because of the format generation gap.

``` php
<?php

$writer = IOFactory::createWriter($oPhpPresentation, 'Keynote');
$writer->save(__DIR__ . '/sample.key');
```

## Serialized
The name of the writer is `Serialized`.

Expand Down
2 changes: 1 addition & 1 deletion src/PhpPresentation/IOFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class IOFactory
*
* @var array<int, string>
*/
private static $autoResolveClasses = ['Serialized', 'ODPresentation', 'PowerPoint97', 'PowerPoint2007'];
private static $autoResolveClasses = ['Serialized', 'ODPresentation', 'PowerPoint97', 'PowerPoint2007', 'Keynote'];

/**
* Create writer.
Expand Down
228 changes: 228 additions & 0 deletions src/PhpPresentation/Reader/Keynote.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
<?php

/**
* This file is part of PHPPresentation - A pure PHP library for reading and writing
* presentations documents.
*
* PHPPresentation is free software distributed under the terms of the GNU Lesser
* General Public License version 3 as published by the Free Software Foundation.
*
* For the full copyright and license information, please read the LICENSE
* file that was distributed with this source code. For the full list of
* contributors, visit https://github.com/PHPOffice/PHPPresentation/contributors.
*
* @see https://github.com/PHPOffice/PHPPresentation
*
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/

declare(strict_types=1);

namespace PhpOffice\PhpPresentation\Reader;

use DOMElement;
use PhpOffice\Common\XMLReader;
use PhpOffice\PhpPresentation\Exception\FileNotFoundException;
use PhpOffice\PhpPresentation\Exception\InvalidFileFormatException;
use PhpOffice\PhpPresentation\PhpPresentation;
use PhpOffice\PhpPresentation\Shape\RichText;
use PhpOffice\PhpPresentation\Slide;
use ZipArchive;

/**
* Reader for the Apple Keynote (.key) presentation format.
*
* This reader targets the legacy iWork '09 single-file structure, where the
* presentation is described by an `index.apxl` XML document stored inside the
* Keynote package (a Zip archive). The modern IWA binary format (Keynote '13 and
* later) is intentionally not handled and is safely rejected by canRead().
*/
class Keynote implements ReaderInterface
{
/**
* Keynote namespace.
*
* @var string
*/
public const NS_KEY = 'http://developer.apple.com/namespaces/keynote2';

/**
* iWork shared namespace.
*
* @var string
*/
public const NS_SF = 'http://developer.apple.com/namespaces/sf';

/**
* iWork shared abstract namespace.
*
* @var string
*/
public const NS_SFA = 'http://developer.apple.com/namespaces/sfa';

/**
* Output object.
*
* @var PhpPresentation
*/
protected $oPhpPresentation;

/**
* Should the images be loaded?
*
* @var bool
*/
protected $loadImages = true;

/**
* Can the current reader read the file?
*/
public function canRead(string $pFilename): bool
{
return $this->fileSupportsUnserializePhpPresentation($pFilename);
}

/**
* Does a file support being read by this reader?
*/
public function fileSupportsUnserializePhpPresentation(string $pFilename = ''): bool
{
if (!file_exists($pFilename)) {
throw new FileNotFoundException($pFilename);
}

$oZip = new ZipArchive();
if (true === $oZip->open($pFilename)) {
$isApxl = is_array($oZip->statName('index.apxl'));
$oZip->close();

return $isApxl;
}

return false;
}

/**
* Loads a Keynote file.
*/
public function load(string $pFilename, int $flags = 0): PhpPresentation
{
if (!$this->fileSupportsUnserializePhpPresentation($pFilename)) {
throw new InvalidFileFormatException($pFilename, self::class);
}

$this->loadImages = !((bool) ($flags & self::SKIP_IMAGES));

return $this->loadFile($pFilename);
}

/**
* Loads the file content into a PhpPresentation object.
*/
protected function loadFile(string $pFilename): PhpPresentation
{
$this->oPhpPresentation = new PhpPresentation();
$this->oPhpPresentation->removeSlideByIndex();

$oXMLReader = new XMLReader();
if (false !== $oXMLReader->getDomFromZip($pFilename, 'index.apxl')) {
$oXMLReader->registerNamespace('key', self::NS_KEY);
$oXMLReader->registerNamespace('sf', self::NS_SF);
$oXMLReader->registerNamespace('sfa', self::NS_SFA);
$this->loadSlides($oXMLReader);
}

return $this->oPhpPresentation;
}

/**
* Reads every slide of the presentation.
*/
protected function loadSlides(XMLReader $oXMLReader): void
{
foreach ($oXMLReader->getElements('/key:presentation/key:slide-list/key:slide') as $oNodeSlide) {
if (!$oNodeSlide instanceof DOMElement) {
continue;
}
$oSlide = $this->oPhpPresentation->createSlide();
$this->loadShapes($oXMLReader, $oNodeSlide, $oSlide);
$this->loadNote($oXMLReader, $oNodeSlide, $oSlide);
}
}

/**
* Reads every text placeholder of a slide.
*/
protected function loadShapes(XMLReader $oXMLReader, DOMElement $oNodeSlide, Slide $oSlide): void
{
foreach ($oXMLReader->getElements('./key:drawables/key:body-placeholder', $oNodeSlide) as $oNodeShape) {
if (!$oNodeShape instanceof DOMElement) {
continue;
}
$oRichText = $oSlide->createRichTextShape();
$this->loadGeometry($oXMLReader, $oNodeShape, $oRichText);
$this->loadText($oXMLReader, $oNodeShape, $oRichText);
}
}

/**
* Reads the position and size of a shape.
*/
protected function loadGeometry(XMLReader $oXMLReader, DOMElement $oNodeShape, RichText $oRichText): void
{
$oNodePosition = $oXMLReader->getElement('./sf:geometry/sf:position', $oNodeShape);
if ($oNodePosition instanceof DOMElement) {
$oRichText->setOffsetX((int) $oNodePosition->getAttributeNS(self::NS_SFA, 'x'));
$oRichText->setOffsetY((int) $oNodePosition->getAttributeNS(self::NS_SFA, 'y'));
}

$oNodeSize = $oXMLReader->getElement('./sf:geometry/sf:size', $oNodeShape);
if ($oNodeSize instanceof DOMElement) {
$oRichText->setWidth((int) $oNodeSize->getAttributeNS(self::NS_SFA, 'w'));
$oRichText->setHeight((int) $oNodeSize->getAttributeNS(self::NS_SFA, 'h'));
}
}

/**
* Reads the paragraphs and runs of a text container.
*/
protected function loadText(XMLReader $oXMLReader, DOMElement $oNodeContainer, RichText $oRichText): void
{
$oNodeBody = $oXMLReader->getElement('./sf:text/sf:text-storage/sf:text-body', $oNodeContainer);
if (!$oNodeBody instanceof DOMElement) {
return;
}

$isFirstParagraph = true;
foreach ($oXMLReader->getElements('./sf:p', $oNodeBody) as $oNodeParagraph) {
if (!$oNodeParagraph instanceof DOMElement) {
continue;
}
$oParagraph = $isFirstParagraph
? $oRichText->getActiveParagraph()
: $oRichText->createParagraph();
$isFirstParagraph = false;

foreach ($oXMLReader->getElements('./sf:span', $oNodeParagraph) as $oNodeSpan) {
if (!$oNodeSpan instanceof DOMElement) {
continue;
}
$oParagraph->createTextRun((string) $oNodeSpan->nodeValue);
}
}
}

/**
* Reads the speaker note of a slide.
*/
protected function loadNote(XMLReader $oXMLReader, DOMElement $oNodeSlide, Slide $oSlide): void
{
$oNodeNote = $oXMLReader->getElement('./key:notes', $oNodeSlide);
if (!$oNodeNote instanceof DOMElement) {
return;
}

$oRichText = $oSlide->getNote()->createRichTextShape();
$this->loadText($oXMLReader, $oNodeNote, $oRichText);
}
}
Loading
Loading