Passed
Push — master ( adef13...679987 )
by Bingo
02:50
created

DocxDocument::readPartWithRels()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpDocxTemplate;
4
5
use DOMDocument;
6
use DOMElement;
7
use Exception;
8
use 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
        //preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
232 1
        preg_match_all('/\{\{(.*?)\}\}/i', $documentPartXML, $matches);
233
234 1
        return $matches[1];
235
    }
236
237 1
    private function getImageArgs(string $varNameWithArgs): array
238
    {
239 1
        $varElements = explode(':', $varNameWithArgs);
240 1
        array_shift($varElements); // first element is name of variable => remove it
241
242 1
        $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 1
        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 1
        return $varInlineArgs;
271
    }
272
273
    /**
274
     * @param mixed $replaceImage
275
     * @param array $varInlineArgs
276
     *
277
     * @return array
278
     */
279 1
    private function prepareImageAttrs($replaceImage, array $varInlineArgs): array
280
    {
281
        // get image path and size
282 1
        $width = null;
283 1
        $height = null;
284 1
        $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 1
        if (is_callable($replaceImage)) {
289
            $replaceImage = $replaceImage();
290
        }
291
292 1
        if (is_array($replaceImage) && isset($replaceImage['path'])) {
293 1
            $imgPath = $replaceImage['path'];
294 1
            if (isset($replaceImage['width'])) {
295 1
                $width = $replaceImage['width'];
296
            }
297 1
            if (isset($replaceImage['height'])) {
298 1
                $height = $replaceImage['height'];
299
            }
300 1
            if (isset($replaceImage['ratio'])) {
301 1
                $ratio = $replaceImage['ratio'];
302
            }
303
        } else {
304
            $imgPath = $replaceImage;
305
        }
306
307 1
        $width = $this->chooseImageDimension($width, isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115);
308 1
        $height = $this->chooseImageDimension($height, isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70);
309
310 1
        $imageData = @getimagesize($imgPath);
311 1
        if (!is_array($imageData)) {
312
            throw new Exception(sprintf('Invalid image: %s', $imgPath));
313
        }
314 1
        list($actualWidth, $actualHeight, $imageType) = $imageData;
315
316
        // fix aspect ratio (by default)
317 1
        if (is_null($ratio) && isset($varInlineArgs['ratio'])) {
318
            $ratio = $varInlineArgs['ratio'];
319
        }
320 1
        if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) {
321 1
            $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight);
322
        }
323
324
        $imageAttrs = array(
325 1
            'src'    => $imgPath,
326 1
            'mime'   => image_type_to_mime_type($imageType),
327 1
            'width'  => $width,
328 1
            'height' => $height,
329
        );
330
331 1
        return $imageAttrs;
332
    }
333
334
    /**
335
     * @param mixed $width
336
     * @param mixed $height
337
     * @param int $actualWidth
338
     * @param int $actualHeight
339
     */
340 1
    private function fixImageWidthHeightRatio(&$width, &$height, int $actualWidth, int $actualHeight): void
341
    {
342 1
        $imageRatio = $actualWidth / $actualHeight;
343
344 1
        if (($width === '') && ($height === '')) { // defined size are empty
345
            $width = $actualWidth . 'px';
346
            $height = $actualHeight . 'px';
347 1
        } 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 1
        } 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 1
            $widthMatches = array();
361 1
            preg_match("/\d([a-z%]+)$/", $width, $widthMatches);
362 1
            $heightMatches = array();
363 1
            preg_match("/\d([a-z%]+)$/", $height, $heightMatches);
364
            // try to fix only if dimensions are same
365 1
            if ($widthMatches[1] == $heightMatches[1]) {
366 1
                $dimention = $widthMatches[1];
367 1
                $widthFloat = (float) $width;
368 1
                $heightFloat = (float) $height;
369 1
                $definedRatio = $widthFloat / $heightFloat;
370
371 1
                if ($imageRatio > $definedRatio) { // image wider than defined box
372
                    $height = ($widthFloat / $imageRatio) . $dimention;
373 1
                } elseif ($imageRatio < $definedRatio) { // image higher than defined box
374
                    $width = ($heightFloat * $imageRatio) . $dimention;
375
                }
376
            }
377
        }
378 1
    }
379
380 1
    private function chooseImageDimension(?int $baseValue, ?int $inlineValue, int $defaultValue): string
381
    {
382 1
        $value = $baseValue;
383 1
        if (is_null($value) && isset($inlineValue)) {
384
            $value = $inlineValue;
385
        }
386 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

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

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

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