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

PHPOffice / PHPWord / 10350157861

12 Aug 2024 10:26AM UTC coverage: 97.215% (+0.004%) from 97.211%
10350157861

Pull #2648

github

web-flow
Merge 160069b98 into 72c29a6ff
Pull Request #2648: Bump phpstan/phpstan-phpunit from 1.3.15 to 1.4.0

12 of 13 new or added lines in 2 files covered. (92.31%)

3 existing lines in 1 file now uncovered.

11623 of 11956 relevant lines covered (97.21%)

31.64 hits per line

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

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

18
namespace PhpOffice\PhpWord\Reader\Word2007;
19

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

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

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

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

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

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

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

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

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

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

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

118
        return $this;
9✔
119
    }
120

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

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

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

145
        return $this;
9✔
146
    }
147

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

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

165
        return $this;
1✔
166
    }
167

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

179
        return $this->commentRefs[$id];
1✔
180
    }
181

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

195
        // PreserveText
196
        if ($xmlReader->elementExists('w:r/w:instrText', $domNode)) {
22✔
197
            $ignoreText = false;
2✔
198
            $textContent = '';
2✔
199
            $fontStyle = $this->readFontStyle($xmlReader, $domNode);
2✔
200
            $nodes = $xmlReader->getElements('w:r', $domNode);
2✔
201
            foreach ($nodes as $node) {
2✔
202
                $instrText = $xmlReader->getValue('w:instrText', $node);
2✔
203
                if ($xmlReader->elementExists('w:fldChar', $node)) {
2✔
204
                    $fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar');
2✔
205
                    if ('begin' == $fldCharType) {
2✔
206
                        $ignoreText = true;
2✔
207
                    } elseif ('end' == $fldCharType) {
2✔
208
                        $ignoreText = false;
2✔
209
                    }
210
                }
211
                if (null !== $instrText) {
2✔
212
                    $textContent .= '{' . $instrText . '}';
2✔
213
                } else {
214
                    if (false === $ignoreText) {
2✔
215
                        $textContent .= $xmlReader->getValue('w:t', $node);
2✔
216
                    }
217
                }
218
            }
219
            $parent->addPreserveText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'), $fontStyle, $paragraphStyle);
2✔
220

221
            return;
2✔
222
        }
223

224
        // Formula
225
        $xmlReader->registerNamespace('m', 'http://schemas.openxmlformats.org/officeDocument/2006/math');
22✔
226
        if ($xmlReader->elementExists('m:oMath', $domNode)) {
22✔
227
            $mathElement = $xmlReader->getElement('m:oMath', $domNode);
1✔
228
            $mathXML = $mathElement->ownerDocument->saveXML($mathElement);
1✔
229
            if (is_string($mathXML)) {
1✔
230
                $reader = new OfficeMathML();
1✔
231
                $math = $reader->read($mathXML);
1✔
232

233
                $parent->addFormula($math);
1✔
234
            }
235

236
            return;
1✔
237
        }
238

239
        // List item
240
        if ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) {
21✔
241
            $numId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:numId');
3✔
242
            $levelId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:ilvl');
3✔
243
            $nodes = $xmlReader->getElements('*', $domNode);
3✔
244

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

247
            foreach ($nodes as $node) {
3✔
248
                $this->readRun($xmlReader, $node, $listItemRun, $docPart, $paragraphStyle);
3✔
249
            }
250

251
            return;
3✔
252
        }
253

254
        // Heading or Title
255
        $headingDepth = $xmlReader->elementExists('w:pPr', $domNode) ? $this->getHeadingDepth($paragraphStyle) : null;
20✔
256
        if ($headingDepth !== null) {
20✔
257
            $textContent = null;
6✔
258
            $nodes = $xmlReader->getElements('w:r|w:hyperlink', $domNode);
6✔
259
            if ($nodes->length === 1) {
6✔
260
                $textContent = htmlspecialchars($xmlReader->getValue('w:t', $nodes->item(0)), ENT_QUOTES, 'UTF-8');
2✔
261
            } else {
262
                $textContent = new TextRun($paragraphStyle);
6✔
263
                foreach ($nodes as $node) {
6✔
264
                    $this->readRun($xmlReader, $node, $textContent, $docPart, $paragraphStyle);
6✔
265
                }
266
            }
267
            $parent->addTitle($textContent, $headingDepth);
6✔
268

269
            return;
6✔
270
        }
271

272
        // Text and TextRun
273
        $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode);
18✔
274
        if (0 === $textRunContainers) {
18✔
275
            $parent->addTextBreak(null, $paragraphStyle);
7✔
276
        } else {
277
            $nodes = $xmlReader->getElements('*', $domNode);
17✔
278
            $paragraph = $parent->addTextRun($paragraphStyle);
17✔
279
            foreach ($nodes as $node) {
17✔
280
                $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle);
17✔
281
            }
282
        }
283
    }
284

285
    /**
286
     * Returns the depth of the Heading, returns 0 for a Title.
287
     *
288
     * @return null|number
289
     */
290
    private function getHeadingDepth(?array $paragraphStyle = null)
291
    {
292
        if (is_array($paragraphStyle) && isset($paragraphStyle['styleName'])) {
10✔
293
            if ('Title' === $paragraphStyle['styleName']) {
8✔
294
                return 0;
4✔
295
            }
296

297
            $headingMatches = [];
4✔
298
            preg_match('/Heading(\d)/', $paragraphStyle['styleName'], $headingMatches);
4✔
299
            if (!empty($headingMatches)) {
4✔
300
                return $headingMatches[1];
2✔
301
            }
302
        }
303

304
        return null;
8✔
305
    }
306

307
    /**
308
     * Read w:r.
309
     *
310
     * @param \PhpOffice\PhpWord\Element\AbstractContainer $parent
311
     * @param string $docPart
312
     * @param mixed $paragraphStyle
313
     *
314
     * @todo Footnote paragraph style
315
     */
316
    protected function readRun(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart, $paragraphStyle = null): void
317
    {
318
        if (in_array($domNode->nodeName, ['w:ins', 'w:del', 'w:smartTag', 'w:hyperlink', 'w:commentReference'])) {
20✔
319
            $nodes = $xmlReader->getElements('*', $domNode);
5✔
320
            foreach ($nodes as $node) {
5✔
321
                $this->readRun($xmlReader, $node, $parent, $docPart, $paragraphStyle);
5✔
322
            }
323
        } elseif ($domNode->nodeName == 'w:r') {
20✔
324
            $fontStyle = $this->readFontStyle($xmlReader, $domNode);
20✔
325
            $nodes = $xmlReader->getElements('*', $domNode);
20✔
326
            foreach ($nodes as $node) {
20✔
327
                $this->readRunChild($xmlReader, $node, $parent, $docPart, $paragraphStyle, $fontStyle);
20✔
328
            }
329
        }
330

331
        if ($xmlReader->elementExists('.//*["commentReference"=local-name()]', $domNode)) {
20✔
332
            $node = iterator_to_array($xmlReader->getElements('.//*["commentReference"=local-name()]', $domNode))[0];
1✔
333
            $attributeIdentifier = $node->attributes->getNamedItem('id');
1✔
334
            if ($attributeIdentifier) {
1✔
335
                $id = $attributeIdentifier->nodeValue;
1✔
336

337
                $this->setCommentReference('start', $id, $parent->getElement($parent->countElements() - 1));
1✔
338
                $this->setCommentReference('end', $id, $parent->getElement($parent->countElements() - 1));
1✔
339
            }
340
        }
341
    }
342

343
    /**
344
     * Parses nodes under w:r.
345
     *
346
     * @param string $docPart
347
     * @param mixed $paragraphStyle
348
     * @param mixed $fontStyle
349
     */
350
    protected function readRunChild(XMLReader $xmlReader, DOMElement $node, AbstractContainer $parent, $docPart, $paragraphStyle = null, $fontStyle = null): void
351
    {
352
        $runParent = $node->parentNode->parentNode;
20✔
353
        if ($node->nodeName == 'w:footnoteReference') {
20✔
354
            // Footnote
355
            $wId = $xmlReader->getAttribute('w:id', $node);
3✔
356
            $footnote = $parent->addFootnote();
3✔
357
            $footnote->setRelationId($wId);
3✔
358
        } elseif ($node->nodeName == 'w:endnoteReference') {
20✔
359
            // Endnote
360
            $wId = $xmlReader->getAttribute('w:id', $node);
3✔
361
            $endnote = $parent->addEndnote();
3✔
362
            $endnote->setRelationId($wId);
3✔
363
        } elseif ($node->nodeName == 'w:pict') {
20✔
364
            // Image
365
            $rId = $xmlReader->getAttribute('r:id', $node, 'v:shape/v:imagedata');
1✔
366
            $target = $this->getMediaTarget($docPart, $rId);
1✔
367
            if ($this->hasImageLoading() && null !== $target) {
1✔
368
                if ('External' == $this->getTargetMode($docPart, $rId)) {
1✔
369
                    $imageSource = $target;
×
370
                } else {
371
                    $imageSource = "zip://{$this->docFile}#{$target}";
1✔
372
                }
373
                $parent->addImage($imageSource);
1✔
374
            }
375
        } elseif ($node->nodeName == 'w:drawing') {
20✔
376
            // Office 2011 Image
377
            $xmlReader->registerNamespace('wp', 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing');
5✔
378
            $xmlReader->registerNamespace('r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
5✔
379
            $xmlReader->registerNamespace('pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture');
5✔
380
            $xmlReader->registerNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
5✔
381

382
            $name = $xmlReader->getAttribute('name', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
5✔
383
            $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
5✔
384
            if ($name === null && $embedId === null) { // some Converters puts images on a different path
5✔
385
                $name = $xmlReader->getAttribute('name', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
×
386
                $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
×
387
            }
388
            $target = $this->getMediaTarget($docPart, $embedId);
5✔
389
            if ($this->hasImageLoading() && null !== $target) {
5✔
390
                $imageSource = "zip://{$this->docFile}#{$target}";
3✔
391
                $parent->addImage($imageSource, null, false, $name);
5✔
392
            }
393
        } elseif ($node->nodeName == 'w:object') {
19✔
394
            // Object
395
            $rId = $xmlReader->getAttribute('r:id', $node, 'o:OLEObject');
2✔
396
            // $rIdIcon = $xmlReader->getAttribute('r:id', $domNode, 'w:object/v:shape/v:imagedata');
397
            $target = $this->getMediaTarget($docPart, $rId);
2✔
398
            if (null !== $target) {
2✔
399
                $textContent = "&lt;Object: {$target}>";
2✔
400
                $parent->addText($textContent, $fontStyle, $paragraphStyle);
2✔
401
            }
402
        } elseif ($node->nodeName == 'w:br') {
19✔
403
            $parent->addTextBreak();
3✔
404
        } elseif ($node->nodeName == 'w:tab') {
19✔
405
            $parent->addText("\t");
1✔
406
        } elseif ($node->nodeName == 'mc:AlternateContent') {
19✔
407
            if ($node->hasChildNodes()) {
1✔
408
                // Get fallback instead of mc:Choice to make sure it is compatible
409
                $fallbackElements = $node->getElementsByTagName('Fallback');
1✔
410

411
                if ($fallbackElements->length) {
1✔
412
                    $fallback = $fallbackElements->item(0);
1✔
413
                    // TextRun
414
                    $textContent = htmlspecialchars($fallback->nodeValue, ENT_QUOTES, 'UTF-8');
1✔
415

416
                    $parent->addText($textContent, $fontStyle, $paragraphStyle);
1✔
417
                }
418
            }
419
        } elseif ($node->nodeName == 'w:t' || $node->nodeName == 'w:delText') {
18✔
420
            // TextRun
421
            $textContent = htmlspecialchars($xmlReader->getValue('.', $node), ENT_QUOTES, 'UTF-8');
17✔
422

423
            if ($runParent->nodeName == 'w:hyperlink') {
17✔
424
                $rId = $xmlReader->getAttribute('r:id', $runParent);
2✔
425
                $target = $this->getMediaTarget($docPart, $rId);
2✔
426
                if (null !== $target) {
2✔
427
                    $parent->addLink($target, $textContent, $fontStyle, $paragraphStyle);
2✔
428
                } else {
429
                    $parent->addText($textContent, $fontStyle, $paragraphStyle);
2✔
430
                }
431
            } else {
432
                /** @var AbstractElement $element */
433
                $element = $parent->addText($textContent, $fontStyle, $paragraphStyle);
17✔
434
                if (in_array($runParent->nodeName, ['w:ins', 'w:del'])) {
17✔
435
                    $type = ($runParent->nodeName == 'w:del') ? TrackChange::DELETED : TrackChange::INSERTED;
3✔
436
                    $author = $runParent->getAttribute('w:author');
3✔
437
                    $date = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $runParent->getAttribute('w:date'));
3✔
438
                    $date = $date instanceof DateTime ? $date : null;
3✔
439
                    $element->setChangeInfo($type, $author, $date);
17✔
440
                }
441
            }
442
        } elseif ($node->nodeName == 'w:softHyphen') {
14✔
443
            $element = $parent->addText("\u{200c}", $fontStyle, $paragraphStyle);
×
444
        }
445
    }
446

447
    /**
448
     * Read w:tbl.
449
     *
450
     * @param mixed $parent
451
     * @param string $docPart
452
     */
453
    protected function readTable(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void
454
    {
455
        // Table style
456
        $tblStyle = null;
10✔
457
        if ($xmlReader->elementExists('w:tblPr', $domNode)) {
10✔
458
            $tblStyle = $this->readTableStyle($xmlReader, $domNode);
7✔
459
        }
460

461
        /** @var \PhpOffice\PhpWord\Element\Table $table Type hint */
462
        $table = $parent->addTable($tblStyle);
10✔
463
        $tblNodes = $xmlReader->getElements('*', $domNode);
10✔
464
        foreach ($tblNodes as $tblNode) {
10✔
465
            if ('w:tblGrid' == $tblNode->nodeName) { // Column
10✔
466
                // @todo Do something with table columns
467
            } elseif ('w:tr' == $tblNode->nodeName) { // Row
10✔
468
                $rowHeight = $xmlReader->getAttribute('w:val', $tblNode, 'w:trPr/w:trHeight');
5✔
469
                $rowHRule = $xmlReader->getAttribute('w:hRule', $tblNode, 'w:trPr/w:trHeight');
5✔
470
                $rowHRule = $rowHRule == 'exact';
5✔
471
                $rowStyle = [
5✔
472
                    'tblHeader' => $xmlReader->elementExists('w:trPr/w:tblHeader', $tblNode),
5✔
473
                    'cantSplit' => $xmlReader->elementExists('w:trPr/w:cantSplit', $tblNode),
5✔
474
                    'exactHeight' => $rowHRule,
5✔
475
                ];
5✔
476

477
                $row = $table->addRow($rowHeight, $rowStyle);
5✔
478
                $rowNodes = $xmlReader->getElements('*', $tblNode);
5✔
479
                foreach ($rowNodes as $rowNode) {
5✔
480
                    if ('w:trPr' == $rowNode->nodeName) { // Row style
5✔
481
                        // @todo Do something with row style
482
                    } elseif ('w:tc' == $rowNode->nodeName) { // Cell
5✔
483
                        $cellWidth = $xmlReader->getAttribute('w:w', $rowNode, 'w:tcPr/w:tcW');
5✔
484
                        $cellStyle = null;
5✔
485
                        if ($xmlReader->elementExists('w:tcPr', $rowNode)) {
5✔
486
                            $cellStyle = $this->readCellStyle($xmlReader, $rowNode);
4✔
487
                        }
488

489
                        $cell = $row->addCell($cellWidth, $cellStyle);
5✔
490
                        $cellNodes = $xmlReader->getElements('*', $rowNode);
5✔
491
                        foreach ($cellNodes as $cellNode) {
5✔
492
                            if ('w:p' == $cellNode->nodeName) { // Paragraph
5✔
493
                                $this->readParagraph($xmlReader, $cellNode, $cell, $docPart);
3✔
494
                            } elseif ($cellNode->nodeName == 'w:tbl') { // Table
5✔
495
                                $this->readTable($xmlReader, $cellNode, $cell, $docPart);
1✔
496
                            }
497
                        }
498
                    }
499
                }
500
            }
501
        }
502
    }
503

504
    /**
505
     * Read w:pPr.
506
     *
507
     * @return null|array
508
     */
509
    protected function readParagraphStyle(XMLReader $xmlReader, DOMElement $domNode)
510
    {
511
        if (!$xmlReader->elementExists('w:pPr', $domNode)) {
13✔
512
            return null;
7✔
513
        }
514

515
        $styleNode = $xmlReader->getElement('w:pPr', $domNode);
13✔
516
        $styleDefs = [
13✔
517
            'styleName' => [self::READ_VALUE, ['w:pStyle', 'w:name']],
13✔
518
            'alignment' => [self::READ_VALUE, 'w:jc'],
13✔
519
            'basedOn' => [self::READ_VALUE, 'w:basedOn'],
13✔
520
            'next' => [self::READ_VALUE, 'w:next'],
13✔
521
            'indent' => [self::READ_VALUE, 'w:ind', 'w:left'],
13✔
522
            'hanging' => [self::READ_VALUE, 'w:ind', 'w:hanging'],
13✔
523
            'spaceAfter' => [self::READ_VALUE, 'w:spacing', 'w:after'],
13✔
524
            'spaceBefore' => [self::READ_VALUE, 'w:spacing', 'w:before'],
13✔
525
            'widowControl' => [self::READ_FALSE, 'w:widowControl'],
13✔
526
            'keepNext' => [self::READ_TRUE,  'w:keepNext'],
13✔
527
            'keepLines' => [self::READ_TRUE,  'w:keepLines'],
13✔
528
            'pageBreakBefore' => [self::READ_TRUE,  'w:pageBreakBefore'],
13✔
529
            'contextualSpacing' => [self::READ_TRUE,  'w:contextualSpacing'],
13✔
530
            'bidi' => [self::READ_TRUE,  'w:bidi'],
13✔
531
            'suppressAutoHyphens' => [self::READ_TRUE,  'w:suppressAutoHyphens'],
13✔
532
            'borderTopStyle' => [self::READ_VALUE, 'w:pBdr/w:top'],
13✔
533
            'borderTopColor' => [self::READ_VALUE, 'w:pBdr/w:top', 'w:color'],
13✔
534
            'borderTopSize' => [self::READ_VALUE, 'w:pBdr/w:top', 'w:sz'],
13✔
535
            'borderRightStyle' => [self::READ_VALUE, 'w:pBdr/w:right'],
13✔
536
            'borderRightColor' => [self::READ_VALUE, 'w:pBdr/w:right', 'w:color'],
13✔
537
            'borderRightSize' => [self::READ_VALUE, 'w:pBdr/w:right', 'w:sz'],
13✔
538
            'borderBottomStyle' => [self::READ_VALUE, 'w:pBdr/w:bottom'],
13✔
539
            'borderBottomColor' => [self::READ_VALUE, 'w:pBdr/w:bottom', 'w:color'],
13✔
540
            'borderBottomSize' => [self::READ_VALUE, 'w:pBdr/w:bottom', 'w:sz'],
13✔
541
            'borderLeftStyle' => [self::READ_VALUE, 'w:pBdr/w:left'],
13✔
542
            'borderLeftColor' => [self::READ_VALUE, 'w:pBdr/w:left', 'w:color'],
13✔
543
            'borderLeftSize' => [self::READ_VALUE, 'w:pBdr/w:left', 'w:sz'],
13✔
544
        ];
13✔
545

546
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
13✔
547
    }
548

549
    /**
550
     * Read w:rPr.
551
     *
552
     * @return null|array
553
     */
554
    protected function readFontStyle(XMLReader $xmlReader, DOMElement $domNode)
555
    {
556
        if (null === $domNode) {
22✔
UNCOV
557
            return null;
×
558
        }
559
        // Hyperlink has an extra w:r child
560
        if ('w:hyperlink' == $domNode->nodeName) {
22✔
UNCOV
561
            $domNode = $xmlReader->getElement('w:r', $domNode);
×
562
        }
563
        if (!$xmlReader->elementExists('w:rPr', $domNode)) {
22✔
564
            return null;
17✔
565
        }
566

567
        $styleNode = $xmlReader->getElement('w:rPr', $domNode);
16✔
568
        $styleDefs = [
16✔
569
            'styleName' => [self::READ_VALUE, 'w:rStyle'],
16✔
570
            'name' => [self::READ_VALUE, 'w:rFonts', ['w:ascii', 'w:hAnsi', 'w:eastAsia', 'w:cs']],
16✔
571
            'hint' => [self::READ_VALUE, 'w:rFonts', 'w:hint'],
16✔
572
            'size' => [self::READ_SIZE,  ['w:sz', 'w:szCs']],
16✔
573
            'color' => [self::READ_VALUE, 'w:color'],
16✔
574
            'underline' => [self::READ_VALUE, 'w:u'],
16✔
575
            'bold' => [self::READ_TRUE,  'w:b'],
16✔
576
            'italic' => [self::READ_TRUE,  'w:i'],
16✔
577
            'strikethrough' => [self::READ_TRUE,  'w:strike'],
16✔
578
            'doubleStrikethrough' => [self::READ_TRUE,  'w:dstrike'],
16✔
579
            'smallCaps' => [self::READ_TRUE,  'w:smallCaps'],
16✔
580
            'allCaps' => [self::READ_TRUE,  'w:caps'],
16✔
581
            'superScript' => [self::READ_EQUAL, 'w:vertAlign', 'w:val', 'superscript'],
16✔
582
            'subScript' => [self::READ_EQUAL, 'w:vertAlign', 'w:val', 'subscript'],
16✔
583
            'fgColor' => [self::READ_VALUE, 'w:highlight'],
16✔
584
            'rtl' => [self::READ_TRUE,  'w:rtl'],
16✔
585
            'lang' => [self::READ_VALUE, 'w:lang'],
16✔
586
            'position' => [self::READ_VALUE, 'w:position'],
16✔
587
            'hidden' => [self::READ_TRUE,  'w:vanish'],
16✔
588
        ];
16✔
589

590
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
16✔
591
    }
592

593
    /**
594
     * Read w:tblPr.
595
     *
596
     * @return null|array|string
597
     *
598
     * @todo Capture w:tblStylePr w:type="firstRow"
599
     */
600
    protected function readTableStyle(XMLReader $xmlReader, DOMElement $domNode)
601
    {
602
        $style = null;
12✔
603
        $margins = ['top', 'left', 'bottom', 'right'];
12✔
604
        $borders = array_merge($margins, ['insideH', 'insideV']);
12✔
605

606
        if ($xmlReader->elementExists('w:tblPr', $domNode)) {
12✔
607
            if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) {
12✔
UNCOV
608
                $style = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle');
×
609
            } else {
610
                $styleNode = $xmlReader->getElement('w:tblPr', $domNode);
12✔
611
                $styleDefs = [];
12✔
612
                foreach ($margins as $side) {
12✔
613
                    $ucfSide = ucfirst($side);
12✔
614
                    $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w'];
12✔
615
                }
616
                foreach ($borders as $side) {
12✔
617
                    $ucfSide = ucfirst($side);
12✔
618
                    $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz'];
12✔
619
                    $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color'];
12✔
620
                    $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val'];
12✔
621
                }
622
                $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type'];
12✔
623
                $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual'];
12✔
624
                $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w'];
12✔
625
                $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
12✔
626

627
                $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode);
12✔
628
                if ($tablePositionNode !== null) {
12✔
629
                    $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode);
1✔
630
                }
631

632
                $indentNode = $xmlReader->getElement('w:tblInd', $styleNode);
12✔
633
                if ($indentNode !== null) {
12✔
634
                    $style['indent'] = $this->readTableIndent($xmlReader, $indentNode);
7✔
635
                }
636
            }
637
        }
638

639
        return $style;
12✔
640
    }
641

642
    /**
643
     * Read w:tblpPr.
644
     *
645
     * @return array
646
     */
647
    private function readTablePosition(XMLReader $xmlReader, DOMElement $domNode)
648
    {
649
        $styleDefs = [
1✔
650
            'leftFromText' => [self::READ_VALUE, '.', 'w:leftFromText'],
1✔
651
            'rightFromText' => [self::READ_VALUE, '.', 'w:rightFromText'],
1✔
652
            'topFromText' => [self::READ_VALUE, '.', 'w:topFromText'],
1✔
653
            'bottomFromText' => [self::READ_VALUE, '.', 'w:bottomFromText'],
1✔
654
            'vertAnchor' => [self::READ_VALUE, '.', 'w:vertAnchor'],
1✔
655
            'horzAnchor' => [self::READ_VALUE, '.', 'w:horzAnchor'],
1✔
656
            'tblpXSpec' => [self::READ_VALUE, '.', 'w:tblpXSpec'],
1✔
657
            'tblpX' => [self::READ_VALUE, '.', 'w:tblpX'],
1✔
658
            'tblpYSpec' => [self::READ_VALUE, '.', 'w:tblpYSpec'],
1✔
659
            'tblpY' => [self::READ_VALUE, '.', 'w:tblpY'],
1✔
660
        ];
1✔
661

662
        return $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
1✔
663
    }
664

665
    /**
666
     * Read w:tblInd.
667
     *
668
     * @return TblWidthComplexType
669
     */
670
    private function readTableIndent(XMLReader $xmlReader, DOMElement $domNode)
671
    {
672
        $styleDefs = [
7✔
673
            'value' => [self::READ_VALUE, '.', 'w:w'],
7✔
674
            'type' => [self::READ_VALUE, '.', 'w:type'],
7✔
675
        ];
7✔
676
        $styleDefs = $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
7✔
677

678
        return new TblWidthComplexType((int) $styleDefs['value'], $styleDefs['type']);
7✔
679
    }
680

681
    /**
682
     * Read w:tcPr.
683
     *
684
     * @return null|array
685
     */
686
    private function readCellStyle(XMLReader $xmlReader, DOMElement $domNode)
687
    {
688
        $styleDefs = [
4✔
689
            'valign' => [self::READ_VALUE, 'w:vAlign'],
4✔
690
            'textDirection' => [self::READ_VALUE, 'w:textDirection'],
4✔
691
            'gridSpan' => [self::READ_VALUE, 'w:gridSpan'],
4✔
692
            'vMerge' => [self::READ_VALUE, 'w:vMerge', null, null, 'continue'],
4✔
693
            'bgColor' => [self::READ_VALUE, 'w:shd', 'w:fill'],
4✔
694
            'noWrap' => [self::READ_VALUE, 'w:noWrap', null, null, true],
4✔
695
        ];
4✔
696
        $style = null;
4✔
697

698
        if ($xmlReader->elementExists('w:tcPr', $domNode)) {
4✔
699
            $styleNode = $xmlReader->getElement('w:tcPr', $domNode);
4✔
700

701
            $borders = ['top', 'left', 'bottom', 'right'];
4✔
702
            foreach ($borders as $side) {
4✔
703
                $ucfSide = ucfirst($side);
4✔
704

705
                $styleDefs['border' . $ucfSide . 'Size'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:sz'];
4✔
706
                $styleDefs['border' . $ucfSide . 'Color'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:color'];
4✔
707
                $styleDefs['border' . $ucfSide . 'Style'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:val'];
4✔
708
            }
709

710
            $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
4✔
711
        }
712

713
        return $style;
4✔
714
    }
715

716
    /**
717
     * Returns the first child element found.
718
     *
719
     * @param null|array|string $elements
720
     *
721
     * @return null|string
722
     */
723
    private function findPossibleElement(XMLReader $xmlReader, ?DOMElement $parentNode = null, $elements = null)
724
    {
725
        if (is_array($elements)) {
24✔
726
            //if element is an array, we take the first element that exists in the XML
727
            foreach ($elements as $possibleElement) {
16✔
728
                if ($xmlReader->elementExists($possibleElement, $parentNode)) {
16✔
729
                    return $possibleElement;
12✔
730
                }
731
            }
732
        } else {
733
            return $elements;
24✔
734
        }
735

736
        return null;
16✔
737
    }
738

739
    /**
740
     * Returns the first attribute found.
741
     *
742
     * @param array|string $attributes
743
     *
744
     * @return null|string
745
     */
746
    private function findPossibleAttribute(XMLReader $xmlReader, DOMElement $node, $attributes)
747
    {
748
        //if attribute is an array, we take the first attribute that exists in the XML
749
        if (is_array($attributes)) {
24✔
750
            foreach ($attributes as $possibleAttribute) {
11✔
751
                if ($xmlReader->getAttribute($possibleAttribute, $node)) {
11✔
752
                    return $possibleAttribute;
7✔
753
                }
754
            }
755

756
            return null;
6✔
757
        }
758

759
        return $attributes;
24✔
760
    }
761

762
    /**
763
     * Read style definition.
764
     *
765
     * @param array $styleDefs
766
     *
767
     * @ignoreScrutinizerPatch
768
     *
769
     * @return array
770
     */
771
    protected function readStyleDefs(XMLReader $xmlReader, ?DOMElement $parentNode = null, $styleDefs = [])
772
    {
773
        $styles = [];
24✔
774

775
        foreach ($styleDefs as $styleProp => $styleVal) {
24✔
776
            [$method, $element, $attribute, $expected, $default] = array_pad($styleVal, 5, null);
24✔
777

778
            $element = $this->findPossibleElement($xmlReader, $parentNode, $element);
24✔
779
            if ($element === null) {
24✔
780
                continue;
16✔
781
            }
782

783
            if ($xmlReader->elementExists($element, $parentNode)) {
24✔
784
                $node = $xmlReader->getElement($element, $parentNode);
24✔
785

786
                $attribute = $this->findPossibleAttribute($xmlReader, $node, $attribute);
24✔
787

788
                // Use w:val as default if no attribute assigned
789
                $attribute = ($attribute === null) ? 'w:val' : $attribute;
24✔
790
                $attributeValue = $xmlReader->getAttribute($attribute, $node) ?? $default;
24✔
791

792
                $styleValue = $this->readStyleDef($method, $attributeValue, $expected);
24✔
793
                if ($styleValue !== null) {
24✔
794
                    $styles[$styleProp] = $styleValue;
24✔
795
                }
796
            }
797
        }
798

799
        return $styles;
24✔
800
    }
801

802
    /**
803
     * Return style definition based on conversion method.
804
     *
805
     * @param string $method
806
     *
807
     * @ignoreScrutinizerPatch
808
     *
809
     * @param null|string $attributeValue
810
     * @param mixed $expected
811
     *
812
     * @return mixed
813
     */
814
    private function readStyleDef($method, $attributeValue, $expected)
815
    {
816
        $style = $attributeValue;
24✔
817

818
        if (self::READ_SIZE == $method) {
24✔
819
            $style = $attributeValue / 2;
12✔
820
        } elseif (self::READ_TRUE == $method) {
24✔
821
            $style = $this->isOn($attributeValue);
15✔
822
        } elseif (self::READ_FALSE == $method) {
21✔
823
            $style = !$this->isOn($attributeValue);
4✔
824
        } elseif (self::READ_EQUAL == $method) {
21✔
825
            $style = $attributeValue == $expected;
2✔
826
        }
827

828
        return $style;
24✔
829
    }
830

831
    /**
832
     * Parses the value of the on/off value, null is considered true as it means the w:val attribute was not present.
833
     *
834
     * @see http://www.datypic.com/sc/ooxml/t-w_ST_OnOff.html
835
     *
836
     * @param string $value
837
     *
838
     * @return bool
839
     */
840
    private function isOn($value = null)
841
    {
842
        return $value === null || $value === '1' || $value === 'true' || $value === 'on';
15✔
843
    }
844

845
    /**
846
     * Returns the target of image, object, or link as stored in ::readMainRels.
847
     *
848
     * @param string $docPart
849
     * @param string $rId
850
     *
851
     * @return null|string
852
     */
853
    private function getMediaTarget($docPart, $rId)
854
    {
855
        $target = null;
6✔
856

857
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
6✔
858
            $target = $this->rels[$docPart][$rId]['target'];
5✔
859
        }
860

861
        return $target;
6✔
862
    }
863

864
    /**
865
     * Returns the target mode.
866
     *
867
     * @param string $docPart
868
     * @param string $rId
869
     *
870
     * @return null|string
871
     */
872
    private function getTargetMode($docPart, $rId)
873
    {
874
        $mode = null;
1✔
875

876
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
1✔
877
            $mode = $this->rels[$docPart][$rId]['targetMode'];
1✔
878
        }
879

880
        return $mode;
1✔
881
    }
882
}
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