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

PHPOffice / PHPWord / 25414441731

06 May 2026 03:12AM UTC coverage: 94.696% (-2.1%) from 96.757%
25414441731

Pull #2874

github

web-flow
Merge 3fa85d6e3 into 0ab0b4940
Pull Request #2874: Addresses issue #12

135 of 420 new or added lines in 30 files covered. (32.14%)

1 existing line in 1 file now uncovered.

12588 of 13293 relevant lines covered (94.7%)

34.84 hits per line

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

95.02
/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\RubyProperties;
26
use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType;
27
use PhpOffice\PhpWord\Element\AbstractContainer;
28
use PhpOffice\PhpWord\Element\AbstractElement;
29
use PhpOffice\PhpWord\Element\FormField;
30
use PhpOffice\PhpWord\Element\Ruby;
31
use PhpOffice\PhpWord\Element\Text;
32
use PhpOffice\PhpWord\Element\TextRun;
33
use PhpOffice\PhpWord\Element\TrackChange;
34
use PhpOffice\PhpWord\PhpWord;
35
use PhpOffice\PhpWord\Shared\XMLReader;
36

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

57
    /**
58
     * Document file.
59
     *
60
     * @var string
61
     */
62
    protected $docFile;
63

64
    /**
65
     * XML file.
66
     *
67
     * @var string
68
     */
69
    protected $xmlFile;
70

71
    /**
72
     * Part relationships.
73
     *
74
     * @var array
75
     */
76
    protected $rels = [];
77

78
    /**                                                                                                                 
79
     * Current page number, tracked via w:lastRenderedPageBreak hints.
80
     *                                                                                                                  
81
     * @var int
82
     */                                                                                                                 
83
    protected $currentPage = 1;
84

85
    /**
86
     * Comment references.
87
     *
88
     * @var array<string, array<string, AbstractElement>>
89
     */
90
    protected $commentRefs = [];
91

92
    /**
93
     * Image Loading.
94
     *
95
     * @var bool
96
     */
97
    protected $imageLoading = true;
98

99
    /**
100
     * Read part.
101
     */
102
    abstract public function read(PhpWord $phpWord);
103

104
    /**
105
     * Create new instance.
106
     *
107
     * @param string $docFile
108
     * @param string $xmlFile
109
     */
110
    public function __construct($docFile, $xmlFile)
111
    {
112
        $this->docFile = $docFile;
53✔
113
        $this->xmlFile = $xmlFile;
53✔
114
    }
115

116
    /**
117
     * Set relationships.
118
     *
119
     * @param array $value
120
     */
121
    public function setRels($value): void
122
    {
123
        $this->rels = $value;
20✔
124
    }
125

126
    public function setImageLoading(bool $value): self
127
    {
128
        $this->imageLoading = $value;
15✔
129

130
        return $this;
15✔
131
    }
132

133
    public function hasImageLoading(): bool
134
    {
135
        return $this->imageLoading;
6✔
136
    }
137

138
    /**
139
     * Get comment references.
140
     *
141
     * @return array<string, array<string, null|AbstractElement>>
142
     */
143
    public function getCommentReferences(): array
144
    {
145
        return $this->commentRefs;
15✔
146
    }
147

148
    /**
149
     * Set comment references.
150
     *
151
     * @param array<string, array<string, null|AbstractElement>> $commentRefs
152
     */
153
    public function setCommentReferences(array $commentRefs): self
154
    {
155
        $this->commentRefs = $commentRefs;
15✔
156

157
        return $this;
15✔
158
    }
159

160
    /**
161
     * Set comment reference.
162
     */
163
    private function setCommentReference(string $type, string $id, AbstractElement $element): self
164
    {
165
        if (!in_array($type, ['start', 'end'])) {
1✔
166
            throw new InvalidArgumentException('Type must be "start" or "end"');
×
167
        }
168

169
        if (!array_key_exists($id, $this->commentRefs)) {
1✔
170
            $this->commentRefs[$id] = [
1✔
171
                'start' => null,
1✔
172
                'end' => null,
1✔
173
            ];
1✔
174
        }
175
        $this->commentRefs[$id][$type] = $element;
1✔
176

177
        return $this;
1✔
178
    }
179

180
    /**
181
     * Get comment reference.
182
     *
183
     * @return array<string, null|AbstractElement>
184
     */
185
    protected function getCommentReference(string $id): array
186
    {
187
        if (!array_key_exists($id, $this->commentRefs)) {
1✔
188
            throw new InvalidArgumentException(sprintf('Comment with id %s isn\'t referenced in document', $id));
×
189
        }
190

191
        return $this->commentRefs[$id];
1✔
192
    }
193

194
    /**
195
     * Read w:p.
196
     *
197
     * @param AbstractContainer $parent
198
     * @param string $docPart
199
     *
200
     * @todo Get font style for preserve text
201
     */
202
    protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void
203
    {
204
        // Paragraph style
205
        $paragraphStyle = $xmlReader->elementExists('w:pPr', $domNode) ? $this->readParagraphStyle($xmlReader, $domNode) : null;
33✔
206

207
        // Caption check must come before FormField/PreserveText — caption paragraphs
208
        // contain SEQ field codes that would otherwise be caught by the instrText branch.
209
        if (is_array($paragraphStyle) && isset($paragraphStyle['styleName']) &&
33✔
210
            str_contains($paragraphStyle['styleName'], 'Caption')) {
33✔
211

212
            // Collect all w:t text nodes (includes cached SEQ field results)
NEW
213
            $textContent = '';
×
NEW
214
            foreach ($xmlReader->getElements('w:r/w:t', $domNode) as $textNode) {
×
NEW
215
                $textContent .= $textNode->nodeValue;
×
216
            }
217

218
            // Derive label from style name ("Caption" → "Figure", "Table Caption" → "Table")
NEW
219
            $lowerStyle = strtolower($paragraphStyle['styleName']);
×
NEW
220
            $label = 'Figure';
×
NEW
221
            foreach (['table', 'photo', 'map', 'plate', 'equation'] as $candidate) {
×
NEW
222
                if (str_contains($lowerStyle, $candidate)) {
×
NEW
223
                    $label = ucfirst($candidate);
×
NEW
224
                    break;
×
225
                }
226
            }
227

NEW
228
            $parent->addCaption($label, trim($textContent) ?: null, null, $paragraphStyle, $this->currentPage);
×
229

NEW
230
            return;
×
231
        }
232

233
        if ($xmlReader->elementExists('w:r/w:fldChar/w:ffData', $domNode)) {
33✔
234
            // FormField
235
            $partOfFormField = false;
3✔
236
            $formNodes = [];
3✔
237
            $formType = null;
3✔
238
            $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag', $domNode);
3✔
239
            if ($textRunContainers > 0) {
3✔
240
                $nodes = $xmlReader->getElements('*', $domNode);
3✔
241
                $paragraph = $parent->addTextRun($paragraphStyle);
3✔
242
                foreach ($nodes as $node) {
3✔
243
                    if ($xmlReader->elementExists('w:fldChar/w:ffData', $node)) {
3✔
244
                        $partOfFormField = true;
3✔
245
                        $formNodes[] = $node;
3✔
246
                        if ($xmlReader->elementExists('w:fldChar/w:ffData/w:ddList', $node)) {
3✔
247
                            $formType = 'dropdown';
1✔
248
                        } elseif ($xmlReader->elementExists('w:fldChar/w:ffData/w:textInput', $node)) {
2✔
249
                            $formType = 'textinput';
1✔
250
                        } elseif ($xmlReader->elementExists('w:fldChar/w:ffData/w:checkBox', $node)) {
1✔
251
                            $formType = 'checkbox';
3✔
252
                        }
253
                    } elseif ($partOfFormField &&
3✔
254
                        $xmlReader->elementExists('w:fldChar', $node) &&
3✔
255
                        'end' == $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar')
3✔
256
                    ) {
257
                        $formNodes[] = $node;
3✔
258
                        $partOfFormField = false;
3✔
259
                        // Process the form fields
260
                        $this->readFormField($xmlReader, $formNodes, $paragraph, $paragraphStyle, $formType);
3✔
261
                    } elseif ($partOfFormField) {
3✔
262
                        $formNodes[] = $node;
3✔
263
                    } else {
264
                        // normal runs
265
                        $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle);
3✔
266
                    }
267
                }
268
            }
269
        } elseif ($xmlReader->elementExists('w:r/w:instrText', $domNode)) {
30✔
270
            // PreserveText
271
            $ignoreText = false;
2✔
272
            $textContent = '';
2✔
273
            $fontStyle = $this->readFontStyle($xmlReader, $domNode);
2✔
274
            $nodes = $xmlReader->getElements('w:r', $domNode);
2✔
275
            foreach ($nodes as $node) {
2✔
276
                if ($xmlReader->elementExists('w:lastRenderedPageBreak', $node)) {
2✔
277
                    $parent->addPageBreak();
×
NEW
278
                    ++$this->currentPage;
×
279
                }
280
                $instrText = $xmlReader->getValue('w:instrText', $node);
2✔
281
                if (null !== $instrText) {
2✔
282
                    $textContent .= '{' . $instrText . '}';
2✔
283
                } else {
284
                    if ($xmlReader->elementExists('w:fldChar', $node)) {
2✔
285
                        $fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar');
2✔
286
                        if ('begin' == $fldCharType) {
2✔
287
                            $ignoreText = true;
2✔
288
                        } elseif ('end' == $fldCharType) {
2✔
289
                            $ignoreText = false;
2✔
290
                        }
291
                    }
292
                    if (false === $ignoreText) {
2✔
293
                        $textContent .= $xmlReader->getValue('w:t', $node);
2✔
294
                    }
295
                }
296
            }
297
            $parent->addPreserveText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'), $fontStyle, $paragraphStyle);
2✔
298

299
            return;
2✔
300
        }
301

302
        // Formula
303
        $xmlReader->registerNamespace('m', 'http://schemas.openxmlformats.org/officeDocument/2006/math');
33✔
304
        if ($xmlReader->elementExists('m:oMath', $domNode)) {
33✔
305
            $mathElement = $xmlReader->getElement('m:oMath', $domNode);
1✔
306
            $mathXML = $mathElement->ownerDocument->saveXML($mathElement);
1✔
307
            if (is_string($mathXML)) {
1✔
308
                $reader = new OfficeMathML();
1✔
309
                $math = $reader->read($mathXML);
1✔
310

311
                $parent->addFormula($math);
1✔
312
            }
313

314
            return;
1✔
315
        }
316

317
        // List item
318
        if ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) {
32✔
319
            $numId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:numId');
3✔
320
            $levelId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:ilvl');
3✔
321
            $nodes = $xmlReader->getElements('*', $domNode);
3✔
322

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

325
            foreach ($nodes as $node) {
3✔
326
                $this->readRun($xmlReader, $node, $listItemRun, $docPart, $paragraphStyle);
3✔
327
            }
328

329
            return;
3✔
330
        }
331

332
        // Heading or Title
333
        $headingDepth = $xmlReader->elementExists('w:pPr', $domNode) ? $this->getHeadingDepth($paragraphStyle) : null;
31✔
334
        if ($headingDepth !== null) {
31✔
335
            $textContent = null;
7✔
336
            $nodes = $xmlReader->getElements('w:r|w:hyperlink', $domNode);
7✔
337
            $hasRubyElement = $xmlReader->elementExists('w:r/w:ruby', $domNode);
7✔
338
            if ($nodes->length === 1 && !$hasRubyElement) {
7✔
339
                $textContent = htmlspecialchars($xmlReader->getValue('w:t', $nodes->item(0)), ENT_QUOTES, 'UTF-8');
2✔
340
            } else {
341
                $textContent = new TextRun($paragraphStyle);
7✔
342
                foreach ($nodes as $node) {
7✔
343
                    $this->readRun($xmlReader, $node, $textContent, $docPart, $paragraphStyle);
7✔
344
                }
345
            }
346
            $parent->addTitle($textContent, $headingDepth);
7✔
347

348
            return;
7✔
349
        }
350

351
        // Text and TextRun
352
        $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode);
28✔
353
        if (0 === $textRunContainers) {
28✔
354
            $parent->addTextBreak(1, $paragraphStyle);
7✔
355
        } else {
356
            $nodes = $xmlReader->getElements('*', $domNode);
27✔
357
            $paragraph = $parent->addTextRun($paragraphStyle);
27✔
358
            foreach ($nodes as $node) {
27✔
359
                $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle);
27✔
360
            }
361
        }
362
    }
363

364
    /**
365
     * @param DOMElement[] $domNodes
366
     * @param AbstractContainer $parent
367
     * @param mixed $paragraphStyle
368
     * @param string $formType
369
     */
370
    private function readFormField(XMLReader $xmlReader, array $domNodes, $parent, $paragraphStyle, $formType): void
371
    {
372
        if (!in_array($formType, ['textinput', 'checkbox', 'dropdown'])) {
3✔
373
            return;
×
374
        }
375

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

379
        foreach ($xmlReader->getElements('*', $ffData) as $node) {
3✔
380
            /** @var DOMElement $node */
381
            switch ($node->localName) {
3✔
382
                case 'name':
3✔
383
                    $formField->setName($node->getAttribute('w:val'));
3✔
384

385
                    break;
3✔
386
                case 'ddList':
3✔
387
                    $listEntries = [];
1✔
388
                    foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
1✔
389
                        switch ($ddListNode->localName) {
1✔
390
                            case 'result':
1✔
391
                                $formField->setValue($xmlReader->getAttribute('w:val', $ddListNode));
1✔
392

393
                                break;
1✔
394
                            case 'default':
1✔
395
                                $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
×
396

397
                                break;
×
398
                            case 'listEntry':
1✔
399
                                $listEntries[] = $xmlReader->getAttribute('w:val', $ddListNode);
1✔
400

401
                                break;
1✔
402
                        }
403
                    }
404
                    $formField->setEntries($listEntries);
1✔
405
                    if (null !== $formField->getValue()) {
1✔
406
                        $formField->setText($listEntries[$formField->getValue()]);
1✔
407
                    }
408

409
                    break;
1✔
410
                case 'textInput':
3✔
411
                    foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
1✔
412
                        switch ($ddListNode->localName) {
1✔
413
                            case 'default':
1✔
414
                                $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
1✔
415

416
                                break;
1✔
417
                            case 'format':
1✔
418
                            case 'maxLength':
1✔
419
                                break;
1✔
420
                        }
421
                    }
422

423
                    break;
1✔
424
                case 'checkBox':
3✔
425
                    foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
1✔
426
                        switch ($ddListNode->localName) {
1✔
427
                            case 'default':
1✔
428
                                $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
1✔
429

430
                                break;
1✔
431
                            case 'checked':
1✔
432
                                $formField->setValue($xmlReader->getAttribute('w:val', $ddListNode));
1✔
433

434
                                break;
1✔
435
                            case 'size':
1✔
436
                            case 'sizeAuto':
1✔
437
                                break;
1✔
438
                        }
439
                    }
440

441
                    break;
1✔
442
            }
443
        }
444

445
        if ('textinput' == $formType) {
3✔
446
            $ignoreText = true;
1✔
447
            $textContent = '';
1✔
448
            foreach ($domNodes as $node) {
1✔
449
                if ($xmlReader->elementExists('w:fldChar', $node)) {
1✔
450
                    $fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar');
1✔
451
                    if ('separate' == $fldCharType) {
1✔
452
                        $ignoreText = false;
1✔
453
                    } elseif ('end' == $fldCharType) {
1✔
454
                        $ignoreText = true;
1✔
455
                    }
456
                }
457

458
                if (false === $ignoreText) {
1✔
459
                    $textContent .= $xmlReader->getValue('w:t', $node);
1✔
460
                }
461
            }
462
            $formField->setValue(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'));
1✔
463
            $formField->setText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'));
1✔
464
        }
465
    }
466

467
    /**
468
     * Returns the depth of the Heading, returns 0 for a Title.
469
     *
470
     * @return null|number
471
     */
472
    private function getHeadingDepth(?array $paragraphStyle = null)
473
    {
474
        if (is_array($paragraphStyle) && isset($paragraphStyle['styleName'])) {
18✔
475
            if ('Title' === $paragraphStyle['styleName']) {
9✔
476
                return 0;
4✔
477
            }
478

479
            $headingMatches = [];
5✔
480
            preg_match('/Heading(\d)/', $paragraphStyle['styleName'], $headingMatches);
5✔
481
            if (!empty($headingMatches)) {
5✔
482
                return $headingMatches[1];
3✔
483
            }
484
        }
485

486
        return null;
15✔
487
    }
488

489
    /**
490
     * Read w:r.
491
     *
492
     * @param AbstractContainer $parent
493
     * @param string $docPart
494
     * @param mixed $paragraphStyle
495
     *
496
     * @todo Footnote paragraph style
497
     */
498
    protected function readRun(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart, $paragraphStyle = null): void
499
    {
500
        if (in_array($domNode->nodeName, ['w:ins', 'w:del', 'w:smartTag', 'w:hyperlink', 'w:commentReference'])) {
31✔
501
            $nodes = $xmlReader->getElements('*', $domNode);
5✔
502
            foreach ($nodes as $node) {
5✔
503
                $this->readRun($xmlReader, $node, $parent, $docPart, $paragraphStyle);
5✔
504
            }
505
        } elseif ($domNode->nodeName == 'w:r') {
31✔
506
            $fontStyle = $this->readFontStyle($xmlReader, $domNode);
31✔
507
            $nodes = $xmlReader->getElements('*', $domNode);
31✔
508
            foreach ($nodes as $node) {
31✔
509
                $this->readRunChild($xmlReader, $node, $parent, $docPart, $paragraphStyle, $fontStyle);
31✔
510
            }
511
        }
512

513
        if ($xmlReader->elementExists('.//*["commentReference"=local-name()]', $domNode)) {
31✔
514
            $node = iterator_to_array($xmlReader->getElements('.//*["commentReference"=local-name()]', $domNode))[0];
1✔
515
            $attributeIdentifier = $node->attributes->getNamedItem('id');
1✔
516
            if ($attributeIdentifier) {
1✔
517
                $id = $attributeIdentifier->nodeValue;
1✔
518

519
                $this->setCommentReference('start', $id, $parent->getElement($parent->countElements() - 1));
1✔
520
                $this->setCommentReference('end', $id, $parent->getElement($parent->countElements() - 1));
1✔
521
            }
522
        }
523
    }
524

525
    /**
526
     * Parses nodes under w:r.
527
     *
528
     * @param string $docPart
529
     * @param mixed $paragraphStyle
530
     * @param mixed $fontStyle
531
     */
532
    protected function readRunChild(XMLReader $xmlReader, DOMElement $node, AbstractContainer $parent, $docPart, $paragraphStyle = null, $fontStyle = null): void
533
    {
534
        $runParent = $node->parentNode->parentNode;
31✔
535
        if ($node->nodeName == 'w:footnoteReference') {
31✔
536
            // Footnote
537
            $wId = $xmlReader->getAttribute('w:id', $node);
3✔
538
            $footnote = $parent->addFootnote();
3✔
539
            $footnote->setRelationId($wId);
3✔
540
        } elseif ($node->nodeName == 'w:endnoteReference') {
31✔
541
            // Endnote
542
            $wId = $xmlReader->getAttribute('w:id', $node);
3✔
543
            $endnote = $parent->addEndnote();
3✔
544
            $endnote->setRelationId($wId);
3✔
545
        } elseif ($node->nodeName == 'w:pict') {
31✔
546
            // Image
547
            $rId = $xmlReader->getAttribute('r:id', $node, 'v:shape/v:imagedata');
1✔
548
            $target = $this->getMediaTarget($docPart, $rId);
1✔
549
            if ($this->hasImageLoading() && null !== $target) {
1✔
550
                if ('External' == $this->getTargetMode($docPart, $rId)) {
1✔
551
                    $imageSource = $target;
×
552
                } else {
553
                    $imageSource = "zip://{$this->docFile}#{$target}";
1✔
554
                }
555
                $parent->addImage($imageSource);
1✔
556
            }
557
        } elseif ($node->nodeName == 'w:drawing') {
31✔
558
            // Office 2011 Image
559
            $xmlReader->registerNamespace('wp', 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing');
5✔
560
            $xmlReader->registerNamespace('r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
5✔
561
            $xmlReader->registerNamespace('pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture');
5✔
562
            $xmlReader->registerNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
5✔
563

564
            $name = $xmlReader->getAttribute('name', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
5✔
565
            $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
5✔
566
            if ($name === null && $embedId === null) { // some Converters puts images on a different path
5✔
567
                $name = $xmlReader->getAttribute('name', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
×
568
                $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
×
569
            }
570
            $target = $this->getMediaTarget($docPart, $embedId);
5✔
571
            if ($this->hasImageLoading() && null !== $target) {
5✔
572
                $imageSource = "zip://{$this->docFile}#{$target}";
3✔
573
                $parent->addImage($imageSource, null, null, false, $name);
5✔
574
            }
575
        } elseif ($node->nodeName == 'w:object') {
30✔
576
            // Object
577
            $rId = $xmlReader->getAttribute('r:id', $node, 'o:OLEObject');
2✔
578
            // $rIdIcon = $xmlReader->getAttribute('r:id', $domNode, 'w:object/v:shape/v:imagedata');
579
            $target = $this->getMediaTarget($docPart, $rId);
2✔
580
            if (null !== $target) {
2✔
581
                $textContent = "&lt;Object: {$target}>";
2✔
582
                $parent->addText($textContent, $fontStyle, $paragraphStyle);
2✔
583
            }
584
        } elseif ($node->nodeName == 'w:br') {
30✔
585
            $parent->addTextBreak();
3✔
586
        } elseif ($node->nodeName == 'w:tab') {
30✔
587
            $parent->addText("\t");
1✔
588
        } elseif ($node->nodeName == 'mc:AlternateContent') {
30✔
589
            if ($node->hasChildNodes()) {
1✔
590
                // Get fallback instead of mc:Choice to make sure it is compatible
591
                $fallbackElements = $node->getElementsByTagName('Fallback');
1✔
592

593
                if ($fallbackElements->length) {
1✔
594
                    $fallback = $fallbackElements->item(0);
1✔
595
                    // TextRun
596
                    $textContent = htmlspecialchars($fallback->nodeValue, ENT_QUOTES, 'UTF-8');
1✔
597

598
                    $parent->addText($textContent, $fontStyle, $paragraphStyle);
1✔
599
                }
600
            }
601
        } elseif ($node->nodeName == 'w:t' || $node->nodeName == 'w:delText') {
29✔
602
            // TextRun
603
            $textContent = htmlspecialchars($xmlReader->getValue('.', $node), ENT_QUOTES, 'UTF-8');
28✔
604

605
            if ($runParent->nodeName == 'w:hyperlink') {
28✔
606
                $rId = $xmlReader->getAttribute('r:id', $runParent);
2✔
607
                $target = $this->getMediaTarget($docPart, $rId);
2✔
608
                if (null !== $target) {
2✔
609
                    $parent->addLink($target, $textContent, $fontStyle, $paragraphStyle);
2✔
610
                } else {
611
                    $parent->addText($textContent, $fontStyle, $paragraphStyle);
2✔
612
                }
613
            } else {
614
                /** @var AbstractElement $element */
615
                $element = $parent->addText($textContent, $fontStyle, $paragraphStyle);
28✔
616
                if (in_array($runParent->nodeName, ['w:ins', 'w:del'])) {
28✔
617
                    $type = ($runParent->nodeName == 'w:del') ? TrackChange::DELETED : TrackChange::INSERTED;
3✔
618
                    $author = $runParent->getAttribute('w:author');
3✔
619
                    $date = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $runParent->getAttribute('w:date'));
3✔
620
                    $date = $date instanceof DateTime ? $date : null;
3✔
621
                    $element->setChangeInfo($type, $author, $date);
28✔
622
                }
623
            }
624
        } elseif ($node->nodeName == 'w:softHyphen') {
20✔
625
            $element = $parent->addText("\u{200c}", $fontStyle, $paragraphStyle);
×
626
        } elseif ($node->nodeName == 'w:ruby') {
20✔
627
            $rubyPropertiesNode = $xmlReader->getElement('w:rubyPr', $node);
2✔
628
            $properties = $this->readRubyProperties($xmlReader, $rubyPropertiesNode);
2✔
629
            // read base text node
630
            $baseText = new TextRun($paragraphStyle);
2✔
631
            $baseTextNode = $xmlReader->getElement('w:rubyBase/w:r', $node);
2✔
632
            $this->readRun($xmlReader, $baseTextNode, $baseText, $docPart, $paragraphStyle);
2✔
633
            // read the actual ruby text (e.g. furigana in Japanese)
634
            $rubyText = new TextRun($paragraphStyle);
2✔
635
            $rubyTextNode = $xmlReader->getElement('w:rt/w:r', $node);
2✔
636
            $this->readRun($xmlReader, $rubyTextNode, $rubyText, $docPart, $paragraphStyle);
2✔
637
            // add element to parent
638
            $parent->addRuby($baseText, $rubyText, $properties);
2✔
639
        }
640
    }
641

642
    /**
643
     * Read w:rubyPr element.
644
     *
645
     * @param XMLReader $xmlReader reader for XML
646
     * @param DOMElement $domNode w:RubyPr element
647
     *
648
     * @return RubyProperties ruby properties from element
649
     */
650
    protected function readRubyProperties(XMLReader $xmlReader, DOMElement $domNode): RubyProperties
651
    {
652
        $rubyAlignment = $xmlReader->getElement('w:rubyAlign', $domNode)->getAttribute('w:val');
2✔
653
        $rubyHps = $xmlReader->getElement('w:hps', $domNode)->getAttribute('w:val'); // font face
2✔
654
        $rubyHpsRaise = $xmlReader->getElement('w:hpsRaise', $domNode)->getAttribute('w:val'); // pts above base text
2✔
655
        $rubyHpsBaseText = $xmlReader->getElement('w:hpsBaseText', $domNode)->getAttribute('w:val'); // base text size
2✔
656
        $rubyLid = $xmlReader->getElement('w:lid', $domNode)->getAttribute('w:val'); // type of ruby
2✔
657
        $properties = new RubyProperties();
2✔
658
        $properties->setAlignment($rubyAlignment);
2✔
659
        $properties->setFontFaceSize((float) $rubyHps);
2✔
660
        $properties->setFontPointsAboveBaseText((float) $rubyHpsRaise);
2✔
661
        $properties->setFontSizeForBaseText((float) $rubyHpsBaseText);
2✔
662
        $properties->setLanguageId($rubyLid);
2✔
663

664
        return $properties;
2✔
665
    }
666

667
    /**
668
     * Read w:tbl.
669
     *
670
     * @param mixed $parent
671
     * @param string $docPart
672
     */
673
    protected function readTable(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void
674
    {
675
        // Table style
676
        $tblStyle = null;
11✔
677
        if ($xmlReader->elementExists('w:tblPr', $domNode)) {
11✔
678
            $tblStyle = $this->readTableStyle($xmlReader, $domNode);
7✔
679
        }
680

681
        /** @var \PhpOffice\PhpWord\Element\Table $table Type hint */
682
        $table = $parent->addTable($tblStyle);
11✔
683
        $tblNodes = $xmlReader->getElements('*', $domNode);
11✔
684
        foreach ($tblNodes as $tblNode) {
11✔
685
            if ('w:tblGrid' == $tblNode->nodeName) { // Column
11✔
686
                // @todo Do something with table columns
687
            } elseif ('w:tr' == $tblNode->nodeName) { // Row
11✔
688
                $rowHeight = $xmlReader->getAttribute('w:val', $tblNode, 'w:trPr/w:trHeight');
6✔
689
                $rowHRule = $xmlReader->getAttribute('w:hRule', $tblNode, 'w:trPr/w:trHeight');
6✔
690
                $rowHRule = $rowHRule == 'exact';
6✔
691
                $rowStyle = [
6✔
692
                    'tblHeader' => $xmlReader->elementExists('w:trPr/w:tblHeader', $tblNode),
6✔
693
                    'cantSplit' => $xmlReader->elementExists('w:trPr/w:cantSplit', $tblNode),
6✔
694
                    'exactHeight' => $rowHRule,
6✔
695
                ];
6✔
696

697
                $row = $table->addRow($rowHeight, $rowStyle);
6✔
698
                $rowNodes = $xmlReader->getElements('*', $tblNode);
6✔
699
                foreach ($rowNodes as $rowNode) {
6✔
700
                    if ('w:trPr' == $rowNode->nodeName) { // Row style
6✔
701
                        // @todo Do something with row style
702
                    } elseif ('w:tc' == $rowNode->nodeName) { // Cell
6✔
703
                        $cellWidth = $xmlReader->getAttribute('w:w', $rowNode, 'w:tcPr/w:tcW');
6✔
704
                        $cellStyle = null;
6✔
705
                        if ($xmlReader->elementExists('w:tcPr', $rowNode)) {
6✔
706
                            $cellStyle = $this->readCellStyle($xmlReader, $rowNode);
5✔
707
                        }
708

709
                        $cell = $row->addCell($cellWidth, $cellStyle);
6✔
710
                        $cellNodes = $xmlReader->getElements('*', $rowNode);
6✔
711
                        foreach ($cellNodes as $cellNode) {
6✔
712
                            if ('w:p' == $cellNode->nodeName) { // Paragraph
6✔
713
                                $this->readParagraph($xmlReader, $cellNode, $cell, $docPart);
3✔
714
                            } elseif ($cellNode->nodeName == 'w:tbl') { // Table
6✔
715
                                $this->readTable($xmlReader, $cellNode, $cell, $docPart);
1✔
716
                            }
717
                        }
718
                    }
719
                }
720
            }
721
        }
722
    }
723

724
    /**
725
     * Read w:pPr.
726
     *
727
     * @return null|array
728
     */
729
    protected function readParagraphStyle(XMLReader $xmlReader, DOMElement $domNode)
730
    {
731
        if (!$xmlReader->elementExists('w:pPr', $domNode)) {
26✔
732
            return null;
13✔
733
        }
734

735
        $styleNode = $xmlReader->getElement('w:pPr', $domNode);
21✔
736
        $styleDefs = [
21✔
737
            'styleName' => [self::READ_VALUE, ['w:pStyle', 'w:name']],
21✔
738
            'alignment' => [self::READ_VALUE, 'w:jc'],
21✔
739
            'basedOn' => [self::READ_VALUE, 'w:basedOn'],
21✔
740
            'next' => [self::READ_VALUE, 'w:next'],
21✔
741
            'indentLeft' => [self::READ_VALUE, 'w:ind', 'w:left'],
21✔
742
            'indentRight' => [self::READ_VALUE, 'w:ind', 'w:right'],
21✔
743
            'indentHanging' => [self::READ_VALUE, 'w:ind', 'w:hanging'],
21✔
744
            'indentFirstLine' => [self::READ_VALUE, 'w:ind', 'w:firstLine'],
21✔
745
            'indentFirstLineChars' => [self::READ_VALUE, 'w:ind', 'w:firstLineChars'],
21✔
746
            'spaceAfter' => [self::READ_VALUE, 'w:spacing', 'w:after'],
21✔
747
            'spaceBefore' => [self::READ_VALUE, 'w:spacing', 'w:before'],
21✔
748
            'widowControl' => [self::READ_FALSE, 'w:widowControl'],
21✔
749
            'keepNext' => [self::READ_TRUE,  'w:keepNext'],
21✔
750
            'keepLines' => [self::READ_TRUE,  'w:keepLines'],
21✔
751
            'pageBreakBefore' => [self::READ_TRUE,  'w:pageBreakBefore'],
21✔
752
            'contextualSpacing' => [self::READ_TRUE,  'w:contextualSpacing'],
21✔
753
            'bidi' => [self::READ_TRUE,  'w:bidi'],
21✔
754
            'suppressAutoHyphens' => [self::READ_TRUE,  'w:suppressAutoHyphens'],
21✔
755
            'borderTopStyle' => [self::READ_VALUE, 'w:pBdr/w:top'],
21✔
756
            'borderTopColor' => [self::READ_VALUE, 'w:pBdr/w:top', 'w:color'],
21✔
757
            'borderTopSize' => [self::READ_VALUE, 'w:pBdr/w:top', 'w:sz'],
21✔
758
            'borderRightStyle' => [self::READ_VALUE, 'w:pBdr/w:right'],
21✔
759
            'borderRightColor' => [self::READ_VALUE, 'w:pBdr/w:right', 'w:color'],
21✔
760
            'borderRightSize' => [self::READ_VALUE, 'w:pBdr/w:right', 'w:sz'],
21✔
761
            'borderBottomStyle' => [self::READ_VALUE, 'w:pBdr/w:bottom'],
21✔
762
            'borderBottomColor' => [self::READ_VALUE, 'w:pBdr/w:bottom', 'w:color'],
21✔
763
            'borderBottomSize' => [self::READ_VALUE, 'w:pBdr/w:bottom', 'w:sz'],
21✔
764
            'borderLeftStyle' => [self::READ_VALUE, 'w:pBdr/w:left'],
21✔
765
            'borderLeftColor' => [self::READ_VALUE, 'w:pBdr/w:left', 'w:color'],
21✔
766
            'borderLeftSize' => [self::READ_VALUE, 'w:pBdr/w:left', 'w:sz'],
21✔
767
        ];
21✔
768

769
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
21✔
770
    }
771

772
    /**
773
     * Read w:rPr.
774
     *
775
     * @return null|array
776
     */
777
    protected function readFontStyle(XMLReader $xmlReader, DOMElement $domNode)
778
    {
779
        if (null === $domNode) {
38✔
780
            return null;
×
781
        }
782
        // Hyperlink has an extra w:r child
783
        if ('w:hyperlink' == $domNode->nodeName) {
38✔
784
            $domNode = $xmlReader->getElement('w:r', $domNode);
×
785
        }
786
        if (!$xmlReader->elementExists('w:rPr', $domNode)) {
38✔
787
            return null;
33✔
788
        }
789

790
        $styleNode = $xmlReader->getElement('w:rPr', $domNode);
27✔
791
        $styleDefs = [
27✔
792
            'styleName' => [self::READ_VALUE, 'w:rStyle'],
27✔
793
            'name' => [self::READ_VALUE, 'w:rFonts', ['w:ascii', 'w:hAnsi', 'w:eastAsia', 'w:cs']],
27✔
794
            'hint' => [self::READ_VALUE, 'w:rFonts', 'w:hint'],
27✔
795
            'size' => [self::READ_SIZE,  ['w:sz', 'w:szCs']],
27✔
796
            'color' => [self::READ_VALUE, 'w:color'],
27✔
797
            'underline' => [self::READ_VALUE, 'w:u'],
27✔
798
            'bold' => [self::READ_TRUE,  'w:b'],
27✔
799
            'italic' => [self::READ_TRUE,  'w:i'],
27✔
800
            'strikethrough' => [self::READ_TRUE,  'w:strike'],
27✔
801
            'doubleStrikethrough' => [self::READ_TRUE,  'w:dstrike'],
27✔
802
            'smallCaps' => [self::READ_TRUE,  'w:smallCaps'],
27✔
803
            'allCaps' => [self::READ_TRUE,  'w:caps'],
27✔
804
            'superScript' => [self::READ_EQUAL, 'w:vertAlign', 'w:val', 'superscript'],
27✔
805
            'subScript' => [self::READ_EQUAL, 'w:vertAlign', 'w:val', 'subscript'],
27✔
806
            'fgColor' => [self::READ_VALUE, 'w:highlight'],
27✔
807
            'rtl' => [self::READ_TRUE,  'w:rtl'],
27✔
808
            'lang' => [self::READ_VALUE, 'w:lang'],
27✔
809
            'position' => [self::READ_VALUE, 'w:position'],
27✔
810
            'hidden' => [self::READ_TRUE,  'w:vanish'],
27✔
811
        ];
27✔
812

813
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
27✔
814
    }
815

816
    /**
817
     * Read w:tblPr.
818
     *
819
     * @return null|array|string
820
     *
821
     * @todo Capture w:tblStylePr w:type="firstRow"
822
     */
823
    protected function readTableStyle(XMLReader $xmlReader, DOMElement $domNode)
824
    {
825
        $style = null;
12✔
826
        $margins = ['top', 'left', 'bottom', 'right'];
12✔
827
        $borders = array_merge($margins, ['insideH', 'insideV']);
12✔
828

829
        if ($xmlReader->elementExists('w:tblPr', $domNode)) {
12✔
830
            if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) {
12✔
831
                $style = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle');
×
832
            } else {
833
                $styleNode = $xmlReader->getElement('w:tblPr', $domNode);
12✔
834
                $styleDefs = [];
12✔
835
                foreach ($margins as $side) {
12✔
836
                    $ucfSide = ucfirst($side);
12✔
837
                    $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w'];
12✔
838
                }
839
                foreach ($borders as $side) {
12✔
840
                    $ucfSide = ucfirst($side);
12✔
841
                    $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz'];
12✔
842
                    $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color'];
12✔
843
                    $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val'];
12✔
844
                }
845
                $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type'];
12✔
846
                $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual'];
12✔
847
                $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w'];
12✔
848
                $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
12✔
849

850
                $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode);
12✔
851
                if ($tablePositionNode !== null) {
12✔
852
                    $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode);
1✔
853
                }
854

855
                $indentNode = $xmlReader->getElement('w:tblInd', $styleNode);
12✔
856
                if ($indentNode !== null) {
12✔
857
                    $style['indent'] = $this->readTableIndent($xmlReader, $indentNode);
7✔
858
                }
859
            }
860
        }
861

862
        return $style;
12✔
863
    }
864

865
    /**
866
     * Read w:tblpPr.
867
     *
868
     * @return array
869
     */
870
    private function readTablePosition(XMLReader $xmlReader, DOMElement $domNode)
871
    {
872
        $styleDefs = [
1✔
873
            'leftFromText' => [self::READ_VALUE, '.', 'w:leftFromText'],
1✔
874
            'rightFromText' => [self::READ_VALUE, '.', 'w:rightFromText'],
1✔
875
            'topFromText' => [self::READ_VALUE, '.', 'w:topFromText'],
1✔
876
            'bottomFromText' => [self::READ_VALUE, '.', 'w:bottomFromText'],
1✔
877
            'vertAnchor' => [self::READ_VALUE, '.', 'w:vertAnchor'],
1✔
878
            'horzAnchor' => [self::READ_VALUE, '.', 'w:horzAnchor'],
1✔
879
            'tblpXSpec' => [self::READ_VALUE, '.', 'w:tblpXSpec'],
1✔
880
            'tblpX' => [self::READ_VALUE, '.', 'w:tblpX'],
1✔
881
            'tblpYSpec' => [self::READ_VALUE, '.', 'w:tblpYSpec'],
1✔
882
            'tblpY' => [self::READ_VALUE, '.', 'w:tblpY'],
1✔
883
        ];
1✔
884

885
        return $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
1✔
886
    }
887

888
    /**
889
     * Read w:tblInd.
890
     *
891
     * @return TblWidthComplexType
892
     */
893
    private function readTableIndent(XMLReader $xmlReader, DOMElement $domNode)
894
    {
895
        $styleDefs = [
7✔
896
            'value' => [self::READ_VALUE, '.', 'w:w'],
7✔
897
            'type' => [self::READ_VALUE, '.', 'w:type'],
7✔
898
        ];
7✔
899
        $styleDefs = $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
7✔
900

901
        return new TblWidthComplexType((int) $styleDefs['value'], $styleDefs['type']);
7✔
902
    }
903

904
    /**
905
     * Read w:tcPr.
906
     *
907
     * @return null|array
908
     */
909
    private function readCellStyle(XMLReader $xmlReader, DOMElement $domNode)
910
    {
911
        $styleDefs = [
5✔
912
            'valign' => [self::READ_VALUE, 'w:vAlign'],
5✔
913
            'textDirection' => [self::READ_VALUE, 'w:textDirection'],
5✔
914
            'gridSpan' => [self::READ_VALUE, 'w:gridSpan'],
5✔
915
            'vMerge' => [self::READ_VALUE, 'w:vMerge', null, null, 'continue'],
5✔
916
            'bgColor' => [self::READ_VALUE, 'w:shd', 'w:fill'],
5✔
917
            'noWrap' => [self::READ_VALUE, 'w:noWrap', null, null, true],
5✔
918
        ];
5✔
919
        $style = null;
5✔
920

921
        if ($xmlReader->elementExists('w:tcPr', $domNode)) {
5✔
922
            $styleNode = $xmlReader->getElement('w:tcPr', $domNode);
5✔
923

924
            $borders = ['top', 'left', 'bottom', 'right'];
5✔
925
            foreach ($borders as $side) {
5✔
926
                $ucfSide = ucfirst($side);
5✔
927

928
                $styleDefs['border' . $ucfSide . 'Size'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:sz'];
5✔
929
                $styleDefs['border' . $ucfSide . 'Color'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:color'];
5✔
930
                $styleDefs['border' . $ucfSide . 'Style'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:val'];
5✔
931
            }
932

933
            $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
5✔
934
        }
935

936
        return $style;
5✔
937
    }
938

939
    /**
940
     * Returns the first child element found.
941
     *
942
     * @param null|array|string $elements
943
     *
944
     * @return null|string
945
     */
946
    private function findPossibleElement(XMLReader $xmlReader, ?DOMElement $parentNode = null, $elements = null)
947
    {
948
        if (is_array($elements)) {
41✔
949
            //if element is an array, we take the first element that exists in the XML
950
            foreach ($elements as $possibleElement) {
32✔
951
                if ($xmlReader->elementExists($possibleElement, $parentNode)) {
32✔
952
                    return $possibleElement;
20✔
953
                }
954
            }
955
        } else {
956
            return $elements;
41✔
957
        }
958

959
        return null;
32✔
960
    }
961

962
    /**
963
     * Returns the first attribute found.
964
     *
965
     * @param array|string $attributes
966
     *
967
     * @return null|string
968
     */
969
    private function findPossibleAttribute(XMLReader $xmlReader, DOMElement $node, $attributes)
970
    {
971
        //if attribute is an array, we take the first attribute that exists in the XML
972
        if (is_array($attributes)) {
40✔
973
            foreach ($attributes as $possibleAttribute) {
19✔
974
                if ($xmlReader->getAttribute($possibleAttribute, $node)) {
19✔
975
                    return $possibleAttribute;
15✔
976
                }
977
            }
978

979
            return null;
8✔
980
        }
981

982
        return $attributes;
40✔
983
    }
984

985
    /**
986
     * Read style definition.
987
     *
988
     * @param array $styleDefs
989
     *
990
     * @ignoreScrutinizerPatch
991
     *
992
     * @return array
993
     */
994
    protected function readStyleDefs(XMLReader $xmlReader, ?DOMElement $parentNode = null, $styleDefs = [])
995
    {
996
        $styles = [];
41✔
997

998
        foreach ($styleDefs as $styleProp => $styleVal) {
41✔
999
            [$method, $element, $attribute, $expected, $default] = array_pad($styleVal, 5, null);
41✔
1000

1001
            $element = $this->findPossibleElement($xmlReader, $parentNode, $element);
41✔
1002
            if ($element === null) {
41✔
1003
                continue;
32✔
1004
            }
1005

1006
            if ($xmlReader->elementExists($element, $parentNode)) {
41✔
1007
                $node = $xmlReader->getElement($element, $parentNode);
40✔
1008

1009
                $attribute = $this->findPossibleAttribute($xmlReader, $node, $attribute);
40✔
1010

1011
                // Use w:val as default if no attribute assigned
1012
                $attribute = ($attribute === null) ? 'w:val' : $attribute;
40✔
1013
                $attributeValue = $xmlReader->getAttribute($attribute, $node) ?? $default;
40✔
1014

1015
                $styleValue = $this->readStyleDef($method, $attributeValue, $expected);
40✔
1016
                if ($styleValue !== null) {
40✔
1017
                    $styles[$styleProp] = $styleValue;
40✔
1018
                }
1019
            }
1020
        }
1021

1022
        return $styles;
41✔
1023
    }
1024

1025
    /**
1026
     * Return style definition based on conversion method.
1027
     *
1028
     * @param string $method
1029
     *
1030
     * @ignoreScrutinizerPatch
1031
     *
1032
     * @param null|string $attributeValue
1033
     * @param mixed $expected
1034
     *
1035
     * @return mixed
1036
     */
1037
    private function readStyleDef($method, $attributeValue, $expected)
1038
    {
1039
        $style = $attributeValue;
40✔
1040

1041
        if (self::READ_SIZE == $method) {
40✔
1042
            $style = $attributeValue / 2;
20✔
1043
        } elseif (self::READ_TRUE == $method) {
40✔
1044
            $style = $this->isOn($attributeValue);
15✔
1045
        } elseif (self::READ_FALSE == $method) {
37✔
1046
            $style = !$this->isOn($attributeValue);
4✔
1047
        } elseif (self::READ_EQUAL == $method) {
37✔
1048
            $style = $attributeValue == $expected;
8✔
1049
        }
1050

1051
        return $style;
40✔
1052
    }
1053

1054
    /**
1055
     * Parses the value of the on/off value, null is considered true as it means the w:val attribute was not present.
1056
     *
1057
     * @see http://www.datypic.com/sc/ooxml/t-w_ST_OnOff.html
1058
     *
1059
     * @param string $value
1060
     *
1061
     * @return bool
1062
     */
1063
    private function isOn($value = null)
1064
    {
1065
        return $value === null || $value === '1' || $value === 'true' || $value === 'on';
15✔
1066
    }
1067

1068
    /**
1069
     * Returns the target of image, object, or link as stored in ::readMainRels.
1070
     *
1071
     * @param string $docPart
1072
     * @param string $rId
1073
     *
1074
     * @return null|string
1075
     */
1076
    private function getMediaTarget($docPart, $rId)
1077
    {
1078
        $target = null;
6✔
1079

1080
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
6✔
1081
            $target = $this->rels[$docPart][$rId]['target'];
5✔
1082
        }
1083

1084
        return $target;
6✔
1085
    }
1086

1087
    /**
1088
     * Returns the target mode.
1089
     *
1090
     * @param string $docPart
1091
     * @param string $rId
1092
     *
1093
     * @return null|string
1094
     */
1095
    private function getTargetMode($docPart, $rId)
1096
    {
1097
        $mode = null;
1✔
1098

1099
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
1✔
1100
            $mode = $this->rels[$docPart][$rId]['targetMode'];
1✔
1101
        }
1102

1103
        return $mode;
1✔
1104
    }
1105
}
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

© 2026 Coveralls, Inc