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

PHPOffice / PHPWord / 17391440130

02 Sep 2025 02:24AM UTC coverage: 96.618% (-0.1%) from 96.757%
17391440130

Pull #2820

github

web-flow
Merge 72cf409fe into 0ab0b4940
Pull Request #2820: fix IOFactory::loading Image style lost , textbox lost

16 of 34 new or added lines in 2 files covered. (47.06%)

1 existing line in 1 file now uncovered.

12514 of 12952 relevant lines covered (96.62%)

35.35 hits per line

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

93.85
/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
     * Comment references.
80
     *
81
     * @var array<string, array<string, AbstractElement>>
82
     */
83
    protected $commentRefs = [];
84

85
    /**
86
     * Image Loading.
87
     *
88
     * @var bool
89
     */
90
    protected $imageLoading = true;
91

92
    /**
93
     * Read part.
94
     */
95
    abstract public function read(PhpWord $phpWord);
96

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

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

119
    public function setImageLoading(bool $value): self
120
    {
121
        $this->imageLoading = $value;
15✔
122

123
        return $this;
15✔
124
    }
125

126
    public function hasImageLoading(): bool
127
    {
128
        return $this->imageLoading;
6✔
129
    }
130

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

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

150
        return $this;
15✔
151
    }
152

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

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

170
        return $this;
1✔
171
    }
172

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

184
        return $this->commentRefs[$id];
1✔
185
    }
186

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

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

265
            return;
2✔
266
        } elseif ($xmlReader->elementExists('w:r/w:pict/v:shape/v:textbox', $domNode)) {
30✔
267
            //textbox
NEW
268
            $shapeStyle=null;
×
NEW
269
            $_style = $xmlReader->getAttribute('style', $domNode, 'w:r/w:pict/v:shape');
×
NEW
270
            if ($_style!=null){
×
NEW
271
                $shapeStyle=[];
×
NEW
272
                foreach (explode(';', $_style) as $attribute){
×
NEW
273
                    if (!empty($attribute)){
×
NEW
274
                        [$attributeKey,$attributeVal] = explode(':',$attribute);
×
NEW
275
                        $attributeKey = str_replace('-', ' ', $attributeKey);
×
NEW
276
                        $attributeKey = ucwords($attributeKey);
×
NEW
277
                        $attributeKey = str_replace(' ', '', $attributeKey);
×
NEW
278
                        $attributeVal = preg_replace('/\s*pt\s*/', '', $attributeVal);
×
NEW
279
                        $shapeStyle[$attributeKey]=$attributeVal;
×
280
                    }
281
                }
282
            }
NEW
283
            if (is_array($paragraphStyle) && count($paragraphStyle)>0){
×
NEW
284
                $shapeStyle = array_merge($shapeStyle, $paragraphStyle);
×
285
            }
NEW
286
            $textBox = $parent->addTextBox($shapeStyle);
×
NEW
287
            $nodes = $xmlReader->getElements('w:r/w:pict/v:shape/v:textbox/w:txbxContent/*', $domNode);
×
NEW
288
            foreach ($nodes as $_node){
×
NEW
289
                $this->readParagraph($xmlReader,$_node,$textBox,$docPart);
×
290
            }
UNCOV
291
            return;
×
292
        }
293

294
        // Formula
295
        $xmlReader->registerNamespace('m', 'http://schemas.openxmlformats.org/officeDocument/2006/math');
33✔
296
        if ($xmlReader->elementExists('m:oMath', $domNode)) {
33✔
297
            $mathElement = $xmlReader->getElement('m:oMath', $domNode);
1✔
298
            $mathXML = $mathElement->ownerDocument->saveXML($mathElement);
1✔
299
            if (is_string($mathXML)) {
1✔
300
                $reader = new OfficeMathML();
1✔
301
                $math = $reader->read($mathXML);
1✔
302

303
                $parent->addFormula($math);
1✔
304
            }
305

306
            return;
1✔
307
        }
308

309
        // List item
310
        if ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) {
32✔
311
            $numId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:numId');
3✔
312
            $levelId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:ilvl');
3✔
313
            $nodes = $xmlReader->getElements('*', $domNode);
3✔
314

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

317
            foreach ($nodes as $node) {
3✔
318
                $this->readRun($xmlReader, $node, $listItemRun, $docPart, $paragraphStyle);
3✔
319
            }
320

321
            return;
3✔
322
        }
323

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

340
            return;
7✔
341
        }
342

343
        // Text and TextRun
344
        $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode);
28✔
345
        if (0 === $textRunContainers) {
28✔
346
            $parent->addTextBreak(1, $paragraphStyle);
7✔
347
        } else {
348
            $nodes = $xmlReader->getElements('*', $domNode);
27✔
349
            $paragraph = $parent->addTextRun($paragraphStyle);
27✔
350
            foreach ($nodes as $node) {
27✔
351
                $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle);
27✔
352
            }
353
        }
354
    }
355

356
    /**
357
     * @param DOMElement[] $domNodes
358
     * @param AbstractContainer $parent
359
     * @param mixed $paragraphStyle
360
     * @param string $formType
361
     */
362
    private function readFormField(XMLReader $xmlReader, array $domNodes, $parent, $paragraphStyle, $formType): void
363
    {
364
        if (!in_array($formType, ['textinput', 'checkbox', 'dropdown'])) {
3✔
365
            return;
×
366
        }
367

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

371
        foreach ($xmlReader->getElements('*', $ffData) as $node) {
3✔
372
            /** @var DOMElement $node */
373
            switch ($node->localName) {
3✔
374
                case 'name':
3✔
375
                    $formField->setName($node->getAttribute('w:val'));
3✔
376

377
                    break;
3✔
378
                case 'ddList':
3✔
379
                    $listEntries = [];
1✔
380
                    foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
1✔
381
                        switch ($ddListNode->localName) {
1✔
382
                            case 'result':
1✔
383
                                $formField->setValue($xmlReader->getAttribute('w:val', $ddListNode));
1✔
384

385
                                break;
1✔
386
                            case 'default':
1✔
387
                                $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
×
388

389
                                break;
×
390
                            case 'listEntry':
1✔
391
                                $listEntries[] = $xmlReader->getAttribute('w:val', $ddListNode);
1✔
392

393
                                break;
1✔
394
                        }
395
                    }
396
                    $formField->setEntries($listEntries);
1✔
397
                    if (null !== $formField->getValue()) {
1✔
398
                        $formField->setText($listEntries[$formField->getValue()]);
1✔
399
                    }
400

401
                    break;
1✔
402
                case 'textInput':
3✔
403
                    foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
1✔
404
                        switch ($ddListNode->localName) {
1✔
405
                            case 'default':
1✔
406
                                $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
1✔
407

408
                                break;
1✔
409
                            case 'format':
1✔
410
                            case 'maxLength':
1✔
411
                                break;
1✔
412
                        }
413
                    }
414

415
                    break;
1✔
416
                case 'checkBox':
3✔
417
                    foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
1✔
418
                        switch ($ddListNode->localName) {
1✔
419
                            case 'default':
1✔
420
                                $formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
1✔
421

422
                                break;
1✔
423
                            case 'checked':
1✔
424
                                $formField->setValue($xmlReader->getAttribute('w:val', $ddListNode));
1✔
425

426
                                break;
1✔
427
                            case 'size':
1✔
428
                            case 'sizeAuto':
1✔
429
                                break;
1✔
430
                        }
431
                    }
432

433
                    break;
1✔
434
            }
435
        }
436

437
        if ('textinput' == $formType) {
3✔
438
            $ignoreText = true;
1✔
439
            $textContent = '';
1✔
440
            foreach ($domNodes as $node) {
1✔
441
                if ($xmlReader->elementExists('w:fldChar', $node)) {
1✔
442
                    $fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar');
1✔
443
                    if ('separate' == $fldCharType) {
1✔
444
                        $ignoreText = false;
1✔
445
                    } elseif ('end' == $fldCharType) {
1✔
446
                        $ignoreText = true;
1✔
447
                    }
448
                }
449

450
                if (false === $ignoreText) {
1✔
451
                    $textContent .= $xmlReader->getValue('w:t', $node);
1✔
452
                }
453
            }
454
            $formField->setValue(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'));
1✔
455
            $formField->setText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'));
1✔
456
        }
457
    }
458

459
    /**
460
     * Returns the depth of the Heading, returns 0 for a Title.
461
     *
462
     * @return null|number
463
     */
464
    private function getHeadingDepth(?array $paragraphStyle = null)
465
    {
466
        if (is_array($paragraphStyle) && isset($paragraphStyle['styleName'])) {
18✔
467
            if ('Title' === $paragraphStyle['styleName']) {
9✔
468
                return 0;
4✔
469
            }
470

471
            $headingMatches = [];
5✔
472
            preg_match('/Heading(\d)/', $paragraphStyle['styleName'], $headingMatches);
5✔
473
            if (!empty($headingMatches)) {
5✔
474
                return $headingMatches[1];
3✔
475
            }
476
        }
477

478
        return null;
15✔
479
    }
480

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

505
        if ($xmlReader->elementExists('.//*["commentReference"=local-name()]', $domNode)) {
31✔
506
            $node = iterator_to_array($xmlReader->getElements('.//*["commentReference"=local-name()]', $domNode))[0];
1✔
507
            $attributeIdentifier = $node->attributes->getNamedItem('id');
1✔
508
            if ($attributeIdentifier) {
1✔
509
                $id = $attributeIdentifier->nodeValue;
1✔
510

511
                $this->setCommentReference('start', $id, $parent->getElement($parent->countElements() - 1));
1✔
512
                $this->setCommentReference('end', $id, $parent->getElement($parent->countElements() - 1));
1✔
513
            }
514
        }
515
    }
516

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

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

600
                if ($fallbackElements->length) {
1✔
601
                    $fallback = $fallbackElements->item(0);
1✔
602
                    // TextRun
603
                    $textContent = htmlspecialchars($fallback->nodeValue, ENT_QUOTES, 'UTF-8');
1✔
604

605
                    $parent->addText($textContent, $fontStyle, $paragraphStyle);
1✔
606
                }
607
            }
608
        } elseif ($node->nodeName == 'w:t' || $node->nodeName == 'w:delText') {
29✔
609
            // TextRun
610
            $textContent = htmlspecialchars($xmlReader->getValue('.', $node), ENT_QUOTES, 'UTF-8');
28✔
611

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

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

671
        return $properties;
2✔
672
    }
673

674
    /**
675
     * Read w:tbl.
676
     *
677
     * @param mixed $parent
678
     * @param string $docPart
679
     */
680
    protected function readTable(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void
681
    {
682
        // Table style
683
        $tblStyle = null;
11✔
684
        if ($xmlReader->elementExists('w:tblPr', $domNode)) {
11✔
685
            $tblStyle = $this->readTableStyle($xmlReader, $domNode);
7✔
686
        }
687

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

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

716
                        $cell = $row->addCell($cellWidth, $cellStyle);
6✔
717
                        $cellNodes = $xmlReader->getElements('*', $rowNode);
6✔
718
                        foreach ($cellNodes as $cellNode) {
6✔
719
                            if ('w:p' == $cellNode->nodeName) { // Paragraph
6✔
720
                                $this->readParagraph($xmlReader, $cellNode, $cell, $docPart);
3✔
721
                            } elseif ($cellNode->nodeName == 'w:tbl') { // Table
6✔
722
                                $this->readTable($xmlReader, $cellNode, $cell, $docPart);
1✔
723
                            }
724
                        }
725
                    }
726
                }
727
            }
728
        }
729
    }
730

731
    /**
732
     * Read w:pPr.
733
     *
734
     * @return null|array
735
     */
736
    protected function readParagraphStyle(XMLReader $xmlReader, DOMElement $domNode)
737
    {
738
        if (!$xmlReader->elementExists('w:pPr', $domNode)) {
26✔
739
            return null;
13✔
740
        }
741

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

776
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
21✔
777
    }
778

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

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

820
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
27✔
821
    }
822

823
    /**
824
     * Read w:tblPr.
825
     *
826
     * @return null|array|string
827
     *
828
     * @todo Capture w:tblStylePr w:type="firstRow"
829
     */
830
    protected function readTableStyle(XMLReader $xmlReader, DOMElement $domNode)
831
    {
832
        $style = null;
12✔
833
        $margins = ['top', 'left', 'bottom', 'right'];
12✔
834
        $borders = array_merge($margins, ['insideH', 'insideV']);
12✔
835

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

857
                $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode);
12✔
858
                if ($tablePositionNode !== null) {
12✔
859
                    $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode);
1✔
860
                }
861

862
                $indentNode = $xmlReader->getElement('w:tblInd', $styleNode);
12✔
863
                if ($indentNode !== null) {
12✔
864
                    $style['indent'] = $this->readTableIndent($xmlReader, $indentNode);
7✔
865
                }
866
            }
867
        }
868

869
        return $style;
12✔
870
    }
871

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

892
        return $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
1✔
893
    }
894

895
    /**
896
     * Read w:tblInd.
897
     *
898
     * @return TblWidthComplexType
899
     */
900
    private function readTableIndent(XMLReader $xmlReader, DOMElement $domNode)
901
    {
902
        $styleDefs = [
7✔
903
            'value' => [self::READ_VALUE, '.', 'w:w'],
7✔
904
            'type' => [self::READ_VALUE, '.', 'w:type'],
7✔
905
        ];
7✔
906
        $styleDefs = $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
7✔
907

908
        return new TblWidthComplexType((int) $styleDefs['value'], $styleDefs['type']);
7✔
909
    }
910

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

928
        if ($xmlReader->elementExists('w:tcPr', $domNode)) {
5✔
929
            $styleNode = $xmlReader->getElement('w:tcPr', $domNode);
5✔
930

931
            $borders = ['top', 'left', 'bottom', 'right'];
5✔
932
            foreach ($borders as $side) {
5✔
933
                $ucfSide = ucfirst($side);
5✔
934

935
                $styleDefs['border' . $ucfSide . 'Size'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:sz'];
5✔
936
                $styleDefs['border' . $ucfSide . 'Color'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:color'];
5✔
937
                $styleDefs['border' . $ucfSide . 'Style'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:val'];
5✔
938
            }
939

940
            $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
5✔
941
        }
942

943
        return $style;
5✔
944
    }
945

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

966
        return null;
32✔
967
    }
968

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

986
            return null;
8✔
987
        }
988

989
        return $attributes;
40✔
990
    }
991

992
    /**
993
     * Read style definition.
994
     *
995
     * @param array $styleDefs
996
     *
997
     * @ignoreScrutinizerPatch
998
     *
999
     * @return array
1000
     */
1001
    protected function readStyleDefs(XMLReader $xmlReader, ?DOMElement $parentNode = null, $styleDefs = [])
1002
    {
1003
        $styles = [];
41✔
1004

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

1008
            $element = $this->findPossibleElement($xmlReader, $parentNode, $element);
41✔
1009
            if ($element === null) {
41✔
1010
                continue;
32✔
1011
            }
1012

1013
            if ($xmlReader->elementExists($element, $parentNode)) {
41✔
1014
                $node = $xmlReader->getElement($element, $parentNode);
40✔
1015

1016
                $attribute = $this->findPossibleAttribute($xmlReader, $node, $attribute);
40✔
1017

1018
                // Use w:val as default if no attribute assigned
1019
                $attribute = ($attribute === null) ? 'w:val' : $attribute;
40✔
1020
                $attributeValue = $xmlReader->getAttribute($attribute, $node) ?? $default;
40✔
1021

1022
                $styleValue = $this->readStyleDef($method, $attributeValue, $expected);
40✔
1023
                if ($styleValue !== null) {
40✔
1024
                    $styles[$styleProp] = $styleValue;
40✔
1025
                }
1026
            }
1027
        }
1028

1029
        return $styles;
41✔
1030
    }
1031

1032
    /**
1033
     * Return style definition based on conversion method.
1034
     *
1035
     * @param string $method
1036
     *
1037
     * @ignoreScrutinizerPatch
1038
     *
1039
     * @param null|string $attributeValue
1040
     * @param mixed $expected
1041
     *
1042
     * @return mixed
1043
     */
1044
    private function readStyleDef($method, $attributeValue, $expected)
1045
    {
1046
        $style = $attributeValue;
40✔
1047

1048
        if (self::READ_SIZE == $method) {
40✔
1049
            $style = $attributeValue / 2;
20✔
1050
        } elseif (self::READ_TRUE == $method) {
40✔
1051
            $style = $this->isOn($attributeValue);
15✔
1052
        } elseif (self::READ_FALSE == $method) {
37✔
1053
            $style = !$this->isOn($attributeValue);
4✔
1054
        } elseif (self::READ_EQUAL == $method) {
37✔
1055
            $style = $attributeValue == $expected;
8✔
1056
        }
1057

1058
        return $style;
40✔
1059
    }
1060

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

1075
    /**
1076
     * Returns the target of image, object, or link as stored in ::readMainRels.
1077
     *
1078
     * @param string $docPart
1079
     * @param string $rId
1080
     *
1081
     * @return null|string
1082
     */
1083
    private function getMediaTarget($docPart, $rId)
1084
    {
1085
        $target = null;
6✔
1086

1087
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
6✔
1088
            $target = $this->rels[$docPart][$rId]['target'];
5✔
1089
        }
1090

1091
        return $target;
6✔
1092
    }
1093

1094
    /**
1095
     * Returns the target mode.
1096
     *
1097
     * @param string $docPart
1098
     * @param string $rId
1099
     *
1100
     * @return null|string
1101
     */
1102
    private function getTargetMode($docPart, $rId)
1103
    {
1104
        $mode = null;
1✔
1105

1106
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
1✔
1107
            $mode = $this->rels[$docPart][$rId]['targetMode'];
1✔
1108
        }
1109

1110
        return $mode;
1✔
1111
    }
1112
}
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