Skip to content

Commit 951c55a

Browse files
authored
Merge branch 'develop' into php72_support_object_classes
2 parents 2a84625 + 0a8718f commit 951c55a

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
/**
@@ -232,6 +266,130 @@ public function setValue($search, $replace, $limit = self::MAXIMUM_REPLACEMENTS_
232266
$this->tempDocumentFooters = $this->setValueForPart($search, $replace, $this->tempDocumentFooters, $limit);
233267
}
234268

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

395-
$this->zipClass->addFromString($this->getMainPartName(), $this->tempDocumentMainPart);
553+
$this->savePartWithRels($this->getMainPartName(), $this->tempDocumentMainPart);
396554

397555
foreach ($this->tempDocumentFooters as $index => $xml) {
398-
$this->zipClass->addFromString($this->getFooterName($index), $xml);
556+
$this->savePartWithRels($this->getFooterName($index), $xml);
399557
}
400558

559+
$this->zipClass->addFromString($this->getDocumentContentTypesName(), $this->tempDocumentContentTypes);
560+
401561
// Close zip file
402562
if (false === $this->zipClass->close()) {
403563
throw new Exception('Could not close zip file.');
@@ -406,6 +566,21 @@ public function save()
406566
return $this->tempDocumentFilename;
407567
}
408568

569+
/**
570+
* @param string $fileName
571+
* @param string $xml
572+
*
573+
* @return void
574+
*/
575+
protected function savePartWithRels($fileName, $xml)
576+
{
577+
$this->zipClass->addFromString($fileName, $xml);
578+
if (isset($this->tempDocumentRelations[$fileName])) {
579+
$relsFileName = $this->getRelationsName($fileName);
580+
$this->zipClass->addFromString($relsFileName, $this->tempDocumentRelations[$fileName]);
581+
}
582+
}
583+
409584
/**
410585
* Saves the result document to the user defined file.
411586
*
@@ -521,6 +696,34 @@ protected function getFooterName($index)
521696
return sprintf('word/footer%d.xml', $index);
522697
}
523698

699+
/**
700+
* Get the name of the relations file for document part.
701+
*
702+
* @param string $docuemntPartName
703+
*
704+
* @return string
705+
*/
706+
protected function getRelationsName($documentPartName)
707+
{
708+
return 'word/_rels/'.pathinfo($documentPartName, PATHINFO_BASENAME).'.rels';
709+
}
710+
711+
protected function getNextRelationsIndex($documentPartName)
712+
{
713+
if (isset($this->tempDocumentRelations[$documentPartName])) {
714+
return substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship');
715+
}
716+
return 1;
717+
}
718+
719+
/**
720+
* @return string
721+
*/
722+
protected function getDocumentContentTypesName()
723+
{
724+
return '[Content_Types].xml';
725+
}
726+
524727
/**
525728
* Find the start position of the nearest table row before $offset.
526729
*

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)