Advertisement
FBnil

phpword_templateprocessor_proposal

Oct 7th, 2017
129
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 50.62 KB | None | 0 0
  1. <?php
  2. /**
  3.  * This file is part of PHPWord - A pure PHP library for reading and writing
  4.  * word processing documents.
  5.  *
  6.  * PHPWord is free software distributed under the terms of the GNU Lesser
  7.  * General Public License version 3 as published by the Free Software Foundation.
  8.  *
  9.  * For the full copyright and license information, please read the LICENSE
  10.  * file that was distributed with this source code. For the full list of
  11.  * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
  12.  *
  13.  * @link      https://github.com/PHPOffice/PHPWord
  14.  * @copyright 2010-2017 PHPWord contributors
  15.  * @license   http://www.gnu.org/licenses/lgpl.txt LGPL version 3
  16.  */
  17.  
  18. namespace PhpOffice\PhpWord;
  19.  
  20. use PhpOffice\PhpWord\Escaper\RegExp;
  21. use PhpOffice\PhpWord\Escaper\Xml;
  22. use PhpOffice\PhpWord\Exception\CopyFileException;
  23. use PhpOffice\PhpWord\Exception\CreateTemporaryFileException;
  24. use PhpOffice\PhpWord\Exception\Exception;
  25. use PhpOffice\PhpWord\Shared\Converter;
  26. use PhpOffice\PhpWord\Shared\ZipArchive;
  27. use Zend\Stdlib\StringUtils;
  28.  
  29. class TemplateProcessor
  30. {
  31.     const MAXIMUM_REPLACEMENTS_DEFAULT = -1;
  32.  
  33.     /**
  34.      * sprintf template for fragment to insert an image
  35.      *
  36.      * sprintf arguments:
  37.      * 1. d, width in EMU
  38.      * 2. d, height in EMU
  39.      * 3. s, graphic id (usually sequential, e.g. "1")
  40.      * 4. s, graphic name (usually sequential with prefix, e.g. "Grafik 1")
  41.      * 5. s, graphic filename (virtual, e.g. "MyLovelyHorse.jpg")
  42.      * 6. s, relationship ID (see argument 1 for RELATIONSHIP_TEMPLATE)
  43.      *
  44.      * @see http://blogs.msdn.com/b/dmahugh/archive/2006/12/10/images-in-open-xml-documents.aspx
  45.      */
  46.     const IMAGE_TEMPLATE = '<w:drawing><wp:inline><wp:extent cx="%1$d" cy="%2$d"/><wp:docPr id="%3$s" name="%4$s"/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="0" name="%5$s"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="%6$s" cstate="print"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="%1$d" cy="%2$d"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing>';
  47.  
  48.     /**
  49.      * sprintf template for fragment to create an image relationship
  50.      *
  51.      * sprintf arguments:
  52.      * 1: s, namespace prefix, including colon
  53.      * 2: s, relationship ID (see argument 7 for IMAGE_TEMPLATE)
  54.      * 3: s, image filename
  55.      *
  56.      * @see http://blogs.msdn.com/b/dmahugh/archive/2006/12/10/images-in-open-xml-documents.aspx
  57.      * @see http://hastobe.net/blogs/stevemorgan/archive/2008/09/15/howto-insert-an-image-into-a-word-document-and-display-it-using-openxml.aspx
  58.      */
  59.     const RELATIONSHIP_TEMPLATE = '<%sRelationship Id="%s" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="%s"/>';
  60.  
  61.     /**
  62.      * sprintf template for fragment to add a default content type
  63.      *
  64.      * sprintf arguments:
  65.      * 1: s, namespace prefix, including colon
  66.      * 2: s, extension
  67.      * 3: s, MIME-type
  68.      */
  69.     const CONTENTTYPE_DEFAULT_TEMPLATE = '<%sDefault Extension="%s" ContentType="%s"/>';
  70.  
  71.     /**
  72.      * sprintf template for fragment to add an override content type
  73.      *
  74.      * sprintf arguments:
  75.      * 1: s, namespace prefix, including colon
  76.      * 2: s, path to part file
  77.      * 3: s, MIME-type
  78.      */
  79.     const CONTENTTYPE_OVERRIDE_TEMPLATE = '<%sOverride PartName="%s" ContentType="%s"/>';
  80.  
  81.     /**
  82.      * Template for a new relationships file
  83.      */
  84.     const RELATIONSHIPS_FILE_TEMPLATE = <<<'ENDXML'
  85. <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  86. <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>
  87. ENDXML;
  88.  
  89.     /**
  90.      * sprintf Template for a table
  91.      *
  92.      * sprintf arguments:
  93.      * 1: s, gridCol definition elements
  94.      * 2: s, table cell elements
  95.      */
  96.     const TABLE_TEMPLATE = '<w:tbl><w:tblPr><w:tblStyle w:val="Tabellenraster"/><w:tblW w:w="0" w:type="auto"/><w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/></w:tblPr><w:tblGrid>%s</w:tblGrid><w:tr>%s</w:tr></w:tbl>';
  97.  
  98.     /**
  99.      * sprintf Template for a gridCol element in a table definition
  100.      *
  101.      * sprintf arguments:
  102.      * 1: d, width of the column in Twips
  103.      */
  104.     const TABLEGRIDCOL_TEMPLATE = '<w:gridCol w:w="%d"/>';
  105.  
  106.     /**
  107.      * sprintf Template for a table cell element in a table definition
  108.      *
  109.      * sprintf arguments:
  110.      * 1: d, width of the cell in Twips
  111.      * 2: s, cell text content
  112.      */
  113.     const TABLECELL_TEMPLATE = '<w:tc><w:tcPr><w:tcW w:w="%d" w:type="dxa"/></w:tcPr><w:p><w:r><w:t>%s</w:t></w:r></w:p></w:tc>';
  114.  
  115.     /**
  116.      * ZipArchive object.
  117.      *
  118.      * @var mixed
  119.      */
  120.     protected $zipClass;
  121.  
  122.     /**
  123.      * @var string Temporary document filename (with path).
  124.      */
  125.     protected $tempDocumentFilename;
  126.  
  127.     /**
  128.      * Content of main document part (in XML format) of the temporary document.
  129.      *
  130.      * @var string
  131.      */
  132.     protected $tempDocumentMainPart;
  133.  
  134.     /**
  135.      * Document relationships (in XML format) of the temporary document.
  136.      *
  137.      * @var string
  138.      */
  139.     protected $tempDocumentRelationships;
  140.  
  141.     /**
  142.      * Content of headers (in XML format) of the temporary document.
  143.      *
  144.      * @var string[]
  145.      */
  146.     protected $tempDocumentHeaders = array();
  147.  
  148.     /**
  149.      * Document header relationships (in XML format) of the temporary document.
  150.      *
  151.      * @var string[]
  152.      */
  153.     protected $tempDocumentHeadersRelationships = array();
  154.  
  155.     /**
  156.      * Content of footers (in XML format) of the temporary document.
  157.      *
  158.      * @var string[]
  159.      */
  160.     protected $tempDocumentFooters = array();
  161.  
  162.     /**
  163.      * Document footer relationships (in XML format) of the temporary document.
  164.      *
  165.      * @var string[]
  166.      */
  167.     protected $tempDocumentFootersRelationships = array();
  168.  
  169.     /**
  170.      * Document content types (in XML format) of the temporary document.
  171.      *
  172.      * @var string
  173.      */
  174.     protected $tempDocumentContentTypes;
  175.  
  176.     /**
  177.      * @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception.
  178.      *
  179.      * @param string $documentTemplate The fully qualified template filename.
  180.      *
  181.      * @throws \PhpOffice\PhpWord\Exception\CreateTemporaryFileException
  182.      * @throws \PhpOffice\PhpWord\Exception\CopyFileException
  183.      */
  184.     public function __construct($documentTemplate)
  185.     {
  186.         // Temporary document filename initialization
  187.         $this->tempDocumentFilename = tempnam(Settings::getTempDir(), 'PhpWord');
  188.         if (false === $this->tempDocumentFilename) {
  189.             throw new CreateTemporaryFileException();
  190.         }
  191.  
  192.         // Template file cloning
  193.         if (false === copy($documentTemplate, $this->tempDocumentFilename)) {
  194.             throw new CopyFileException($documentTemplate, $this->tempDocumentFilename);
  195.         }
  196.  
  197.         // Temporary document content extraction
  198.         $this->zipClass = new ZipArchive();
  199.         $this->zipClass->open($this->tempDocumentFilename);
  200.         $index = 1;
  201.         while (false !== $this->zipClass->locateName($this->getHeaderName($index))) {
  202.             $this->tempDocumentHeaders[$index] = $this->fixBrokenMacros(
  203.                 $this->zipClass->getFromName($this->getHeaderName($index))
  204.             );
  205.             if (false !== $this->zipClass->locateName($this->getHeaderRelsName($index))) {
  206.                 $this->tempDocumentHeadersRelationships[$index] = $this->zipClass->getFromName($this->getHeaderRelsName($index));
  207.             }
  208.             $index++;
  209.         }
  210.         $index = 1;
  211.         while (false !== $this->zipClass->locateName($this->getFooterName($index))) {
  212.             $this->tempDocumentFooters[$index] = $this->fixBrokenMacros(
  213.                 $this->zipClass->getFromName($this->getFooterName($index))
  214.             );
  215.             if (false !== $this->zipClass->locateName($this->getFooterRelsName($index))) {
  216.                 $this->tempDocumentFootersRelationships[$index] = $this->zipClass->getFromName($this->getFooterRelsName($index));
  217.             }
  218.             $index++;
  219.         }
  220.         $this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName($this->getMainPartName()));
  221.         $this->tempDocumentRelationships = $this->zipClass->getFromName($this->getMainPartRelsName());
  222.         $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getContentTypesPartName());
  223.     }
  224.  
  225.     /**
  226.      * @param string $xml
  227.      * @param \XSLTProcessor $xsltProcessor
  228.      *
  229.      * @return string
  230.      *
  231.      * @throws \PhpOffice\PhpWord\Exception\Exception
  232.      */
  233.     protected function transformSingleXml($xml, $xsltProcessor)
  234.     {
  235.         $domDocument = new \DOMDocument();
  236.         if (false === $domDocument->loadXML($xml)) {
  237.             throw new Exception('Could not load the given XML document.');
  238.         }
  239.  
  240.         $transformedXml = $xsltProcessor->transformToXml($domDocument);
  241.         if (false === $transformedXml) {
  242.             throw new Exception('Could not transform the given XML document.');
  243.         }
  244.  
  245.         return $transformedXml;
  246.     }
  247.  
  248.     /**
  249.      * @param mixed $xml
  250.      * @param \XSLTProcessor $xsltProcessor
  251.      *
  252.      * @return mixed
  253.      */
  254.     protected function transformXml($xml, $xsltProcessor)
  255.     {
  256.         if (is_array($xml)) {
  257.             foreach ($xml as &$item) {
  258.                 $item = $this->transformSingleXml($item, $xsltProcessor);
  259.             }
  260.         } else {
  261.             $xml = $this->transformSingleXml($xml, $xsltProcessor);
  262.         }
  263.  
  264.         return $xml;
  265.     }
  266.  
  267.     /**
  268.      * Applies XSL style sheet to template's parts.
  269.      *
  270.      * Note: since the method doesn't make any guess on logic of the provided XSL style sheet,
  271.      * make sure that output is correctly escaped. Otherwise you may get broken document.
  272.      *
  273.      * @param \DOMDocument $xslDomDocument
  274.      * @param array $xslOptions
  275.      * @param string $xslOptionsUri
  276.      *
  277.      * @return void
  278.      *
  279.      * @throws \PhpOffice\PhpWord\Exception\Exception
  280.      */
  281.     public function applyXslStyleSheet($xslDomDocument, $xslOptions = array(), $xslOptionsUri = '')
  282.     {
  283.         $xsltProcessor = new \XSLTProcessor();
  284.  
  285.         $xsltProcessor->importStylesheet($xslDomDocument);
  286.         if (false === $xsltProcessor->setParameter($xslOptionsUri, $xslOptions)) {
  287.             throw new Exception('Could not set values for the given XSL style sheet parameters.');
  288.         }
  289.  
  290.         $this->tempDocumentHeaders = $this->transformXml($this->tempDocumentHeaders, $xsltProcessor);
  291.         $this->tempDocumentMainPart = $this->transformXml($this->tempDocumentMainPart, $xsltProcessor);
  292.         $this->tempDocumentFooters = $this->transformXml($this->tempDocumentFooters, $xsltProcessor);
  293.     }
  294.  
  295.     /**
  296.      * @param string $macro
  297.      *
  298.      * @return string
  299.      */
  300.     protected static function ensureMacroCompleted($macro)
  301.     {
  302.         if (substr($macro, 0, 2) !== '${' && substr($macro, -1) !== '}') {
  303.             $macro = '${' . $macro . '}';
  304.         }
  305.  
  306.         return $macro;
  307.     }
  308.  
  309.     /**
  310.      * @param string $subject
  311.      *
  312.      * @return string
  313.      */
  314.     protected static function ensureUtf8Encoded($subject)
  315.     {
  316.         if (!StringUtils::isValidUtf8($subject)) {
  317.             $subject = utf8_encode($subject);
  318.         }
  319.  
  320.         return $subject;
  321.     }
  322.  
  323.     /**
  324.      * @param mixed $search
  325.      * @param mixed $replace
  326.      * @param integer $limit
  327.      *
  328.      * @return void
  329.      */
  330.     public function setValue($search, $replace, $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT)
  331.     {
  332.         if (is_array($search)) {
  333.             foreach ($search as &$item) {
  334.                 $item = self::ensureMacroCompleted($item);
  335.             }
  336.         } else {
  337.             $search = self::ensureMacroCompleted($search);
  338.         }
  339.  
  340.         if (is_array($replace)) {
  341.             foreach ($replace as &$item) {
  342.                 $item = self::ensureUtf8Encoded($item);
  343.             }
  344.         } else {
  345.             $replace = self::ensureUtf8Encoded($replace);
  346.         }
  347.  
  348.         if (Settings::isOutputEscapingEnabled()) {
  349.             $xmlEscaper = new Xml();
  350.             $replace = $xmlEscaper->escape($replace);
  351.         }
  352.  
  353.         $this->tempDocumentHeaders = $this->setValueForPart($search, $replace, $this->tempDocumentHeaders, $limit);
  354.         $this->tempDocumentMainPart = $this->setValueForPart($search, $replace, $this->tempDocumentMainPart, $limit);
  355.         $this->tempDocumentFooters = $this->setValueForPart($search, $replace, $this->tempDocumentFooters, $limit);
  356.     }
  357.  
  358.     /**
  359.      * Updates a file inside the document, from a string (with binary data)
  360.      *
  361.      * @param string $localname
  362.      * @param string $contents
  363.      *
  364.      * @return bool
  365.      */
  366.     public function zipAddFromString($localname, $contents)
  367.     {
  368.         return $this->zipClass->AddFromString($localname, $contents);
  369.     }
  370.  
  371.     /**
  372.      * Returns array of all variables in template.
  373.      *
  374.      * @return string[]
  375.      */
  376.     public function getVariables()
  377.     {
  378.         $variables = $this->getVariablesForPart($this->tempDocumentMainPart);
  379.  
  380.         foreach ($this->tempDocumentHeaders as $headerXML) {
  381.             $variables = array_merge($variables, $this->getVariablesForPart($headerXML));
  382.         }
  383.  
  384.         foreach ($this->tempDocumentFooters as $footerXML) {
  385.             $variables = array_merge($variables, $this->getVariablesForPart($footerXML));
  386.         }
  387.  
  388.         return array_unique($variables);
  389.     }
  390.  
  391.     /**
  392.      * Insert a table at the place marked by the block template
  393.      * @param string $search Name of block template
  394.      * @param array $columns Array keyed on column (template) name, containing
  395.      *                       column width in CSS units (string) or twips (integer)
  396.      * @param boolean $throwexception
  397.      *
  398.      * @return \PhpOffice\PhpWord\TemplateProcessor
  399.      */
  400.     public function insertTable($search, array $columns, $throwexception = false)
  401.     {
  402.         $gridCols = '';
  403.         $cells = '';
  404.  
  405.         foreach ($columns as $variable => $width) {
  406.             $twipWidth = is_string($width) ? Converter::cssToTwip($width) : $width;
  407.             $gridCols .= sprintf(static::TABLEGRIDCOL_TEMPLATE, $twipWidth);
  408.             $cells .= sprintf(static::TABLECELL_TEMPLATE, $twipWidth, static::ensureMacroCompleted($variable));
  409.         }
  410.  
  411.         return $this->replaceSegment(
  412.             $this->ensureMacroCompleted($search),
  413.             'w:p',
  414.             sprintf(static::TABLE_TEMPLATE, $gridCols, $cells),
  415.             'MainPart',
  416.             $throwexception
  417.         );
  418.     }
  419.  
  420.     /**
  421.      * Delete a table containing the given variable
  422.      *
  423.      * @param string $search
  424.      *
  425.      * @return \PhpOffice\PhpWord\TemplateProcessor
  426.      */
  427.     public function deleteTable($search)
  428.     {
  429.         return $this->deleteSegment($this->ensureMacroCompleted($search), 'w:tbl');
  430.     }
  431.  
  432.     /**
  433.      * Clone a table row in a template document.
  434.      *
  435.      * @param string  $search
  436.      * @param integer $numberOfClones
  437.      * @param bool    $replace
  438.      * @param bool    $incrementVariables
  439.      * @param bool    $throwexception
  440.      *
  441.      * @return string|null
  442.      *
  443.      * @throws \PhpOffice\PhpWord\Exception\Exception
  444.      */
  445.     public function cloneRow(
  446.         $search,
  447.         $numberOfClones = 1,
  448.         $replace = true,
  449.         $incrementVariables = true,
  450.         $throwexception = false
  451.     ) {
  452.         if ('${' !== substr($search, 0, 2) && '}' !== substr($search, -1)) {
  453.             $search = '${' . $search . '}';
  454.         }
  455.  
  456.         $tagPos = strpos($this->tempDocumentMainPart, $search);
  457.         if (!$tagPos) {
  458.             if ($throwexception) {
  459.                 throw new Exception(
  460.                     "Can not clone row, template variable not found or variable contains markup."
  461.                 );
  462.             } else {
  463.                 return null;
  464.             }
  465.         }
  466.  
  467.         $rowStart = $this->findTagLeft('<w:tr>', $tagPos, $throwexception); // findRowStart
  468.         $rowEnd = $this->findTagRight('</w:tr>', $tagPos); // findRowEnd
  469.         $xmlRow = $this->getSlice($rowStart, $rowEnd);
  470.  
  471.         // Check if there's a cell spanning multiple rows.
  472.         if (preg_match('#<w:vMerge w:val="restart"/>#', $xmlRow)) {
  473.             // $extraRowStart = $rowEnd;
  474.             $extraRowEnd = $rowEnd;
  475.             while (true) {
  476.                 $extraRowStart = $this->findTagLeft('<w:tr>', $extraRowEnd + 1, $throwexception); // findRowStart
  477.                 $extraRowEnd = $this->findTagRight('</w:tr>', $extraRowEnd + 1); // findRowEnd
  478.  
  479.                 if (!$extraRowEnd) {
  480.                     break;
  481.                 }
  482.  
  483.                 // If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row.
  484.                 $tmpXmlRow = $this->getSlice($extraRowStart, $extraRowEnd);
  485.                 if (!preg_match('#<w:vMerge/>#', $tmpXmlRow)
  486.                     && !preg_match('#<w:vMerge w:val="continue" />#', $tmpXmlRow)
  487.                 ) {
  488.                     break;
  489.                 }
  490.                 // This row was a spanned row, update $rowEnd and search for the next row.
  491.                 $rowEnd = $extraRowEnd;
  492.             }
  493.             $xmlRow = $this->getSlice($rowStart, $rowEnd);
  494.         }
  495.  
  496.         if ($replace) {
  497.             $result = $this->getSlice(0, $rowStart);
  498.             for ($i = 1; $i <= $numberOfClones; $i++) {
  499.                 if ($incrementVariables) {
  500.                     $result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow);
  501.                 } else {
  502.                     $result .= $xmlRow;
  503.                 }
  504.             }
  505.             $result .= $this->getSlice($rowEnd);
  506.  
  507.             $this->tempDocumentMainPart = $result;
  508.         }
  509.  
  510.         return $xmlRow;
  511.     }
  512.  
  513.     /**
  514.      * Delete a row containing the given variable
  515.      *
  516.      * @param string $search
  517.      *
  518.      * @return \PhpOffice\PhpWord\Template
  519.      */
  520.     public function deleteRow($search)
  521.     {
  522.         return $this->deleteSegment($this->ensureMacroCompleted($search), 'w:tr');
  523.     }
  524.  
  525.     /**
  526.      * Clone a block.
  527.      *
  528.      * @param string  $blockname
  529.      * @param integer $clones
  530.      * @param boolean $replace
  531.      * @param boolean $incrementVariables
  532.      * @param boolean $throwexception
  533.      *
  534.      * @return string|null
  535.      */
  536.     public function cloneBlock(
  537.         $blockname,
  538.         $clones = 1,
  539.         $replace = true,
  540.         $incrementVariables = true,
  541.         $throwexception = false
  542.     ) {
  543.         $startSearch = '${'  . $blockname . '}';
  544.         $endSearch =   '${/' . $blockname . '}';
  545.  
  546.         if (substr($blockname, -1) == '/') { // singleton/closed block
  547.             return $this->cloneSegment($startSearch, 'w:p', 'MainPart', $clones, $replace, $incrementVariables, $throwexception);
  548.         }
  549.  
  550.         $startTagPos = strpos($this->tempDocumentMainPart, $startSearch);
  551.         $endTagPos = strpos($this->tempDocumentMainPart, $endSearch, $startTagPos);
  552.  
  553.         if (!$startTagPos || !$endTagPos) {
  554.             if ($throwexception) {
  555.                 throw new Exception(
  556.                     "Can not find block '$blockname', template variable not found or variable contains markup."
  557.                 );
  558.             } else {
  559.                 return null; // Block not found, return null
  560.             }
  561.         }
  562.  
  563.         $startBlockStart = $this->findTagLeft('<w:p>', $startTagPos, $throwexception); // findBlockStart()
  564.         $startBlockEnd = $this->findTagRight('</w:p>', $startTagPos); // findBlockEnd()
  565.         // $xmlStart = $this->getSlice($startBlockStart, $startBlockEnd);
  566.  
  567.         if (!$startBlockStart || !$startBlockEnd) {
  568.             if ($throwexception) {
  569.                 throw new Exception(
  570.                     "Can not find start paragraph around block '$blockname'"
  571.                 );
  572.             } else {
  573.                 return false;
  574.             }
  575.         }
  576.  
  577.         $endBlockStart = $this->findTagLeft('<w:p>', $endTagPos, $throwexception); // findBlockStart()
  578.         $endBlockEnd = $this->findTagRight('</w:p>', $endTagPos); // findBlockEnd()
  579.         // $xmlEnd = $this->getSlice($endBlockStart, $endBlockEnd);
  580.  
  581.         if (!$endBlockStart || !$endBlockEnd) {
  582.             if ($throwexception) {
  583.                 throw new Exception(
  584.                     "Can not find end paragraph around block '$blockname'"
  585.                 );
  586.             } else {
  587.                 return false;
  588.             }
  589.         }
  590.  
  591.         if ($startBlockEnd == $endBlockEnd) { // inline block
  592.             $startBlockStart = $startTagPos;
  593.             $startBlockEnd = $startTagPos + strlen($startSearch);
  594.             $endBlockStart = $endTagPos;
  595.             $endBlockEnd = $endTagPos + strlen($endSearch);
  596.         }
  597.  
  598.         $xmlBlock = $this->getSlice($startBlockEnd, $endBlockStart);
  599.  
  600.         if ($replace) {
  601.             $result = $this->getSlice(0, $startBlockStart);
  602.             for ($i = 1; $i <= $clones; $i++) {
  603.                 if ($incrementVariables) {
  604.                     $result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlBlock);
  605.                 } else {
  606.                     $result .= $xmlBlock;
  607.                 }
  608.             }
  609.             $result .= $this->getSlice($endBlockEnd);
  610.  
  611.             $this->tempDocumentMainPart = $result;
  612.         }
  613.  
  614.         return $xmlBlock;
  615.     }
  616.  
  617.     /**
  618.      * Clone a segment.
  619.      *
  620.      * @param string  $needle
  621.      * @param string  $xmltag
  622.      * @param string  $docpart
  623.      * @param integer $clones
  624.      * @param boolean $replace
  625.      * @param boolean $incrementVariables
  626.      * @param boolean $throwexception
  627.      *
  628.      * @return string|null
  629.      */
  630.     public function cloneSegment(
  631.         $needle,
  632.         $xmltag,
  633.         $docpart = 'MainPart',
  634.         $clones = 1,
  635.         $replace = true,
  636.         $incrementVariables = true,
  637.         $throwexception = false
  638.     ) {
  639.         $needlePos = strpos($this->{"tempDocument$docpart"}, $needle);
  640.  
  641.         if (!$needlePos) {
  642.             if ($throwexception) {
  643.                 throw new Exception(
  644.                     "Can not find segment '$needle', text not found or text contains markup."
  645.                 );
  646.             } else {
  647.                 return null; // Segment not found, return null
  648.             }
  649.         }
  650.  
  651.         $startSegmentStart = $this->findTagLeft("<$xmltag>", $needlePos, $throwexception);
  652.         $endSegmentEnd = $this->findTagRight("</$xmltag>", $needlePos);
  653.  
  654.         if (!$startSegmentStart || !$endSegmentEnd) {
  655.             if ($throwexception) {
  656.                 throw new Exception(
  657.                     "Can not find <$xmltag> around segment '$needle'"
  658.                 );
  659.             } else {
  660.                 return false;
  661.             }
  662.         }
  663.  
  664.         $xmlSegment = $this->getSlice($startSegmentStart, $endSegmentEnd);
  665.  
  666.         if ($replace) {
  667.             $result = $this->getSlice(0, $startSegmentStart);
  668.             for ($i = 1; $i <= $clones; $i++) {
  669.                 if ($incrementVariables) {
  670.                     $result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlSegment);
  671.                 } else {
  672.                     $result .= $xmlSegment;
  673.                 }
  674.             }
  675.             $result .= $this->getSlice($endSegmentEnd);
  676.  
  677.             $this->{"tempDocument$docpart"} = $result;
  678.         }
  679.  
  680.         return $xmlSegment;
  681.     }
  682.  
  683.     /**
  684.      * Get a block. (first block found)
  685.      *
  686.      * @param string  $blockname
  687.      * @param boolean $throwexception
  688.      *
  689.      * @return string|null
  690.      */
  691.     public function getBlock($blockname, $throwexception = false)
  692.     {
  693.         return $this->cloneBlock($blockname, 1, false, false, $throwexception);
  694.     }
  695.  
  696.     /**
  697.      * Get a segment. (first segment found)
  698.      *
  699.      * @param string  $needle
  700.      * @param string  $xmltag
  701.      * @param string  $docpart
  702.      * @param boolean $throwexception
  703.      *
  704.      * @return string|null
  705.      */
  706.     public function getSegment($needle, $xmltag, $docpart = 'MainPart', $throwexception = false)
  707.     {
  708.         return $this->cloneSegment($needle, $xmltag, $docpart, 1, false, false, $throwexception);
  709.     }
  710.  
  711.     /**
  712.      * Get a row. (first block found)
  713.      *
  714.      * @param string  $rowname
  715.      * @param boolean $throwexception
  716.      *
  717.      * @return string|null
  718.      */
  719.     public function getRow($rowname, $throwexception = false)
  720.     {
  721.         return $this->cloneRow($rowname, 1, false, false, $throwexception);
  722.     }
  723.  
  724.     /**
  725.      * Replace a block.
  726.      *
  727.      * @param string  $blockname
  728.      * @param string  $replacement
  729.      * @param boolean $throwexception
  730.      *
  731.      * @return false on no replacement, true on replacement
  732.      */
  733.     public function replaceBlock($blockname, $replacement = '', $throwexception = false)
  734.     {
  735.         $startSearch = '${'  . $blockname . '}';
  736.         $endSearch   = '${/' . $blockname . '}';
  737.  
  738.         if (substr($blockname, -1) == '/') { // singleton/closed block
  739.             return $this->replaceSegment($startSearch, 'w:p', $replacement, 'MainPart', $throwexception);
  740.         }
  741.  
  742.         $startTagPos = strpos($this->tempDocumentMainPart, $startSearch);
  743.         $endTagPos = strpos($this->tempDocumentMainPart, $endSearch, $startTagPos);
  744.  
  745.         if (!$startTagPos || !$endTagPos) {
  746.             if ($throwexception) {
  747.                 throw new Exception(
  748.                     "Can not find block '$blockname', template variable not found or variable contains markup."
  749.                 );
  750.             } else {
  751.                 return false;
  752.             }
  753.         }
  754.  
  755.         $startBlockStart = $this->findTagLeft('<w:p>', $startTagPos, $throwexception); // findBlockStart()
  756.         $endBlockEnd = $this->findTagRight('</w:p>', $endTagPos); // findBlockEnd()
  757.  
  758.         if (!$startBlockStart || !$endBlockEnd) {
  759.             if ($throwexception) {
  760.                 throw new Exception(
  761.                     "Can not find end paragraph around block '$blockname'"
  762.                 );
  763.             } else {
  764.                 return false;
  765.             }
  766.         }
  767.  
  768.         $startBlockEnd = $this->findTagRight('</w:p>', $startTagPos); // findBlockEnd()
  769.         if ($startBlockEnd == $endBlockEnd) { // inline block
  770.             $startBlockStart = $startTagPos;
  771.             $endBlockEnd = $endTagPos + strlen($endSearch);
  772.         }
  773.  
  774.         $this->tempDocumentMainPart =
  775.             $this->getSlice(0, $startBlockStart)
  776.             . $replacement
  777.             . $this->getSlice($endBlockEnd);
  778.  
  779.         return true;
  780.     }
  781.  
  782.  
  783.     /**
  784.      * Replace a segment.
  785.      *
  786.      * @param string  $needle
  787.      * @param string  $xmltag
  788.      * @param string  $replacement
  789.      * @param string  $docpart
  790.      * @param boolean $throwexception
  791.      *
  792.      * @return false on no replacement, true on replacement
  793.      */
  794.     public function replaceSegment($needle, $xmltag, $replacement = '', $docpart = 'MainPart', $throwexception = false)
  795.     {
  796.         $TagPos = strpos($this->{"tempDocument$docpart"}, $needle);
  797.  
  798.         if ($TagPos === false) {
  799.             if ($throwexception) {
  800.                 throw new Exception(
  801.                     "Can not find segment '$needle', text not found or text contains markup."
  802.                 );
  803.             } else {
  804.                 return false;
  805.             }
  806.         }
  807.  
  808.         $SegmentStart = $this->findTagLeft("<$xmltag>", $TagPos, $throwexception);
  809.         $SegmentEnd = $this->findTagRight("</$xmltag>", $TagPos);
  810.  
  811.         $this->{"tempDocument$docpart"} =
  812.             $this->getSlice(0, $SegmentStart)
  813.             . $replacement
  814.             . $this->getSlice($SegmentEnd);
  815.  
  816.         return true;
  817.     }
  818.  
  819.     /**
  820.      * Insert an image at locations specifed using an image placeholder
  821.      *
  822.      * The placeholder takes the form ${img:name}, or ${img:name:width:height}
  823.      *
  824.      * @param string $name        Image placeholder name (${img:$name})
  825.      * @param string $srcFilename Path to image file to insert
  826.      * @param string $width       Width of image, including units (e.g. 240pt);
  827.      *                            will be read from image assuming 96dpi if not
  828.      *                            supplied either here or as part of the
  829.      *                            placeholder
  830.      * @param string $height      Height of image, including units (e.g. 360pt);
  831.      *                            will be read from image assuming 96dpi if not
  832.      *                            supplied either here or as part of the
  833.      *                            placeholder
  834.      * @param string $mimeType    MIME-type of image; will be autodetected from
  835.      *                            image if not supplied
  836.      * @param string $filename    Name of file as it should be inserted in
  837.      *                            document: the basename of $srcFilename if not
  838.      *                            supplied
  839.      *
  840.      * @return \PhpOffice\PhpWord\Template
  841.      *
  842.      * @throws \PhpOffice\PhpWord\Exception\Exception
  843.      */
  844.     public function insertImage($name, $srcFilename, $width = null, $height = null, $mimeType = null, $filename = null)
  845.     {
  846.         if (($width === null) || ($width === null) || ($mimeType === null)) {
  847.             $imageinfo = getimagesize($srcFilename);
  848.             if (!empty($imageinfo)) {
  849.                 if ($width === null) {
  850.                     $width = Converter::pixelToCm($imageinfo[0]);
  851.                 }
  852.                 if ($height === null) {
  853.                     $height = Converter::pixelToCm($imageinfo[1]);
  854.                 }
  855.                 if ($mimeType === null) {
  856.                     $mimeType = $imageinfo['mime'];
  857.                 }
  858.             }
  859.         }
  860.         if ($filename === null) {
  861.             $filename = basename($srcFilename);
  862.         }
  863.         $width = Converter::cssToEmu($width);
  864.         $height = Converter::cssToEmu($height);
  865.  
  866.         $name = preg_replace('/^(?:\\$?{?img:)(.*)\\}?/', '$1', $name);
  867.  
  868.         $mediaPath = $this->addImageToArchive($srcFilename, $mimeType);
  869.  
  870.         foreach ($this->tempDocumentHeaders as $index => $header) {
  871.             $tempHeaderRelationships = array_key_exists($index, $this->tempDocumentHeadersRelationships) ? $this->tempDocumentHeadersRelationships[$index] : null;
  872.             $imageInsertResult = $this->insertImageForPart($header, $tempHeaderRelationships, $name, $mediaPath, $width, $height, $filename);
  873.             if ($imageInsertResult) {
  874.                 list($this->tempDocumentHeaders[$index], $this->tempDocumentHeadersRelationships[$index]) = $imageInsertResult;
  875.             }
  876.         }
  877.  
  878.         $imageInsertResult = $this->insertImageForPart($this->tempDocumentMainPart, $this->tempDocumentRelationships, $name, $mediaPath, $width, $height, $filename);
  879.         if ($imageInsertResult) {
  880.             list($this->tempDocumentMainPart, $this->tempDocumentRelationships) = $imageInsertResult;
  881.         }
  882.  
  883.         foreach ($this->tempDocumentFooters as $index => $footer) {
  884.             $tempFooterRelationships = array_key_exists($index, $this->tempDocumentFootersRelationships) ? $this->tempDocumentFootersRelationships[$index] : null;
  885.             $imageInsertResult = $this->insertImageForPart($footer, $tempFooterRelationships, $name, $mediaPath, $width, $height, $filename);
  886.             if ($imageInsertResult) {
  887.                 list($this->tempDocumentFooters[$index], $this->tempDocumentFootersRelationships[$index]) = $imageInsertResult;
  888.             }
  889.         }
  890.  
  891.         return $this;
  892.     }
  893.  
  894.     /**
  895.      * Delete a block of text.
  896.      *
  897.      * @param string $blockname
  898.      *
  899.      * @return true on block found and deleted, false on block not found.
  900.      */
  901.     public function deleteBlock($blockname)
  902.     {
  903.         return $this->replaceBlock($blockname, '', false);
  904.     }
  905.  
  906.     /**
  907.      * Delete a segment of text.
  908.      *
  909.      * @param string $needle
  910.      * @param string $xmltag
  911.      * @param string $docpart
  912.      *
  913.      * @return true on segment found and deleted, false on segment not found.
  914.      */
  915.     public function deleteSegment($needle, $xmltag, $docpart = 'MainPart')
  916.     {
  917.         return $this->replaceSegment($needle, $xmltag, '', $docpart, false);
  918.     }
  919.  
  920.     /**
  921.      * Saves the result document.
  922.      *
  923.      * @return string
  924.      *
  925.      * @throws \PhpOffice\PhpWord\Exception\Exception
  926.      */
  927.     public function save()
  928.     {
  929.         foreach ($this->tempDocumentHeaders as $index => $xml) {
  930.             $this->zipClass->addFromString($this->getHeaderName($index), $xml);
  931.         }
  932.         foreach ($this->tempDocumentHeadersRelationships as $index => $xml) {
  933.             $this->zipClass->addFromString($this->getHeaderRelsName($index), $xml);
  934.         }
  935.  
  936.         $this->zipClass->addFromString($this->getMainPartName(), $this->tempDocumentMainPart);
  937.         $this->zipClass->addFromString($this->getMainPartRelsName(), $this->tempDocumentRelationships);
  938.  
  939.         foreach ($this->tempDocumentFooters as $index => $xml) {
  940.             $this->zipClass->addFromString($this->getFooterName($index), $xml);
  941.         }
  942.         foreach ($this->tempDocumentFootersRelationships as $index => $xml) {
  943.             $this->zipClass->addFromString($this->getFooterRelsName($index), $xml);
  944.         }
  945.  
  946.         $this->zipClass->addFromString($this->getContentTypesPartName(), $this->tempDocumentContentTypes);
  947.  
  948.         // Close zip file
  949.         if (false === $this->zipClass->close()) {
  950.             throw new Exception('Could not close zip file.');
  951.         }
  952.  
  953.         return $this->tempDocumentFilename;
  954.     }
  955.  
  956.     /**
  957.      * Saves the result document to the user defined file.
  958.      *
  959.      * @since 0.8.0
  960.      *
  961.      * @param string $fileName
  962.      *
  963.      * @return void
  964.      */
  965.     public function saveAs($fileName)
  966.     {
  967.         $tempFileName = $this->save();
  968.  
  969.         if (file_exists($fileName)) {
  970.             unlink($fileName);
  971.         }
  972.  
  973.         /*
  974.          * Note: we do not use `rename` function here, because it looses file ownership data on Windows platform.
  975.          * As a result, user cannot open the file directly getting "Access denied" message.
  976.          *
  977.          * @see https://github.com/PHPOffice/PHPWord/issues/532
  978.          */
  979.         copy($tempFileName, $fileName);
  980.         unlink($tempFileName);
  981.     }
  982.  
  983.     /**
  984.      * Finds parts of broken macros and sticks them together.
  985.      * Macros, while being edited, could be implicitly broken by some of the word processors.
  986.      *
  987.      * @param string $documentPart The document part in XML representation.
  988.      *
  989.      * @return string
  990.      */
  991.     protected function fixBrokenMacros($documentPart)
  992.     {
  993.         $fixedDocumentPart = $documentPart;
  994.  
  995.         $fixedDocumentPart = preg_replace_callback(
  996.             '|\$(?:<[^{]*)?\{[^}]*\}|U',
  997.             function ($match) {
  998.                 return strip_tags($match[0]);
  999.             },
  1000.             $fixedDocumentPart
  1001.         );
  1002.  
  1003.         return $fixedDocumentPart;
  1004.     }
  1005.  
  1006.     /**
  1007.      * Find and replace macros in the given XML section.
  1008.      *
  1009.      * @param mixed   $search
  1010.      * @param mixed   $replace
  1011.      * @param string  $documentPartXML
  1012.      * @param integer $limit
  1013.      *
  1014.      * @return string
  1015.      */
  1016.     protected function setValueForPart($search, $replace, $documentPartXML, $limit)
  1017.     {
  1018.         // Shift-Enter
  1019.         if (is_array($replace)) {
  1020.             foreach ($replace as &$item) {
  1021.                 $item = preg_replace('~\R~u', '</w:t><w:br/><w:t>', $item);
  1022.             }
  1023.         } else {
  1024.             $replace = preg_replace('~\R~u', '</w:t><w:br/><w:t>', $replace);
  1025.         }
  1026.  
  1027.         // Note: we can't use the same function for both cases here, because of performance considerations.
  1028.         if (self::MAXIMUM_REPLACEMENTS_DEFAULT === $limit) {
  1029.             return str_replace($search, $replace, $documentPartXML);
  1030.         } else {
  1031.             $regExpEscaper = new RegExp();
  1032.             return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML, $limit);
  1033.         }
  1034.     }
  1035.  
  1036.     /**
  1037.      * Find all variables in $documentPartXML.
  1038.      *
  1039.      * @param string $documentPartXML
  1040.      *
  1041.      * @return string[]
  1042.      */
  1043.     protected function getVariablesForPart($documentPartXML)
  1044.     {
  1045.         preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
  1046.  
  1047.         return $matches[1];
  1048.     }
  1049.  
  1050.     /**
  1051.      * Get the name of the header file for $index.
  1052.      *
  1053.      * @param integer $index
  1054.      *
  1055.      * @return string
  1056.      */
  1057.     protected function getHeaderName($index)
  1058.     {
  1059.         return sprintf('word/header%d.xml', $index);
  1060.     }
  1061.  
  1062.     /**
  1063.      * Get the name of the header relationships file for $index
  1064.      *
  1065.      * @param  integer $index
  1066.      * @return string
  1067.      */
  1068.     protected function getHeaderRelsName($index)
  1069.     {
  1070.         return sprintf('word/_rels/header%d.xml.rels', $index);
  1071.     }
  1072.  
  1073.     /**
  1074.      * @return string
  1075.      */
  1076.     protected function getMainPartName()
  1077.     {
  1078.         return 'word/document.xml';
  1079.     }
  1080.  
  1081.     /**
  1082.      * Get the name of the relationships file for the main document
  1083.      *
  1084.      * @return string
  1085.      */
  1086.     protected function getMainPartRelsName()
  1087.     {
  1088.         return 'word/_rels/document.xml.rels';
  1089.     }
  1090.  
  1091.     /**
  1092.      * Get the name of the footer file for $index.
  1093.      *
  1094.      * @param integer $index
  1095.      *
  1096.      * @return string
  1097.      */
  1098.     protected function getFooterName($index)
  1099.     {
  1100.         return sprintf('word/footer%d.xml', $index);
  1101.     }
  1102.  
  1103.     /**
  1104.      * Get the name of the footer relationships file for $index
  1105.      *
  1106.      * @param integer $index
  1107.      *
  1108.      * @return string
  1109.      */
  1110.     protected function getFooterRelsName($index)
  1111.     {
  1112.         return sprintf('word/_rels/footer%d.xml.rels', $index);
  1113.     }
  1114.  
  1115.     /**
  1116.      * Get the name of the content types file
  1117.      *
  1118.      * @return string
  1119.      */
  1120.     protected function getContentTypesPartName()
  1121.     {
  1122.         return '[Content_Types].xml';
  1123.     }
  1124.  
  1125.     /**
  1126.      * Get the name of the Media Image file for $index $extension
  1127.      *
  1128.      * @param integer $index
  1129.      * @param string  $extension
  1130.      *
  1131.      * @return string
  1132.      */
  1133.     protected function getMediaImageName($index, $extension)
  1134.     {
  1135.         return sprintf('word/media/image%d.%s', $index, $extension);
  1136.     }
  1137.  
  1138.     /**
  1139.      * Find the start position of the nearest $tag before $offset.
  1140.      *
  1141.      * @param string  $tag
  1142.      * @param integer $offset
  1143.      * @param boolean $throwexception
  1144.      *
  1145.      * @return integer
  1146.      *
  1147.      * @throws \PhpOffice\PhpWord\Exception\Exception
  1148.      */
  1149.     protected function findTagLeft($tag, $offset = 0, $throwexception = false)
  1150.     {
  1151.         $tagStart = strrpos(
  1152.             $this->tempDocumentMainPart,
  1153.             substr($tag, 0, -1) . ' ',
  1154.             ((strlen($this->tempDocumentMainPart) - $offset) * -1)
  1155.         );
  1156.  
  1157.         if (!$tagStart) {
  1158.             $tagStart = strrpos(
  1159.                 $this->tempDocumentMainPart,
  1160.                 $tag,
  1161.                 ((strlen($this->tempDocumentMainPart) - $offset) * -1)
  1162.             );
  1163.         }
  1164.         if (!$tagStart) {
  1165.             if ($throwexception) {
  1166.                 throw new Exception('Can not find the start position of the item to clone.');
  1167.             } else {
  1168.                 return 0;
  1169.             }
  1170.         }
  1171.  
  1172.         return $tagStart;
  1173.     }
  1174.  
  1175.     /**
  1176.      * Find the end position of the nearest $tag after $offset.
  1177.      *
  1178.      * @param string $tag
  1179.      * @param integer $offset
  1180.      *
  1181.      * @return integer
  1182.      */
  1183.     protected function findTagRight($tag, $offset = 0)
  1184.     {
  1185.         $pos = strpos($this->tempDocumentMainPart, $tag, $offset);
  1186.         if ($pos !== false) {
  1187.             return $pos + strlen($tag);
  1188.         } else {
  1189.             return 0;
  1190.         }
  1191.     }
  1192.  
  1193.     /**
  1194.      * Get a slice of a string.
  1195.      *
  1196.      * @param integer $startPosition
  1197.      * @param integer $endPosition
  1198.      *
  1199.      * @return string
  1200.      */
  1201.     protected function getSlice($startPosition, $endPosition = 0)
  1202.     {
  1203.         if (!$endPosition) {
  1204.             $endPosition = strlen($this->tempDocumentMainPart);
  1205.         }
  1206.  
  1207.         return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition));
  1208.     }
  1209.  
  1210.     /**
  1211.      * Add an image from file to the document archive (in word/media)
  1212.      *
  1213.      * @param $strFilename Path of file to add to achive
  1214.      * @param $mimeType    Mime type of the image
  1215.      *
  1216.      * @return string Internal path reference for media file (media/imageX.yyy)
  1217.      *
  1218.      * @throws \PhpOffice\PhpWord\Exception\Exception
  1219.      */
  1220.     protected function addImageToArchive($strFilename, $mimeType = null)
  1221.     {
  1222.         $basename = basename($strFilename);
  1223.         if (strpos($basename, '.') !== false) {
  1224.             $extension = strtolower(substr($basename, strpos($basename, '.') + 1));
  1225.         } else {
  1226.             $extension = preg_replace('#^[^/]*/(\\w+)(?:\W.*)?#', '$1', strtolower($mimeType));
  1227.         }
  1228.         // special case where MIMEtype doesn't match usual file extension
  1229.         if ($extension === 'jpeg') {
  1230.             $extension = 'jpg';
  1231.         }
  1232.         $counter = 1;
  1233.         // find the next lowest file number
  1234.         while ($this->zipClass->locateName(
  1235.             $mediaFileName = $this->getMediaImageName($counter, $extension)
  1236.         ) !== false ) {
  1237.             $counter++;
  1238.         }
  1239.         if (!$this->zipClass->addFile($strFilename, $mediaFileName)) {
  1240.             throw new Exception('Could not add image to archive');
  1241.         }
  1242.         // now make sure this extension/MIMEtype is referenced
  1243.         $regex = sprintf(
  1244.             '/<(?:\\w+:)?Default(?:\\s*(?:Extension\\s*=\\s*"(%s)"|ContentType\\s*=\\s*"(%s)")){2}/i',
  1245.             preg_quote($extension, '/'),
  1246.             preg_quote($mimeType, '/')
  1247.         );
  1248.         $matches = null;
  1249.         if (preg_match($regex, $this->tempDocumentContentTypes, $matches)) {
  1250.             if ($matches[2] !== $mimeType) {
  1251.                 // requires an Override
  1252.                 $this->addContentTypeOverride($mediaFileName, $mimeType);
  1253.             }
  1254.         } else {
  1255.             // add a default content type
  1256.             $fragment = sprintf(static::CONTENTTYPE_DEFAULT_TEMPLATE, '$1', $extension, $mimeType);
  1257.             $this->tempDocumentContentTypes = preg_replace(
  1258.                 '/(?=<(\\w+:)?Override\b)/i',
  1259.                 $fragment,
  1260.                 $this->tempDocumentContentTypes,
  1261.                 1
  1262.             );
  1263.         }
  1264.         // references to the media must not include the path prefix word/
  1265.         return substr($mediaFileName, 5);
  1266.     }
  1267.  
  1268.     /**
  1269.      * Add a content-type override for an included file
  1270.      *
  1271.      * @param string $fileName Full internal name/path of file
  1272.      * @param string $mimeType Content-type for file
  1273.      *
  1274.      * @return \PhpOffice\PhpWord\Template
  1275.      */
  1276.     protected function addContentTypeOverride($fileName, $mimeType)
  1277.     {
  1278.         $fragment = sprintf(static::CONTENTTYPE_OVERRIDE_TEMPLATE, '$1', $fileName, $mimeType);
  1279.         $this->tempDocumentContentTypes = preg_replace(
  1280.             '#(?=</(\\w+:)?Types>\\s*$)#i',
  1281.             $fragment,
  1282.             $this->tempDocumentContentTypes
  1283.         );
  1284.         return $this;
  1285.     }
  1286.  
  1287.     /**
  1288.      * Add a relationship to an image to a document part relationships XML
  1289.      *
  1290.      * Note that if the given relationship ID already exists then the XML is
  1291.      * returned unchanged, even if a the existing relationship is to a different
  1292.      * media file.
  1293.      *
  1294.      * @param string $tempPartRelationships Relationships XML
  1295.      * @param string $mediaFileName         Internal path reference for media file
  1296.      * @param string $relId                 ID for this relationship
  1297.      *
  1298.      * @return string
  1299.      */
  1300.     protected function addImageRelationship($tempPartRelationships, $mediaFileName, $relId)
  1301.     {
  1302.         if (!$tempPartRelationships) {
  1303.             $tempPartRelationships = static::RELATIONSHIPS_FILE_TEMPLATE;
  1304.         }
  1305.         if (!preg_match('/\bId\s*=\\\s*"([^"]+)"/i', $tempPartRelationships)) {
  1306.             $relXML = sprintf(static::RELATIONSHIP_TEMPLATE, '$1', $relId, $mediaFileName);
  1307.             $tempPartRelationships = preg_replace(
  1308.                 '#(?=</(\\w+:)?Relationships>\\s*$)#i',
  1309.                 $relXML,
  1310.                 $tempPartRelationships
  1311.             );
  1312.         }
  1313.         return $tempPartRelationships;
  1314.     }
  1315.  
  1316.     /**
  1317.      * Get the relationship ID for the given part and media file
  1318.      *
  1319.      * If this relationship already exists then the existing ID is returned,
  1320.      * otherwise the next ID in the sequence rIdX is returned (this relationship
  1321.      * does howver not yet actually exist!)
  1322.      *
  1323.      * @param string $tempPartRelationships Relationships XML
  1324.      * @param string $mediaFileName         Internal path reference for media file
  1325.      *
  1326.      * @return string
  1327.      */
  1328.     protected function getPartRelationshipId($tempPartRelationships, $mediaFileName)
  1329.     {
  1330.         if (!$tempPartRelationships) {
  1331.             // not yet any relationships file
  1332.             $relId = 'rId1';
  1333.         } else {
  1334.             $matches = null;
  1335.             $regex = sprintf(
  1336.                 '/<(?:\\w+:)?Relationship\\s+Id\\s*=\\s*"([^"]+)"[^>]+Target\\s*=\\s*"%1$s"/i',
  1337.                 preg_quote($mediaFileName, '/')
  1338.             );
  1339.             if (preg_match($regex, $tempPartRelationships, $matches)) {
  1340.                 // relationship already exists: grab ID of it
  1341.                 $relId = $matches[1];
  1342.             } else {
  1343.                 // work out next relationship number and use it
  1344.                 $relIds = null;
  1345.                 $matches = preg_match_all('/Id\\s*=\\s*"([^"]+)"/i', $tempPartRelationships, $relIds);
  1346.                 if ($matches) {
  1347.                     $lastId = 0;
  1348.                     foreach ($relIds[1] as $existingRelId) {
  1349.                         if (preg_match('/^rId\\d+/i', $existingRelId)) {
  1350.                             $lastId = max($lastId, intval(substr($existingRelId, 3), 10));
  1351.                         }
  1352.                     }
  1353.                     $relId = sprintf('rId%d', ++$lastId);
  1354.                 } else {
  1355.                     // I guess the relationships file was empty...
  1356.                     $relId = 'rId1';
  1357.                 }
  1358.             }
  1359.         }
  1360.         return $relId;
  1361.     }
  1362.  
  1363.     /**
  1364.      * Insert an image into placeholders in a document part
  1365.      *
  1366.      * If a placeholder was found and the image inserted then an array with
  1367.      * the changed $documentPartXML at index 0 and $partRelationshipsXML at
  1368.      * index 1. If no changes were made then false is returned.
  1369.      *
  1370.      * @param string $tempDocumentPart      XML of document part
  1371.      * @param string $tempPartRelationships XML of document part relationships
  1372.      * @param string $name                  Image placeholder name (${img:$name})
  1373.      * @param string $mediaFileName         Internal path reference for media
  1374.      *                                      file
  1375.      * @param number $width                 Width of image in EMU
  1376.      * @param number $height                Height of image in EMU
  1377.      * @param string $filename              Name of file as it should be inserted
  1378.      *                                      in document
  1379.      *
  1380.      * @return string[]|false
  1381.      */
  1382.     protected function insertImageForPart(
  1383.         $tempDocumentPart,
  1384.         $tempPartRelationships,
  1385.         $name,
  1386.         $mediaFileName,
  1387.         $width,
  1388.         $height,
  1389.         $filename
  1390.     ) {
  1391.         $relId = $this->getPartRelationshipId($tempPartRelationships, $mediaFileName);
  1392.         // hacks for no class scope in callback function in PHP5.3
  1393.         $class = __CLASS__;
  1394.         $count = 0;
  1395.         $graphicIdMatches = preg_match_all(
  1396.             '/(?<=<wp:docPr id=")[^"]+/u',
  1397.             $tempDocumentPart,
  1398.             $graphicIds,
  1399.             PREG_PATTERN_ORDER
  1400.         );
  1401.         if ($graphicIdMatches) {
  1402.             $nextGraphicId = max($graphicIds[0]) + 1;
  1403.         } else {
  1404.             $nextGraphicId = 0;
  1405.         }
  1406.         $tempDocumentPart = preg_replace_callback(
  1407.             '/(<w:t(?:>|\s[^>]*>))?\\$((?:<[^>]+>)*)\\{([^\\}]+)\\}(<\\/w:t>)?/u',
  1408.             function ($match) use ($relId, $name, $class, $width, $height, $filename, &$nextGraphicId) {
  1409.                 $variable = explode(':', strip_tags($match[3]));
  1410.                 if ((count($variable) > 1) && ($variable[0] == 'img') && ($variable[1] == $name)) {
  1411.                     // we just gotta hope this random element Id will be unique!
  1412.                     $graphicId = $nextGraphicId++;
  1413.                     if (count($variable) > 3) {
  1414.                         $myWidth = Converter::cssToEmu($variable[2]);
  1415.                         $myHeight = Converter::cssToEmu($variable[3]);
  1416.                     } else {
  1417.                         $myWidth = $width;
  1418.                         $myHeight = $height;
  1419.                     }
  1420.                     $block = sprintf(
  1421.                         $class::IMAGE_TEMPLATE,
  1422.                         $myWidth,
  1423.                         $myHeight,
  1424.                         $graphicId,
  1425.                         sprintf('Graphic %d', $graphicId),
  1426.                         $filename,
  1427.                         $relId
  1428.                     );
  1429.                     // sort out opening and closing text block tags if we've not removed both
  1430.                     if (!($match[1] && $match[4])) {
  1431.                         if (!$match[1]) {
  1432.                             /*
  1433.                              * we remove a closing text tag but not an opening one,
  1434.                              * so need to close the text block before the image
  1435.                              */
  1436.                             $block = '</w:t>' . $block;
  1437.                         }
  1438.                         if (!$match[4]) {
  1439.                             /*
  1440.                              * we remove an opening text tag but not a closing one,
  1441.                              * so need to start a new text block after the image
  1442.                              */
  1443.                             $block .= '<w:t>';
  1444.                         }
  1445.                     }
  1446.                     return $block;
  1447.                 } else {
  1448.                     return $match[0];
  1449.                 }
  1450.             },
  1451.             $tempDocumentPart,
  1452.             -1,
  1453.             $count
  1454.         );
  1455.         // only need to add relationship if image actually added to this part
  1456.         if ($count > 0) {
  1457.             $tempPartRelationships = $this->addImageRelationship($tempPartRelationships, $mediaFileName, $relId);
  1458.             return array($tempDocumentPart, $tempPartRelationships);
  1459.         } else {
  1460.             return false;
  1461.         }
  1462.     }
  1463. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement