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 .= '
\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('