Support adding images in Templates (#1170)

* setImageValue() + fix adding files via ZipArchive
* fix phpdoc variable name
* Changed logic that determines extension image file extension for document to depend on MIME type. This same logic is used in Element/Image.php
* support <w:t> tags with arguments
* allow setup size of image into template variable
* support of 'ratio' replace attribute + documentation
This commit is contained in:
Maxim 2018-12-26 15:35:21 +02:00 committed by troosan
parent e6496bf411
commit d5da80b56e
7 changed files with 462 additions and 15 deletions

View File

@ -24,8 +24,6 @@ matrix:
- php: 5.3
- php: 7.0
- php: 7.3
allow_failures:
- php: 7.3
cache:
directories:

View File

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

View File

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

View File

@ -129,6 +129,7 @@ class ZipArchive
{
$result = true;
$this->filename = $filename;
$this->tempDir = Settings::getTempDir();
if (!$this->usePclzip) {
$zip = new \ZipArchive();
@ -139,7 +140,6 @@ class ZipArchive
$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 @@ class ZipArchive
$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

View File

@ -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 @@ class TemplateProcessor
$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 @@ class TemplateProcessor
$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 = '<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(
'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('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
}
$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>';
}
/**
* @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 = '<w:pict><v:shape type="#_x0000_t75" style="width:{WIDTH};height:{HEIGHT}"><v:imagedata r:id="{RID}" o:title=""/></v:shape></w:pict>';
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 @@ class TemplateProcessor
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 @@ class TemplateProcessor
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 @@ class TemplateProcessor
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], '<Relationship');
}
return 1;
}
/**
* @return string
*/
protected function getDocumentContentTypesName()
{
return '[Content_Types].xml';
}
/**
* Find the start position of the nearest table row before $offset.
*

View File

@ -187,7 +187,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
{
$templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx');
$this->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 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
$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