From 3cb6d86e22a3f54f6c072713e9a93bf92c4c1ebb Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 07:12:57 +0700 Subject: [PATCH 01/10] Unit tests for PDF writer; Enable `zip://zipFilename#archiveFilename` construct for image source --- CHANGELOG.md | 2 +- samples/Sample_Header.php | 10 +- src/PhpWord/Element/Image.php | 157 +++++++++------ src/PhpWord/Reader/Word2007.php | 83 ++++---- src/PhpWord/Writer/AbstractWriter.php | 51 +++++ src/PhpWord/Writer/HTML.php | 5 +- src/PhpWord/Writer/PDF/AbstractRenderer.php | 2 +- src/PhpWord/Writer/PDF/DomPDF.php | 14 +- src/PhpWord/Writer/Word2007.php | 38 +++- tests/PhpWord/Tests/Element/ImageTest.php | 186 +++++------------- tests/PhpWord/Tests/SettingsTest.php | 19 +- tests/PhpWord/Tests/Writer/PDF/DomPDFTest.php | 70 +++++++ tests/PhpWord/Tests/Writer/PDFTest.php | 51 +++++ .../Tests/Writer/Word2007/BaseTest.php | 5 + 14 files changed, 434 insertions(+), 259 deletions(-) create mode 100644 tests/PhpWord/Tests/Writer/PDF/DomPDFTest.php create mode 100644 tests/PhpWord/Tests/Writer/PDFTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d8268a73..b06aa79c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ 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 diff --git a/samples/Sample_Header.php b/samples/Sample_Header.php index c79aee32..2ab5a579 100644 --- a/samples/Sample_Header.php +++ b/samples/Sample_Header.php @@ -16,7 +16,7 @@ $writers = array('Word2007' => 'docx', 'ODText' => 'odt', 'RTF' => 'rtf', 'HTML' // Set PDF renderer $rendererName = \PhpOffice\PhpWord\Settings::PDF_RENDERER_DOMPDF; -$rendererLibraryPath = ''; // DomPDF library path +$rendererLibraryPath = 'D:\www\local\minerva\dompdf'; // DomPDF library path if (!\PhpOffice\PhpWord\Settings::setPdfRenderer($rendererName, $rendererLibraryPath)) { $writers['PDF'] = null; @@ -85,9 +85,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 ce4b1609..19451c9e 100755 --- a/src/PhpWord/Element/Image.php +++ b/src/PhpWord/Element/Image.php @@ -85,66 +85,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); } /** @@ -248,10 +193,55 @@ class Image extends AbstractElement } /** - * Set image functions + * Check memory image, supported type, image functions, and proportional width/height + * + * @param string $source */ - private function setImageFunctions() + private function checkImage($source) { + $isArchive = strpos($source, 'zip://') !== false; + + // Check is memory image + if (stripos(strrev($source), strrev('.php')) === 0) { + $this->isMemImage = true; + } elseif ($isArchive) { + $this->isMemImage = false; + } else { + $this->isMemImage = (filter_var($source, FILTER_VALIDATE_URL) !== false); + } + + // Define supported types + if ($this->isMemImage) { + $supportedTypes = array( + IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG + ); + } else { + $supportedTypes = array( + IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG, + IMAGETYPE_BMP, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM + ); + } + + // Check from zip file or actual file + if ($isArchive) { + $imageData = $this->getArchivedImageSize($source); + } else { + $imageData = @getimagesize($source); + } + + // Check if image exists by detecting image data + if (!is_array($imageData)) { + throw new InvalidImageException(); + } + // Put image data into variables + list($actualWidth, $actualHeight, $imageType) = $imageData; + // Check if image type is supported + if (!in_array($imageType, $supportedTypes)) { + throw new UnsupportedImageTypeException(); + } + + // Define image functions + $this->imageType = image_type_to_mime_type($imageType); switch ($this->imageType) { case 'image/png': $this->imageCreateFunc = 'imagecreatefrompng'; @@ -264,13 +254,12 @@ class Image extends AbstractElement $this->imageExtension = 'gif'; break; case 'image/jpeg': - case 'image/jpg': $this->imageCreateFunc = 'imagecreatefromjpeg'; $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; @@ -278,5 +267,49 @@ class Image extends AbstractElement $this->imageExtension = 'tif'; break; } + + // Check image width & height + $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)); + } + } + + } + + /** + * Get image size from archive + * + * @param string $source + * @return array|null + */ + private function getArchivedImageSize($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; } } diff --git a/src/PhpWord/Reader/Word2007.php b/src/PhpWord/Reader/Word2007.php index 9d74e540..931ba01a 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 @@ class Word2007 extends AbstractReader implements ReaderInterface { $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 @@ class Word2007 extends AbstractReader implements ReaderInterface switch ($rel['type']) { case 'officeDocument': - $this->readDocument($filename, $rel['target']); + $this->readDocument($rel['target']); break; case 'core-properties': @@ -84,16 +91,16 @@ class Word2007 extends AbstractReader implements ReaderInterface '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 @@ class Word2007 extends AbstractReader implements ReaderInterface 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 @@ class Word2007 extends AbstractReader implements ReaderInterface /** * 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 @@ class Word2007 extends AbstractReader implements ReaderInterface /** * 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 @@ class Word2007 extends AbstractReader implements ReaderInterface /** * 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 @@ class Word2007 extends AbstractReader implements ReaderInterface /** * 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 @@ class Word2007 extends AbstractReader implements ReaderInterface $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 @@ class Word2007 extends AbstractReader implements ReaderInterface $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 @@ class Word2007 extends AbstractReader implements ReaderInterface /** * 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 @@ class Word2007 extends AbstractReader implements ReaderInterface /** * 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 @@ class Word2007 extends AbstractReader implements ReaderInterface /** * 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 @@ class Word2007 extends AbstractReader implements ReaderInterface // 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 @@ class Word2007 extends AbstractReader implements ReaderInterface /** * 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 @@ class Word2007 extends AbstractReader implements ReaderInterface $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 @@ class Word2007 extends AbstractReader implements ReaderInterface /** * 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 @@ class Word2007 extends AbstractReader implements ReaderInterface $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 684a5d32..9403d724 100644 --- a/src/PhpWord/Writer/AbstractWriter.php +++ b/src/PhpWord/Writer/AbstractWriter.php @@ -62,6 +62,13 @@ abstract class AbstractWriter implements WriterInterface */ private $tempFilename; + /** + * Temporary directory + * + * @var string + */ + private $tempDir; + /** * Get PhpWord object * @@ -156,6 +163,14 @@ abstract class AbstractWriter implements WriterInterface */ protected function getTempFile($filename) { + // Temporary directory + $tempDir = sys_get_temp_dir() . '/PHPWordMedia/'; + if (!is_dir($tempDir)) { + mkdir($tempDir); + } + $this->tempDir = $tempDir; + + // Temporary file $this->originalFilename = $filename; if (strtolower($filename) == 'php://output' || strtolower($filename) == 'php://stdout') { $filename = @tempnam(sys_get_temp_dir(), 'phpword_'); @@ -168,6 +183,16 @@ abstract class AbstractWriter implements WriterInterface return $this->tempFilename; } + /** + * Cleanup temporary file + * + * If a temporary file was used, copy it to the correct file stream + */ + protected function getTempDir() + { + return $this->tempDir; + } + /** * Cleanup temporary file * @@ -175,12 +200,18 @@ abstract class AbstractWriter implements WriterInterface */ protected function cleanupTempFile() { + // File if ($this->originalFilename != $this->tempFilename) { if (copy($this->tempFilename, $this->originalFilename) === false) { throw new Exception("Could not copy temporary zip file {$this->tempFilename} to {$this->originalFilename}."); } @unlink($this->tempFilename); } + + // Directory + if (is_dir($this->tempDir)) { + $this->deleteDir($this->tempDir); + } } /** @@ -215,4 +246,24 @@ abstract class AbstractWriter implements WriterInterface 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 f0879332..f0720f66 100644 --- a/src/PhpWord/Writer/HTML.php +++ b/src/PhpWord/Writer/HTML.php @@ -347,7 +347,10 @@ class HTML extends AbstractWriter implements WriterInterface */ private function writeListItem($element) { - return $this->writeUnsupportedElement($element, false); + $text = htmlspecialchars($element->getTextObject()->getText()); + $html = '

' . $text . '' . PHP_EOL; + + return $html; } /** diff --git a/src/PhpWord/Writer/PDF/AbstractRenderer.php b/src/PhpWord/Writer/PDF/AbstractRenderer.php index 3ee96966..be48a9f2 100644 --- a/src/PhpWord/Writer/PDF/AbstractRenderer.php +++ b/src/PhpWord/Writer/PDF/AbstractRenderer.php @@ -69,7 +69,7 @@ abstract class AbstractRenderer extends \PhpOffice\PhpWord\Writer\HTML public function __construct(PhpWord $phpWord) { parent::__construct($phpWord); - $this->tempDir = sys_get_temp_dir(); + $this->setTempDir(sys_get_temp_dir()); } /** diff --git a/src/PhpWord/Writer/PDF/DomPDF.php b/src/PhpWord/Writer/PDF/DomPDF.php index a5db2d99..d4c7b5f8 100644 --- a/src/PhpWord/Writer/PDF/DomPDF.php +++ b/src/PhpWord/Writer/PDF/DomPDF.php @@ -13,14 +13,6 @@ use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Exception\Exception; -/** Require DomPDF library */ -$pdfRendererClassFile = Settings::getPdfRendererPath() . '/dompdf_config.inc.php'; -if (file_exists($pdfRendererClassFile)) { - require_once $pdfRendererClassFile; -} else { - throw new Exception('Unable to load PDF Rendering library'); -} - /** * DomPDF writer */ @@ -34,6 +26,12 @@ class DomPDF extends AbstractRenderer implements \PhpOffice\PhpWord\Writer\Write public function __construct(PhpWord $phpWord) { parent::__construct($phpWord); + $configFile = Settings::getPdfRendererPath() . '/dompdf_config.inc.php'; + if (file_exists($configFile)) { + require_once $configFile; + } else { + throw new Exception('Unable to load PDF Rendering library'); + } } /** diff --git a/src/PhpWord/Writer/Word2007.php b/src/PhpWord/Writer/Word2007.php index 7b552905..0d0c0f69 100755 --- a/src/PhpWord/Writer/Word2007.php +++ b/src/PhpWord/Writer/Word2007.php @@ -145,10 +145,11 @@ class Word2007 extends AbstractWriter implements WriterInterface private function addFilesToPackage($objZip, $elements) { foreach ($elements as $element) { - // Do not add link + // Skip link if ($element['type'] == 'link') { continue; } + // Retrieve remote image if (isset($element['isMemImage']) && $element['isMemImage']) { $image = call_user_func($element['createFunction'], $element['source']); @@ -159,8 +160,9 @@ class Word2007 extends AbstractWriter implements WriterInterface $objZip->addFromString('word/' . $element['target'], $imageContents); imagedestroy($image); } else { - $objZip->addFile($element['source'], 'word/' . $element['target']); + $this->addFileToPackage($objZip, $element['source'], $element['target']); } + // Register content types if ($element['type'] == 'image') { $imageExtension = $element['imageExtension']; @@ -176,6 +178,38 @@ class Word2007 extends AbstractWriter implements WriterInterface } } + /** + * Add file to package + * + * @param mixed $objZip + * @param string $source + * @param string $target + */ + private function addFileToPackage($objZip, $source, $target) + { + $isArchive = strpos($source, 'zip://') !== false; + $actualSource = null; + if ($isArchive) { + $source = substr($source, 6); + list($zipFilename, $imageFilename) = explode('#', $source); + + $zip = new \ZipArchive(); + if ($zip->open($zipFilename) !== false) { + if ($zip->locateName($imageFilename)) { + $zip->extractTo($this->getTempDir(), $imageFilename); + $actualSource = $this->getTempDir() . DIRECTORY_SEPARATOR . $imageFilename; + } + } + $zip->close(); + } else { + $actualSource = $source; + } + + if (!is_null($actualSource)) { + $objZip->addFile($actualSource, 'word/' . $target); + } + } + /** * Add header/footer media files * diff --git a/tests/PhpWord/Tests/Element/ImageTest.php b/tests/PhpWord/Tests/Element/ImageTest.php index eae9fc5f..eb482398 100644 --- a/tests/PhpWord/Tests/Element/ImageTest.php +++ b/tests/PhpWord/Tests/Element/ImageTest.php @@ -51,34 +51,29 @@ class ImageTest extends \PHPUnit_Framework_TestCase /** * Valid image types */ - public function testValidImageTypes() + public function testImages() { - new Image(__DIR__ . "/../_files/images/mars_noext_jpg"); - new Image(__DIR__ . "/../_files/images/mars.jpg"); - new Image(__DIR__ . "/../_files/images/mario.gif"); - new Image(__DIR__ . "/../_files/images/firefox.png"); - new Image(__DIR__ . "/../_files/images/duke_nukem.bmp"); - new Image(__DIR__ . "/../_files/images/angela_merkel.tif"); - } + $images = array( + array('mars.jpg', 'image/jpeg', 'jpg', 'imagecreatefromjpeg', 'imagejpeg'), + array('mario.gif', 'image/gif', 'gif', 'imagecreatefromgif', 'imagegif'), + array('firefox.png', 'image/png', 'png', 'imagecreatefrompng', 'imagepng'), + array('duke_nukem.bmp', 'image/bmp', 'bmp', null, null), + array('angela_merkel.tif', 'image/tiff', 'tif', null, null), + ); - /** - * Image not found - * - * @expectedException \PhpOffice\PhpWord\Exception\InvalidImageException - */ - public function testImageNotFound() - { - new Image(__DIR__ . "/../_files/images/thisisnotarealimage"); - } - - /** - * Invalid image types - * - * @expectedException \PhpOffice\PhpWord\Exception\UnsupportedImageTypeException - */ - public function testInvalidImageTypes() - { - new Image(__DIR__ . "/../_files/images/alexz-johnson.pcx"); + foreach ($images as $imageData) { + list($source, $type, $extension, $createFunction, $imageFunction) = $imageData; + $source = __DIR__ . "/../_files/images/" . $source; + $image = new Image($source); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $image); + $this->assertEquals($image->getSource(), $source); + $this->assertEquals($image->getMediaId(), md5($source)); + $this->assertEquals($image->getImageType(), $type); + $this->assertEquals($image->getImageExtension(), $extension); + $this->assertEquals($image->getImageCreateFunction(), $createFunction); + $this->assertEquals($image->getImageFunction(), $imageFunction); + $this->assertFalse($image->getIsMemImage()); + } } /** @@ -88,18 +83,40 @@ class ImageTest extends \PHPUnit_Framework_TestCase { $oImage = new Image( __DIR__ . "/../_files/images/earth.jpg", - array('width' => 210, 'height' => 210, 'align' => 'center') + array('height' => 210, 'align' => 'center') ); $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Image', $oImage->getStyle()); } /** - * Test set wrapping style + * Test invalid local image + * + * @expectedException \PhpOffice\PhpWord\Exception\InvalidImageException */ - public function testStyleWrappingStyle() + public function testInvalidImageLocal() { + new Image(__DIR__ . "/../_files/images/thisisnotarealimage"); + } + /** + * Test invalid PHP Image + * + * @expectedException \PhpOffice\PhpWord\Exception\InvalidImageException + */ + public function testInvalidImagePhp() + { + $object = new Image('test.php'); + } + + /** + * Test unsupported image + * + * @expectedException \PhpOffice\PhpWord\Exception\UnsupportedImageTypeException + */ + public function testUnsupportedImage() + { + $object = new Image('http://samples.libav.org/image-samples/RACECAR.BMP'); } /** @@ -107,117 +124,20 @@ class ImageTest extends \PHPUnit_Framework_TestCase */ public function testRelationID() { - $oImage = new Image(__DIR__ . "/../_files/images/earth.jpg"); + $oImage = new Image(__DIR__ . "/../_files/images/earth.jpg", array('width' => 100)); $iVal = rand(1, 1000); $oImage->setRelationId($iVal); $this->assertEquals($oImage->getRelationId(), $iVal); } /** - * Get is watermark + * Test archived image */ - public function testWatermark() + public function testArchivedImage() { - $oImage = new Image(__DIR__ . "/../_files/images/earth.jpg"); - $oImage->setIsWatermark(true); - $this->assertEquals($oImage->getIsWatermark(), true); - } - - /** - * Test PNG - */ - public function testPNG() - { - $src = __DIR__ . "/../_files/images/firefox.png"; - $oImage = new Image($src, array('width' => 100)); - - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $oImage); - $this->assertEquals($oImage->getSource(), $src); - $this->assertEquals($oImage->getMediaId(), md5($src)); - $this->assertEquals($oImage->getImageCreateFunction(), 'imagecreatefrompng'); - $this->assertEquals($oImage->getImageFunction(), 'imagepng'); - $this->assertEquals($oImage->getImageExtension(), 'png'); - $this->assertEquals($oImage->getImageType(), 'image/png'); - } - - /** - * Test GIF - */ - public function testGIF() - { - $src = __DIR__ . "/../_files/images/mario.gif"; - $oImage = new Image($src, array('height' => 100)); - - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $oImage); - $this->assertEquals($oImage->getSource(), $src); - $this->assertEquals($oImage->getMediaId(), md5($src)); - $this->assertEquals($oImage->getImageCreateFunction(), 'imagecreatefromgif'); - $this->assertEquals($oImage->getImageFunction(), 'imagegif'); - $this->assertEquals($oImage->getImageExtension(), 'gif'); - $this->assertEquals($oImage->getImageType(), 'image/gif'); - } - - /** - * Test JPG - */ - public function testJPG() - { - $src = __DIR__ . "/../_files/images/earth.jpg"; - $oImage = new Image($src); - - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $oImage); - $this->assertEquals($oImage->getSource(), $src); - $this->assertEquals($oImage->getMediaId(), md5($src)); - $this->assertEquals($oImage->getImageCreateFunction(), 'imagecreatefromjpeg'); - $this->assertEquals($oImage->getImageFunction(), 'imagejpeg'); - $this->assertEquals($oImage->getImageExtension(), 'jpg'); - $this->assertEquals($oImage->getImageType(), 'image/jpeg'); - } - - /** - * Test BMP - */ - public function testBMP() - { - $oImage = new Image(__DIR__ . "/../_files/images/duke_nukem.bmp"); - - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $oImage); - $this->assertEquals($oImage->getImageCreateFunction(), null); - $this->assertEquals($oImage->getImageFunction(), null); - $this->assertEquals($oImage->getImageExtension(), 'bmp'); - $this->assertEquals($oImage->getImageType(), 'image/bmp'); - } - - /** - * Test TIFF - */ - public function testTIFF() - { - $oImage = new Image(__DIR__ . "/../_files/images/angela_merkel.tif"); - - $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $oImage); - $this->assertEquals($oImage->getImageCreateFunction(), null); - $this->assertEquals($oImage->getImageFunction(), null); - $this->assertEquals($oImage->getImageType(), 'image/tiff'); - } - - /** - * Test PHP Image - * - * @expectedException \PhpOffice\PhpWord\Exception\InvalidImageException - */ - public function testPhpImage() - { - $object = new Image('test.php'); - } - - /** - * Test PCX Image and Memory - * - * @expectedException \PhpOffice\PhpWord\Exception\UnsupportedImageTypeException - */ - public function testPcxImage() - { - $object = new Image('http://samples.libav.org/image-samples/RACECAR.BMP'); + $archiveFile = __DIR__ . "/../_files/documents/reader.docx"; + $imageFile = 'word/media/image1.jpeg'; + $image = new Image("zip://{$archiveFile}#{$imageFile}"); + $this->assertEquals('image/jpeg', $image->getImageType()); } } diff --git a/tests/PhpWord/Tests/SettingsTest.php b/tests/PhpWord/Tests/SettingsTest.php index fa78489e..46efdf76 100644 --- a/tests/PhpWord/Tests/SettingsTest.php +++ b/tests/PhpWord/Tests/SettingsTest.php @@ -19,9 +19,9 @@ use PhpOffice\PhpWord\Settings; class SettingsTest extends \PHPUnit_Framework_TestCase { /** - * Get/set compatibity option + * Test set/get compatibity option */ - public function testGetSetCompatibility() + public function testSetGetCompatibility() { $this->assertTrue(Settings::getCompatibility()); $this->assertTrue(Settings::setCompatibility(false)); @@ -30,12 +30,23 @@ class SettingsTest extends \PHPUnit_Framework_TestCase } /** - * Get/set zip class + * Test set/get zip class */ - public function testGetSetZipClass() + public function testSetGetZipClass() { $this->assertEquals(Settings::ZIPARCHIVE, Settings::getZipClass()); $this->assertTrue(Settings::setZipClass(Settings::PCLZIP)); $this->assertFalse(Settings::setZipClass('foo')); } + + public function testSetGetPdfRenderer() + { + $domPdfPath = realpath(PHPWORD_TESTS_BASE_DIR . '/../vendor/dompdf/dompdf'); + + $this->assertFalse(Settings::setPdfRenderer('FOO', 'dummy/path')); + $this->assertTrue(Settings::setPdfRenderer(Settings::PDF_RENDERER_DOMPDF, $domPdfPath)); + $this->assertEquals(Settings::PDF_RENDERER_DOMPDF, Settings::getPdfRendererName()); + $this->assertEquals($domPdfPath, Settings::getPdfRendererPath()); + $this->assertFalse(Settings::setPdfRendererPath('dummy/path')); + } } diff --git a/tests/PhpWord/Tests/Writer/PDF/DomPDFTest.php b/tests/PhpWord/Tests/Writer/PDF/DomPDFTest.php new file mode 100644 index 00000000..3282eecb --- /dev/null +++ b/tests/PhpWord/Tests/Writer/PDF/DomPDFTest.php @@ -0,0 +1,70 @@ +addSection(); + $section->addText('Test 1'); + + $rendererName = Settings::PDF_RENDERER_DOMPDF; + $rendererLibraryPath = realpath(PHPWORD_TESTS_BASE_DIR . '/../vendor/dompdf/dompdf'); + Settings::setPdfRenderer($rendererName, $rendererLibraryPath); + $writer = new PDF($phpWord); + $writer->save($file); + + $this->assertTrue(file_exists($file)); + + unlink($file); + } + + /** + * Test set/get abstract renderer properties + */ + public function testSetGetAbstractRendererProperties() + { + define('DOMPDF_ENABLE_AUTOLOAD', false); + $file = __DIR__ . "/../../_files/temp.pdf"; + + $rendererName = Settings::PDF_RENDERER_DOMPDF; + $rendererLibraryPath = realpath(PHPWORD_TESTS_BASE_DIR . '/../vendor/dompdf/dompdf'); + Settings::setPdfRenderer($rendererName, $rendererLibraryPath); + $writer = new PDF(new PhpWord()); + + $writer->setFont('arial'); + $this->assertEquals('arial', $writer->getFont()); + + $writer->setPaperSize(); + $this->assertEquals(9, $writer->getPaperSize()); + + $writer->setOrientation(); + $this->assertEquals('default', $writer->getOrientation()); + + $writer->setTempDir(sys_get_temp_dir()); + $this->assertEquals(sys_get_temp_dir(), $writer->getTempDir()); + } +} diff --git a/tests/PhpWord/Tests/Writer/PDFTest.php b/tests/PhpWord/Tests/Writer/PDFTest.php new file mode 100644 index 00000000..fba93054 --- /dev/null +++ b/tests/PhpWord/Tests/Writer/PDFTest.php @@ -0,0 +1,51 @@ +save($file); + + $this->assertTrue(file_exists($file)); + + unlink($file); + } + + /** + * Test construct exception + * + * @expectedException \PhpOffice\PhpWord\Exception\Exception + * @expectedExceptionMessage PDF rendering library or library path has not been defined. + */ + public function testConstructException() + { + $writer = new PDF(new PhpWord()); + } +} diff --git a/tests/PhpWord/Tests/Writer/Word2007/BaseTest.php b/tests/PhpWord/Tests/Writer/Word2007/BaseTest.php index 904f45bd..05002578 100644 --- a/tests/PhpWord/Tests/Writer/Word2007/BaseTest.php +++ b/tests/PhpWord/Tests/Writer/Word2007/BaseTest.php @@ -159,6 +159,11 @@ class BaseTest extends \PHPUnit_Framework_TestCase $section->addImage(__DIR__ . "/../../_files/images/earth.jpg", $styles); } + $archiveFile = realpath(__DIR__ . '/../../_files/documents/reader.docx'); + $imageFile = 'word/media/image1.jpeg'; + $source = 'zip://D:\www\local\phpword\tests\PhpWord\Tests\_files\documents\reader.docx#' . $imageFile; + $section->addImage($source); + $doc = TestHelperDOCX::getDocument($phpWord); // behind From 20b363f00ee146579e2b4450f795f7a183489c5e Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 07:54:55 +0700 Subject: [PATCH 02/10] Remove local path from sample and add DomPDF in `composer.json` `require-dev` --- composer.json | 3 ++- samples/Sample_Header.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 6a2d9573..e06d5005 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "ext-zip": "*" }, "require-dev": { - "phpunit/phpunit": "3.7.*" + "phpunit/phpunit": "3.7.*", + "dompdf/dompdf" : "0.6.*" }, "suggest": { "ext-gd2": "*", diff --git a/samples/Sample_Header.php b/samples/Sample_Header.php index 2ab5a579..6fe43507 100644 --- a/samples/Sample_Header.php +++ b/samples/Sample_Header.php @@ -16,7 +16,7 @@ $writers = array('Word2007' => 'docx', 'ODText' => 'odt', 'RTF' => 'rtf', 'HTML' // Set PDF renderer $rendererName = \PhpOffice\PhpWord\Settings::PDF_RENDERER_DOMPDF; -$rendererLibraryPath = 'D:\www\local\minerva\dompdf'; // DomPDF library path +$rendererLibraryPath = ''; // DomPDF library path if (!\PhpOffice\PhpWord\Settings::setPdfRenderer($rendererName, $rendererLibraryPath)) { $writers['PDF'] = null; From 04b14ea42dfd661b9f114e6aa41ea6de6c1e2e83 Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 08:04:18 +0700 Subject: [PATCH 03/10] Fix `Writer\Word2007\BaseTest` error --- tests/PhpWord/Tests/Writer/Word2007/BaseTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhpWord/Tests/Writer/Word2007/BaseTest.php b/tests/PhpWord/Tests/Writer/Word2007/BaseTest.php index 05002578..ba01cb3e 100644 --- a/tests/PhpWord/Tests/Writer/Word2007/BaseTest.php +++ b/tests/PhpWord/Tests/Writer/Word2007/BaseTest.php @@ -161,7 +161,7 @@ class BaseTest extends \PHPUnit_Framework_TestCase $archiveFile = realpath(__DIR__ . '/../../_files/documents/reader.docx'); $imageFile = 'word/media/image1.jpeg'; - $source = 'zip://D:\www\local\phpword\tests\PhpWord\Tests\_files\documents\reader.docx#' . $imageFile; + $source = 'zip://' . $archiveFile . '#' . $imageFile; $section->addImage($source); $doc = TestHelperDOCX::getDocument($phpWord); From 9c738f7eae18e613764d796367570481a21398f4 Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 09:50:56 +0700 Subject: [PATCH 04/10] Refactor Element\Image and some samples --- samples/Sample_07_TemplateCloneRow.php | 6 +- samples/Sample_23_TemplateBlock.php | 6 +- samples/Sample_Header.php | 19 ++- src/PhpWord/Element/Image.php | 164 +++++++++++++++---------- 4 files changed, 122 insertions(+), 73 deletions(-) diff --git a/samples/Sample_07_TemplateCloneRow.php b/samples/Sample_07_TemplateCloneRow.php index 3049466d..0e79e69f 100755 --- a/samples/Sample_07_TemplateCloneRow.php +++ b/samples/Sample_07_TemplateCloneRow.php @@ -60,4 +60,8 @@ echo date('H:i:s'), " Write to Word2007 format", EOL; $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 bbde5a72..d0856ec2 100644 --- a/samples/Sample_23_TemplateBlock.php +++ b/samples/Sample_23_TemplateBlock.php @@ -18,4 +18,8 @@ echo date('H:i:s'), " Write to Word2007 format", EOL; $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 6fe43507..210d9ee4 100644 --- a/samples/Sample_Header.php +++ b/samples/Sample_Header.php @@ -46,18 +46,17 @@ if ($handle = opendir('.')) { } /** - * 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; diff --git a/src/PhpWord/Element/Image.php b/src/PhpWord/Element/Image.php index 19451c9e..f7752b27 100755 --- a/src/PhpWord/Element/Image.php +++ b/src/PhpWord/Element/Image.php @@ -18,6 +18,13 @@ use PhpOffice\PhpWord\Style\Image as ImageStyle; */ 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 * @@ -199,89 +213,51 @@ class Image extends AbstractElement */ private function checkImage($source) { - $isArchive = strpos($source, 'zip://') !== false; + $this->setSourceType($source); - // Check is memory image - if (stripos(strrev($source), strrev('.php')) === 0) { - $this->isMemImage = true; - } elseif ($isArchive) { - $this->isMemImage = false; - } else { - $this->isMemImage = (filter_var($source, FILTER_VALIDATE_URL) !== false); - } - - // Define supported types - if ($this->isMemImage) { - $supportedTypes = array( - IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG - ); - } else { - $supportedTypes = array( - IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG, - IMAGETYPE_BMP, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM - ); - } - - // Check from zip file or actual file - if ($isArchive) { - $imageData = $this->getArchivedImageSize($source); + // Check image data + if ($this->sourceType == self::SOURCE_ARCHIVE) { + $imageData = $this->getArchiveImageSize($source); } else { $imageData = @getimagesize($source); } - - // Check if image exists by detecting image data if (!is_array($imageData)) { throw new InvalidImageException(); } - // Put image data into variables list($actualWidth, $actualHeight, $imageType) = $imageData; - // Check if image type is supported + + // 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); - switch ($this->imageType) { - case 'image/png': - $this->imageCreateFunc = 'imagecreatefrompng'; - $this->imageFunc = 'imagepng'; - $this->imageExtension = 'png'; - break; - case 'image/gif': - $this->imageCreateFunc = 'imagecreatefromgif'; - $this->imageFunc = 'imagegif'; - $this->imageExtension = 'gif'; - break; - case 'image/jpeg': - $this->imageCreateFunc = 'imagecreatefromjpeg'; - $this->imageFunc = 'imagejpeg'; - $this->imageExtension = 'jpg'; - break; - case 'image/bmp': - case 'image/x-ms-bmp': - $this->imageType = 'image/bmp'; - $this->imageExtension = 'bmp'; - break; - case 'image/tiff': - $this->imageExtension = 'tif'; - break; - } + $this->setFunctions(); + $this->setProportionalSize($actualWidth, $actualHeight); + } - // Check image width & height - $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)); - } + /** + * 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; } - } /** @@ -290,7 +266,7 @@ class Image extends AbstractElement * @param string $source * @return array|null */ - private function getArchivedImageSize($source) + private function getArchiveImageSize($source) { $imageData = null; $source = substr($source, 6); @@ -312,4 +288,56 @@ class Image extends AbstractElement return $imageData; } + + /** + * Set image functions and extensions + */ + private function setFunctions() + { + switch ($this->imageType) { + case 'image/png': + $this->imageCreateFunc = 'imagecreatefrompng'; + $this->imageFunc = 'imagepng'; + $this->imageExtension = 'png'; + break; + case 'image/gif': + $this->imageCreateFunc = 'imagecreatefromgif'; + $this->imageFunc = 'imagegif'; + $this->imageExtension = 'gif'; + break; + case 'image/jpeg': + case 'image/jpg': + $this->imageCreateFunc = 'imagecreatefromjpeg'; + $this->imageFunc = 'imagejpeg'; + $this->imageExtension = 'jpg'; + break; + case 'image/bmp': + case 'image/x-ms-bmp': + $this->imageType = 'image/bmp'; + $this->imageExtension = 'bmp'; + break; + case 'image/tiff': + $this->imageExtension = 'tif'; + break; + } + } + + /** + * Set proportional width/height if one dimension not available + */ + 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)); + } + } + } } From 2829fd82160df459c96065efdd2703b82745046a Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 12:12:32 +0700 Subject: [PATCH 05/10] Enable image in HTML writer --- CHANGELOG.md | 4 +- docs/intro.rst | 114 ++++++++++---------- src/PhpWord/Element/Image.php | 13 +++ src/PhpWord/Writer/AbstractWriter.php | 34 +++--- src/PhpWord/Writer/HTML.php | 60 ++++++++++- src/PhpWord/Writer/PDF/AbstractRenderer.php | 30 +----- src/PhpWord/Writer/Word2007.php | 2 + tests/PhpWord/Tests/SettingsTest.php | 3 + 8 files changed, 161 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b06aa79c..0b9cf9a2 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,8 +32,8 @@ This release marked heavy refactorings on internal code structure with the creat - 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) - @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/docs/intro.rst b/docs/intro.rst index 0f1f3d74..53a352fe 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/src/PhpWord/Element/Image.php b/src/PhpWord/Element/Image.php index f7752b27..e4e91fe6 100755 --- a/src/PhpWord/Element/Image.php +++ b/src/PhpWord/Element/Image.php @@ -126,6 +126,16 @@ class Image extends AbstractElement return $this->source; } + /** + * Get image source type + * + * @return string + */ + public function getSourceType() + { + return $this->sourceType; + } + /** * Get image media ID * @@ -324,6 +334,9 @@ class Image extends AbstractElement /** * Set proportional width/height if one dimension not available + * + * @param integer $actualWidth + * @param integer $actualHeight */ private function setProportionalSize($actualWidth, $actualHeight) { diff --git a/src/PhpWord/Writer/AbstractWriter.php b/src/PhpWord/Writer/AbstractWriter.php index 9403d724..08785ecd 100644 --- a/src/PhpWord/Writer/AbstractWriter.php +++ b/src/PhpWord/Writer/AbstractWriter.php @@ -164,11 +164,7 @@ abstract class AbstractWriter implements WriterInterface protected function getTempFile($filename) { // Temporary directory - $tempDir = sys_get_temp_dir() . '/PHPWordMedia/'; - if (!is_dir($tempDir)) { - mkdir($tempDir); - } - $this->tempDir = $tempDir; + $this->setTempDir(); // Temporary file $this->originalFilename = $filename; @@ -184,23 +180,30 @@ abstract class AbstractWriter implements WriterInterface } /** - * Cleanup temporary file - * - * If a temporary file was used, copy it to the correct file stream + * Get temporary directory */ protected function getTempDir() { return $this->tempDir; } + /** + * Set temporary directory + */ + protected function setTempDir() + { + $tempDir = sys_get_temp_dir() . '/PHPWordMedia/'; + if (!is_dir($tempDir)) { + mkdir($tempDir); + } + $this->tempDir = $tempDir; + } + /** * Cleanup temporary file - * - * If a temporary file was used, copy it to the correct file stream */ protected function cleanupTempFile() { - // File if ($this->originalFilename != $this->tempFilename) { if (copy($this->tempFilename, $this->originalFilename) === false) { throw new Exception("Could not copy temporary zip file {$this->tempFilename} to {$this->originalFilename}."); @@ -208,7 +211,14 @@ abstract class AbstractWriter implements WriterInterface @unlink($this->tempFilename); } - // Directory + $this->clearTempDir(); + } + + /** + * Clear temporary directory + */ + protected function clearTempDir() + { if (is_dir($this->tempDir)) { $this->deleteDir($this->tempDir); } diff --git a/src/PhpWord/Writer/HTML.php b/src/PhpWord/Writer/HTML.php index f0720f66..1205f112 100644 --- a/src/PhpWord/Writer/HTML.php +++ b/src/PhpWord/Writer/HTML.php @@ -36,6 +36,13 @@ use PhpOffice\PhpWord\TOC; */ class HTML extends AbstractWriter implements WriterInterface { + /** + * Is the current writer creating PDF? + * + * @var boolean + */ + protected $isPdf = false; + /** * Create new instance */ @@ -53,9 +60,11 @@ class HTML extends AbstractWriter implements WriterInterface public function save($filename = null) { if (!is_null($this->getPhpWord())) { + $this->setTempDir(); $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."); } @@ -421,7 +430,18 @@ class HTML extends AbstractWriter implements WriterInterface */ 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)) { + $html = ''; + if (!$withoutP) { + $html = '

' . $html . '

' . PHP_EOL; + } + } + } + + return $html; } /** @@ -597,4 +617,42 @@ class HTML extends AbstractWriter implements WriterInterface return $string; } + + /** + * Get Base64 image data + * + * @return string|null + */ + private function getBase64ImageData(Image $element) + { + $imageData = null; + $source = $element->getSource(); + $imageType = $element->getImageType(); + + // Get actual source + if ($element->getSourceType() == 'archive') { + $source = substr($source, 6); + list($zipFilename, $imageFilename) = explode('#', $source); + $zip = new \ZipArchive(); + if ($zip->open($zipFilename) !== false) { + if ($zip->locateName($imageFilename)) { + $zip->extractTo($this->getTempDir(), $imageFilename); + $actualSource = $this->getTempDir() . DIRECTORY_SEPARATOR . $imageFilename; + } + } + $zip->close(); + } else { + $actualSource = $source; + } + + // Read image binary data and convert into Base64 + if ($fp = fopen($actualSource, "rb", 0)) { + $image = fread($fp, filesize($actualSource)); + fclose($fp); + $base64 = chunk_split(base64_encode($image)); + $imageData = 'data:' . $imageType . ';base64,' . $base64; + } + + return $imageData; + } } diff --git a/src/PhpWord/Writer/PDF/AbstractRenderer.php b/src/PhpWord/Writer/PDF/AbstractRenderer.php index be48a9f2..64f196e9 100644 --- a/src/PhpWord/Writer/PDF/AbstractRenderer.php +++ b/src/PhpWord/Writer/PDF/AbstractRenderer.php @@ -69,7 +69,6 @@ abstract class AbstractRenderer extends \PhpOffice\PhpWord\Writer\HTML public function __construct(PhpWord $phpWord) { parent::__construct($phpWord); - $this->setTempDir(sys_get_temp_dir()); } /** @@ -141,36 +140,11 @@ abstract class AbstractRenderer extends \PhpOffice\PhpWord\Writer\HTML return $this; } - /** - * Get temporary storage directory - * - * @return string - */ - public function getTempDir() - { - return $this->tempDir; - } - - /** - * Set temporary storage directory - * - * @param string $pValue Temporary storage directory - * @return self - */ - public function setTempDir($pValue = '') - { - if (is_dir($pValue)) { - $this->tempDir = $pValue; - } else { - throw new Exception("Directory does not exist: $pValue"); - } - return $this; - } - /** * Save PhpWord to PDF file, pre-save * * @param string $pFilename Name of the file to save as + * @return resource */ protected function prepareForSave($pFilename = null) { @@ -178,6 +152,8 @@ abstract class AbstractRenderer extends \PhpOffice\PhpWord\Writer\HTML if ($fileHandle === false) { throw new Exception("Could not open file $pFilename for writing."); } + $this->isPdf = true; + return $fileHandle; } diff --git a/src/PhpWord/Writer/Word2007.php b/src/PhpWord/Writer/Word2007.php index 0d0c0f69..920a159c 100755 --- a/src/PhpWord/Writer/Word2007.php +++ b/src/PhpWord/Writer/Word2007.php @@ -181,6 +181,8 @@ class Word2007 extends AbstractWriter implements WriterInterface /** * Add file to package * + * Get the actual source from an archive image + * * @param mixed $objZip * @param string $source * @param string $target diff --git a/tests/PhpWord/Tests/SettingsTest.php b/tests/PhpWord/Tests/SettingsTest.php index 46efdf76..e7968355 100644 --- a/tests/PhpWord/Tests/SettingsTest.php +++ b/tests/PhpWord/Tests/SettingsTest.php @@ -39,6 +39,9 @@ class SettingsTest extends \PHPUnit_Framework_TestCase $this->assertFalse(Settings::setZipClass('foo')); } + /** + * Test set/get PDF renderer + */ public function testSetGetPdfRenderer() { $domPdfPath = realpath(PHPWORD_TESTS_BASE_DIR . '/../vendor/dompdf/dompdf'); From 406534cd429963693c21be1dfcfcecc763379d8c Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 13:37:48 +0700 Subject: [PATCH 06/10] Fix unit test error --- src/PhpWord/Writer/AbstractWriter.php | 66 +++++++++++++++------------ src/PhpWord/Writer/HTML.php | 2 +- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/PhpWord/Writer/AbstractWriter.php b/src/PhpWord/Writer/AbstractWriter.php index 08785ecd..8373b6bd 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 * @@ -62,13 +69,6 @@ abstract class AbstractWriter implements WriterInterface */ private $tempFilename; - /** - * Temporary directory - * - * @var string - */ - private $tempDir; - /** * Get PhpWord object * @@ -88,7 +88,7 @@ abstract class AbstractWriter implements WriterInterface * Set PhpWord object * * @param PhpWord - * @return $this + * @return self */ public function setPhpWord(PhpWord $phpWord = null) { @@ -126,7 +126,7 @@ abstract class AbstractWriter implements WriterInterface * * @param boolean $pValue * @param string $pDirectory - * @return $this + * @return self */ public function setUseDiskCaching($pValue = false, $pDirectory = null) { @@ -153,6 +153,32 @@ abstract class AbstractWriter implements WriterInterface 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 * @@ -164,7 +190,7 @@ abstract class AbstractWriter implements WriterInterface protected function getTempFile($filename) { // Temporary directory - $this->setTempDir(); + $this->setTempDir(sys_get_temp_dir() . '/PHPWordWriter/'); // Temporary file $this->originalFilename = $filename; @@ -179,26 +205,6 @@ abstract class AbstractWriter implements WriterInterface return $this->tempFilename; } - /** - * Get temporary directory - */ - protected function getTempDir() - { - return $this->tempDir; - } - - /** - * Set temporary directory - */ - protected function setTempDir() - { - $tempDir = sys_get_temp_dir() . '/PHPWordMedia/'; - if (!is_dir($tempDir)) { - mkdir($tempDir); - } - $this->tempDir = $tempDir; - } - /** * Cleanup temporary file */ diff --git a/src/PhpWord/Writer/HTML.php b/src/PhpWord/Writer/HTML.php index 1205f112..07d23e27 100644 --- a/src/PhpWord/Writer/HTML.php +++ b/src/PhpWord/Writer/HTML.php @@ -60,7 +60,7 @@ class HTML extends AbstractWriter implements WriterInterface public function save($filename = null) { if (!is_null($this->getPhpWord())) { - $this->setTempDir(); + $this->setTempDir(sys_get_temp_dir() . '/PHPWordWriter/'); $hFile = fopen($filename, 'w') or die("can't open file"); fwrite($hFile, $this->writeDocument()); fclose($hFile); From 3d8ae044b73044dd2173f10e5e674126f71bd37b Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 14:45:43 +0700 Subject: [PATCH 07/10] Set image size and additional tests for HTML writer --- src/PhpWord/Writer/HTML.php | 44 ++++++++++++++++--------- tests/PhpWord/Tests/Writer/HTMLTest.php | 13 +++++--- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/PhpWord/Writer/HTML.php b/src/PhpWord/Writer/HTML.php index 07d23e27..3baa15e0 100644 --- a/src/PhpWord/Writer/HTML.php +++ b/src/PhpWord/Writer/HTML.php @@ -27,7 +27,6 @@ use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style\Font; use PhpOffice\PhpWord\Style\Paragraph; -use PhpOffice\PhpWord\TOC; /** * HTML writer @@ -170,10 +169,10 @@ class HTML extends AbstractWriter implements WriterInterface $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); } } } @@ -257,10 +256,10 @@ class HTML extends AbstractWriter implements WriterInterface $html .= $this->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; @@ -402,10 +401,10 @@ class HTML extends AbstractWriter implements WriterInterface $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 { @@ -434,9 +433,13 @@ class HTML extends AbstractWriter implements WriterInterface if (!$this->isPdf) { $imageData = $this->getBase64ImageData($element); if (!is_null($imageData)) { - $html = ''; + $style = $this->assembleCss(array( + 'width' => $element->getStyle()->getWidth() . 'px', + 'height' => $element->getStyle()->getHeight() . 'px', + )); + $html = ""; if (!$withoutP) { - $html = '

' . $html . '

' . PHP_EOL; + $html = "

{$html}

" . PHP_EOL; } } } @@ -626,11 +629,12 @@ class HTML extends AbstractWriter implements WriterInterface private function getBase64ImageData(Image $element) { $imageData = null; + $imageBinary = null; $source = $element->getSource(); $imageType = $element->getImageType(); - // Get actual source - if ($element->getSourceType() == 'archive') { + // Get actual source from archive image + if ($element->getSourceType() == Image::SOURCE_ARCHIVE) { $source = substr($source, 6); list($zipFilename, $imageFilename) = explode('#', $source); $zip = new \ZipArchive(); @@ -646,10 +650,20 @@ class HTML extends AbstractWriter implements WriterInterface } // Read image binary data and convert into Base64 - if ($fp = fopen($actualSource, "rb", 0)) { - $image = fread($fp, filesize($actualSource)); - fclose($fp); - $base64 = chunk_split(base64_encode($image)); + if ($element->getSourceType() == Image::SOURCE_GD) { + $imageResource = call_user_func($element->getImageCreateFunction(), $actualSource); + ob_start(); + call_user_func($element->getImageFunction(), $imageResource); + $imageBinary = ob_get_contents(); + ob_end_clean(); + } else { + if ($fp = fopen($actualSource, 'rb', false)) { + $imageBinary = fread($fp, filesize($actualSource)); + fclose($fp); + } + } + if (!is_null($imageBinary)) { + $base64 = chunk_split(base64_encode($imageBinary)); $imageData = 'data:' . $imageType . ';base64,' . $base64; } diff --git a/tests/PhpWord/Tests/Writer/HTMLTest.php b/tests/PhpWord/Tests/Writer/HTMLTest.php index 40ea88b0..354da0e6 100644 --- a/tests/PhpWord/Tests/Writer/HTMLTest.php +++ b/tests/PhpWord/Tests/Writer/HTMLTest.php @@ -45,7 +45,9 @@ class HTMLTest extends \PHPUnit_Framework_TestCase */ public function testSave() { - $imageSrc = __DIR__ . "/../_files/images/PhpWord.png"; + $localImage = __DIR__ . "/../_files/images/PhpWord.png"; + $archiveImage = 'zip://' . __DIR__ . '/../_files/documents/reader.docx#word/media/image1.jpeg'; + $gdImage = 'http://php.net/images/logos/php-med-trans-light.gif'; $objectSrc = __DIR__ . "/../_files/documents/sheet.xls"; $file = __DIR__ . "/../_files/temp.html"; @@ -64,7 +66,9 @@ class HTMLTest extends \PHPUnit_Framework_TestCase $section->addTitle('Test', 1); $section->addPageBreak(); $section->addListItem('Test'); - $section->addImage($imageSrc); + $section->addImage($localImage); + $section->addImage($archiveImage); + $section->addImage($gdImage); $section->addObject($objectSrc); $section->addFootnote(); $section->addEndnote(); @@ -77,7 +81,7 @@ class HTMLTest extends \PHPUnit_Framework_TestCase $textrun = $section->addTextRun('Paragraph'); $textrun->addLink('http://test.com'); - $textrun->addImage($imageSrc); + $textrun->addImage($localImage); $textrun->addFootnote(); $textrun->addEndnote(); @@ -90,10 +94,11 @@ class HTMLTest extends \PHPUnit_Framework_TestCase $cell->addLink('http://test.com'); $cell->addTextBreak(); $cell->addListItem('Test'); - $cell->addImage($imageSrc); + $cell->addImage($localImage); $cell->addObject($objectSrc); $cell->addFootnote(); $cell->addEndnote(); + $cell = $table->addRow()->addCell(); $writer = new HTML($phpWord); $writer->save($file); From 6612ee88c01ea5d957f269ea09a4126e080bd541 Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 15:24:38 +0700 Subject: [PATCH 08/10] Update composer.json and .travis.yml as per #205 --- .travis.yml | 3 ++- composer.json | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f322426d..c9a91c8d 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 - composer install --prefer-source - - composer selfupdate --quiet ## PHP_CodeSniffer - pyrus install pear/PHP_CodeSniffer - phpenv rehash diff --git a/composer.json b/composer.json index e06d5005..97d3e003 100644 --- a/composer.json +++ b/composer.json @@ -37,13 +37,13 @@ "ext-zip": "*" }, "require-dev": { - "phpunit/phpunit": "3.7.*", - "dompdf/dompdf" : "0.6.*" + "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": { From 1ed13cc036ee33c0099d1640c9634793131638b5 Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 15:28:18 +0700 Subject: [PATCH 09/10] Include dompdf version --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c9a91c8d..ad3df613 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ before_script: # - curl -s http://getcomposer.org/installer | php # - php composer.phar install --prefer-source - composer self-update - - composer require dompdf/dompdf + - composer require dompdf/dompdf:0.6.* - composer install --prefer-source ## PHP_CodeSniffer - pyrus install pear/PHP_CodeSniffer From 52b3506bbb5f0bedd8d6b3490e12dbc3bb70b2af Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Wed, 16 Apr 2014 17:17:42 +0700 Subject: [PATCH 10/10] HTML Writer: Enable footnotes and endnotes --- CHANGELOG.md | 2 +- docs/intro.rst | 4 +- src/PhpWord/Writer/HTML.php | 104 ++++++++++++++++++++++++++++++------ 3 files changed, 92 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b9cf9a2..759f41e8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ This release marked heavy refactorings on internal code structure with the creat - 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: text, textrun, link, title, textbreak, table, image (as Base64) - @ivanlanin GH-203 GH-67 GH-147 +- 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/docs/intro.rst b/docs/intro.rst index 53a352fe..a617777d 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -101,9 +101,9 @@ Writers + +-----------------------+--------+-------+-------+-------+-------+ | | Footer | ✓ | | | | | + +-----------------------+--------+-------+-------+-------+-------+ -| | Footnote | ✓ | | | | | +| | Footnote | ✓ | | | ✓ | | + +-----------------------+--------+-------+-------+-------+-------+ -| | Endnote | ✓ | | | | | +| | Endnote | ✓ | | | ✓ | | +-------------------------+-----------------------+--------+-------+-------+-------+-------+ | **Graphs** | 2D basic graphs | | | | | | + +-----------------------+--------+-------+-------+-------+-------+ diff --git a/src/PhpWord/Writer/HTML.php b/src/PhpWord/Writer/HTML.php index 3baa15e0..bd98e893 100644 --- a/src/PhpWord/Writer/HTML.php +++ b/src/PhpWord/Writer/HTML.php @@ -22,7 +22,9 @@ use PhpOffice\PhpWord\Element\Text; 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; @@ -42,6 +44,13 @@ class HTML extends AbstractWriter implements WriterInterface */ protected $isPdf = false; + /** + * Footnotes and endnotes collection + * + * @var array + */ + protected $notes = array(); + /** * Create new instance */ @@ -85,6 +94,7 @@ class HTML extends AbstractWriter implements WriterInterface $html .= '' . PHP_EOL; $html .= '' . PHP_EOL; $html .= $this->writeHTMLBody(); + $html .= $this->writeNotes(); $html .= '' . PHP_EOL; $html .= '' . PHP_EOL; @@ -181,6 +191,32 @@ class HTML extends AbstractWriter implements WriterInterface 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 * @@ -226,19 +262,20 @@ class HTML extends AbstractWriter implements WriterInterface } /** - * 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 .= 'writeFootnote($element); } } - $html .= '

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

'; + $html .= PHP_EOL; } return $html; @@ -279,11 +317,14 @@ class HTML extends AbstractWriter implements WriterInterface { $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; } @@ -467,7 +508,7 @@ class HTML extends AbstractWriter implements WriterInterface */ private function writeFootnote($element) { - return $this->writeUnsupportedElement($element, true); + return $this->writeNote($element); } /** @@ -478,7 +519,25 @@ class HTML extends AbstractWriter implements WriterInterface */ 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; } /** @@ -509,18 +568,33 @@ class HTML extends AbstractWriter implements WriterInterface */ private function writeStyles() { - $bodyCss = array(); $css = '