Passed
Push — master ( 679987...7f4fee )
by Bingo
03:22
created

DocxDocument::setImageValue()   C

Complexity

Conditions 13
Paths 240

Size

Total Lines 65
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 13.2134

Importance

Changes 0
Metric Value
cc 13
eloc 38
c 0
b 0
f 0
nc 240
nop 3
dl 0
loc 65
ccs 33
cts 37
cp 0.8919
crap 13.2134
rs 5.2833

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

423
            $imgName = 'image_' . $rid . '_' . /** @scrutinizer ignore-type */ pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt;
Loading history...
424
            //$this->zipClass->pclzipAddFile($imgPath, 'word/media/' . $imgName);
425 1
            $this->zipClass->addFile($imgPath, 'word/media/' . $imgName);
426
427 1
            $this->tempDocumentNewImages[$imgPath] = $imgName;
428
429
            // setup type for image
430 1
            $xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl);
431 1
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>';
432
        }
433
434 1
        $xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl);
435
436 1
        if (!isset($this->tempDocumentRelations[$partFileName])) {
437
            // create new relations file
438
            $this->tempDocumentRelations[$partFileName] = $newRelationsTpl;
439
            // and add it to content types
440
            $xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl);
441
            $this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>';
442
        }
443
444
        // add image to relations
445 1
        $this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>';
446 1
    }
447
448
    /**
449
     * @param mixed $search
450
     * @param mixed $replace Path to image, or array("path" => xx, "width" => yy, "height" => zz)
451
     * @param int $limit
452
     */
453 1
    public function setImageValue($search, $replace, ?int $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT): void
454
    {
455
        // prepare $search_replace
456 1
        if (!is_array($search)) {
457
            $search = array($search);
458
        }
459
460 1
        $replacesList = array();
461 1
        if (!is_array($replace) || isset($replace['path'])) {
462
            $replacesList[] = $replace;
463
        } else {
464 1
            $replacesList = array_values($replace);
465
        }
466
467 1
        $searchReplace = array();
468 1
        foreach ($search as $searchIdx => $searchString) {
469 1
            $searchReplace[$searchString] = isset($replacesList[$searchIdx]) ? $replacesList[$searchIdx] : $replacesList[0];
470
        }
471
472
        // collect document parts
473
        $searchParts = array(
474 1
            $this->getMainPartName() => &$this->tempDocumentMainPart,
475
        );
476 1
        foreach (array_keys($this->tempDocumentHeaders) as $headerIndex) {
477
            $searchParts[$this->getHeaderName($headerIndex)] = &$this->tempDocumentHeaders[$headerIndex];
478
        }
479 1
        foreach (array_keys($this->tempDocumentFooters) as $headerIndex) {
480
            $searchParts[$this->getFooterName($headerIndex)] = &$this->tempDocumentFooters[$headerIndex];
481
        }
482
483
        // define templates
484
        // result can be verified via "Open XML SDK 2.5 Productivity Tool" (http://www.microsoft.com/en-us/download/details.aspx?id=30425)
485 1
        $imgTpl = '<w:pict><v:shape type="#_x0000_t75" style="width:{WIDTH};height:{HEIGHT}" stroked="f"><v:imagedata r:id="{RID}" o:title=""/></v:shape></w:pict>';
486
487 1
        foreach ($searchParts as $partFileName => &$partContent) {
488 1
            $partVariables = $this->getVariablesForPart($partContent);
489
490 1
            foreach ($searchReplace as $searchString => $replaceImage) {
491
                $varsToReplace = array_filter($partVariables, function ($partVar) use ($searchString) {
492 1
                    return ($partVar == $searchString) || preg_match('/^' . preg_quote($searchString) . ':/', $partVar);
493 1
                });
494
495 1
                foreach ($varsToReplace as $varNameWithArgs) {
496 1
                    $varInlineArgs = $this->getImageArgs($varNameWithArgs);
497 1
                    $preparedImageAttrs = $this->prepareImageAttrs($replaceImage, $varInlineArgs);
498 1
                    $imgPath = $preparedImageAttrs['src'];
499
500
                    // get image index
501 1
                    $imgIndex = $this->getNextRelationsIndex($partFileName);
502 1
                    $rid = 'rId' . $imgIndex;
503
504
                    // replace preparations
505 1
                    $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']);
506 1
                    $xmlImage = str_replace(array('{RID}', '{WIDTH}', '{HEIGHT}'), array($rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']), $imgTpl);
507
508
                    // replace variable
509 1
                    $varNameWithArgsFixed = self::ensureMacroCompleted($varNameWithArgs);
510 1
                    $matches = array();
511 1
                    if (preg_match('/(<[^<]+>)([^<]*)(' . preg_quote($varNameWithArgsFixed) . ')([^>]*)(<[^>]+>)/Uu', $partContent, $matches)) {
512 1
                        $wholeTag = $matches[0];
513 1
                        array_shift($matches);
514 1
                        list($openTag, $prefix, , $postfix, $closeTag) = $matches;
515 1
                        $replaceXml = $openTag . $prefix . $closeTag . $xmlImage . $openTag . $postfix . $closeTag;
516
                        // replace on each iteration, because in one tag we can have 2+ inline variables => before proceed next variable we need to change $partContent
517 1
                        $partContent = $this->setValueForPart($wholeTag, $replaceXml, $partContent, $limit);
518
                    }
519
                }
520
            }
521
        }
522 1
    }
523
524
    /**
525
     * Find and replace macros in the given XML section.
526
     *
527
     * @param mixed $search
528
     * @param mixed $replace
529
     * @param string $documentPartXML
530
     * @param int $limit
531
     *
532
     * @return string
533
     */
534 1
    protected function setValueForPart($search, $replace, string $documentPartXML, ?int $limit): string
535
    {
536
        // Note: we can't use the same function for both cases here, because of performance considerations.
537 1
        if (self::MAXIMUM_REPLACEMENTS_DEFAULT === $limit) {
538
            return str_replace($search, $replace, $documentPartXML);
539
        }
540 1
        $regExpEscaper = new RegExp();
541
542 1
        return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML, $limit);
0 ignored issues
show
Bug introduced by
It seems like $limit can also be of type null; however, parameter $limit of preg_replace() does only seem to accept integer, 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

542
        return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML, /** @scrutinizer ignore-type */ $limit);
Loading history...
543
    }
544
545
    /**
546
     * Get document.xml contents as DOMDocument
547
     *
548
     * @return DOMDocument
549
     */
550 8
    public function getDOMDocument(): DOMDocument
551
    {
552 8
        $dom = new DOMDocument();
553 8
        $dom->loadXML($this->document);
554 8
        return $dom;
555
    }
556
557
    /**
558
     * Update document.xml contents
559
     *
560
     * @param DOMDocument $dom - new contents
561
     */
562 6
    public function updateDOMDocument(DOMDocument $dom): void
563
    {
564 6
        $this->document = $dom->saveXml();
565 6
        file_put_contents($this->tmpDir . "/word/document.xml", $this->document);
566 6
    }
567
568
    /**
569
     * Fix table corruption
570
     *
571
     * @param string $xml - xml to fix
572
     *
573
     * @return DOMDocument
574
     */
575 6
    public function fixTables(string $xml): DOMDocument
576
    {
577 6
        $dom = new DOMDocument();
578 6
        $dom->loadXML($xml);
579 6
        $tables = $dom->getElementsByTagName('tbl');
580 6
        foreach ($tables as $table) {
581 2
            $columns = [];
582 2
            $columnsLen = 0;
583 2
            $toAdd = 0;
584 2
            $tableGrid = null;
585 2
            foreach ($table->childNodes as $el) {
586 2
                if ($el->nodeName == 'w:tblGrid') {
587 2
                    $tableGrid = $el;
588 2
                    foreach ($el->childNodes as $col) {
589 2
                        if ($col->nodeName == 'w:gridCol') {
590 2
                            $columns[] = $col;
591 2
                            $columnsLen += 1;
592
                        }
593
                    }
594 2
                } elseif ($el->nodeName == 'w:tr') {
595 2
                    $cellsLen = 0;
596 2
                    foreach ($el->childNodes as $col) {
597 2
                        if ($col->nodeName == 'w:tc') {
598 2
                            $cellsLen += 1;
599
                        }
600
                    }
601 2
                    if (($columnsLen + $toAdd) < $cellsLen) {
602 2
                        $toAdd = $cellsLen - $columnsLen;
603
                    }
604
                }
605
            }
606
607
            // add columns, if necessary
608 2
            if (!is_null($tableGrid) && $toAdd > 0) {
609
                $width = 0;
610
                foreach ($columns as $col) {
611
                    if (!is_null($col->getAttribute('w:w'))) {
612
                        $width += $col->getAttribute('w:w');
613
                    }
614
                }
615
                if ($width > 0) {
616
                    $oldAverage = $width / $columnsLen;
617
                    $newAverage = round($width / ($columnsLen + $toAdd));
618
                    foreach ($columns as $col) {
619
                        $col->setAttribute('w:w', round($col->getAttribute('w:w') * $newAverage / $oldAverage));
620
                    }
621
                    while ($toAdd > 0) {
622
                        $newCol = $dom->createElement("w:gridCol");
623
                        $newCol->setAttribute('w:w', $newAverage);
624
                        $tableGrid->appendChild($newCol);
625
                        $toAdd -= 1;
626
                    }
627
                }
628
            }
629
630
            // remove columns, if necessary
631 2
            $columns = [];
632 2
            foreach ($tableGrid->childNodes as $col) {
633 2
                if ($col->nodeName == 'w:gridCol') {
634 2
                    $columns[] = $col;
635
                }
636
            }
637 2
            $columnsLen = count($columns);
638
639 2
            $cellsLen = 0;
640 2
            $cellsLenMax = 0;
641 2
            foreach ($table->childNodes as $el) {
642 2
                if ($el->nodeName == 'w:tr') {
643 2
                    $cells = [];
644 2
                    foreach ($el->childNodes as $col) {
645 2
                        if ($col->nodeName == 'w:tc') {
646 2
                            $cells[] = $col;
647
                        }
648
                    }
649 2
                    $cellsLen = $this->getCellLen($cells);
650 2
                    $cellsLenMax = max($cellsLenMax, $cellsLen);
651
                }
652
            }
653 2
            $toRemove = $cellsLen - $cellsLenMax;
654 2
            if ($toRemove > 0) {
655
                $removedWidth = 0.0;
656
                for ($i = $columnsLen - 1; ($i + 1) >= $toRemove; $i -= 1) {
657
                    $extraCol = $columns[$i];
658
                    $removedWidth += $extraCol->getAttribute('w:w');
659
                    $tableGrid->removeChild($extraCol);
660
                }
661
662
                $columnsLeft = [];
663
                foreach ($tableGrid->childNodes as $col) {
664
                    if ($col->nodeName == 'w:gridCol') {
665
                        $columnsLeft[] = $col;
666
                    }
667
                }
668
                $extraSpace = 0;
669
                if (count($columnsLeft) > 0) {
670
                    $extraSpace = $removedWidth / count($columnsLeft);
671
                }
672
                foreach ($columnsLeft as $col) {
673 2
                    $col->setAttribute('w:w', round($col->getAttribute('w:w') + $extraSpace));
674
                }
675
            }
676
        }
677 6
        return $dom;
678
    }
679
680
    /**
681
     * Get total cells length
682
     *
683
     * @param array $cells - cells
684
     *
685
     * @return int
686
     */
687 2
    private function getCellLen(array $cells): int
688
    {
689 2
        $total = 0;
690 2
        foreach ($cells as $cell) {
691 2
            foreach ($cell->childNodes as $tc) {
692 2
                if ($tc->nodeName == 'w:tcPr') {
693 2
                    foreach ($tc->childNodes as $span) {
694 2
                        if ($span->nodeName == 'w:gridSpan') {
695 1
                            $total += intval($span->getAttribute('w:val'));
696 2
                            break;
697
                        }
698
                    }
699 2
                    break;
700
                }
701
            }
702
        }
703 2
        return $total + 1;
704
    }
705
706
    /**
707
     * Save the document to the target path
708
     *
709
     * @param string $path - target path
710
     */
711 2
    public function save(string $path): void
712
    {
713 2
        $rootPath = realpath($this->tmpDir);
714
715 2
        $zip = new ZipArchive();
716 2
        $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
717
718 2
        $files = new RecursiveIteratorIterator(
719 2
            new RecursiveDirectoryIterator($rootPath),
720 2
            RecursiveIteratorIterator::LEAVES_ONLY
721
        );
722
723 2
        foreach ($files as $name => $file) {
724 2
            if (!$file->isDir()) {
725 2
                $filePath = $file->getRealPath();
726 2
                $relativePath = substr($filePath, strlen($rootPath) + 1);
727 2
                $zip->addFile($filePath, $relativePath);
728
            }
729
        }
730
731 2
        $zip->close();
732
733 2
        if (isset($this->zipClass)) {
734 2
            $this->zipClass->close();
735
        }
736
737 2
        $this->rrmdir($this->tmpDir);
738 2
    }
739
740
    /**
741
     * Remove recursively directory
742
     *
743
     * @param string $dir - target directory
744
     */
745 7
    private function rrmdir(string $dir): void
746
    {
747 7
        $objects = scandir($dir);
748 7
        if (is_array($objects)) {
0 ignored issues
show
introduced by
The condition is_array($objects) is always true.
Loading history...
749 7
            foreach ($objects as $object) {
750 7
                if ($object != "." && $object != "..") {
751 7
                    if (filetype($dir . "/" . $object) == "dir") {
752 7
                        $this->rrmdir($dir . "/" . $object);
753
                    } else {
754 7
                        unlink($dir . "/" . $object);
755
                    }
756
                }
757
            }
758 7
            reset($objects);
759 7
            rmdir($dir);
760
        }
761 7
    }
762
763
    /**
764
     * Close document
765
     */
766 5
    public function close(): void
767
    {
768 5
        if (isset($this->zipClass)) {
769 5
            $this->zipClass->close();
770
        }
771 5
        $this->rrmdir($this->tmpDir);
772 5
    }
773
}
774