Passed
Push — master ( 8f1d03...3f48a5 )
by Bingo
02:51
created

DocxDocument::addImageToRelations()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 53
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 5.3456

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 30
c 1
b 0
f 0
nc 7
nop 4
dl 0
loc 53
ccs 19
cts 25
cp 0.76
crap 5.3456
rs 9.1288

How to fix   Long Method   

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 11
    public function __construct(string $path)
36
    {
37 11
        if (file_exists($path)) {
38 11
            $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
        } else {
43
            throw new Exception("The template " . $path . " was not found!");
44
        }
45 11
    }
46
47
    /**
48
     * Extract (unzip) document contents
49
     */
50 11
    private function extract(): void
51
    {
52 11
        if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) {
53
            $this->rrmdir($this->tmpDir);
54
        }
55
56 11
        mkdir($this->tmpDir);
57
58 11
        $this->zipClass->open($this->path);
59 11
        $this->zipClass->extractTo($this->tmpDir);
60
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 11
    }
67
68
    /**
69
     * Get document main part
70
     *
71
     * @return string
72
     */
73 1
    public function getDocumentMainPart(): string
74
    {
75 1
        return $this->tempDocumentMainPart;
76
    }
77
78
    /**
79
     * Get the name of main part document (method from PhpOffice\PhpWord)
80
     *
81
     * @return string
82
     */
83 11
    public function getMainPartName(): string
84
    {
85 11
        $contentTypes = $this->zipClass->getFromName('[Content_Types].xml');
86
87
        $pattern = '~PartName="\/(word\/document.*?\.xml)" ' .
88
                   'ContentType="application\/vnd\.openxmlformats-officedocument' .
89 11
                   '\.wordprocessingml\.document\.main\+xml"~';
90
91 11
        $matches = [];
92 11
        preg_match($pattern, $contentTypes, $matches);
93
94 11
        return array_key_exists(1, $matches) ? $matches[1] : sprintf('word%sdocument.xml', DIRECTORY_SEPARATOR);
95
    }
96
97
    /**
98
     * @return string
99
     */
100 11
    private function getDocumentContentTypesName(): string
101
    {
102 11
        return '[Content_Types].xml';
103
    }
104
105
    /**
106
     * Read document part (method from PhpOffice\PhpWord)
107
     *
108
     * @param string $fileName
109
     *
110
     * @return string
111
     */
112 11
    private function readPartWithRels(string $fileName): string
113
    {
114 11
        $relsFileName = $this->getRelationsName($fileName);
115 11
        $partRelations = $this->zipClass->getFromName($relsFileName);
116 11
        if ($partRelations !== false) {
117 11
            $this->tempDocumentRelations[$fileName] = $partRelations;
118
        }
119
120 11
        return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
121
    }
122
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 11
    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
135 1
    public function getNextRelationsIndex(string $documentPartName): int
136
    {
137 1
        if (isset($this->tempDocumentRelations[$documentPartName])) {
138 1
            $candidate = substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship');
139 1
            while (strpos($this->tempDocumentRelations[$documentPartName], 'Id="rId' . $candidate . '"') !== false) {
140
                $candidate++;
141
            }
142
143 1
            return $candidate;
144
        }
145
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 11
    private function fixBrokenMacros(string $documentPart): string
157
    {
158 11
        return preg_replace_callback(
159 11
            '/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U',
160
            function ($match) {
161
                return strip_tags($match[0]);
162 11
            },
163
            $documentPart
164
        );
165
    }
166
167
    /**
168
     * @param string $macro
169
     *
170
     * @return string
171
     */
172
    protected static function ensureMacroCompleted(string $macro): string
173
    {
174
        if (substr($macro, 0, 2) !== '{{' && substr($macro, -1) !== '}}') {
175
            $macro = '{{' . $macro . '}}';
176
        }
177
        return $macro;
178
    }
179
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
        $matches = array();
214
        preg_match_all('/\{\{(.*?)\}\}/i', $documentPartXML, $matches);
215
        return $matches[1];
216
    }
217
218
    private function getImageArgs(string $varNameWithArgs): array
219
    {
220
        $varElements = explode(':', $varNameWithArgs);
221
        array_shift($varElements); // first element is name of variable => remove it
222
223
        $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
        foreach ($varElements as $argIdx => $varArg) {
226
            if (strpos($varArg, '=')) { // arg=value
227
                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
254
    /**
255
     * @param mixed $replaceImage
256
     * @param array $varInlineArgs
257
     *
258
     * @return array
259
     */
260 1
    public function prepareImageAttrs($replaceImage, array $varInlineArgs = []): array
261
    {
262
        // get image path and size
263 1
        $width = null;
264 1
        $height = null;
265 1
        $unit = null;
266 1
        $ratio = null;
267
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 1
        if (is_callable($replaceImage)) {
271
            $replaceImage = $replaceImage();
272
        }
273
274 1
        if (is_array($replaceImage) && isset($replaceImage['path'])) {
275 1
            $imgPath = $replaceImage['path'];
276 1
            if (isset($replaceImage['width'])) {
277 1
                $width = $replaceImage['width'];
278
            }
279 1
            if (isset($replaceImage['height'])) {
280 1
                $height = $replaceImage['height'];
281
            }
282 1
            if (isset($replaceImage['unit'])) {
283 1
                $unit = $replaceImage['unit'];
284
            }
285 1
            if (isset($replaceImage['ratio'])) {
286 1
                $ratio = $replaceImage['ratio'];
287
            }
288
        } else {
289
            $imgPath = $replaceImage;
290
        }
291
292 1
        $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
295 1
        $imageData = @getimagesize($imgPath);
296 1
        if (!is_array($imageData)) {
297
            throw new Exception(sprintf('Invalid image: %s', $imgPath));
298
        }
299 1
        list($actualWidth, $actualHeight, $imageType) = $imageData;
300
301
        // fix aspect ratio (by default)
302 1
        if (is_null($ratio) && isset($varInlineArgs['ratio'])) {
303
            $ratio = $varInlineArgs['ratio'];
304
        }
305 1
        if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) {
306 1
            $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight);
307
        }
308
309
        $imageAttrs = array(
310 1
            'src' => $imgPath,
311 1
            'mime' => image_type_to_mime_type($imageType),
312 1
            'width' => $width * 9525,
313 1
            'height' => $height * 9525,
314
        );
315
316 1
        return $imageAttrs;
317
    }
318
319
    /**
320
     * @param mixed $width
321
     * @param mixed $height
322
     * @param int $actualWidth
323
     * @param int $actualHeight
324
     */
325 1
    private function fixImageWidthHeightRatio(&$width, &$height, int $actualWidth, int $actualHeight): void
326
    {
327 1
        $imageRatio = $actualWidth / $actualHeight;
328
329 1
        if (($width === '') && ($height === '')) { // defined size are empty
330
            $width = $actualWidth;
331
            $height = $actualHeight;
332 1
        } elseif ($width === '') { // defined width is empty
333
            $heightFloat = (float)$height;
334
            $width = $heightFloat * $imageRatio;
335 1
        } elseif ($height === '') { // defined height is empty
336
            $widthFloat = (float)$width;
337
            $height = $widthFloat / $imageRatio;
338
        }
339 1
    }
340
341
    /**
342
     * @param mixed $baseValue
343
     * @param string $unit
344
     * @param int|null $inlineValue
345
     * @param int $defaultValue
346
     */
347 1
    private function chooseImageDimension($baseValue, string $unit, ?int $inlineValue, int $defaultValue): string
348
    {
349 1
        $value = $baseValue;
350 1
        if (is_null($value) && isset($inlineValue)) {
351
            $value = $inlineValue;
352
        }
353 1
        if (is_null($value)) {
354
            $value = $defaultValue;
355
        }
356 1
        switch ($unit) {
357 1
            case 'mm':
358
                $value = $value * 3.8; // 1mm = 3.8px
359
                break;
360 1
            case 'pt':
361
                $value = $value / 3 * 4; // 1pt = 4/3 px
362
                break;
363 1
            case 'pc':
364
                $value = $value * 16; // 1px = 16px
365
                break;
366
        }
367 1
        return $value;
368
    }
369
370 1
    public function addImageToRelations(string $partFileName, string $rid, string $imgPath, string $imageMimeType): void
371
    {
372
        // define templates
373 1
        $typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>';
374 1
        $relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>';
375 1
        $newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n" . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
376 1
        $newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
377
        $extTransform = array(
378 1
            'image/jpeg' => 'jpeg',
379
            'image/png'  => 'png',
380
            'image/bmp'  => 'bmp',
381
            'image/gif'  => 'gif',
382
        );
383
        //tempDocumentRelations
384
385
        // get image embed name
386 1
        if (isset($this->tempDocumentNewImages[$imgPath])) {
387
            $imgName = $this->tempDocumentNewImages[$imgPath];
388
        } else {
389
            // transform extension
390 1
            if (isset($extTransform[$imageMimeType])) {
391 1
                $imgExt = $extTransform[$imageMimeType];
392
            } else {
393
                throw new Exception("Unsupported image type $imageMimeType");
394
            }
395
396
            // add image to document
397 1
            $imgName = 'image_' . $rid . '_' . pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($partFileName, ...late\PATHINFO_FILENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

397
            $imgName = 'image_' . $rid . '_' . /** @scrutinizer ignore-type */ pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
Loading history...
398 1
            $this->tempDocumentNewImages[$imgPath] = $imgName;
399
400 1
            $targetDir = sprintf('%s%sword%smedia', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR);
401 1
            if (!file_exists($targetDir)) {
402
                mkdir($targetDir, 0777, true);
403
            }
404 1
            copy($imgPath, sprintf('%s%s%s', $targetDir, DIRECTORY_SEPARATOR, $imgName));
405
406
            // setup type for image
407 1
            $xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl);
408 1
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
409
        }
410
411 1
        $xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl);
412
413 1
        if (!isset($this->tempDocumentRelations[$partFileName])) {
414
            // create new relations file
415
            $this->tempDocumentRelations[$partFileName] = $newRelationsTpl;
416
            // and add it to content types
417
            $xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl);
418
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>';
419
        }
420
421
        // add image to relations
422 1
        $this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>';
423 1
    }
424
425 1
    public function getImageTemplate(): string
426
    {
427 1
        return '</w:t></w:r><w:r><w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> <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">';
428
    }
429
430
    /**
431
     * Find and replace macros in the given XML section.
432
     *
433
     * @param mixed $search
434
     * @param mixed $replace
435
     * @param string $documentPartXML
436
     *
437
     * @return string
438
     */
439
    protected function setValueForPart($search, $replace, string $documentPartXML): string
440
    {
441
        // Note: we can't use the same function for both cases here, because of performance considerations.
442
        $regExpEscaper = new RegExp();
443
444
        return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML);
445
    }
446
447
    /**
448
     * Get document.xml contents as DOMDocument
449
     *
450
     * @return DOMDocument
451
     */
452 8
    public function getDOMDocument(): DOMDocument
453
    {
454 8
        $dom = new DOMDocument();
455
456 8
        $dom->loadXML($this->document);
457 8
        return $dom;
458
    }
459
460
    /**
461
     * Update document.xml contents
462
     *
463
     * @param DOMDocument $dom - new contents
464
     */
465 6
    public function updateDOMDocument(DOMDocument $dom): void
466
    {
467 6
        $this->document = $dom->saveXml();
468 6
        file_put_contents(sprintf('%s%sword%sdocument.xml', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR), $this->document);
469 6
    }
470
471
    /**
472
     * Fix table corruption
473
     *
474
     * @param string $xml - xml to fix
475
     *
476
     * @return DOMDocument
477
     */
478 6
    public function fixTables(string $xml): DOMDocument
479
    {
480 6
        $dom = new DOMDocument();
481 6
        $dom->loadXML($xml);
482 6
        $tables = $dom->getElementsByTagName('tbl');
483 6
        foreach ($tables as $table) {
484 3
            $columns = [];
485 3
            $columnsLen = 0;
486 3
            $toAdd = 0;
487 3
            $tableGrid = null;
488 3
            foreach ($table->childNodes as $el) {
489 3
                if ($el->nodeName == 'w:tblGrid') {
490 3
                    $tableGrid = $el;
491 3
                    foreach ($el->childNodes as $col) {
492 3
                        if ($col->nodeName == 'w:gridCol') {
493 3
                            $columns[] = $col;
494 3
                            $columnsLen += 1;
495
                        }
496
                    }
497 3
                } elseif ($el->nodeName == 'w:tr') {
498 3
                    $cellsLen = 0;
499 3
                    foreach ($el->childNodes as $col) {
500 3
                        if ($col->nodeName == 'w:tc') {
501 3
                            $cellsLen += 1;
502
                        }
503
                    }
504 3
                    if (($columnsLen + $toAdd) < $cellsLen) {
505
                        $toAdd = $cellsLen - $columnsLen;
506
                    }
507
                }
508
            }
509
510
            // add columns, if necessary
511 3
            if (!is_null($tableGrid) && $toAdd > 0) {
512
                $width = 0;
513
                foreach ($columns as $col) {
514
                    if (!is_null($col->getAttribute('w:w'))) {
515
                        $width += $col->getAttribute('w:w');
516
                    }
517
                }
518
                if ($width > 0) {
519
                    $oldAverage = $width / $columnsLen;
520
                    $newAverage = round($width / ($columnsLen + $toAdd));
521
                    foreach ($columns as $col) {
522
                        $col->setAttribute('w:w', round($col->getAttribute('w:w') * $newAverage / $oldAverage));
523
                    }
524
                    while ($toAdd > 0) {
525
                        $newCol = $dom->createElement("w:gridCol");
526
                        $newCol->setAttribute('w:w', $newAverage);
527
                        $tableGrid->appendChild($newCol);
528
                        $toAdd -= 1;
529
                    }
530
                }
531
            }
532
533
            // remove columns, if necessary
534 3
            $columns = [];
535 3
            foreach ($tableGrid->childNodes as $col) {
536 3
                if ($col->nodeName == 'w:gridCol') {
537 3
                    $columns[] = $col;
538
                }
539
            }
540 3
            $columnsLen = count($columns);
541
542 3
            $cellsLen = 0;
543 3
            $cellsLenMax = 0;
544 3
            foreach ($table->childNodes as $el) {
545 3
                if ($el->nodeName == 'w:tr') {
546 3
                    $cells = [];
547 3
                    foreach ($el->childNodes as $col) {
548 3
                        if ($col->nodeName == 'w:tc') {
549 3
                            $cells[] = $col;
550
                        }
551
                    }
552 3
                    $cellsLen = $this->getCellLen($cells);
553 3
                    $cellsLenMax = max($cellsLenMax, $cellsLen);
554
                }
555
            }
556 3
            $toRemove = $cellsLen - $cellsLenMax;
557 3
            if ($toRemove > 0) {
558
                $removedWidth = 0.0;
559
                for ($i = $columnsLen - 1; ($i + 1) >= $toRemove; $i -= 1) {
560
                    $extraCol = $columns[$i];
561
                    $removedWidth += $extraCol->getAttribute('w:w');
562
                    $tableGrid->removeChild($extraCol);
563
                }
564
565
                $columnsLeft = [];
566
                foreach ($tableGrid->childNodes as $col) {
567
                    if ($col->nodeName == 'w:gridCol') {
568
                        $columnsLeft[] = $col;
569
                    }
570
                }
571
                $extraSpace = 0;
572
                if (count($columnsLeft) > 0) {
573
                    $extraSpace = $removedWidth / count($columnsLeft);
574
                }
575
                foreach ($columnsLeft as $col) {
576
                    $col->setAttribute('w:w', round($col->getAttribute('w:w') + $extraSpace));
577
                }
578
            }
579
        }
580 6
        return $dom;
581
    }
582
583
    /**
584
     * Get total cells length
585
     *
586
     * @param array $cells - cells
587
     *
588
     * @return int
589
     */
590 3
    private function getCellLen(array $cells): int
591
    {
592 3
        $total = 0;
593 3
        foreach ($cells as $cell) {
594 3
            foreach ($cell->childNodes as $tc) {
595 3
                if ($tc->nodeName == 'w:tcPr') {
596 3
                    foreach ($tc->childNodes as $span) {
597 3
                        if ($span->nodeName == 'w:gridSpan') {
598 1
                            $total += intval($span->getAttribute('w:val'));
599 1
                            break;
600
                        }
601
                    }
602 3
                    break;
603
                }
604
            }
605
        }
606 3
        return $total + 1;
607
    }
608
609
    /**
610
     * @param string $fileName
611
     */
612 2
    protected function savePartWithRels(string $fileName): void
613
    {
614 2
        if (isset($this->tempDocumentRelations[$fileName])) {
615 2
            $relsFileName = $this->getRelationsName($fileName);
616 2
            $targetDir = dirname($this->tmpDir . DIRECTORY_SEPARATOR . $relsFileName);
617 2
            if (!file_exists($targetDir)) {
618
                mkdir($targetDir, 0777, true);
619
            }
620 2
            file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $relsFileName, $this->tempDocumentRelations[$fileName]);
621
        }
622 2
    }
623
624
    /**
625
     * Save the document to the target path
626
     *
627
     * @param string $path - target path
628
     */
629 2
    public function save(string $path): void
630
    {
631 2
        $rootPath = realpath($this->tmpDir);
632
633 2
        $zip = new ZipArchive();
634 2
        $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
635
636 2
        $this->savePartWithRels($this->getMainPartName());
637 2
        file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $this->getDocumentContentTypesName(), $this->tempDocumentContentTypes);
638
639 2
        $files = new RecursiveIteratorIterator(
640 2
            new RecursiveDirectoryIterator($rootPath),
641 2
            RecursiveIteratorIterator::LEAVES_ONLY
642
        );
643
644 2
        foreach ($files as $name => $file) {
645 2
            if (!$file->isDir()) {
646 2
                $filePath = $file->getRealPath();
647 2
                $relativePath = substr($filePath, strlen($rootPath) + 1);
648 2
                $zip->addFile($filePath, $relativePath);
649
            }
650
        }
651
652 2
        $zip->close();
653
654 2
        if (isset($this->zipClass)) {
655 2
            $this->zipClass->close();
656
        }
657
658 2
        $this->rrmdir($this->tmpDir);
659 2
    }
660
661
    /**
662
     * Remove recursively directory
663
     *
664
     * @param string $dir - target directory
665
     */
666 7
    private function rrmdir(string $dir): void
667
    {
668 7
        $objects = scandir($dir);
669 7
        if (is_array($objects)) {
0 ignored issues
show
introduced by
The condition is_array($objects) is always true.
Loading history...
670 7
            foreach ($objects as $object) {
671 7
                if ($object != "." && $object != "..") {
672 7
                    if (filetype($dir . DIRECTORY_SEPARATOR . $object) == "dir") {
673 7
                        $this->rrmdir($dir . DIRECTORY_SEPARATOR . $object);
674
                    } else {
675 7
                        unlink($dir . DIRECTORY_SEPARATOR . $object);
676
                    }
677
                }
678
            }
679 7
            reset($objects);
680 7
            rmdir($dir);
681
        }
682 7
    }
683
684
    /**
685
     * Close document
686
     */
687 5
    public function close(): void
688
    {
689 5
        if (isset($this->zipClass)) {
690 5
            $this->zipClass->close();
691
        }
692 5
        $this->rrmdir($this->tmpDir);
693 5
    }
694
}
695