From f829559f65ba517f635771ad7a83d1f3018ab6f5 Mon Sep 17 00:00:00 2001 From: Ivan Lanin Date: Fri, 18 Apr 2014 23:12:51 +0700 Subject: [PATCH] ODT Writer: Basic image writing support --- CHANGELOG.md | 1 + docs/intro.rst | 2 +- src/PhpWord/Element/Image.php | 54 ++++++++++++ src/PhpWord/Media.php | 19 +++-- src/PhpWord/Writer/AbstractWriter.php | 76 ++++++++++++++++- src/PhpWord/Writer/ODText.php | 14 +++- src/PhpWord/Writer/ODText/Content.php | 111 +++++++++++++++++-------- src/PhpWord/Writer/ODText/Manifest.php | 14 +++- src/PhpWord/Writer/Word2007.php | 107 +++++++----------------- src/PhpWord/Writer/Word2007/Rels.php | 8 +- 10 files changed, 281 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 587d0ad5..0ada7257 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ This release marked heavy refactorings on internal code structure with the creat - PDF Writer: Basic PDF writer using DomPDF: All HTML element except image - @ivanlanin GH-68 - DOCX Writer: Change `docProps/app.xml` `Application` to `PHPWord` - @ivanlanin - DOCX Writer: Create `word/settings.xml` and `word/webSettings.xml` dynamically - @ivanlanin +- ODT Writer: Basic image writing - @ivanlanin ### Bugfixes diff --git a/docs/intro.rst b/docs/intro.rst index a617777d..2c5d413d 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -89,7 +89,7 @@ Writers + +-----------------------+--------+-------+-------+-------+-------+ | | Table | ✓ | ✓ | | ✓ | ✓ | + +-----------------------+--------+-------+-------+-------+-------+ -| | Image | ✓ | | | ✓ | | +| | Image | ✓ | ✓ | | ✓ | | + +-----------------------+--------+-------+-------+-------+-------+ | | Object | ✓ | | | | | + +-----------------------+--------+-------+-------+-------+-------+ diff --git a/src/PhpWord/Element/Image.php b/src/PhpWord/Element/Image.php index a2a33793..53becbbf 100755 --- a/src/PhpWord/Element/Image.php +++ b/src/PhpWord/Element/Image.php @@ -89,6 +89,20 @@ class Image extends AbstractElement */ private $isMemImage; + /** + * Image target file name + * + * @var string + */ + private $target; + + /** + * Image media index + * + * @var integer + */ + private $mediaIndex; + /** * Create new image element * @@ -217,6 +231,46 @@ class Image extends AbstractElement return $this->isMemImage; } + /** + * Get target file name + * + * @return string + */ + public function getTarget() + { + return $this->target; + } + + /** + * Set target file name + * + * @param string $value + */ + public function setTarget($value) + { + $this->target = $value; + } + + /** + * Get media index + * + * @return integer + */ + public function getMediaIndex() + { + return $this->mediaIndex; + } + + /** + * Set media index + * + * @param integer $value + */ + public function setMediaIndex($value) + { + $this->mediaIndex = $value; + } + /** * Check memory image, supported type, image functions, and proportional width/height * diff --git a/src/PhpWord/Media.php b/src/PhpWord/Media.php index 8c06fe81..a4cd4912 100755 --- a/src/PhpWord/Media.php +++ b/src/PhpWord/Media.php @@ -36,7 +36,7 @@ class Media * @since 0.9.2 * @since 0.10.0 */ - public static function addElement($container, $mediaType, $source, Image $image = null) + public static function addElement($container, $mediaType, $source, Image &$image = null) { // Assign unique media Id and initiate media container if none exists $mediaId = md5($container . $source); @@ -48,10 +48,10 @@ class Media if (!array_key_exists($mediaId, self::$elements[$container])) { $mediaCount = self::countElements($container); $mediaTypeCount = self::countElements($container, $mediaType); - $mediaData = array(); + $mediaTypeCount++; $rId = ++$mediaCount; $target = null; - $mediaTypeCount++; + $mediaData = array('mediaIndex' => $mediaTypeCount); switch ($mediaType) { // Images @@ -68,12 +68,14 @@ class Media $mediaData['createFunction'] = $image->getImageCreateFunction(); $mediaData['imageFunction'] = $image->getImageFunction(); } - $target = "media/{$container}_image{$mediaTypeCount}.{$extension}"; + $target = "{$container}_image{$mediaTypeCount}.{$extension}"; + $image->setTarget($target); + $image->setMediaIndex($mediaTypeCount); break; // Objects case 'object': - $target = "embeddings/{$container}_oleObject{$mediaTypeCount}.bin"; + $target = "{$container}_oleObject{$mediaTypeCount}.bin"; break; // Links @@ -89,7 +91,12 @@ class Media self::$elements[$container][$mediaId] = $mediaData; return $rId; } else { - return self::$elements[$container][$mediaId]['rID']; + $mediaData = self::$elements[$container][$mediaId]; + if (!is_null($image)) { + $image->setTarget($mediaData['target']); + $image->setMediaIndex($mediaData['mediaIndex']); + } + return $mediaData['rID']; } } diff --git a/src/PhpWord/Writer/AbstractWriter.php b/src/PhpWord/Writer/AbstractWriter.php index 8e5a23d1..a909cd26 100644 --- a/src/PhpWord/Writer/AbstractWriter.php +++ b/src/PhpWord/Writer/AbstractWriter.php @@ -30,10 +30,17 @@ abstract class AbstractWriter implements WriterInterface /** * Individual writers * - * @var mixed + * @var array */ protected $writerParts = array(); + /** + * Paths to store media files + * + * @var array + */ + protected $mediaPaths = array('image' => '', 'object' => ''); + /** * Use disk caching * @@ -263,6 +270,73 @@ abstract class AbstractWriter implements WriterInterface return $objZip; } + /** + * Add files to package + * + * @param mixed $objZip + * @param mixed $elements + */ + protected function addFilesToPackage($objZip, $elements) + { + foreach ($elements as $element) { + $type = $element['type']; // image|object|link + + // Skip nonregistered types and set target + if (!array_key_exists($type, $this->mediaPaths)) { + continue; + } + $target = $this->mediaPaths[$type] . $element['target']; + + // Retrive GD image content or get local media + if (isset($element['isMemImage']) && $element['isMemImage']) { + $image = call_user_func($element['createFunction'], $element['source']); + ob_start(); + call_user_func($element['imageFunction'], $image); + $imageContents = ob_get_contents(); + ob_end_clean(); + $objZip->addFromString($target, $imageContents); + imagedestroy($image); + } else { + $this->addFileToPackage($objZip, $element['source'], $target); + } + } + } + + /** + * Add file to package + * + * Get the actual source from an archive image + * + * @param mixed $objZip + * @param string $source + * @param string $target + */ + protected function addFileToPackage($objZip, $source, $target) + { + $isArchive = strpos($source, 'zip://') !== false; + $actualSource = null; + if ($isArchive) { + $source = substr($source, 6); + list($zipFilename, $imageFilename) = explode('#', $source); + + $zipClass = \PhpOffice\PhpWord\Settings::getZipClass(); + $zip = new $zipClass(); + 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, $target); + } + } + /** * Delete directory * diff --git a/src/PhpWord/Writer/ODText.php b/src/PhpWord/Writer/ODText.php index b050ef44..7b99d050 100755 --- a/src/PhpWord/Writer/ODText.php +++ b/src/PhpWord/Writer/ODText.php @@ -9,8 +9,9 @@ namespace PhpOffice\PhpWord\Writer; -use PhpOffice\PhpWord\Exception\Exception; +use PhpOffice\PhpWord\Media; use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\Exception\Exception; use PhpOffice\PhpWord\Writer\ODText\Content; use PhpOffice\PhpWord\Writer\ODText\Manifest; use PhpOffice\PhpWord\Writer\ODText\Meta; @@ -43,6 +44,9 @@ class ODText extends AbstractWriter implements WriterInterface foreach ($this->writerParts as $writer) { $writer->setParentWriter($this); } + + // Set package paths + $this->mediaPaths = array('image' => 'Pictures/'); } /** @@ -57,7 +61,13 @@ class ODText extends AbstractWriter implements WriterInterface $filename = $this->getTempFile($filename); $objZip = $this->getZipArchive($filename); - // Add mimetype to ZIP file + // Add section media files + $sectionMedia = Media::getElements('section'); + if (!empty($sectionMedia)) { + $this->addFilesToPackage($objZip, $sectionMedia); + } + + // Add parts $objZip->addFromString('mimetype', $this->getWriterPart('mimetype')->writeMimetype()); $objZip->addFromString('content.xml', $this->getWriterPart('content')->writeContent($this->phpWord)); $objZip->addFromString('meta.xml', $this->getWriterPart('meta')->writeMeta($this->phpWord)); diff --git a/src/PhpWord/Writer/ODText/Content.php b/src/PhpWord/Writer/ODText/Content.php index 89c39c31..6b4d6dfa 100644 --- a/src/PhpWord/Writer/ODText/Content.php +++ b/src/PhpWord/Writer/ODText/Content.php @@ -9,7 +9,9 @@ namespace PhpOffice\PhpWord\Writer\ODText; -use PhpOffice\PhpWord\Exception\Exception; +use PhpOffice\PhpWord\Media; +use PhpOffice\PhpWord\Style; +use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Element\Image; use PhpOffice\PhpWord\Element\Link; use PhpOffice\PhpWord\Element\ListItem; @@ -20,11 +22,11 @@ use PhpOffice\PhpWord\Element\Text; use PhpOffice\PhpWord\Element\TextBreak; use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Element\Title; -use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\Exception\Exception; +use PhpOffice\PhpWord\Shared\Drawing; use PhpOffice\PhpWord\Shared\XMLWriter; use PhpOffice\PhpWord\Style\Font; use PhpOffice\PhpWord\Style\Paragraph; -use PhpOffice\PhpWord\Style; /** * ODText content part writer @@ -98,33 +100,7 @@ class Content extends Base $this->writeFontFaces($xmlWriter); // office:font-face-decls - $this->writeAutomaticStyles($xmlWriter); // office:automatic-styles - - // Tables - $sections = $phpWord->getSections(); - $countSections = count($sections); - if ($countSections > 0) { - $sectionId = 0; - foreach ($sections as $section) { - $sectionId++; - $elements = $section->getElements(); - foreach ($elements as $element) { - if ($elements instanceof Table) { - $xmlWriter->startElement('style:style'); - $xmlWriter->writeAttribute('style:name', $element->getElementId()); - $xmlWriter->writeAttribute('style:family', 'table'); - $xmlWriter->startElement('style:table-properties'); - //$xmlWriter->writeAttribute('style:width', 'table'); - $xmlWriter->writeAttribute('style:rel-width', 100); - $xmlWriter->writeAttribute('table:align', 'center'); - $xmlWriter->endElement(); - $xmlWriter->endElement(); - } - } - } - } - - $xmlWriter->endElement(); + $this->writeAutomaticStyles($xmlWriter, $phpWord); // office:automatic-styles // office:body $xmlWriter->startElement('office:body'); @@ -371,7 +347,33 @@ class Content extends Base */ protected function writeImage(XMLWriter $xmlWriter, Image $element) { - $this->writeUnsupportedElement($xmlWriter, 'Image'); + $mediaIndex = $element->getMediaIndex(); + $target = 'Pictures/' . $element->getTarget(); + $style = $element->getStyle(); + $width = Drawing::pixelsToCentimeters($style->getWidth()); + $height = Drawing::pixelsToCentimeters($style->getHeight()); + + $xmlWriter->startElement('text:p'); + $xmlWriter->writeAttribute('text:style-name', 'Standard'); + + $xmlWriter->startElement('draw:frame'); + $xmlWriter->writeAttribute('draw:style-name', 'fr' . $mediaIndex); + $xmlWriter->writeAttribute('draw:name', $element->getElementId()); + $xmlWriter->writeAttribute('text:anchor-type', 'as-char'); + $xmlWriter->writeAttribute('svg:width', $width . 'cm'); + $xmlWriter->writeAttribute('svg:height', $height . 'cm'); + $xmlWriter->writeAttribute('draw:z-index', $mediaIndex); + + $xmlWriter->startElement('draw:image'); + $xmlWriter->writeAttribute('xlink:href', $target); + $xmlWriter->writeAttribute('xlink:type', 'simple'); + $xmlWriter->writeAttribute('xlink:show', 'embed'); + $xmlWriter->writeAttribute('xlink:actuate', 'onLoad'); + $xmlWriter->endElement(); // draw:image + + $xmlWriter->endElement(); // draw:frame + + $xmlWriter->endElement(); // text:p } /** @@ -398,9 +400,11 @@ class Content extends Base /** * Write automatic styles */ - private function writeAutomaticStyles(XMLWriter $xmlWriter) + private function writeAutomaticStyles(XMLWriter $xmlWriter, PhpWord $phpWord) { $xmlWriter->startElement('office:automatic-styles'); + + // Font and paragraph $styles = Style::getStyles(); $numPStyles = 0; if (count($styles) > 0) { @@ -437,7 +441,6 @@ class Content extends Base } } } - if ($numPStyles == 0) { // style:style $xmlWriter->startElement('style:style'); @@ -452,5 +455,47 @@ class Content extends Base $xmlWriter->endElement(); } } + + // Images + $imageData = Media::getElements('section'); + foreach ($imageData as $imageId => $image) { + if ($image['type'] == 'image') { + $xmlWriter->startElement('style:style'); + $xmlWriter->writeAttribute('style:name', 'fr' . $image['rID']); + $xmlWriter->writeAttribute('style:family', 'graphic'); + $xmlWriter->writeAttribute('style:parent-style-name', 'Graphics'); + $xmlWriter->startElement('style:graphic-properties'); + $xmlWriter->writeAttribute('style:vertical-pos', 'top'); + $xmlWriter->writeAttribute('style:vertical-rel', 'baseline'); + $xmlWriter->endElement(); + $xmlWriter->endElement(); + } + } + + // Tables + $sections = $phpWord->getSections(); + $countSections = count($sections); + if ($countSections > 0) { + $sectionId = 0; + foreach ($sections as $section) { + $sectionId++; + $elements = $section->getElements(); + foreach ($elements as $element) { + if ($elements instanceof Table) { + $xmlWriter->startElement('style:style'); + $xmlWriter->writeAttribute('style:name', $element->getElementId()); + $xmlWriter->writeAttribute('style:family', 'table'); + $xmlWriter->startElement('style:table-properties'); + //$xmlWriter->writeAttribute('style:width', 'table'); + $xmlWriter->writeAttribute('style:rel-width', 100); + $xmlWriter->writeAttribute('table:align', 'center'); + $xmlWriter->endElement(); + $xmlWriter->endElement(); + } + } + } + } + + $xmlWriter->endElement(); // office:automatic-styles } } diff --git a/src/PhpWord/Writer/ODText/Manifest.php b/src/PhpWord/Writer/ODText/Manifest.php index 220834c4..c3fef79e 100755 --- a/src/PhpWord/Writer/ODText/Manifest.php +++ b/src/PhpWord/Writer/ODText/Manifest.php @@ -9,6 +9,8 @@ namespace PhpOffice\PhpWord\Writer\ODText; +use PhpOffice\PhpWord\Media; + /** * ODText manifest part writer */ @@ -54,9 +56,19 @@ class Manifest extends AbstractWriterPart $xmlWriter->writeAttribute('manifest:full-path', 'styles.xml'); $xmlWriter->endElement(); + // Media files + $media = Media::getElements('section'); + foreach ($media as $medium) { + if ($medium['type'] == 'image') { + $xmlWriter->startElement('manifest:file-entry'); + $xmlWriter->writeAttribute('manifest:media-type', $medium['imageType']); + $xmlWriter->writeAttribute('manifest:full-path', 'Pictures/' . $medium['target']); + $xmlWriter->endElement(); + } + } + $xmlWriter->endElement(); // manifest:manifest - // Return return $xmlWriter->getData(); } } diff --git a/src/PhpWord/Writer/Word2007.php b/src/PhpWord/Writer/Word2007.php index 44df6ccc..b6403553 100755 --- a/src/PhpWord/Writer/Word2007.php +++ b/src/PhpWord/Writer/Word2007.php @@ -70,6 +70,9 @@ class Word2007 extends AbstractWriter implements WriterInterface foreach ($this->writerParts as $writer) { $writer->setParentWriter($this); } + + // Set package paths + $this->mediaPaths = array('image' => 'word/media/', 'object' => 'word/embeddings/'); } /** @@ -93,6 +96,7 @@ class Word2007 extends AbstractWriter implements WriterInterface $sectionMedia = Media::getElements('section'); if (!empty($sectionMedia)) { $this->addFilesToPackage($objZip, $sectionMedia); + $this->registerContentTypes($sectionMedia); foreach ($sectionMedia as $element) { $this->docRels[] = $element; } @@ -140,83 +144,6 @@ class Word2007 extends AbstractWriter implements WriterInterface } } - /** - * Add section files to package - * - * @param mixed $objZip - * @param mixed $elements - */ - private function addFilesToPackage($objZip, $elements) - { - foreach ($elements as $element) { - // Skip link - if ($element['type'] == 'link') { - continue; - } - - // Retrieve remote image - if (isset($element['isMemImage']) && $element['isMemImage']) { - $image = call_user_func($element['createFunction'], $element['source']); - ob_start(); - call_user_func($element['imageFunction'], $image); - $imageContents = ob_get_contents(); - ob_end_clean(); - $objZip->addFromString('word/' . $element['target'], $imageContents); - imagedestroy($image); - } else { - $this->addFileToPackage($objZip, $element['source'], $element['target']); - } - - // Register content types - if ($element['type'] == 'image') { - $imageExtension = $element['imageExtension']; - $imageType = $element['imageType']; - if (!array_key_exists($imageExtension, $this->cTypes['default'])) { - $this->cTypes['default'][$imageExtension] = $imageType; - } - } else { - if (!array_key_exists('bin', $this->cTypes['default'])) { - $this->cTypes['default']['bin'] = 'application/vnd.openxmlformats-officedocument.oleObject'; - } - } - } - } - - /** - * Add file to package - * - * Get the actual source from an archive image - * - * @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); - - $zipClass = \PhpOffice\PhpWord\Settings::getZipClass(); - $zip = new $zipClass(); - 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 * @@ -231,6 +158,7 @@ class Word2007 extends AbstractWriter implements WriterInterface if (count($media) > 0) { if (!empty($media)) { $this->addFilesToPackage($objZip, $media); + $this->registerContentTypes($media); } $relsFile = "word/_rels/{$file}.xml.rels"; $objZip->addFromString($relsFile, $this->getWriterPart('rels')->writeMediaRels($media)); @@ -281,14 +209,37 @@ class Word2007 extends AbstractWriter implements WriterInterface // Add footnotes media files, relations, and contents if ($collection::countElements() > 0) { $media = Media::getElements($notesType); - $elements = $collection::getElements(); $this->addFilesToPackage($objZip, $media); + $this->registerContentTypes($media); if (!empty($media)) { $objZip->addFromString($relsFile, $this->getWriterPart('rels')->writeMediaRels($media)); } + $elements = $collection::getElements(); $objZip->addFromString($xmlPath, $this->getWriterPart($notesTypes)->writeNotes($elements, $notesTypes)); $this->cTypes['override']["/{$xmlPath}"] = $notesTypes; $this->docRels[] = array('target' => $xmlFile, 'type' => $notesTypes, 'rID' => ++$rId); } } + + /** + * Register content types for each media + * + * @param array $media + */ + private function registerContentTypes($media) + { + foreach ($media as $medium) { + $mediumType = $medium['type']; + if ($mediumType == 'image') { + $extension = $medium['imageExtension']; + if (!array_key_exists($extension, $this->cTypes['default'])) { + $this->cTypes['default'][$extension] = $medium['imageType']; + } + } elseif ($mediumType == 'object') { + if (!array_key_exists('bin', $this->cTypes['default'])) { + $this->cTypes['default']['bin'] = 'application/vnd.openxmlformats-officedocument.oleObject'; + } + } + } + } } diff --git a/src/PhpWord/Writer/Word2007/Rels.php b/src/PhpWord/Writer/Word2007/Rels.php index 86cd7300..1d182b5d 100755 --- a/src/PhpWord/Writer/Word2007/Rels.php +++ b/src/PhpWord/Writer/Word2007/Rels.php @@ -98,10 +98,12 @@ class Rels extends AbstractWriterPart // Media relationships if (!is_null($mediaRels) && is_array($mediaRels)) { $mapping = array('image' => 'image', 'object' => 'oleObject', 'link' => 'hyperlink'); + $targetPaths = array('image' => 'media/', 'object' => 'embeddings/'); foreach ($mediaRels as $mediaRel) { - $type = $mediaRel['type']; - $type = array_key_exists($type, $mapping) ? $mapping[$type] : $type; - $target = $mediaRel['target']; + $mediaType = $mediaRel['type']; + $type = array_key_exists($mediaType, $mapping) ? $mapping[$mediaType] : $mediaType; + $target = array_key_exists($mediaType, $targetPaths) ? $targetPaths[$mediaType] : ''; + $target .= $mediaRel['target']; $targetMode = ($type == 'hyperlink') ? 'External' : ''; $this->writeRel($xmlWriter, $id++, "officeDocument/2006/relationships/{$type}", $target, $targetMode); }