DocxDocument::addImageToRelations()   A
last analyzed

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

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

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