See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 3 namespace PhpOffice\PhpSpreadsheet\Reader; 4 5 use PhpOffice\PhpSpreadsheet\Cell\Coordinate; 6 use PhpOffice\PhpSpreadsheet\Cell\DataType; 7 use PhpOffice\PhpSpreadsheet\Cell\Hyperlink; 8 use PhpOffice\PhpSpreadsheet\DefinedName; 9 use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; 10 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\AutoFilter; 11 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Chart; 12 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ColumnAndRowAttributes; 13 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ConditionalStyles; 14 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\DataValidations; 15 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Hyperlinks; 16 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces; 17 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\PageSetup; 18 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader; 19 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions; 20 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews; 21 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles; 22 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme; 23 use PhpOffice\PhpSpreadsheet\Reader\Xlsx\WorkbookView; 24 use PhpOffice\PhpSpreadsheet\ReferenceHelper; 25 use PhpOffice\PhpSpreadsheet\RichText\RichText; 26 use PhpOffice\PhpSpreadsheet\Settings; 27 use PhpOffice\PhpSpreadsheet\Shared\Date; 28 use PhpOffice\PhpSpreadsheet\Shared\Drawing; 29 use PhpOffice\PhpSpreadsheet\Shared\File; 30 use PhpOffice\PhpSpreadsheet\Shared\Font; 31 use PhpOffice\PhpSpreadsheet\Shared\StringHelper; 32 use PhpOffice\PhpSpreadsheet\Spreadsheet; 33 use PhpOffice\PhpSpreadsheet\Style\Color; 34 use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont; 35 use PhpOffice\PhpSpreadsheet\Style\NumberFormat; 36 use PhpOffice\PhpSpreadsheet\Style\Style; 37 use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing; 38 use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; 39 use SimpleXMLElement; 40 use Throwable; 41 use XMLReader; 42 use ZipArchive; 43 44 class Xlsx extends BaseReader 45 { 46 const INITIAL_FILE = '_rels/.rels'; 47 48 /** 49 * ReferenceHelper instance. 50 * 51 * @var ReferenceHelper 52 */ 53 private $referenceHelper; 54 55 /** 56 * @var ZipArchive 57 */ 58 private $zip; 59 60 /** @var Styles */ 61 private $styleReader; 62 63 /** 64 * Create a new Xlsx Reader instance. 65 */ 66 public function __construct() 67 { 68 parent::__construct(); 69 $this->referenceHelper = ReferenceHelper::getInstance(); 70 $this->securityScanner = XmlScanner::getInstance($this); 71 } 72 73 /** 74 * Can the current IReader read the file? 75 */ 76 public function canRead(string $filename): bool 77 { 78 if (!File::testFileNoThrow($filename, self::INITIAL_FILE)) { 79 return false; 80 } 81 82 $result = false; 83 $this->zip = $zip = new ZipArchive(); 84 85 if ($zip->open($filename) === true) { 86 [$workbookBasename] = $this->getWorkbookBaseName(); 87 $result = !empty($workbookBasename); 88 89 $zip->close(); 90 } 91 92 return $result; 93 } 94 95 /** 96 * @param mixed $value 97 */ 98 public static function testSimpleXml($value): SimpleXMLElement 99 { 100 return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><root></root>'); 101 } 102 103 public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement 104 { 105 return self::testSimpleXml($value === null ? $value : $value->attributes($ns)); 106 } 107 108 // Phpstan thinks, correctly, that xpath can return false. 109 // Scrutinizer thinks it can't. 110 // Sigh. 111 private static function xpathNoFalse(SimpleXmlElement $sxml, string $path): array 112 { 113 return self::falseToArray($sxml->xpath($path)); 114 } 115 116 /** 117 * @param mixed $value 118 */ 119 public static function falseToArray($value): array 120 { 121 return is_array($value) ? $value : []; 122 } 123 124 private function loadZip(string $filename, string $ns = ''): SimpleXMLElement 125 { 126 $contents = $this->getFromZipArchive($this->zip, $filename); 127 $rels = simplexml_load_string( 128 $this->securityScanner->scan($contents), 129 'SimpleXMLElement', 130 Settings::getLibXmlLoaderOptions(), 131 $ns 132 ); 133 134 return self::testSimpleXml($rels); 135 } 136 137 // This function is just to identify cases where I'm not sure 138 // why empty namespace is required. 139 private function loadZipNonamespace(string $filename, string $ns): SimpleXMLElement 140 { 141 $contents = $this->getFromZipArchive($this->zip, $filename); 142 $rels = simplexml_load_string( 143 $this->securityScanner->scan($contents), 144 'SimpleXMLElement', 145 Settings::getLibXmlLoaderOptions(), 146 ($ns === '' ? $ns : '') 147 ); 148 149 return self::testSimpleXml($rels); 150 } 151 152 private const REL_TO_MAIN = [ 153 Namespaces::PURL_OFFICE_DOCUMENT => Namespaces::PURL_MAIN, 154 Namespaces::THUMBNAIL => '', 155 ]; 156 157 private const REL_TO_DRAWING = [ 158 Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING, 159 ]; 160 161 private const REL_TO_CHART = [ 162 Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART, 163 ]; 164 165 /** 166 * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. 167 * 168 * @param string $filename 169 * 170 * @return array 171 */ 172 public function listWorksheetNames($filename) 173 { 174 File::assertFile($filename, self::INITIAL_FILE); 175 176 $worksheetNames = []; 177 178 $this->zip = $zip = new ZipArchive(); 179 $zip->open($filename); 180 181 // The files we're looking at here are small enough that simpleXML is more efficient than XMLReader 182 $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS); 183 foreach ($rels->Relationship as $relx) { 184 $rel = self::getAttributes($relx); 185 $relType = (string) $rel['Type']; 186 $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN; 187 if ($mainNS !== '') { 188 $xmlWorkbook = $this->loadZip((string) $rel['Target'], $mainNS); 189 190 if ($xmlWorkbook->sheets) { 191 foreach ($xmlWorkbook->sheets->sheet as $eleSheet) { 192 // Check if sheet should be skipped 193 $worksheetNames[] = (string) self::getAttributes($eleSheet)['name']; 194 } 195 } 196 } 197 } 198 199 $zip->close(); 200 201 return $worksheetNames; 202 } 203 204 /** 205 * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). 206 * 207 * @param string $filename 208 * 209 * @return array 210 */ 211 public function listWorksheetInfo($filename) 212 { 213 File::assertFile($filename, self::INITIAL_FILE); 214 215 $worksheetInfo = []; 216 217 $this->zip = $zip = new ZipArchive(); 218 $zip->open($filename); 219 220 $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS); 221 foreach ($rels->Relationship as $relx) { 222 $rel = self::getAttributes($relx); 223 $relType = (string) $rel['Type']; 224 $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN; 225 if ($mainNS !== '') { 226 $relTarget = (string) $rel['Target']; 227 $dir = dirname($relTarget); 228 $namespace = dirname($relType); 229 $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', ''); 230 231 $worksheets = []; 232 foreach ($relsWorkbook->Relationship as $elex) { 233 $ele = self::getAttributes($elex); 234 if ( 235 ((string) $ele['Type'] === "$namespace/worksheet") || 236 ((string) $ele['Type'] === "$namespace/chartsheet") 237 ) { 238 $worksheets[(string) $ele['Id']] = $ele['Target']; 239 } 240 } 241 242 $xmlWorkbook = $this->loadZip($relTarget, $mainNS); 243 if ($xmlWorkbook->sheets) { 244 $dir = dirname($relTarget); 245 /** @var SimpleXMLElement $eleSheet */ 246 foreach ($xmlWorkbook->sheets->sheet as $eleSheet) { 247 $tmpInfo = [ 248 'worksheetName' => (string) self::getAttributes($eleSheet)['name'], 249 'lastColumnLetter' => 'A', 250 'lastColumnIndex' => 0, 251 'totalRows' => 0, 252 'totalColumns' => 0, 253 ]; 254 255 $fileWorksheet = (string) $worksheets[(string) self::getArrayItem(self::getAttributes($eleSheet, $namespace), 'id')]; 256 $fileWorksheetPath = strpos($fileWorksheet, '/') === 0 ? substr($fileWorksheet, 1) : "$dir/$fileWorksheet"; 257 258 $xml = new XMLReader(); 259 $xml->xml( 260 $this->securityScanner->scanFile( 261 'zip://' . File::realpath($filename) . '#' . $fileWorksheetPath 262 ), 263 null, 264 Settings::getLibXmlLoaderOptions() 265 ); 266 $xml->setParserProperty(2, true); 267 268 $currCells = 0; 269 while ($xml->read()) { 270 if ($xml->localName == 'row' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) { 271 $row = $xml->getAttribute('r'); 272 $tmpInfo['totalRows'] = $row; 273 $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); 274 $currCells = 0; 275 } elseif ($xml->localName == 'c' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) { 276 $cell = $xml->getAttribute('r'); 277 $currCells = $cell ? max($currCells, Coordinate::indexesFromString($cell)[0]) : ($currCells + 1); 278 } 279 } 280 $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); 281 $xml->close(); 282 283 $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; 284 $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); 285 286 $worksheetInfo[] = $tmpInfo; 287 } 288 } 289 } 290 } 291 292 $zip->close(); 293 294 return $worksheetInfo; 295 } 296 297 private static function castToBoolean(SimpleXMLElement $c): bool 298 { 299 $value = isset($c->v) ? (string) $c->v : null; 300 if ($value == '0') { 301 return false; 302 } elseif ($value == '1') { 303 return true; 304 } 305 306 return (bool) $c->v; 307 } 308 309 private static function castToError(?SimpleXMLElement $c): ?string 310 { 311 return isset($c, $c->v) ? (string) $c->v : null; 312 } 313 314 private static function castToString(?SimpleXMLElement $c): ?string 315 { 316 return isset($c, $c->v) ? (string) $c->v : null; 317 } 318 319 /** 320 * @param mixed $value 321 * @param mixed $calculatedValue 322 */ 323 private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void 324 { 325 if ($c === null) { 326 return; 327 } 328 $attr = $c->f->attributes(); 329 $cellDataType = 'f'; 330 $value = "={$c->f}"; 331 $calculatedValue = self::$castBaseType($c); 332 333 // Shared formula? 334 if (isset($attr['t']) && strtolower((string) $attr['t']) == 'shared') { 335 $instance = (string) $attr['si']; 336 337 if (!isset($sharedFormulas[(string) $attr['si']])) { 338 $sharedFormulas[$instance] = ['master' => $r, 'formula' => $value]; 339 } else { 340 $master = Coordinate::indexesFromString($sharedFormulas[$instance]['master']); 341 $current = Coordinate::indexesFromString($r); 342 343 $difference = [0, 0]; 344 $difference[0] = $current[0] - $master[0]; 345 $difference[1] = $current[1] - $master[1]; 346 347 $value = $this->referenceHelper->updateFormulaReferences($sharedFormulas[$instance]['formula'], 'A1', $difference[0], $difference[1]); 348 } 349 } 350 } 351 352 /** 353 * @param string $fileName 354 */ 355 private function fileExistsInArchive(ZipArchive $archive, $fileName = ''): bool 356 { 357 // Root-relative paths 358 if (strpos($fileName, '//') !== false) { 359 $fileName = substr($fileName, strpos($fileName, '//') + 1); 360 } 361 $fileName = File::realpath($fileName); 362 363 // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming 364 // so we need to load case-insensitively from the zip file 365 366 // Apache POI fixes 367 $contents = $archive->locateName($fileName, ZipArchive::FL_NOCASE); 368 if ($contents === false) { 369 $contents = $archive->locateName(substr($fileName, 1), ZipArchive::FL_NOCASE); 370 } 371 372 return $contents !== false; 373 } 374 375 /** 376 * @param string $fileName 377 * 378 * @return string 379 */ 380 private function getFromZipArchive(ZipArchive $archive, $fileName = '') 381 { 382 // Root-relative paths 383 if (strpos($fileName, '//') !== false) { 384 $fileName = substr($fileName, strpos($fileName, '//') + 1); 385 } 386 // Relative paths generated by dirname($filename) when $filename 387 // has no path (i.e.files in root of the zip archive) 388 $fileName = (string) preg_replace('/^\.\//', '', $fileName); 389 $fileName = File::realpath($fileName); 390 391 // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming 392 // so we need to load case-insensitively from the zip file 393 394 // Apache POI fixes 395 $contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE); 396 if ($contents === false) { 397 $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE); 398 } 399 400 return ($contents === false) ? '' : $contents; 401 } 402 403 /** 404 * Loads Spreadsheet from file. 405 */ 406 protected function loadSpreadsheetFromFile(string $filename): Spreadsheet 407 { 408 File::assertFile($filename, self::INITIAL_FILE); 409 410 // Initialisations 411 $excel = new Spreadsheet(); 412 $excel->removeSheetByIndex(0); 413 $addingFirstCellStyleXf = true; 414 $addingFirstCellXf = true; 415 416 $unparsedLoadedData = []; 417 418 $this->zip = $zip = new ZipArchive(); 419 $zip->open($filename); 420 421 // Read the theme first, because we need the colour scheme when reading the styles 422 [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName(); 423 $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; 424 $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART; 425 $wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS); 426 $theme = null; 427 $this->styleReader = new Styles(); 428 foreach ($wbRels->Relationship as $relx) { 429 $rel = self::getAttributes($relx); 430 $relTarget = (string) $rel['Target']; 431 if (substr($relTarget, 0, 4) === '/xl/') { 432 $relTarget = substr($relTarget, 4); 433 } 434 switch ($rel['Type']) { 435 case "$xmlNamespaceBase/theme": 436 $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2']; 437 $themeOrderAdditional = count($themeOrderArray); 438 439 $xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS); 440 $xmlThemeName = self::getAttributes($xmlTheme); 441 $xmlTheme = $xmlTheme->children($drawingNS); 442 $themeName = (string) $xmlThemeName['name']; 443 444 $colourScheme = self::getAttributes($xmlTheme->themeElements->clrScheme); 445 $colourSchemeName = (string) $colourScheme['name']; 446 $colourScheme = $xmlTheme->themeElements->clrScheme->children($drawingNS); 447 448 $themeColours = []; 449 foreach ($colourScheme as $k => $xmlColour) { 450 $themePos = array_search($k, $themeOrderArray); 451 if ($themePos === false) { 452 $themePos = $themeOrderAdditional++; 453 } 454 if (isset($xmlColour->sysClr)) { 455 $xmlColourData = self::getAttributes($xmlColour->sysClr); 456 $themeColours[$themePos] = (string) $xmlColourData['lastClr']; 457 } elseif (isset($xmlColour->srgbClr)) { 458 $xmlColourData = self::getAttributes($xmlColour->srgbClr); 459 $themeColours[$themePos] = (string) $xmlColourData['val']; 460 } 461 } 462 $theme = new Theme($themeName, $colourSchemeName, $themeColours); 463 $this->styleReader->setTheme($theme); 464 465 break; 466 } 467 } 468 469 $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS); 470 471 $propertyReader = new PropertyReader($this->securityScanner, $excel->getProperties()); 472 foreach ($rels->Relationship as $relx) { 473 $rel = self::getAttributes($relx); 474 $relTarget = (string) $rel['Target']; 475 $relType = (string) $rel['Type']; 476 $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN; 477 switch ($relType) { 478 case Namespaces::CORE_PROPERTIES: 479 $propertyReader->readCoreProperties($this->getFromZipArchive($zip, $relTarget)); 480 481 break; 482 case "$xmlNamespaceBase/extended-properties": 483 $propertyReader->readExtendedProperties($this->getFromZipArchive($zip, $relTarget)); 484 485 break; 486 case "$xmlNamespaceBase/custom-properties": 487 $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget)); 488 489 break; 490 //Ribbon 491 case Namespaces::EXTENSIBILITY: 492 $customUI = $relTarget; 493 if ($customUI) { 494 $this->readRibbon($excel, $customUI, $zip); 495 } 496 497 break; 498 case "$xmlNamespaceBase/officeDocument": 499 $dir = dirname($relTarget); 500 501 // Do not specify namespace in next stmt - do it in Xpath 502 $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', ''); 503 $relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS); 504 505 $sharedStrings = []; 506 $relType = "rel:Relationship[@Type='" 507 //. Namespaces::SHARED_STRINGS 508 . "$xmlNamespaceBase/sharedStrings" 509 . "']"; 510 $xpath = self::getArrayItem($relsWorkbook->xpath($relType)); 511 512 if ($xpath) { 513 $xmlStrings = $this->loadZip("$dir/$xpath[Target]", $mainNS); 514 if (isset($xmlStrings->si)) { 515 foreach ($xmlStrings->si as $val) { 516 if (isset($val->t)) { 517 $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t); 518 } elseif (isset($val->r)) { 519 $sharedStrings[] = $this->parseRichText($val); 520 } 521 } 522 } 523 } 524 525 $worksheets = []; 526 $macros = $customUI = null; 527 foreach ($relsWorkbook->Relationship as $elex) { 528 $ele = self::getAttributes($elex); 529 switch ($ele['Type']) { 530 case Namespaces::WORKSHEET: 531 case Namespaces::PURL_WORKSHEET: 532 $worksheets[(string) $ele['Id']] = $ele['Target']; 533 534 break; 535 case Namespaces::CHARTSHEET: 536 if ($this->includeCharts === true) { 537 $worksheets[(string) $ele['Id']] = $ele['Target']; 538 } 539 540 break; 541 // a vbaProject ? (: some macros) 542 case Namespaces::VBA: 543 $macros = $ele['Target']; 544 545 break; 546 } 547 } 548 549 if ($macros !== null) { 550 $macrosCode = $this->getFromZipArchive($zip, 'xl/vbaProject.bin'); //vbaProject.bin always in 'xl' dir and always named vbaProject.bin 551 if ($macrosCode !== false) { 552 $excel->setMacrosCode($macrosCode); 553 $excel->setHasMacros(true); 554 //short-circuit : not reading vbaProject.bin.rel to get Signature =>allways vbaProjectSignature.bin in 'xl' dir 555 $Certificate = $this->getFromZipArchive($zip, 'xl/vbaProjectSignature.bin'); 556 if ($Certificate !== false) { 557 $excel->setMacrosCertificate($Certificate); 558 } 559 } 560 } 561 562 $relType = "rel:Relationship[@Type='" 563 . "$xmlNamespaceBase/styles" 564 . "']"; 565 $xpath = self::getArrayItem(self::xpathNoFalse($relsWorkbook, $relType)); 566 567 if ($xpath === null) { 568 $xmlStyles = self::testSimpleXml(null); 569 } else { 570 $xmlStyles = $this->loadZip("$dir/$xpath[Target]", $mainNS); 571 } 572 573 $palette = self::extractPalette($xmlStyles); 574 $this->styleReader->setWorkbookPalette($palette); 575 $fills = self::extractStyles($xmlStyles, 'fills', 'fill'); 576 $fonts = self::extractStyles($xmlStyles, 'fonts', 'font'); 577 $borders = self::extractStyles($xmlStyles, 'borders', 'border'); 578 $xfTags = self::extractStyles($xmlStyles, 'cellXfs', 'xf'); 579 $cellXfTags = self::extractStyles($xmlStyles, 'cellStyleXfs', 'xf'); 580 581 $styles = []; 582 $cellStyles = []; 583 $numFmts = null; 584 if (/*$xmlStyles && */ $xmlStyles->numFmts[0]) { 585 $numFmts = $xmlStyles->numFmts[0]; 586 } 587 if (isset($numFmts) && ($numFmts !== null)) { 588 $numFmts->registerXPathNamespace('sml', $mainNS); 589 } 590 $this->styleReader->setNamespace($mainNS); 591 if (!$this->readDataOnly/* && $xmlStyles*/) { 592 foreach ($xfTags as $xfTag) { 593 $xf = self::getAttributes($xfTag); 594 $numFmt = null; 595 596 if ($xf['numFmtId']) { 597 if (isset($numFmts)) { 598 $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]")); 599 600 if (isset($tmpNumFmt['formatCode'])) { 601 $numFmt = (string) $tmpNumFmt['formatCode']; 602 } 603 } 604 605 // We shouldn't override any of the built-in MS Excel values (values below id 164) 606 // But there's a lot of naughty homebrew xlsx writers that do use "reserved" id values that aren't actually used 607 // So we make allowance for them rather than lose formatting masks 608 if ( 609 $numFmt === null && 610 (int) $xf['numFmtId'] < 164 && 611 NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== '' 612 ) { 613 $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']); 614 } 615 } 616 $quotePrefix = (bool) ($xf['quotePrefix'] ?? false); 617 618 $style = (object) [ 619 'numFmt' => $numFmt ?? NumberFormat::FORMAT_GENERAL, 620 'font' => $fonts[(int) ($xf['fontId'])], 621 'fill' => $fills[(int) ($xf['fillId'])], 622 'border' => $borders[(int) ($xf['borderId'])], 623 'alignment' => $xfTag->alignment, 624 'protection' => $xfTag->protection, 625 'quotePrefix' => $quotePrefix, 626 ]; 627 $styles[] = $style; 628 629 // add style to cellXf collection 630 $objStyle = new Style(); 631 $this->styleReader->readStyle($objStyle, $style); 632 if ($addingFirstCellXf) { 633 $excel->removeCellXfByIndex(0); // remove the default style 634 $addingFirstCellXf = false; 635 } 636 $excel->addCellXf($objStyle); 637 } 638 639 foreach ($cellXfTags as $xfTag) { 640 $xf = self::getAttributes($xfTag); 641 $numFmt = NumberFormat::FORMAT_GENERAL; 642 if ($numFmts && $xf['numFmtId']) { 643 $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]")); 644 if (isset($tmpNumFmt['formatCode'])) { 645 $numFmt = (string) $tmpNumFmt['formatCode']; 646 } elseif ((int) $xf['numFmtId'] < 165) { 647 $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']); 648 } 649 } 650 651 $quotePrefix = (bool) ($xf['quotePrefix'] ?? false); 652 653 $cellStyle = (object) [ 654 'numFmt' => $numFmt, 655 'font' => $fonts[(int) ($xf['fontId'])], 656 'fill' => $fills[((int) $xf['fillId'])], 657 'border' => $borders[(int) ($xf['borderId'])], 658 'alignment' => $xfTag->alignment, 659 'protection' => $xfTag->protection, 660 'quotePrefix' => $quotePrefix, 661 ]; 662 $cellStyles[] = $cellStyle; 663 664 // add style to cellStyleXf collection 665 $objStyle = new Style(); 666 $this->styleReader->readStyle($objStyle, $cellStyle); 667 if ($addingFirstCellStyleXf) { 668 $excel->removeCellStyleXfByIndex(0); // remove the default style 669 $addingFirstCellStyleXf = false; 670 } 671 $excel->addCellStyleXf($objStyle); 672 } 673 } 674 $this->styleReader->setStyleXml($xmlStyles); 675 $this->styleReader->setNamespace($mainNS); 676 $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles); 677 $dxfs = $this->styleReader->dxfs($this->readDataOnly); 678 $styles = $this->styleReader->styles(); 679 680 $xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS); 681 $xmlWorkbookNS = $this->loadZip($relTarget, $mainNS); 682 683 // Set base date 684 if ($xmlWorkbookNS->workbookPr) { 685 Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); 686 $attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr); 687 if (isset($attrs1904['date1904'])) { 688 if (self::boolean((string) $attrs1904['date1904'])) { 689 Date::setExcelCalendar(Date::CALENDAR_MAC_1904); 690 } 691 } 692 } 693 694 // Set protection 695 $this->readProtection($excel, $xmlWorkbook); 696 697 $sheetId = 0; // keep track of new sheet id in final workbook 698 $oldSheetId = -1; // keep track of old sheet id in final workbook 699 $countSkippedSheets = 0; // keep track of number of skipped sheets 700 $mapSheetId = []; // mapping of sheet ids from old to new 701 702 $charts = $chartDetails = []; 703 704 if ($xmlWorkbookNS->sheets) { 705 /** @var SimpleXMLElement $eleSheet */ 706 foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) { 707 $eleSheetAttr = self::getAttributes($eleSheet); 708 ++$oldSheetId; 709 710 // Check if sheet should be skipped 711 if (is_array($this->loadSheetsOnly) && !in_array((string) $eleSheetAttr['name'], $this->loadSheetsOnly)) { 712 ++$countSkippedSheets; 713 $mapSheetId[$oldSheetId] = null; 714 715 continue; 716 } 717 718 $sheetReferenceId = (string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id'); 719 if (isset($worksheets[$sheetReferenceId]) === false) { 720 ++$countSkippedSheets; 721 $mapSheetId[$oldSheetId] = null; 722 723 continue; 724 } 725 // Map old sheet id in original workbook to new sheet id. 726 // They will differ if loadSheetsOnly() is being used 727 $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets; 728 729 // Load sheet 730 $docSheet = $excel->createSheet(); 731 // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet 732 // references in formula cells... during the load, all formulae should be correct, 733 // and we're simply bringing the worksheet name in line with the formula, not the 734 // reverse 735 $docSheet->setTitle((string) $eleSheetAttr['name'], false, false); 736 737 $fileWorksheet = (string) $worksheets[$sheetReferenceId]; 738 $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS); 739 $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS); 740 741 $sharedFormulas = []; 742 743 if (isset($eleSheetAttr['state']) && (string) $eleSheetAttr['state'] != '') { 744 $docSheet->setSheetState((string) $eleSheetAttr['state']); 745 } 746 if ($xmlSheetNS) { 747 $xmlSheetMain = $xmlSheetNS->children($mainNS); 748 // Setting Conditional Styles adjusts selected cells, so we need to execute this 749 // before reading the sheet view data to get the actual selected cells 750 if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) { 751 (new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->load(); 752 } 753 if (!$this->readDataOnly && $xmlSheet->extLst) { 754 (new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->loadFromExt($this->styleReader); 755 } 756 if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) { 757 $sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet); 758 $sheetViews->load(); 759 } 760 761 $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheet); 762 $sheetViewOptions->load($this->getReadDataOnly(), $this->styleReader); 763 764 (new ColumnAndRowAttributes($docSheet, $xmlSheet)) 765 ->load($this->getReadFilter(), $this->getReadDataOnly()); 766 } 767 768 if ($xmlSheetNS && $xmlSheetNS->sheetData && $xmlSheetNS->sheetData->row) { 769 $cIndex = 1; // Cell Start from 1 770 foreach ($xmlSheetNS->sheetData->row as $row) { 771 $rowIndex = 1; 772 foreach ($row->c as $c) { 773 $cAttr = self::getAttributes($c); 774 $r = (string) $cAttr['r']; 775 if ($r == '') { 776 $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex; 777 } 778 $cellDataType = (string) $cAttr['t']; 779 $value = null; 780 $calculatedValue = null; 781 782 // Read cell? 783 if ($this->getReadFilter() !== null) { 784 $coordinates = Coordinate::coordinateFromString($r); 785 786 if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) { 787 if (isset($cAttr->f)) { 788 $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError'); 789 } 790 ++$rowIndex; 791 792 continue; 793 } 794 } 795 796 // Read cell! 797 switch ($cellDataType) { 798 case 's': 799 if ((string) $c->v != '') { 800 $value = $sharedStrings[(int) ($c->v)]; 801 802 if ($value instanceof RichText) { 803 $value = clone $value; 804 } 805 } else { 806 $value = ''; 807 } 808 809 break; 810 case 'b': 811 if (!isset($c->f)) { 812 if (isset($c->v)) { 813 $value = self::castToBoolean($c); 814 } else { 815 $value = null; 816 $cellDataType = DATATYPE::TYPE_NULL; 817 } 818 } else { 819 // Formula 820 $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToBoolean'); 821 if (isset($c->f['t'])) { 822 $att = $c->f; 823 $docSheet->getCell($r)->setFormulaAttributes($att); 824 } 825 } 826 827 break; 828 case 'inlineStr': 829 if (isset($c->f)) { 830 $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError'); 831 } else { 832 $value = $this->parseRichText($c->is); 833 } 834 835 break; 836 case 'e': 837 if (!isset($c->f)) { 838 $value = self::castToError($c); 839 } else { 840 // Formula 841 $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError'); 842 } 843 844 break; 845 default: 846 if (!isset($c->f)) { 847 $value = self::castToString($c); 848 } else { 849 // Formula 850 $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToString'); 851 if (isset($c->f['t'])) { 852 $attributes = $c->f['t']; 853 $docSheet->getCell($r)->setFormulaAttributes(['t' => (string) $attributes]); 854 } 855 } 856 857 break; 858 } 859 860 // read empty cells or the cells are not empty 861 if ($this->readEmptyCells || ($value !== null && $value !== '')) { 862 // Rich text? 863 if ($value instanceof RichText && $this->readDataOnly) { 864 $value = $value->getPlainText(); 865 } 866 867 $cell = $docSheet->getCell($r); 868 // Assign value 869 if ($cellDataType != '') { 870 // it is possible, that datatype is numeric but with an empty string, which result in an error 871 if ($cellDataType === DataType::TYPE_NUMERIC && ($value === '' || $value === null)) { 872 $cellDataType = DataType::TYPE_NULL; 873 } 874 if ($cellDataType !== DataType::TYPE_NULL) { 875 $cell->setValueExplicit($value, $cellDataType); 876 } 877 } else { 878 $cell->setValue($value); 879 } 880 if ($calculatedValue !== null) { 881 $cell->setCalculatedValue($calculatedValue); 882 } 883 884 // Style information? 885 if ($cAttr['s'] && !$this->readDataOnly) { 886 // no style index means 0, it seems 887 $cell->setXfIndex(isset($styles[(int) ($cAttr['s'])]) ? 888 (int) ($cAttr['s']) : 0); 889 } 890 } 891 ++$rowIndex; 892 } 893 ++$cIndex; 894 } 895 } 896 897 $aKeys = ['sheet', 'objects', 'scenarios', 'formatCells', 'formatColumns', 'formatRows', 'insertColumns', 'insertRows', 'insertHyperlinks', 'deleteColumns', 'deleteRows', 'selectLockedCells', 'sort', 'autoFilter', 'pivotTables', 'selectUnlockedCells']; 898 if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) { 899 foreach ($aKeys as $key) { 900 $method = 'set' . ucfirst($key); 901 $docSheet->getProtection()->$method(self::boolean((string) $xmlSheet->sheetProtection[$key])); 902 } 903 } 904 905 if ($xmlSheet) { 906 $this->readSheetProtection($docSheet, $xmlSheet); 907 } 908 909 if ($this->readDataOnly === false) { 910 $this->readAutoFilterTables($xmlSheet, $docSheet, $dir, $fileWorksheet, $zip); 911 } 912 913 if ($xmlSheet && $xmlSheet->mergeCells && $xmlSheet->mergeCells->mergeCell && !$this->readDataOnly) { 914 foreach ($xmlSheet->mergeCells->mergeCell as $mergeCell) { 915 $mergeRef = (string) $mergeCell['ref']; 916 if (strpos($mergeRef, ':') !== false) { 917 $docSheet->mergeCells((string) $mergeCell['ref'], Worksheet::MERGE_CELL_CONTENT_HIDE); 918 } 919 } 920 } 921 922 if ($xmlSheet && !$this->readDataOnly) { 923 $unparsedLoadedData = (new PageSetup($docSheet, $xmlSheet))->load($unparsedLoadedData); 924 } 925 926 if ($xmlSheet !== false && isset($xmlSheet->extLst, $xmlSheet->extLst->ext, $xmlSheet->extLst->ext['uri']) && ($xmlSheet->extLst->ext['uri'] == '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}')) { 927 // Create dataValidations node if does not exists, maybe is better inside the foreach ? 928 if (!$xmlSheet->dataValidations) { 929 $xmlSheet->addChild('dataValidations'); 930 } 931 932 foreach ($xmlSheet->extLst->ext->children('x14', true)->dataValidations->dataValidation as $item) { 933 $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation'); 934 foreach ($item->attributes() ?? [] as $attr) { 935 $node->addAttribute($attr->getName(), $attr); 936 } 937 $node->addAttribute('sqref', $item->children('xm', true)->sqref); 938 $node->addChild('formula1', $item->formula1->children('xm', true)->f); 939 } 940 } 941 942 if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) { 943 (new DataValidations($docSheet, $xmlSheet))->load(); 944 } 945 946 // unparsed sheet AlternateContent 947 if ($xmlSheet && !$this->readDataOnly) { 948 $mc = $xmlSheet->children(Namespaces::COMPATIBILITY); 949 if ($mc->AlternateContent) { 950 foreach ($mc->AlternateContent as $alternateContent) { 951 $alternateContent = self::testSimpleXml($alternateContent); 952 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['AlternateContents'][] = $alternateContent->asXML(); 953 } 954 } 955 } 956 957 // Add hyperlinks 958 if (!$this->readDataOnly) { 959 $hyperlinkReader = new Hyperlinks($docSheet); 960 // Locate hyperlink relations 961 $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; 962 if ($zip->locateName($relationsFileName)) { 963 $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS); 964 $hyperlinkReader->readHyperlinks($relsWorksheet); 965 } 966 967 // Loop through hyperlinks 968 if ($xmlSheetNS && $xmlSheetNS->children($mainNS)->hyperlinks) { 969 $hyperlinkReader->setHyperlinks($xmlSheetNS->children($mainNS)->hyperlinks); 970 } 971 } 972 973 // Add comments 974 $comments = []; 975 $vmlComments = []; 976 if (!$this->readDataOnly) { 977 // Locate comment relations 978 $commentRelations = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; 979 if ($zip->locateName($commentRelations)) { 980 $relsWorksheet = $this->loadZip($commentRelations, Namespaces::RELATIONSHIPS); 981 foreach ($relsWorksheet->Relationship as $elex) { 982 $ele = self::getAttributes($elex); 983 if ($ele['Type'] == Namespaces::COMMENTS) { 984 $comments[(string) $ele['Id']] = (string) $ele['Target']; 985 } 986 if ($ele['Type'] == Namespaces::VML) { 987 $vmlComments[(string) $ele['Id']] = (string) $ele['Target']; 988 } 989 } 990 } 991 992 // Loop through comments 993 foreach ($comments as $relName => $relPath) { 994 // Load comments file 995 $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath); 996 // okay to ignore namespace - using xpath 997 $commentsFile = $this->loadZip($relPath, ''); 998 999 // Utility variables 1000 $authors = []; 1001 $commentsFile->registerXpathNamespace('com', $mainNS); 1002 $authorPath = self::xpathNoFalse($commentsFile, 'com:authors/com:author'); 1003 foreach ($authorPath as $author) { 1004 $authors[] = (string) $author; 1005 } 1006 1007 // Loop through contents 1008 $contentPath = self::xpathNoFalse($commentsFile, 'com:commentList/com:comment'); 1009 foreach ($contentPath as $comment) { 1010 $commentx = $comment->attributes(); 1011 $commentModel = $docSheet->getComment((string) $commentx['ref']); 1012 if (isset($commentx['authorId'])) { 1013 $commentModel->setAuthor($authors[(int) $commentx['authorId']]); 1014 } 1015 $commentModel->setText($this->parseRichText($comment->children($mainNS)->text)); 1016 } 1017 } 1018 1019 // later we will remove from it real vmlComments 1020 $unparsedVmlDrawings = $vmlComments; 1021 1022 // Loop through VML comments 1023 foreach ($vmlComments as $relName => $relPath) { 1024 // Load VML comments file 1025 $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath); 1026 1027 try { 1028 // no namespace okay - processed with Xpath 1029 $vmlCommentsFile = $this->loadZip($relPath, ''); 1030 $vmlCommentsFile->registerXPathNamespace('v', Namespaces::URN_VML); 1031 } catch (Throwable $ex) { 1032 //Ignore unparsable vmlDrawings. Later they will be moved from $unparsedVmlDrawings to $unparsedLoadedData 1033 continue; 1034 } 1035 1036 // Locate VML drawings image relations 1037 $drowingImages = []; 1038 $VMLDrawingsRelations = dirname($relPath) . '/_rels/' . basename($relPath) . '.rels'; 1039 if ($zip->locateName($VMLDrawingsRelations)) { 1040 $relsVMLDrawing = $this->loadZip($VMLDrawingsRelations, Namespaces::RELATIONSHIPS); 1041 foreach ($relsVMLDrawing->Relationship as $elex) { 1042 $ele = self::getAttributes($elex); 1043 if ($ele['Type'] == Namespaces::IMAGE) { 1044 $drowingImages[(string) $ele['Id']] = (string) $ele['Target']; 1045 } 1046 } 1047 } 1048 1049 $shapes = self::xpathNoFalse($vmlCommentsFile, '//v:shape'); 1050 foreach ($shapes as $shape) { 1051 $shape->registerXPathNamespace('v', Namespaces::URN_VML); 1052 1053 if (isset($shape['style'])) { 1054 $style = (string) $shape['style']; 1055 $fillColor = strtoupper(substr((string) $shape['fillcolor'], 1)); 1056 $column = null; 1057 $row = null; 1058 $fillImageRelId = null; 1059 $fillImageTitle = ''; 1060 1061 $clientData = $shape->xpath('.//x:ClientData'); 1062 if (is_array($clientData) && !empty($clientData)) { 1063 $clientData = $clientData[0]; 1064 1065 if (isset($clientData['ObjectType']) && (string) $clientData['ObjectType'] == 'Note') { 1066 $temp = $clientData->xpath('.//x:Row'); 1067 if (is_array($temp)) { 1068 $row = $temp[0]; 1069 } 1070 1071 $temp = $clientData->xpath('.//x:Column'); 1072 if (is_array($temp)) { 1073 $column = $temp[0]; 1074 } 1075 } 1076 } 1077 1078 $fillImageRelNode = $shape->xpath('.//v:fill/@o:relid'); 1079 if (is_array($fillImageRelNode) && !empty($fillImageRelNode)) { 1080 $fillImageRelNode = $fillImageRelNode[0]; 1081 1082 if (isset($fillImageRelNode['relid'])) { 1083 $fillImageRelId = (string) $fillImageRelNode['relid']; 1084 } 1085 } 1086 1087 $fillImageTitleNode = $shape->xpath('.//v:fill/@o:title'); 1088 if (is_array($fillImageTitleNode) && !empty($fillImageTitleNode)) { 1089 $fillImageTitleNode = $fillImageTitleNode[0]; 1090 1091 if (isset($fillImageTitleNode['title'])) { 1092 $fillImageTitle = (string) $fillImageTitleNode['title']; 1093 } 1094 } 1095 1096 if (($column !== null) && ($row !== null)) { 1097 // Set comment properties 1098 $comment = $docSheet->getCommentByColumnAndRow($column + 1, $row + 1); 1099 $comment->getFillColor()->setRGB($fillColor); 1100 if (isset($drowingImages[$fillImageRelId])) { 1101 $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); 1102 $objDrawing->setName($fillImageTitle); 1103 $imagePath = str_replace('../', 'xl/', $drowingImages[$fillImageRelId]); 1104 $objDrawing->setPath( 1105 'zip://' . File::realpath($filename) . '#' . $imagePath, 1106 true, 1107 $zip 1108 ); 1109 $comment->setBackgroundImage($objDrawing); 1110 } 1111 1112 // Parse style 1113 $styleArray = explode(';', str_replace(' ', '', $style)); 1114 foreach ($styleArray as $stylePair) { 1115 $stylePair = explode(':', $stylePair); 1116 1117 if ($stylePair[0] == 'margin-left') { 1118 $comment->setMarginLeft($stylePair[1]); 1119 } 1120 if ($stylePair[0] == 'margin-top') { 1121 $comment->setMarginTop($stylePair[1]); 1122 } 1123 if ($stylePair[0] == 'width') { 1124 $comment->setWidth($stylePair[1]); 1125 } 1126 if ($stylePair[0] == 'height') { 1127 $comment->setHeight($stylePair[1]); 1128 } 1129 if ($stylePair[0] == 'visibility') { 1130 $comment->setVisible($stylePair[1] == 'visible'); 1131 } 1132 } 1133 1134 unset($unparsedVmlDrawings[$relName]); 1135 } 1136 } 1137 } 1138 } 1139 1140 // unparsed vmlDrawing 1141 if ($unparsedVmlDrawings) { 1142 foreach ($unparsedVmlDrawings as $rId => $relPath) { 1143 $rId = substr($rId, 3); // rIdXXX 1144 $unparsedVmlDrawing = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['vmlDrawings']; 1145 $unparsedVmlDrawing[$rId] = []; 1146 $unparsedVmlDrawing[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $relPath); 1147 $unparsedVmlDrawing[$rId]['relFilePath'] = $relPath; 1148 $unparsedVmlDrawing[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedVmlDrawing[$rId]['filePath'])); 1149 unset($unparsedVmlDrawing); 1150 } 1151 } 1152 1153 // Header/footer images 1154 if ($xmlSheet && $xmlSheet->legacyDrawingHF) { 1155 if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { 1156 $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS); 1157 $vmlRelationship = ''; 1158 1159 foreach ($relsWorksheet->Relationship as $ele) { 1160 if ($ele['Type'] == Namespaces::VML) { 1161 $vmlRelationship = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); 1162 } 1163 } 1164 1165 if ($vmlRelationship != '') { 1166 // Fetch linked images 1167 $relsVML = $this->loadZipNoNamespace(dirname($vmlRelationship) . '/_rels/' . basename($vmlRelationship) . '.rels', Namespaces::RELATIONSHIPS); 1168 $drawings = []; 1169 if (isset($relsVML->Relationship)) { 1170 foreach ($relsVML->Relationship as $ele) { 1171 if ($ele['Type'] == Namespaces::IMAGE) { 1172 $drawings[(string) $ele['Id']] = self::dirAdd($vmlRelationship, $ele['Target']); 1173 } 1174 } 1175 } 1176 // Fetch VML document 1177 $vmlDrawing = $this->loadZipNoNamespace($vmlRelationship, ''); 1178 $vmlDrawing->registerXPathNamespace('v', Namespaces::URN_VML); 1179 1180 $hfImages = []; 1181 1182 $shapes = self::xpathNoFalse($vmlDrawing, '//v:shape'); 1183 foreach ($shapes as $idx => $shape) { 1184 $shape->registerXPathNamespace('v', Namespaces::URN_VML); 1185 $imageData = $shape->xpath('//v:imagedata'); 1186 1187 if (empty($imageData)) { 1188 continue; 1189 } 1190 1191 $imageData = $imageData[$idx]; 1192 1193 $imageData = self::getAttributes($imageData, Namespaces::URN_MSOFFICE); 1194 $style = self::toCSSArray((string) $shape['style']); 1195 1196 $hfImages[(string) $shape['id']] = new HeaderFooterDrawing(); 1197 if (isset($imageData['title'])) { 1198 $hfImages[(string) $shape['id']]->setName((string) $imageData['title']); 1199 } 1200 1201 $hfImages[(string) $shape['id']]->setPath('zip://' . File::realpath($filename) . '#' . $drawings[(string) $imageData['relid']], false); 1202 $hfImages[(string) $shape['id']]->setResizeProportional(false); 1203 $hfImages[(string) $shape['id']]->setWidth($style['width']); 1204 $hfImages[(string) $shape['id']]->setHeight($style['height']); 1205 if (isset($style['margin-left'])) { 1206 $hfImages[(string) $shape['id']]->setOffsetX($style['margin-left']); 1207 } 1208 $hfImages[(string) $shape['id']]->setOffsetY($style['margin-top']); 1209 $hfImages[(string) $shape['id']]->setResizeProportional(true); 1210 } 1211 1212 $docSheet->getHeaderFooter()->setImages($hfImages); 1213 } 1214 } 1215 } 1216 } 1217 1218 // TODO: Autoshapes from twoCellAnchors! 1219 $drawingFilename = dirname("$dir/$fileWorksheet") 1220 . '/_rels/' 1221 . basename($fileWorksheet) 1222 . '.rels'; 1223 if (substr($drawingFilename, 0, 7) === 'xl//xl/') { 1224 $drawingFilename = substr($drawingFilename, 4); 1225 } 1226 if ($zip->locateName($drawingFilename)) { 1227 $relsWorksheet = $this->loadZipNoNamespace($drawingFilename, Namespaces::RELATIONSHIPS); 1228 $drawings = []; 1229 foreach ($relsWorksheet->Relationship as $ele) { 1230 if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") { 1231 $eleTarget = (string) $ele['Target']; 1232 if (substr($eleTarget, 0, 4) === '/xl/') { 1233 $drawings[(string) $ele['Id']] = substr($eleTarget, 1); 1234 } else { 1235 $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); 1236 } 1237 } 1238 } 1239 1240 if ($xmlSheet->drawing && !$this->readDataOnly) { 1241 $unparsedDrawings = []; 1242 $fileDrawing = null; 1243 foreach ($xmlSheet->drawing as $drawing) { 1244 $drawingRelId = (string) self::getArrayItem(self::getAttributes($drawing, $xmlNamespaceBase), 'id'); 1245 $fileDrawing = $drawings[$drawingRelId]; 1246 $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels'; 1247 $relsDrawing = $this->loadZipNoNamespace($drawingFilename, $xmlNamespaceBase); 1248 1249 $images = []; 1250 $hyperlinks = []; 1251 if ($relsDrawing && $relsDrawing->Relationship) { 1252 foreach ($relsDrawing->Relationship as $ele) { 1253 $eleType = (string) $ele['Type']; 1254 if ($eleType === Namespaces::HYPERLINK) { 1255 $hyperlinks[(string) $ele['Id']] = (string) $ele['Target']; 1256 } 1257 if ($eleType === "$xmlNamespaceBase/image") { 1258 $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $ele['Target']); 1259 } elseif ($eleType === "$xmlNamespaceBase/chart") { 1260 if ($this->includeCharts) { 1261 $eleTarget = (string) $ele['Target']; 1262 if (substr($eleTarget, 0, 4) === '/xl/') { 1263 $index = substr($eleTarget, 1); 1264 } else { 1265 $index = self::dirAdd($fileDrawing, $eleTarget); 1266 } 1267 $charts[$index] = [ 1268 'id' => (string) $ele['Id'], 1269 'sheet' => $docSheet->getTitle(), 1270 ]; 1271 } 1272 } 1273 } 1274 } 1275 1276 $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, ''); 1277 $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING); 1278 1279 if ($xmlDrawingChildren->oneCellAnchor) { 1280 foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) { 1281 if ($oneCellAnchor->pic->blipFill) { 1282 /** @var SimpleXMLElement $blip */ 1283 $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; 1284 /** @var SimpleXMLElement $xfrm */ 1285 $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm; 1286 /** @var SimpleXMLElement $outerShdw */ 1287 $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; 1288 /** @var SimpleXMLElement $hlinkClick */ 1289 $hlinkClick = $oneCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; 1290 1291 $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); 1292 $objDrawing->setName((string) self::getArrayItem(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name')); 1293 $objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'descr')); 1294 $embedImageKey = (string) self::getArrayItem( 1295 self::getAttributes($blip, $xmlNamespaceBase), 1296 'embed' 1297 ); 1298 if (isset($images[$embedImageKey])) { 1299 $objDrawing->setPath( 1300 'zip://' . File::realpath($filename) . '#' . 1301 $images[$embedImageKey], 1302 false 1303 ); 1304 } else { 1305 $linkImageKey = (string) self::getArrayItem( 1306 $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 1307 'link' 1308 ); 1309 if (isset($images[$linkImageKey])) { 1310 $url = str_replace('xl/drawings/', '', $images[$linkImageKey]); 1311 $objDrawing->setPath($url); 1312 } 1313 } 1314 $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1)); 1315 1316 $objDrawing->setOffsetX((int) Drawing::EMUToPixels($oneCellAnchor->from->colOff)); 1317 $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff)); 1318 $objDrawing->setResizeProportional(false); 1319 $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cx'))); 1320 $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cy'))); 1321 if ($xfrm) { 1322 $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot'))); 1323 } 1324 if ($outerShdw) { 1325 $shadow = $objDrawing->getShadow(); 1326 $shadow->setVisible(true); 1327 $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'blurRad'))); 1328 $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'dist'))); 1329 $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($outerShdw), 'dir'))); 1330 $shadow->setAlignment((string) self::getArrayItem(self::getAttributes($outerShdw), 'algn')); 1331 $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr; 1332 $shadow->getColor()->setRGB(self::getArrayItem(self::getAttributes($clr), 'val')); 1333 $shadow->setAlpha(self::getArrayItem(self::getAttributes($clr->alpha), 'val') / 1000); 1334 } 1335 1336 $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks); 1337 1338 $objDrawing->setWorksheet($docSheet); 1339 } elseif ($this->includeCharts && $oneCellAnchor->graphicFrame) { 1340 // Exported XLSX from Google Sheets positions charts with a oneCellAnchor 1341 $coordinates = Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1); 1342 $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff); 1343 $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff); 1344 $width = Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cx')); 1345 $height = Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cy')); 1346 1347 $graphic = $oneCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; 1348 /** @var SimpleXMLElement $chartRef */ 1349 $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; 1350 $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); 1351 1352 $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ 1353 'fromCoordinate' => $coordinates, 1354 'fromOffsetX' => $offsetX, 1355 'fromOffsetY' => $offsetY, 1356 'width' => $width, 1357 'height' => $height, 1358 'worksheetTitle' => $docSheet->getTitle(), 1359 'oneCellAnchor' => true, 1360 ]; 1361 } 1362 } 1363 } 1364 if ($xmlDrawingChildren->twoCellAnchor) { 1365 foreach ($xmlDrawingChildren->twoCellAnchor as $twoCellAnchor) { 1366 if ($twoCellAnchor->pic->blipFill) { 1367 $blip = $twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; 1368 $xfrm = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm; 1369 $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; 1370 $hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; 1371 $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); 1372 /** @scrutinizer ignore-call */ 1373 $editAs = $twoCellAnchor->attributes(); 1374 if (isset($editAs, $editAs['editAs'])) { 1375 $objDrawing->setEditAs($editAs['editAs']); 1376 } 1377 $objDrawing->setName((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name')); 1378 $objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr')); 1379 $embedImageKey = (string) self::getArrayItem( 1380 self::getAttributes($blip, $xmlNamespaceBase), 1381 'embed' 1382 ); 1383 if (isset($images[$embedImageKey])) { 1384 $objDrawing->setPath( 1385 'zip://' . File::realpath($filename) . '#' . 1386 $images[$embedImageKey], 1387 false 1388 ); 1389 } else { 1390 $linkImageKey = (string) self::getArrayItem( 1391 $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 1392 'link' 1393 ); 1394 if (isset($images[$linkImageKey])) { 1395 $url = str_replace('xl/drawings/', '', $images[$linkImageKey]); 1396 $objDrawing->setPath($url); 1397 } 1398 } 1399 $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1)); 1400 1401 $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff)); 1402 $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff)); 1403 1404 $objDrawing->setCoordinates2(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1)); 1405 1406 $objDrawing->setOffsetX2(Drawing::EMUToPixels($twoCellAnchor->to->colOff)); 1407 $objDrawing->setOffsetY2(Drawing::EMUToPixels($twoCellAnchor->to->rowOff)); 1408 1409 $objDrawing->setResizeProportional(false); 1410 1411 if ($xfrm) { 1412 $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cx'))); 1413 $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cy'))); 1414 $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot'))); 1415 } 1416 if ($outerShdw) { 1417 $shadow = $objDrawing->getShadow(); 1418 $shadow->setVisible(true); 1419 $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'blurRad'))); 1420 $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'dist'))); 1421 $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($outerShdw), 'dir'))); 1422 $shadow->setAlignment((string) self::getArrayItem(self::getAttributes($outerShdw), 'algn')); 1423 $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr; 1424 $shadow->getColor()->setRGB(self::getArrayItem(self::getAttributes($clr), 'val')); 1425 $shadow->setAlpha(self::getArrayItem(self::getAttributes($clr->alpha), 'val') / 1000); 1426 } 1427 1428 $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks); 1429 1430 $objDrawing->setWorksheet($docSheet); 1431 } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) { 1432 $fromCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1); 1433 $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff); 1434 $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff); 1435 $toCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1); 1436 $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff); 1437 $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff); 1438 $graphic = $twoCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; 1439 /** @var SimpleXMLElement $chartRef */ 1440 $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; 1441 $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); 1442 1443 $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ 1444 'fromCoordinate' => $fromCoordinate, 1445 'fromOffsetX' => $fromOffsetX, 1446 'fromOffsetY' => $fromOffsetY, 1447 'toCoordinate' => $toCoordinate, 1448 'toOffsetX' => $toOffsetX, 1449 'toOffsetY' => $toOffsetY, 1450 'worksheetTitle' => $docSheet->getTitle(), 1451 ]; 1452 } 1453 } 1454 } 1455 if ($xmlDrawingChildren->absoluteAnchor) { 1456 foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) { 1457 if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) { 1458 $graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; 1459 /** @var SimpleXMLElement $chartRef */ 1460 $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; 1461 $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); 1462 $width = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cx')[0]); 1463 $height = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cy')[0]); 1464 1465 $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ 1466 'fromCoordinate' => 'A1', 1467 'fromOffsetX' => 0, 1468 'fromOffsetY' => 0, 1469 'width' => $width, 1470 'height' => $height, 1471 'worksheetTitle' => $docSheet->getTitle(), 1472 ]; 1473 } 1474 } 1475 } 1476 if (empty($relsDrawing) && $xmlDrawing->count() == 0) { 1477 // Save Drawing without rels and children as unparsed 1478 $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML(); 1479 } 1480 } 1481 1482 // store original rId of drawing files 1483 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = []; 1484 foreach ($relsWorksheet->Relationship as $ele) { 1485 if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") { 1486 $drawingRelId = (string) $ele['Id']; 1487 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId; 1488 if (isset($unparsedDrawings[$drawingRelId])) { 1489 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['Drawings'][$drawingRelId] = $unparsedDrawings[$drawingRelId]; 1490 } 1491 } 1492 } 1493 1494 // unparsed drawing AlternateContent 1495 $xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY); 1496 1497 if ($xmlAltDrawing->AlternateContent) { 1498 foreach ($xmlAltDrawing->AlternateContent as $alternateContent) { 1499 $alternateContent = self::testSimpleXml($alternateContent); 1500 $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingAlternateContents'][] = $alternateContent->asXML(); 1501 } 1502 } 1503 } 1504 } 1505 1506 $this->readFormControlProperties($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData); 1507 $this->readPrinterSettings($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData); 1508 1509 // Loop through definedNames 1510 if ($xmlWorkbook->definedNames) { 1511 foreach ($xmlWorkbook->definedNames->definedName as $definedName) { 1512 // Extract range 1513 $extractedRange = (string) $definedName; 1514 if (($spos = strpos($extractedRange, '!')) !== false) { 1515 $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos)); 1516 } else { 1517 $extractedRange = str_replace('$', '', $extractedRange); 1518 } 1519 1520 // Valid range? 1521 if ($extractedRange == '') { 1522 continue; 1523 } 1524 1525 // Some definedNames are only applicable if we are on the same sheet... 1526 if ((string) $definedName['localSheetId'] != '' && (string) $definedName['localSheetId'] == $oldSheetId) { 1527 // Switch on type 1528 switch ((string) $definedName['name']) { 1529 case '_xlnm._FilterDatabase': 1530 if ((string) $definedName['hidden'] !== '1') { 1531 $extractedRange = explode(',', $extractedRange); 1532 foreach ($extractedRange as $range) { 1533 $autoFilterRange = $range; 1534 if (strpos($autoFilterRange, ':') !== false) { 1535 $docSheet->getAutoFilter()->setRange($autoFilterRange); 1536 } 1537 } 1538 } 1539 1540 break; 1541 case '_xlnm.Print_Titles': 1542 // Split $extractedRange 1543 $extractedRange = explode(',', $extractedRange); 1544 1545 // Set print titles 1546 foreach ($extractedRange as $range) { 1547 $matches = []; 1548 $range = str_replace('$', '', $range); 1549 1550 // check for repeating columns, e g. 'A:A' or 'A:D' 1551 if (preg_match('/!?([A-Z]+)\:([A-Z]+)$/', $range, $matches)) { 1552 $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$matches[1], $matches[2]]); 1553 } elseif (preg_match('/!?(\d+)\:(\d+)$/', $range, $matches)) { 1554 // check for repeating rows, e.g. '1:1' or '1:5' 1555 $docSheet->getPageSetup()->setRowsToRepeatAtTop([$matches[1], $matches[2]]); 1556 } 1557 } 1558 1559 break; 1560 case '_xlnm.Print_Area': 1561 $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: []; 1562 $newRangeSets = []; 1563 foreach ($rangeSets as $rangeSet) { 1564 [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); 1565 if (empty($rangeSet)) { 1566 continue; 1567 } 1568 if (strpos($rangeSet, ':') === false) { 1569 $rangeSet = $rangeSet . ':' . $rangeSet; 1570 } 1571 $newRangeSets[] = str_replace('$', '', $rangeSet); 1572 } 1573 if (count($newRangeSets) > 0) { 1574 $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); 1575 } 1576 1577 break; 1578 default: 1579 break; 1580 } 1581 } 1582 } 1583 } 1584 1585 // Next sheet id 1586 ++$sheetId; 1587 } 1588 1589 // Loop through definedNames 1590 if ($xmlWorkbook->definedNames) { 1591 foreach ($xmlWorkbook->definedNames->definedName as $definedName) { 1592 // Extract range 1593 $extractedRange = (string) $definedName; 1594 1595 // Valid range? 1596 if ($extractedRange == '') { 1597 continue; 1598 } 1599 1600 // Some definedNames are only applicable if we are on the same sheet... 1601 if ((string) $definedName['localSheetId'] != '') { 1602 // Local defined name 1603 // Switch on type 1604 switch ((string) $definedName['name']) { 1605 case '_xlnm._FilterDatabase': 1606 case '_xlnm.Print_Titles': 1607 case '_xlnm.Print_Area': 1608 break; 1609 default: 1610 if ($mapSheetId[(int) $definedName['localSheetId']] !== null) { 1611 $range = Worksheet::extractSheetTitle((string) $definedName, true); 1612 $scope = $excel->getSheet($mapSheetId[(int) $definedName['localSheetId']]); 1613 if (strpos((string) $definedName, '!') !== false) { 1614 $range[0] = str_replace("''", "'", $range[0]); 1615 $range[0] = str_replace("'", '', $range[0]); 1616 if ($worksheet = $excel->getSheetByName($range[0])) { // @phpstan-ignore-line 1617 $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope)); 1618 } else { 1619 $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope)); 1620 } 1621 } else { 1622 $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true)); 1623 } 1624 } 1625 1626 break; 1627 } 1628 } elseif (!isset($definedName['localSheetId'])) { 1629 $definedRange = (string) $definedName; 1630 // "Global" definedNames 1631 $locatedSheet = null; 1632 if (strpos((string) $definedName, '!') !== false) { 1633 // Modify range, and extract the first worksheet reference 1634 // Need to split on a comma or a space if not in quotes, and extract the first part. 1635 $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $definedRange); 1636 // Extract sheet name 1637 [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); // @phpstan-ignore-line 1638 $extractedSheetName = trim($extractedSheetName, "'"); 1639 1640 // Locate sheet 1641 $locatedSheet = $excel->getSheetByName($extractedSheetName); 1642 } 1643 1644 if ($locatedSheet === null && !DefinedName::testIfFormula($definedRange)) { 1645 $definedRange = '#REF!'; 1646 } 1647 $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $definedRange, false)); 1648 } 1649 } 1650 } 1651 } 1652 1653 (new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly); 1654 1655 break; 1656 } 1657 } 1658 1659 if (!$this->readDataOnly) { 1660 $contentTypes = $this->loadZip('[Content_Types].xml'); 1661 1662 // Default content types 1663 foreach ($contentTypes->Default as $contentType) { 1664 switch ($contentType['ContentType']) { 1665 case 'application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings': 1666 $unparsedLoadedData['default_content_types'][(string) $contentType['Extension']] = (string) $contentType['ContentType']; 1667 1668 break; 1669 } 1670 } 1671 1672 // Override content types 1673 foreach ($contentTypes->Override as $contentType) { 1674 switch ($contentType['ContentType']) { 1675 case 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml': 1676 if ($this->includeCharts) { 1677 $chartEntryRef = ltrim((string) $contentType['PartName'], '/'); 1678 $chartElements = $this->loadZip($chartEntryRef); 1679 $chartReader = new Chart($chartNS, $drawingNS); 1680 $objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml')); 1681 if (isset($charts[$chartEntryRef])) { 1682 $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; 1683 if (isset($chartDetails[$chartPositionRef])) { 1684 $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); // @phpstan-ignore-line 1685 $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); 1686 // For oneCellAnchor or absoluteAnchor positioned charts, 1687 // toCoordinate is not in the data. Does it need to be calculated? 1688 if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) { 1689 // twoCellAnchor 1690 $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); 1691 $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']); 1692 } else { 1693 // oneCellAnchor or absoluteAnchor (e.g. Chart sheet) 1694 $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); 1695 $objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']); 1696 if (array_key_exists('oneCellAnchor', $chartDetails[$chartPositionRef])) { 1697 $objChart->setOneCellAnchor($chartDetails[$chartPositionRef]['oneCellAnchor']); 1698 } 1699 } 1700 } 1701 } 1702 } 1703 1704 break; 1705 1706 // unparsed 1707 case 'application/vnd.ms-excel.controlproperties+xml': 1708 $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType']; 1709 1710 break; 1711 } 1712 } 1713 } 1714 1715 $excel->setUnparsedLoadedData($unparsedLoadedData); 1716 1717 $zip->close(); 1718 1719 return $excel; 1720 } 1721 1722 /** 1723 * @return RichText 1724 */ 1725 private function parseRichText(?SimpleXMLElement $is) 1726 { 1727 $value = new RichText(); 1728 1729 if (isset($is->t)) { 1730 $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); 1731 } else { 1732 if (is_object($is->r)) { 1733 /** @var SimpleXMLElement $run */ 1734 foreach ($is->r as $run) { 1735 if (!isset($run->rPr)) { 1736 $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); 1737 } else { 1738 $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); 1739 $objFont = $objText->getFont() ?? new StyleFont(); 1740 1741 if (isset($run->rPr->rFont)) { 1742 $attr = $run->rPr->rFont->attributes(); 1743 if (isset($attr['val'])) { 1744 $objFont->setName((string) $attr['val']); 1745 } 1746 } 1747 if (isset($run->rPr->sz)) { 1748 $attr = $run->rPr->sz->attributes(); 1749 if (isset($attr['val'])) { 1750 $objFont->setSize((float) $attr['val']); 1751 } 1752 } 1753 if (isset($run->rPr->color)) { 1754 $objFont->setColor(new Color($this->styleReader->readColor($run->rPr->color))); 1755 } 1756 if (isset($run->rPr->b)) { 1757 $attr = $run->rPr->b->attributes(); 1758 if ( 1759 (isset($attr['val']) && self::boolean((string) $attr['val'])) || 1760 (!isset($attr['val'])) 1761 ) { 1762 $objFont->setBold(true); 1763 } 1764 } 1765 if (isset($run->rPr->i)) { 1766 $attr = $run->rPr->i->attributes(); 1767 if ( 1768 (isset($attr['val']) && self::boolean((string) $attr['val'])) || 1769 (!isset($attr['val'])) 1770 ) { 1771 $objFont->setItalic(true); 1772 } 1773 } 1774 if (isset($run->rPr->vertAlign)) { 1775 $attr = $run->rPr->vertAlign->attributes(); 1776 if (isset($attr['val'])) { 1777 $vertAlign = strtolower((string) $attr['val']); 1778 if ($vertAlign == 'superscript') { 1779 $objFont->setSuperscript(true); 1780 } 1781 if ($vertAlign == 'subscript') { 1782 $objFont->setSubscript(true); 1783 } 1784 } 1785 } 1786 if (isset($run->rPr->u)) { 1787 $attr = $run->rPr->u->attributes(); 1788 if (!isset($attr['val'])) { 1789 $objFont->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); 1790 } else { 1791 $objFont->setUnderline((string) $attr['val']); 1792 } 1793 } 1794 if (isset($run->rPr->strike)) { 1795 $attr = $run->rPr->strike->attributes(); 1796 if ( 1797 (isset($attr['val']) && self::boolean((string) $attr['val'])) || 1798 (!isset($attr['val'])) 1799 ) { 1800 $objFont->setStrikethrough(true); 1801 } 1802 } 1803 } 1804 } 1805 } 1806 } 1807 1808 return $value; 1809 } 1810 1811 private function readRibbon(Spreadsheet $excel, string $customUITarget, ZipArchive $zip): void 1812 { 1813 $baseDir = dirname($customUITarget); 1814 $nameCustomUI = basename($customUITarget); 1815 // get the xml file (ribbon) 1816 $localRibbon = $this->getFromZipArchive($zip, $customUITarget); 1817 $customUIImagesNames = []; 1818 $customUIImagesBinaries = []; 1819 // something like customUI/_rels/customUI.xml.rels 1820 $pathRels = $baseDir . '/_rels/' . $nameCustomUI . '.rels'; 1821 $dataRels = $this->getFromZipArchive($zip, $pathRels); 1822 if ($dataRels) { 1823 // exists and not empty if the ribbon have some pictures (other than internal MSO) 1824 $UIRels = simplexml_load_string( 1825 $this->securityScanner->scan($dataRels), 1826 'SimpleXMLElement', 1827 Settings::getLibXmlLoaderOptions() 1828 ); 1829 if (false !== $UIRels) { 1830 // we need to save id and target to avoid parsing customUI.xml and "guess" if it's a pseudo callback who load the image 1831 foreach ($UIRels->Relationship as $ele) { 1832 if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/image') { 1833 // an image ? 1834 $customUIImagesNames[(string) $ele['Id']] = (string) $ele['Target']; 1835 $customUIImagesBinaries[(string) $ele['Target']] = $this->getFromZipArchive($zip, $baseDir . '/' . (string) $ele['Target']); 1836 } 1837 } 1838 } 1839 } 1840 if ($localRibbon) { 1841 $excel->setRibbonXMLData($customUITarget, $localRibbon); 1842 if (count($customUIImagesNames) > 0 && count($customUIImagesBinaries) > 0) { 1843 $excel->setRibbonBinObjects($customUIImagesNames, $customUIImagesBinaries); 1844 } else { 1845 $excel->setRibbonBinObjects(null, null); 1846 } 1847 } else { 1848 $excel->setRibbonXMLData(null, null); 1849 $excel->setRibbonBinObjects(null, null); 1850 } 1851 } 1852 1853 /** 1854 * @param null|array|bool|SimpleXMLElement $array 1855 * @param int|string $key 1856 * 1857 * @return mixed 1858 */ 1859 private static function getArrayItem($array, $key = 0) 1860 { 1861 return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null); 1862 } 1863 1864 /** 1865 * @param null|SimpleXMLElement|string $base 1866 * @param null|SimpleXMLElement|string $add 1867 */ 1868 private static function dirAdd($base, $add): string 1869 { 1870 $base = (string) $base; 1871 $add = (string) $add; 1872 1873 return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); 1874 } 1875 1876 private static function toCSSArray(string $style): array 1877 { 1878 $style = self::stripWhiteSpaceFromStyleString($style); 1879 1880 $temp = explode(';', $style); 1881 $style = []; 1882 foreach ($temp as $item) { 1883 $item = explode(':', $item); 1884 1885 if (strpos($item[1], 'px') !== false) { 1886 $item[1] = str_replace('px', '', $item[1]); 1887 } 1888 if (strpos($item[1], 'pt') !== false) { 1889 $item[1] = str_replace('pt', '', $item[1]); 1890 $item[1] = (string) Font::fontSizeToPixels((int) $item[1]); 1891 } 1892 if (strpos($item[1], 'in') !== false) { 1893 $item[1] = str_replace('in', '', $item[1]); 1894 $item[1] = (string) Font::inchSizeToPixels((int) $item[1]); 1895 } 1896 if (strpos($item[1], 'cm') !== false) { 1897 $item[1] = str_replace('cm', '', $item[1]); 1898 $item[1] = (string) Font::centimeterSizeToPixels((int) $item[1]); 1899 } 1900 1901 $style[$item[0]] = $item[1]; 1902 } 1903 1904 return $style; 1905 } 1906 1907 public static function stripWhiteSpaceFromStyleString(string $string): string 1908 { 1909 return trim(str_replace(["\r", "\n", ' '], '', $string), ';'); 1910 } 1911 1912 /** 1913 * @param mixed $value 1914 */ 1915 private static function boolean($value): bool 1916 { 1917 if (is_object($value)) { 1918 $value = (string) $value; 1919 } 1920 if (is_numeric($value)) { 1921 return (bool) $value; 1922 } 1923 1924 return $value === 'true' || $value === 'TRUE'; 1925 } 1926 1927 /** 1928 * @param array $hyperlinks 1929 */ 1930 private function readHyperLinkDrawing(\PhpOffice\PhpSpreadsheet\Worksheet\Drawing $objDrawing, SimpleXMLElement $cellAnchor, $hyperlinks): void 1931 { 1932 $hlinkClick = $cellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; 1933 1934 if ($hlinkClick->count() === 0) { 1935 return; 1936 } 1937 1938 $hlinkId = (string) self::getAttributes($hlinkClick, Namespaces::SCHEMA_OFFICE_DOCUMENT)['id']; 1939 $hyperlink = new Hyperlink( 1940 $hyperlinks[$hlinkId], 1941 (string) self::getArrayItem(self::getAttributes($cellAnchor->pic->nvPicPr->cNvPr), 'name') 1942 ); 1943 $objDrawing->setHyperlink($hyperlink); 1944 } 1945 1946 private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook): void 1947 { 1948 if (!$xmlWorkbook->workbookProtection) { 1949 return; 1950 } 1951 1952 $excel->getSecurity()->setLockRevision(self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision')); 1953 $excel->getSecurity()->setLockStructure(self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure')); 1954 $excel->getSecurity()->setLockWindows(self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows')); 1955 1956 if ($xmlWorkbook->workbookProtection['revisionsPassword']) { 1957 $excel->getSecurity()->setRevisionsPassword( 1958 (string) $xmlWorkbook->workbookProtection['revisionsPassword'], 1959 true 1960 ); 1961 } 1962 1963 if ($xmlWorkbook->workbookProtection['workbookPassword']) { 1964 $excel->getSecurity()->setWorkbookPassword( 1965 (string) $xmlWorkbook->workbookProtection['workbookPassword'], 1966 true 1967 ); 1968 } 1969 } 1970 1971 private static function getLockValue(SimpleXmlElement $protection, string $key): ?bool 1972 { 1973 $returnValue = null; 1974 $protectKey = $protection[$key]; 1975 if (!empty($protectKey)) { 1976 $protectKey = (string) $protectKey; 1977 $returnValue = $protectKey !== 'false' && (bool) $protectKey; 1978 } 1979 1980 return $returnValue; 1981 } 1982 1983 private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void 1984 { 1985 $zip = $this->zip; 1986 if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { 1987 return; 1988 } 1989 1990 $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; 1991 $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS); 1992 $ctrlProps = []; 1993 foreach ($relsWorksheet->Relationship as $ele) { 1994 if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/ctrlProp') { 1995 $ctrlProps[(string) $ele['Id']] = $ele; 1996 } 1997 } 1998 1999 $unparsedCtrlProps = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['ctrlProps']; 2000 foreach ($ctrlProps as $rId => $ctrlProp) { 2001 $rId = substr($rId, 3); // rIdXXX 2002 $unparsedCtrlProps[$rId] = []; 2003 $unparsedCtrlProps[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $ctrlProp['Target']); 2004 $unparsedCtrlProps[$rId]['relFilePath'] = (string) $ctrlProp['Target']; 2005 $unparsedCtrlProps[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedCtrlProps[$rId]['filePath'])); 2006 } 2007 unset($unparsedCtrlProps); 2008 } 2009 2010 private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void 2011 { 2012 $zip = $this->zip; 2013 if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { 2014 return; 2015 } 2016 2017 $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; 2018 $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS); 2019 $sheetPrinterSettings = []; 2020 foreach ($relsWorksheet->Relationship as $ele) { 2021 if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/printerSettings') { 2022 $sheetPrinterSettings[(string) $ele['Id']] = $ele; 2023 } 2024 } 2025 2026 $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings']; 2027 foreach ($sheetPrinterSettings as $rId => $printerSettings) { 2028 $rId = substr($rId, 3) . 'ps'; // rIdXXX, add 'ps' suffix to avoid identical resource identifier collision with unparsed vmlDrawing 2029 $unparsedPrinterSettings[$rId] = []; 2030 $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $printerSettings['Target']); 2031 $unparsedPrinterSettings[$rId]['relFilePath'] = (string) $printerSettings['Target']; 2032 $unparsedPrinterSettings[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedPrinterSettings[$rId]['filePath'])); 2033 } 2034 unset($unparsedPrinterSettings); 2035 } 2036 2037 private function getWorkbookBaseName(): array 2038 { 2039 $workbookBasename = ''; 2040 $xmlNamespaceBase = ''; 2041 2042 // check if it is an OOXML archive 2043 $rels = $this->loadZip(self::INITIAL_FILE); 2044 foreach ($rels->children(Namespaces::RELATIONSHIPS)->Relationship as $rel) { 2045 $rel = self::getAttributes($rel); 2046 $type = (string) $rel['Type']; 2047 switch ($type) { 2048 case Namespaces::OFFICE_DOCUMENT: 2049 case Namespaces::PURL_OFFICE_DOCUMENT: 2050 $basename = basename((string) $rel['Target']); 2051 $xmlNamespaceBase = dirname($type); 2052 if (preg_match('/workbook.*\.xml/', $basename)) { 2053 $workbookBasename = $basename; 2054 } 2055 2056 break; 2057 } 2058 } 2059 2060 return [$workbookBasename, $xmlNamespaceBase]; 2061 } 2062 2063 private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void 2064 { 2065 if ($this->readDataOnly || !$xmlSheet->sheetProtection) { 2066 return; 2067 } 2068 2069 $algorithmName = (string) $xmlSheet->sheetProtection['algorithmName']; 2070 $protection = $docSheet->getProtection(); 2071 $protection->setAlgorithm($algorithmName); 2072 2073 if ($algorithmName) { 2074 $protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true); 2075 $protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']); 2076 $protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']); 2077 } else { 2078 $protection->setPassword((string) $xmlSheet->sheetProtection['password'], true); 2079 } 2080 2081 if ($xmlSheet->protectedRanges->protectedRange) { 2082 foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) { 2083 $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true); 2084 } 2085 } 2086 } 2087 2088 private function readAutoFilterTables( 2089 SimpleXMLElement $xmlSheet, 2090 Worksheet $docSheet, 2091 string $dir, 2092 string $fileWorksheet, 2093 ZipArchive $zip 2094 ): void { 2095 if ($xmlSheet && $xmlSheet->autoFilter) { 2096 // In older files, autofilter structure is defined in the worksheet file 2097 (new AutoFilter($docSheet, $xmlSheet))->load(); 2098 } elseif ($xmlSheet && $xmlSheet->tableParts && (int) $xmlSheet->tableParts['count'] > 0) { 2099 // But for Office365, MS decided to make it all just a bit more complicated 2100 $this->readAutoFilterTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); 2101 } 2102 } 2103 2104 private function readAutoFilterTablesInTablesFile( 2105 SimpleXMLElement $xmlSheet, 2106 string $dir, 2107 string $fileWorksheet, 2108 ZipArchive $zip, 2109 Worksheet $docSheet 2110 ): void { 2111 foreach ($xmlSheet->tableParts->tablePart as $tablePart) { 2112 $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT); 2113 $tablePartRel = (string) $relation['id']; 2114 $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'; 2115 2116 if ($zip->locateName($relationsFileName)) { 2117 $relsTableReferences = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS); 2118 foreach ($relsTableReferences->Relationship as $relationship) { 2119 $relationshipAttributes = self::getAttributes($relationship, ''); 2120 2121 if ((string) $relationshipAttributes['Id'] === $tablePartRel) { 2122 $relationshipFileName = (string) $relationshipAttributes['Target']; 2123 $relationshipFilePath = dirname("$dir/$fileWorksheet") . '/' . $relationshipFileName; 2124 $relationshipFilePath = File::realpath($relationshipFilePath); 2125 2126 if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) { 2127 $autoFilter = $this->loadZip($relationshipFilePath); 2128 (new AutoFilter($docSheet, $autoFilter))->load(); 2129 } 2130 } 2131 } 2132 } 2133 } 2134 } 2135 2136 private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array 2137 { 2138 $array = []; 2139 if ($sxml && $sxml->{$node1}->{$node2}) { 2140 foreach ($sxml->{$node1}->{$node2} as $node) { 2141 $array[] = $node; 2142 } 2143 } 2144 2145 return $array; 2146 } 2147 2148 private static function extractPalette(?SimpleXMLElement $sxml): array 2149 { 2150 $array = []; 2151 if ($sxml && $sxml->colors->indexedColors) { 2152 foreach ($sxml->colors->indexedColors->rgbColor as $node) { 2153 if ($node !== null) { 2154 $attr = $node->attributes(); 2155 if (isset($attr['rgb'])) { 2156 $array[] = (string) $attr['rgb']; 2157 } 2158 } 2159 } 2160 } 2161 2162 return (count($array) === 64) ? $array : []; 2163 } 2164 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body