2014-08-16 15:21:58 +04:00
< ? php
2021-04-16 15:06:37 -05:00
2014-08-16 15:21:58 +04:00
/**
* This file is part of PHPWord - A pure PHP library for reading and writing
* word processing documents .
*
* PHPWord is free software distributed under the terms of the GNU Lesser
* General Public License version 3 as published by the Free Software Foundation .
*
* For the full copyright and license information , please read the LICENSE
* file that was distributed with this source code . For the full list of
* contributors , visit https :// github . com / PHPOffice / PHPWord / contributors .
*
2017-11-04 22:44:12 +01:00
* @ see https :// github . com / PHPOffice / PHPWord
2022-09-16 11:45:45 +02:00
*
2014-08-16 15:21:58 +04:00
* @ license http :// www . gnu . org / licenses / lgpl . txt LGPL version 3
*/
namespace PhpOffice\PhpWord ;
2022-09-16 11:45:45 +02:00
use DOMDocument ;
2021-04-16 15:06:37 -05:00
use PhpOffice\PhpWord\Escaper\RegExp ;
2021-04-16 15:19:55 -05:00
use PhpOffice\PhpWord\Escaper\Xml ;
use PhpOffice\PhpWord\Exception\CopyFileException ;
use PhpOffice\PhpWord\Exception\CreateTemporaryFileException ;
use PhpOffice\PhpWord\Exception\Exception ;
use PhpOffice\PhpWord\Shared\Text ;
use PhpOffice\PhpWord\Shared\XMLWriter ;
use PhpOffice\PhpWord\Shared\ZipArchive ;
2022-09-16 11:45:45 +02:00
use XSLTProcessor ;
2014-08-16 15:21:58 +04:00
class TemplateProcessor
{
2015-08-30 18:03:31 +04:00
const MAXIMUM_REPLACEMENTS_DEFAULT = - 1 ;
2014-08-16 15:21:58 +04:00
/**
* ZipArchive object .
*
* @ var mixed
*/
2015-04-14 22:47:41 +03:00
protected $zipClass ;
2014-08-16 15:21:58 +04:00
/**
2017-11-04 22:44:12 +01:00
* @ var string Temporary document filename ( with path )
2014-08-16 15:21:58 +04:00
*/
2015-04-15 00:12:37 +03:00
protected $tempDocumentFilename ;
2014-08-16 15:21:58 +04:00
/**
2022-09-16 11:45:45 +02:00
* Content of main document part ( in XML format ) of the temporary document .
2014-08-16 15:21:58 +04:00
*
* @ var string
*/
2015-04-15 00:12:37 +03:00
protected $tempDocumentMainPart ;
2014-08-16 15:21:58 +04:00
2019-02-04 23:59:37 +01:00
/**
2022-09-16 11:45:45 +02:00
* Content of settings part ( in XML format ) of the temporary document .
2019-02-04 23:59:37 +01:00
*
* @ var string
*/
protected $tempDocumentSettingsPart ;
2014-08-16 15:21:58 +04:00
/**
2022-09-16 11:45:45 +02:00
* Content of headers ( in XML format ) of the temporary document .
2014-08-16 15:21:58 +04:00
*
* @ var string []
*/
2022-09-16 11:45:45 +02:00
protected $tempDocumentHeaders = [];
2014-08-16 15:21:58 +04:00
/**
2022-09-16 11:45:45 +02:00
* Content of footers ( in XML format ) of the temporary document .
2014-08-16 15:21:58 +04:00
*
* @ var string []
*/
2022-09-16 11:45:45 +02:00
protected $tempDocumentFooters = [];
2014-08-16 15:21:58 +04:00
2018-12-26 15:35:21 +02:00
/**
* Document relations ( in XML format ) of the temporary document .
*
* @ var string []
*/
2022-09-16 11:45:45 +02:00
protected $tempDocumentRelations = [];
2018-12-26 15:35:21 +02:00
/**
* Document content types ( in XML format ) of the temporary document .
*
* @ var string
*/
protected $tempDocumentContentTypes = '' ;
/**
2022-09-16 11:45:45 +02:00
* new inserted images list .
2018-12-26 15:35:21 +02:00
*
* @ var string []
*/
2022-09-16 11:45:45 +02:00
protected $tempDocumentNewImages = [];
2018-12-26 15:35:21 +02:00
2014-08-16 15:21:58 +04:00
/**
2017-11-04 22:44:12 +01:00
* @ since 0.12 . 0 Throws CreateTemporaryFileException and CopyFileException instead of Exception
2014-08-16 15:21:58 +04:00
*
2017-11-04 22:44:12 +01:00
* @ param string $documentTemplate The fully qualified template filename
2014-08-16 15:21:58 +04:00
*/
public function __construct ( $documentTemplate )
{
// Temporary document filename initialization
2015-04-15 00:12:37 +03:00
$this -> tempDocumentFilename = tempnam ( Settings :: getTempDir (), 'PhpWord' );
if ( false === $this -> tempDocumentFilename ) {
2018-12-29 22:03:01 +01:00
throw new CreateTemporaryFileException (); // @codeCoverageIgnore
2014-08-16 15:21:58 +04:00
}
// Template file cloning
2015-04-15 00:12:37 +03:00
if ( false === copy ( $documentTemplate , $this -> tempDocumentFilename )) {
2018-12-29 22:03:01 +01:00
throw new CopyFileException ( $documentTemplate , $this -> tempDocumentFilename ); // @codeCoverageIgnore
2014-08-16 15:21:58 +04:00
}
// Temporary document content extraction
$this -> zipClass = new ZipArchive ();
2015-04-15 00:12:37 +03:00
$this -> zipClass -> open ( $this -> tempDocumentFilename );
2014-08-16 15:21:58 +04:00
$index = 1 ;
2015-04-11 21:25:40 +04:00
while ( false !== $this -> zipClass -> locateName ( $this -> getHeaderName ( $index ))) {
2018-12-26 15:35:21 +02:00
$this -> tempDocumentHeaders [ $index ] = $this -> readPartWithRels ( $this -> getHeaderName ( $index ));
2022-09-16 11:45:45 +02:00
++ $index ;
2014-08-16 15:21:58 +04:00
}
$index = 1 ;
2015-04-11 21:25:40 +04:00
while ( false !== $this -> zipClass -> locateName ( $this -> getFooterName ( $index ))) {
2018-12-26 15:35:21 +02:00
$this -> tempDocumentFooters [ $index ] = $this -> readPartWithRels ( $this -> getFooterName ( $index ));
2022-09-16 11:45:45 +02:00
++ $index ;
2014-08-16 15:21:58 +04:00
}
2018-12-26 15:35:21 +02:00
$this -> tempDocumentMainPart = $this -> readPartWithRels ( $this -> getMainPartName ());
2019-02-04 23:59:37 +01:00
$this -> tempDocumentSettingsPart = $this -> readPartWithRels ( $this -> getSettingsPartName ());
2018-12-26 15:35:21 +02:00
$this -> tempDocumentContentTypes = $this -> zipClass -> getFromName ( $this -> getDocumentContentTypesName ());
}
2018-12-29 22:03:01 +01:00
/**
2022-09-16 11:45:45 +02:00
* Expose zip class .
2018-12-29 22:03:01 +01:00
*
* To replace an image : $templateProcessor -> zip () -> AddFromString ( " word/media/image1.jpg " , file_get_contents ( $file )); < br >
* To read a file : $templateProcessor -> zip () -> getFromName ( " word/media/image1.jpg " );
*
* @ return \PhpOffice\PhpWord\Shared\ZipArchive
*/
public function zip ()
{
return $this -> zipClass ;
}
2018-12-26 15:35:21 +02:00
/**
* @ 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 ));
2016-07-30 16:02:23 +04:00
}
/**
* @ param string $xml
2022-09-16 11:45:45 +02:00
* @ param XSLTProcessor $xsltProcessor
2017-11-04 22:44:12 +01:00
*
* @ return string
2016-07-30 16:02:23 +04:00
*/
protected function transformSingleXml ( $xml , $xsltProcessor )
{
2020-10-17 15:48:39 +02:00
if ( \PHP_VERSION_ID < 80000 ) {
$orignalLibEntityLoader = libxml_disable_entity_loader ( true );
}
2022-09-16 11:45:45 +02:00
$domDocument = new DOMDocument ();
2016-07-30 16:02:23 +04:00
if ( false === $domDocument -> loadXML ( $xml )) {
throw new Exception ( 'Could not load the given XML document.' );
}
$transformedXml = $xsltProcessor -> transformToXml ( $domDocument );
if ( false === $transformedXml ) {
throw new Exception ( 'Could not transform the given XML document.' );
}
2020-10-17 15:48:39 +02:00
if ( \PHP_VERSION_ID < 80000 ) {
libxml_disable_entity_loader ( $orignalLibEntityLoader );
}
2016-07-30 16:02:23 +04:00
return $transformedXml ;
}
/**
* @ param mixed $xml
2022-09-16 11:45:45 +02:00
* @ param XSLTProcessor $xsltProcessor
2016-07-30 16:02:23 +04:00
*
* @ return mixed
*/
protected function transformXml ( $xml , $xsltProcessor )
{
if ( is_array ( $xml )) {
foreach ( $xml as & $item ) {
$item = $this -> transformSingleXml ( $item , $xsltProcessor );
}
2017-10-19 19:02:26 +03:00
unset ( $item );
2016-07-30 16:02:23 +04:00
} else {
$xml = $this -> transformSingleXml ( $xml , $xsltProcessor );
}
return $xml ;
2014-08-16 15:21:58 +04:00
}
/**
* Applies XSL style sheet to template ' s parts .
2017-09-17 21:38:00 +02:00
*
2016-06-04 20:06:37 +04:00
* Note : since the method doesn ' t make any guess on logic of the provided XSL style sheet ,
* make sure that output is correctly escaped . Otherwise you may get broken document .
2014-08-16 15:21:58 +04:00
*
2022-09-16 11:45:45 +02:00
* @ param DOMDocument $xslDomDocument
2014-08-16 15:21:58 +04:00
* @ param array $xslOptions
2016-07-30 16:02:23 +04:00
* @ param string $xslOptionsUri
2014-08-16 15:21:58 +04:00
*/
2022-09-16 11:45:45 +02:00
public function applyXslStyleSheet ( $xslDomDocument , $xslOptions = [], $xslOptionsUri = '' ) : void
2014-08-16 15:21:58 +04:00
{
2022-09-16 11:45:45 +02:00
$xsltProcessor = new XSLTProcessor ();
2014-08-16 15:21:58 +04:00
2016-07-30 16:02:23 +04:00
$xsltProcessor -> importStylesheet ( $xslDomDocument );
if ( false === $xsltProcessor -> setParameter ( $xslOptionsUri , $xslOptions )) {
2014-08-16 15:21:58 +04:00
throw new Exception ( 'Could not set values for the given XSL style sheet parameters.' );
}
2016-07-30 16:02:23 +04:00
$this -> tempDocumentHeaders = $this -> transformXml ( $this -> tempDocumentHeaders , $xsltProcessor );
$this -> tempDocumentMainPart = $this -> transformXml ( $this -> tempDocumentMainPart , $xsltProcessor );
$this -> tempDocumentFooters = $this -> transformXml ( $this -> tempDocumentFooters , $xsltProcessor );
2014-08-16 15:21:58 +04:00
}
/**
2016-04-23 19:49:10 +04:00
* @ param string $macro
2015-08-30 18:03:31 +04:00
*
2016-04-23 19:49:10 +04:00
* @ return string
2014-08-16 15:21:58 +04:00
*/
2016-04-23 19:49:10 +04:00
protected static function ensureMacroCompleted ( $macro )
2014-08-16 15:21:58 +04:00
{
2015-09-10 23:33:02 +10:00
if ( substr ( $macro , 0 , 2 ) !== '${' && substr ( $macro , - 1 ) !== '}' ) {
$macro = '${' . $macro . '}' ;
}
2015-09-10 23:50:23 +10:00
2016-04-23 19:49:10 +04:00
return $macro ;
}
2015-09-10 23:50:23 +10:00
2016-04-23 19:49:10 +04:00
/**
* @ param string $subject
*
* @ return string
*/
protected static function ensureUtf8Encoded ( $subject )
{
2022-09-16 11:45:45 +02:00
if ( ! Text :: isUTF8 ( $subject ) && null !== $subject ) {
2016-04-23 19:49:10 +04:00
$subject = utf8_encode ( $subject );
2014-08-16 15:21:58 +04:00
}
2022-09-16 11:45:45 +02:00
return ( null !== $subject ) ? $subject : '' ;
2016-04-23 19:49:10 +04:00
}
2014-08-16 15:21:58 +04:00
2019-01-31 01:26:19 +01:00
/**
* @ param string $search
* @ param \PhpOffice\PhpWord\Element\AbstractElement $complexType
*/
2022-09-16 11:45:45 +02:00
public function setComplexValue ( $search , Element\AbstractElement $complexType ) : void
2019-01-31 01:26:19 +01:00
{
$elementName = substr ( get_class ( $complexType ), strrpos ( get_class ( $complexType ), '\\' ) + 1 );
$objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName ;
$xmlWriter = new XMLWriter ();
/** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */
$elementWriter = new $objectClass ( $xmlWriter , $complexType , true );
$elementWriter -> write ();
$where = $this -> findContainingXmlBlockForMacro ( $search , 'w:r' );
2020-09-22 16:28:52 +02:00
2020-12-30 14:02:29 +01:00
if ( $where === false ) {
2021-02-05 21:25:21 +01:00
return ;
2020-09-22 16:28:52 +02:00
}
2019-01-31 01:26:19 +01:00
$block = $this -> getSlice ( $where [ 'start' ], $where [ 'end' ]);
$textParts = $this -> splitTextIntoTexts ( $block );
$this -> replaceXmlBlock ( $search , $textParts , 'w:r' );
$search = static :: ensureMacroCompleted ( $search );
$this -> replaceXmlBlock ( $search , $xmlWriter -> getData (), 'w:r' );
}
/**
* @ param string $search
* @ param \PhpOffice\PhpWord\Element\AbstractElement $complexType
*/
2022-09-16 11:45:45 +02:00
public function setComplexBlock ( $search , Element\AbstractElement $complexType ) : void
2019-01-31 01:26:19 +01:00
{
$elementName = substr ( get_class ( $complexType ), strrpos ( get_class ( $complexType ), '\\' ) + 1 );
$objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName ;
$xmlWriter = new XMLWriter ();
/** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */
$elementWriter = new $objectClass ( $xmlWriter , $complexType , false );
$elementWriter -> write ();
$this -> replaceXmlBlock ( $search , $xmlWriter -> getData (), 'w:p' );
}
2016-04-23 19:49:10 +04:00
/**
* @ param mixed $search
* @ param mixed $replace
2017-11-04 22:44:12 +01:00
* @ param int $limit
2016-04-23 19:49:10 +04:00
*/
2022-09-16 11:45:45 +02:00
public function setValue ( $search , $replace , $limit = self :: MAXIMUM_REPLACEMENTS_DEFAULT ) : void
2016-04-23 19:49:10 +04:00
{
if ( is_array ( $search )) {
foreach ( $search as & $item ) {
2017-10-19 18:51:57 +03:00
$item = static :: ensureMacroCompleted ( $item );
2016-04-23 19:49:10 +04:00
}
2017-10-19 19:02:26 +03:00
unset ( $item );
2016-04-23 19:49:10 +04:00
} else {
2017-10-19 18:51:57 +03:00
$search = static :: ensureMacroCompleted ( $search );
2014-08-16 15:21:58 +04:00
}
2016-04-23 19:49:10 +04:00
if ( is_array ( $replace )) {
foreach ( $replace as & $item ) {
2018-07-16 19:49:30 +02:00
$item = static :: ensureUtf8Encoded ( $item );
2016-04-23 19:49:10 +04:00
}
2017-10-19 19:02:26 +03:00
unset ( $item );
2016-04-23 19:49:10 +04:00
} else {
2018-07-16 19:49:30 +02:00
$replace = static :: ensureUtf8Encoded ( $replace );
2016-04-23 19:49:10 +04:00
}
2016-06-04 20:06:37 +04:00
if ( Settings :: isOutputEscapingEnabled ()) {
$xmlEscaper = new Xml ();
$replace = $xmlEscaper -> escape ( $replace );
}
2016-04-23 19:49:10 +04:00
$this -> tempDocumentHeaders = $this -> setValueForPart ( $search , $replace , $this -> tempDocumentHeaders , $limit );
$this -> tempDocumentMainPart = $this -> setValueForPart ( $search , $replace , $this -> tempDocumentMainPart , $limit );
$this -> tempDocumentFooters = $this -> setValueForPart ( $search , $replace , $this -> tempDocumentFooters , $limit );
2014-08-16 15:21:58 +04:00
}
2015-11-27 14:30:22 +01:00
/**
* Set values from a one - dimensional array of " variable => value " - pairs .
*/
2022-09-16 11:45:45 +02:00
public function setValues ( array $values ) : void
2015-11-27 14:30:22 +01:00
{
foreach ( $values as $macro => $replace ) {
$this -> setValue ( $macro , $replace );
}
}
2021-02-06 10:40:52 +01:00
/**
* @ param string $search
*/
2022-09-16 11:45:45 +02:00
public function setChart ( $search , Element\AbstractElement $chart ) : void
2021-02-06 10:40:52 +01:00
{
$elementName = substr ( get_class ( $chart ), strrpos ( get_class ( $chart ), '\\' ) + 1 );
$objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName ;
// Get the next relation id
2021-02-06 21:32:30 +01:00
$rId = $this -> getNextRelationsIndex ( $this -> getMainPartName ());
2021-02-06 10:40:52 +01:00
$chart -> setRelationId ( $rId );
// Define the chart filename
$filename = " charts/chart { $rId } .xml " ;
2021-02-06 21:32:30 +01:00
2021-02-06 10:40:52 +01:00
// Get the part writer
$writerPart = new \PhpOffice\PhpWord\Writer\Word2007\Part\Chart ();
$writerPart -> setElement ( $chart );
// ContentTypes.xml
$this -> zipClass -> addFromString ( " word/ { $filename } " , $writerPart -> write ());
// add chart to content type
$xmlRelationsType = " <Override PartName= \" /word/ { $filename } \" ContentType= \" application/vnd.openxmlformats-officedocument.drawingml.chart+xml \" /> " ;
$this -> tempDocumentContentTypes = str_replace ( '</Types>' , $xmlRelationsType , $this -> tempDocumentContentTypes ) . '</Types>' ;
// Add the chart to relations
$xmlChartRelation = " <Relationship Id= \" rId { $rId } \" Type= \" http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart \" Target= \" charts/chart { $rId } .xml \" /> " ;
$this -> tempDocumentRelations [ $this -> getMainPartName ()] = str_replace ( '</Relationships>' , $xmlChartRelation , $this -> tempDocumentRelations [ $this -> getMainPartName ()]) . '</Relationships>' ;
// Write the chart
$xmlWriter = new XMLWriter ();
$elementWriter = new $objectClass ( $xmlWriter , $chart , true );
$elementWriter -> write ();
// Place it in the template
$this -> replaceXmlBlock ( $search , '<w:p>' . $xmlWriter -> getData () . '</w:p>' , 'w:p' );
}
2018-12-26 15:35:21 +02:00
private function getImageArgs ( $varNameWithArgs )
{
$varElements = explode ( ':' , $varNameWithArgs );
array_shift ( $varElements ); // first element is name of variable => remove it
2022-09-16 11:45:45 +02:00
$varInlineArgs = [];
2018-12-26 15:35:21 +02:00
// 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
2022-09-16 11:45:45 +02:00
[ $argName , $argValue ] = explode ( '=' , $varArg , 2 );
2018-12-26 15:35:21 +02:00
$argName = strtolower ( $argName );
if ( $argName == 'size' ) {
2022-09-16 11:45:45 +02:00
[ $varInlineArgs [ 'width' ], $varInlineArgs [ 'height' ]] = explode ( 'x' , $argValue , 2 );
2018-12-26 15:35:21 +02:00
} 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
2022-09-16 11:45:45 +02:00
[ $varInlineArgs [ 'width' ], $varInlineArgs [ 'height' ]] = explode ( 'x' , $varArg , 2 );
2018-12-26 15:35:21 +02:00
} else { // :60:40:f
switch ( $argIdx ) {
case 0 :
$varInlineArgs [ 'width' ] = $varArg ;
2022-09-16 11:45:45 +02:00
2018-12-26 15:35:21 +02:00
break ;
case 1 :
$varInlineArgs [ 'height' ] = $varArg ;
2022-09-16 11:45:45 +02:00
2018-12-26 15:35:21 +02:00
break ;
case 2 :
$varInlineArgs [ 'ratio' ] = $varArg ;
2022-09-16 11:45:45 +02:00
2018-12-26 15:35:21 +02:00
break ;
}
}
}
return $varInlineArgs ;
}
private function chooseImageDimension ( $baseValue , $inlineValue , $defaultValue )
{
$value = $baseValue ;
2022-09-16 11:45:45 +02:00
if ( null === $value && isset ( $inlineValue )) {
2018-12-26 15:35:21 +02:00
$value = $inlineValue ;
}
2022-09-16 11:45:45 +02:00
if ( ! preg_match ( '/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i' , $value ? ? '' )) {
2018-12-26 15:35:21 +02:00
$value = null ;
}
2022-09-16 11:45:45 +02:00
if ( null === $value ) {
2018-12-26 15:35:21 +02:00
$value = $defaultValue ;
}
if ( is_numeric ( $value )) {
$value .= 'px' ;
}
return $value ;
}
2022-09-16 11:45:45 +02:00
private function fixImageWidthHeightRatio ( & $width , & $height , $actualWidth , $actualHeight ) : void
2018-12-26 15:35:21 +02:00
{
$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 ;
2022-09-16 11:45:45 +02:00
$matches = [];
preg_match ( '/\\d([a-z%]+)$/' , $height , $matches );
2018-12-26 15:35:21 +02:00
$width = $widthFloat . $matches [ 1 ];
} elseif ( $height === '' ) { // defined height is empty
$widthFloat = ( float ) $width ;
$heightFloat = $widthFloat / $imageRatio ;
2022-09-16 11:45:45 +02:00
$matches = [];
preg_match ( '/\\d([a-z%]+)$/' , $width , $matches );
2018-12-26 15:35:21 +02:00
$height = $heightFloat . $matches [ 1 ];
} else { // we have defined size, but we need also check it aspect ratio
2022-09-16 11:45:45 +02:00
$widthMatches = [];
preg_match ( '/\\d([a-z%]+)$/' , $width , $widthMatches );
$heightMatches = [];
preg_match ( '/\\d([a-z%]+)$/' , $height , $heightMatches );
2018-12-26 15:35:21 +02:00
// 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 ;
2019-09-09 13:49:16 +02:00
// a closure can be passed as replacement value which after resolving, can contain the replacement info for the image
// use case: only when a image if found, the replacement tags can be generated
if ( is_callable ( $replaceImage )) {
$replaceImage = $replaceImage ();
}
2018-12-26 15:35:21 +02:00
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 ;
}
2022-09-16 11:45:45 +02:00
$width = $this -> chooseImageDimension ( $width , $varInlineArgs [ 'width' ] ? ? null , 115 );
$height = $this -> chooseImageDimension ( $height , $varInlineArgs [ 'height' ] ? ? null , 70 );
2018-12-26 15:35:21 +02:00
$imageData = @ getimagesize ( $imgPath );
if ( ! is_array ( $imageData )) {
throw new Exception ( sprintf ( 'Invalid image: %s' , $imgPath ));
}
2022-09-16 11:45:45 +02:00
[ $actualWidth , $actualHeight , $imageType ] = $imageData ;
2018-12-26 15:35:21 +02:00
// fix aspect ratio (by default)
2022-09-16 11:45:45 +02:00
if ( null === $ratio && isset ( $varInlineArgs [ 'ratio' ])) {
2018-12-26 15:35:21 +02:00
$ratio = $varInlineArgs [ 'ratio' ];
}
2022-09-16 11:45:45 +02:00
if ( null === $ratio || ! in_array ( strtolower ( $ratio ), [ '' , '-' , 'f' , 'false' ])) {
2018-12-26 15:35:21 +02:00
$this -> fixImageWidthHeightRatio ( $width , $height , $actualWidth , $actualHeight );
}
2022-09-16 11:45:45 +02:00
$imageAttrs = [
'src' => $imgPath ,
'mime' => image_type_to_mime_type ( $imageType ),
'width' => $width ,
2018-12-26 15:35:21 +02:00
'height' => $height ,
2022-09-16 11:45:45 +02:00
];
2018-12-26 15:35:21 +02:00
return $imageAttrs ;
}
2022-09-16 11:45:45 +02:00
private function addImageToRelations ( $partFileName , $rid , $imgPath , $imageMimeType ) : void
2018-12-26 15:35:21 +02:00
{
// 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"/>' ;
2022-09-16 11:45:45 +02:00
$extTransform = [
2018-12-26 15:35:21 +02:00
'image/jpeg' => 'jpeg' ,
2022-09-16 11:45:45 +02:00
'image/png' => 'png' ,
'image/bmp' => 'bmp' ,
'image/gif' => 'gif' ,
];
2018-12-26 15:35:21 +02:00
// 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
2022-09-16 11:45:45 +02:00
$xmlImageType = str_replace ([ '{IMG}' , '{EXT}' ], [ $imgName , $imgExt ], $typeTpl );
2018-12-26 15:35:21 +02:00
$this -> tempDocumentContentTypes = str_replace ( '</Types>' , $xmlImageType , $this -> tempDocumentContentTypes ) . '</Types>' ;
}
2022-09-16 11:45:45 +02:00
$xmlImageRelation = str_replace ([ '{RID}' , '{IMG}' ], [ $rid , $imgName ], $relationTpl );
2018-12-26 15:35:21 +02:00
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
*/
2022-09-16 11:45:45 +02:00
public function setImageValue ( $search , $replace , $limit = self :: MAXIMUM_REPLACEMENTS_DEFAULT ) : void
2018-12-26 15:35:21 +02:00
{
// prepare $search_replace
if ( ! is_array ( $search )) {
2022-09-16 11:45:45 +02:00
$search = [ $search ];
2018-12-26 15:35:21 +02:00
}
2022-09-16 11:45:45 +02:00
$replacesList = [];
2018-12-26 15:35:21 +02:00
if ( ! is_array ( $replace ) || isset ( $replace [ 'path' ])) {
$replacesList [] = $replace ;
} else {
$replacesList = array_values ( $replace );
}
2022-09-16 11:45:45 +02:00
$searchReplace = [];
2018-12-26 15:35:21 +02:00
foreach ( $search as $searchIdx => $searchString ) {
2022-09-16 11:45:45 +02:00
$searchReplace [ $searchString ] = $replacesList [ $searchIdx ] ? ? $replacesList [ 0 ];
2018-12-26 15:35:21 +02:00
}
// collect document parts
2022-09-16 11:45:45 +02:00
$searchParts = [
2021-04-16 15:06:37 -05:00
$this -> getMainPartName () => & $this -> tempDocumentMainPart ,
2022-09-16 11:45:45 +02:00
];
2018-12-26 15:35:21 +02:00
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)
2020-12-23 16:00:06 +05:00
$imgTpl = '<w:pict><v:shape type="#_x0000_t75" style="width:{WIDTH};height:{HEIGHT}" stroked="f"><v:imagedata r:id="{RID}" o:title=""/></v:shape></w:pict>' ;
2018-12-26 15:35:21 +02:00
2020-11-11 17:18:52 +01:00
$i = 0 ;
2018-12-26 15:35:21 +02:00
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' ]);
2022-09-16 11:45:45 +02:00
$xmlImage = str_replace ([ '{RID}' , '{WIDTH}' , '{HEIGHT}' ], [ $rid , $preparedImageAttrs [ 'width' ], $preparedImageAttrs [ 'height' ]], $imgTpl );
2018-12-26 15:35:21 +02:00
// replace variable
2018-12-26 20:07:53 +01:00
$varNameWithArgsFixed = static :: ensureMacroCompleted ( $varNameWithArgs );
2022-09-16 11:45:45 +02:00
$matches = [];
2018-12-26 15:35:21 +02:00
if ( preg_match ( '/(<[^<]+>)([^<]*)(' . preg_quote ( $varNameWithArgsFixed ) . ')([^>]*)(<[^>]+>)/Uu' , $partContent , $matches )) {
$wholeTag = $matches [ 0 ];
array_shift ( $matches );
2022-09-16 11:45:45 +02:00
[ $openTag , $prefix , , $postfix , $closeTag ] = $matches ;
2018-12-26 15:35:21 +02:00
$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 );
}
2021-02-07 15:06:01 +01:00
if ( ++ $i >= $limit ) {
2020-11-11 17:18:52 +01:00
break ;
}
2018-12-26 15:35:21 +02:00
}
}
}
}
2018-02-05 17:45:24 +01:00
/**
* Returns count of all variables in template .
*
* @ return array
*/
public function getVariableCount ()
{
$variables = $this -> getVariablesForPart ( $this -> tempDocumentMainPart );
foreach ( $this -> tempDocumentHeaders as $headerXML ) {
$variables = array_merge (
$variables ,
$this -> getVariablesForPart ( $headerXML )
);
}
foreach ( $this -> tempDocumentFooters as $footerXML ) {
$variables = array_merge (
$variables ,
$this -> getVariablesForPart ( $footerXML )
);
}
return array_count_values ( $variables );
}
2014-08-16 15:21:58 +04:00
/**
* Returns array of all variables in template .
*
* @ return string []
*/
public function getVariables ()
{
2018-02-05 17:49:23 +01:00
return array_keys ( $this -> getVariableCount ());
2014-08-16 15:21:58 +04:00
}
/**
* Clone a table row in a template document .
*
* @ param string $search
2017-11-04 22:44:12 +01:00
* @ param int $numberOfClones
2014-08-16 15:21:58 +04:00
*/
2022-09-16 11:45:45 +02:00
public function cloneRow ( $search , $numberOfClones ) : void
2014-08-16 15:21:58 +04:00
{
2017-10-19 17:33:46 +03:00
$search = static :: ensureMacroCompleted ( $search );
2014-08-16 15:21:58 +04:00
2015-04-15 00:12:37 +03:00
$tagPos = strpos ( $this -> tempDocumentMainPart , $search );
2014-08-16 15:21:58 +04:00
if ( ! $tagPos ) {
2017-11-04 22:44:12 +01:00
throw new Exception ( 'Can not clone row, template variable not found or variable contains markup.' );
2014-08-16 15:21:58 +04:00
}
$rowStart = $this -> findRowStart ( $tagPos );
$rowEnd = $this -> findRowEnd ( $tagPos );
$xmlRow = $this -> getSlice ( $rowStart , $rowEnd );
// Check if there's a cell spanning multiple rows.
if ( preg_match ( '#<w:vMerge w:val="restart"/>#' , $xmlRow )) {
// $extraRowStart = $rowEnd;
$extraRowEnd = $rowEnd ;
while ( true ) {
$extraRowStart = $this -> findRowStart ( $extraRowEnd + 1 );
$extraRowEnd = $this -> findRowEnd ( $extraRowEnd + 1 );
// If extraRowEnd is lower then 7, there was no next row found.
if ( $extraRowEnd < 7 ) {
break ;
}
// If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row.
$tmpXmlRow = $this -> getSlice ( $extraRowStart , $extraRowEnd );
2021-04-16 23:38:14 +02:00
if ( ! preg_match ( '#<w:vMerge/>#' , $tmpXmlRow ) &&
2021-04-16 15:06:37 -05:00
! preg_match ( '#<w:vMerge w:val="continue"\s*/>#' , $tmpXmlRow )
) {
2014-08-16 15:21:58 +04:00
break ;
}
// This row was a spanned row, update $rowEnd and search for the next row.
$rowEnd = $extraRowEnd ;
}
$xmlRow = $this -> getSlice ( $rowStart , $rowEnd );
}
$result = $this -> getSlice ( 0 , $rowStart );
2022-09-16 11:45:45 +02:00
$result .= implode ( '' , $this -> indexClonedVariables ( $numberOfClones , $xmlRow ));
2014-08-16 15:21:58 +04:00
$result .= $this -> getSlice ( $rowEnd );
2015-04-15 00:12:37 +03:00
$this -> tempDocumentMainPart = $result ;
2014-08-16 15:21:58 +04:00
}
2021-06-23 12:07:45 +02:00
/**
* Delete a table row in a template document .
*/
2022-11-06 19:01:51 +01:00
public function deleteRow ( string $search ) : void
2021-06-23 12:07:45 +02:00
{
if ( '${' !== substr ( $search , 0 , 2 ) && '}' !== substr ( $search , - 1 )) {
$search = '${' . $search . '}' ;
}
$tagPos = strpos ( $this -> tempDocumentMainPart , $search );
if ( ! $tagPos ) {
2022-11-07 22:38:19 +01:00
throw new Exception ( sprintf ( 'Can not delete row %s, template variable not found or variable contains markup.' , $search ));
2021-06-23 12:07:45 +02:00
}
$tableStart = $this -> findTableStart ( $tagPos );
$tableEnd = $this -> findTableEnd ( $tagPos );
$xmlTable = $this -> getSlice ( $tableStart , $tableEnd );
if ( substr_count ( $xmlTable , '<w:tr' ) === 1 ) {
$this -> tempDocumentMainPart = $this -> getSlice ( 0 , $tableStart ) . $this -> getSlice ( $tableEnd );
return ;
}
$rowStart = $this -> findRowStart ( $tagPos );
$rowEnd = $this -> findRowEnd ( $tagPos );
$xmlRow = $this -> getSlice ( $rowStart , $rowEnd );
$this -> tempDocumentMainPart = $this -> getSlice ( 0 , $rowStart ) . $this -> getSlice ( $rowEnd );
// Check if there's a cell spanning multiple rows.
if ( preg_match ( '#<w:vMerge w:val="restart"/>#' , $xmlRow )) {
$extraRowStart = $rowStart ;
while ( true ) {
$extraRowStart = $this -> findRowStart ( $extraRowStart + 1 );
$extraRowEnd = $this -> findRowEnd ( $extraRowStart + 1 );
// If extraRowEnd is lower then 7, there was no next row found.
if ( $extraRowEnd < 7 ) {
break ;
}
// If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row.
$tmpXmlRow = $this -> getSlice ( $extraRowStart , $extraRowEnd );
if ( ! preg_match ( '#<w:vMerge/>#' , $tmpXmlRow ) &&
! preg_match ( '#<w:vMerge w:val="continue" />#' , $tmpXmlRow )
) {
break ;
}
$tableStart = $this -> findTableStart ( $extraRowEnd + 1 );
$tableEnd = $this -> findTableEnd ( $extraRowEnd + 1 );
$xmlTable = $this -> getSlice ( $tableStart , $tableEnd );
if ( substr_count ( $xmlTable , '<w:tr' ) === 1 ) {
$this -> tempDocumentMainPart = $this -> getSlice ( 0 , $tableStart ) . $this -> getSlice ( $tableEnd );
return ;
}
$this -> tempDocumentMainPart = $this -> getSlice ( 0 , $extraRowStart ) . $this -> getSlice ( $extraRowEnd );
}
}
}
2015-11-27 14:30:22 +01:00
/**
2019-01-03 18:57:00 +01:00
* Clones a table row and populates it ' s values from a two - dimensional array in a template document .
2015-11-27 14:30:22 +01:00
*
* @ param string $search
2019-01-03 18:57:00 +01:00
* @ param array $values
2015-11-27 14:30:22 +01:00
*/
2022-09-16 11:45:45 +02:00
public function cloneRowAndSetValues ( $search , $values ) : void
2015-11-27 14:30:22 +01:00
{
2019-01-03 18:57:00 +01:00
$this -> cloneRow ( $search , count ( $values ));
2015-11-27 14:30:22 +01:00
2019-01-03 18:57:00 +01:00
foreach ( $values as $rowKey => $rowData ) {
$rowNumber = $rowKey + 1 ;
foreach ( $rowData as $macro => $replace ) {
$this -> setValue ( $macro . '#' . $rowNumber , $replace );
2015-11-27 14:30:22 +01:00
}
}
}
2014-08-16 15:21:58 +04:00
/**
* Clone a block .
*
* @ param string $blockname
2018-12-27 01:44:37 +01:00
* @ param int $clones How many time the block should be cloned
2017-11-04 22:44:12 +01:00
* @ param bool $replace
2018-12-27 01:44:37 +01:00
* @ param bool $indexVariables If true , any variables inside the block will be indexed ( postfixed with #1, #2, ...)
2018-12-27 22:13:48 +01:00
* @ param array $variableReplacements Array containing replacements for macros found inside the block to clone
2015-08-30 18:03:31 +04:00
*
2022-09-16 11:45:45 +02:00
* @ return null | string
2014-08-16 15:21:58 +04:00
*/
2018-12-27 22:13:48 +01:00
public function cloneBlock ( $blockname , $clones = 1 , $replace = true , $indexVariables = false , $variableReplacements = null )
2014-08-16 15:21:58 +04:00
{
$xmlBlock = null ;
2022-09-16 11:45:45 +02:00
$matches = [];
2014-08-16 15:21:58 +04:00
preg_match (
2020-01-21 16:42:19 +07:00
'/(.*((?s)<w:p\b(?:(?!<w:p\b).)*?\${' . $blockname . '}<\/w:.*?p>))(.*)((?s)<w:p\b(?:(?!<w:p\b).)[^$]*?\${\/' . $blockname . '}<\/w:.*?p>)/is' ,
2015-04-15 00:12:37 +03:00
$this -> tempDocumentMainPart ,
2014-08-16 15:21:58 +04:00
$matches
);
if ( isset ( $matches [ 3 ])) {
$xmlBlock = $matches [ 3 ];
2018-12-27 01:44:37 +01:00
if ( $indexVariables ) {
$cloned = $this -> indexClonedVariables ( $clones , $xmlBlock );
2018-12-27 22:13:48 +01:00
} elseif ( $variableReplacements !== null && is_array ( $variableReplacements )) {
$cloned = $this -> replaceClonedVariables ( $variableReplacements , $xmlBlock );
2018-12-27 01:44:37 +01:00
} else {
2022-09-16 11:45:45 +02:00
$cloned = [];
for ( $i = 1 ; $i <= $clones ; ++ $i ) {
2018-12-27 01:44:37 +01:00
$cloned [] = $xmlBlock ;
}
}
2014-08-16 15:21:58 +04:00
if ( $replace ) {
2015-04-15 00:12:37 +03:00
$this -> tempDocumentMainPart = str_replace (
2014-08-16 15:21:58 +04:00
$matches [ 2 ] . $matches [ 3 ] . $matches [ 4 ],
implode ( '' , $cloned ),
2015-04-15 00:12:37 +03:00
$this -> tempDocumentMainPart
2014-08-16 15:21:58 +04:00
);
}
}
return $xmlBlock ;
}
/**
* Replace a block .
*
* @ param string $blockname
* @ param string $replacement
*/
2022-09-16 11:45:45 +02:00
public function replaceBlock ( $blockname , $replacement ) : void
2014-08-16 15:21:58 +04:00
{
2022-09-16 11:45:45 +02:00
$matches = [];
2014-08-16 15:21:58 +04:00
preg_match (
'/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is' ,
2015-04-15 00:12:37 +03:00
$this -> tempDocumentMainPart ,
2014-08-16 15:21:58 +04:00
$matches
);
if ( isset ( $matches [ 3 ])) {
2015-04-15 00:12:37 +03:00
$this -> tempDocumentMainPart = str_replace (
2014-08-16 15:21:58 +04:00
$matches [ 2 ] . $matches [ 3 ] . $matches [ 4 ],
$replacement ,
2015-04-15 00:12:37 +03:00
$this -> tempDocumentMainPart
2014-08-16 15:21:58 +04:00
);
}
}
/**
* Delete a block of text .
*
* @ param string $blockname
*/
2022-09-16 11:45:45 +02:00
public function deleteBlock ( $blockname ) : void
2014-08-16 15:21:58 +04:00
{
$this -> replaceBlock ( $blockname , '' );
}
2019-02-04 23:59:37 +01:00
/**
2022-09-16 11:45:45 +02:00
* Automatically Recalculate Fields on Open .
2019-02-04 23:59:37 +01:00
*
* @ param bool $update
*/
2022-09-16 11:45:45 +02:00
public function setUpdateFields ( $update = true ) : void
2019-02-04 23:59:37 +01:00
{
$string = $update ? 'true' : 'false' ;
2022-09-16 11:45:45 +02:00
$matches = [];
2019-02-04 23:59:37 +01:00
if ( preg_match ( '/<w:updateFields w:val=\"(true|false|1|0|on|off)\"\/>/' , $this -> tempDocumentSettingsPart , $matches )) {
$this -> tempDocumentSettingsPart = str_replace ( $matches [ 0 ], '<w:updateFields w:val="' . $string . '"/>' , $this -> tempDocumentSettingsPart );
} else {
$this -> tempDocumentSettingsPart = str_replace ( '</w:settings>' , '<w:updateFields w:val="' . $string . '"/></w:settings>' , $this -> tempDocumentSettingsPart );
}
}
2014-08-16 15:21:58 +04:00
/**
* Saves the result document .
*
2017-11-04 22:44:12 +01:00
* @ return string
2014-08-16 15:21:58 +04:00
*/
public function save ()
{
2016-07-30 16:02:23 +04:00
foreach ( $this -> tempDocumentHeaders as $index => $xml ) {
2018-12-26 15:35:21 +02:00
$this -> savePartWithRels ( $this -> getHeaderName ( $index ), $xml );
2014-08-16 15:21:58 +04:00
}
2018-12-26 15:35:21 +02:00
$this -> savePartWithRels ( $this -> getMainPartName (), $this -> tempDocumentMainPart );
2019-02-04 23:59:37 +01:00
$this -> savePartWithRels ( $this -> getSettingsPartName (), $this -> tempDocumentSettingsPart );
2014-08-16 15:21:58 +04:00
2016-07-30 16:02:23 +04:00
foreach ( $this -> tempDocumentFooters as $index => $xml ) {
2018-12-26 15:35:21 +02:00
$this -> savePartWithRels ( $this -> getFooterName ( $index ), $xml );
2014-08-16 15:21:58 +04:00
}
2018-12-26 15:35:21 +02:00
$this -> zipClass -> addFromString ( $this -> getDocumentContentTypesName (), $this -> tempDocumentContentTypes );
2014-08-16 15:21:58 +04:00
// Close zip file
if ( false === $this -> zipClass -> close ()) {
2018-12-29 22:03:01 +01:00
throw new Exception ( 'Could not close zip file.' ); // @codeCoverageIgnore
2014-08-16 15:21:58 +04:00
}
2015-04-15 00:12:37 +03:00
return $this -> tempDocumentFilename ;
2014-08-16 15:21:58 +04:00
}
2018-12-26 15:35:21 +02:00
/**
* @ param string $fileName
* @ param string $xml
*/
2022-09-16 11:45:45 +02:00
protected function savePartWithRels ( $fileName , $xml ) : void
2018-12-26 15:35:21 +02:00
{
$this -> zipClass -> addFromString ( $fileName , $xml );
if ( isset ( $this -> tempDocumentRelations [ $fileName ])) {
$relsFileName = $this -> getRelationsName ( $fileName );
$this -> zipClass -> addFromString ( $relsFileName , $this -> tempDocumentRelations [ $fileName ]);
}
}
2014-08-16 15:21:58 +04:00
/**
* Saves the result document to the user defined file .
*
* @ since 0.8 . 0
*
* @ param string $fileName
*/
2022-09-16 11:45:45 +02:00
public function saveAs ( $fileName ) : void
2014-08-16 15:21:58 +04:00
{
$tempFileName = $this -> save ();
if ( file_exists ( $fileName )) {
unlink ( $fileName );
}
2015-07-02 21:39:54 +03:00
/*
2018-01-26 18:31:35 +01:00
* Note : we do not use `rename` function here , because it loses file ownership data on Windows platform .
2015-07-02 21:46:29 +03:00
* As a result , user cannot open the file directly getting " Access denied " message .
2015-08-30 18:03:31 +04:00
*
2015-07-02 21:39:54 +03:00
* @ see https :// github . com / PHPOffice / PHPWord / issues / 532
*/
copy ( $tempFileName , $fileName );
unlink ( $tempFileName );
2014-08-16 15:21:58 +04:00
}
2015-04-11 21:25:40 +04:00
/**
* Finds parts of broken macros and sticks them together .
* Macros , while being edited , could be implicitly broken by some of the word processors .
*
2017-11-04 22:44:12 +01:00
* @ param string $documentPart The document part in XML representation
2015-04-11 21:25:40 +04:00
*
* @ return string
*/
2015-04-11 21:41:58 +04:00
protected function fixBrokenMacros ( $documentPart )
{
2018-12-08 00:22:04 +02:00
return preg_replace_callback (
'/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U' ,
2015-06-02 22:15:58 +03:00
function ( $match ) {
return strip_tags ( $match [ 0 ]);
},
2018-12-08 00:22:04 +02:00
$documentPart
2015-06-02 22:15:58 +03:00
);
2015-04-11 21:25:40 +04:00
}
2014-08-16 15:21:58 +04:00
/**
2015-08-30 18:03:31 +04:00
* Find and replace macros in the given XML section .
2014-08-16 15:21:58 +04:00
*
2016-04-23 19:49:10 +04:00
* @ param mixed $search
* @ param mixed $replace
2014-08-16 15:21:58 +04:00
* @ param string $documentPartXML
2017-11-04 22:44:12 +01:00
* @ param int $limit
2015-08-30 18:03:31 +04:00
*
2014-08-16 15:21:58 +04:00
* @ return string
*/
2016-04-23 19:49:10 +04:00
protected function setValueForPart ( $search , $replace , $documentPartXML , $limit )
2014-08-16 15:21:58 +04:00
{
2015-08-30 18:03:31 +04:00
// Note: we can't use the same function for both cases here, because of performance considerations.
if ( self :: MAXIMUM_REPLACEMENTS_DEFAULT === $limit ) {
return str_replace ( $search , $replace , $documentPartXML );
}
2017-11-04 22:44:12 +01:00
$regExpEscaper = new RegExp ();
return preg_replace ( $regExpEscaper -> escape ( $search ), $replace , $documentPartXML , $limit );
2014-08-16 15:21:58 +04:00
}
/**
* Find all variables in $documentPartXML .
*
* @ param string $documentPartXML
2015-08-30 18:03:31 +04:00
*
2014-08-16 15:21:58 +04:00
* @ return string []
*/
protected function getVariablesForPart ( $documentPartXML )
{
2022-09-16 11:45:45 +02:00
$matches = [];
2014-08-16 15:21:58 +04:00
preg_match_all ( '/\$\{(.*?)}/i' , $documentPartXML , $matches );
return $matches [ 1 ];
}
/**
2016-07-30 16:02:23 +04:00
* Get the name of the header file for $index .
2014-08-16 15:21:58 +04:00
*
2017-11-04 22:44:12 +01:00
* @ param int $index
2015-08-30 18:03:31 +04:00
*
2014-08-16 15:21:58 +04:00
* @ return string
*/
2016-07-30 16:02:23 +04:00
protected function getHeaderName ( $index )
2014-08-16 15:21:58 +04:00
{
2016-07-30 16:02:23 +04:00
return sprintf ( 'word/header%d.xml' , $index );
2014-08-16 15:21:58 +04:00
}
/**
2018-10-11 11:28:44 +02:00
* Usually , the name of main part document will be 'document.xml' . However , some . docx files ( possibly those from Office 365 , experienced also on documents from Word Online created from blank templates ) have file 'document22.xml' in their zip archive instead of 'document.xml' . This method searches content types file to correctly determine the file name .
*
2016-07-30 16:02:23 +04:00
* @ return string
*/
protected function getMainPartName ()
{
2018-10-11 08:55:38 +02:00
$contentTypes = $this -> zipClass -> getFromName ( '[Content_Types].xml' );
$pattern = '~PartName="\/(word\/document.*?\.xml)" ContentType="application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document\.main\+xml"~' ;
2022-09-16 11:45:45 +02:00
$matches = [];
2018-11-16 23:00:23 +01:00
preg_match ( $pattern , $contentTypes , $matches );
2018-11-16 23:33:38 +01:00
2018-11-20 22:40:54 +01:00
return array_key_exists ( 1 , $matches ) ? $matches [ 1 ] : 'word/document.xml' ;
2016-07-30 16:02:23 +04:00
}
2019-02-04 23:59:37 +01:00
/**
2022-09-16 11:45:45 +02:00
* The name of the file containing the Settings part .
2019-02-04 23:59:37 +01:00
*
* @ return string
*/
protected function getSettingsPartName ()
{
return 'word/settings.xml' ;
}
2016-07-30 16:02:23 +04:00
/**
* Get the name of the footer file for $index .
2014-08-16 15:21:58 +04:00
*
2017-11-04 22:44:12 +01:00
* @ param int $index
2015-08-30 18:03:31 +04:00
*
2014-08-16 15:21:58 +04:00
* @ return string
*/
2016-07-30 16:02:23 +04:00
protected function getFooterName ( $index )
2014-08-16 15:21:58 +04:00
{
2016-07-30 16:02:23 +04:00
return sprintf ( 'word/footer%d.xml' , $index );
2014-08-16 15:21:58 +04:00
}
2018-12-26 15:35:21 +02:00
/**
* 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 ])) {
2021-04-16 15:06:37 -05:00
$candidate = substr_count ( $this -> tempDocumentRelations [ $documentPartName ], '<Relationship' );
while ( strpos ( $this -> tempDocumentRelations [ $documentPartName ], 'Id="rId' . $candidate . '"' ) !== false ) {
2022-09-16 11:45:45 +02:00
++ $candidate ;
2021-04-16 15:06:37 -05:00
}
2021-04-16 15:19:55 -05:00
2021-04-16 15:06:37 -05:00
return $candidate ;
2018-12-26 15:35:21 +02:00
}
return 1 ;
}
/**
* @ return string
*/
protected function getDocumentContentTypesName ()
{
return '[Content_Types].xml' ;
}
2021-06-23 12:07:45 +02:00
/**
* Find the start position of the nearest table before $offset .
*/
2022-11-16 22:02:21 +01:00
private function findTableStart ( int $offset ) : int
2021-06-23 12:07:45 +02:00
{
2022-11-07 22:38:19 +01:00
$rowStart = strrpos (
$this -> tempDocumentMainPart ,
'<w:tbl ' ,
(( strlen ( $this -> tempDocumentMainPart ) - $offset ) * - 1 )
);
2021-06-23 12:07:45 +02:00
if ( ! $rowStart ) {
2022-11-07 22:38:19 +01:00
$rowStart = strrpos (
$this -> tempDocumentMainPart ,
'<w:tbl>' ,
(( strlen ( $this -> tempDocumentMainPart ) - $offset ) * - 1 )
);
2021-06-23 12:07:45 +02:00
}
if ( ! $rowStart ) {
throw new Exception ( 'Can not find the start position of the table.' );
}
return $rowStart ;
}
/**
2022-11-07 22:38:19 +01:00
* Find the end position of the nearest table row after $offset .
*/
2022-11-16 22:02:21 +01:00
private function findTableEnd ( int $offset ) : int
2021-06-23 12:07:45 +02:00
{
return strpos ( $this -> tempDocumentMainPart , '</w:tbl>' , $offset ) + 7 ;
}
2014-08-16 15:21:58 +04:00
/**
* Find the start position of the nearest table row before $offset .
*
2017-11-04 22:44:12 +01:00
* @ param int $offset
2015-08-30 18:03:31 +04:00
*
2017-11-04 22:44:12 +01:00
* @ return int
2014-08-16 15:21:58 +04:00
*/
2015-04-14 22:47:41 +03:00
protected function findRowStart ( $offset )
2014-08-16 15:21:58 +04:00
{
2015-04-15 00:12:37 +03:00
$rowStart = strrpos ( $this -> tempDocumentMainPart , '<w:tr ' , (( strlen ( $this -> tempDocumentMainPart ) - $offset ) * - 1 ));
2014-08-16 15:21:58 +04:00
if ( ! $rowStart ) {
2015-04-15 00:12:37 +03:00
$rowStart = strrpos ( $this -> tempDocumentMainPart , '<w:tr>' , (( strlen ( $this -> tempDocumentMainPart ) - $offset ) * - 1 ));
2014-08-16 15:21:58 +04:00
}
if ( ! $rowStart ) {
throw new Exception ( 'Can not find the start position of the row to clone.' );
}
return $rowStart ;
}
/**
* Find the end position of the nearest table row after $offset .
*
2017-11-04 22:44:12 +01:00
* @ param int $offset
2015-08-30 18:03:31 +04:00
*
2017-11-04 22:44:12 +01:00
* @ return int
2014-08-16 15:21:58 +04:00
*/
2015-04-14 22:47:41 +03:00
protected function findRowEnd ( $offset )
2014-08-16 15:21:58 +04:00
{
2015-04-15 00:12:37 +03:00
return strpos ( $this -> tempDocumentMainPart , '</w:tr>' , $offset ) + 7 ;
2014-08-16 15:21:58 +04:00
}
/**
* Get a slice of a string .
*
2017-11-04 22:44:12 +01:00
* @ param int $startPosition
* @ param int $endPosition
2015-08-30 18:03:31 +04:00
*
2014-08-16 15:21:58 +04:00
* @ return string
*/
2015-04-14 22:47:41 +03:00
protected function getSlice ( $startPosition , $endPosition = 0 )
2014-08-16 15:21:58 +04:00
{
if ( ! $endPosition ) {
2015-04-15 00:12:37 +03:00
$endPosition = strlen ( $this -> tempDocumentMainPart );
2014-08-16 15:21:58 +04:00
}
2015-04-15 00:12:37 +03:00
return substr ( $this -> tempDocumentMainPart , $startPosition , ( $endPosition - $startPosition ));
2014-08-16 15:21:58 +04:00
}
2016-06-15 14:46:07 -04:00
/**
2018-12-27 01:44:37 +01:00
* Replaces variable names in cloned
2022-09-16 11:45:45 +02:00
* rows / blocks with indexed names .
2016-06-15 14:46:07 -04:00
*
2018-12-27 01:44:37 +01:00
* @ param int $count
2016-06-15 14:46:07 -04:00
* @ param string $xmlBlock
*
* @ return string
*/
protected function indexClonedVariables ( $count , $xmlBlock )
{
2022-09-16 11:45:45 +02:00
$results = [];
for ( $i = 1 ; $i <= $count ; ++ $i ) {
2019-12-02 08:54:45 +01:00
$results [] = preg_replace ( '/\$\{([^:]*?)(:.*?)?\}/' , '\${\1#' . $i . '\2}' , $xmlBlock );
2016-06-15 14:46:07 -04:00
}
2018-12-27 01:44:37 +01:00
2016-06-15 14:46:07 -04:00
return $results ;
}
2018-12-27 22:13:48 +01:00
/**
2022-09-16 11:45:45 +02:00
* Raplaces variables with values from array , array keys are the variable names .
2018-12-27 22:13:48 +01:00
*
* @ param array $variableReplacements
* @ param string $xmlBlock
*
* @ return string []
*/
protected function replaceClonedVariables ( $variableReplacements , $xmlBlock )
{
2022-09-16 11:45:45 +02:00
$results = [];
2018-12-27 22:13:48 +01:00
foreach ( $variableReplacements as $replacementArray ) {
$localXmlBlock = $xmlBlock ;
foreach ( $replacementArray as $search => $replacement ) {
$localXmlBlock = $this -> setValueForPart ( self :: ensureMacroCompleted ( $search ), $replacement , $localXmlBlock , self :: MAXIMUM_REPLACEMENTS_DEFAULT );
}
$results [] = $localXmlBlock ;
}
return $results ;
}
2019-01-31 01:26:19 +01:00
/**
2022-09-16 11:45:45 +02:00
* Replace an XML block surrounding a macro with a new block .
2019-01-31 01:26:19 +01:00
*
* @ param string $macro Name of macro
* @ param string $block New block content
* @ param string $blockType XML tag type of block
2022-09-16 11:45:45 +02:00
*
2019-01-31 01:26:19 +01:00
* @ return \PhpOffice\PhpWord\TemplateProcessor Fluent interface
*/
2021-02-08 22:08:29 +01:00
public function replaceXmlBlock ( $macro , $block , $blockType = 'w:p' )
2019-01-31 01:26:19 +01:00
{
$where = $this -> findContainingXmlBlockForMacro ( $macro , $blockType );
2019-02-04 22:57:33 +01:00
if ( is_array ( $where )) {
2019-01-31 01:26:19 +01:00
$this -> tempDocumentMainPart = $this -> getSlice ( 0 , $where [ 'start' ]) . $block . $this -> getSlice ( $where [ 'end' ]);
}
return $this ;
}
/**
* Find start and end of XML block containing the given macro
2022-09-16 11:45:45 +02:00
* e . g . < w : p >... $ { macro } ...</ w : p >.
2019-01-31 01:26:19 +01:00
*
* Note that only the first instance of the macro will be found
*
* @ param string $macro Name of macro
* @ param string $blockType XML tag for block
2022-09-16 11:45:45 +02:00
*
2019-01-31 01:26:19 +01:00
* @ return bool | int [] FALSE if not found , otherwise array with start and end
*/
protected function findContainingXmlBlockForMacro ( $macro , $blockType = 'w:p' )
{
$macroPos = $this -> findMacro ( $macro );
2019-02-04 21:53:19 +01:00
if ( 0 > $macroPos ) {
2019-01-31 01:26:19 +01:00
return false ;
}
$start = $this -> findXmlBlockStart ( $macroPos , $blockType );
if ( 0 > $start ) {
return false ;
}
$end = $this -> findXmlBlockEnd ( $start , $blockType );
2019-02-04 21:53:19 +01:00
//if not found or if resulting string does not contain the macro we are searching for
if ( 0 > $end || strstr ( $this -> getSlice ( $start , $end ), $macro ) === false ) {
2019-01-31 01:26:19 +01:00
return false ;
}
2022-09-16 11:45:45 +02:00
return [ 'start' => $start , 'end' => $end ];
2019-01-31 01:26:19 +01:00
}
/**
2022-09-16 11:45:45 +02:00
* Find the position of ( the start of ) a macro .
2019-01-31 01:26:19 +01:00
*
* Returns - 1 if not found , otherwise position of opening $
*
* Note that only the first instance of the macro will be found
*
* @ param string $search Macro name
2019-02-04 22:57:33 +01:00
* @ param int $offset Offset from which to start searching
2022-09-16 11:45:45 +02:00
*
2019-01-31 01:26:19 +01:00
* @ return int - 1 if macro not found
*/
protected function findMacro ( $search , $offset = 0 )
{
$search = static :: ensureMacroCompleted ( $search );
$pos = strpos ( $this -> tempDocumentMainPart , $search , $offset );
return ( $pos === false ) ? - 1 : $pos ;
}
/**
2022-09-16 11:45:45 +02:00
* Find the start position of the nearest XML block start before $offset .
2019-01-31 01:26:19 +01:00
*
* @ param int $offset Search position
* @ param string $blockType XML Block tag
2022-09-16 11:45:45 +02:00
*
2019-01-31 01:26:19 +01:00
* @ return int - 1 if block start not found
*/
protected function findXmlBlockStart ( $offset , $blockType )
{
2019-02-04 21:53:19 +01:00
$reverseOffset = ( strlen ( $this -> tempDocumentMainPart ) - $offset ) * - 1 ;
2019-01-31 01:26:19 +01:00
// first try XML tag with attributes
2019-02-04 21:53:19 +01:00
$blockStart = strrpos ( $this -> tempDocumentMainPart , '<' . $blockType . ' ' , $reverseOffset );
2019-01-31 01:26:19 +01:00
// if not found, or if found but contains the XML tag without attribute
if ( false === $blockStart || strrpos ( $this -> getSlice ( $blockStart , $offset ), '<' . $blockType . '>' )) {
// also try XML tag without attributes
2019-02-04 21:53:19 +01:00
$blockStart = strrpos ( $this -> tempDocumentMainPart , '<' . $blockType . '>' , $reverseOffset );
2019-01-31 01:26:19 +01:00
}
return ( $blockStart === false ) ? - 1 : $blockStart ;
}
/**
2022-09-16 11:45:45 +02:00
* Find the nearest block end position after $offset .
2019-01-31 01:26:19 +01:00
*
* @ param int $offset Search position
* @ param string $blockType XML Block tag
2022-09-16 11:45:45 +02:00
*
2019-01-31 01:26:19 +01:00
* @ return int - 1 if block end not found
*/
protected function findXmlBlockEnd ( $offset , $blockType )
{
$blockEndStart = strpos ( $this -> tempDocumentMainPart , '</' . $blockType . '>' , $offset );
// return position of end of tag if found, otherwise -1
return ( $blockEndStart === false ) ? - 1 : $blockEndStart + 3 + strlen ( $blockType );
}
/**
2022-09-16 11:45:45 +02:00
* Splits a w : r / w : t into a list of w : r where each $ { macro } is in a separate w : r .
2019-01-31 01:26:19 +01:00
*
* @ param string $text
2022-09-16 11:45:45 +02:00
*
2019-01-31 01:26:19 +01:00
* @ return string
*/
protected function splitTextIntoTexts ( $text )
{
if ( ! $this -> textNeedsSplitting ( $text )) {
return $text ;
}
2022-09-16 11:45:45 +02:00
$matches = [];
2019-01-31 01:26:19 +01:00
if ( preg_match ( '/(<w:rPr.*<\/w:rPr>)/i' , $text , $matches )) {
$extractedStyle = $matches [ 0 ];
} else {
$extractedStyle = '' ;
}
$unformattedText = preg_replace ( '/>\s+</' , '><' , $text );
2022-09-16 11:45:45 +02:00
$result = str_replace ([ '${' , '}' ], [ '</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">${' , '}</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">' ], $unformattedText );
2019-01-31 01:26:19 +01:00
2022-09-16 11:45:45 +02:00
return str_replace ([ '<w:r>' . $extractedStyle . '<w:t xml:space="preserve"></w:t></w:r>' , '<w:r><w:t xml:space="preserve"></w:t></w:r>' , '<w:t>' ], [ '' , '' , '<w:t xml:space="preserve">' ], $result );
2019-01-31 01:26:19 +01:00
}
/**
2022-09-16 11:45:45 +02:00
* Returns true if string contains a macro that is not in it ' s own w : r .
2019-01-31 01:26:19 +01:00
*
* @ param string $text
2022-09-16 11:45:45 +02:00
*
2019-01-31 01:26:19 +01:00
* @ return bool
*/
protected function textNeedsSplitting ( $text )
{
return preg_match ( '/[^>]\${|}[^<]/i' , $text ) == 1 ;
}
2014-08-16 15:21:58 +04:00
}