Test Setup Failed
Push — master ( b30e76...2d6fd0 )
by Bingo
03:03
created

DocxDocument::readPartWithRels()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpDocxTemplate;
4
5
use DOMDocument;
6
use DOMElement;
7
use Exception;
8
use RecursiveIteratorIterator;
9
use RecursiveDirectoryIterator;
10
use PhpDocxTemplate\Escaper\RegExp;
11
use ZipArchive;
12
13
/**
14
 * Class DocxDocument
15
 *
16
 * @package PhpDocxTemplate
17
 */
18
class DocxDocument
19
{
20
    private $path;
21
    private $tmpDir;
22
    private $document;
23
    private $zipClass;
24
    private $tempDocumentMainPart;
25
    private $tempDocumentRelations = [];
26
    private $tempDocumentContentTypes = '';
27
    private $tempDocumentNewImages = [];
28
    private $skipFiles = [];
0 ignored issues
show
introduced by
The private property $skipFiles is not used, and could be removed.
Loading history...
29
30
    /**
31
     * Construct an instance of Document
32
     *
33
     * @param string $path - path to the document
34
     *
35
     * @throws Exception
36
     */
37 11
    public function __construct(string $path)
38
    {
39 11
        if (file_exists($path)) {
40 11
            $this->path = $path;
41 11
            $this->tmpDir = sys_get_temp_dir() . "/" . uniqid("", true) . date("His");
42 11
            $this->zipClass = new ZipArchive();
43 11
            $this->extract();
44
        } else {
45
            throw new Exception("The template " . $path . " was not found!");
46
        }
47 11
    }
48
49
    /**
50
     * Extract (unzip) document contents
51
     */
52 11
    private function extract(): void
53
    {
54 11
        if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) {
55
            $this->rrmdir($this->tmpDir);
56
        }
57
58 11
        mkdir($this->tmpDir);
59
60 11
        $this->zipClass->open($this->path);
61 11
        $this->zipClass->extractTo($this->tmpDir);
62
63 11
        $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
64
65 11
        $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName());
66
67 11
        $this->document = file_get_contents($this->tmpDir . "/word/document.xml");
68 11
    }
69
70
    /**
71
     * Get document main part
72
     *
73
     * @return string
74
     */
75 1
    public function getDocumentMainPart(): string
76
    {
77 1
        return $this->tempDocumentMainPart;
78
    }
79
80
    /**
81
     * Get the name of main part document (method from PhpOffice\PhpWord)
82
     *
83
     * @return string
84
     */
85 11
    public function getMainPartName(): string
86
    {
87 11
        $contentTypes = $this->zipClass->getFromName('[Content_Types].xml');
88
89
        $pattern = '~PartName="\/(word\/document.*?\.xml)" ' .
90
                   'ContentType="application\/vnd\.openxmlformats-officedocument' .
91 11
                   '\.wordprocessingml\.document\.main\+xml"~';
92
93 11
        $matches = [];
94 11
        preg_match($pattern, $contentTypes, $matches);
95
96 11
        return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml';
97
    }
98
99
    /**
100
     * @return string
101
     */
102 11
    private function getDocumentContentTypesName(): string
103
    {
104 11
        return '[Content_Types].xml';
105
    }
106
107
    /**
108
     * Read document part (method from PhpOffice\PhpWord)
109
     *
110
     * @param string $fileName
111
     *
112
     * @return string
113
     */
114 11
    private function readPartWithRels(string $fileName): string
115
    {
116 11
        $relsFileName = $this->getRelationsName($fileName);
117 11
        $partRelations = $this->zipClass->getFromName($relsFileName);
118 11
        if ($partRelations !== false) {
119 11
            $this->tempDocumentRelations[$fileName] = $partRelations;
120
        }
121
122 11
        return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
123
    }
124
125
    /**
126
     * Get the name of the relations file for document part (method from PhpOffice\PhpWord)
127
     *
128
     * @param string $documentPartName
129
     *
130
     * @return string
131
     */
132 11
    private function getRelationsName(string $documentPartName): string
133
    {
134 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

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

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

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