Skip to content

Commit 0a8718f

Browse files
authored
Merge pull request #1 from SailorMax/template_processor__set_image_value
setImageValue() + fix adding files via ZipArchive
2 parents 0beeb27 + a37f60f commit 0a8718f

File tree

3 files changed

+271
-12
lines changed

3 files changed

+271
-12
lines changed

src/PhpWord/Shared/ZipArchive.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public function open($filename, $flags = null)
129129
{
130130
$result = true;
131131
$this->filename = $filename;
132+
$this->tempDir = Settings::getTempDir();
132133

133134
if (!$this->usePclzip) {
134135
$zip = new \ZipArchive();
@@ -139,7 +140,6 @@ public function open($filename, $flags = null)
139140
$this->numFiles = $zip->numFiles;
140141
} else {
141142
$zip = new \PclZip($this->filename);
142-
$this->tempDir = Settings::getTempDir();
143143
$this->numFiles = count($zip->listContent());
144144
}
145145
$this->zip = $zip;
@@ -244,7 +244,13 @@ public function pclzipAddFile($filename, $localname = null)
244244
$pathRemoved = $filenameParts['dirname'];
245245
$pathAdded = $localnameParts['dirname'];
246246

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

249255
if ($tempFile) {
250256
// Remove temp file, if created

src/PhpWord/TemplateProcessor.php

+213-10
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,27 @@ class TemplateProcessor
6262
*/
6363
protected $tempDocumentFooters = array();
6464

65+
/**
66+
* Document relations (in XML format) of the temporary document.
67+
*
68+
* @var string[]
69+
*/
70+
protected $tempDocumentRelations = array();
71+
72+
/**
73+
* Document content types (in XML format) of the temporary document.
74+
*
75+
* @var string
76+
*/
77+
protected $tempDocumentContentTypes = "";
78+
79+
/**
80+
* new inserted images list
81+
*
82+
* @var string[]
83+
*/
84+
protected $tempDocumentNewImages = array();
85+
6586
/**
6687
* @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception.
6788
*
@@ -88,19 +109,32 @@ public function __construct($documentTemplate)
88109
$this->zipClass->open($this->tempDocumentFilename);
89110
$index = 1;
90111
while (false !== $this->zipClass->locateName($this->getHeaderName($index))) {
91-
$this->tempDocumentHeaders[$index] = $this->fixBrokenMacros(
92-
$this->zipClass->getFromName($this->getHeaderName($index))
93-
);
112+
$this->tempDocumentHeaders[$index] = $this->readPartWithRels($this->getHeaderName($index));
94113
$index++;
95114
}
96115
$index = 1;
97116
while (false !== $this->zipClass->locateName($this->getFooterName($index))) {
98-
$this->tempDocumentFooters[$index] = $this->fixBrokenMacros(
99-
$this->zipClass->getFromName($this->getFooterName($index))
100-
);
117+
$this->tempDocumentFooters[$index] = $this->readPartWithRels($this->getFooterName($index));
101118
$index++;
102119
}
103-
$this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName($this->getMainPartName()));
120+
121+
$this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
122+
$this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName());
123+
}
124+
125+
/**
126+
* @param string $fileName
127+
*
128+
* @return string
129+
*/
130+
protected function readPartWithRels($fileName)
131+
{
132+
$relsFileName = $this->getRelationsName($fileName);
133+
$partRelations = $this->zipClass->getFromName($relsFileName);
134+
if ($partRelations !== false) {
135+
$this->tempDocumentRelations[$fileName] = $partRelations;
136+
}
137+
return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
104138
}
105139

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

273+
/**
274+
* @param mixed $search
275+
* @param mixed $replace Path to image, or array("path" => xx, "width" => yy, "height" => zz)
276+
* @param integer $limit
277+
*
278+
* @return void
279+
*/
280+
public function setImageValue($search, $replace, $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT)
281+
{
282+
// prepare $search_replace
283+
if (!is_array($search)) {
284+
$search = array($search);
285+
}
286+
287+
$replacesList = array();
288+
if (!is_array($replace) || isset($replace["path"])) {
289+
$replacesList[] = $replace;
290+
} else {
291+
$replacesList = array_values($replace);
292+
}
293+
294+
$searchReplace = array();
295+
foreach ($search as $searchIdx => $searchString) {
296+
$searchReplace[$searchString] = isset($replacesList[$searchIdx]) ? $replacesList[$searchIdx] : $replacesList[0];
297+
}
298+
//
299+
300+
// define templates
301+
// result can be verified via "Open XML SDK 2.5 Productivity Tool" (http://www.microsoft.com/en-us/download/details.aspx?id=30425)
302+
$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>';
303+
$typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>';
304+
$relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>';
305+
$newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n".'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
306+
$newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
307+
$extTransform = array(
308+
"jpg" => "jpeg",
309+
"JPG" => "jpeg",
310+
"png" => "png",
311+
"PNG" => "png",
312+
);
313+
//
314+
315+
$searchParts = array(
316+
$this->getMainPartName() => &$this->tempDocumentMainPart,
317+
);
318+
foreach (array_keys($this->tempDocumentHeaders) as $headerIndex) {
319+
$searchParts[ $this->getHeaderName($headerIndex) ] = &$this->tempDocumentHeaders[$headerIndex];
320+
}
321+
foreach (array_keys($this->tempDocumentFooters) as $headerIndex) {
322+
$searchParts[ $this->getFooterName($headerIndex) ] = &$this->tempDocumentFooters[$headerIndex];
323+
}
324+
325+
foreach ($searchParts as $partFileName => &$partContent) {
326+
$partVariables = $this->getVariablesForPart($partContent);
327+
328+
$partSearchReplaces = array();
329+
foreach ($searchReplace as $search => $replace) {
330+
if (!in_array($search, $partVariables)) {
331+
continue;
332+
}
333+
334+
// get image path and size
335+
$width = 115;
336+
$height = 70;
337+
if (is_array($replace) && isset($replace["path"])) {
338+
$imgPath = $replace["path"];
339+
if (isset($replace["width"])) {
340+
$width = $replace["width"];
341+
}
342+
if (isset($replace["height"])) {
343+
$height = $replace["height"];
344+
}
345+
} else {
346+
$imgPath = $replace;
347+
}
348+
349+
// get image index
350+
$imgIndex = $this->getNextRelationsIndex($partFileName);
351+
$rid = 'rId' . $imgIndex;
352+
353+
// get image embed name
354+
if (isset($this->tempDocumentNewImages[$imgPath])) {
355+
$imgName = $this->tempDocumentNewImages[$imgPath];
356+
} else {
357+
// transform extension
358+
$imgExt = pathinfo($imgPath, PATHINFO_EXTENSION);
359+
if (isset($extTransform)) {
360+
$imgExt = $extTransform[$imgExt];
361+
}
362+
363+
// add image to document
364+
$imgName = 'image' . $imgIndex . '_' . pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
365+
$this->zipClass->pclzipAddFile($imgPath, 'word/media/' . $imgName);
366+
$this->tempDocumentNewImages[$imgPath] = $imgName;
367+
368+
// setup type for image
369+
$xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl) ;
370+
$this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
371+
}
372+
373+
$xmlImage = str_replace(array('{RID}', '{WIDTH}', '{HEIGHT}'), array($rid, $width, $height), $imgTpl) ;
374+
$xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl);
375+
376+
if (!isset($this->tempDocumentRelations[$partFileName])) {
377+
// create new relations file
378+
$this->tempDocumentRelations[$partFileName] = $newRelationsTpl;
379+
// and add it to content types
380+
$xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl);
381+
$this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>';
382+
}
383+
384+
// add image to relations
385+
$this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>';
386+
387+
// collect prepared replaces
388+
$partSearchReplaces["<w:t>".self::ensureMacroCompleted($search)."</w:t>"] = $xmlImage;
389+
}
390+
391+
if ($partSearchReplaces) {
392+
$partContent = $this->setValueForPart(array_keys($partSearchReplaces), $partSearchReplaces, $partContent, $limit);
393+
}
394+
}
395+
}
396+
239397
/**
240398
* Returns array of all variables in template.
241399
*
@@ -399,15 +557,17 @@ public function deleteBlock($blockname)
399557
public function save()
400558
{
401559
foreach ($this->tempDocumentHeaders as $index => $xml) {
402-
$this->zipClass->addFromString($this->getHeaderName($index), $xml);
560+
$this->savePartWithRels($this->getHeaderName($index), $xml);
403561
}
404562

405-
$this->zipClass->addFromString($this->getMainPartName(), $this->tempDocumentMainPart);
563+
$this->savePartWithRels($this->getMainPartName(), $this->tempDocumentMainPart);
406564

407565
foreach ($this->tempDocumentFooters as $index => $xml) {
408-
$this->zipClass->addFromString($this->getFooterName($index), $xml);
566+
$this->savePartWithRels($this->getFooterName($index), $xml);
409567
}
410568

569+
$this->zipClass->addFromString($this->getDocumentContentTypesName(), $this->tempDocumentContentTypes);
570+
411571
// Close zip file
412572
if (false === $this->zipClass->close()) {
413573
throw new Exception('Could not close zip file.');
@@ -416,6 +576,21 @@ public function save()
416576
return $this->tempDocumentFilename;
417577
}
418578

579+
/**
580+
* @param string $fileName
581+
* @param string $xml
582+
*
583+
* @return void
584+
*/
585+
protected function savePartWithRels($fileName, $xml)
586+
{
587+
$this->zipClass->addFromString($fileName, $xml);
588+
if (isset($this->tempDocumentRelations[$fileName])) {
589+
$relsFileName = $this->getRelationsName($fileName);
590+
$this->zipClass->addFromString($relsFileName, $this->tempDocumentRelations[$fileName]);
591+
}
592+
}
593+
419594
/**
420595
* Saves the result document to the user defined file.
421596
*
@@ -533,6 +708,34 @@ protected function getFooterName($index)
533708
return sprintf('word/footer%d.xml', $index);
534709
}
535710

711+
/**
712+
* Get the name of the relations file for document part.
713+
*
714+
* @param string $docuemntPartName
715+
*
716+
* @return string
717+
*/
718+
protected function getRelationsName($documentPartName)
719+
{
720+
return 'word/_rels/'.pathinfo($documentPartName, PATHINFO_BASENAME).'.rels';
721+
}
722+
723+
protected function getNextRelationsIndex($documentPartName)
724+
{
725+
if (isset($this->tempDocumentRelations[$documentPartName])) {
726+
return substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship');
727+
}
728+
return 1;
729+
}
730+
731+
/**
732+
* @return string
733+
*/
734+
protected function getDocumentContentTypesName()
735+
{
736+
return '[Content_Types].xml';
737+
}
738+
536739
/**
537740
* Find the start position of the nearest table row before $offset.
538741
*

tests/PhpWord/TemplateProcessorTest.php

+50
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,56 @@ public function testMacrosCanBeReplacedInHeaderAndFooter()
200200
$this->assertTrue($docFound);
201201
}
202202

203+
/**
204+
* @covers ::setImageValue
205+
* @test
206+
*/
207+
public function testSetImageValue()
208+
{
209+
$templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx');
210+
$imagePath = __DIR__ . '/_files/images/earth.jpg';
211+
212+
$variablesReplace = array(
213+
'headerValue' => $imagePath,
214+
'documentContent' => array("path" => $imagePath, "width" => 500, "height" => 500),
215+
'footerValue' => array("path" => $imagePath, "width" => 50, "height" => 50),
216+
);
217+
$templateProcessor->setImageValue(array_keys($variablesReplace), $variablesReplace);
218+
219+
$docName = 'header-footer-images-test-result.docx';
220+
$templateProcessor->saveAs($docName);
221+
$docFound = file_exists($docName);
222+
223+
if ($docFound) {
224+
$expectedDocumentZip = new \ZipArchive();
225+
$expectedDocumentZip->open($docName);
226+
$expectedContentTypesXml = $expectedDocumentZip->getFromName('[Content_Types].xml');
227+
$expectedDocumentRelationsXml = $expectedDocumentZip->getFromName('word/_rels/document.xml.rels');
228+
$expectedHeaderRelationsXml = $expectedDocumentZip->getFromName('word/_rels/header1.xml.rels');
229+
$expectedFooterRelationsXml = $expectedDocumentZip->getFromName('word/_rels/footer1.xml.rels');
230+
$expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml');
231+
$expectedHeaderPartXml = $expectedDocumentZip->getFromName('word/header1.xml');
232+
$expectedFooterPartXml = $expectedDocumentZip->getFromName('word/footer1.xml');
233+
$expectedImage = $expectedDocumentZip->getFromName('word/media/image5_document.jpeg');
234+
if (false === $expectedDocumentZip->close()) {
235+
throw new \Exception("Could not close zip file \"{$docName}\".");
236+
}
237+
238+
$this->assertTrue(!empty($expectedImage), 'Embed image doesn\'t found.');
239+
$this->assertTrue(strpos($expectedContentTypesXml, '/word/media/image5_document.jpeg') > 0, '[Content_Types].xml missed "/word/media/image5_document.jpeg"');
240+
$this->assertTrue(strpos($expectedContentTypesXml, '/word/_rels/header1.xml.rels') > 0, '[Content_Types].xml missed "/word/_rels/header1.xml.rels"');
241+
$this->assertTrue(strpos($expectedContentTypesXml, '/word/_rels/footer1.xml.rels') > 0, '[Content_Types].xml missed "/word/_rels/footer1.xml.rels"');
242+
$this->assertTrue(strpos($expectedMainPartXml, '${documentContent}') === false, 'word/document.xml has no image.');
243+
$this->assertTrue(strpos($expectedHeaderPartXml, '${headerValue}') === false, 'word/header1.xml has no image.');
244+
$this->assertTrue(strpos($expectedFooterPartXml, '${footerValue}') === false, 'word/footer1.xml has no image.');
245+
$this->assertTrue(strpos($expectedDocumentRelationsXml, 'media/image5_document.jpeg') > 0, 'word/_rels/document.xml.rels missed "media/image5_document.jpeg"');
246+
$this->assertTrue(strpos($expectedHeaderRelationsXml, 'media/image5_document.jpeg') > 0, 'word/_rels/header1.xml.rels missed "media/image5_document.jpeg"');
247+
$this->assertTrue(strpos($expectedFooterRelationsXml, 'media/image5_document.jpeg') > 0, 'word/_rels/footer1.xml.rels missed "media/image5_document.jpeg"');
248+
249+
unlink($docName);
250+
}
251+
}
252+
203253
/**
204254
* @covers ::cloneBlock
205255
* @covers ::deleteBlock

0 commit comments

Comments
 (0)