• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PHPWord / 12909093108

22 Jan 2025 01:18PM UTC coverage: 96.921% (-0.03%) from 96.947%
12909093108

Pull #2697

github

web-flow
Merge 9f75457b3 into 136f5499f
Pull Request #2697: Writer Word2007: Support for padding in Table Cell

73 of 79 new or added lines in 3 files covered. (92.41%)

3 existing lines in 1 file now uncovered.

11993 of 12374 relevant lines covered (96.92%)

34.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.2
/src/PhpWord/Reader/Word2007/AbstractPart.php
1
<?php
2

3
/**
4
 * This file is part of PHPWord - A pure PHP library for reading and writing
5
 * word processing documents.
6
 *
7
 * PHPWord is free software distributed under the terms of the GNU Lesser
8
 * General Public License version 3 as published by the Free Software Foundation.
9
 *
10
 * For the full copyright and license information, please read the LICENSE
11
 * file that was distributed with this source code. For the full list of
12
 * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
13
 *
14
 * @see         https://github.com/PHPOffice/PHPWord
15
 *
16
 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
17
 */
18

19
namespace PhpOffice\PhpWord\Reader\Word2007;
20

21
use DateTime;
22
use DOMElement;
23
use InvalidArgumentException;
24
use PhpOffice\Math\Reader\OfficeMathML;
25
use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType;
26
use PhpOffice\PhpWord\Element\AbstractContainer;
27
use PhpOffice\PhpWord\Element\AbstractElement;
28
use PhpOffice\PhpWord\Element\FormField;
29
use PhpOffice\PhpWord\Element\TextRun;
30
use PhpOffice\PhpWord\Element\TrackChange;
31
use PhpOffice\PhpWord\PhpWord;
32
use PhpOffice\PhpWord\Shared\XMLReader;
33

34
/**
35
 * Abstract part reader.
36
 *
37
 * This class is inherited by ODText reader
38
 *
39
 * @since 0.10.0
40
 */
41
abstract class AbstractPart
42
{
43
    /**
44
     * Conversion method.
45
     *
46
     * @const int
47
     */
48
    const READ_VALUE = 'attributeValue';            // Read attribute value
49
    const READ_EQUAL = 'attributeEquals';           // Read `true` when attribute value equals specified value
50
    const READ_TRUE = 'attributeTrue';              // Read `true` when element exists
51
    const READ_FALSE = 'attributeFalse';            // Read `false` when element exists
52
    const READ_SIZE = 'attributeMultiplyByTwo';     // Read special attribute value for Font::$size
53

54
    /**
55
     * Document file.
56
     *
57
     * @var string
58
     */
59
    protected $docFile;
60

61
    /**
62
     * XML file.
63
     *
64
     * @var string
65
     */
66
    protected $xmlFile;
67

68
    /**
69
     * Part relationships.
70
     *
71
     * @var array
72
     */
73
    protected $rels = [];
74

75
    /**
76
     * Comment references.
77
     *
78
     * @var array<string, array<string, AbstractElement>>
79
     */
80
    protected $commentRefs = [];
81

82
    /**
83
     * Image Loading.
84
     *
85
     * @var bool
86
     */
87
    protected $imageLoading = true;
88

89
    /**
90
     * Read part.
91
     */
92
    abstract public function read(PhpWord $phpWord);
93

94
    /**
95
     * Create new instance.
96
     *
97
     * @param string $docFile
98
     * @param string $xmlFile
99
     */
100
    public function __construct($docFile, $xmlFile)
101
    {
102
        $this->docFile = $docFile;
43✔
103
        $this->xmlFile = $xmlFile;
43✔
104
    }
105

106
    /**
107
     * Set relationships.
108
     *
109
     * @param array $value
110
     */
111
    public function setRels($value): void
112
    {
113
        $this->rels = $value;
13✔
114
    }
115

116
    public function setImageLoading(bool $value): self
117
    {
118
        $this->imageLoading = $value;
9✔
119

120
        return $this;
9✔
121
    }
122

123
    public function hasImageLoading(): bool
124
    {
125
        return $this->imageLoading;
6✔
126
    }
127

128
    /**
129
     * Get comment references.
130
     *
131
     * @return array<string, array<string, null|AbstractElement>>
132
     */
133
    public function getCommentReferences(): array
134
    {
135
        return $this->commentRefs;
9✔
136
    }
137

138
    /**
139
     * Set comment references.
140
     *
141
     * @param array<string, array<string, null|AbstractElement>> $commentRefs
142
     */
143
    public function setCommentReferences(array $commentRefs): self
144
    {
145
        $this->commentRefs = $commentRefs;
9✔
146

147
        return $this;
9✔
148
    }
149

150
    /**
151
     * Set comment reference.
152
     */
153
    private function setCommentReference(string $type, string $id, AbstractElement $element): self
154
    {
155
        if (!in_array($type, ['start', 'end'])) {
1✔
156
            throw new InvalidArgumentException('Type must be "start" or "end"');
×
157
        }
158

159
        if (!array_key_exists($id, $this->commentRefs)) {
1✔
160
            $this->commentRefs[$id] = [
1✔
161
                'start' => null,
1✔
162
                'end' => null,
1✔
163
            ];
1✔
164
        }
165
        $this->commentRefs[$id][$type] = $element;
1✔
166

167
        return $this;
1✔
168
    }
169

170
    /**
171
     * Get comment reference.
172
     *
173
     * @return array<string, null|AbstractElement>
174
     */
175
    protected function getCommentReference(string $id): array
176
    {
177
        if (!array_key_exists($id, $this->commentRefs)) {
1✔
178
            throw new InvalidArgumentException(sprintf('Comment with id %s isn\'t referenced in document', $id));
×
179
        }
180

181
        return $this->commentRefs[$id];
1✔
182
    }
183

184
    /**
185
     * Read w:p.
186
     *
187
     * @param AbstractContainer $parent
188
     * @param string $docPart
189
     *
190
     * @todo Get font style for preserve text
191
     */
192
    protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void
193
    {
194
        // Paragraph style
195
        $paragraphStyle = $xmlReader->elementExists('w:pPr', $domNode) ? $this->readParagraphStyle($xmlReader, $domNode) : null;
29✔
196

197
        if ($xmlReader->elementExists('w:r/w:fldChar/w:ffData', $domNode)) {
29✔
198
            // FormField
199
            $partOfFormField = false;
3✔
200
            $formNodes = [];
3✔
201
            $formType = null;
3✔
202
            $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag', $domNode);
3✔
203
            if ($textRunContainers > 0) {
3✔
204
                $nodes = $xmlReader->getElements('*', $domNode);
3✔
205
                $paragraph = $parent->addTextRun($paragraphStyle);
3✔
206
                foreach ($nodes as $node) {
3✔
207
                    if ($xmlReader->elementExists('w:fldChar/w:ffData', $node)) {
3✔
208
                        $partOfFormField = true;
3✔
209
                        $formNodes[] = $node;
3✔
210
                        if ($xmlReader->elementExists('w:fldChar/w:ffData/w:ddList', $node)) {
3✔
211
                            $formType = 'dropdown';
1✔
212
                        } elseif ($xmlReader->elementExists('w:fldChar/w:ffData/w:textInput', $node)) {
2✔
213
                            $formType = 'textinput';
1✔
214
                        } elseif ($xmlReader->elementExists('w:fldChar/w:ffData/w:checkBox', $node)) {
1✔
215
                            $formType = 'checkbox';
3✔
216
                        }
217
                    } elseif ($partOfFormField &&
3✔
218
                        $xmlReader->elementExists('w:fldChar', $node) &&
3✔
219
                        'end' == $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar')
3✔
220
                    ) {
221
                        $formNodes[] = $node;
3✔
222
                        $partOfFormField = false;
3✔
223
                        // Process the form fields
224
                        $this->readFormField($xmlReader, $formNodes, $paragraph, $paragraphStyle, $formType);
3✔
225
                    } elseif ($partOfFormField) {
3✔
226
                        $formNodes[] = $node;
3✔
227
                    } else {
228
                        // normal runs
229
                        $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle);
3✔
230
                    }
231
                }
232
            }
233
        } elseif ($xmlReader->elementExists('w:r/w:instrText', $domNode)) {
26✔
234
            // PreserveText
235
            $ignoreText = false;
2✔
236
            $textContent = '';
2✔
237
            $fontStyle = $this->readFontStyle($xmlReader, $domNode);
2✔
238
            $nodes = $xmlReader->getElements('w:r', $domNode);
2✔
239
            foreach ($nodes as $node) {
2✔
240
                if ($xmlReader->elementExists('w:lastRenderedPageBreak', $node)) {
2✔
241
                    $parent->addPageBreak();
×
242
                }
243
                $instrText = $xmlReader->getValue('w:instrText', $node);
2✔
244
                if (null !== $instrText) {
2✔
245
                    $textContent .= '{' . $instrText . '}';
2✔
246
                } else {
247
                    if ($xmlReader->elementExists('w:fldChar', $node)) {
2✔
248
                        $fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar');
2✔
249
                        if ('begin' == $fldCharType) {
2✔
250
                            $ignoreText = true;
2✔
251
                        } elseif ('end' == $fldCharType) {
2✔
252
                            $ignoreText = false;
2✔
253
                        }
254
                    }
255
                    if (false === $ignoreText) {
2✔
256
                        $textContent .= $xmlReader->getValue('w:t', $node);
2✔
257
                    }
258
                }
259
            }
260
            $parent->addPreserveText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'), $fontStyle, $paragraphStyle);
2✔
261

262
            return;
2✔
263
        }
264

265
        // Formula
266
        $xmlReader->registerNamespace('m', 'http://schemas.openxmlformats.org/officeDocument/2006/math');
29✔
267
        if ($xmlReader->elementExists('m:oMath', $domNode)) {
29✔
268
            $mathElement = $xmlReader->getElement('m:oMath', $domNode);
1✔
269
            $mathXML = $mathElement->ownerDocument->saveXML($mathElement);
1✔
270
            if (is_string($mathXML)) {
1✔
271
                $reader = new OfficeMathML();
1✔
272
                $math = $reader->read($mathXML);
1✔
273

274
                $parent->addFormula($math);
1✔
275
            }
276

277
            return;
1✔
278
        }
279

280
        // List item
281
        if ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) {
28✔
282
            $numId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:numId');
3✔
283
            $levelId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:ilvl');
3✔
284
            $nodes = $xmlReader->getElements('*', $domNode);
3✔
285

286
            $listItemRun = $parent->addListItemRun($levelId, "PHPWordList{$numId}", $paragraphStyle);
3✔
287

288
            foreach ($nodes as $node) {
3✔
289
                $this->readRun($xmlReader, $node, $listItemRun, $docPart, $paragraphStyle);
3✔
290
            }
291

292
            return;
3✔
293
        }
294

295
        // Heading or Title
296
        $headingDepth = $xmlReader->elementExists('w:pPr', $domNode) ? $this->getHeadingDepth($paragraphStyle) : null;
27✔
297
        if ($headingDepth !== null) {
27✔
298
            $textContent = null;
6✔
299
            $nodes = $xmlReader->getElements('w:r|w:hyperlink', $domNode);
6✔
300
            if ($nodes->length === 1) {
6✔
301
                $textContent = htmlspecialchars($xmlReader->getValue('w:t', $nodes->item(0)), ENT_QUOTES, 'UTF-8');
2✔
302
            } else {
303
                $textContent = new TextRun($paragraphStyle);
6✔
304
                foreach ($nodes as $node) {
6✔
305
                    $this->readRun($xmlReader, $node, $textContent, $docPart, $paragraphStyle);
6✔
306
                }
307
            }
308
            $parent->addTitle($textContent, $headingDepth);
6✔
309

310
            return;
6✔
311
        }
312

313
        // Text and TextRun
314
        $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode);
25✔
315
        if (0 === $textRunContainers) {
25✔
316
            $parent->addTextBreak(1, $paragraphStyle);
7✔
317
        } else {
318
            $nodes = $xmlReader->getElements('*', $domNode);
24✔
319
            $paragraph = $parent->addTextRun($paragraphStyle);
24✔
320
            foreach ($nodes as $node) {
24✔
321
                $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle);
24✔
322
            }
323
        }
324
    }
325

326
    /**
327
     * @param DOMElement[] $domNodes
328
     * @param AbstractContainer $parent
329
     * @param mixed $paragraphStyle
330
     * @param string $formType
331
     */
332
    private function readFormField(XMLReader $xmlReader, array $domNodes, $parent, $paragraphStyle, $formType): void
333
    {
334
        if (!in_array($formType, ['textinput', 'checkbox', 'dropdown'])) {
3✔
335
            return;
×
336
        }
337

338
        $formField = $parent->addFormField($formType, null, $paragraphStyle);
3✔
339
        $ffData = $xmlReader->getElement('w:fldChar/w:ffData', $domNodes[0]);
3✔
340

341
        foreach ($xmlReader->getElements('*', $ffData) as $node) {
3✔
342
            /** @var DOMElement $node */
343
            switch ($node->localName) {
3✔
344
                case 'name':
3✔
345
                    $formField->setName($node->getAttribute('w:val'));
3✔
346

347
                    break;
3✔
348
                case 'ddList':
3✔
349
                    $listEntries = [];
1✔
350
                    foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
1✔
351
                        switch ($ddListNode->localName) {
1✔
352
                            case 'result':
1✔
353
                                $formField->setValue($xmlReader->getAttribute('w:val', $ddListNode));
1✔
354

355
                                break;
1✔
356
                            case 'default':
1✔
357
                                $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
×
358

359
                                break;
×
360
                            case 'listEntry':
1✔
361
                                $listEntries[] = $xmlReader->getAttribute('w:val', $ddListNode);
1✔
362

363
                                break;
1✔
364
                        }
365
                    }
366
                    $formField->setEntries($listEntries);
1✔
367
                    if (null !== $formField->getValue()) {
1✔
368
                        $formField->setText($listEntries[$formField->getValue()]);
1✔
369
                    }
370

371
                    break;
1✔
372
                case 'textInput':
3✔
373
                    foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
1✔
374
                        switch ($ddListNode->localName) {
1✔
375
                            case 'default':
1✔
376
                                $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
1✔
377

378
                                break;
1✔
379
                            case 'format':
1✔
380
                            case 'maxLength':
1✔
381
                                break;
1✔
382
                        }
383
                    }
384

385
                    break;
1✔
386
                case 'checkBox':
3✔
387
                    foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
1✔
388
                        switch ($ddListNode->localName) {
1✔
389
                            case 'default':
1✔
390
                                $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
1✔
391

392
                                break;
1✔
393
                            case 'checked':
1✔
394
                                $formField->setValue($xmlReader->getAttribute('w:val', $ddListNode));
1✔
395

396
                                break;
1✔
397
                            case 'size':
1✔
398
                            case 'sizeAuto':
1✔
399
                                break;
1✔
400
                        }
401
                    }
402

403
                    break;
1✔
404
            }
405
        }
406

407
        if ('textinput' == $formType) {
3✔
408
            $ignoreText = true;
1✔
409
            $textContent = '';
1✔
410
            foreach ($domNodes as $node) {
1✔
411
                if ($xmlReader->elementExists('w:fldChar', $node)) {
1✔
412
                    $fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar');
1✔
413
                    if ('separate' == $fldCharType) {
1✔
414
                        $ignoreText = false;
1✔
415
                    } elseif ('end' == $fldCharType) {
1✔
416
                        $ignoreText = true;
1✔
417
                    }
418
                }
419

420
                if (false === $ignoreText) {
1✔
421
                    $textContent .= $xmlReader->getValue('w:t', $node);
1✔
422
                }
423
            }
424
            $formField->setValue(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'));
1✔
425
            $formField->setText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'));
1✔
426
        }
427
    }
428

429
    /**
430
     * Returns the depth of the Heading, returns 0 for a Title.
431
     *
432
     * @return null|number
433
     */
434
    private function getHeadingDepth(?array $paragraphStyle = null)
435
    {
436
        if (is_array($paragraphStyle) && isset($paragraphStyle['styleName'])) {
15✔
437
            if ('Title' === $paragraphStyle['styleName']) {
8✔
438
                return 0;
4✔
439
            }
440

441
            $headingMatches = [];
4✔
442
            preg_match('/Heading(\d)/', $paragraphStyle['styleName'], $headingMatches);
4✔
443
            if (!empty($headingMatches)) {
4✔
444
                return $headingMatches[1];
2✔
445
            }
446
        }
447

448
        return null;
13✔
449
    }
450

451
    /**
452
     * Read w:r.
453
     *
454
     * @param AbstractContainer $parent
455
     * @param string $docPart
456
     * @param mixed $paragraphStyle
457
     *
458
     * @todo Footnote paragraph style
459
     */
460
    protected function readRun(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart, $paragraphStyle = null): void
461
    {
462
        if (in_array($domNode->nodeName, ['w:ins', 'w:del', 'w:smartTag', 'w:hyperlink', 'w:commentReference'])) {
27✔
463
            $nodes = $xmlReader->getElements('*', $domNode);
5✔
464
            foreach ($nodes as $node) {
5✔
465
                $this->readRun($xmlReader, $node, $parent, $docPart, $paragraphStyle);
5✔
466
            }
467
        } elseif ($domNode->nodeName == 'w:r') {
27✔
468
            $fontStyle = $this->readFontStyle($xmlReader, $domNode);
27✔
469
            $nodes = $xmlReader->getElements('*', $domNode);
27✔
470
            foreach ($nodes as $node) {
27✔
471
                $this->readRunChild($xmlReader, $node, $parent, $docPart, $paragraphStyle, $fontStyle);
27✔
472
            }
473
        }
474

475
        if ($xmlReader->elementExists('.//*["commentReference"=local-name()]', $domNode)) {
27✔
476
            $node = iterator_to_array($xmlReader->getElements('.//*["commentReference"=local-name()]', $domNode))[0];
1✔
477
            $attributeIdentifier = $node->attributes->getNamedItem('id');
1✔
478
            if ($attributeIdentifier) {
1✔
479
                $id = $attributeIdentifier->nodeValue;
1✔
480

481
                $this->setCommentReference('start', $id, $parent->getElement($parent->countElements() - 1));
1✔
482
                $this->setCommentReference('end', $id, $parent->getElement($parent->countElements() - 1));
1✔
483
            }
484
        }
485
    }
486

487
    /**
488
     * Parses nodes under w:r.
489
     *
490
     * @param string $docPart
491
     * @param mixed $paragraphStyle
492
     * @param mixed $fontStyle
493
     */
494
    protected function readRunChild(XMLReader $xmlReader, DOMElement $node, AbstractContainer $parent, $docPart, $paragraphStyle = null, $fontStyle = null): void
495
    {
496
        $runParent = $node->parentNode->parentNode;
27✔
497
        if ($node->nodeName == 'w:footnoteReference') {
27✔
498
            // Footnote
499
            $wId = $xmlReader->getAttribute('w:id', $node);
3✔
500
            $footnote = $parent->addFootnote();
3✔
501
            $footnote->setRelationId($wId);
3✔
502
        } elseif ($node->nodeName == 'w:endnoteReference') {
27✔
503
            // Endnote
504
            $wId = $xmlReader->getAttribute('w:id', $node);
3✔
505
            $endnote = $parent->addEndnote();
3✔
506
            $endnote->setRelationId($wId);
3✔
507
        } elseif ($node->nodeName == 'w:pict') {
27✔
508
            // Image
509
            $rId = $xmlReader->getAttribute('r:id', $node, 'v:shape/v:imagedata');
1✔
510
            $target = $this->getMediaTarget($docPart, $rId);
1✔
511
            if ($this->hasImageLoading() && null !== $target) {
1✔
512
                if ('External' == $this->getTargetMode($docPart, $rId)) {
1✔
513
                    $imageSource = $target;
×
514
                } else {
515
                    $imageSource = "zip://{$this->docFile}#{$target}";
1✔
516
                }
517
                $parent->addImage($imageSource);
1✔
518
            }
519
        } elseif ($node->nodeName == 'w:drawing') {
27✔
520
            // Office 2011 Image
521
            $xmlReader->registerNamespace('wp', 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing');
5✔
522
            $xmlReader->registerNamespace('r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
5✔
523
            $xmlReader->registerNamespace('pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture');
5✔
524
            $xmlReader->registerNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
5✔
525

526
            $name = $xmlReader->getAttribute('name', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
5✔
527
            $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
5✔
528
            if ($name === null && $embedId === null) { // some Converters puts images on a different path
5✔
529
                $name = $xmlReader->getAttribute('name', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
×
530
                $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
×
531
            }
532
            $target = $this->getMediaTarget($docPart, $embedId);
5✔
533
            if ($this->hasImageLoading() && null !== $target) {
5✔
534
                $imageSource = "zip://{$this->docFile}#{$target}";
3✔
535
                $parent->addImage($imageSource, null, false, $name);
5✔
536
            }
537
        } elseif ($node->nodeName == 'w:object') {
26✔
538
            // Object
539
            $rId = $xmlReader->getAttribute('r:id', $node, 'o:OLEObject');
2✔
540
            // $rIdIcon = $xmlReader->getAttribute('r:id', $domNode, 'w:object/v:shape/v:imagedata');
541
            $target = $this->getMediaTarget($docPart, $rId);
2✔
542
            if (null !== $target) {
2✔
543
                $textContent = "&lt;Object: {$target}>";
2✔
544
                $parent->addText($textContent, $fontStyle, $paragraphStyle);
2✔
545
            }
546
        } elseif ($node->nodeName == 'w:br') {
26✔
547
            $parent->addTextBreak();
3✔
548
        } elseif ($node->nodeName == 'w:tab') {
26✔
549
            $parent->addText("\t");
1✔
550
        } elseif ($node->nodeName == 'mc:AlternateContent') {
26✔
551
            if ($node->hasChildNodes()) {
1✔
552
                // Get fallback instead of mc:Choice to make sure it is compatible
553
                $fallbackElements = $node->getElementsByTagName('Fallback');
1✔
554

555
                if ($fallbackElements->length) {
1✔
556
                    $fallback = $fallbackElements->item(0);
1✔
557
                    // TextRun
558
                    $textContent = htmlspecialchars($fallback->nodeValue, ENT_QUOTES, 'UTF-8');
1✔
559

560
                    $parent->addText($textContent, $fontStyle, $paragraphStyle);
1✔
561
                }
562
            }
563
        } elseif ($node->nodeName == 'w:t' || $node->nodeName == 'w:delText') {
25✔
564
            // TextRun
565
            $textContent = htmlspecialchars($xmlReader->getValue('.', $node), ENT_QUOTES, 'UTF-8');
24✔
566

567
            if ($runParent->nodeName == 'w:hyperlink') {
24✔
568
                $rId = $xmlReader->getAttribute('r:id', $runParent);
2✔
569
                $target = $this->getMediaTarget($docPart, $rId);
2✔
570
                if (null !== $target) {
2✔
571
                    $parent->addLink($target, $textContent, $fontStyle, $paragraphStyle);
2✔
572
                } else {
573
                    $parent->addText($textContent, $fontStyle, $paragraphStyle);
2✔
574
                }
575
            } else {
576
                /** @var AbstractElement $element */
577
                $element = $parent->addText($textContent, $fontStyle, $paragraphStyle);
24✔
578
                if (in_array($runParent->nodeName, ['w:ins', 'w:del'])) {
24✔
579
                    $type = ($runParent->nodeName == 'w:del') ? TrackChange::DELETED : TrackChange::INSERTED;
3✔
580
                    $author = $runParent->getAttribute('w:author');
3✔
581
                    $date = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $runParent->getAttribute('w:date'));
3✔
582
                    $date = $date instanceof DateTime ? $date : null;
3✔
583
                    $element->setChangeInfo($type, $author, $date);
24✔
584
                }
585
            }
586
        } elseif ($node->nodeName == 'w:softHyphen') {
17✔
587
            $element = $parent->addText("\u{200c}", $fontStyle, $paragraphStyle);
×
588
        }
589
    }
590

591
    /**
592
     * Read w:tbl.
593
     *
594
     * @param mixed $parent
595
     * @param string $docPart
596
     */
597
    protected function readTable(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void
598
    {
599
        // Table style
600
        $tblStyle = null;
11✔
601
        if ($xmlReader->elementExists('w:tblPr', $domNode)) {
11✔
602
            $tblStyle = $this->readTableStyle($xmlReader, $domNode);
7✔
603
        }
604

605
        /** @var \PhpOffice\PhpWord\Element\Table $table Type hint */
606
        $table = $parent->addTable($tblStyle);
11✔
607
        $tblNodes = $xmlReader->getElements('*', $domNode);
11✔
608
        foreach ($tblNodes as $tblNode) {
11✔
609
            if ('w:tblGrid' == $tblNode->nodeName) { // Column
11✔
610
                // @todo Do something with table columns
611
            } elseif ('w:tr' == $tblNode->nodeName) { // Row
11✔
612
                $rowHeight = $xmlReader->getAttribute('w:val', $tblNode, 'w:trPr/w:trHeight');
6✔
613
                $rowHRule = $xmlReader->getAttribute('w:hRule', $tblNode, 'w:trPr/w:trHeight');
6✔
614
                $rowHRule = $rowHRule == 'exact';
6✔
615
                $rowStyle = [
6✔
616
                    'tblHeader' => $xmlReader->elementExists('w:trPr/w:tblHeader', $tblNode),
6✔
617
                    'cantSplit' => $xmlReader->elementExists('w:trPr/w:cantSplit', $tblNode),
6✔
618
                    'exactHeight' => $rowHRule,
6✔
619
                ];
6✔
620

621
                $row = $table->addRow($rowHeight, $rowStyle);
6✔
622
                $rowNodes = $xmlReader->getElements('*', $tblNode);
6✔
623
                foreach ($rowNodes as $rowNode) {
6✔
624
                    if ('w:trPr' == $rowNode->nodeName) { // Row style
6✔
625
                        // @todo Do something with row style
626
                    } elseif ('w:tc' == $rowNode->nodeName) { // Cell
6✔
627
                        $cellWidth = $xmlReader->getAttribute('w:w', $rowNode, 'w:tcPr/w:tcW');
6✔
628
                        $cellStyle = null;
6✔
629
                        if ($xmlReader->elementExists('w:tcPr', $rowNode)) {
6✔
630
                            $cellStyle = $this->readCellStyle($xmlReader, $rowNode);
5✔
631
                        }
632

633
                        $cell = $row->addCell($cellWidth, $cellStyle);
6✔
634
                        $cellNodes = $xmlReader->getElements('*', $rowNode);
6✔
635
                        foreach ($cellNodes as $cellNode) {
6✔
636
                            if ('w:p' == $cellNode->nodeName) { // Paragraph
6✔
637
                                $this->readParagraph($xmlReader, $cellNode, $cell, $docPart);
3✔
638
                            } elseif ($cellNode->nodeName == 'w:tbl') { // Table
6✔
639
                                $this->readTable($xmlReader, $cellNode, $cell, $docPart);
1✔
640
                            }
641
                        }
642
                    }
643
                }
644
            }
645
        }
646
    }
647

648
    /**
649
     * Read w:pPr.
650
     *
651
     * @return null|array
652
     */
653
    protected function readParagraphStyle(XMLReader $xmlReader, DOMElement $domNode)
654
    {
655
        if (!$xmlReader->elementExists('w:pPr', $domNode)) {
18✔
656
            return null;
7✔
657
        }
658

659
        $styleNode = $xmlReader->getElement('w:pPr', $domNode);
18✔
660
        $styleDefs = [
18✔
661
            'styleName' => [self::READ_VALUE, ['w:pStyle', 'w:name']],
18✔
662
            'alignment' => [self::READ_VALUE, 'w:jc'],
18✔
663
            'basedOn' => [self::READ_VALUE, 'w:basedOn'],
18✔
664
            'next' => [self::READ_VALUE, 'w:next'],
18✔
665
            'indentLeft' => [self::READ_VALUE, 'w:ind', 'w:left'],
18✔
666
            'indentRight' => [self::READ_VALUE, 'w:ind', 'w:right'],
18✔
667
            'indentHanging' => [self::READ_VALUE, 'w:ind', 'w:hanging'],
18✔
668
            'indentFirstLine' => [self::READ_VALUE, 'w:ind', 'w:firstLine'],
18✔
669
            'spaceAfter' => [self::READ_VALUE, 'w:spacing', 'w:after'],
18✔
670
            'spaceBefore' => [self::READ_VALUE, 'w:spacing', 'w:before'],
18✔
671
            'widowControl' => [self::READ_FALSE, 'w:widowControl'],
18✔
672
            'keepNext' => [self::READ_TRUE,  'w:keepNext'],
18✔
673
            'keepLines' => [self::READ_TRUE,  'w:keepLines'],
18✔
674
            'pageBreakBefore' => [self::READ_TRUE,  'w:pageBreakBefore'],
18✔
675
            'contextualSpacing' => [self::READ_TRUE,  'w:contextualSpacing'],
18✔
676
            'bidi' => [self::READ_TRUE,  'w:bidi'],
18✔
677
            'suppressAutoHyphens' => [self::READ_TRUE,  'w:suppressAutoHyphens'],
18✔
678
            'borderTopStyle' => [self::READ_VALUE, 'w:pBdr/w:top'],
18✔
679
            'borderTopColor' => [self::READ_VALUE, 'w:pBdr/w:top', 'w:color'],
18✔
680
            'borderTopSize' => [self::READ_VALUE, 'w:pBdr/w:top', 'w:sz'],
18✔
681
            'borderRightStyle' => [self::READ_VALUE, 'w:pBdr/w:right'],
18✔
682
            'borderRightColor' => [self::READ_VALUE, 'w:pBdr/w:right', 'w:color'],
18✔
683
            'borderRightSize' => [self::READ_VALUE, 'w:pBdr/w:right', 'w:sz'],
18✔
684
            'borderBottomStyle' => [self::READ_VALUE, 'w:pBdr/w:bottom'],
18✔
685
            'borderBottomColor' => [self::READ_VALUE, 'w:pBdr/w:bottom', 'w:color'],
18✔
686
            'borderBottomSize' => [self::READ_VALUE, 'w:pBdr/w:bottom', 'w:sz'],
18✔
687
            'borderLeftStyle' => [self::READ_VALUE, 'w:pBdr/w:left'],
18✔
688
            'borderLeftColor' => [self::READ_VALUE, 'w:pBdr/w:left', 'w:color'],
18✔
689
            'borderLeftSize' => [self::READ_VALUE, 'w:pBdr/w:left', 'w:sz'],
18✔
690
        ];
18✔
691

692
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
18✔
693
    }
694

695
    /**
696
     * Read w:rPr.
697
     *
698
     * @return null|array
699
     */
700
    protected function readFontStyle(XMLReader $xmlReader, DOMElement $domNode)
701
    {
702
        if (null === $domNode) {
29✔
UNCOV
703
            return null;
×
704
        }
705
        // Hyperlink has an extra w:r child
706
        if ('w:hyperlink' == $domNode->nodeName) {
29✔
UNCOV
707
            $domNode = $xmlReader->getElement('w:r', $domNode);
×
708
        }
709
        if (!$xmlReader->elementExists('w:rPr', $domNode)) {
29✔
710
            return null;
24✔
711
        }
712

713
        $styleNode = $xmlReader->getElement('w:rPr', $domNode);
19✔
714
        $styleDefs = [
19✔
715
            'styleName' => [self::READ_VALUE, 'w:rStyle'],
19✔
716
            'name' => [self::READ_VALUE, 'w:rFonts', ['w:ascii', 'w:hAnsi', 'w:eastAsia', 'w:cs']],
19✔
717
            'hint' => [self::READ_VALUE, 'w:rFonts', 'w:hint'],
19✔
718
            'size' => [self::READ_SIZE,  ['w:sz', 'w:szCs']],
19✔
719
            'color' => [self::READ_VALUE, 'w:color'],
19✔
720
            'underline' => [self::READ_VALUE, 'w:u'],
19✔
721
            'bold' => [self::READ_TRUE,  'w:b'],
19✔
722
            'italic' => [self::READ_TRUE,  'w:i'],
19✔
723
            'strikethrough' => [self::READ_TRUE,  'w:strike'],
19✔
724
            'doubleStrikethrough' => [self::READ_TRUE,  'w:dstrike'],
19✔
725
            'smallCaps' => [self::READ_TRUE,  'w:smallCaps'],
19✔
726
            'allCaps' => [self::READ_TRUE,  'w:caps'],
19✔
727
            'superScript' => [self::READ_EQUAL, 'w:vertAlign', 'w:val', 'superscript'],
19✔
728
            'subScript' => [self::READ_EQUAL, 'w:vertAlign', 'w:val', 'subscript'],
19✔
729
            'fgColor' => [self::READ_VALUE, 'w:highlight'],
19✔
730
            'rtl' => [self::READ_TRUE,  'w:rtl'],
19✔
731
            'lang' => [self::READ_VALUE, 'w:lang'],
19✔
732
            'position' => [self::READ_VALUE, 'w:position'],
19✔
733
            'hidden' => [self::READ_TRUE,  'w:vanish'],
19✔
734
        ];
19✔
735

736
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
19✔
737
    }
738

739
    /**
740
     * Read w:tblPr.
741
     *
742
     * @return null|array|string
743
     *
744
     * @todo Capture w:tblStylePr w:type="firstRow"
745
     */
746
    protected function readTableStyle(XMLReader $xmlReader, DOMElement $domNode)
747
    {
748
        $style = null;
12✔
749
        $margins = ['top', 'left', 'bottom', 'right'];
12✔
750
        $borders = array_merge($margins, ['insideH', 'insideV']);
12✔
751

752
        if ($xmlReader->elementExists('w:tblPr', $domNode)) {
12✔
753
            if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) {
12✔
UNCOV
754
                $style = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle');
×
755
            } else {
756
                $styleNode = $xmlReader->getElement('w:tblPr', $domNode);
12✔
757
                $styleDefs = [];
12✔
758
                foreach ($margins as $side) {
12✔
759
                    $ucfSide = ucfirst($side);
12✔
760
                    $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w'];
12✔
761
                }
762
                foreach ($borders as $side) {
12✔
763
                    $ucfSide = ucfirst($side);
12✔
764
                    $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz'];
12✔
765
                    $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color'];
12✔
766
                    $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val'];
12✔
767
                }
768
                $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type'];
12✔
769
                $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual'];
12✔
770
                $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w'];
12✔
771
                $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
12✔
772

773
                $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode);
12✔
774
                if ($tablePositionNode !== null) {
12✔
775
                    $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode);
1✔
776
                }
777

778
                $indentNode = $xmlReader->getElement('w:tblInd', $styleNode);
12✔
779
                if ($indentNode !== null) {
12✔
780
                    $style['indent'] = $this->readTableIndent($xmlReader, $indentNode);
7✔
781
                }
782
            }
783
        }
784

785
        return $style;
12✔
786
    }
787

788
    /**
789
     * Read w:tblpPr.
790
     *
791
     * @return array
792
     */
793
    private function readTablePosition(XMLReader $xmlReader, DOMElement $domNode)
794
    {
795
        $styleDefs = [
1✔
796
            'leftFromText' => [self::READ_VALUE, '.', 'w:leftFromText'],
1✔
797
            'rightFromText' => [self::READ_VALUE, '.', 'w:rightFromText'],
1✔
798
            'topFromText' => [self::READ_VALUE, '.', 'w:topFromText'],
1✔
799
            'bottomFromText' => [self::READ_VALUE, '.', 'w:bottomFromText'],
1✔
800
            'vertAnchor' => [self::READ_VALUE, '.', 'w:vertAnchor'],
1✔
801
            'horzAnchor' => [self::READ_VALUE, '.', 'w:horzAnchor'],
1✔
802
            'tblpXSpec' => [self::READ_VALUE, '.', 'w:tblpXSpec'],
1✔
803
            'tblpX' => [self::READ_VALUE, '.', 'w:tblpX'],
1✔
804
            'tblpYSpec' => [self::READ_VALUE, '.', 'w:tblpYSpec'],
1✔
805
            'tblpY' => [self::READ_VALUE, '.', 'w:tblpY'],
1✔
806
        ];
1✔
807

808
        return $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
1✔
809
    }
810

811
    /**
812
     * Read w:tblInd.
813
     *
814
     * @return TblWidthComplexType
815
     */
816
    private function readTableIndent(XMLReader $xmlReader, DOMElement $domNode)
817
    {
818
        $styleDefs = [
7✔
819
            'value' => [self::READ_VALUE, '.', 'w:w'],
7✔
820
            'type' => [self::READ_VALUE, '.', 'w:type'],
7✔
821
        ];
7✔
822
        $styleDefs = $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
7✔
823

824
        return new TblWidthComplexType((int) $styleDefs['value'], $styleDefs['type']);
7✔
825
    }
826

827
    /**
828
     * Read w:tcPr.
829
     *
830
     * @return null|array
831
     */
832
    private function readCellStyle(XMLReader $xmlReader, DOMElement $domNode)
833
    {
834
        $styleDefs = [
5✔
835
            'valign' => [self::READ_VALUE, 'w:vAlign'],
5✔
836
            'textDirection' => [self::READ_VALUE, 'w:textDirection'],
5✔
837
            'gridSpan' => [self::READ_VALUE, 'w:gridSpan'],
5✔
838
            'vMerge' => [self::READ_VALUE, 'w:vMerge', null, null, 'continue'],
5✔
839
            'bgColor' => [self::READ_VALUE, 'w:shd', 'w:fill'],
5✔
840
            'noWrap' => [self::READ_VALUE, 'w:noWrap', null, null, true],
5✔
841
        ];
5✔
842
        $style = null;
5✔
843

844
        if ($xmlReader->elementExists('w:tcPr', $domNode)) {
5✔
845
            $styleNode = $xmlReader->getElement('w:tcPr', $domNode);
5✔
846

847
            $borders = ['top', 'left', 'bottom', 'right'];
5✔
848
            foreach ($borders as $side) {
5✔
849
                $ucfSide = ucfirst($side);
5✔
850

851
                $styleDefs['border' . $ucfSide . 'Size'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:sz'];
5✔
852
                $styleDefs['border' . $ucfSide . 'Color'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:color'];
5✔
853
                $styleDefs['border' . $ucfSide . 'Style'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:val'];
5✔
854
            }
855

856
            $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
5✔
857
        }
858

859
        return $style;
5✔
860
    }
861

862
    /**
863
     * Returns the first child element found.
864
     *
865
     * @param null|array|string $elements
866
     *
867
     * @return null|string
868
     */
869
    private function findPossibleElement(XMLReader $xmlReader, ?DOMElement $parentNode = null, $elements = null)
870
    {
871
        if (is_array($elements)) {
32✔
872
            //if element is an array, we take the first element that exists in the XML
873
            foreach ($elements as $possibleElement) {
23✔
874
                if ($xmlReader->elementExists($possibleElement, $parentNode)) {
23✔
875
                    return $possibleElement;
12✔
876
                }
877
            }
878
        } else {
879
            return $elements;
32✔
880
        }
881

882
        return null;
23✔
883
    }
884

885
    /**
886
     * Returns the first attribute found.
887
     *
888
     * @param array|string $attributes
889
     *
890
     * @return null|string
891
     */
892
    private function findPossibleAttribute(XMLReader $xmlReader, DOMElement $node, $attributes)
893
    {
894
        //if attribute is an array, we take the first attribute that exists in the XML
895
        if (is_array($attributes)) {
31✔
896
            foreach ($attributes as $possibleAttribute) {
11✔
897
                if ($xmlReader->getAttribute($possibleAttribute, $node)) {
11✔
898
                    return $possibleAttribute;
7✔
899
                }
900
            }
901

902
            return null;
6✔
903
        }
904

905
        return $attributes;
31✔
906
    }
907

908
    /**
909
     * Read style definition.
910
     *
911
     * @param array $styleDefs
912
     *
913
     * @ignoreScrutinizerPatch
914
     *
915
     * @return array
916
     */
917
    protected function readStyleDefs(XMLReader $xmlReader, ?DOMElement $parentNode = null, $styleDefs = [])
918
    {
919
        $styles = [];
32✔
920

921
        foreach ($styleDefs as $styleProp => $styleVal) {
32✔
922
            [$method, $element, $attribute, $expected, $default] = array_pad($styleVal, 5, null);
32✔
923

924
            $element = $this->findPossibleElement($xmlReader, $parentNode, $element);
32✔
925
            if ($element === null) {
32✔
926
                continue;
23✔
927
            }
928

929
            if ($xmlReader->elementExists($element, $parentNode)) {
32✔
930
                $node = $xmlReader->getElement($element, $parentNode);
31✔
931

932
                $attribute = $this->findPossibleAttribute($xmlReader, $node, $attribute);
31✔
933

934
                // Use w:val as default if no attribute assigned
935
                $attribute = ($attribute === null) ? 'w:val' : $attribute;
31✔
936
                $attributeValue = $xmlReader->getAttribute($attribute, $node) ?? $default;
31✔
937

938
                $styleValue = $this->readStyleDef($method, $attributeValue, $expected);
31✔
939
                if ($styleValue !== null) {
31✔
940
                    $styles[$styleProp] = $styleValue;
31✔
941
                }
942
            }
943
        }
944

945
        return $styles;
32✔
946
    }
947

948
    /**
949
     * Return style definition based on conversion method.
950
     *
951
     * @param string $method
952
     *
953
     * @ignoreScrutinizerPatch
954
     *
955
     * @param null|string $attributeValue
956
     * @param mixed $expected
957
     *
958
     * @return mixed
959
     */
960
    private function readStyleDef($method, $attributeValue, $expected)
961
    {
962
        $style = $attributeValue;
31✔
963

964
        if (self::READ_SIZE == $method) {
31✔
965
            $style = $attributeValue / 2;
12✔
966
        } elseif (self::READ_TRUE == $method) {
31✔
967
            $style = $this->isOn($attributeValue);
15✔
968
        } elseif (self::READ_FALSE == $method) {
28✔
969
            $style = !$this->isOn($attributeValue);
4✔
970
        } elseif (self::READ_EQUAL == $method) {
28✔
971
            $style = $attributeValue == $expected;
2✔
972
        }
973

974
        return $style;
31✔
975
    }
976

977
    /**
978
     * Parses the value of the on/off value, null is considered true as it means the w:val attribute was not present.
979
     *
980
     * @see http://www.datypic.com/sc/ooxml/t-w_ST_OnOff.html
981
     *
982
     * @param string $value
983
     *
984
     * @return bool
985
     */
986
    private function isOn($value = null)
987
    {
988
        return $value === null || $value === '1' || $value === 'true' || $value === 'on';
15✔
989
    }
990

991
    /**
992
     * Returns the target of image, object, or link as stored in ::readMainRels.
993
     *
994
     * @param string $docPart
995
     * @param string $rId
996
     *
997
     * @return null|string
998
     */
999
    private function getMediaTarget($docPart, $rId)
1000
    {
1001
        $target = null;
6✔
1002

1003
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
6✔
1004
            $target = $this->rels[$docPart][$rId]['target'];
5✔
1005
        }
1006

1007
        return $target;
6✔
1008
    }
1009

1010
    /**
1011
     * Returns the target mode.
1012
     *
1013
     * @param string $docPart
1014
     * @param string $rId
1015
     *
1016
     * @return null|string
1017
     */
1018
    private function getTargetMode($docPart, $rId)
1019
    {
1020
        $mode = null;
1✔
1021

1022
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
1✔
1023
            $mode = $this->rels[$docPart][$rId]['targetMode'];
1✔
1024
        }
1025

1026
        return $mode;
1✔
1027
    }
1028
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc