diff --git a/.travis.yml b/.travis.yml index 881decfe2a..1d32cfda34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,8 +24,6 @@ matrix: - php: 5.3 - php: 7.0 - php: 7.3 - allow_failures: - - php: 7.3 cache: directories: diff --git a/CHANGELOG.md b/CHANGELOG.md index 220d2eeb20..ae41bd4de3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ v0.16.0 (xx dec 2018) - Add setting Chart Title and Legend visibility @Tom-Magill #1433 - Add ability to pass a Style object in Section constructor @ndench #1416 - Add support for hidden text @Alexmg86 #1527 +- Add support for setting images in TemplateProcessor @SailorMax #1170 ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst index af03b24586..0cc5683ac9 100644 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -7,14 +7,32 @@ You can create an OOXML document template with included search-patterns (macros) To deal with a template file, use ``new TemplateProcessor`` statement. After TemplateProcessor instance creation the document template is copied into the temporary directory. Then you can use ``TemplateProcessor::setValue`` method to change the value of a search pattern. The search-pattern model is: ``${search-pattern}``. +The search-pattern model for images can be like: +- ``${search-image-pattern}`` +- ``${search-image-pattern:[width]:[height]:[ratio]}`` +- ``${search-image-pattern:[width]x[height]}`` +- ``${search-image-pattern:size=[width]x[height]}`` +- ``${search-image-pattern:width=[width]:height=[height]:ratio=false}`` +Where: +- [width] and [height] can be just numbers or numbers with measure, which supported by Word (cm|mm|in|pt|pc|px|%|em|ex) +- [ratio] uses only for ``false``, ``-`` or ``f`` to turn off respect aspect ration of image. By default template image size uses as 'container' size. + Example: +.. code-block:: doc + + ${CompanyLogo} + ${UserLogo:50:50} ${Name} - ${City} - ${Street} + .. code-block:: php $templateProcessor = new TemplateProcessor('Template.docx'); $templateProcessor->setValue('Name', 'John Doe'); $templateProcessor->setValue(array('City', 'Street'), array('Detroit', '12th Street')); + $templateProcessor->setImageValue('CompanyLogo', 'path/to/company/logo.png'); + $templateProcessor->setImageValue('UserLogo', array('path' => 'path/to/logo.png', 'width' => 100, 'height' => 100, 'ratio' => false)); + It is not possible to directly add new OOXML elements to the template file being processed, but it is possible to transform headers, main document part, and footers of the template using XSLT (see ``TemplateProcessor::applyXslStyleSheet``). See ``Sample_07_TemplateCloneRow.php`` for example on how to create diff --git a/src/PhpWord/Shared/ZipArchive.php b/src/PhpWord/Shared/ZipArchive.php index 2783e17e1c..a941916596 100644 --- a/src/PhpWord/Shared/ZipArchive.php +++ b/src/PhpWord/Shared/ZipArchive.php @@ -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(); @@ -139,7 +140,6 @@ public function open($filename, $flags = null) $this->numFiles = $zip->numFiles; } else { $zip = new \PclZip($this->filename); - $this->tempDir = Settings::getTempDir(); $zipContent = $zip->listContent(); $this->numFiles = is_array($zipContent) ? count($zipContent) : 0; } @@ -245,7 +245,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 diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 7996b7f8f1..4ec9e2cc89 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -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 * @@ -88,19 +109,33 @@ 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)); } /** @@ -233,6 +268,274 @@ public function setValue($search, $replace, $limit = self::MAXIMUM_REPLACEMENTS_ $this->tempDocumentFooters = $this->setValueForPart($search, $replace, $this->tempDocumentFooters, $limit); } + private function getImageArgs($varNameWithArgs) + { + $varElements = explode(':', $varNameWithArgs); + array_shift($varElements); // first element is name of variable => remove it + + $varInlineArgs = array(); + // size format documentation: https://msdn.microsoft.com/en-us/library/documentformat.openxml.vml.shape%28v=office.14%29.aspx?f=255&MSPPError=-2147217396 + foreach ($varElements as $argIdx => $varArg) { + if (strpos($varArg, '=')) { // arg=value + list($argName, $argValue) = explode('=', $varArg, 2); + $argName = strtolower($argName); + if ($argName == 'size') { + list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $argValue, 2); + } else { + $varInlineArgs[strtolower($argName)] = $argValue; + } + } elseif (preg_match('/^([0-9]*[a-z%]{0,2}|auto)x([0-9]*[a-z%]{0,2}|auto)$/i', $varArg)) { // 60x40 + list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $varArg, 2); + } else { // :60:40:f + switch ($argIdx) { + case 0: + $varInlineArgs['width'] = $varArg; + break; + case 1: + $varInlineArgs['height'] = $varArg; + break; + case 2: + $varInlineArgs['ratio'] = $varArg; + break; + } + } + } + + return $varInlineArgs; + } + + private function chooseImageDimension($baseValue, $inlineValue, $defaultValue) + { + $value = $baseValue; + if (is_null($value) && isset($inlineValue)) { + $value = $inlineValue; + } + if (!preg_match('/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i', $value)) { + $value = null; + } + if (is_null($value)) { + $value = $defaultValue; + } + if (is_numeric($value)) { + $value .= 'px'; + } + + return $value; + } + + private function fixImageWidthHeightRatio(&$width, &$height, $actualWidth, $actualHeight) + { + $imageRatio = $actualWidth / $actualHeight; + + if (($width === '') && ($height === '')) { // defined size are empty + $width = $actualWidth . 'px'; + $height = $actualHeight . 'px'; + } elseif ($width === '') { // defined width is empty + $heightFloat = (float) $height; + $widthFloat = $heightFloat * $imageRatio; + $matches = array(); + preg_match("/\d([a-z%]+)$/", $height, $matches); + $width = $widthFloat . $matches[1]; + } elseif ($height === '') { // defined height is empty + $widthFloat = (float) $width; + $heightFloat = $widthFloat / $imageRatio; + $matches = array(); + preg_match("/\d([a-z%]+)$/", $width, $matches); + $height = $heightFloat . $matches[1]; + } else { // we have defined size, but we need also check it aspect ratio + $widthMatches = array(); + preg_match("/\d([a-z%]+)$/", $width, $widthMatches); + $heightMatches = array(); + preg_match("/\d([a-z%]+)$/", $height, $heightMatches); + // try to fix only if dimensions are same + if ($widthMatches[1] == $heightMatches[1]) { + $dimention = $widthMatches[1]; + $widthFloat = (float) $width; + $heightFloat = (float) $height; + $definedRatio = $widthFloat / $heightFloat; + + if ($imageRatio > $definedRatio) { // image wider than defined box + $height = ($widthFloat / $imageRatio) . $dimention; + } elseif ($imageRatio < $definedRatio) { // image higher than defined box + $width = ($heightFloat * $imageRatio) . $dimention; + } + } + } + } + + private function prepareImageAttrs($replaceImage, $varInlineArgs) + { + // get image path and size + $width = null; + $height = null; + $ratio = null; + if (is_array($replaceImage) && isset($replaceImage['path'])) { + $imgPath = $replaceImage['path']; + if (isset($replaceImage['width'])) { + $width = $replaceImage['width']; + } + if (isset($replaceImage['height'])) { + $height = $replaceImage['height']; + } + if (isset($replaceImage['ratio'])) { + $ratio = $replaceImage['ratio']; + } + } else { + $imgPath = $replaceImage; + } + + $width = $this->chooseImageDimension($width, isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115); + $height = $this->chooseImageDimension($height, isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70); + + $imageData = @getimagesize($imgPath); + if (!is_array($imageData)) { + throw new Exception(sprintf('Invalid image: %s', $imgPath)); + } + list($actualWidth, $actualHeight, $imageType) = $imageData; + + // fix aspect ratio (by default) + if (is_null($ratio) && isset($varInlineArgs['ratio'])) { + $ratio = $varInlineArgs['ratio']; + } + if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) { + $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight); + } + + $imageAttrs = array( + 'src' => $imgPath, + 'mime' => image_type_to_mime_type($imageType), + 'width' => $width, + 'height' => $height, + ); + + return $imageAttrs; + } + + private function addImageToRelations($partFileName, $rid, $imgPath, $imageMimeType) + { + // define templates + $typeTpl = ''; + $relationTpl = ''; + $newRelationsTpl = '' . "\n" . ''; + $newRelationsTypeTpl = ''; + $extTransform = array( + 'image/jpeg' => 'jpeg', + 'image/png' => 'png', + 'image/bmp' => 'bmp', + 'image/gif' => 'gif', + ); + + // get image embed name + if (isset($this->tempDocumentNewImages[$imgPath])) { + $imgName = $this->tempDocumentNewImages[$imgPath]; + } else { + // transform extension + if (isset($extTransform[$imageMimeType])) { + $imgExt = $extTransform[$imageMimeType]; + } else { + throw new Exception("Unsupported image type $imageMimeType"); + } + + // add image to document + $imgName = 'image_' . $rid . '_' . 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('', $xmlImageType, $this->tempDocumentContentTypes) . ''; + } + + $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('', $xmlRelationsType, $this->tempDocumentContentTypes) . ''; + } + + // add image to relations + $this->tempDocumentRelations[$partFileName] = str_replace('', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . ''; + } + + /** + * @param mixed $search + * @param mixed $replace Path to image, or array("path" => xx, "width" => yy, "height" => zz) + * @param int $limit + */ + 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]; + } + + // collect document parts + $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]; + } + + // 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 = ''; + + foreach ($searchParts as $partFileName => &$partContent) { + $partVariables = $this->getVariablesForPart($partContent); + + foreach ($searchReplace as $searchString => $replaceImage) { + $varsToReplace = array_filter($partVariables, function ($partVar) use ($searchString) { + return ($partVar == $searchString) || preg_match('/^' . preg_quote($searchString) . ':/', $partVar); + }); + + foreach ($varsToReplace as $varNameWithArgs) { + $varInlineArgs = $this->getImageArgs($varNameWithArgs); + $preparedImageAttrs = $this->prepareImageAttrs($replaceImage, $varInlineArgs); + $imgPath = $preparedImageAttrs['src']; + + // get image index + $imgIndex = $this->getNextRelationsIndex($partFileName); + $rid = 'rId' . $imgIndex; + + // replace preparations + $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']); + $xmlImage = str_replace(array('{RID}', '{WIDTH}', '{HEIGHT}'), array($rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']), $imgTpl); + + // replace variable + $varNameWithArgsFixed = self::ensureMacroCompleted($varNameWithArgs); + $matches = array(); + if (preg_match('/(<[^<]+>)([^<]*)(' . preg_quote($varNameWithArgsFixed) . ')([^>]*)(<[^>]+>)/Uu', $partContent, $matches)) { + $wholeTag = $matches[0]; + array_shift($matches); + list($openTag, $prefix, , $postfix, $closeTag) = $matches; + $replaceXml = $openTag . $prefix . $closeTag . $xmlImage . $openTag . $postfix . $closeTag; + // replace on each iteration, because in one tag we can have 2+ inline variables => before proceed next variable we need to change $partContent + $partContent = $this->setValueForPart($wholeTag, $replaceXml, $partContent, $limit); + } + } + } + } + } + /** * Returns count of all variables in template. * @@ -406,15 +709,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.'); @@ -423,6 +728,19 @@ public function save() return $this->tempDocumentFilename; } + /** + * @param string $fileName + * @param string $xml + */ + 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. * @@ -542,6 +860,35 @@ protected function getFooterName($index) return sprintf('word/footer%d.xml', $index); } + /** + * Get the name of the relations file for document part. + * + * @param string $documentPartName + * + * @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], 'assertEquals(array('documentContent', 'headerValue', 'footerValue'), $templateProcessor->getVariables()); + $this->assertEquals(array('documentContent', 'headerValue:100:100', 'footerValue'), $templateProcessor->getVariables()); $macroNames = array('headerValue', 'documentContent', 'footerValue'); $macroValues = array('Header Value', 'Document text.', 'Footer Value'); @@ -200,6 +200,83 @@ 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' => 100, 'height' => 50, 'ratio' => false), + ); + $templateProcessor->setImageValue(array_keys($variablesReplace), $variablesReplace); + + $docName = 'header-footer-images-test-result.docx'; + $templateProcessor->saveAs($docName); + + $this->assertFileExists($docName, "Generated file '{$docName}' not found!"); + + $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/image_rId11_document.jpeg'); + if (false === $expectedDocumentZip->close()) { + throw new \Exception("Could not close zip file \"{$docName}\"."); + } + + $this->assertNotEmpty($expectedImage, 'Embed image doesn\'t found.'); + $this->assertContains('/word/media/image_rId11_document.jpeg', $expectedContentTypesXml, '[Content_Types].xml missed "/word/media/image5_document.jpeg"'); + $this->assertContains('/word/_rels/header1.xml.rels', $expectedContentTypesXml, '[Content_Types].xml missed "/word/_rels/header1.xml.rels"'); + $this->assertContains('/word/_rels/footer1.xml.rels', $expectedContentTypesXml, '[Content_Types].xml missed "/word/_rels/footer1.xml.rels"'); + $this->assertNotContains('${documentContent}', $expectedMainPartXml, 'word/document.xml has no image.'); + $this->assertNotContains('${headerValue}', $expectedHeaderPartXml, 'word/header1.xml has no image.'); + $this->assertNotContains('${footerValue}', $expectedFooterPartXml, 'word/footer1.xml has no image.'); + $this->assertContains('media/image_rId11_document.jpeg', $expectedDocumentRelationsXml, 'word/_rels/document.xml.rels missed "media/image5_document.jpeg"'); + $this->assertContains('media/image_rId11_document.jpeg', $expectedHeaderRelationsXml, 'word/_rels/header1.xml.rels missed "media/image5_document.jpeg"'); + $this->assertContains('media/image_rId11_document.jpeg', $expectedFooterRelationsXml, 'word/_rels/footer1.xml.rels missed "media/image5_document.jpeg"'); + + unlink($docName); + + // dynamic generated doc + $testFileName = 'images-test-sample.docx'; + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $section->addText('${Test:width=100:ratio=true}'); + $objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007'); + $objWriter->save($testFileName); + $this->assertFileExists($testFileName, "Generated file '{$testFileName}' not found!"); + + $resultFileName = 'images-test-result.docx'; + $templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor($testFileName); + unlink($testFileName); + $templateProcessor->setImageValue('Test', $imagePath); + $templateProcessor->setImageValue('Test1', $imagePath); + $templateProcessor->setImageValue('Test2', $imagePath); + $templateProcessor->saveAs($resultFileName); + $this->assertFileExists($resultFileName, "Generated file '{$resultFileName}' not found!"); + + $expectedDocumentZip = new \ZipArchive(); + $expectedDocumentZip->open($resultFileName); + $expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml'); + if (false === $expectedDocumentZip->close()) { + throw new \Exception("Could not close zip file \"{$resultFileName}\"."); + } + unlink($resultFileName); + + $this->assertNotContains('${Test}', $expectedMainPartXml, 'word/document.xml has no image.'); + } + /** * @covers ::cloneBlock * @covers ::deleteBlock diff --git a/tests/PhpWord/_files/templates/header-footer.docx b/tests/PhpWord/_files/templates/header-footer.docx index 647d52222d..dc2e9e8bea 100644 Binary files a/tests/PhpWord/_files/templates/header-footer.docx and b/tests/PhpWord/_files/templates/header-footer.docx differ