diff --git a/.travis.yml b/.travis.yml index f322426d77..ad3df6137d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,8 +18,9 @@ before_script: ## Composer # - curl -s http://getcomposer.org/installer | php # - php composer.phar install --prefer-source + - composer self-update + - composer require dompdf/dompdf:0.6.* - composer install --prefer-source - - composer selfupdate --quiet ## PHP_CodeSniffer - pyrus install pear/PHP_CodeSniffer - phpenv rehash diff --git a/CHANGELOG.md b/CHANGELOG.md index d8268a730d..759f41e8eb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,13 +27,13 @@ This release marked heavy refactorings on internal code structure with the creat - Object: Ability to add object in header, footer, textrun, and footnote - @ivanlanin GH-187 - Media: Add `Media::resetElements()` to reset all media data - @juzi GH-19 - General: Add `Style::resetStyles()`, `Footnote::resetElements()`, and `TOC::resetTitles()` - @ivanlanin GH-187 -- Reader: Ability to read header, footer, footnotes, link, preservetext, textbreak, pagebreak, table, and list - @ivanlanin +- DOCX Reader: Ability to read header, footer, footnotes, link, preservetext, textbreak, pagebreak, table, list, image - @ivanlanin - Endnote: Ability to add endnotes - @ivanlanin - ListItem: Ability to create custom list and reset list number - @ivanlanin GH-10 GH-198 - ODT Writer: Basic table writing support - @ivanlanin - Image: Keep image aspect ratio if only 1 dimension styled - @japonicus GH-194 -- HTML Writer: Basic HTML writer initiated - @ivanlanin GH-203 GH-67 GH-147 -- PDF Writer: Basic PDF writer initiated using DomPDF - @ivanlanin GH-68 +- HTML Writer: Basic HTML writer: text, textrun, link, title, textbreak, table, image (as Base64), footnote, endnote - @ivanlanin GH-203 GH-67 GH-147 +- PDF Writer: Basic PDF writer using DomPDF: All HTML element except image - @ivanlanin GH-68 ### Bugfixes diff --git a/composer.json b/composer.json index 6a2d9573fc..97d3e00387 100644 --- a/composer.json +++ b/composer.json @@ -40,9 +40,10 @@ "phpunit/phpunit": "3.7.*" }, "suggest": { - "ext-gd2": "*", - "ext-xmlwriter": "*", - "ext-xsl": "*" + "ext-gd2": "Required to add images", + "ext-xmlwriter": "Required to write DOCX and ODT", + "ext-xsl": "Required to apply XSL style sheet to template part", + "dompdf/dompdf": "Required to write PDF" }, "autoload": { "psr-4": { diff --git a/docs/intro.rst b/docs/intro.rst index 0f1f3d74ed..a617777de2 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -62,63 +62,63 @@ Below are the supported features for each file formats. Writers ~~~~~~~ -+-------------------------------------------------+--------+-------+-------+ -| Features | DOCX | ODT | RTF | -+=========================+=======================+========+=======+=======+ -| **Document Properties** | Standard | | | | -+ +-----------------------+--------+-------+-------+ -| | Extended | | | | -+ +-----------------------+--------+-------+-------+ -| | UserDefined | | | | -+-------------------------+-----------------------+--------+-------+-------+ -| **Element Type** | Text | ✓ | ✓ | ✓ | -+ +-----------------------+--------+-------+-------+ -| | Text Run | ✓ | ✓ | ✓ | -+ +-----------------------+--------+-------+-------+ -| | Title | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Link | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Preserve Text | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Text Break | ✓ | ✓ | ✓ | -+ +-----------------------+--------+-------+-------+ -| | Page Break | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | List | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Table | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Image | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Object | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Watermark | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Table of Contents | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Header | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Footer | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Footnote | ✓ | | | -+ +-----------------------+--------+-------+-------+ -| | Endnote | ✓ | | | -+-------------------------+-----------------------+--------+-------+-------+ -| **Graphs** | 2D basic graphs | | | | -+ +-----------------------+--------+-------+-------+ -| | 2D advanced graphs | | | | -+ +-----------------------+--------+-------+-------+ -| | 3D graphs | | | | -+-------------------------+-----------------------+--------+-------+-------+ -| **Math** | OMML support | | | | -+ +-----------------------+--------+-------+-------+ -| | MathML support | | | | -+-------------------------+-----------------------+--------+-------+-------+ -| **Bonus** | Encryption | | | | -+ +-----------------------+--------+-------+-------+ -| | Protection | | | | -+-------------------------+-----------------------+--------+-------+-------+ ++-------------------------------------------------+--------+-------+-------+-------+-------+ +| Features | DOCX | ODT | RTF | HTML | PDF | ++=========================+=======================+========+=======+=======+=======+=======+ +| **Document Properties** | Standard | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Extended | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | UserDefined | ✓ | | | | | ++-------------------------+-----------------------+--------+-------+-------+-------+-------+ +| **Element Type** | Text | ✓ | ✓ | ✓ | ✓ | ✓ | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Text Run | ✓ | ✓ | ✓ | ✓ | ✓ | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Title | ✓ | | | ✓ | ✓ | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Link | ✓ | | | ✓ | ✓ | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Preserve Text | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Text Break | ✓ | ✓ | ✓ | ✓ | ✓ | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Page Break | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | List | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Table | ✓ | ✓ | | ✓ | ✓ | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Image | ✓ | | | ✓ | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Object | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Watermark | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Table of Contents | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Header | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Footer | ✓ | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Footnote | ✓ | | | ✓ | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Endnote | ✓ | | | ✓ | | ++-------------------------+-----------------------+--------+-------+-------+-------+-------+ +| **Graphs** | 2D basic graphs | | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | 2D advanced graphs | | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | 3D graphs | | | | | | ++-------------------------+-----------------------+--------+-------+-------+-------+-------+ +| **Math** | OMML support | | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | MathML support | | | | | | ++-------------------------+-----------------------+--------+-------+-------+-------+-------+ +| **Bonus** | Encryption | | | | | | ++ +-----------------------+--------+-------+-------+-------+-------+ +| | Protection | | | | | | ++-------------------------+-----------------------+--------+-------+-------+-------+-------+ Readers diff --git a/samples/Sample_07_TemplateCloneRow.php b/samples/Sample_07_TemplateCloneRow.php index 3049466d48..0e79e69f9a 100755 --- a/samples/Sample_07_TemplateCloneRow.php +++ b/samples/Sample_07_TemplateCloneRow.php @@ -60,4 +60,8 @@ $document->saveAs($name); rename($name, "results/{$name}"); -include_once 'Sample_Footer.php'; +$writers = array('Word2007' => 'docx'); +echo getEndingNotes($writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/samples/Sample_23_TemplateBlock.php b/samples/Sample_23_TemplateBlock.php index bbde5a72e8..d0856ec210 100644 --- a/samples/Sample_23_TemplateBlock.php +++ b/samples/Sample_23_TemplateBlock.php @@ -18,4 +18,8 @@ $document->saveAs($name); rename($name, "results/{$name}"); -include_once 'Sample_Footer.php'; +$writers = array('Word2007' => 'docx'); +echo getEndingNotes($writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/samples/Sample_Header.php b/samples/Sample_Header.php index c79aee32de..210d9ee493 100644 --- a/samples/Sample_Header.php +++ b/samples/Sample_Header.php @@ -46,18 +46,17 @@ } /** - * Get results + * Write documents * * @param \PhpOffice\PhpWord\PhpWord $phpWord * @param string $filename * @param array $writers - * @return string */ function write($phpWord, $filename, $writers) { $result = ''; - // Write + // Write documents foreach ($writers as $writer => $extension) { $result .= date('H:i:s') . " Write to {$writer} format"; if (!is_null($extension)) { @@ -70,6 +69,20 @@ function write($phpWord, $filename, $writers) $result .= EOL; } + $result .= getEndingNotes($writers); + + return $result; +} + +/** + * Get ending notes + * + * @param array $writers + */ +function getEndingNotes($writers) +{ + $result = ''; + // Do not show execution time for index if (!IS_INDEX) { $result .= date('H:i:s') . " Done writing file(s)" . EOL; @@ -85,9 +98,11 @@ function write($phpWord, $filename, $writers) $result .= '

 

'; $result .= '

Results: '; foreach ($types as $type) { - $resultFile = 'results/' . SCRIPT_FILENAME . '.' . $type; - if (file_exists($resultFile)) { - $result .= "{$type} "; + if (!is_null($type)) { + $resultFile = 'results/' . SCRIPT_FILENAME . '.' . $type; + if (file_exists($resultFile)) { + $result .= "{$type} "; + } } } $result .= '

'; diff --git a/src/PhpWord/Element/Image.php b/src/PhpWord/Element/Image.php index ce4b160971..e4e91fe6da 100755 --- a/src/PhpWord/Element/Image.php +++ b/src/PhpWord/Element/Image.php @@ -18,6 +18,13 @@ */ class Image extends AbstractElement { + /** + * Image source type constants + */ + const SOURCE_LOCAL = 'local'; // Local images + const SOURCE_GD = 'gd'; // Generated using GD + const SOURCE_ARCHIVE = 'archive'; // Image in archives zip://$archive#$image + /** * Image source * @@ -25,6 +32,13 @@ class Image extends AbstractElement */ private $source; + /** + * Source type: local|gd|archive + * + * @var string + */ + private $sourceType; + /** * Image style * @@ -85,66 +99,11 @@ class Image extends AbstractElement */ public function __construct($source, $style = null, $isWatermark = false) { - // Detect if it's a memory image, by .php ext or by URL - if (stripos(strrev($source), strrev('.php')) === 0) { - $this->isMemImage = true; - } else { - $this->isMemImage = (filter_var($source, FILTER_VALIDATE_URL) !== false); - } - - // Check supported types - if ($this->isMemImage) { - $supportedTypes = array('image/jpeg', 'image/gif', 'image/png'); - $imgData = @getimagesize($source); - if (!is_array($imgData)) { - throw new InvalidImageException(); - } - $this->imageType = $imgData['mime']; // string - if (!in_array($this->imageType, $supportedTypes)) { - throw new UnsupportedImageTypeException(); - } - } else { - $supportedTypes = array( - IMAGETYPE_JPEG, IMAGETYPE_GIF, - IMAGETYPE_PNG, IMAGETYPE_BMP, - IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM - ); - if (!file_exists($source)) { - throw new InvalidImageException(); - } - $imgData = getimagesize($source); - if (function_exists('exif_imagetype')) { - $this->imageType = exif_imagetype($source); - } else { - // @codeCoverageIgnoreStart - $tmp = getimagesize($source); - $this->imageType = $tmp[2]; - // @codeCoverageIgnoreEnd - } - if (!in_array($this->imageType, $supportedTypes)) { - throw new UnsupportedImageTypeException(); - } - $this->imageType = image_type_to_mime_type($this->imageType); - } - - // Set private properties $this->source = $source; - $this->isWatermark = $isWatermark; + $this->setIsWatermark($isWatermark); $this->style = $this->setStyle(new ImageStyle(), $style, true); - $styleWidth = $this->style->getWidth(); - $styleHeight = $this->style->getHeight(); - list($actualWidth, $actualHeight) = $imgData; - if (!($styleWidth && $styleHeight)) { - if ($styleWidth == null && $styleHeight == null) { - $this->style->setWidth($actualWidth); - $this->style->setHeight($actualHeight); - } elseif ($styleWidth) { - $this->style->setHeight($actualHeight * ($styleWidth / $actualWidth)); - } else { - $this->style->setWidth($actualWidth * ($styleHeight / $actualHeight)); - } - } - $this->setImageFunctions(); + + $this->checkImage($source); } /** @@ -167,6 +126,16 @@ public function getSource() return $this->source; } + /** + * Get image source type + * + * @return string + */ + public function getSourceType() + { + return $this->sourceType; + } + /** * Get image media ID * @@ -248,9 +217,92 @@ public function getIsMemImage() } /** - * Set image functions + * Check memory image, supported type, image functions, and proportional width/height + * + * @param string $source */ - private function setImageFunctions() + private function checkImage($source) + { + $this->setSourceType($source); + + // Check image data + if ($this->sourceType == self::SOURCE_ARCHIVE) { + $imageData = $this->getArchiveImageSize($source); + } else { + $imageData = @getimagesize($source); + } + if (!is_array($imageData)) { + throw new InvalidImageException(); + } + list($actualWidth, $actualHeight, $imageType) = $imageData; + + // Check image type support + $supportedTypes = array(IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG); + if ($this->sourceType != self::SOURCE_GD) { + $supportedTypes = array_merge($supportedTypes, array(IMAGETYPE_BMP, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM)); + } + if (!in_array($imageType, $supportedTypes)) { + throw new UnsupportedImageTypeException(); + } + + // Define image functions + $this->imageType = image_type_to_mime_type($imageType); + $this->setFunctions(); + $this->setProportionalSize($actualWidth, $actualHeight); + } + + /** + * Set source type + * + * @param string $source + */ + private function setSourceType($source) + { + if (stripos(strrev($source), strrev('.php')) === 0) { + $this->isMemImage = true; + $this->sourceType = self::SOURCE_GD; + } elseif (strpos($source, 'zip://') !== false) { + $this->isMemImage = false; + $this->sourceType = self::SOURCE_ARCHIVE; + } else { + $this->isMemImage = (filter_var($source, FILTER_VALIDATE_URL) !== false); + $this->sourceType = $this->isMemImage ? self::SOURCE_GD : self::SOURCE_LOCAL; + } + } + + /** + * Get image size from archive + * + * @param string $source + * @return array|null + */ + private function getArchiveImageSize($source) + { + $imageData = null; + $source = substr($source, 6); + list($zipFilename, $imageFilename) = explode('#', $source); + $tempFilename = tempnam(sys_get_temp_dir(), 'PHPWordImage'); + + $zip = new \ZipArchive(); + if ($zip->open($zipFilename) !== false) { + if ($zip->locateName($imageFilename)) { + $imageContent = $zip->getFromName($imageFilename); + if ($imageContent !== false) { + file_put_contents($tempFilename, $imageContent); + $imageData = @getimagesize($tempFilename); + unlink($tempFilename); + } + } + $zip->close(); + } + + return $imageData; + } + + /** + * Set image functions and extensions + */ + private function setFunctions() { switch ($this->imageType) { case 'image/png': @@ -269,8 +321,8 @@ private function setImageFunctions() $this->imageFunc = 'imagejpeg'; $this->imageExtension = 'jpg'; break; - case 'image/x-ms-bmp': case 'image/bmp': + case 'image/x-ms-bmp': $this->imageType = 'image/bmp'; $this->imageExtension = 'bmp'; break; @@ -279,4 +331,26 @@ private function setImageFunctions() break; } } + + /** + * Set proportional width/height if one dimension not available + * + * @param integer $actualWidth + * @param integer $actualHeight + */ + private function setProportionalSize($actualWidth, $actualHeight) + { + $styleWidth = $this->style->getWidth(); + $styleHeight = $this->style->getHeight(); + if (!($styleWidth && $styleHeight)) { + if ($styleWidth == null && $styleHeight == null) { + $this->style->setWidth($actualWidth); + $this->style->setHeight($actualHeight); + } elseif ($styleWidth) { + $this->style->setHeight($actualHeight * ($styleWidth / $actualWidth)); + } else { + $this->style->setWidth($actualWidth * ($styleHeight / $actualHeight)); + } + } + } } diff --git a/src/PhpWord/Reader/Word2007.php b/src/PhpWord/Reader/Word2007.php index 9d74e54040..931ba01ac0 100644 --- a/src/PhpWord/Reader/Word2007.php +++ b/src/PhpWord/Reader/Word2007.php @@ -38,6 +38,13 @@ class Word2007 extends AbstractReader implements ReaderInterface */ private $rels = array('main' => array(), 'document' => array()); + /** + * Filename + * + * @var string + */ + private $filename; + /** * Loads PhpWord from file * @@ -48,17 +55,17 @@ public function load($filename) { $this->phpWord = new PhpWord(); - $this->readRelationships($filename); - + $this->filename = $filename; + $this->readRelationships(); // Read styles and numbering first foreach ($this->rels['document'] as $rId => $rel) { switch ($rel['type']) { case 'styles': - $this->readStyles($filename, $rel['target']); + $this->readStyles($rel['target']); break; case 'numbering': - $this->readNumbering($filename, $rel['target']); + $this->readNumbering($rel['target']); break; } } @@ -68,7 +75,7 @@ public function load($filename) switch ($rel['type']) { case 'officeDocument': - $this->readDocument($filename, $rel['target']); + $this->readDocument($rel['target']); break; case 'core-properties': @@ -84,16 +91,16 @@ public function load($filename) 'dcterms:modified' => 'setModified', ); $callbacks = array('dcterms:created' => 'strtotime', 'dcterms:modified' => 'strtotime'); - $this->readDocProps($filename, $rel['target'], $mapping, $callbacks); + $this->readDocProps($rel['target'], $mapping, $callbacks); break; case 'extended-properties': $mapping = array('Company' => 'setCompany', 'Manager' => 'setManager'); - $this->readDocProps($filename, $rel['target'], $mapping); + $this->readDocProps($rel['target'], $mapping); break; case 'custom-properties': - $this->readDocPropsCustom($filename, $rel['target']); + $this->readDocPropsCustom($rel['target']); break; } } @@ -103,7 +110,7 @@ public function load($filename) switch ($rel['type']) { case 'footnotes': case 'endnotes': - $this->readNotes($filename, $rel['target'], $rel['type']); + $this->readNotes($rel['target'], $rel['type']); break; } } @@ -113,24 +120,22 @@ public function load($filename) /** * Read all relationship files - * - * @param string $filename */ - private function readRelationships($filename) + private function readRelationships() { // _rels/.rels - $this->rels['main'] = $this->getRels($filename, '_rels/.rels'); + $this->rels['main'] = $this->getRels('_rels/.rels'); // word/_rels/*.xml.rels $wordRelsPath = 'word/_rels/'; $zipClass = Settings::getZipClass(); $zip = new $zipClass(); - if ($zip->open($filename) === true) { + if ($zip->open($this->filename) === true) { for ($i = 0; $i < $zip->numFiles; $i++) { $xmlFile = $zip->getNameIndex($i); if ((substr($xmlFile, 0, strlen($wordRelsPath))) == $wordRelsPath && (substr($xmlFile, -1)) != '/') { $docPart = str_replace('.xml.rels', '', str_replace($wordRelsPath, '', $xmlFile)); - $this->rels[$docPart] = $this->getRels($filename, $xmlFile, 'word/'); + $this->rels[$docPart] = $this->getRels($xmlFile, 'word/'); } } $zip->close(); @@ -140,15 +145,14 @@ private function readRelationships($filename) /** * Read core and extended document properties * - * @param string $filename * @param string $xmlFile * @param array $mapping * @param array $callbacks */ - private function readDocProps($filename, $xmlFile, $mapping, $callbacks = array()) + private function readDocProps($xmlFile, $mapping, $callbacks = array()) { $xmlReader = new XMLReader(); - $xmlReader->getDomFromZip($filename, $xmlFile); + $xmlReader->getDomFromZip($this->filename, $xmlFile); $docProps = $this->phpWord->getDocumentProperties(); $nodes = $xmlReader->getElements('*'); @@ -172,13 +176,12 @@ private function readDocProps($filename, $xmlFile, $mapping, $callbacks = array( /** * Read custom document properties * - * @param string $filename * @param string $xmlFile */ - private function readDocPropsCustom($filename, $xmlFile) + private function readDocPropsCustom($xmlFile) { $xmlReader = new XMLReader(); - $xmlReader->getDomFromZip($filename, $xmlFile); + $xmlReader->getDomFromZip($this->filename, $xmlFile); $docProps = $this->phpWord->getDocumentProperties(); $nodes = $xmlReader->getElements('*'); @@ -198,13 +201,12 @@ private function readDocPropsCustom($filename, $xmlFile) /** * Read document.xml * - * @param string $filename * @param string $xmlFile */ - private function readDocument($filename, $xmlFile) + private function readDocument($xmlFile) { $xmlReader = new XMLReader(); - $xmlReader->getDomFromZip($filename, $xmlFile); + $xmlReader->getDomFromZip($this->filename, $xmlFile); $nodes = $xmlReader->getElements('w:body/*'); if ($nodes->length > 0) { @@ -225,7 +227,7 @@ private function readDocument($filename, $xmlFile) $settings = $this->readSectionStyle($xmlReader, $settingsNode); $section->setSettings($settings); if (!is_null($settings)) { - $this->readHeaderFooter($filename, $settings, $section); + $this->readHeaderFooter($settings, $section); } } $section = $this->phpWord->addSection(); @@ -240,7 +242,7 @@ private function readDocument($filename, $xmlFile) $settings = $this->readSectionStyle($xmlReader, $node); $section->setSettings($settings); if (!is_null($settings)) { - $this->readHeaderFooter($filename, $settings, $section); + $this->readHeaderFooter($settings, $section); } break; } @@ -251,13 +253,12 @@ private function readDocument($filename, $xmlFile) /** * Read styles.xml * - * @param string $filename * @param string $xmlFile */ - private function readStyles($filename, $xmlFile) + private function readStyles($xmlFile) { $xmlReader = new XMLReader(); - $xmlReader->getDomFromZip($filename, $xmlFile); + $xmlReader->getDomFromZip($this->filename, $xmlFile); $nodes = $xmlReader->getElements('w:style'); if ($nodes->length > 0) { @@ -303,15 +304,14 @@ private function readStyles($filename, $xmlFile) /** * Read numbering.xml * - * @param string $filename * @param string $xmlFile */ - private function readNumbering($filename, $xmlFile) + private function readNumbering($xmlFile) { $abstracts = array(); $numberings = array(); $xmlReader = new XMLReader(); - $xmlReader->getDomFromZip($filename, $xmlFile); + $xmlReader->getDomFromZip($this->filename, $xmlFile); // Abstract numbering definition $nodes = $xmlReader->getElements('w:abstractNum'); @@ -395,11 +395,10 @@ private function readNumberingLevel(XMLReader $xmlReader, \DOMElement $subnode, /** * Read header footer * - * @param string $filename * @param array $settings * @param Section $section */ - private function readHeaderFooter($filename, $settings, &$section) + private function readHeaderFooter($settings, &$section) { if (is_array($settings) && array_key_exists('hf', $settings)) { foreach ($settings['hf'] as $rId => $hfSetting) { @@ -410,7 +409,7 @@ private function readHeaderFooter($filename, $settings, &$section) // Read header/footer content $xmlReader = new XMLReader(); - $xmlReader->getDomFromZip($filename, $xmlFile); + $xmlReader->getDomFromZip($this->filename, $xmlFile); $nodes = $xmlReader->getElements('*'); if ($nodes->length > 0) { foreach ($nodes as $node) { @@ -434,18 +433,17 @@ private function readHeaderFooter($filename, $settings, &$section) /** * Read (footnotes|endnotes).xml * - * @param string $filename * @param string $xmlFile * @param string $notesType */ - private function readNotes($filename, $xmlFile, $notesType = 'footnotes') + private function readNotes($xmlFile, $notesType = 'footnotes') { $notesType = ($notesType == 'endnotes') ? 'endnotes' : 'footnotes'; $collectionClass = 'PhpOffice\\PhpWord\\' . ucfirst($notesType); $collection = $collectionClass::getElements(); $xmlReader = new XMLReader(); - $xmlReader->getDomFromZip($filename, $xmlFile); + $xmlReader->getDomFromZip($this->filename, $xmlFile); $nodes = $xmlReader->getElements('*'); if ($nodes->length > 0) { foreach ($nodes as $node) { @@ -581,8 +579,8 @@ private function readRun(XMLReader $xmlReader, \DOMElement $domNode, &$parent, $ $rId = $xmlReader->getAttribute('r:id', $domNode, 'w:pict/v:shape/v:imagedata'); $target = $this->getMediaTarget($docPart, $rId); if (!is_null($target)) { - $textContent = ""; - $parent->addText($textContent, $fStyle, $pStyle); + $imageSource = "zip://{$this->filename}#{$target}"; + $parent->addImage($imageSource); } // Object @@ -943,12 +941,11 @@ private function readCellStyle(XMLReader $xmlReader, \DOMElement $domNode) /** * Get relationship array * - * @param string $filename * @param string $xmlFile * @param string $targetPrefix * @return array */ - private function getRels($filename, $xmlFile, $targetPrefix = '') + private function getRels($xmlFile, $targetPrefix = '') { $metaPrefix = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/'; $officePrefix = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/'; @@ -956,7 +953,7 @@ private function getRels($filename, $xmlFile, $targetPrefix = '') $rels = array(); $xmlReader = new XMLReader(); - $xmlReader->getDomFromZip($filename, $xmlFile); + $xmlReader->getDomFromZip($this->filename, $xmlFile); $nodes = $xmlReader->getElements('*'); foreach ($nodes as $node) { $rId = $xmlReader->getAttribute('Id', $node); diff --git a/src/PhpWord/Writer/AbstractWriter.php b/src/PhpWord/Writer/AbstractWriter.php index 684a5d3227..8373b6bd97 100644 --- a/src/PhpWord/Writer/AbstractWriter.php +++ b/src/PhpWord/Writer/AbstractWriter.php @@ -48,6 +48,13 @@ abstract class AbstractWriter implements WriterInterface */ private $diskCachingDirectory = './'; + /** + * Temporary directory + * + * @var string + */ + private $tempDir = ''; + /** * Original file name * @@ -81,7 +88,7 @@ public function getPhpWord() * Set PhpWord object * * @param PhpWord - * @return $this + * @return self */ public function setPhpWord(PhpWord $phpWord = null) { @@ -119,7 +126,7 @@ public function getUseDiskCaching() * * @param boolean $pValue * @param string $pDirectory - * @return $this + * @return self */ public function setUseDiskCaching($pValue = false, $pDirectory = null) { @@ -146,6 +153,32 @@ public function getDiskCachingDirectory() return $this->diskCachingDirectory; } + /** + * Get temporary directory + * + * @return string + */ + public function getTempDir() + { + return $this->tempDir; + } + + /** + * Set temporary directory + * + * @param string $value + * @return self + */ + public function setTempDir($value) + { + if (!is_dir($value)) { + mkdir($value); + } + $this->tempDir = $value; + + return $this; + } + /** * Get temporary file name * @@ -156,6 +189,10 @@ public function getDiskCachingDirectory() */ protected function getTempFile($filename) { + // Temporary directory + $this->setTempDir(sys_get_temp_dir() . '/PHPWordWriter/'); + + // Temporary file $this->originalFilename = $filename; if (strtolower($filename) == 'php://output' || strtolower($filename) == 'php://stdout') { $filename = @tempnam(sys_get_temp_dir(), 'phpword_'); @@ -170,8 +207,6 @@ protected function getTempFile($filename) /** * Cleanup temporary file - * - * If a temporary file was used, copy it to the correct file stream */ protected function cleanupTempFile() { @@ -181,6 +216,18 @@ protected function cleanupTempFile() } @unlink($this->tempFilename); } + + $this->clearTempDir(); + } + + /** + * Clear temporary directory + */ + protected function clearTempDir() + { + if (is_dir($this->tempDir)) { + $this->deleteDir($this->tempDir); + } } /** @@ -215,4 +262,24 @@ protected function getZipArchive($filename) return $objZip; } + + /** + * Delete directory + * + * @param string $dir + */ + private function deleteDir($dir) + { + foreach (scandir($dir) as $file) { + if ($file === '.' || $file === '..') { + continue; + } elseif (is_file($dir . "/" . $file)) { + unlink($dir . "/" . $file); + } elseif (is_dir($dir . "/" . $file)) { + $this->deleteDir($dir . "/" . $file); + } + } + + rmdir($dir); + } } diff --git a/src/PhpWord/Writer/HTML.php b/src/PhpWord/Writer/HTML.php index f08793329d..bd98e89391 100644 --- a/src/PhpWord/Writer/HTML.php +++ b/src/PhpWord/Writer/HTML.php @@ -22,12 +22,13 @@ use PhpOffice\PhpWord\Element\TextBreak; use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Element\Title; +use PhpOffice\PhpWord\Endnotes; use PhpOffice\PhpWord\Exception\Exception; +use PhpOffice\PhpWord\Footnotes; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style\Font; use PhpOffice\PhpWord\Style\Paragraph; -use PhpOffice\PhpWord\TOC; /** * HTML writer @@ -36,6 +37,20 @@ */ class HTML extends AbstractWriter implements WriterInterface { + /** + * Is the current writer creating PDF? + * + * @var boolean + */ + protected $isPdf = false; + + /** + * Footnotes and endnotes collection + * + * @var array + */ + protected $notes = array(); + /** * Create new instance */ @@ -53,9 +68,11 @@ public function __construct(PhpWord $phpWord = null) public function save($filename = null) { if (!is_null($this->getPhpWord())) { + $this->setTempDir(sys_get_temp_dir() . '/PHPWordWriter/'); $hFile = fopen($filename, 'w') or die("can't open file"); fwrite($hFile, $this->writeDocument()); fclose($hFile); + $this->clearTempDir(); } else { throw new Exception("No PHPWord assigned."); } @@ -77,6 +94,7 @@ public function writeDocument() $html .= '' . PHP_EOL; $html .= '' . PHP_EOL; $html .= $this->writeHTMLBody(); + $html .= $this->writeNotes(); $html .= '' . PHP_EOL; $html .= '' . PHP_EOL; @@ -161,10 +179,10 @@ private function writeHTMLBody() $html .= $this->writeImage($element); } elseif ($element instanceof Object) { $html .= $this->writeObject($element); - } elseif ($element instanceof Footnote) { - $html .= $this->writeFootnote($element); } elseif ($element instanceof Endnote) { $html .= $this->writeEndnote($element); + } elseif ($element instanceof Footnote) { + $html .= $this->writeFootnote($element); } } } @@ -173,6 +191,32 @@ private function writeHTMLBody() return $html; } + /** + * Write footnote/endnote contents + */ + private function writeNotes() + { + $footnote = Footnotes::getElements(); + $endnote = Endnotes::getElements(); + $html = ''; + + if (count($this->notes) > 0) { + $html .= "
"; + foreach ($this->notes as $noteId => $noteMark) { + $noteAnchor = "note-{$noteId}"; + list($noteType, $noteTypeId) = explode('-', $noteMark); + $collection = $$noteType; + if (array_key_exists($noteTypeId, $collection)) { + $element = $collection[$noteTypeId]; + $content = "{$noteId}" . $this->writeTextRun($element, true); + $html .= "

{$content}

" . PHP_EOL; + } + } + } + + return $html; + } + /** * Get text * @@ -218,19 +262,20 @@ private function writeText($text, $withoutP = false) } /** - * Get text run content + * Write text run content * - * @param TextRun $textrun + * @param TextRun|Footnote|Endnote $textrun * @return string */ - private function writeTextRun($textrun) + private function writeTextRun($textrun, $withoutP = false) { $html = ''; $elements = $textrun->getElements(); if (count($elements) > 0) { $paragraphStyle = $textrun->getParagraphStyle(); $spIsObject = ($paragraphStyle instanceof Paragraph); - $html .= 'writeTextBreak($element, true); } elseif ($element instanceof Image) { $html .= $this->writeImage($element, true); - } elseif ($element instanceof Footnote) { - $html .= $this->writeFootnote($element); } elseif ($element instanceof Endnote) { $html .= $this->writeEndnote($element); + } elseif ($element instanceof Footnote) { + $html .= $this->writeFootnote($element); } } - $html .= '

' . PHP_EOL; + $html .= $withoutP ? '' : '

'; + $html .= PHP_EOL; } return $html; @@ -271,11 +317,14 @@ private function writeLink($element, $withoutP = false) { $url = $element->getLinkSrc(); $text = $element->getLinkName(); + if ($text == '') { + $text = $url; + } $html = ''; if (!$withoutP) { $html .= "

" . PHP_EOL; } - $html .= "{$text}" . PHP_EOL; + $html .= "{$text}" . PHP_EOL; if (!$withoutP) { $html .= "

" . PHP_EOL; } @@ -347,7 +396,10 @@ private function writePageBreak($element) */ private function writeListItem($element) { - return $this->writeUnsupportedElement($element, false); + $text = htmlspecialchars($element->getTextObject()->getText()); + $html = '

' . $text . '' . PHP_EOL; + + return $html; } /** @@ -390,10 +442,10 @@ private function writeTable($element) $html .= $this->writeImage($content); } elseif ($content instanceof Object) { $html .= $this->writeObject($content); - } elseif ($element instanceof Footnote) { - $html .= $this->writeFootnote($element); } elseif ($element instanceof Endnote) { $html .= $this->writeEndnote($element); + } elseif ($element instanceof Footnote) { + $html .= $this->writeFootnote($element); } } } else { @@ -418,7 +470,22 @@ private function writeTable($element) */ private function writeImage($element, $withoutP = false) { - return $this->writeUnsupportedElement($element, $withoutP); + $html = $this->writeUnsupportedElement($element, $withoutP); + if (!$this->isPdf) { + $imageData = $this->getBase64ImageData($element); + if (!is_null($imageData)) { + $style = $this->assembleCss(array( + 'width' => $element->getStyle()->getWidth() . 'px', + 'height' => $element->getStyle()->getHeight() . 'px', + )); + $html = ""; + if (!$withoutP) { + $html = "

{$html}

" . PHP_EOL; + } + } + } + + return $html; } /** @@ -441,7 +508,7 @@ private function writeObject($element, $withoutP = false) */ private function writeFootnote($element) { - return $this->writeUnsupportedElement($element, true); + return $this->writeNote($element); } /** @@ -452,7 +519,25 @@ private function writeFootnote($element) */ private function writeEndnote($element) { - return $this->writeUnsupportedElement($element, true); + return $this->writeNote($element); + } + + /** + * Write footnote/endnote marks + * + * @param Footnote|Endnote $element + * @return string + */ + private function writeNote($element) + { + $index = count($this->notes) + 1; + $prefix = ($element instanceof Endnote) ? 'endnote' : 'footnote'; + $noteMark = $prefix . '-' . $element->getRelationId(); + $noteAnchor = "note-{$index}"; + $this->notes[$index] = $noteMark; + $html = "{$index}"; + + return $html; } /** @@ -483,18 +568,33 @@ private function writeUnsupportedElement($element, $withoutP = false) */ private function writeStyles() { - $bodyCss = array(); $css = '