ODT Writer: Basic image writing support

This commit is contained in:
Ivan Lanin 2014-04-18 23:12:51 +07:00
parent f5f03a5b2b
commit f829559f65
10 changed files with 281 additions and 125 deletions

View File

@ -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

View File

@ -89,7 +89,7 @@ Writers
+ +-----------------------+--------+-------+-------+-------+-------+
| | Table | ✓ | ✓ | | ✓ | ✓ |
+ +-----------------------+--------+-------+-------+-------+-------+
| | Image | ✓ | | | ✓ | |
| | Image | ✓ | | | ✓ | |
+ +-----------------------+--------+-------+-------+-------+-------+
| | Object | ✓ | | | | |
+ +-----------------------+--------+-------+-------+-------+-------+

View File

@ -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
*

View File

@ -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'];
}
}

View File

@ -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
*

View File

@ -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));

View File

@ -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
}
}

View File

@ -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();
}
}

View File

@ -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';
}
}
}
}
}

View File

@ -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);
}