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 DOMAttr; 6 use DOMDocument; 7 use DOMElement; 8 use DOMNode; 9 use PhpOffice\PhpSpreadsheet\Cell\Coordinate; 10 use PhpOffice\PhpSpreadsheet\Cell\DataType; 11 use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter; 12 use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames; 13 use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator; 14 use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings; 15 use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties; 16 use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; 17 use PhpOffice\PhpSpreadsheet\RichText\RichText; 18 use PhpOffice\PhpSpreadsheet\Settings; 19 use PhpOffice\PhpSpreadsheet\Shared\Date; 20 use PhpOffice\PhpSpreadsheet\Shared\File; 21 use PhpOffice\PhpSpreadsheet\Spreadsheet; 22 use PhpOffice\PhpSpreadsheet\Style\NumberFormat; 23 use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; 24 use Throwable; 25 use XMLReader; 26 use ZipArchive; 27 28 class Ods extends BaseReader 29 { 30 const INITIAL_FILE = 'content.xml'; 31 32 /** 33 * Create a new Ods Reader instance. 34 */ 35 public function __construct() 36 { 37 parent::__construct(); 38 $this->securityScanner = XmlScanner::getInstance($this); 39 } 40 41 /** 42 * Can the current IReader read the file? 43 */ 44 public function canRead(string $filename): bool 45 { 46 $mimeType = 'UNKNOWN'; 47 48 // Load file 49 50 if (File::testFileNoThrow($filename, '')) { 51 $zip = new ZipArchive(); 52 if ($zip->open($filename) === true) { 53 // check if it is an OOXML archive 54 $stat = $zip->statName('mimetype'); 55 if ($stat && ($stat['size'] <= 255)) { 56 $mimeType = $zip->getFromName($stat['name']); 57 } elseif ($zip->statName('META-INF/manifest.xml')) { 58 $xml = simplexml_load_string( 59 $this->securityScanner->scan($zip->getFromName('META-INF/manifest.xml')), 60 'SimpleXMLElement', 61 Settings::getLibXmlLoaderOptions() 62 ); 63 $namespacesContent = $xml->getNamespaces(true); 64 if (isset($namespacesContent['manifest'])) { 65 $manifest = $xml->children($namespacesContent['manifest']); 66 foreach ($manifest as $manifestDataSet) { 67 $manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']); 68 if ($manifestAttributes && $manifestAttributes->{'full-path'} == '/') { 69 $mimeType = (string) $manifestAttributes->{'media-type'}; 70 71 break; 72 } 73 } 74 } 75 } 76 77 $zip->close(); 78 } 79 } 80 81 return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet'; 82 } 83 84 /** 85 * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object. 86 * 87 * @param string $filename 88 * 89 * @return string[] 90 */ 91 public function listWorksheetNames($filename) 92 { 93 File::assertFile($filename, self::INITIAL_FILE); 94 95 $worksheetNames = []; 96 97 $xml = new XMLReader(); 98 $xml->xml( 99 $this->securityScanner->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE), 100 null, 101 Settings::getLibXmlLoaderOptions() 102 ); 103 $xml->setParserProperty(2, true); 104 105 // Step into the first level of content of the XML 106 $xml->read(); 107 while ($xml->read()) { 108 // Quickly jump through to the office:body node 109 while (self::getXmlName($xml) !== 'office:body') { 110 if ($xml->isEmptyElement) { 111 $xml->read(); 112 } else { 113 $xml->next(); 114 } 115 } 116 // Now read each node until we find our first table:table node 117 while ($xml->read()) { 118 $xmlName = self::getXmlName($xml); 119 if ($xmlName == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { 120 // Loop through each table:table node reading the table:name attribute for each worksheet name 121 do { 122 $worksheetNames[] = $xml->getAttribute('table:name'); 123 $xml->next(); 124 } while (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT); 125 } 126 } 127 } 128 129 return $worksheetNames; 130 } 131 132 /** 133 * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). 134 * 135 * @param string $filename 136 * 137 * @return array 138 */ 139 public function listWorksheetInfo($filename) 140 { 141 File::assertFile($filename, self::INITIAL_FILE); 142 143 $worksheetInfo = []; 144 145 $xml = new XMLReader(); 146 $xml->xml( 147 $this->securityScanner->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE), 148 null, 149 Settings::getLibXmlLoaderOptions() 150 ); 151 $xml->setParserProperty(2, true); 152 153 // Step into the first level of content of the XML 154 $xml->read(); 155 while ($xml->read()) { 156 // Quickly jump through to the office:body node 157 while (self::getXmlName($xml) !== 'office:body') { 158 if ($xml->isEmptyElement) { 159 $xml->read(); 160 } else { 161 $xml->next(); 162 } 163 } 164 // Now read each node until we find our first table:table node 165 while ($xml->read()) { 166 if (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { 167 $worksheetNames[] = $xml->getAttribute('table:name'); 168 169 $tmpInfo = [ 170 'worksheetName' => $xml->getAttribute('table:name'), 171 'lastColumnLetter' => 'A', 172 'lastColumnIndex' => 0, 173 'totalRows' => 0, 174 'totalColumns' => 0, 175 ]; 176 177 // Loop through each child node of the table:table element reading 178 $currCells = 0; 179 do { 180 $xml->read(); 181 if (self::getXmlName($xml) == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { 182 $rowspan = $xml->getAttribute('table:number-rows-repeated'); 183 $rowspan = empty($rowspan) ? 1 : $rowspan; 184 $tmpInfo['totalRows'] += $rowspan; 185 $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); 186 $currCells = 0; 187 // Step into the row 188 $xml->read(); 189 do { 190 $doread = true; 191 if (self::getXmlName($xml) == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { 192 if (!$xml->isEmptyElement) { 193 ++$currCells; 194 $xml->next(); 195 $doread = false; 196 } 197 } elseif (self::getXmlName($xml) == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { 198 $mergeSize = $xml->getAttribute('table:number-columns-repeated'); 199 $currCells += (int) $mergeSize; 200 } 201 if ($doread) { 202 $xml->read(); 203 } 204 } while (self::getXmlName($xml) != 'table:table-row'); 205 } 206 } while (self::getXmlName($xml) != 'table:table'); 207 208 $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); 209 $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; 210 $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); 211 $worksheetInfo[] = $tmpInfo; 212 } 213 } 214 } 215 216 return $worksheetInfo; 217 } 218 219 /** 220 * Counteract Phpstan caching. 221 * 222 * @phpstan-impure 223 */ 224 private static function getXmlName(XMLReader $xml): string 225 { 226 return $xml->name; 227 } 228 229 /** 230 * Loads PhpSpreadsheet from file. 231 */ 232 protected function loadSpreadsheetFromFile(string $filename): Spreadsheet 233 { 234 // Create new Spreadsheet 235 $spreadsheet = new Spreadsheet(); 236 237 // Load into this instance 238 return $this->loadIntoExisting($filename, $spreadsheet); 239 } 240 241 /** 242 * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. 243 * 244 * @param string $filename 245 * 246 * @return Spreadsheet 247 */ 248 public function loadIntoExisting($filename, Spreadsheet $spreadsheet) 249 { 250 File::assertFile($filename, self::INITIAL_FILE); 251 252 $zip = new ZipArchive(); 253 $zip->open($filename); 254 255 // Meta 256 257 $xml = @simplexml_load_string( 258 $this->securityScanner->scan($zip->getFromName('meta.xml')), 259 'SimpleXMLElement', 260 Settings::getLibXmlLoaderOptions() 261 ); 262 if ($xml === false) { 263 throw new Exception('Unable to read data from {$pFilename}'); 264 } 265 266 $namespacesMeta = $xml->getNamespaces(true); 267 268 (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta); 269 270 // Styles 271 272 $dom = new DOMDocument('1.01', 'UTF-8'); 273 $dom->loadXML( 274 $this->securityScanner->scan($zip->getFromName('styles.xml')), 275 Settings::getLibXmlLoaderOptions() 276 ); 277 278 $pageSettings = new PageSettings($dom); 279 280 // Main Content 281 282 $dom = new DOMDocument('1.01', 'UTF-8'); 283 $dom->loadXML( 284 $this->securityScanner->scan($zip->getFromName(self::INITIAL_FILE)), 285 Settings::getLibXmlLoaderOptions() 286 ); 287 288 $officeNs = $dom->lookupNamespaceUri('office'); 289 $tableNs = $dom->lookupNamespaceUri('table'); 290 $textNs = $dom->lookupNamespaceUri('text'); 291 $xlinkNs = $dom->lookupNamespaceUri('xlink'); 292 293 $pageSettings->readStyleCrossReferences($dom); 294 295 $autoFilterReader = new AutoFilter($spreadsheet, $tableNs); 296 $definedNameReader = new DefinedNames($spreadsheet, $tableNs); 297 298 // Content 299 $spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body') 300 ->item(0) 301 ->getElementsByTagNameNS($officeNs, 'spreadsheet'); 302 303 foreach ($spreadsheets as $workbookData) { 304 /** @var DOMElement $workbookData */ 305 $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table'); 306 307 $worksheetID = 0; 308 foreach ($tables as $worksheetDataSet) { 309 /** @var DOMElement $worksheetDataSet */ 310 $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name'); 311 312 // Check loadSheetsOnly 313 if ( 314 isset($this->loadSheetsOnly) 315 && $worksheetName 316 && !in_array($worksheetName, $this->loadSheetsOnly) 317 ) { 318 continue; 319 } 320 321 $worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name'); 322 323 // Create sheet 324 if ($worksheetID > 0) { 325 $spreadsheet->createSheet(); // First sheet is added by default 326 } 327 $spreadsheet->setActiveSheetIndex($worksheetID); 328 329 if ($worksheetName || is_numeric($worksheetName)) { 330 // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in 331 // formula cells... during the load, all formulae should be correct, and we're simply 332 // bringing the worksheet name in line with the formula, not the reverse 333 $spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false); 334 } 335 336 // Go through every child of table element 337 $rowID = 1; 338 foreach ($worksheetDataSet->childNodes as $childNode) { 339 /** @var DOMElement $childNode */ 340 341 // Filter elements which are not under the "table" ns 342 if ($childNode->namespaceURI != $tableNs) { 343 continue; 344 } 345 346 $key = $childNode->nodeName; 347 348 // Remove ns from node name 349 if (strpos($key, ':') !== false) { 350 $keyChunks = explode(':', $key); 351 $key = array_pop($keyChunks); 352 } 353 354 switch ($key) { 355 case 'table-header-rows': 356 /// TODO :: Figure this out. This is only a partial implementation I guess. 357 // ($rowData it's not used at all and I'm not sure that PHPExcel 358 // has an API for this) 359 360 // foreach ($rowData as $keyRowData => $cellData) { 361 // $rowData = $cellData; 362 // break; 363 // } 364 break; 365 case 'table-row': 366 if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) { 367 $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated'); 368 } else { 369 $rowRepeats = 1; 370 } 371 372 $columnID = 'A'; 373 /** @var DOMElement $cellData */ 374 foreach ($childNode->childNodes as $cellData) { 375 if ($this->getReadFilter() !== null) { 376 if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { 377 ++$columnID; 378 379 continue; 380 } 381 } 382 383 // Initialize variables 384 $formatting = $hyperlink = null; 385 $hasCalculatedValue = false; 386 $cellDataFormula = ''; 387 388 if ($cellData->hasAttributeNS($tableNs, 'formula')) { 389 $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula'); 390 $hasCalculatedValue = true; 391 } 392 393 // Annotations 394 $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation'); 395 396 if ($annotation->length > 0) { 397 $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p'); 398 399 if ($textNode->length > 0) { 400 $text = $this->scanElementForText($textNode->item(0)); 401 402 $spreadsheet->getActiveSheet() 403 ->getComment($columnID . $rowID) 404 ->setText($this->parseRichText($text)); 405 // ->setAuthor( $author ) 406 } 407 } 408 409 // Content 410 411 /** @var DOMElement[] $paragraphs */ 412 $paragraphs = []; 413 414 foreach ($cellData->childNodes as $item) { 415 /** @var DOMElement $item */ 416 417 // Filter text:p elements 418 if ($item->nodeName == 'text:p') { 419 $paragraphs[] = $item; 420 } 421 } 422 423 if (count($paragraphs) > 0) { 424 // Consolidate if there are multiple p records (maybe with spans as well) 425 $dataArray = []; 426 427 // Text can have multiple text:p and within those, multiple text:span. 428 // text:p newlines, but text:span does not. 429 // Also, here we assume there is no text data is span fields are specified, since 430 // we have no way of knowing proper positioning anyway. 431 432 foreach ($paragraphs as $pData) { 433 $dataArray[] = $this->scanElementForText($pData); 434 } 435 $allCellDataText = implode("\n", $dataArray); 436 437 $type = $cellData->getAttributeNS($officeNs, 'value-type'); 438 439 switch ($type) { 440 case 'string': 441 $type = DataType::TYPE_STRING; 442 $dataValue = $allCellDataText; 443 444 foreach ($paragraphs as $paragraph) { 445 $link = $paragraph->getElementsByTagNameNS($textNs, 'a'); 446 if ($link->length > 0) { 447 $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href'); 448 } 449 } 450 451 break; 452 case 'boolean': 453 $type = DataType::TYPE_BOOL; 454 $dataValue = ($allCellDataText == 'TRUE') ? true : false; 455 456 break; 457 case 'percentage': 458 $type = DataType::TYPE_NUMERIC; 459 $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); 460 461 // percentage should always be float 462 //if (floor($dataValue) == $dataValue) { 463 // $dataValue = (int) $dataValue; 464 //} 465 $formatting = NumberFormat::FORMAT_PERCENTAGE_00; 466 467 break; 468 case 'currency': 469 $type = DataType::TYPE_NUMERIC; 470 $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); 471 472 if (floor($dataValue) == $dataValue) { 473 $dataValue = (int) $dataValue; 474 } 475 $formatting = NumberFormat::FORMAT_CURRENCY_USD_SIMPLE; 476 477 break; 478 case 'float': 479 $type = DataType::TYPE_NUMERIC; 480 $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); 481 482 if (floor($dataValue) == $dataValue) { 483 if ($dataValue == (int) $dataValue) { 484 $dataValue = (int) $dataValue; 485 } 486 } 487 488 break; 489 case 'date': 490 $type = DataType::TYPE_NUMERIC; 491 $value = $cellData->getAttributeNS($officeNs, 'date-value'); 492 $dataValue = Date::convertIsoDate($value); 493 494 if ($dataValue != floor($dataValue)) { 495 $formatting = NumberFormat::FORMAT_DATE_XLSX15 496 . ' ' 497 . NumberFormat::FORMAT_DATE_TIME4; 498 } else { 499 $formatting = NumberFormat::FORMAT_DATE_XLSX15; 500 } 501 502 break; 503 case 'time': 504 $type = DataType::TYPE_NUMERIC; 505 506 $timeValue = $cellData->getAttributeNS($officeNs, 'time-value'); 507 508 $dataValue = Date::PHPToExcel( 509 strtotime( 510 '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? []) 511 ) 512 ); 513 $formatting = NumberFormat::FORMAT_DATE_TIME4; 514 515 break; 516 default: 517 $dataValue = null; 518 } 519 } else { 520 $type = DataType::TYPE_NULL; 521 $dataValue = null; 522 } 523 524 if ($hasCalculatedValue) { 525 $type = DataType::TYPE_FORMULA; 526 $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1); 527 $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula); 528 } 529 530 if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { 531 $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); 532 } else { 533 $colRepeats = 1; 534 } 535 536 if ($type !== null) { 537 for ($i = 0; $i < $colRepeats; ++$i) { 538 if ($i > 0) { 539 ++$columnID; 540 } 541 542 if ($type !== DataType::TYPE_NULL) { 543 for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) { 544 $rID = $rowID + $rowAdjust; 545 546 $cell = $spreadsheet->getActiveSheet() 547 ->getCell($columnID . $rID); 548 549 // Set value 550 if ($hasCalculatedValue) { 551 $cell->setValueExplicit($cellDataFormula, $type); 552 } else { 553 $cell->setValueExplicit($dataValue, $type); 554 } 555 556 if ($hasCalculatedValue) { 557 $cell->setCalculatedValue($dataValue); 558 } 559 560 // Set other properties 561 if ($formatting !== null) { 562 $spreadsheet->getActiveSheet() 563 ->getStyle($columnID . $rID) 564 ->getNumberFormat() 565 ->setFormatCode($formatting); 566 } else { 567 $spreadsheet->getActiveSheet() 568 ->getStyle($columnID . $rID) 569 ->getNumberFormat() 570 ->setFormatCode(NumberFormat::FORMAT_GENERAL); 571 } 572 573 if ($hyperlink !== null) { 574 $cell->getHyperlink() 575 ->setUrl($hyperlink); 576 } 577 } 578 } 579 } 580 } 581 582 // Merged cells 583 $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet); 584 585 ++$columnID; 586 } 587 $rowID += $rowRepeats; 588 589 break; 590 } 591 } 592 $pageSettings->setVisibilityForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); 593 $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); 594 ++$worksheetID; 595 } 596 597 $autoFilterReader->read($workbookData); 598 $definedNameReader->read($workbookData); 599 } 600 $spreadsheet->setActiveSheetIndex(0); 601 602 if ($zip->locateName('settings.xml') !== false) { 603 $this->processSettings($zip, $spreadsheet); 604 } 605 606 // Return 607 return $spreadsheet; 608 } 609 610 private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void 611 { 612 $dom = new DOMDocument('1.01', 'UTF-8'); 613 $dom->loadXML( 614 $this->securityScanner->scan($zip->getFromName('settings.xml')), 615 Settings::getLibXmlLoaderOptions() 616 ); 617 //$xlinkNs = $dom->lookupNamespaceUri('xlink'); 618 $configNs = $dom->lookupNamespaceUri('config'); 619 //$oooNs = $dom->lookupNamespaceUri('ooo'); 620 $officeNs = $dom->lookupNamespaceUri('office'); 621 $settings = $dom->getElementsByTagNameNS($officeNs, 'settings') 622 ->item(0); 623 $this->lookForActiveSheet($settings, $spreadsheet, $configNs); 624 $this->lookForSelectedCells($settings, $spreadsheet, $configNs); 625 } 626 627 private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void 628 { 629 /** @var DOMElement $t */ 630 foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) { 631 if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') { 632 try { 633 $spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? ''); 634 } catch (Throwable $e) { 635 // do nothing 636 } 637 638 break; 639 } 640 } 641 } 642 643 private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void 644 { 645 /** @var DOMElement $t */ 646 foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) { 647 if ($t->getAttributeNs($configNs, 'name') === 'Tables') { 648 foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) { 649 $setRow = $setCol = ''; 650 $wsname = $ws->getAttributeNs($configNs, 'name'); 651 foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) { 652 $attrName = $configItem->getAttributeNs($configNs, 'name'); 653 if ($attrName === 'CursorPositionX') { 654 $setCol = $configItem->nodeValue; 655 } 656 if ($attrName === 'CursorPositionY') { 657 $setRow = $configItem->nodeValue; 658 } 659 } 660 $this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow"); 661 } 662 663 break; 664 } 665 } 666 } 667 668 private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void 669 { 670 if (is_numeric($setCol) && is_numeric($setRow)) { 671 try { 672 $spreadsheet->getSheetByName($wsname)->setSelectedCellByColumnAndRow($setCol + 1, $setRow + 1); 673 } catch (Throwable $e) { 674 // do nothing 675 } 676 } 677 } 678 679 /** 680 * Recursively scan element. 681 * 682 * @return string 683 */ 684 protected function scanElementForText(DOMNode $element) 685 { 686 $str = ''; 687 foreach ($element->childNodes as $child) { 688 /** @var DOMNode $child */ 689 if ($child->nodeType == XML_TEXT_NODE) { 690 $str .= $child->nodeValue; 691 } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') { 692 // It's a space 693 694 // Multiple spaces? 695 /** @var DOMAttr $cAttr */ 696 $cAttr = $child->attributes->getNamedItem('c'); 697 $multiplier = self::getMultiplier($cAttr); 698 $str .= str_repeat(' ', $multiplier); 699 } 700 701 if ($child->hasChildNodes()) { 702 $str .= $this->scanElementForText($child); 703 } 704 } 705 706 return $str; 707 } 708 709 private static function getMultiplier(?DOMAttr $cAttr): int 710 { 711 if ($cAttr) { 712 $multiplier = (int) $cAttr->nodeValue; 713 } else { 714 $multiplier = 1; 715 } 716 717 return $multiplier; 718 } 719 720 /** 721 * @param string $is 722 * 723 * @return RichText 724 */ 725 private function parseRichText($is) 726 { 727 $value = new RichText(); 728 $value->createText($is); 729 730 return $value; 731 } 732 733 private function processMergedCells( 734 DOMElement $cellData, 735 string $tableNs, 736 string $type, 737 string $columnID, 738 int $rowID, 739 Spreadsheet $spreadsheet 740 ): void { 741 if ( 742 $cellData->hasAttributeNS($tableNs, 'number-columns-spanned') 743 || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned') 744 ) { 745 if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) { 746 $columnTo = $columnID; 747 748 if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) { 749 $columnIndex = Coordinate::columnIndexFromString($columnID); 750 $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned'); 751 $columnIndex -= 2; 752 753 $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1); 754 } 755 756 $rowTo = $rowID; 757 758 if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) { 759 $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1; 760 } 761 762 $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; 763 $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); 764 } 765 } 766 } 767 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body