Test Failed
Push — master ( 7f4fee...1025fc )
by Bingo
03:26
created

DocxDocument::chooseImageDimension()   A

Complexity

Conditions 6
Paths 16

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6.2163

Importance

Changes 0
Metric Value
cc 6
eloc 10
nc 16
nop 3
dl 0
loc 17
ccs 9
cts 11
cp 0.8182
crap 6.2163
rs 9.2222
c 0
b 0
f 0
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 $tempDocumentRelations = [];
27
    private $tempDocumentContentTypes = '';
28
    private $tempDocumentNewImages = [];
29
30
    /**
31
     * Construct an instance of Document
32
     *
33
     * @param string $path - path to the document
34
     *
35
     * @throws Exception
36
     */
37
    public function __construct(string $path)
38
    {
39 11
        if (file_exists($path)) {
40
            $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 11
        } else {
45 11
            throw new Exception("The template " . $path . " was not found!");
46
        }
47
    }
48
49 11
    /**
50
     * Extract (unzip) document contents
51
     */
52
    private function extract(): void
53
    {
54 11
        if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) {
55
            $this->rrmdir($this->tmpDir);
56 11
        }
57
58
        mkdir($this->tmpDir);
59
60 11
        $this->zipClass->open($this->path);
61
        $this->zipClass->extractTo($this->tmpDir);
62 11
63 11
        $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName());
64
65 11
        $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName());
66 11
67
        //$this->zipClass->close();
68
69
        $this->document = file_get_contents($this->tmpDir . "/word/document.xml");
70 11
    }
71 11
72
    /**
73
     * Get document main part
74
     *
75
     * @return string
76 11
     */
77
    public function getDocumentMainPart(): string
78 11
    {
79
        return $this->tempDocumentMainPart;
80
    }
81
82 11
    /**
83 11
     * Get the name of main part document (method from PhpOffice\PhpWord)
84
     *
85
     * @return string
86
     */
87
    private function getMainPartName(): string
88
    {
89
        $contentTypes = $this->zipClass->getFromName('[Content_Types].xml');
90 1
91
        $pattern = '~PartName="\/(word\/document.*?\.xml)" ' .
92 1
                   'ContentType="application\/vnd\.openxmlformats-officedocument' .
93
                   '\.wordprocessingml\.document\.main\+xml"~';
94
95
        $matches = [];
96
        preg_match($pattern, $contentTypes, $matches);
97
98
        return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml';
99
    }
100 11
101
    /**
102 11
     * @return string
103
     */
104
    private function getDocumentContentTypesName(): string
105
    {
106 11
        return '[Content_Types].xml';
107
    }
108 11
109 11
    /**
110
     * Read document part (method from PhpOffice\PhpWord)
111 11
     *
112
     * @param string $fileName
113
     *
114
     * @return string
115
     */
116
    private function readPartWithRels(string $fileName): string
117 11
    {
118
        $relsFileName = $this->getRelationsName($fileName);
119 11
        $partRelations = $this->zipClass->getFromName($relsFileName);
120
        if ($partRelations !== false) {
121
            $this->tempDocumentRelations[$fileName] = $partRelations;
122
        }
123
124
        return $this->fixBrokenMacros($this->zipClass->getFromName($fileName));
125
    }
126
127
    /**
128
     * Get the name of the relations file for document part (method from PhpOffice\PhpWord)
129 11
     *
130
     * @param string $documentPartName
131 11
     *
132 11
     * @return string
133 11
     */
134 11
    private function getRelationsName(string $documentPartName): string
135
    {
136
        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

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

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

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