From 5914e8ec3752f72ec6c9b372533986ee13954021 Mon Sep 17 00:00:00 2001 From: Sambit Chakraborty <58760654+Sambit003@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:14:09 +0530 Subject: [PATCH] Writer EPub3 : Added support (#2724) * Feat: Added Epub3 writer support * Add: Tests * Fix: Code convention * Update: composer.lock file with latest requirement * Update: composer.lock file with latest requirement * Update: composer.lock file with latest requirement * Remove: composer.lock * Update: Code conventions for PHP 8.x & Update: .gitignore to include composer.lock (in support of PR #2722 : Autoload) * Add: Text & Image Element * Improvement of Epub3 elements code * Add: Unit tests for full EPub3 codebase * Fix: Code convention errors for EPub3 Unit tests * Fix: Added the suggestions * Revert: composer.json changes -> Now again included the gd & zip extension * Update: composer.json * Add: Generating Epub samples with adherence to the Epub 3 checking procedures * Fix: Null type error in php8.x runtime * Update: Changelog --- .gitignore | 1 + composer.json | 4 +- docs/changes/1.x/1.4.0.md | 2 + samples/Sample_Header.php | 2 +- src/PhpWord/IOFactory.php | 2 +- src/PhpWord/Shared/ZipArchive.php | 11 ++ src/PhpWord/Writer/EPub3.php | 91 ++++++++++++ .../Writer/EPub3/Element/AbstractElement.php | 45 ++++++ src/PhpWord/Writer/EPub3/Element/Image.php | 45 ++++++ src/PhpWord/Writer/EPub3/Element/Text.php | 50 +++++++ src/PhpWord/Writer/EPub3/Part.php | 45 ++++++ .../Writer/EPub3/Part/AbstractPart.php | 56 ++++++++ src/PhpWord/Writer/EPub3/Part/Content.php | 130 ++++++++++++++++++ .../Writer/EPub3/Part/ContentXhtml.php | 117 ++++++++++++++++ src/PhpWord/Writer/EPub3/Part/Manifest.php | 40 ++++++ src/PhpWord/Writer/EPub3/Part/Meta.php | 74 ++++++++++ src/PhpWord/Writer/EPub3/Part/Mimetype.php | 33 +++++ src/PhpWord/Writer/EPub3/Part/Nav.php | 53 +++++++ .../Writer/EPub3/Style/AbstractStyle.php | 75 ++++++++++ src/PhpWord/Writer/EPub3/Style/Font.php | 39 ++++++ src/PhpWord/Writer/EPub3/Style/Paragraph.php | 39 ++++++ src/PhpWord/Writer/EPub3/Style/Table.php | 43 ++++++ src/PhpWord/Writer/WriterPartInterface.php | 10 ++ .../Writer/EPub3/Element/ImageTest.php | 69 ++++++++++ .../Writer/EPub3/Element/TextTest.php | 83 +++++++++++ .../PhpWordTests/Writer/EPub3/ElementTest.php | 26 ++++ .../Writer/EPub3/Part/AbstractPartTest.php | 28 ++++ .../Writer/EPub3/Part/ContentTest.php | 38 +++++ .../Writer/EPub3/Part/ManifestTest.php | 35 +++++ .../Writer/EPub3/Part/MetaTest.php | 53 +++++++ .../Writer/EPub3/Part/MimetypeTest.php | 32 +++++ .../Writer/EPub3/Part/NavTest.php | 49 +++++++ tests/PhpWordTests/Writer/EPub3/PartTest.php | 28 ++++ .../Writer/EPub3/Style/AbstractStyleTest.php | 24 ++++ .../Writer/EPub3/Style/FontTest.php | 25 ++++ .../Writer/EPub3/Style/ParagraphTest.php | 25 ++++ .../Writer/EPub3/Style/TableTest.php | 28 ++++ tests/PhpWordTests/Writer/EPub3/StyleTest.php | 23 ++++ tests/PhpWordTests/Writer/EPub3Test.php | 126 +++++++++++++++++ 39 files changed, 1695 insertions(+), 4 deletions(-) create mode 100644 src/PhpWord/Writer/EPub3.php create mode 100644 src/PhpWord/Writer/EPub3/Element/AbstractElement.php create mode 100644 src/PhpWord/Writer/EPub3/Element/Image.php create mode 100644 src/PhpWord/Writer/EPub3/Element/Text.php create mode 100644 src/PhpWord/Writer/EPub3/Part.php create mode 100644 src/PhpWord/Writer/EPub3/Part/AbstractPart.php create mode 100644 src/PhpWord/Writer/EPub3/Part/Content.php create mode 100644 src/PhpWord/Writer/EPub3/Part/ContentXhtml.php create mode 100644 src/PhpWord/Writer/EPub3/Part/Manifest.php create mode 100644 src/PhpWord/Writer/EPub3/Part/Meta.php create mode 100644 src/PhpWord/Writer/EPub3/Part/Mimetype.php create mode 100644 src/PhpWord/Writer/EPub3/Part/Nav.php create mode 100644 src/PhpWord/Writer/EPub3/Style/AbstractStyle.php create mode 100644 src/PhpWord/Writer/EPub3/Style/Font.php create mode 100644 src/PhpWord/Writer/EPub3/Style/Paragraph.php create mode 100644 src/PhpWord/Writer/EPub3/Style/Table.php create mode 100644 src/PhpWord/Writer/WriterPartInterface.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Element/ImageTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Element/TextTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/ElementTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/AbstractPartTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/ContentTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/ManifestTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/MetaTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/MimetypeTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/NavTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/PartTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Style/AbstractStyleTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Style/FontTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Style/ParagraphTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/Style/TableTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3/StyleTest.php create mode 100644 tests/PhpWordTests/Writer/EPub3Test.php diff --git a/.gitignore b/.gitignore index 0b9d0608d0..6918df72e6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ _build /build phpunit.xml composer.phar +composer.lock vendor /report /build diff --git a/composer.json b/composer.json index efebe941e7..5055105d16 100644 --- a/composer.json +++ b/composer.json @@ -108,10 +108,10 @@ "require": { "php": "^7.1|^8.0", "ext-dom": "*", - "ext-gd": "*", + "ext-gd": "*", + "ext-zip": "*", "ext-json": "*", "ext-xml": "*", - "ext-zip": "*", "phpoffice/math": "^0.2" }, "require-dev": { diff --git a/docs/changes/1.x/1.4.0.md b/docs/changes/1.x/1.4.0.md index f12bb05b0d..96b86a1bac 100644 --- a/docs/changes/1.x/1.4.0.md +++ b/docs/changes/1.x/1.4.0.md @@ -16,6 +16,8 @@ - Writer ODText: Support Default font color by [@MichaelPFrey](https://github.com/MichaelPFrey) in [#2735](https://github.com/PHPOffice/PHPWord/pull/2735) - Add basic ruby text (phonetic guide) support for Word2007 and HTML Reader/Writer, RTF Writer, basic support for ODT writing by [@Deadpikle](https://github.com/Deadpikle) in [#2727](https://github.com/PHPOffice/PHPWord/pull/2727) +- Added Support for Writer Epub3 by [@Sambit003](https://github.com/Sambit003) in [#2724](https://github.com/PHPOffice/PHPWord/pull/2724) + ### Bug fixes - Writer ODText: Support for images inside a textRun by [@Progi1984](https://github.com/Progi1984) fixing [#2240](https://github.com/PHPOffice/PHPWord/issues/2240) in [#2668](https://github.com/PHPOffice/PHPWord/pull/2668) diff --git a/samples/Sample_Header.php b/samples/Sample_Header.php index eab7033275..57bb10a4c6 100644 --- a/samples/Sample_Header.php +++ b/samples/Sample_Header.php @@ -31,7 +31,7 @@ } // Set writers -$writers = ['Word2007' => 'docx', 'ODText' => 'odt', 'RTF' => 'rtf', 'HTML' => 'html', 'PDF' => 'pdf']; +$writers = ['Word2007' => 'docx', 'ODText' => 'odt', 'RTF' => 'rtf', 'HTML' => 'html', 'PDF' => 'pdf', 'EPub3' => 'epub']; // Set PDF renderer if (null === Settings::getPdfRendererPath()) { diff --git a/src/PhpWord/IOFactory.php b/src/PhpWord/IOFactory.php index 55c374a8b9..50c419cae2 100644 --- a/src/PhpWord/IOFactory.php +++ b/src/PhpWord/IOFactory.php @@ -36,7 +36,7 @@ abstract class IOFactory */ public static function createWriter(PhpWord $phpWord, $name = 'Word2007') { - if ($name !== 'WriterInterface' && !in_array($name, ['ODText', 'RTF', 'Word2007', 'HTML', 'PDF'], true)) { + if ($name !== 'WriterInterface' && !in_array($name, ['ODText', 'RTF', 'Word2007', 'HTML', 'PDF', 'EPub3'], true)) { throw new Exception("\"{$name}\" is not a valid writer."); } diff --git a/src/PhpWord/Shared/ZipArchive.php b/src/PhpWord/Shared/ZipArchive.php index 3ee3869191..bce7f18e0f 100644 --- a/src/PhpWord/Shared/ZipArchive.php +++ b/src/PhpWord/Shared/ZipArchive.php @@ -423,4 +423,15 @@ public function pclzipLocateName($filename) return ($listIndex > -1) ? $listIndex : false; } + + /** + * Add an empty directory to the zip archive (emulate \ZipArchive). + * + * @param string $dirname Directory name to add to the zip archive + */ + public function addEmptyDir(string $dirname): bool + { + // Create a directory entry by adding an empty file with trailing slash + return $this->addFromString(rtrim($dirname, '/') . '/', ''); + } } diff --git a/src/PhpWord/Writer/EPub3.php b/src/PhpWord/Writer/EPub3.php new file mode 100644 index 0000000000..b2ed9700d1 --- /dev/null +++ b/src/PhpWord/Writer/EPub3.php @@ -0,0 +1,91 @@ +setPhpWord($phpWord); + + // Create parts + $this->parts = [ + 'Mimetype' => 'mimetype', + 'Content' => 'content.opf', + 'Toc' => 'toc.ncx', + 'Styles' => 'styles.css', + 'Manifest' => 'META-INF/container.xml', + 'Nav' => 'nav.xhtml', + 'ContentXhtml' => 'content.xhtml', + ]; + foreach (array_keys($this->parts) as $partName) { + $partClass = static::class . '\\Part\\' . $partName; + if (class_exists($partClass)) { + /** @var WriterPartInterface $part */ + $part = new $partClass($partName === 'Content' || $partName === 'ContentXhtml' ? $phpWord : null); + $part->setParentWriter($this); + $this->writerParts[strtolower($partName)] = $part; + } + } + + // Set package paths + $this->mediaPaths = ['image' => 'Images/', 'object' => 'Objects/']; + } + + /** + * Save PhpWord to file. + */ + public function save(string $filename): void + { + $filename = $this->getTempFile($filename); + $zip = $this->getZipArchive($filename); + + // Add mimetype first without compression + $zip->addFromString('mimetype', 'application/epub+zip'); + $zip->addEmptyDir('META-INF'); + + // Add other files + foreach ($this->parts as $partName => $fileName) { + if ($fileName === '') { + continue; + } + $part = $this->getWriterPart($partName); + if (!$part instanceof AbstractPart) { + continue; + } + $zip->addFromString($fileName, $part->write()); + } + + // Close zip archive + $zip->close(); + + // Cleanup temp file + $this->cleanupTempFile(); + } +} diff --git a/src/PhpWord/Writer/EPub3/Element/AbstractElement.php b/src/PhpWord/Writer/EPub3/Element/AbstractElement.php new file mode 100644 index 0000000000..c95efec0f6 --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Element/AbstractElement.php @@ -0,0 +1,45 @@ +getXmlWriter(); + $xmlWriter->setIndent(false); + $element = $this->getElement(); + if (!$element instanceof ImageElement) { + return; + } + $mediaIndex = $element->getMediaIndex(); + $target = 'media/image' . $mediaIndex . '.' . $element->getImageExtension(); + if (!$this->withoutP) { + $xmlWriter->startElement('p'); + } + $xmlWriter->startElement('img'); + $xmlWriter->writeAttribute('src', $target); + $style = ''; + if ($element->getStyle()->getWidth() !== null) { + $style .= 'width:' . $element->getStyle()->getWidth() . 'px;'; + } + if ($element->getStyle()->getHeight() !== null) { + $style .= 'height:' . $element->getStyle()->getHeight() . 'px;'; + } + if ($style !== '') { + $xmlWriter->writeAttribute('style', $style); + } + $xmlWriter->endElement(); // img + if (!$this->withoutP) { + $xmlWriter->endElement(); // p + } + } +} diff --git a/src/PhpWord/Writer/EPub3/Element/Text.php b/src/PhpWord/Writer/EPub3/Element/Text.php new file mode 100644 index 0000000000..2e138c0509 --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Element/Text.php @@ -0,0 +1,50 @@ +getXmlWriter(); + $xmlWriter->setIndent(true); + $xmlWriter->setIndentString(' '); + $element = $this->getElement(); + if (!$element instanceof \PhpOffice\PhpWord\Element\Text) { + return; + } + + $fontStyle = $element->getFontStyle(); + $paragraphStyle = $element->getParagraphStyle(); + + if (!$this->withoutP) { + $xmlWriter->startElement('p'); + if (is_string($paragraphStyle) && $paragraphStyle !== '') { + $xmlWriter->writeAttribute('class', $paragraphStyle); + } + } + + if (!empty($fontStyle)) { + $xmlWriter->startElement('span'); + if (is_string($fontStyle)) { + $xmlWriter->writeAttribute('class', $fontStyle); + } + } + + $xmlWriter->text($element->getText()); + + if (!empty($fontStyle)) { + $xmlWriter->endElement(); // span + } + + if (!$this->withoutP) { + $xmlWriter->endElement(); // p + } + } +} diff --git a/src/PhpWord/Writer/EPub3/Part.php b/src/PhpWord/Writer/EPub3/Part.php new file mode 100644 index 0000000000..25dfa6654d --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Part.php @@ -0,0 +1,45 @@ +parentWriter = $writer; + } + + /** + * Get parent writer. + */ + public function getParentWriter(): AbstractWriter + { + return $this->parentWriter; + } + + /** + * Write part content. + */ + abstract public function write(): string; +} diff --git a/src/PhpWord/Writer/EPub3/Part/Content.php b/src/PhpWord/Writer/EPub3/Part/Content.php new file mode 100644 index 0000000000..217c56cc1f --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Part/Content.php @@ -0,0 +1,130 @@ +phpWord = $phpWord; + } + + /** + * Get XML Writer. + * + * @return XMLWriter + */ + protected function getXmlWriter() + { + $xmlWriter = new XMLWriter(); + $xmlWriter->openMemory(); + $xmlWriter->startDocument('1.0', 'UTF-8'); + + return $xmlWriter; + } + + /** + * Write part content. + */ + public function write(): string + { + if ($this->phpWord === null) { + throw new Exception('No PhpWord assigned.'); + } + + $xmlWriter = $this->getXmlWriter(); + $docInfo = $this->phpWord->getDocInfo(); + + // Write package + $xmlWriter->startElement('package'); + $xmlWriter->writeAttribute('xmlns', 'http://www.idpf.org/2007/opf'); + $xmlWriter->writeAttribute('version', '3.0'); + $xmlWriter->writeAttribute('unique-identifier', 'book-id'); + $xmlWriter->writeAttribute('xml:lang', 'en'); + + // Write metadata + $xmlWriter->startElement('metadata'); + $xmlWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); + $xmlWriter->writeAttribute('xmlns:opf', 'http://www.idpf.org/2007/opf'); + + // Required elements + $xmlWriter->startElement('dc:identifier'); + $xmlWriter->writeAttribute('id', 'book-id'); + $xmlWriter->text('book-id-' . uniqid()); + $xmlWriter->endElement(); + $xmlWriter->writeElement('dc:title', $docInfo->getTitle() ?: 'Untitled'); + $xmlWriter->writeElement('dc:language', 'en'); + + // Required modified timestamp + $xmlWriter->startElement('meta'); + $xmlWriter->writeAttribute('property', 'dcterms:modified'); + $xmlWriter->text(date('Y-m-d\TH:i:s\Z')); + $xmlWriter->endElement(); + + $xmlWriter->endElement(); // metadata + + // Write manifest + $xmlWriter->startElement('manifest'); + + // Add nav document (required) + $xmlWriter->startElement('item'); + $xmlWriter->writeAttribute('id', 'nav'); + $xmlWriter->writeAttribute('href', 'nav.xhtml'); + $xmlWriter->writeAttribute('media-type', 'application/xhtml+xml'); + $xmlWriter->writeAttribute('properties', 'nav'); + $xmlWriter->endElement(); + + // Add content document + $xmlWriter->startElement('item'); + $xmlWriter->writeAttribute('id', 'content'); + $xmlWriter->writeAttribute('href', 'content.xhtml'); + $xmlWriter->writeAttribute('media-type', 'application/xhtml+xml'); + $xmlWriter->endElement(); + + $xmlWriter->endElement(); // manifest + + // Write spine + $xmlWriter->startElement('spine'); + $xmlWriter->startElement('itemref'); + $xmlWriter->writeAttribute('idref', 'content'); + $xmlWriter->endElement(); + $xmlWriter->endElement(); // spine + + $xmlWriter->endElement(); // package + + return $xmlWriter->outputMemory(true); + } +} diff --git a/src/PhpWord/Writer/EPub3/Part/ContentXhtml.php b/src/PhpWord/Writer/EPub3/Part/ContentXhtml.php new file mode 100644 index 0000000000..3ebd82638e --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Part/ContentXhtml.php @@ -0,0 +1,117 @@ +phpWord = $phpWord; + } + + /** + * Get XML Writer. + * + * @return XMLWriter + */ + protected function getXmlWriter() + { + $xmlWriter = new XMLWriter(); + $xmlWriter->openMemory(); + + return $xmlWriter; + } + + /** + * Write part content. + */ + public function write(): string + { + if ($this->phpWord === null) { + throw new \PhpOffice\PhpWord\Exception\Exception('No PhpWord assigned.'); + } + + $xmlWriter = $this->getXmlWriter(); + + $xmlWriter->startDocument('1.0', 'UTF-8'); + $xmlWriter->startElement('html'); + $xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $xmlWriter->writeAttribute('xmlns:epub', 'http://www.idpf.org/2007/ops'); + $xmlWriter->startElement('head'); + $xmlWriter->writeElement('title', $this->phpWord->getDocInfo()->getTitle() ?: 'Untitled'); + $xmlWriter->endElement(); // head + $xmlWriter->startElement('body'); + + // Write sections content + foreach ($this->phpWord->getSections() as $section) { + $xmlWriter->startElement('div'); + $xmlWriter->writeAttribute('class', 'section'); + + foreach ($section->getElements() as $element) { + if ($element instanceof TextRun) { + $xmlWriter->startElement('p'); + $this->writeTextRun($element, $xmlWriter); + $xmlWriter->endElement(); // p + } elseif (method_exists($element, 'getText')) { + $text = $element->getText(); + $xmlWriter->startElement('p'); + if ($text instanceof TextRun) { + $this->writeTextRun($text, $xmlWriter); + } elseif ($text !== null) { + $xmlWriter->text((string) $text); + } + $xmlWriter->endElement(); // p + } + } + + $xmlWriter->endElement(); // div + } + + $xmlWriter->endElement(); // body + $xmlWriter->endElement(); // html + + return $xmlWriter->outputMemory(true); + } + + protected function writeTextElement(\PhpOffice\PhpWord\Element\AbstractElement $textElement, XMLWriter $xmlWriter): void + { + if ($textElement instanceof Text) { + $text = $textElement->getText(); + if ($text !== null) { + $xmlWriter->text((string) $text); + } + } elseif (is_object($textElement) && method_exists($textElement, 'getText')) { + $text = $textElement->getText(); + if ($text instanceof TextRun) { + $this->writeTextRun($text, $xmlWriter); + } elseif ($text !== null) { + $xmlWriter->text((string) $text); + } + } + } + + protected function writeTextRun(TextRun $textRun, XMLWriter $xmlWriter): void + { + foreach ($textRun->getElements() as $element) { + $this->writeTextElement($element, $xmlWriter); + } + } +} diff --git a/src/PhpWord/Writer/EPub3/Part/Manifest.php b/src/PhpWord/Writer/EPub3/Part/Manifest.php new file mode 100644 index 0000000000..fcb0d1f428 --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Part/Manifest.php @@ -0,0 +1,40 @@ +'; + $content .= ''; + $content .= ''; + $content .= ''; + $content .= ''; + $content .= ''; + + return $content; + } +} diff --git a/src/PhpWord/Writer/EPub3/Part/Meta.php b/src/PhpWord/Writer/EPub3/Part/Meta.php new file mode 100644 index 0000000000..4b01097653 --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Part/Meta.php @@ -0,0 +1,74 @@ +openMemory(); + $xmlWriter->startDocument('1.0', 'UTF-8'); + + return $xmlWriter; + } + + /** + * Write part content. + */ + public function write(): string + { + $xmlWriter = $this->getXmlWriter(); + + $xmlWriter->startElement('metadata'); + $xmlWriter->writeAttribute('xmlns', 'http://www.idpf.org/2007/opf'); + $xmlWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); + + // Write basic metadata + $title = $this->getParentWriter()->getPhpWord()->getDocInfo()->getTitle() ?: 'Sample EPub3 Document'; + $xmlWriter->writeRaw('' . htmlspecialchars($title, ENT_QUOTES) . ''); + $xmlWriter->writeElement('dc:language', 'en'); + $xmlWriter->writeElement('dc:identifier', 'urn:uuid:12345'); + $xmlWriter->writeAttribute('id', 'bookid'); + + // Write document info if available + $docInfo = $this->getParentWriter()->getPhpWord()->getDocInfo(); + if ($docInfo->getCreator()) { + $xmlWriter->writeElement('dc:creator', $docInfo->getCreator()); + } + + // Write modification date + $xmlWriter->startElement('meta'); + $xmlWriter->writeAttribute('property', 'dcterms:modified'); + $xmlWriter->text('2023-01-01T00:00:00Z'); + $xmlWriter->endElement(); + + $xmlWriter->endElement(); // metadata + + return $xmlWriter->getData(); + } +} diff --git a/src/PhpWord/Writer/EPub3/Part/Mimetype.php b/src/PhpWord/Writer/EPub3/Part/Mimetype.php new file mode 100644 index 0000000000..8e5b7d41ba --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Part/Mimetype.php @@ -0,0 +1,33 @@ +openMemory(); + + return $xmlWriter; + } + + public function write(): string + { + $xmlWriter = $this->getXmlWriter(); + + $xmlWriter->startDocument('1.0', 'UTF-8'); + $xmlWriter->startElement('html'); + $xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $xmlWriter->writeAttribute('xmlns:epub', 'http://www.idpf.org/2007/ops'); + + $xmlWriter->startElement('head'); + $xmlWriter->writeElement('title', 'Navigation'); + $xmlWriter->endElement(); // head + + $xmlWriter->startElement('body'); + $xmlWriter->startElement('nav'); + $xmlWriter->writeAttribute('epub:type', 'toc'); + $xmlWriter->writeAttribute('id', 'toc'); + + // Add navigation items here if needed + $xmlWriter->writeElement('h1', 'Table of Contents'); + $xmlWriter->startElement('ol'); + // Add at least one list item to satisfy EPUB 3.3 requirements + $xmlWriter->startElement('li'); + $xmlWriter->startElement('a'); + $xmlWriter->writeAttribute('href', 'content.xhtml'); + $xmlWriter->text('Content'); + $xmlWriter->endElement(); // a + $xmlWriter->endElement(); // li + $xmlWriter->endElement(); // ol + + $xmlWriter->endElement(); // nav + $xmlWriter->endElement(); // body + $xmlWriter->endElement(); // html + + return $xmlWriter->outputMemory(); + } +} diff --git a/src/PhpWord/Writer/EPub3/Style/AbstractStyle.php b/src/PhpWord/Writer/EPub3/Style/AbstractStyle.php new file mode 100644 index 0000000000..82cd488b62 --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Style/AbstractStyle.php @@ -0,0 +1,75 @@ +parentWriter = $writer; + + return $this; + } + + /** + * Set XML Writer. + */ + public function setXmlWriter(XMLWriter $writer): self + { + $this->xmlWriter = $writer; + + return $this; + } + + /** + * Get parent writer. + */ + public function getParentWriter(): AbstractWriter + { + return $this->parentWriter; + } + + /** + * Write style content. + */ + abstract public function write(): string; +} diff --git a/src/PhpWord/Writer/EPub3/Style/Font.php b/src/PhpWord/Writer/EPub3/Style/Font.php new file mode 100644 index 0000000000..4e35eeee41 --- /dev/null +++ b/src/PhpWord/Writer/EPub3/Style/Font.php @@ -0,0 +1,39 @@ +xmlWriter = new XMLWriter(); + $style = new ImageStyle(); + $style->setWidth(100); + $style->setHeight(100); + $this->element = new Image('tests/PhpWordTests/_files/images/earth.jpg', $style); + $this->writer = new ImageWriter($this->xmlWriter, $this->element); + } + + public function testWrite(): void + { + $this->writer->write(); + + $expected = '

'; + self::assertEquals($expected, $this->xmlWriter->getData()); + } + + public function testWriteWithoutP(): void + { + $style = new ImageStyle(); + $style->setWidth(100); + $style->setHeight(100); + $this->element = new Image('tests/PhpWordTests/_files/images/earth.jpg', $style); + $this->writer = new ImageWriter($this->xmlWriter, $this->element, true); + + $this->writer->write(); + + $expected = ''; + self::assertEquals($expected, $this->xmlWriter->getData()); + } + + public function testWriteWithInvalidElement(): void + { + $invalidElement = $this->createMock(\PhpOffice\PhpWord\Element\AbstractElement::class); + $writer = new ImageWriter($this->xmlWriter, $invalidElement); + + $writer->write(); + + self::assertEquals('', $this->xmlWriter->getData()); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Element/TextTest.php b/tests/PhpWordTests/Writer/EPub3/Element/TextTest.php new file mode 100644 index 0000000000..38490691d5 --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Element/TextTest.php @@ -0,0 +1,83 @@ +xmlWriter = new XMLWriter(); + $this->element = new Text('Sample Text'); + $this->writer = new TextWriter($this->xmlWriter, $this->element); + } + + public function testWrite(): void + { + $this->writer->write(); + + $expected = "

\n Sample Text\n

\n"; + self::assertEquals($expected, $this->xmlWriter->getData()); + } + + public function testWriteWithFontStyle(): void + { + $this->element->setFontStyle('customStyle'); + + $this->writer->write(); + + $expected = "

\n Sample Text\n

\n"; + self::assertEquals($expected, $this->xmlWriter->getData()); + } + + public function testWriteWithParagraphStyle(): void + { + $this->element->setParagraphStyle('paragraphStyle'); + + $this->writer->write(); + + $expected = "

\n Sample Text\n

\n"; + self::assertEquals($expected, $this->xmlWriter->getData()); + } + + public function testWriteWithoutP(): void + { + $text = new Text('Sample Text'); + $xmlWriter = new XMLWriter(); + $this->writer = new TextWriter($xmlWriter, $text, true); + + $this->writer->write(); + + $expected = "Sample Text\n"; + self::assertEquals($expected, $xmlWriter->getData()); + } + + public function testWriteWithInvalidElement(): void + { + $invalidElement = $this->createMock(\PhpOffice\PhpWord\Element\AbstractElement::class); + $writer = new TextWriter($this->xmlWriter, $invalidElement); + + $writer->write(); + + self::assertEquals('', $this->xmlWriter->getData()); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/ElementTest.php b/tests/PhpWordTests/Writer/EPub3/ElementTest.php new file mode 100644 index 0000000000..af745d9176 --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/ElementTest.php @@ -0,0 +1,26 @@ +expectException(\PhpOffice\PhpWord\Exception\Exception::class); + + $element = $this->createMock(AbstractElement::class); + WriterElement::getElementClass($element); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Part/AbstractPartTest.php b/tests/PhpWordTests/Writer/EPub3/Part/AbstractPartTest.php new file mode 100644 index 0000000000..5151eea24c --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Part/AbstractPartTest.php @@ -0,0 +1,28 @@ +part = $this->getMockForAbstractClass(AbstractPart::class); + } + + public function testParentWriter(): void + { + $writer = new EPub3(); + $this->part->setParentWriter($writer); + + self::assertInstanceOf(EPub3::class, $this->part->getParentWriter()); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Part/ContentTest.php b/tests/PhpWordTests/Writer/EPub3/Part/ContentTest.php new file mode 100644 index 0000000000..6745ea2eda --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Part/ContentTest.php @@ -0,0 +1,38 @@ +content = new Content($phpWord); + $section = $phpWord->addSection(); + $section->addText('Test content'); + + $writer = new EPub3($phpWord); + $this->content->setParentWriter($writer); + } + + public function testWrite(): void + { + $result = $this->content->write(); + + self::assertIsString($result); + self::assertStringContainsString('', $result); + self::assertStringContainsString('', $result); + self::assertStringContainsString('', $result); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Part/ManifestTest.php b/tests/PhpWordTests/Writer/EPub3/Part/ManifestTest.php new file mode 100644 index 0000000000..d1755da9af --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Part/ManifestTest.php @@ -0,0 +1,35 @@ +manifest = new Manifest(); + $phpWord = new PhpWord(); + $writer = new EPub3($phpWord); + $this->manifest->setParentWriter($writer); + } + + public function testWrite(): void + { + $result = $this->manifest->write(); + + self::assertStringContainsString('', $result); + self::assertIsString($result); + self::assertStringContainsString('', $result); + self::assertStringContainsString('meta = new Meta(); + $phpWord = new PhpWord(); + $writer = new EPub3($phpWord); + $this->meta->setParentWriter($writer); + } + + public function testWrite(): void + { + $result = $this->meta->write(); + + self::assertIsString($result); + self::assertStringContainsString('', $result); + self::assertStringContainsString('getDocInfo(); + $properties->setCreator('PHPWord'); + $properties->setTitle('Test Title'); + $properties->setKeywords('test, keywords'); + + $writer = new EPub3($phpWord); + $this->meta->setParentWriter($writer); + + $expected = '\nTest Titleenurn:uuid:12345PHPWord2023-01-01T00:00:00Z'; + + $result = $this->meta->write(); + + self::assertStringContainsString('PHPWord', $result); + self::assertStringContainsString('Test Title', $result); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Part/MimetypeTest.php b/tests/PhpWordTests/Writer/EPub3/Part/MimetypeTest.php new file mode 100644 index 0000000000..c1cd57f705 --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Part/MimetypeTest.php @@ -0,0 +1,32 @@ +mimetype = new Mimetype(); + $phpWord = new PhpWord(); + $writer = new EPub3($phpWord); + $this->mimetype->setParentWriter($writer); + } + + public function testWrite(): void + { + $result = $this->mimetype->write(); + + self::assertIsString($result); + self::assertEquals('application/epub+zip', $result); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Part/NavTest.php b/tests/PhpWordTests/Writer/EPub3/Part/NavTest.php new file mode 100644 index 0000000000..a050f95511 --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Part/NavTest.php @@ -0,0 +1,49 @@ +write(); + + // Test that valid XML is generated + $dom = new DOMDocument(); + $dom->loadXML($xml); + + // Test required XML elements and attributes exist + self::assertEquals('html', $dom->documentElement->nodeName); + self::assertEquals('http://www.w3.org/1999/xhtml', $dom->documentElement->getAttribute('xmlns')); + self::assertEquals('http://www.idpf.org/2007/ops', $dom->documentElement->getAttribute('xmlns:epub')); + + // Test nav element + $navElements = $dom->getElementsByTagName('nav'); + self::assertEquals(1, $navElements->length); + $navElement = $navElements->item(0); + self::assertEquals('toc', $navElement->getAttribute('epub:type')); + self::assertEquals('toc', $navElement->getAttribute('id')); + + // Test title exists + $titleElements = $dom->getElementsByTagName('title'); + self::assertEquals(1, $titleElements->length); + self::assertEquals('Navigation', $titleElements->item(0)->nodeValue); + + // Test TOC header exists + $h1Elements = $dom->getElementsByTagName('h1'); + self::assertEquals(1, $h1Elements->length); + self::assertEquals('Table of Contents', $h1Elements->item(0)->nodeValue); + + // Test TOC list structure exists + $olElements = $dom->getElementsByTagName('ol'); + self::assertEquals(1, $olElements->length); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/PartTest.php b/tests/PhpWordTests/Writer/EPub3/PartTest.php new file mode 100644 index 0000000000..ed91c60bcb --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/PartTest.php @@ -0,0 +1,28 @@ +expectException(\PhpOffice\PhpWord\Exception\Exception::class); + + Part::getPartClass('InvalidType'); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Style/AbstractStyleTest.php b/tests/PhpWordTests/Writer/EPub3/Style/AbstractStyleTest.php new file mode 100644 index 0000000000..fb9135f0cd --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Style/AbstractStyleTest.php @@ -0,0 +1,24 @@ +getMockForAbstractClass(AbstractStyle::class); + + $result = $style->setParentWriter($parentWriter); + + self::assertSame($style, $result); + self::assertSame($parentWriter, $style->getParentWriter()); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Style/FontTest.php b/tests/PhpWordTests/Writer/EPub3/Style/FontTest.php new file mode 100644 index 0000000000..26572af3a4 --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Style/FontTest.php @@ -0,0 +1,25 @@ +write(); + + self::assertStringContainsString('font-family: "Times New Roman", Times, serif;', $content); + self::assertStringContainsString('font-size: 12pt;', $content); + self::assertStringContainsString('color: #000000;', $content); + self::assertStringStartsWith('body {', $content); + self::assertStringEndsWith('}', $content); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Style/ParagraphTest.php b/tests/PhpWordTests/Writer/EPub3/Style/ParagraphTest.php new file mode 100644 index 0000000000..bcaab0bab1 --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Style/ParagraphTest.php @@ -0,0 +1,25 @@ +write(); + + self::assertStringContainsString('margin-top: 0;', $content); + self::assertStringContainsString('margin-bottom: 1em;', $content); + self::assertStringContainsString('text-align: left;', $content); + self::assertStringStartsWith('p {', $content); + self::assertStringEndsWith('}', $content); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/Style/TableTest.php b/tests/PhpWordTests/Writer/EPub3/Style/TableTest.php new file mode 100644 index 0000000000..1d6ff0d6bb --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/Style/TableTest.php @@ -0,0 +1,28 @@ +write(); + + self::assertStringContainsString('border-collapse: collapse;', $content); + self::assertStringContainsString('width: 100%;', $content); + self::assertStringContainsString('border: 1px solid black;', $content); + self::assertStringContainsString('padding: 8px;', $content); + self::assertStringContainsString('text-align: left;', $content); + self::assertStringContainsString('table {', $content); + self::assertStringContainsString('th, td {', $content); + self::assertStringEndsWith('}', $content); + } +} diff --git a/tests/PhpWordTests/Writer/EPub3/StyleTest.php b/tests/PhpWordTests/Writer/EPub3/StyleTest.php new file mode 100644 index 0000000000..15db0d581d --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3/StyleTest.php @@ -0,0 +1,23 @@ +setXmlWriter($xmlWriter); + $object->write(); + + self::assertEquals('', $xmlWriter->getData()); + } + } +} diff --git a/tests/PhpWordTests/Writer/EPub3Test.php b/tests/PhpWordTests/Writer/EPub3Test.php new file mode 100644 index 0000000000..acdbb5e32f --- /dev/null +++ b/tests/PhpWordTests/Writer/EPub3Test.php @@ -0,0 +1,126 @@ +getPhpWord()); + self::assertEquals('./', $object->getDiskCachingDirectory()); + foreach (['Content', 'Manifest', 'Mimetype'] as $part) { + self::assertInstanceOf( + "PhpOffice\\PhpWord\\Writer\\Epub3\\Part\\{$part}", + $object->getWriterPart($part) + ); + self::assertInstanceOf( + 'PhpOffice\\PhpWord\\Writer\\Epub3', + $object->getWriterPart($part)->getParentWriter() + ); + } + } + + /** + * Test construction with null. + */ + public function testConstructWithNull(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('No PhpWord assigned.'); + + $writer = new EPub3(); + $writer->getWriterPart('content')->write(); + } + + /** + * Test saving document. + */ + public function testSave(): void + { + $imageSrc = __DIR__ . '/../_files/images/PhpWord.png'; + $file = __DIR__ . '/../_files/temp.epub'; + + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $section->addText('Test 1'); + $section->addTextBreak(); + $section->addText('Test 2', null, ['alignment' => Jc::CENTER]); + $section->addLink('https://github.com/PHPOffice/PHPWord'); + $section->addTitle('Test', 1); + $section->addPageBreak(); + $section->addImage($imageSrc); + $writer = new EPub3($phpWord); + $writer->save($file); + self::assertFileExists($file); + unlink($file); + } + + /** + * Test PHP output. + */ + public function testSavePhpOutput(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $section->addText('Test'); + $writer = new EPub3($phpWord); + ob_start(); + $writer->save('php://output'); + $contents = ob_get_contents(); + self::assertTrue(ob_end_clean()); + self::assertNotEmpty($contents); + } + + /** + * Test disk caching. + */ + public function testSetGetUseDiskCaching(): void + { + $object = new EPub3(); + $object->setUseDiskCaching(true, PHPWORD_TESTS_BASE_DIR); + self::assertTrue($object->isUseDiskCaching()); + self::assertEquals(PHPWORD_TESTS_BASE_DIR, $object->getDiskCachingDirectory()); + } + + /** + * Test disk caching exception. + */ + public function testSetUseDiskCachingException(): void + { + $this->expectException(Exception::class); + $dir = implode(DIRECTORY_SEPARATOR, [PHPWORD_TESTS_BASE_DIR, 'foo']); + + $object = new EPub3(); + $object->setUseDiskCaching(true, $dir); + } +}