Passed
Push — master ( de4e3d...a7f25a )
by Bingo
02:44
created

DocxDocument::setImageValue()   C

Complexity

Conditions 13
Paths 240

Size

Total Lines 65
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 35.8827

Importance

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

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
    private function getNextRelationsIndex(string $documentPartName): int
153
    {
154
        if (isset($this->tempDocumentRelations[$documentPartName])) {
155
            $candidate = substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship');
156
            while (strpos($this->tempDocumentRelations[$documentPartName], 'Id="rId' . $candidate . '"') !== false) {
157
                $candidate++;
158
            }
159
160
            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
    protected static function ensureMacroCompleted(string $macro): string
190
    {
191
        if (substr($macro, 0, 2) !== '${' && substr($macro, -1) !== '}') {
192
            $macro = '${' . $macro . '}';
193
        }
194
        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
        //preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
232 1
        preg_match_all('/\{\{(.*?)\}\}/i', $documentPartXML, $matches);
233
234 1
        return $matches[1];
235
    }
236
237
    private function getImageArgs(string $varNameWithArgs): array
238
    {
239
        $varElements = explode(':', $varNameWithArgs);
240
        array_shift($varElements); // first element is name of variable => remove it
241
242
        $varInlineArgs = array();
243
        // size format documentation: https://msdn.microsoft.com/en-us/library/documentformat.openxml.vml.shape%28v=office.14%29.aspx?f=255&MSPPError=-2147217396
244
        foreach ($varElements as $argIdx => $varArg) {
245
            if (strpos($varArg, '=')) { // arg=value
246
                list($argName, $argValue) = explode('=', $varArg, 2);
247
                $argName = strtolower($argName);
248
                if ($argName == 'size') {
249
                    list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $argValue, 2);
250
                } else {
251
                    $varInlineArgs[strtolower($argName)] = $argValue;
252
                }
253
            } elseif (preg_match('/^([0-9]*[a-z%]{0,2}|auto)x([0-9]*[a-z%]{0,2}|auto)$/i', $varArg)) { // 60x40
254
                list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $varArg, 2);
255
            } else { // :60:40:f
256
                switch ($argIdx) {
257
                    case 0:
258
                        $varInlineArgs['width'] = $varArg;
259
                        break;
260
                    case 1:
261
                        $varInlineArgs['height'] = $varArg;
262
                        break;
263
                    case 2:
264
                        $varInlineArgs['ratio'] = $varArg;
265
                        break;
266
                }
267
            }
268
        }
269
270
        return $varInlineArgs;
271
    }
272
273
    /**
274
     * @param mixed $replaceImage
275
     * @param array $varInlineArgs
276
     *
277
     * @return array
278
     */
279
    private function prepareImageAttrs($replaceImage, array $varInlineArgs): array
280
    {
281
        // get image path and size
282
        $width = null;
283
        $height = null;
284
        $ratio = null;
285
286
        // a closure can be passed as replacement value which after resolving, can contain the replacement info for the image
287
        // use case: only when a image if found, the replacement tags can be generated
288
        if (is_callable($replaceImage)) {
289
            $replaceImage = $replaceImage();
290
        }
291
292
        if (is_array($replaceImage) && isset($replaceImage['path'])) {
293
            $imgPath = $replaceImage['path'];
294
            if (isset($replaceImage['width'])) {
295
                $width = $replaceImage['width'];
296
            }
297
            if (isset($replaceImage['height'])) {
298
                $height = $replaceImage['height'];
299
            }
300
            if (isset($replaceImage['ratio'])) {
301
                $ratio = $replaceImage['ratio'];
302
            }
303
        } else {
304
            $imgPath = $replaceImage;
305
        }
306
307
        $width = $this->chooseImageDimension($width, isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115);
308
        $height = $this->chooseImageDimension($height, isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70);
309
310
        $imageData = @getimagesize($imgPath);
311
        if (!is_array($imageData)) {
312
            throw new Exception(sprintf('Invalid image: %s', $imgPath));
313
        }
314
        list($actualWidth, $actualHeight, $imageType) = $imageData;
315
316
        // fix aspect ratio (by default)
317
        if (is_null($ratio) && isset($varInlineArgs['ratio'])) {
318
            $ratio = $varInlineArgs['ratio'];
319
        }
320
        if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) {
321
            $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight);
322
        }
323
324
        $imageAttrs = array(
325
            'src'    => $imgPath,
326
            'mime'   => image_type_to_mime_type($imageType),
327
            'width'  => $width,
328
            'height' => $height,
329
        );
330
331
        return $imageAttrs;
332
    }
333
334
    /**
335
     * @param mixed $width
336
     * @param mixed $height
337
     * @param int $actualWidth
338
     * @param int $actualHeight
339
     */
340
    private function fixImageWidthHeightRatio(&$width, &$height, int $actualWidth, int $actualHeight): void
341
    {
342
        $imageRatio = $actualWidth / $actualHeight;
343
344
        if (($width === '') && ($height === '')) { // defined size are empty
345
            $width = $actualWidth . 'px';
346
            $height = $actualHeight . 'px';
347
        } elseif ($width === '') { // defined width is empty
348
            $heightFloat = (float) $height;
349
            $widthFloat = $heightFloat * $imageRatio;
350
            $matches = array();
351
            preg_match("/\d([a-z%]+)$/", $height, $matches);
352
            $width = $widthFloat . $matches[1];
353
        } elseif ($height === '') { // defined height is empty
354
            $widthFloat = (float) $width;
355
            $heightFloat = $widthFloat / $imageRatio;
356
            $matches = array();
357
            preg_match("/\d([a-z%]+)$/", $width, $matches);
358
            $height = $heightFloat . $matches[1];
359
        } else { // we have defined size, but we need also check it aspect ratio
360
            $widthMatches = array();
361
            preg_match("/\d([a-z%]+)$/", $width, $widthMatches);
362
            $heightMatches = array();
363
            preg_match("/\d([a-z%]+)$/", $height, $heightMatches);
364
            // try to fix only if dimensions are same
365
            if ($widthMatches[1] == $heightMatches[1]) {
366
                $dimention = $widthMatches[1];
367
                $widthFloat = (float) $width;
368
                $heightFloat = (float) $height;
369
                $definedRatio = $widthFloat / $heightFloat;
370
371
                if ($imageRatio > $definedRatio) { // image wider than defined box
372
                    $height = ($widthFloat / $imageRatio) . $dimention;
373
                } elseif ($imageRatio < $definedRatio) { // image higher than defined box
374
                    $width = ($heightFloat * $imageRatio) . $dimention;
375
                }
376
            }
377
        }
378
    }
379
380
    private function chooseImageDimension(?int $baseValue, ?int $inlineValue, int $defaultValue): string
381
    {
382
        $value = $baseValue;
383
        if (is_null($value) && isset($inlineValue)) {
384
            $value = $inlineValue;
385
        }
386
        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

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

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

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