Test Setup Failed
Pull Request — master (#7)
by
unknown
08:47
created

DocxDocument::addImageToRelations()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 53
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 5.1588

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 22
cts 27
cp 0.8148
crap 5.1588
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
    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] : 'word/document.xml';
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 'word/_rels/' . pathinfo($documentPartName, PATHINFO_BASENAME) . '.rels';
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($documentPartNa...late\PATHINFO_BASENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

132
        return 'word/_rels/' . /** @scrutinizer ignore-type */ pathinfo($documentPartName, PATHINFO_BASENAME) . '.rels';
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
        $ratio = null;
266 1
267 1
        // a closure can be passed as replacement value which after resolving, can contain the replacement info for the image
268
        // use case: only when a image if found, the replacement tags can be generated
269
        if (is_callable($replaceImage)) {
270
            $replaceImage = $replaceImage();
271 1
        }
272
273
        if (is_array($replaceImage) && isset($replaceImage['path'])) {
274
            $imgPath = $replaceImage['path'];
275 1
            if (isset($replaceImage['width'])) {
276 1
                $width = $replaceImage['width'];
277 1
            }
278 1
            if (isset($replaceImage['height'])) {
279
                $height = $replaceImage['height'];
280 1
            }
281 1
            if (isset($replaceImage['ratio'])) {
282
                $ratio = $replaceImage['ratio'];
283 1
            }
284 1
        } else {
285
            $imgPath = $replaceImage;
286
        }
287
288
        $width = $this->chooseImageDimension($width, isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115);
289
        $height = $this->chooseImageDimension($height, isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70);
290 1
291 1
        $imageData = @getimagesize($imgPath);
292
        if (!is_array($imageData)) {
293 1
            throw new Exception(sprintf('Invalid image: %s', $imgPath));
294 1
        }
295
        list($actualWidth, $actualHeight, $imageType) = $imageData;
296
297 1
        // fix aspect ratio (by default)
298
        if (is_null($ratio) && isset($varInlineArgs['ratio'])) {
299
            $ratio = $varInlineArgs['ratio'];
300 1
        }
301
        if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) {
302
            $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight);
303 1
        }
304 1
305
        $imageAttrs = array(
306
            'src'    => $imgPath,
307
            'mime'   => image_type_to_mime_type($imageType),
308 1
            'width'  => $width * 9525,
309 1
            'height' => $height * 9525,
310 1
        );
311 1
312
        return $imageAttrs;
313
    }
314 1
315
    /**
316
     * @param mixed $width
317
     * @param mixed $height
318
     * @param int $actualWidth
319
     * @param int $actualHeight
320
     */
321
    private function fixImageWidthHeightRatio(&$width, &$height, int $actualWidth, int $actualHeight): void
322
    {
323 1
        $imageRatio = $actualWidth / $actualHeight;
324
325 1
        if (($width === '') && ($height === '')) { // defined size are empty
326
            $width = $actualWidth;
327 1
            $height = $actualHeight;
328
        } elseif ($width === '') { // defined width is empty
329
            $heightFloat = (float) $height;
330 1
            $width = $heightFloat * $imageRatio;
331
        } elseif ($height === '') { // defined height is empty
332
            $widthFloat = (float) $width;
333
            $height = $widthFloat / $imageRatio;
334
        }
335
    }
336 1
337
    private function chooseImageDimension(?int $baseValue, ?int $inlineValue, int $defaultValue): string
338
    {
339
        $value = $baseValue;
340
        if (is_null($value) && isset($inlineValue)) {
341
            $value = $inlineValue;
342
        }
343 1
        if (!preg_match('/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i', $value)) {
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

343
        if (!preg_match('/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i', /** @scrutinizer ignore-type */ $value)) {
Loading history...
344 1
            $value = null;
345 1
        }
346 1
        if (is_null($value)) {
347
            $value = $defaultValue;
348 1
        }
349 1
350 1
        return $value;
351 1
    }
352 1
353
    public function addImageToRelations(string $partFileName, string $rid, string $imgPath, string $imageMimeType): void
354 1
    {
355 1
        // define templates
356 1
        $typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>';
357 1
        $relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>';
358
        $newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n" . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
359
        $newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
360
        $extTransform = array(
361 1
            'image/jpeg' => 'jpeg',
362
            'image/png'  => 'png',
363 1
            'image/bmp'  => 'bmp',
364
            'image/gif'  => 'gif',
365 1
        );
366 1
        //tempDocumentRelations
367
368
        // get image embed name
369 1
        if (isset($this->tempDocumentNewImages[$imgPath])) {
370
            $imgName = $this->tempDocumentNewImages[$imgPath];
371
        } else {
372 1
            // transform extension
373
            if (isset($extTransform[$imageMimeType])) {
374
                $imgExt = $extTransform[$imageMimeType];
375 1
            } else {
376 1
                throw new Exception("Unsupported image type $imageMimeType");
377
            }
378
379 1
            // add image to document
380
            $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

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