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

DocxDocument::prepareImageAttrs()   D

Complexity

Conditions 17
Paths 170

Size

Total Lines 57
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 17.6845

Importance

Changes 0
Metric Value
cc 17
eloc 34
c 0
b 0
f 0
nc 170
nop 2
dl 0
loc 57
ccs 26
cts 30
cp 0.8667
crap 17.6845
rs 4.6333

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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