Test Failed
Push — master ( 7301ac...e89b70 )
by Bingo
02:04 queued 12s
created

DocxDocument::updateDOMDocument()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

132
        return sprintf('word%s_rels%s%s.rels', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, /** @scrutinizer ignore-type */ pathinfo($documentPartName, PATHINFO_BASENAME));
Loading history...
133
    }
134 11
135
    public function getNextRelationsIndex(string $documentPartName): int
136
    {
137 1
        if (isset($this->tempDocumentRelations[$documentPartName])) {
138
            $candidate = substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship');
139 1
            while (strpos($this->tempDocumentRelations[$documentPartName], 'Id="rId' . $candidate . '"') !== false) {
140 1
                $candidate++;
141 1
            }
142
143
            return $candidate;
144
        }
145 1
146
        return 1;
147
    }
148
149
    /**
150
     * Finds parts of broken macros and sticks them together (method from PhpOffice\PhpWord)
151
     *
152
     * @param string $documentPart
153
     *
154
     * @return string
155
     */
156
    private function fixBrokenMacros(string $documentPart): string
157
    {
158 11
        return preg_replace_callback(
159
            '/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U',
160 11
            function ($match) {
161 11
                return strip_tags($match[0]);
162
            },
163
            $documentPart
164 11
        );
165 11
    }
166
167
    /**
168
     * @param string $macro
169
     *
170
     * @return string
171
     */
172
    protected static function ensureMacroCompleted(string $macro): string
173
    {
174 1
        if (substr($macro, 0, 2) !== '{{' && substr($macro, -1) !== '}}') {
175
            $macro = '{{' . $macro . '}}';
176 1
        }
177 1
        return $macro;
178
    }
179 1
180
    /**
181
     * Get the name of the header file for $index.
182
     *
183
     * @param int $index
184
     *
185
     * @return string
186
     */
187
    private function getHeaderName(int $index): string
188
    {
189
        return sprintf('word%sheader%d.xml', DIRECTORY_SEPARATOR, $index);
190
    }
191
192
    /**
193
     * Get the name of the footer file for $index.
194
     *
195
     * @param int $index
196
     *
197
     * @return string
198
     */
199
    private function getFooterName(int $index): string
200
    {
201
        return sprintf('word%sfooter%d.xml', DIRECTORY_SEPARATOR, $index);
202
    }
203
204
    /**
205
     * Find all variables in $documentPartXML.
206
     *
207
     * @param string $documentPartXML
208
     *
209
     * @return string[]
210
     */
211
    private function getVariablesForPart(string $documentPartXML): array
212
    {
213 1
        $matches = array();
214
        preg_match_all('/\{\{(.*?)\}\}/i', $documentPartXML, $matches);
215 1
        return $matches[1];
216 1
    }
217 1
218
    private function getImageArgs(string $varNameWithArgs): array
219
    {
220 1
        $varElements = explode(':', $varNameWithArgs);
221
        array_shift($varElements); // first element is name of variable => remove it
222 1
223 1
        $varInlineArgs = array();
224
        // size format documentation: https://msdn.microsoft.com/en-us/library/documentformat.openxml.vml.shape%28v=office.14%29.aspx?f=255&MSPPError=-2147217396
225 1
        foreach ($varElements as $argIdx => $varArg) {
226
            if (strpos($varArg, '=')) { // arg=value
227 1
                list($argName, $argValue) = explode('=', $varArg, 2);
228
                $argName = strtolower($argName);
229
                if ($argName == 'size') {
230
                    list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $argValue, 2);
231
                } else {
232
                    $varInlineArgs[strtolower($argName)] = $argValue;
233
                }
234
            } elseif (preg_match('/^([0-9]*[a-z%]{0,2}|auto)x([0-9]*[a-z%]{0,2}|auto)$/i', $varArg)) { // 60x40
235
                list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $varArg, 2);
236
            } else { // :60:40:f
237
                switch ($argIdx) {
238
                    case 0:
239
                        $varInlineArgs['width'] = $varArg;
240
                        break;
241
                    case 1:
242
                        $varInlineArgs['height'] = $varArg;
243
                        break;
244
                    case 2:
245
                        $varInlineArgs['ratio'] = $varArg;
246
                        break;
247
                }
248
            }
249
        }
250
251
        return $varInlineArgs;
252
    }
253 1
254
    /**
255
     * @param mixed $replaceImage
256
     * @param array $varInlineArgs
257
     *
258
     * @return array
259
     */
260
    public function prepareImageAttrs($replaceImage, array $varInlineArgs = []): array
261
    {
262 1
        // get image path and size
263
        $width = null;
264
        $height = null;
265 1
        $unit = null;
266 1
        $ratio = null;
267 1
268
        // a closure can be passed as replacement value which after resolving, can contain the replacement info for the image
269
        // use case: only when a image if found, the replacement tags can be generated
270
        if (is_callable($replaceImage)) {
271 1
            $replaceImage = $replaceImage();
272
        }
273
274
        if (is_array($replaceImage) && isset($replaceImage['path'])) {
275 1
            $imgPath = $replaceImage['path'];
276 1
            if (isset($replaceImage['width'])) {
277 1
                $width = $replaceImage['width'];
278 1
            }
279
            if (isset($replaceImage['height'])) {
280 1
                $height = $replaceImage['height'];
281 1
            }
282
            if (isset($replaceImage['unit'])) {
283 1
                $unit = $replaceImage['unit'];
284 1
            }
285
            if (isset($replaceImage['ratio'])) {
286
                $ratio = $replaceImage['ratio'];
287
            }
288
        } else {
289
            $imgPath = $replaceImage;
290 1
        }
291 1
292
        $width = $this->chooseImageDimension($width, $unit ? $unit : 'px', isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115);
293 1
        $height = $this->chooseImageDimension($height, $unit ? $unit : 'px', isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70);
294 1
295
        $imageData = @getimagesize($imgPath);
296
        if (!is_array($imageData)) {
297 1
            throw new Exception(sprintf('Invalid image: %s', $imgPath));
298
        }
299
        list($actualWidth, $actualHeight, $imageType) = $imageData;
300 1
301
        // fix aspect ratio (by default)
302
        if (is_null($ratio) && isset($varInlineArgs['ratio'])) {
303 1
            $ratio = $varInlineArgs['ratio'];
304 1
        }
305
        if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) {
306
            $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight);
307
        }
308 1
309 1
        $imageAttrs = array(
310 1
            'src' => $imgPath,
311 1
            'mime' => image_type_to_mime_type($imageType),
312
            'width' => $width * 9525,
313
            'height' => $height * 9525,
314 1
        );
315
316
        return $imageAttrs;
317
    }
318
319
    /**
320
     * @param mixed $width
321
     * @param mixed $height
322
     * @param int $actualWidth
323 1
     * @param int $actualHeight
324
     */
325 1
    private function fixImageWidthHeightRatio(&$width, &$height, int $actualWidth, int $actualHeight): void
326
    {
327 1
        $imageRatio = $actualWidth / $actualHeight;
328
329
        if (($width === '') && ($height === '')) { // defined size are empty
330 1
            $width = $actualWidth;
331
            $height = $actualHeight;
332
        } elseif ($width === '') { // defined width is empty
333
            $heightFloat = (float)$height;
334
            $width = $heightFloat * $imageRatio;
335
        } elseif ($height === '') { // defined height is empty
336 1
            $widthFloat = (float)$width;
337
            $height = $widthFloat / $imageRatio;
338
        }
339
    }
340
341
    private function chooseImageDimension($baseValue, string $unit, ?int $inlineValue, int $defaultValue): string
342
    {
343 1
        $value = $baseValue;
344 1
        if (is_null($value) && isset($inlineValue)) {
345 1
            $value = $inlineValue;
346 1
        }
347
        if (is_null($value)) {
348 1
            $value = $defaultValue;
349 1
        }
350 1
        switch ($unit) {
351 1
            case 'mm':
352 1
                $value = $value * 3.8; // 1mm = 3.8px
353
                break;
354 1
            case 'pt':
355 1
                $value = $value / 3 * 4; // 1pt = 4/3 px
356 1
                break;
357 1
            case 'pc':
358
                $value = $value * 16; // 1px = 16px
359
                break;
360
        }
361 1
        return $value;
362
    }
363 1
364
    public function addImageToRelations(string $partFileName, string $rid, string $imgPath, string $imageMimeType): void
365 1
    {
366 1
        // define templates
367
        $typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>';
368
        $relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>';
369 1
        $newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n" . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
370
        $newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
371
        $extTransform = array(
372 1
            'image/jpeg' => 'jpeg',
373
            'image/png'  => 'png',
374
            'image/bmp'  => 'bmp',
375 1
            'image/gif'  => 'gif',
376 1
        );
377
        //tempDocumentRelations
378
379 1
        // get image embed name
380
        if (isset($this->tempDocumentNewImages[$imgPath])) {
381
            $imgName = $this->tempDocumentNewImages[$imgPath];
382 1
        } else {
383
            // transform extension
384
            if (isset($extTransform[$imageMimeType])) {
385 1
                $imgExt = $extTransform[$imageMimeType];
386 1
            } else {
387 1
                throw new Exception("Unsupported image type $imageMimeType");
388 1
            }
389
390 1
            // add image to document
391
            $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

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