Passed
Push — master ( e79061...af4049 )
by Bingo
03:12
created

DocxDocument::getDocumentContentTypesName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpDocxTemplate;
4
5
use DOMDocument;
6
use DOMElement;
7
use Exception;
8
use ZipArchive;
9
use RecursiveIteratorIterator;
10
use RecursiveDirectoryIterator;
11
use PhpDocxTemplate\Escaper\RegExp;
12
13
/**
14
 * Class DocxDocument
15
 *
16
 * @package PhpDocxTemplate
17
 */
18
class DocxDocument
19
{
20
    private $path;
21
    private $tmpDir;
22
    private $document;
23
    private $zipClass;
24
    private $tempDocumentMainPart;
25
    private $tempDocumentRelations = [];
26
    private $tempDocumentContentTypes = '';
27
    private $tempDocumentNewImages = [];
28
    private $skipFiles = [];
29
30
    /**
31
     * Construct an instance of Document
32
     *
33
     * @param string $path - path to the document
34
     *
35
     * @throws Exception
36
     */
37 11
    public function __construct(string $path)
38
    {
39 11
        if (file_exists($path)) {
40 11
            $this->path = $path;
41 11
            $this->tmpDir = sys_get_temp_dir() . "/" . uniqid("", true) . date("His");
42 11
            $this->zipClass = new ZipArchive();
43 11
            $this->extract();
44
        } else {
45
            throw new Exception("The template " . $path . " was not found!");
46
        }
47 11
    }
48
49
    /**
50
     * Extract (unzip) document contents
51
     */
52 11
    private function extract(): void
53
    {
54 11
        if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) {
55
            $this->rrmdir($this->tmpDir);
56
        }
57
58 11
        mkdir($this->tmpDir);
59
60 11
        $this->zipClass->open($this->path);
61 11
        $this->zipClass->extractTo($this->tmpDir);
62
63 11
        $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
64
65 11
        $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName());
66
67 11
        $this->document = file_get_contents($this->tmpDir . "/word/document.xml");
68 11
    }
69
70
    /**
71
     * Get document main part
72
     *
73
     * @return string
74
     */
75 1
    public function getDocumentMainPart(): string
76
    {
77 1
        return $this->tempDocumentMainPart;
78
    }
79
80
    /**
81
     * Get the name of main part document (method from PhpOffice\PhpWord)
82
     *
83
     * @return string
84
     */
85 11
    private function getMainPartName(): string
86
    {
87 11
        $contentTypes = $this->zipClass->getFromName('[Content_Types].xml');
88
89
        $pattern = '~PartName="\/(word\/document.*?\.xml)" ' .
90
                   'ContentType="application\/vnd\.openxmlformats-officedocument' .
91 11
                   '\.wordprocessingml\.document\.main\+xml"~';
92
93 11
        $matches = [];
94 11
        preg_match($pattern, $contentTypes, $matches);
95
96 11
        return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml';
97
    }
98
99
    /**
100
     * @return string
101
     */
102 11
    private function getDocumentContentTypesName(): string
103
    {
104 11
        return '[Content_Types].xml';
105
    }
106
107
    /**
108
     * Read document part (method from PhpOffice\PhpWord)
109
     *
110
     * @param string $fileName
111
     *
112
     * @return string
113
     */
114 11
    private function readPartWithRels(string $fileName): string
115
    {
116 11
        $relsFileName = $this->getRelationsName($fileName);
117 11
        $partRelations = $this->zipClass->getFromName($relsFileName);
118 11
        if ($partRelations !== false) {
119 11
            $this->tempDocumentRelations[$fileName] = $partRelations;
120
        }
121
122 11
        return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
123
    }
124
125
    /**
126
     * Get the name of the relations file for document part (method from PhpOffice\PhpWord)
127
     *
128
     * @param string $documentPartName
129
     *
130
     * @return string
131
     */
132 11
    private function getRelationsName(string $documentPartName): string
133
    {
134 11
        return 'word/_rels/' . pathinfo($documentPartName, PATHINFO_BASENAME) . '.rels';
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($documentPartNa...late\PATHINFO_BASENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

134
        return 'word/_rels/' . /** @scrutinizer ignore-type */ pathinfo($documentPartName, PATHINFO_BASENAME) . '.rels';
Loading history...
135
    }
136
137 1
    public function getNextRelationsIndex(string $documentPartName): int
138
    {
139 1
        if (isset($this->tempDocumentRelations[$documentPartName])) {
140 1
            $candidate = substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship');
141 1
            while (strpos($this->tempDocumentRelations[$documentPartName], 'Id="rId' . $candidate . '"') !== false) {
142
                $candidate++;
143
            }
144
145 1
            return $candidate;
146
        }
147
148 1
        return 1;
149
    }
150
151
    /**
152
     * Finds parts of broken macros and sticks them together (method from PhpOffice\PhpWord)
153
     *
154
     * @param string $documentPart
155
     *
156
     * @return string
157
     */
158 11
    private function fixBrokenMacros(string $documentPart): string
159
    {
160 11
        return preg_replace_callback(
161 11
            '/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U',
162
            function ($match) {
163
                return strip_tags($match[0]);
164 11
            },
165 11
            $documentPart
166
        );
167
    }
168
169
    /**
170
     * @param string $macro
171
     *
172
     * @return string
173
     */
174 1
    protected static function ensureMacroCompleted(string $macro): string
175
    {
176 1
        if (substr($macro, 0, 2) !== '{{' && substr($macro, -1) !== '}}') {
177 1
            $macro = '{{' . $macro . '}}';
178
        }
179 1
        return $macro;
180
    }
181
182
    /**
183
     * Get the name of the header file for $index.
184
     *
185
     * @param int $index
186
     *
187
     * @return string
188
     */
189
    private function getHeaderName(int $index): string
190
    {
191
        return sprintf('word/header%d.xml', $index);
192
    }
193
194
    /**
195
     * Get the name of the footer file for $index.
196
     *
197
     * @param int $index
198
     *
199
     * @return string
200
     */
201
    private function getFooterName(int $index): string
202
    {
203
        return sprintf('word/footer%d.xml', $index);
204
    }
205
206
    /**
207
     * Find all variables in $documentPartXML.
208
     *
209
     * @param string $documentPartXML
210
     *
211
     * @return string[]
212
     */
213 1
    private function getVariablesForPart(string $documentPartXML): array
214
    {
215 1
        $matches = array();
216 1
        preg_match_all('/\{\{(.*?)\}\}/i', $documentPartXML, $matches);
217 1
        return $matches[1];
218
    }
219
220 1
    private function getImageArgs(string $varNameWithArgs): array
221
    {
222 1
        $varElements = explode(':', $varNameWithArgs);
223 1
        array_shift($varElements); // first element is name of variable => remove it
224
225 1
        $varInlineArgs = array();
226
        // size format documentation: https://msdn.microsoft.com/en-us/library/documentformat.openxml.vml.shape%28v=office.14%29.aspx?f=255&MSPPError=-2147217396
227 1
        foreach ($varElements as $argIdx => $varArg) {
228
            if (strpos($varArg, '=')) { // arg=value
229
                list($argName, $argValue) = explode('=', $varArg, 2);
230
                $argName = strtolower($argName);
231
                if ($argName == 'size') {
232
                    list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $argValue, 2);
233
                } else {
234
                    $varInlineArgs[strtolower($argName)] = $argValue;
235
                }
236
            } elseif (preg_match('/^([0-9]*[a-z%]{0,2}|auto)x([0-9]*[a-z%]{0,2}|auto)$/i', $varArg)) { // 60x40
237
                list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $varArg, 2);
238
            } else { // :60:40:f
239
                switch ($argIdx) {
240
                    case 0:
241
                        $varInlineArgs['width'] = $varArg;
242
                        break;
243
                    case 1:
244
                        $varInlineArgs['height'] = $varArg;
245
                        break;
246
                    case 2:
247
                        $varInlineArgs['ratio'] = $varArg;
248
                        break;
249
                }
250
            }
251
        }
252
253 1
        return $varInlineArgs;
254
    }
255
256
    /**
257
     * @param mixed $replaceImage
258
     * @param array $varInlineArgs
259
     *
260
     * @return array
261
     */
262 1
    public function prepareImageAttrs($replaceImage, array $varInlineArgs = []): array
263
    {
264
        // get image path and size
265 1
        $width = null;
266 1
        $height = null;
267 1
        $ratio = null;
268
269
        // a closure can be passed as replacement value which after resolving, can contain the replacement info for the image
270
        // use case: only when a image if found, the replacement tags can be generated
271 1
        if (is_callable($replaceImage)) {
272
            $replaceImage = $replaceImage();
273
        }
274
275 1
        if (is_array($replaceImage) && isset($replaceImage['path'])) {
276 1
            $imgPath = $replaceImage['path'];
277 1
            if (isset($replaceImage['width'])) {
278 1
                $width = $replaceImage['width'];
279
            }
280 1
            if (isset($replaceImage['height'])) {
281 1
                $height = $replaceImage['height'];
282
            }
283 1
            if (isset($replaceImage['ratio'])) {
284 1
                $ratio = $replaceImage['ratio'];
285
            }
286
        } else {
287
            $imgPath = $replaceImage;
288
        }
289
290 1
        $width = $this->chooseImageDimension($width, isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115);
291 1
        $height = $this->chooseImageDimension($height, isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70);
292
293 1
        $imageData = @getimagesize($imgPath);
294 1
        if (!is_array($imageData)) {
295
            throw new Exception(sprintf('Invalid image: %s', $imgPath));
296
        }
297 1
        list($actualWidth, $actualHeight, $imageType) = $imageData;
298
299
        // fix aspect ratio (by default)
300 1
        if (is_null($ratio) && isset($varInlineArgs['ratio'])) {
301
            $ratio = $varInlineArgs['ratio'];
302
        }
303 1
        if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) {
304 1
            $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight);
305
        }
306
307
        $imageAttrs = array(
308 1
            'src'    => $imgPath,
309 1
            'mime'   => image_type_to_mime_type($imageType),
310 1
            'width'  => $width,
311 1
            'height' => $height,
312
        );
313
314 1
        return $imageAttrs;
315
    }
316
317
    /**
318
     * @param mixed $width
319
     * @param mixed $height
320
     * @param int $actualWidth
321
     * @param int $actualHeight
322
     */
323 1
    private function fixImageWidthHeightRatio(&$width, &$height, int $actualWidth, int $actualHeight): void
324
    {
325 1
        $imageRatio = $actualWidth / $actualHeight;
326
327 1
        if (($width === '') && ($height === '')) { // defined size are empty
328
            $width = $actualWidth . 'pt';
329
            $height = $actualHeight . 'pt';
330 1
        } elseif ($width === '') { // defined width is empty
331
            $heightFloat = (float) $height;
332
            $widthFloat = $heightFloat * $imageRatio;
333
            $matches = array();
334
            preg_match("/\d([a-z%]+)$/", $height, $matches);
335
            $width = $widthFloat . $matches[1];
336 1
        } elseif ($height === '') { // defined height is empty
337
            $widthFloat = (float) $width;
338
            $heightFloat = $widthFloat / $imageRatio;
339
            $matches = array();
340
            preg_match("/\d([a-z%]+)$/", $width, $matches);
341
            $height = $heightFloat . $matches[1];
342
        } else { // we have defined size, but we need also check it aspect ratio
343 1
            $widthMatches = array();
344 1
            preg_match("/\d([a-z%]+)$/", $width, $widthMatches);
345 1
            $heightMatches = array();
346 1
            preg_match("/\d([a-z%]+)$/", $height, $heightMatches);
347
            // try to fix only if dimensions are same
348 1
            if ($widthMatches[1] == $heightMatches[1]) {
349 1
                $dimention = $widthMatches[1];
350 1
                $widthFloat = (float) $width;
351 1
                $heightFloat = (float) $height;
352 1
                $definedRatio = $widthFloat / $heightFloat;
353
354 1
                if ($imageRatio > $definedRatio) { // image wider than defined box
355 1
                    $height = ($widthFloat / $imageRatio) . $dimention;
356 1
                } elseif ($imageRatio < $definedRatio) { // image higher than defined box
357 1
                    $width = ($heightFloat * $imageRatio) . $dimention;
358
                }
359
            }
360
        }
361 1
    }
362
363 1
    private function chooseImageDimension(?int $baseValue, ?int $inlineValue, int $defaultValue): string
364
    {
365 1
        $value = $baseValue;
366 1
        if (is_null($value) && isset($inlineValue)) {
367
            $value = $inlineValue;
368
        }
369 1
        if (!preg_match('/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i', $value)) {
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

369
        if (!preg_match('/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i', /** @scrutinizer ignore-type */ $value)) {
Loading history...
370
            $value = null;
371
        }
372 1
        if (is_null($value)) {
373
            $value = $defaultValue;
374
        }
375 1
        if (is_numeric($value)) {
0 ignored issues
show
introduced by
The condition is_numeric($value) is always true.
Loading history...
376 1
            $value .= 'pt';
377
        }
378
379 1
        return $value;
380
    }
381
382 1
    public function addImageToRelations(string $partFileName, string $rid, string $imgPath, string $imageMimeType): void
383
    {
384
        // define templates
385 1
        $typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>';
386 1
        $relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>';
387 1
        $newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n" . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
388 1
        $newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
389
        $extTransform = array(
390 1
            'image/jpeg' => 'jpeg',
391
            'image/png'  => 'png',
392
            'image/bmp'  => 'bmp',
393
            'image/gif'  => 'gif',
394
        );
395
396
        // get image embed name
397 1
        if (isset($this->tempDocumentNewImages[$imgPath])) {
398
            $imgName = $this->tempDocumentNewImages[$imgPath];
399
        } else {
400
            // transform extension
401 1
            if (isset($extTransform[$imageMimeType])) {
402 1
                $imgExt = $extTransform[$imageMimeType];
403
            } else {
404
                throw new Exception("Unsupported image type $imageMimeType");
405
            }
406
407
            // add image to document
408 1
            $imgName = 'image_' . $rid . '_' . pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($partFileName, ...late\PATHINFO_FILENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

408
            $imgName = 'image_' . $rid . '_' . /** @scrutinizer ignore-type */ pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
Loading history...
409 1
            $this->tempDocumentNewImages[$imgPath] = $imgName;
410
411
            // setup type for image
412 1
            $xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl);
413 1
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
414
        }
415
416 1
        $xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl);
417
418 1
        if (!isset($this->tempDocumentRelations[$partFileName])) {
419
            // create new relations file
420 1
            $this->tempDocumentRelations[$partFileName] = $newRelationsTpl;
421
            // and add it to content types
422 1
            $xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl);
423 1
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>';
424
        }
425
426
        // add image to relations
427 1
        $this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>';
428
429 1
        if (!in_array($this->getDocumentContentTypesName(), $this->skipFiles)) {
430 1
            $this->skipFiles[] = basename($this->getDocumentContentTypesName());
431
        }
432 1
    }
433
434
    /**
435
     * @param mixed $search
436
     * @param mixed $replace Path to image, or array("path" => xx, "width" => yy, "height" => zz)
437
     */
438 1
    public function setImageValue($search, $replace): void
439
    {
440
        // prepare $search_replace
441 1
        if (!is_array($search)) {
442
            $search = array($search);
443
        }
444
445 1
        $replacesList = array();
446 1
        if (!is_array($replace) || isset($replace['path'])) {
447
            $replacesList[] = $replace;
448
        } else {
449 1
            $replacesList = array_values($replace);
450
        }
451
452 1
        $searchReplace = array();
453 1
        foreach ($search as $searchIdx => $searchString) {
454 1
            $searchReplace[$searchString] = isset($replacesList[$searchIdx]) ? $replacesList[$searchIdx] : $replacesList[0];
455
        }
456
457
        // collect document parts
458
        $searchParts = array(
459 1
            $this->getMainPartName() => &$this->tempDocumentMainPart,
460
        );
461
        // define templates
462
        // result can be verified via "Open XML SDK 2.5 Productivity Tool" (http://www.microsoft.com/en-us/download/details.aspx?id=30425)
463 1
        $imgTpl = $this->getImageTemplate();
464
465 1
        foreach ($searchParts as $partFileName => &$partContent) {
466 1
            $partVariables = $this->getVariablesForPart($partContent);
467
468 1
            foreach ($searchReplace as $searchString => $replaceImage) {
469
                $varsToReplace = array_filter($partVariables, function ($partVar) use ($searchString) {
470 1
                    return ($partVar == $searchString) || preg_match('/^' . preg_quote($searchString) . ':/', $partVar);
471 1
                });
472
473 1
                foreach ($varsToReplace as $varNameWithArgs) {
474 1
                    $varInlineArgs = $this->getImageArgs($varNameWithArgs);
475
476 1
                    $preparedImageAttrs = $this->prepareImageAttrs($replaceImage, $varInlineArgs);
477 1
                    $imgPath = $preparedImageAttrs['src'];
478
479
                    // get image index
480 1
                    $imgIndex = $this->getNextRelationsIndex($partFileName);
481 1
                    $rid = 'rId' . $imgIndex;
482
483
                    // replace preparations
484 1
                    $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']);
485 1
                    $xmlImage = str_replace(array('{RID}', '{WIDTH}', '{HEIGHT}'), array($rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']), $imgTpl);
486
487
                    // replace variable
488 1
                    $varNameWithArgsFixed = self::ensureMacroCompleted($varNameWithArgs);
489 1
                    $matches = array();
490 1
                    if (preg_match('/(<[^<]+>)([^<]*)(' . preg_quote($varNameWithArgsFixed) . ')([^>]*)(<[^>]+>)/Uu', $partContent, $matches)) {
491 1
                        $wholeTag = $matches[0];
492 1
                        array_shift($matches);
493 1
                        list($openTag, $prefix, , $postfix, $closeTag) = $matches;
494 1
                        $replaceXml = $openTag . $prefix . $closeTag . $xmlImage . $openTag . $postfix . $closeTag;
495
                        // replace on each iteration, because in one tag we can have 2+ inline variables => before proceed next variable we need to change $partContent
496 1
                        $partContent = $this->setValueForPart($wholeTag, $replaceXml, $partContent);
497
                    }
498
                }
499
            }
500
        }
501
502 1
        $this->document = $this->tempDocumentMainPart;
503 1
    }
504
505 1
    public function getImageTemplate(): string
506
    {
507 1
        return '<w:pict><v:shape xmlns:v="urn:schemas-microsoft-com:vml" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:o="urn:schemas-microsoft-com:office:office" type="#_x0000_t75" style="width:{WIDTH};height:{HEIGHT}" stroked="f"><v:imagedata r:id="{RID}" o:title=""/></v:shape></w:pict>';
508
    }
509
510
    /**
511
     * Find and replace macros in the given XML section.
512
     *
513
     * @param mixed $search
514
     * @param mixed $replace
515
     * @param string $documentPartXML
516
     *
517
     * @return string
518
     */
519 1
    protected function setValueForPart($search, $replace, string $documentPartXML): string
520
    {
521
        // Note: we can't use the same function for both cases here, because of performance considerations.
522 1
        $regExpEscaper = new RegExp();
523
524 1
        return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML);
525
    }
526
527
    /**
528
     * Get document.xml contents as DOMDocument
529
     *
530
     * @return DOMDocument
531
     */
532 8
    public function getDOMDocument(): DOMDocument
533
    {
534 8
        $dom = new DOMDocument();
535
536 8
        $dom->loadXML($this->document);
537 8
        return $dom;
538
    }
539
540
    /**
541
     * Update document.xml contents
542
     *
543
     * @param DOMDocument $dom - new contents
544
     */
545 6
    public function updateDOMDocument(DOMDocument $dom): void
546
    {
547 6
        $this->document = $dom->saveXml();
548 6
        file_put_contents($this->tmpDir . "/word/document.xml", $this->document);
549 6
    }
550
551
    /**
552
     * Fix table corruption
553
     *
554
     * @param string $xml - xml to fix
555
     *
556
     * @return DOMDocument
557
     */
558 6
    public function fixTables(string $xml): DOMDocument
559
    {
560 6
        $dom = new DOMDocument();
561 6
        $dom->loadXML($xml);
562 6
        $tables = $dom->getElementsByTagName('tbl');
563 6
        foreach ($tables as $table) {
564 3
            $columns = [];
565 3
            $columnsLen = 0;
566 3
            $toAdd = 0;
567 3
            $tableGrid = null;
568 3
            foreach ($table->childNodes as $el) {
569 3
                if ($el->nodeName == 'w:tblGrid') {
570 3
                    $tableGrid = $el;
571 3
                    foreach ($el->childNodes as $col) {
572 3
                        if ($col->nodeName == 'w:gridCol') {
573 3
                            $columns[] = $col;
574 3
                            $columnsLen += 1;
575
                        }
576
                    }
577 3
                } elseif ($el->nodeName == 'w:tr') {
578 3
                    $cellsLen = 0;
579 3
                    foreach ($el->childNodes as $col) {
580 3
                        if ($col->nodeName == 'w:tc') {
581 3
                            $cellsLen += 1;
582
                        }
583
                    }
584 3
                    if (($columnsLen + $toAdd) < $cellsLen) {
585 3
                        $toAdd = $cellsLen - $columnsLen;
586
                    }
587
                }
588
            }
589
590
            // add columns, if necessary
591 3
            if (!is_null($tableGrid) && $toAdd > 0) {
592
                $width = 0;
593
                foreach ($columns as $col) {
594
                    if (!is_null($col->getAttribute('w:w'))) {
595
                        $width += $col->getAttribute('w:w');
596
                    }
597
                }
598
                if ($width > 0) {
599
                    $oldAverage = $width / $columnsLen;
600
                    $newAverage = round($width / ($columnsLen + $toAdd));
601
                    foreach ($columns as $col) {
602
                        $col->setAttribute('w:w', round($col->getAttribute('w:w') * $newAverage / $oldAverage));
603
                    }
604
                    while ($toAdd > 0) {
605
                        $newCol = $dom->createElement("w:gridCol");
606
                        $newCol->setAttribute('w:w', $newAverage);
607
                        $tableGrid->appendChild($newCol);
608
                        $toAdd -= 1;
609
                    }
610
                }
611
            }
612
613
            // remove columns, if necessary
614 3
            $columns = [];
615 3
            foreach ($tableGrid->childNodes as $col) {
616 3
                if ($col->nodeName == 'w:gridCol') {
617 3
                    $columns[] = $col;
618
                }
619
            }
620 3
            $columnsLen = count($columns);
621
622 3
            $cellsLen = 0;
623 3
            $cellsLenMax = 0;
624 3
            foreach ($table->childNodes as $el) {
625 3
                if ($el->nodeName == 'w:tr') {
626 3
                    $cells = [];
627 3
                    foreach ($el->childNodes as $col) {
628 3
                        if ($col->nodeName == 'w:tc') {
629 3
                            $cells[] = $col;
630
                        }
631
                    }
632 3
                    $cellsLen = $this->getCellLen($cells);
633 3
                    $cellsLenMax = max($cellsLenMax, $cellsLen);
634
                }
635
            }
636 3
            $toRemove = $cellsLen - $cellsLenMax;
637 3
            if ($toRemove > 0) {
638
                $removedWidth = 0.0;
639
                for ($i = $columnsLen - 1; ($i + 1) >= $toRemove; $i -= 1) {
640
                    $extraCol = $columns[$i];
641
                    $removedWidth += $extraCol->getAttribute('w:w');
642
                    $tableGrid->removeChild($extraCol);
643
                }
644
645
                $columnsLeft = [];
646
                foreach ($tableGrid->childNodes as $col) {
647
                    if ($col->nodeName == 'w:gridCol') {
648
                        $columnsLeft[] = $col;
649
                    }
650
                }
651
                $extraSpace = 0;
652
                if (count($columnsLeft) > 0) {
653
                    $extraSpace = $removedWidth / count($columnsLeft);
654
                }
655
                foreach ($columnsLeft as $col) {
656 3
                    $col->setAttribute('w:w', round($col->getAttribute('w:w') + $extraSpace));
657
                }
658
            }
659
        }
660 6
        return $dom;
661
    }
662
663
    /**
664
     * Get total cells length
665
     *
666
     * @param array $cells - cells
667
     *
668
     * @return int
669
     */
670 3
    private function getCellLen(array $cells): int
671
    {
672 3
        $total = 0;
673 3
        foreach ($cells as $cell) {
674 3
            foreach ($cell->childNodes as $tc) {
675 3
                if ($tc->nodeName == 'w:tcPr') {
676 3
                    foreach ($tc->childNodes as $span) {
677 3
                        if ($span->nodeName == 'w:gridSpan') {
678 1
                            $total += intval($span->getAttribute('w:val'));
679 3
                            break;
680
                        }
681
                    }
682 3
                    break;
683
                }
684
            }
685
        }
686 3
        return $total + 1;
687
    }
688
689
    /**
690
     * @param ZipArchive $target
691
     * @param string $fileName
692
     * @param string $xml
693
     */
694 2
    protected function savePartWithRels(ZipArchive $target, string $fileName, string $xml): void
695
    {
696 2
        if (isset($this->tempDocumentRelations[$fileName])) {
697 2
            $relsFileName = $this->getRelationsName($fileName);
698 2
            $this->skipFiles[] = basename($relsFileName);
699 2
            $target->addFromString($relsFileName, $this->tempDocumentRelations[$fileName]);
700
        }
701 2
    }
702
703
    /**
704
     * Save the document to the target path
705
     *
706
     * @param string $path - target path
707
     */
708 2
    public function save(string $path): void
709
    {
710 2
        $rootPath = realpath($this->tmpDir);
711
712 2
        $zip = new ZipArchive();
713 2
        $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
714
715 2
        $this->savePartWithRels($zip, $this->getMainPartName(), $this->tempDocumentMainPart);
716 2
        $zip->addFromString($this->getDocumentContentTypesName(), $this->tempDocumentContentTypes);
717
718 2
        foreach ($this->tempDocumentNewImages as $imgPath => $imgName) {
719 1
            $zip->addFile($imgPath, 'word/media/' . $imgName);
720
        }
721
722 2
        $files = new RecursiveIteratorIterator(
723 2
            new RecursiveDirectoryIterator($rootPath),
724 2
            RecursiveIteratorIterator::LEAVES_ONLY
725
        );
726
727 2
        foreach ($files as $name => $file) {
728 2
            if (!$file->isDir()) {
729 2
                $filePath = $file->getRealPath();
730 2
                $relativePath = substr($filePath, strlen($rootPath) + 1);
731 2
                if (!in_array(basename($filePath), $this->skipFiles)) {
732 2
                    $zip->addFile($filePath, $relativePath);
733
                }
734
            }
735
        }
736
737 2
        $zip->close();
738
739 2
        if (isset($this->zipClass)) {
740 2
            $this->zipClass->close();
741
        }
742
743 2
        $this->rrmdir($this->tmpDir);
744 2
    }
745
746
    /**
747
     * Remove recursively directory
748
     *
749
     * @param string $dir - target directory
750
     */
751 7
    private function rrmdir(string $dir): void
752
    {
753 7
        $objects = scandir($dir);
754 7
        if (is_array($objects)) {
0 ignored issues
show
introduced by
The condition is_array($objects) is always true.
Loading history...
755 7
            foreach ($objects as $object) {
756 7
                if ($object != "." && $object != "..") {
757 7
                    if (filetype($dir . "/" . $object) == "dir") {
758 7
                        $this->rrmdir($dir . "/" . $object);
759
                    } else {
760 7
                        unlink($dir . "/" . $object);
761
                    }
762
                }
763
            }
764 7
            reset($objects);
765 7
            rmdir($dir);
766
        }
767 7
    }
768
769
    /**
770
     * Close document
771
     */
772 5
    public function close(): void
773
    {
774 5
        if (isset($this->zipClass)) {
775 5
            $this->zipClass->close();
776
        }
777 5
        $this->rrmdir($this->tmpDir);
778 5
    }
779
}
780