Skip to content

Commit

Permalink
Merge pull request #1 from SailorMax/template_processor__set_image_value
Browse files Browse the repository at this point in the history
setImageValue() + fix adding files via ZipArchive
  • Loading branch information
SailorMax authored Nov 10, 2017
2 parents 0beeb27 + a37f60f commit 0a8718f
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 12 deletions.
10 changes: 8 additions & 2 deletions src/PhpWord/Shared/ZipArchive.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public function open($filename, $flags = null)
{
$result = true;
$this->filename = $filename;
$this->tempDir = Settings::getTempDir();

if (!$this->usePclzip) {
$zip = new \ZipArchive();
Expand All @@ -139,7 +140,6 @@ public function open($filename, $flags = null)
$this->numFiles = $zip->numFiles;
} else {
$zip = new \PclZip($this->filename);
$this->tempDir = Settings::getTempDir();
$this->numFiles = count($zip->listContent());
}
$this->zip = $zip;
Expand Down Expand Up @@ -244,7 +244,13 @@ public function pclzipAddFile($filename, $localname = null)
$pathRemoved = $filenameParts['dirname'];
$pathAdded = $localnameParts['dirname'];

$res = $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $pathRemoved, PCLZIP_OPT_ADD_PATH, $pathAdded);
if (!$this->usePclzip) {
$pathAdded = $pathAdded . '/' . ltrim(str_replace('\\', '/', substr($filename, strlen($pathRemoved))), '/');
//$res = $zip->addFile($filename, $pathAdded);
$res = $zip->addFromString($pathAdded, file_get_contents($filename)); // addFile can't use subfolders in some cases
} else {
$res = $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $pathRemoved, PCLZIP_OPT_ADD_PATH, $pathAdded);
}

if ($tempFile) {
// Remove temp file, if created
Expand Down
223 changes: 213 additions & 10 deletions src/PhpWord/TemplateProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ class TemplateProcessor
*/
protected $tempDocumentFooters = array();

/**
* Document relations (in XML format) of the temporary document.
*
* @var string[]
*/
protected $tempDocumentRelations = array();

/**
* Document content types (in XML format) of the temporary document.
*
* @var string
*/
protected $tempDocumentContentTypes = "";

/**
* new inserted images list
*
* @var string[]
*/
protected $tempDocumentNewImages = array();

/**
* @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception.
*
Expand All @@ -88,19 +109,32 @@ public function __construct($documentTemplate)
$this->zipClass->open($this->tempDocumentFilename);
$index = 1;
while (false !== $this->zipClass->locateName($this->getHeaderName($index))) {
$this->tempDocumentHeaders[$index] = $this->fixBrokenMacros(
$this->zipClass->getFromName($this->getHeaderName($index))
);
$this->tempDocumentHeaders[$index] = $this->readPartWithRels($this->getHeaderName($index));
$index++;
}
$index = 1;
while (false !== $this->zipClass->locateName($this->getFooterName($index))) {
$this->tempDocumentFooters[$index] = $this->fixBrokenMacros(
$this->zipClass->getFromName($this->getFooterName($index))
);
$this->tempDocumentFooters[$index] = $this->readPartWithRels($this->getFooterName($index));
$index++;
}
$this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName($this->getMainPartName()));

$this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
$this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName());
}

/**
* @param string $fileName
*
* @return string
*/
protected function readPartWithRels($fileName)
{
$relsFileName = $this->getRelationsName($fileName);
$partRelations = $this->zipClass->getFromName($relsFileName);
if ($partRelations !== false) {
$this->tempDocumentRelations[$fileName] = $partRelations;
}
return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
}

/**
Expand Down Expand Up @@ -236,6 +270,130 @@ public function setValue($search, $replace, $limit = self::MAXIMUM_REPLACEMENTS_
$this->tempDocumentFooters = $this->setValueForPart($search, $replace, $this->tempDocumentFooters, $limit);
}

/**
* @param mixed $search
* @param mixed $replace Path to image, or array("path" => xx, "width" => yy, "height" => zz)
* @param integer $limit
*
* @return void
*/
public function setImageValue($search, $replace, $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT)
{
// prepare $search_replace
if (!is_array($search)) {
$search = array($search);
}

$replacesList = array();
if (!is_array($replace) || isset($replace["path"])) {
$replacesList[] = $replace;
} else {
$replacesList = array_values($replace);
}

$searchReplace = array();
foreach ($search as $searchIdx => $searchString) {
$searchReplace[$searchString] = isset($replacesList[$searchIdx]) ? $replacesList[$searchIdx] : $replacesList[0];
}
//

// define templates
// result can be verified via "Open XML SDK 2.5 Productivity Tool" (http://www.microsoft.com/en-us/download/details.aspx?id=30425)
$imgTpl = '<w:pict><v:shape type="#_x0000_t75" style="width:{WIDTH}px;height:{HEIGHT}px"><v:imagedata r:id="{RID}" o:title=""/></v:shape></w:pict>';
$typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>';
$relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>';
$newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n".'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
$newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
$extTransform = array(
"jpg" => "jpeg",
"JPG" => "jpeg",
"png" => "png",
"PNG" => "png",
);
//

$searchParts = array(
$this->getMainPartName() => &$this->tempDocumentMainPart,
);
foreach (array_keys($this->tempDocumentHeaders) as $headerIndex) {
$searchParts[ $this->getHeaderName($headerIndex) ] = &$this->tempDocumentHeaders[$headerIndex];
}
foreach (array_keys($this->tempDocumentFooters) as $headerIndex) {
$searchParts[ $this->getFooterName($headerIndex) ] = &$this->tempDocumentFooters[$headerIndex];
}

foreach ($searchParts as $partFileName => &$partContent) {
$partVariables = $this->getVariablesForPart($partContent);

$partSearchReplaces = array();
foreach ($searchReplace as $search => $replace) {
if (!in_array($search, $partVariables)) {
continue;
}

// get image path and size
$width = 115;
$height = 70;
if (is_array($replace) && isset($replace["path"])) {
$imgPath = $replace["path"];
if (isset($replace["width"])) {
$width = $replace["width"];
}
if (isset($replace["height"])) {
$height = $replace["height"];
}
} else {
$imgPath = $replace;
}

// get image index
$imgIndex = $this->getNextRelationsIndex($partFileName);
$rid = 'rId' . $imgIndex;

// get image embed name
if (isset($this->tempDocumentNewImages[$imgPath])) {
$imgName = $this->tempDocumentNewImages[$imgPath];
} else {
// transform extension
$imgExt = pathinfo($imgPath, PATHINFO_EXTENSION);
if (isset($extTransform)) {
$imgExt = $extTransform[$imgExt];
}

// add image to document
$imgName = 'image' . $imgIndex . '_' . pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
$this->zipClass->pclzipAddFile($imgPath, 'word/media/' . $imgName);
$this->tempDocumentNewImages[$imgPath] = $imgName;

// setup type for image
$xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl) ;
$this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
}

$xmlImage = str_replace(array('{RID}', '{WIDTH}', '{HEIGHT}'), array($rid, $width, $height), $imgTpl) ;
$xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl);

if (!isset($this->tempDocumentRelations[$partFileName])) {
// create new relations file
$this->tempDocumentRelations[$partFileName] = $newRelationsTpl;
// and add it to content types
$xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl);
$this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>';
}

// add image to relations
$this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>';

// collect prepared replaces
$partSearchReplaces["<w:t>".self::ensureMacroCompleted($search)."</w:t>"] = $xmlImage;
}

if ($partSearchReplaces) {
$partContent = $this->setValueForPart(array_keys($partSearchReplaces), $partSearchReplaces, $partContent, $limit);
}
}
}

/**
* Returns array of all variables in template.
*
Expand Down Expand Up @@ -399,15 +557,17 @@ public function deleteBlock($blockname)
public function save()
{
foreach ($this->tempDocumentHeaders as $index => $xml) {
$this->zipClass->addFromString($this->getHeaderName($index), $xml);
$this->savePartWithRels($this->getHeaderName($index), $xml);
}

$this->zipClass->addFromString($this->getMainPartName(), $this->tempDocumentMainPart);
$this->savePartWithRels($this->getMainPartName(), $this->tempDocumentMainPart);

foreach ($this->tempDocumentFooters as $index => $xml) {
$this->zipClass->addFromString($this->getFooterName($index), $xml);
$this->savePartWithRels($this->getFooterName($index), $xml);
}

$this->zipClass->addFromString($this->getDocumentContentTypesName(), $this->tempDocumentContentTypes);

// Close zip file
if (false === $this->zipClass->close()) {
throw new Exception('Could not close zip file.');
Expand All @@ -416,6 +576,21 @@ public function save()
return $this->tempDocumentFilename;
}

/**
* @param string $fileName
* @param string $xml
*
* @return void
*/
protected function savePartWithRels($fileName, $xml)
{
$this->zipClass->addFromString($fileName, $xml);
if (isset($this->tempDocumentRelations[$fileName])) {
$relsFileName = $this->getRelationsName($fileName);
$this->zipClass->addFromString($relsFileName, $this->tempDocumentRelations[$fileName]);
}
}

/**
* Saves the result document to the user defined file.
*
Expand Down Expand Up @@ -533,6 +708,34 @@ protected function getFooterName($index)
return sprintf('word/footer%d.xml', $index);
}

/**
* Get the name of the relations file for document part.
*
* @param string $docuemntPartName
*
* @return string
*/
protected function getRelationsName($documentPartName)
{
return 'word/_rels/'.pathinfo($documentPartName, PATHINFO_BASENAME).'.rels';
}

protected function getNextRelationsIndex($documentPartName)
{
if (isset($this->tempDocumentRelations[$documentPartName])) {
return substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship');
}
return 1;
}

/**
* @return string
*/
protected function getDocumentContentTypesName()
{
return '[Content_Types].xml';
}

/**
* Find the start position of the nearest table row before $offset.
*
Expand Down
50 changes: 50 additions & 0 deletions tests/PhpWord/TemplateProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,56 @@ public function testMacrosCanBeReplacedInHeaderAndFooter()
$this->assertTrue($docFound);
}

/**
* @covers ::setImageValue
* @test
*/
public function testSetImageValue()
{
$templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx');
$imagePath = __DIR__ . '/_files/images/earth.jpg';

$variablesReplace = array(
'headerValue' => $imagePath,
'documentContent' => array("path" => $imagePath, "width" => 500, "height" => 500),
'footerValue' => array("path" => $imagePath, "width" => 50, "height" => 50),
);
$templateProcessor->setImageValue(array_keys($variablesReplace), $variablesReplace);

$docName = 'header-footer-images-test-result.docx';
$templateProcessor->saveAs($docName);
$docFound = file_exists($docName);

if ($docFound) {
$expectedDocumentZip = new \ZipArchive();
$expectedDocumentZip->open($docName);
$expectedContentTypesXml = $expectedDocumentZip->getFromName('[Content_Types].xml');
$expectedDocumentRelationsXml = $expectedDocumentZip->getFromName('word/_rels/document.xml.rels');
$expectedHeaderRelationsXml = $expectedDocumentZip->getFromName('word/_rels/header1.xml.rels');
$expectedFooterRelationsXml = $expectedDocumentZip->getFromName('word/_rels/footer1.xml.rels');
$expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml');
$expectedHeaderPartXml = $expectedDocumentZip->getFromName('word/header1.xml');
$expectedFooterPartXml = $expectedDocumentZip->getFromName('word/footer1.xml');
$expectedImage = $expectedDocumentZip->getFromName('word/media/image5_document.jpeg');
if (false === $expectedDocumentZip->close()) {
throw new \Exception("Could not close zip file \"{$docName}\".");
}

$this->assertTrue(!empty($expectedImage), 'Embed image doesn\'t found.');
$this->assertTrue(strpos($expectedContentTypesXml, '/word/media/image5_document.jpeg') > 0, '[Content_Types].xml missed "/word/media/image5_document.jpeg"');
$this->assertTrue(strpos($expectedContentTypesXml, '/word/_rels/header1.xml.rels') > 0, '[Content_Types].xml missed "/word/_rels/header1.xml.rels"');
$this->assertTrue(strpos($expectedContentTypesXml, '/word/_rels/footer1.xml.rels') > 0, '[Content_Types].xml missed "/word/_rels/footer1.xml.rels"');
$this->assertTrue(strpos($expectedMainPartXml, '${documentContent}') === false, 'word/document.xml has no image.');
$this->assertTrue(strpos($expectedHeaderPartXml, '${headerValue}') === false, 'word/header1.xml has no image.');
$this->assertTrue(strpos($expectedFooterPartXml, '${footerValue}') === false, 'word/footer1.xml has no image.');
$this->assertTrue(strpos($expectedDocumentRelationsXml, 'media/image5_document.jpeg') > 0, 'word/_rels/document.xml.rels missed "media/image5_document.jpeg"');
$this->assertTrue(strpos($expectedHeaderRelationsXml, 'media/image5_document.jpeg') > 0, 'word/_rels/header1.xml.rels missed "media/image5_document.jpeg"');
$this->assertTrue(strpos($expectedFooterRelationsXml, 'media/image5_document.jpeg') > 0, 'word/_rels/footer1.xml.rels missed "media/image5_document.jpeg"');

unlink($docName);
}
}

/**
* @covers ::cloneBlock
* @covers ::deleteBlock
Expand Down

0 comments on commit 0a8718f

Please sign in to comment.