Passed
Push — master ( 297bad...6214f1 )
by Bingo
02:52
created

DocxDocument::getImageArgs()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 34
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
cc 8
eloc 25
nc 8
nop 1
dl 0
loc 34
ccs 0
cts 23
cp 0
crap 72
rs 8.4444
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
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 $tempDocumentHeaders = [];
25
    private $tempDocumentFooters = [];
26
    private $tempDocumentRelations = [];
27
    private $tempDocumentContentTypes = '';
28
    private $tempDocumentNewImages = [];
29
30
    /**
31
     * Construct an instance of Document
32
     *
33
     * @param string $path - path to the document
34
     *
35
     * @throws Exception
36
     */
37 10
    public function __construct(string $path)
38
    {
39 10
        if (file_exists($path)) {
40 10
            $this->path = $path;
41 10
            $this->tmpDir = sys_get_temp_dir() . "/" . uniqid("", true) . date("His");
42 10
            $this->zipClass = new ZipArchive();
43 10
            $this->extract();
44
        } else {
45
            throw new Exception("The template " . $path . " was not found!");
46
        }
47 10
    }
48
49
    /**
50
     * Extract (unzip) document contents
51
     */
52 10
    private function extract(): void
53
    {
54 10
        if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) {
55
            $this->rrmdir($this->tmpDir);
56
        }
57
58 10
        mkdir($this->tmpDir);
59
60 10
        $this->zipClass->open($this->path);
61 10
        $this->zipClass->extractTo($this->tmpDir);
62
63 10
        $index = 1;
64 10
        while (false !== $this->zipClass->locateName($this->getHeaderName($index))) {
65
            $this->tempDocumentHeaders[$index] = $this->readPartWithRels($this->getHeaderName($index));
66
            $index += 1;
67
        }
68 10
        $index = 1;
69 10
        while (false !== $this->zipClass->locateName($this->getFooterName($index))) {
70
            $this->tempDocumentFooters[$index] = $this->readPartWithRels($this->getFooterName($index));
71
            $index += 1;
72
        }
73
74 10
        $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
75
76 10
        $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName());
77
78 10
        $this->zipClass->close();
79
80 10
        $this->document = file_get_contents($this->tmpDir . "/word/document.xml");
81 10
    }
82
83
    /**
84
     * Get document main part
85
     *
86
     * @return string
87
     */
88 1
    public function getDocumentMainPart(): string
89
    {
90 1
        return $this->tempDocumentMainPart;
91
    }
92
93
    /**
94
     * Get the name of main part document (method from PhpOffice\PhpWord)
95
     *
96
     * @return string
97
     */
98 10
    private function getMainPartName(): string
99
    {
100 10
        $contentTypes = $this->zipClass->getFromName('[Content_Types].xml');
101
102
        $pattern = '~PartName="\/(word\/document.*?\.xml)" ' .
103
                   'ContentType="application\/vnd\.openxmlformats-officedocument' .
104 10
                   '\.wordprocessingml\.document\.main\+xml"~';
105
106 10
        $matches = [];
107 10
        preg_match($pattern, $contentTypes, $matches);
108
109 10
        return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml';
110
    }
111
112
    /**
113
     * @return string
114
     */
115 10
    private function getDocumentContentTypesName(): string
116
    {
117 10
        return '[Content_Types].xml';
118
    }
119
120
    /**
121
     * Read document part (method from PhpOffice\PhpWord)
122
     *
123
     * @param string $fileName
124
     *
125
     * @return string
126
     */
127 10
    private function readPartWithRels(string $fileName): string
128
    {
129 10
        $relsFileName = $this->getRelationsName($fileName);
130 10
        $partRelations = $this->zipClass->getFromName($relsFileName);
131 10
        if ($partRelations !== false) {
132 10
            $this->tempDocumentRelations[$fileName] = $partRelations;
133
        }
134
135 10
        return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
136
    }
137
138
    /**
139
     * Get the name of the relations file for document part (method from PhpOffice\PhpWord)
140
     *
141
     * @param string $documentPartName
142
     *
143
     * @return string
144
     */
145 10
    private function getRelationsName(string $documentPartName): string
146
    {
147 10
        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

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

383
        if (!preg_match('/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i', /** @scrutinizer ignore-type */ $value)) {
Loading history...
384
            $value = null;
385
        }
386
        if (is_null($value)) {
387
            $value = $defaultValue;
388
        }
389
        if (is_numeric($value)) {
0 ignored issues
show
introduced by
The condition is_numeric($value) is always true.
Loading history...
390
            $value .= 'px';
391
        }
392
393
        return $value;
394
    }
395
396
    private function addImageToRelations(string $partFileName, string $rid, string $imgPath, string $imageMimeType): void
397
    {
398
        // define templates
399
        $typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>';
400
        $relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>';
401
        $newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n" . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
402
        $newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
403
        $extTransform = array(
404
            'image/jpeg' => 'jpeg',
405
            'image/png'  => 'png',
406
            'image/bmp'  => 'bmp',
407
            'image/gif'  => 'gif',
408
        );
409
410
        // get image embed name
411
        if (isset($this->tempDocumentNewImages[$imgPath])) {
412
            $imgName = $this->tempDocumentNewImages[$imgPath];
413
        } else {
414
            // transform extension
415
            if (isset($extTransform[$imageMimeType])) {
416
                $imgExt = $extTransform[$imageMimeType];
417
            } else {
418
                throw new Exception("Unsupported image type $imageMimeType");
419
            }
420
421
            // add image to document
422
            $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

422
            $imgName = 'image_' . $rid . '_' . /** @scrutinizer ignore-type */ pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
Loading history...
423
            $this->zipClass->pclzipAddFile($imgPath, 'word/media/' . $imgName);
424
            $this->tempDocumentNewImages[$imgPath] = $imgName;
425
426
            // setup type for image
427
            $xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl);
428
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
429
        }
430
431
        $xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl);
432
433
        if (!isset($this->tempDocumentRelations[$partFileName])) {
434
            // create new relations file
435
            $this->tempDocumentRelations[$partFileName] = $newRelationsTpl;
436
            // and add it to content types
437
            $xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl);
438
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>';
439
        }
440
441
        // add image to relations
442
        $this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>';
443
    }
444
445
    /**
446
     * @param mixed $search
447
     * @param mixed $replace Path to image, or array("path" => xx, "width" => yy, "height" => zz)
448
     * @param int $limit
449
     */
450
    public function setImageValue($search, $replace, int $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT): void
0 ignored issues
show
Bug introduced by
The constant PhpDocxTemplate\DocxDocu...UM_REPLACEMENTS_DEFAULT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
451
    {
452
        // prepare $search_replace
453
        if (!is_array($search)) {
454
            $search = array($search);
455
        }
456
457
        $replacesList = array();
458
        if (!is_array($replace) || isset($replace['path'])) {
459
            $replacesList[] = $replace;
460
        } else {
461
            $replacesList = array_values($replace);
462
        }
463
464
        $searchReplace = array();
465
        foreach ($search as $searchIdx => $searchString) {
466
            $searchReplace[$searchString] = isset($replacesList[$searchIdx]) ? $replacesList[$searchIdx] : $replacesList[0];
467
        }
468
469
        // collect document parts
470
        $searchParts = array(
471
            $this->getMainPartName() => &$this->tempDocumentMainPart,
472
        );
473
        foreach (array_keys($this->tempDocumentHeaders) as $headerIndex) {
474
            $searchParts[$this->getHeaderName($headerIndex)] = &$this->tempDocumentHeaders[$headerIndex];
475
        }
476
        foreach (array_keys($this->tempDocumentFooters) as $headerIndex) {
477
            $searchParts[$this->getFooterName($headerIndex)] = &$this->tempDocumentFooters[$headerIndex];
478
        }
479
480
        // define templates
481
        // result can be verified via "Open XML SDK 2.5 Productivity Tool" (http://www.microsoft.com/en-us/download/details.aspx?id=30425)
482
        $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>';
483
484
        foreach ($searchParts as $partFileName => &$partContent) {
485
            $partVariables = $this->getVariablesForPart($partContent);
486
487
            foreach ($searchReplace as $searchString => $replaceImage) {
488
                $varsToReplace = array_filter($partVariables, function ($partVar) use ($searchString) {
489
                    return ($partVar == $searchString) || preg_match('/^' . preg_quote($searchString) . ':/', $partVar);
490
                });
491
492
                foreach ($varsToReplace as $varNameWithArgs) {
493
                    $varInlineArgs = $this->getImageArgs($varNameWithArgs);
494
                    $preparedImageAttrs = $this->prepareImageAttrs($replaceImage, $varInlineArgs);
495
                    $imgPath = $preparedImageAttrs['src'];
496
497
                    // get image index
498
                    $imgIndex = $this->getNextRelationsIndex($partFileName);
499
                    $rid = 'rId' . $imgIndex;
500
501
                    // replace preparations
502
                    $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']);
503
                    $xmlImage = str_replace(array('{RID}', '{WIDTH}', '{HEIGHT}'), array($rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']), $imgTpl);
504
505
                    // replace variable
506
                    $varNameWithArgsFixed = self::ensureMacroCompleted($varNameWithArgs);
507
                    $matches = array();
508
                    if (preg_match('/(<[^<]+>)([^<]*)(' . preg_quote($varNameWithArgsFixed) . ')([^>]*)(<[^>]+>)/Uu', $partContent, $matches)) {
509
                        $wholeTag = $matches[0];
510
                        array_shift($matches);
511
                        list($openTag, $prefix, , $postfix, $closeTag) = $matches;
512
                        $replaceXml = $openTag . $prefix . $closeTag . $xmlImage . $openTag . $postfix . $closeTag;
513
                        // replace on each iteration, because in one tag we can have 2+ inline variables => before proceed next variable we need to change $partContent
514
                        $partContent = $this->setValueForPart($wholeTag, $replaceXml, $partContent, $limit);
0 ignored issues
show
Bug introduced by
The method setValueForPart() does not exist on PhpDocxTemplate\DocxDocument. ( Ignorable by Annotation )

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

514
                        /** @scrutinizer ignore-call */ 
515
                        $partContent = $this->setValueForPart($wholeTag, $replaceXml, $partContent, $limit);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
515
                    }
516
                }
517
            }
518
        }
519
    }
520
521
522
    /**
523
     * Get document.xml contents as DOMDocument
524
     *
525
     * @return DOMDocument
526
     */
527 7
    public function getDOMDocument(): DOMDocument
528
    {
529 7
        $dom = new DOMDocument();
530 7
        $dom->loadXML($this->document);
531 7
        return $dom;
532
    }
533
534
    /**
535
     * Update document.xml contents
536
     *
537
     * @param DOMDocument $dom - new contents
538
     */
539 5
    public function updateDOMDocument(DOMDocument $dom): void
540
    {
541 5
        $this->document = $dom->saveXml();
542 5
        file_put_contents($this->tmpDir . "/word/document.xml", $this->document);
543 5
    }
544
545
    /**
546
     * Fix table corruption
547
     *
548
     * @param string $xml - xml to fix
549
     *
550
     * @return DOMDocument
551
     */
552 5
    public function fixTables(string $xml): DOMDocument
553
    {
554 5
        $dom = new DOMDocument();
555 5
        $dom->loadXML($xml);
556 5
        $tables = $dom->getElementsByTagName('tbl');
557 5
        foreach ($tables as $table) {
558 2
            $columns = [];
559 2
            $columnsLen = 0;
560 2
            $toAdd = 0;
561 2
            $tableGrid = null;
562 2
            foreach ($table->childNodes as $el) {
563 2
                if ($el->nodeName == 'w:tblGrid') {
564 2
                    $tableGrid = $el;
565 2
                    foreach ($el->childNodes as $col) {
566 2
                        if ($col->nodeName == 'w:gridCol') {
567 2
                            $columns[] = $col;
568 2
                            $columnsLen += 1;
569
                        }
570
                    }
571 2
                } elseif ($el->nodeName == 'w:tr') {
572 2
                    $cellsLen = 0;
573 2
                    foreach ($el->childNodes as $col) {
574 2
                        if ($col->nodeName == 'w:tc') {
575 2
                            $cellsLen += 1;
576
                        }
577
                    }
578 2
                    if (($columnsLen + $toAdd) < $cellsLen) {
579 2
                        $toAdd = $cellsLen - $columnsLen;
580
                    }
581
                }
582
            }
583
584
            // add columns, if necessary
585 2
            if (!is_null($tableGrid) && $toAdd > 0) {
586
                $width = 0;
587
                foreach ($columns as $col) {
588
                    if (!is_null($col->getAttribute('w:w'))) {
589
                        $width += $col->getAttribute('w:w');
590
                    }
591
                }
592
                if ($width > 0) {
593
                    $oldAverage = $width / $columnsLen;
594
                    $newAverage = round($width / ($columnsLen + $toAdd));
595
                    foreach ($columns as $col) {
596
                        $col->setAttribute('w:w', round($col->getAttribute('w:w') * $newAverage / $oldAverage));
597
                    }
598
                    while ($toAdd > 0) {
599
                        $newCol = $dom->createElement("w:gridCol");
600
                        $newCol->setAttribute('w:w', $newAverage);
601
                        $tableGrid->appendChild($newCol);
602
                        $toAdd -= 1;
603
                    }
604
                }
605
            }
606
607
            // remove columns, if necessary
608 2
            $columns = [];
609 2
            foreach ($tableGrid->childNodes as $col) {
610 2
                if ($col->nodeName == 'w:gridCol') {
611 2
                    $columns[] = $col;
612
                }
613
            }
614 2
            $columnsLen = count($columns);
615
616 2
            $cellsLen = 0;
617 2
            $cellsLenMax = 0;
618 2
            foreach ($table->childNodes as $el) {
619 2
                if ($el->nodeName == 'w:tr') {
620 2
                    $cells = [];
621 2
                    foreach ($el->childNodes as $col) {
622 2
                        if ($col->nodeName == 'w:tc') {
623 2
                            $cells[] = $col;
624
                        }
625
                    }
626 2
                    $cellsLen = $this->getCellLen($cells);
627 2
                    $cellsLenMax = max($cellsLenMax, $cellsLen);
628
                }
629
            }
630 2
            $toRemove = $cellsLen - $cellsLenMax;
631 2
            if ($toRemove > 0) {
632
                $removedWidth = 0.0;
633
                for ($i = $columnsLen - 1; ($i + 1) >= $toRemove; $i -= 1) {
634
                    $extraCol = $columns[$i];
635
                    $removedWidth += $extraCol->getAttribute('w:w');
636
                    $tableGrid->removeChild($extraCol);
637
                }
638
639
                $columnsLeft = [];
640
                foreach ($tableGrid->childNodes as $col) {
641
                    if ($col->nodeName == 'w:gridCol') {
642
                        $columnsLeft[] = $col;
643
                    }
644
                }
645
                $extraSpace = 0;
646
                if (count($columnsLeft) > 0) {
647
                    $extraSpace = $removedWidth / count($columnsLeft);
648
                }
649
                foreach ($columnsLeft as $col) {
650 2
                    $col->setAttribute('w:w', round($col->getAttribute('w:w') + $extraSpace));
651
                }
652
            }
653
        }
654 5
        return $dom;
655
    }
656
657
    /**
658
     * Get total cells length
659
     *
660
     * @param array $cells - cells
661
     *
662
     * @return int
663
     */
664 2
    private function getCellLen(array $cells): int
665
    {
666 2
        $total = 0;
667 2
        foreach ($cells as $cell) {
668 2
            foreach ($cell->childNodes as $tc) {
669 2
                if ($tc->nodeName == 'w:tcPr') {
670 2
                    foreach ($tc->childNodes as $span) {
671 2
                        if ($span->nodeName == 'w:gridSpan') {
672 1
                            $total += intval($span->getAttribute('w:val'));
673 2
                            break;
674
                        }
675
                    }
676 2
                    break;
677
                }
678
            }
679
        }
680 2
        return $total + 1;
681
    }
682
683
    /**
684
     * Save the document to the target path
685
     *
686
     * @param string $path - target path
687
     */
688 1
    public function save(string $path): void
689
    {
690 1
        $rootPath = realpath($this->tmpDir);
691
692 1
        $zip = new ZipArchive();
693 1
        $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
694
695 1
        $files = new RecursiveIteratorIterator(
696 1
            new RecursiveDirectoryIterator($rootPath),
697 1
            RecursiveIteratorIterator::LEAVES_ONLY
698
        );
699
700 1
        foreach ($files as $name => $file) {
701 1
            if (!$file->isDir()) {
702 1
                $filePath = $file->getRealPath();
703 1
                $relativePath = substr($filePath, strlen($rootPath) + 1);
704 1
                $zip->addFile($filePath, $relativePath);
705
            }
706
        }
707
708 1
        $zip->close();
709
710 1
        $this->rrmdir($this->tmpDir);
711 1
    }
712
713
    /**
714
     * Remove recursively directory
715
     *
716
     * @param string $dir - target directory
717
     */
718 6
    private function rrmdir(string $dir): void
719
    {
720 6
        $objects = scandir($dir);
721 6
        if (is_array($objects)) {
0 ignored issues
show
introduced by
The condition is_array($objects) is always true.
Loading history...
722 6
            foreach ($objects as $object) {
723 6
                if ($object != "." && $object != "..") {
724 6
                    if (filetype($dir . "/" . $object) == "dir") {
725 6
                        $this->rrmdir($dir . "/" . $object);
726
                    } else {
727 6
                        unlink($dir . "/" . $object);
728
                    }
729
                }
730
            }
731 6
            reset($objects);
732 6
            rmdir($dir);
733
        }
734 6
    }
735
736
    /**
737
     * Close document
738
     */
739 5
    public function close(): void
740
    {
741 5
        $this->rrmdir($this->tmpDir);
742 5
    }
743
}
744