Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

   1  <?php
   2  
   3  namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
   4  
   5  use PhpOffice\PhpSpreadsheet\Cell\Cell;
   6  use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
   7  use PhpOffice\PhpSpreadsheet\RichText\RichText;
   8  use PhpOffice\PhpSpreadsheet\Settings;
   9  use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
  10  use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
  11  use PhpOffice\PhpSpreadsheet\Style\Conditional;
  12  use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
  13  use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension;
  14  use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column;
  15  use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
  16  use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
  17  use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as PhpspreadsheetWorksheet;
  18  
  19  class Worksheet extends WriterPart
  20  {
  21      /**
  22       * Write worksheet to XML format.
  23       *
  24       * @param string[] $stringTable
  25       * @param bool $includeCharts Flag indicating if we should write charts
  26       *
  27       * @return string XML Output
  28       */
  29      public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = null, $includeCharts = false)
  30      {
  31          // Create XML writer
  32          $objWriter = null;
  33          if ($this->getParentWriter()->getUseDiskCaching()) {
  34              $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
  35          } else {
  36              $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
  37          }
  38  
  39          // XML header
  40          $objWriter->startDocument('1.0', 'UTF-8', 'yes');
  41  
  42          // Worksheet
  43          $objWriter->startElement('worksheet');
  44          $objWriter->writeAttribute('xml:space', 'preserve');
  45          $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
  46          $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
  47  
  48          $objWriter->writeAttribute('xmlns:xdr', 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing');
  49          $objWriter->writeAttribute('xmlns:x14', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main');
  50          $objWriter->writeAttribute('xmlns:xm', 'http://schemas.microsoft.com/office/excel/2006/main');
  51          $objWriter->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006');
  52          $objWriter->writeAttribute('mc:Ignorable', 'x14ac');
  53          $objWriter->writeAttribute('xmlns:x14ac', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac');
  54  
  55          // sheetPr
  56          $this->writeSheetPr($objWriter, $worksheet);
  57  
  58          // Dimension
  59          $this->writeDimension($objWriter, $worksheet);
  60  
  61          // sheetViews
  62          $this->writeSheetViews($objWriter, $worksheet);
  63  
  64          // sheetFormatPr
  65          $this->writeSheetFormatPr($objWriter, $worksheet);
  66  
  67          // cols
  68          $this->writeCols($objWriter, $worksheet);
  69  
  70          // sheetData
  71          $this->writeSheetData($objWriter, $worksheet, $stringTable);
  72  
  73          // sheetProtection
  74          $this->writeSheetProtection($objWriter, $worksheet);
  75  
  76          // protectedRanges
  77          $this->writeProtectedRanges($objWriter, $worksheet);
  78  
  79          // autoFilter
  80          $this->writeAutoFilter($objWriter, $worksheet);
  81  
  82          // mergeCells
  83          $this->writeMergeCells($objWriter, $worksheet);
  84  
  85          // conditionalFormatting
  86          $this->writeConditionalFormatting($objWriter, $worksheet);
  87  
  88          // dataValidations
  89          $this->writeDataValidations($objWriter, $worksheet);
  90  
  91          // hyperlinks
  92          $this->writeHyperlinks($objWriter, $worksheet);
  93  
  94          // Print options
  95          $this->writePrintOptions($objWriter, $worksheet);
  96  
  97          // Page margins
  98          $this->writePageMargins($objWriter, $worksheet);
  99  
 100          // Page setup
 101          $this->writePageSetup($objWriter, $worksheet);
 102  
 103          // Header / footer
 104          $this->writeHeaderFooter($objWriter, $worksheet);
 105  
 106          // Breaks
 107          $this->writeBreaks($objWriter, $worksheet);
 108  
 109          // Drawings and/or Charts
 110          $this->writeDrawings($objWriter, $worksheet, $includeCharts);
 111  
 112          // LegacyDrawing
 113          $this->writeLegacyDrawing($objWriter, $worksheet);
 114  
 115          // LegacyDrawingHF
 116          $this->writeLegacyDrawingHF($objWriter, $worksheet);
 117  
 118          // AlternateContent
 119          $this->writeAlternateContent($objWriter, $worksheet);
 120  
 121          // ConditionalFormattingRuleExtensionList
 122          // (Must be inserted last. Not insert last, an Excel parse error will occur)
 123          $this->writeExtLst($objWriter, $worksheet);
 124  
 125          $objWriter->endElement();
 126  
 127          // Return
 128          return $objWriter->getData();
 129      }
 130  
 131      /**
 132       * Write SheetPr.
 133       */
 134      private function writeSheetPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 135      {
 136          // sheetPr
 137          $objWriter->startElement('sheetPr');
 138          if ($worksheet->getParent()->hasMacros()) {
 139              //if the workbook have macros, we need to have codeName for the sheet
 140              if (!$worksheet->hasCodeName()) {
 141                  $worksheet->setCodeName($worksheet->getTitle());
 142              }
 143              self::writeAttributeNotNull($objWriter, 'codeName', $worksheet->getCodeName());
 144          }
 145          $autoFilterRange = $worksheet->getAutoFilter()->getRange();
 146          if (!empty($autoFilterRange)) {
 147              $objWriter->writeAttribute('filterMode', 1);
 148              if (!$worksheet->getAutoFilter()->getEvaluated()) {
 149                  $worksheet->getAutoFilter()->showHideRows();
 150              }
 151          }
 152  
 153          // tabColor
 154          if ($worksheet->isTabColorSet()) {
 155              $objWriter->startElement('tabColor');
 156              $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB());
 157              $objWriter->endElement();
 158          }
 159  
 160          // outlinePr
 161          $objWriter->startElement('outlinePr');
 162          $objWriter->writeAttribute('summaryBelow', ($worksheet->getShowSummaryBelow() ? '1' : '0'));
 163          $objWriter->writeAttribute('summaryRight', ($worksheet->getShowSummaryRight() ? '1' : '0'));
 164          $objWriter->endElement();
 165  
 166          // pageSetUpPr
 167          if ($worksheet->getPageSetup()->getFitToPage()) {
 168              $objWriter->startElement('pageSetUpPr');
 169              $objWriter->writeAttribute('fitToPage', '1');
 170              $objWriter->endElement();
 171          }
 172  
 173          $objWriter->endElement();
 174      }
 175  
 176      /**
 177       * Write Dimension.
 178       */
 179      private function writeDimension(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 180      {
 181          // dimension
 182          $objWriter->startElement('dimension');
 183          $objWriter->writeAttribute('ref', $worksheet->calculateWorksheetDimension());
 184          $objWriter->endElement();
 185      }
 186  
 187      /**
 188       * Write SheetViews.
 189       */
 190      private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 191      {
 192          // sheetViews
 193          $objWriter->startElement('sheetViews');
 194  
 195          // Sheet selected?
 196          $sheetSelected = false;
 197          if ($this->getParentWriter()->getSpreadsheet()->getIndex($worksheet) == $this->getParentWriter()->getSpreadsheet()->getActiveSheetIndex()) {
 198              $sheetSelected = true;
 199          }
 200  
 201          // sheetView
 202          $objWriter->startElement('sheetView');
 203          $objWriter->writeAttribute('tabSelected', $sheetSelected ? '1' : '0');
 204          $objWriter->writeAttribute('workbookViewId', '0');
 205  
 206          // Zoom scales
 207          if ($worksheet->getSheetView()->getZoomScale() != 100) {
 208              $objWriter->writeAttribute('zoomScale', $worksheet->getSheetView()->getZoomScale());
 209          }
 210          if ($worksheet->getSheetView()->getZoomScaleNormal() != 100) {
 211              $objWriter->writeAttribute('zoomScaleNormal', $worksheet->getSheetView()->getZoomScaleNormal());
 212          }
 213  
 214          // Show zeros (Excel also writes this attribute only if set to false)
 215          if ($worksheet->getSheetView()->getShowZeros() === false) {
 216              $objWriter->writeAttribute('showZeros', 0);
 217          }
 218  
 219          // View Layout Type
 220          if ($worksheet->getSheetView()->getView() !== SheetView::SHEETVIEW_NORMAL) {
 221              $objWriter->writeAttribute('view', $worksheet->getSheetView()->getView());
 222          }
 223  
 224          // Gridlines
 225          if ($worksheet->getShowGridlines()) {
 226              $objWriter->writeAttribute('showGridLines', 'true');
 227          } else {
 228              $objWriter->writeAttribute('showGridLines', 'false');
 229          }
 230  
 231          // Row and column headers
 232          if ($worksheet->getShowRowColHeaders()) {
 233              $objWriter->writeAttribute('showRowColHeaders', '1');
 234          } else {
 235              $objWriter->writeAttribute('showRowColHeaders', '0');
 236          }
 237  
 238          // Right-to-left
 239          if ($worksheet->getRightToLeft()) {
 240              $objWriter->writeAttribute('rightToLeft', 'true');
 241          }
 242  
 243          $topLeftCell = $worksheet->getTopLeftCell();
 244          $activeCell = $worksheet->getActiveCell();
 245          $sqref = $worksheet->getSelectedCells();
 246  
 247          // Pane
 248          $pane = '';
 249          if ($worksheet->getFreezePane()) {
 250              [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane() ?? '');
 251              $xSplit = Coordinate::columnIndexFromString($xSplit);
 252              --$xSplit;
 253              --$ySplit;
 254  
 255              // pane
 256              $pane = 'topRight';
 257              $objWriter->startElement('pane');
 258              if ($xSplit > 0) {
 259                  $objWriter->writeAttribute('xSplit', $xSplit);
 260              }
 261              if ($ySplit > 0) {
 262                  $objWriter->writeAttribute('ySplit', $ySplit);
 263                  $pane = ($xSplit > 0) ? 'bottomRight' : 'bottomLeft';
 264              }
 265              self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell);
 266              $objWriter->writeAttribute('activePane', $pane);
 267              $objWriter->writeAttribute('state', 'frozen');
 268              $objWriter->endElement();
 269  
 270              if (($xSplit > 0) && ($ySplit > 0)) {
 271                  //    Write additional selections if more than two panes (ie both an X and a Y split)
 272                  $objWriter->startElement('selection');
 273                  $objWriter->writeAttribute('pane', 'topRight');
 274                  $objWriter->endElement();
 275                  $objWriter->startElement('selection');
 276                  $objWriter->writeAttribute('pane', 'bottomLeft');
 277                  $objWriter->endElement();
 278              }
 279          } else {
 280              self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell);
 281          }
 282  
 283          // Selection
 284          // Only need to write selection element if we have a split pane
 285          // We cheat a little by over-riding the active cell selection, setting it to the split cell
 286          $objWriter->startElement('selection');
 287          if ($pane != '') {
 288              $objWriter->writeAttribute('pane', $pane);
 289          }
 290          $objWriter->writeAttribute('activeCell', $activeCell);
 291          $objWriter->writeAttribute('sqref', $sqref);
 292          $objWriter->endElement();
 293  
 294          $objWriter->endElement();
 295  
 296          $objWriter->endElement();
 297      }
 298  
 299      /**
 300       * Write SheetFormatPr.
 301       */
 302      private function writeSheetFormatPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 303      {
 304          // sheetFormatPr
 305          $objWriter->startElement('sheetFormatPr');
 306  
 307          // Default row height
 308          if ($worksheet->getDefaultRowDimension()->getRowHeight() >= 0) {
 309              $objWriter->writeAttribute('customHeight', 'true');
 310              $objWriter->writeAttribute('defaultRowHeight', StringHelper::formatNumber($worksheet->getDefaultRowDimension()->getRowHeight()));
 311          } else {
 312              $objWriter->writeAttribute('defaultRowHeight', '14.4');
 313          }
 314  
 315          // Set Zero Height row
 316          if (
 317              (string) $worksheet->getDefaultRowDimension()->getZeroHeight() === '1' ||
 318              strtolower((string) $worksheet->getDefaultRowDimension()->getZeroHeight()) == 'true'
 319          ) {
 320              $objWriter->writeAttribute('zeroHeight', '1');
 321          }
 322  
 323          // Default column width
 324          if ($worksheet->getDefaultColumnDimension()->getWidth() >= 0) {
 325              $objWriter->writeAttribute('defaultColWidth', StringHelper::formatNumber($worksheet->getDefaultColumnDimension()->getWidth()));
 326          }
 327  
 328          // Outline level - row
 329          $outlineLevelRow = 0;
 330          foreach ($worksheet->getRowDimensions() as $dimension) {
 331              if ($dimension->getOutlineLevel() > $outlineLevelRow) {
 332                  $outlineLevelRow = $dimension->getOutlineLevel();
 333              }
 334          }
 335          $objWriter->writeAttribute('outlineLevelRow', (int) $outlineLevelRow);
 336  
 337          // Outline level - column
 338          $outlineLevelCol = 0;
 339          foreach ($worksheet->getColumnDimensions() as $dimension) {
 340              if ($dimension->getOutlineLevel() > $outlineLevelCol) {
 341                  $outlineLevelCol = $dimension->getOutlineLevel();
 342              }
 343          }
 344          $objWriter->writeAttribute('outlineLevelCol', (int) $outlineLevelCol);
 345  
 346          $objWriter->endElement();
 347      }
 348  
 349      /**
 350       * Write Cols.
 351       */
 352      private function writeCols(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 353      {
 354          // cols
 355          if (count($worksheet->getColumnDimensions()) > 0) {
 356              $objWriter->startElement('cols');
 357  
 358              $worksheet->calculateColumnWidths();
 359  
 360              // Loop through column dimensions
 361              foreach ($worksheet->getColumnDimensions() as $colDimension) {
 362                  // col
 363                  $objWriter->startElement('col');
 364                  $objWriter->writeAttribute('min', Coordinate::columnIndexFromString($colDimension->getColumnIndex()));
 365                  $objWriter->writeAttribute('max', Coordinate::columnIndexFromString($colDimension->getColumnIndex()));
 366  
 367                  if ($colDimension->getWidth() < 0) {
 368                      // No width set, apply default of 10
 369                      $objWriter->writeAttribute('width', '9.10');
 370                  } else {
 371                      // Width set
 372                      $objWriter->writeAttribute('width', StringHelper::formatNumber($colDimension->getWidth()));
 373                  }
 374  
 375                  // Column visibility
 376                  if ($colDimension->getVisible() === false) {
 377                      $objWriter->writeAttribute('hidden', 'true');
 378                  }
 379  
 380                  // Auto size?
 381                  if ($colDimension->getAutoSize()) {
 382                      $objWriter->writeAttribute('bestFit', 'true');
 383                  }
 384  
 385                  // Custom width?
 386                  if ($colDimension->getWidth() != $worksheet->getDefaultColumnDimension()->getWidth()) {
 387                      $objWriter->writeAttribute('customWidth', 'true');
 388                  }
 389  
 390                  // Collapsed
 391                  if ($colDimension->getCollapsed() === true) {
 392                      $objWriter->writeAttribute('collapsed', 'true');
 393                  }
 394  
 395                  // Outline level
 396                  if ($colDimension->getOutlineLevel() > 0) {
 397                      $objWriter->writeAttribute('outlineLevel', $colDimension->getOutlineLevel());
 398                  }
 399  
 400                  // Style
 401                  $objWriter->writeAttribute('style', $colDimension->getXfIndex());
 402  
 403                  $objWriter->endElement();
 404              }
 405  
 406              $objWriter->endElement();
 407          }
 408      }
 409  
 410      /**
 411       * Write SheetProtection.
 412       */
 413      private function writeSheetProtection(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 414      {
 415          // sheetProtection
 416          $objWriter->startElement('sheetProtection');
 417  
 418          $protection = $worksheet->getProtection();
 419  
 420          if ($protection->getAlgorithm()) {
 421              $objWriter->writeAttribute('algorithmName', $protection->getAlgorithm());
 422              $objWriter->writeAttribute('hashValue', $protection->getPassword());
 423              $objWriter->writeAttribute('saltValue', $protection->getSalt());
 424              $objWriter->writeAttribute('spinCount', $protection->getSpinCount());
 425          } elseif ($protection->getPassword() !== '') {
 426              $objWriter->writeAttribute('password', $protection->getPassword());
 427          }
 428  
 429          $objWriter->writeAttribute('sheet', ($protection->getSheet() ? 'true' : 'false'));
 430          $objWriter->writeAttribute('objects', ($protection->getObjects() ? 'true' : 'false'));
 431          $objWriter->writeAttribute('scenarios', ($protection->getScenarios() ? 'true' : 'false'));
 432          $objWriter->writeAttribute('formatCells', ($protection->getFormatCells() ? 'true' : 'false'));
 433          $objWriter->writeAttribute('formatColumns', ($protection->getFormatColumns() ? 'true' : 'false'));
 434          $objWriter->writeAttribute('formatRows', ($protection->getFormatRows() ? 'true' : 'false'));
 435          $objWriter->writeAttribute('insertColumns', ($protection->getInsertColumns() ? 'true' : 'false'));
 436          $objWriter->writeAttribute('insertRows', ($protection->getInsertRows() ? 'true' : 'false'));
 437          $objWriter->writeAttribute('insertHyperlinks', ($protection->getInsertHyperlinks() ? 'true' : 'false'));
 438          $objWriter->writeAttribute('deleteColumns', ($protection->getDeleteColumns() ? 'true' : 'false'));
 439          $objWriter->writeAttribute('deleteRows', ($protection->getDeleteRows() ? 'true' : 'false'));
 440          $objWriter->writeAttribute('selectLockedCells', ($protection->getSelectLockedCells() ? 'true' : 'false'));
 441          $objWriter->writeAttribute('sort', ($protection->getSort() ? 'true' : 'false'));
 442          $objWriter->writeAttribute('autoFilter', ($protection->getAutoFilter() ? 'true' : 'false'));
 443          $objWriter->writeAttribute('pivotTables', ($protection->getPivotTables() ? 'true' : 'false'));
 444          $objWriter->writeAttribute('selectUnlockedCells', ($protection->getSelectUnlockedCells() ? 'true' : 'false'));
 445          $objWriter->endElement();
 446      }
 447  
 448      private static function writeAttributeIf(XMLWriter $objWriter, $condition, string $attr, string $val): void
 449      {
 450          if ($condition) {
 451              $objWriter->writeAttribute($attr, $val);
 452          }
 453      }
 454  
 455      private static function writeAttributeNotNull(XMLWriter $objWriter, string $attr, ?string $val): void
 456      {
 457          if ($val !== null) {
 458              $objWriter->writeAttribute($attr, $val);
 459          }
 460      }
 461  
 462      private static function writeElementIf(XMLWriter $objWriter, $condition, string $attr, string $val): void
 463      {
 464          if ($condition) {
 465              $objWriter->writeElement($attr, $val);
 466          }
 467      }
 468  
 469      private static function writeOtherCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
 470      {
 471          if (
 472              $conditional->getConditionType() == Conditional::CONDITION_CELLIS
 473              || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT
 474              || $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION
 475          ) {
 476              foreach ($conditional->getConditions() as $formula) {
 477                  // Formula
 478                  $objWriter->writeElement('formula', Xlfn::addXlfn($formula));
 479              }
 480          } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) {
 481              // formula copied from ms xlsx xml source file
 482              $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))=0');
 483          } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSBLANKS) {
 484              // formula copied from ms xlsx xml source file
 485              $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))>0');
 486          }
 487      }
 488  
 489      private static function writeTextCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
 490      {
 491          $txt = $conditional->getText();
 492          if ($txt !== null) {
 493              $objWriter->writeAttribute('text', $txt);
 494              if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) {
 495                  $objWriter->writeElement('formula', 'NOT(ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . ')))');
 496              } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_BEGINSWITH) {
 497                  $objWriter->writeElement('formula', 'LEFT(' . $cellCoordinate . ',' . strlen($txt) . ')="' . $txt . '"');
 498              } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_ENDSWITH) {
 499                  $objWriter->writeElement('formula', 'RIGHT(' . $cellCoordinate . ',' . strlen($txt) . ')="' . $txt . '"');
 500              } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_NOTCONTAINS) {
 501                  $objWriter->writeElement('formula', 'ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . '))');
 502              }
 503          }
 504      }
 505  
 506      private static function writeExtConditionalFormattingElements(XMLWriter $objWriter, ConditionalFormattingRuleExtension $ruleExtension): void
 507      {
 508          $prefix = 'x14';
 509          $objWriter->startElementNs($prefix, 'conditionalFormatting', null);
 510  
 511          $objWriter->startElementNs($prefix, 'cfRule', null);
 512          $objWriter->writeAttribute('type', $ruleExtension->getCfRule());
 513          $objWriter->writeAttribute('id', $ruleExtension->getId());
 514          $objWriter->startElementNs($prefix, 'dataBar', null);
 515          $dataBar = $ruleExtension->getDataBarExt();
 516          foreach ($dataBar->getXmlAttributes() as $attrKey => $val) {
 517              $objWriter->writeAttribute($attrKey, $val);
 518          }
 519          $minCfvo = $dataBar->getMinimumConditionalFormatValueObject();
 520          if ($minCfvo) {
 521              $objWriter->startElementNs($prefix, 'cfvo', null);
 522              $objWriter->writeAttribute('type', $minCfvo->getType());
 523              if ($minCfvo->getCellFormula()) {
 524                  $objWriter->writeElement('xm:f', $minCfvo->getCellFormula());
 525              }
 526              $objWriter->endElement(); //end cfvo
 527          }
 528  
 529          $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject();
 530          if ($maxCfvo) {
 531              $objWriter->startElementNs($prefix, 'cfvo', null);
 532              $objWriter->writeAttribute('type', $maxCfvo->getType());
 533              if ($maxCfvo->getCellFormula()) {
 534                  $objWriter->writeElement('xm:f', $maxCfvo->getCellFormula());
 535              }
 536              $objWriter->endElement(); //end cfvo
 537          }
 538  
 539          foreach ($dataBar->getXmlElements() as $elmKey => $elmAttr) {
 540              $objWriter->startElementNs($prefix, $elmKey, null);
 541              foreach ($elmAttr as $attrKey => $attrVal) {
 542                  $objWriter->writeAttribute($attrKey, $attrVal);
 543              }
 544              $objWriter->endElement(); //end elmKey
 545          }
 546          $objWriter->endElement(); //end dataBar
 547          $objWriter->endElement(); //end cfRule
 548          $objWriter->writeElement('xm:sqref', $ruleExtension->getSqref());
 549          $objWriter->endElement(); //end conditionalFormatting
 550      }
 551  
 552      private static function writeDataBarElements(XMLWriter $objWriter, $dataBar): void
 553      {
 554          /** @var ConditionalDataBar $dataBar */
 555          if ($dataBar) {
 556              $objWriter->startElement('dataBar');
 557              self::writeAttributeIf($objWriter, null !== $dataBar->getShowValue(), 'showValue', $dataBar->getShowValue() ? '1' : '0');
 558  
 559              $minCfvo = $dataBar->getMinimumConditionalFormatValueObject();
 560              if ($minCfvo) {
 561                  $objWriter->startElement('cfvo');
 562                  self::writeAttributeIf($objWriter, $minCfvo->getType(), 'type', (string) $minCfvo->getType());
 563                  self::writeAttributeIf($objWriter, $minCfvo->getValue(), 'val', (string) $minCfvo->getValue());
 564                  $objWriter->endElement();
 565              }
 566              $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject();
 567              if ($maxCfvo) {
 568                  $objWriter->startElement('cfvo');
 569                  self::writeAttributeIf($objWriter, $maxCfvo->getType(), 'type', (string) $maxCfvo->getType());
 570                  self::writeAttributeIf($objWriter, $maxCfvo->getValue(), 'val', (string) $maxCfvo->getValue());
 571                  $objWriter->endElement();
 572              }
 573              if ($dataBar->getColor()) {
 574                  $objWriter->startElement('color');
 575                  $objWriter->writeAttribute('rgb', $dataBar->getColor());
 576                  $objWriter->endElement();
 577              }
 578              $objWriter->endElement(); // end dataBar
 579  
 580              if ($dataBar->getConditionalFormattingRuleExt()) {
 581                  $objWriter->startElement('extLst');
 582                  $extension = $dataBar->getConditionalFormattingRuleExt();
 583                  $objWriter->startElement('ext');
 584                  $objWriter->writeAttribute('uri', '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}');
 585                  $objWriter->startElementNs('x14', 'id', null);
 586                  $objWriter->text($extension->getId());
 587                  $objWriter->endElement();
 588                  $objWriter->endElement();
 589                  $objWriter->endElement(); //end extLst
 590              }
 591          }
 592      }
 593  
 594      /**
 595       * Write ConditionalFormatting.
 596       */
 597      private function writeConditionalFormatting(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 598      {
 599          // Conditional id
 600          $id = 1;
 601  
 602          // Loop through styles in the current worksheet
 603          foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
 604              foreach ($conditionalStyles as $conditional) {
 605                  // WHY was this again?
 606                  // if ($this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) == '') {
 607                  //    continue;
 608                  // }
 609                  if ($conditional->getConditionType() != Conditional::CONDITION_NONE) {
 610                      // conditionalFormatting
 611                      $objWriter->startElement('conditionalFormatting');
 612                      $objWriter->writeAttribute('sqref', $cellCoordinate);
 613  
 614                      // cfRule
 615                      $objWriter->startElement('cfRule');
 616                      $objWriter->writeAttribute('type', $conditional->getConditionType());
 617                      self::writeAttributeIf(
 618                          $objWriter,
 619                          ($conditional->getConditionType() != Conditional::CONDITION_DATABAR),
 620                          'dxfId',
 621                          (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())
 622                      );
 623                      $objWriter->writeAttribute('priority', $id++);
 624  
 625                      self::writeAttributeif(
 626                          $objWriter,
 627                          (
 628                              $conditional->getConditionType() === Conditional::CONDITION_CELLIS
 629                              || $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT
 630                              || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT
 631                          ) && $conditional->getOperatorType() !== Conditional::OPERATOR_NONE,
 632                          'operator',
 633                          $conditional->getOperatorType()
 634                      );
 635  
 636                      self::writeAttributeIf($objWriter, $conditional->getStopIfTrue(), 'stopIfTrue', '1');
 637  
 638                      if (
 639                          $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT
 640                          || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT
 641                      ) {
 642                          self::writeTextCondElements($objWriter, $conditional, $cellCoordinate);
 643                      } else {
 644                          self::writeOtherCondElements($objWriter, $conditional, $cellCoordinate);
 645                      }
 646  
 647                      //<dataBar>
 648                      self::writeDataBarElements($objWriter, $conditional->getDataBar());
 649  
 650                      $objWriter->endElement(); //end cfRule
 651  
 652                      $objWriter->endElement();
 653                  }
 654              }
 655          }
 656      }
 657  
 658      /**
 659       * Write DataValidations.
 660       */
 661      private function writeDataValidations(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 662      {
 663          // Datavalidation collection
 664          $dataValidationCollection = $worksheet->getDataValidationCollection();
 665  
 666          // Write data validations?
 667          if (!empty($dataValidationCollection)) {
 668              $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection);
 669              $objWriter->startElement('dataValidations');
 670              $objWriter->writeAttribute('count', count($dataValidationCollection));
 671  
 672              foreach ($dataValidationCollection as $coordinate => $dv) {
 673                  $objWriter->startElement('dataValidation');
 674  
 675                  if ($dv->getType() != '') {
 676                      $objWriter->writeAttribute('type', $dv->getType());
 677                  }
 678  
 679                  if ($dv->getErrorStyle() != '') {
 680                      $objWriter->writeAttribute('errorStyle', $dv->getErrorStyle());
 681                  }
 682  
 683                  if ($dv->getOperator() != '') {
 684                      $objWriter->writeAttribute('operator', $dv->getOperator());
 685                  }
 686  
 687                  $objWriter->writeAttribute('allowBlank', ($dv->getAllowBlank() ? '1' : '0'));
 688                  $objWriter->writeAttribute('showDropDown', (!$dv->getShowDropDown() ? '1' : '0'));
 689                  $objWriter->writeAttribute('showInputMessage', ($dv->getShowInputMessage() ? '1' : '0'));
 690                  $objWriter->writeAttribute('showErrorMessage', ($dv->getShowErrorMessage() ? '1' : '0'));
 691  
 692                  if ($dv->getErrorTitle() !== '') {
 693                      $objWriter->writeAttribute('errorTitle', $dv->getErrorTitle());
 694                  }
 695                  if ($dv->getError() !== '') {
 696                      $objWriter->writeAttribute('error', $dv->getError());
 697                  }
 698                  if ($dv->getPromptTitle() !== '') {
 699                      $objWriter->writeAttribute('promptTitle', $dv->getPromptTitle());
 700                  }
 701                  if ($dv->getPrompt() !== '') {
 702                      $objWriter->writeAttribute('prompt', $dv->getPrompt());
 703                  }
 704  
 705                  $objWriter->writeAttribute('sqref', $dv->getSqref() ?? $coordinate);
 706  
 707                  if ($dv->getFormula1() !== '') {
 708                      $objWriter->writeElement('formula1', $dv->getFormula1());
 709                  }
 710                  if ($dv->getFormula2() !== '') {
 711                      $objWriter->writeElement('formula2', $dv->getFormula2());
 712                  }
 713  
 714                  $objWriter->endElement();
 715              }
 716  
 717              $objWriter->endElement();
 718          }
 719      }
 720  
 721      /**
 722       * Write Hyperlinks.
 723       */
 724      private function writeHyperlinks(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 725      {
 726          // Hyperlink collection
 727          $hyperlinkCollection = $worksheet->getHyperlinkCollection();
 728  
 729          // Relation ID
 730          $relationId = 1;
 731  
 732          // Write hyperlinks?
 733          if (!empty($hyperlinkCollection)) {
 734              $objWriter->startElement('hyperlinks');
 735  
 736              foreach ($hyperlinkCollection as $coordinate => $hyperlink) {
 737                  $objWriter->startElement('hyperlink');
 738  
 739                  $objWriter->writeAttribute('ref', $coordinate);
 740                  if (!$hyperlink->isInternal()) {
 741                      $objWriter->writeAttribute('r:id', 'rId_hyperlink_' . $relationId);
 742                      ++$relationId;
 743                  } else {
 744                      $objWriter->writeAttribute('location', str_replace('sheet://', '', $hyperlink->getUrl()));
 745                  }
 746  
 747                  if ($hyperlink->getTooltip() !== '') {
 748                      $objWriter->writeAttribute('tooltip', $hyperlink->getTooltip());
 749                      $objWriter->writeAttribute('display', $hyperlink->getTooltip());
 750                  }
 751  
 752                  $objWriter->endElement();
 753              }
 754  
 755              $objWriter->endElement();
 756          }
 757      }
 758  
 759      /**
 760       * Write ProtectedRanges.
 761       */
 762      private function writeProtectedRanges(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 763      {
 764          if (count($worksheet->getProtectedCells()) > 0) {
 765              // protectedRanges
 766              $objWriter->startElement('protectedRanges');
 767  
 768              // Loop protectedRanges
 769              foreach ($worksheet->getProtectedCells() as $protectedCell => $passwordHash) {
 770                  // protectedRange
 771                  $objWriter->startElement('protectedRange');
 772                  $objWriter->writeAttribute('name', 'p' . md5($protectedCell));
 773                  $objWriter->writeAttribute('sqref', $protectedCell);
 774                  if (!empty($passwordHash)) {
 775                      $objWriter->writeAttribute('password', $passwordHash);
 776                  }
 777                  $objWriter->endElement();
 778              }
 779  
 780              $objWriter->endElement();
 781          }
 782      }
 783  
 784      /**
 785       * Write MergeCells.
 786       */
 787      private function writeMergeCells(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 788      {
 789          if (count($worksheet->getMergeCells()) > 0) {
 790              // mergeCells
 791              $objWriter->startElement('mergeCells');
 792  
 793              // Loop mergeCells
 794              foreach ($worksheet->getMergeCells() as $mergeCell) {
 795                  // mergeCell
 796                  $objWriter->startElement('mergeCell');
 797                  $objWriter->writeAttribute('ref', $mergeCell);
 798                  $objWriter->endElement();
 799              }
 800  
 801              $objWriter->endElement();
 802          }
 803      }
 804  
 805      /**
 806       * Write PrintOptions.
 807       */
 808      private function writePrintOptions(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 809      {
 810          // printOptions
 811          $objWriter->startElement('printOptions');
 812  
 813          $objWriter->writeAttribute('gridLines', ($worksheet->getPrintGridlines() ? 'true' : 'false'));
 814          $objWriter->writeAttribute('gridLinesSet', 'true');
 815  
 816          if ($worksheet->getPageSetup()->getHorizontalCentered()) {
 817              $objWriter->writeAttribute('horizontalCentered', 'true');
 818          }
 819  
 820          if ($worksheet->getPageSetup()->getVerticalCentered()) {
 821              $objWriter->writeAttribute('verticalCentered', 'true');
 822          }
 823  
 824          $objWriter->endElement();
 825      }
 826  
 827      /**
 828       * Write PageMargins.
 829       */
 830      private function writePageMargins(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 831      {
 832          // pageMargins
 833          $objWriter->startElement('pageMargins');
 834          $objWriter->writeAttribute('left', StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()));
 835          $objWriter->writeAttribute('right', StringHelper::formatNumber($worksheet->getPageMargins()->getRight()));
 836          $objWriter->writeAttribute('top', StringHelper::formatNumber($worksheet->getPageMargins()->getTop()));
 837          $objWriter->writeAttribute('bottom', StringHelper::formatNumber($worksheet->getPageMargins()->getBottom()));
 838          $objWriter->writeAttribute('header', StringHelper::formatNumber($worksheet->getPageMargins()->getHeader()));
 839          $objWriter->writeAttribute('footer', StringHelper::formatNumber($worksheet->getPageMargins()->getFooter()));
 840          $objWriter->endElement();
 841      }
 842  
 843      /**
 844       * Write AutoFilter.
 845       */
 846      private function writeAutoFilter(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 847      {
 848          $autoFilterRange = $worksheet->getAutoFilter()->getRange();
 849          if (!empty($autoFilterRange)) {
 850              // autoFilter
 851              $objWriter->startElement('autoFilter');
 852  
 853              // Strip any worksheet reference from the filter coordinates
 854              $range = Coordinate::splitRange($autoFilterRange);
 855              $range = $range[0];
 856              //    Strip any worksheet ref
 857              [$ws, $range[0]] = PhpspreadsheetWorksheet::extractSheetTitle($range[0], true);
 858              $range = implode(':', $range);
 859  
 860              $objWriter->writeAttribute('ref', str_replace('$', '', $range));
 861  
 862              $columns = $worksheet->getAutoFilter()->getColumns();
 863              if (count($columns) > 0) {
 864                  foreach ($columns as $columnID => $column) {
 865                      $rules = $column->getRules();
 866                      if (count($rules) > 0) {
 867                          $objWriter->startElement('filterColumn');
 868                          $objWriter->writeAttribute('colId', $worksheet->getAutoFilter()->getColumnOffset($columnID));
 869  
 870                          $objWriter->startElement($column->getFilterType());
 871                          if ($column->getJoin() == Column::AUTOFILTER_COLUMN_JOIN_AND) {
 872                              $objWriter->writeAttribute('and', 1);
 873                          }
 874  
 875                          foreach ($rules as $rule) {
 876                              if (
 877                                  ($column->getFilterType() === Column::AUTOFILTER_FILTERTYPE_FILTER) &&
 878                                  ($rule->getOperator() === Rule::AUTOFILTER_COLUMN_RULE_EQUAL) &&
 879                                  ($rule->getValue() === '')
 880                              ) {
 881                                  //    Filter rule for Blanks
 882                                  $objWriter->writeAttribute('blank', 1);
 883                              } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER) {
 884                                  //    Dynamic Filter Rule
 885                                  $objWriter->writeAttribute('type', $rule->getGrouping());
 886                                  $val = $column->getAttribute('val');
 887                                  if ($val !== null) {
 888                                      $objWriter->writeAttribute('val', "$val");
 889                                  }
 890                                  $maxVal = $column->getAttribute('maxVal');
 891                                  if ($maxVal !== null) {
 892                                      $objWriter->writeAttribute('maxVal', "$maxVal");
 893                                  }
 894                              } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_TOPTENFILTER) {
 895                                  //    Top 10 Filter Rule
 896                                  $ruleValue = $rule->getValue();
 897                                  if (!is_array($ruleValue)) {
 898                                      $objWriter->writeAttribute('val', "$ruleValue");
 899                                  }
 900                                  $objWriter->writeAttribute('percent', (($rule->getOperator() === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) ? '1' : '0'));
 901                                  $objWriter->writeAttribute('top', (($rule->getGrouping() === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) ? '1' : '0'));
 902                              } else {
 903                                  //    Filter, DateGroupItem or CustomFilter
 904                                  $objWriter->startElement($rule->getRuleType());
 905  
 906                                  if ($rule->getOperator() !== Rule::AUTOFILTER_COLUMN_RULE_EQUAL) {
 907                                      $objWriter->writeAttribute('operator', $rule->getOperator());
 908                                  }
 909                                  if ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DATEGROUP) {
 910                                      // Date Group filters
 911                                      $ruleValue = $rule->getValue();
 912                                      if (is_array($ruleValue)) {
 913                                          foreach ($ruleValue as $key => $value) {
 914                                              $objWriter->writeAttribute($key, "$value");
 915                                          }
 916                                      }
 917                                      $objWriter->writeAttribute('dateTimeGrouping', $rule->getGrouping());
 918                                  } else {
 919                                      $ruleValue = $rule->getValue();
 920                                      if (!is_array($ruleValue)) {
 921                                          $objWriter->writeAttribute('val', "$ruleValue");
 922                                      }
 923                                  }
 924  
 925                                  $objWriter->endElement();
 926                              }
 927                          }
 928  
 929                          $objWriter->endElement();
 930  
 931                          $objWriter->endElement();
 932                      }
 933                  }
 934              }
 935              $objWriter->endElement();
 936          }
 937      }
 938  
 939      /**
 940       * Write PageSetup.
 941       */
 942      private function writePageSetup(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 943      {
 944          // pageSetup
 945          $objWriter->startElement('pageSetup');
 946          $objWriter->writeAttribute('paperSize', $worksheet->getPageSetup()->getPaperSize());
 947          $objWriter->writeAttribute('orientation', $worksheet->getPageSetup()->getOrientation());
 948  
 949          if ($worksheet->getPageSetup()->getScale() !== null) {
 950              $objWriter->writeAttribute('scale', $worksheet->getPageSetup()->getScale());
 951          }
 952          if ($worksheet->getPageSetup()->getFitToHeight() !== null) {
 953              $objWriter->writeAttribute('fitToHeight', $worksheet->getPageSetup()->getFitToHeight());
 954          } else {
 955              $objWriter->writeAttribute('fitToHeight', '0');
 956          }
 957          if ($worksheet->getPageSetup()->getFitToWidth() !== null) {
 958              $objWriter->writeAttribute('fitToWidth', $worksheet->getPageSetup()->getFitToWidth());
 959          } else {
 960              $objWriter->writeAttribute('fitToWidth', '0');
 961          }
 962          if ($worksheet->getPageSetup()->getFirstPageNumber() !== null) {
 963              $objWriter->writeAttribute('firstPageNumber', $worksheet->getPageSetup()->getFirstPageNumber());
 964              $objWriter->writeAttribute('useFirstPageNumber', '1');
 965          }
 966          $objWriter->writeAttribute('pageOrder', $worksheet->getPageSetup()->getPageOrder());
 967  
 968          $getUnparsedLoadedData = $worksheet->getParent()->getUnparsedLoadedData();
 969          if (isset($getUnparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId'])) {
 970              $objWriter->writeAttribute('r:id', $getUnparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId']);
 971          }
 972  
 973          $objWriter->endElement();
 974      }
 975  
 976      /**
 977       * Write Header / Footer.
 978       */
 979      private function writeHeaderFooter(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
 980      {
 981          // headerFooter
 982          $objWriter->startElement('headerFooter');
 983          $objWriter->writeAttribute('differentOddEven', ($worksheet->getHeaderFooter()->getDifferentOddEven() ? 'true' : 'false'));
 984          $objWriter->writeAttribute('differentFirst', ($worksheet->getHeaderFooter()->getDifferentFirst() ? 'true' : 'false'));
 985          $objWriter->writeAttribute('scaleWithDoc', ($worksheet->getHeaderFooter()->getScaleWithDocument() ? 'true' : 'false'));
 986          $objWriter->writeAttribute('alignWithMargins', ($worksheet->getHeaderFooter()->getAlignWithMargins() ? 'true' : 'false'));
 987  
 988          $objWriter->writeElement('oddHeader', $worksheet->getHeaderFooter()->getOddHeader());
 989          $objWriter->writeElement('oddFooter', $worksheet->getHeaderFooter()->getOddFooter());
 990          $objWriter->writeElement('evenHeader', $worksheet->getHeaderFooter()->getEvenHeader());
 991          $objWriter->writeElement('evenFooter', $worksheet->getHeaderFooter()->getEvenFooter());
 992          $objWriter->writeElement('firstHeader', $worksheet->getHeaderFooter()->getFirstHeader());
 993          $objWriter->writeElement('firstFooter', $worksheet->getHeaderFooter()->getFirstFooter());
 994          $objWriter->endElement();
 995      }
 996  
 997      /**
 998       * Write Breaks.
 999       */
1000      private function writeBreaks(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1001      {
1002          // Get row and column breaks
1003          $aRowBreaks = [];
1004          $aColumnBreaks = [];
1005          foreach ($worksheet->getBreaks() as $cell => $breakType) {
1006              if ($breakType == PhpspreadsheetWorksheet::BREAK_ROW) {
1007                  $aRowBreaks[] = $cell;
1008              } elseif ($breakType == PhpspreadsheetWorksheet::BREAK_COLUMN) {
1009                  $aColumnBreaks[] = $cell;
1010              }
1011          }
1012  
1013          // rowBreaks
1014          if (!empty($aRowBreaks)) {
1015              $objWriter->startElement('rowBreaks');
1016              $objWriter->writeAttribute('count', count($aRowBreaks));
1017              $objWriter->writeAttribute('manualBreakCount', count($aRowBreaks));
1018  
1019              foreach ($aRowBreaks as $cell) {
1020                  $coords = Coordinate::coordinateFromString($cell);
1021  
1022                  $objWriter->startElement('brk');
1023                  $objWriter->writeAttribute('id', $coords[1]);
1024                  $objWriter->writeAttribute('man', '1');
1025                  $objWriter->endElement();
1026              }
1027  
1028              $objWriter->endElement();
1029          }
1030  
1031          // Second, write column breaks
1032          if (!empty($aColumnBreaks)) {
1033              $objWriter->startElement('colBreaks');
1034              $objWriter->writeAttribute('count', count($aColumnBreaks));
1035              $objWriter->writeAttribute('manualBreakCount', count($aColumnBreaks));
1036  
1037              foreach ($aColumnBreaks as $cell) {
1038                  $coords = Coordinate::coordinateFromString($cell);
1039  
1040                  $objWriter->startElement('brk');
1041                  $objWriter->writeAttribute('id', Coordinate::columnIndexFromString($coords[0]) - 1);
1042                  $objWriter->writeAttribute('man', '1');
1043                  $objWriter->endElement();
1044              }
1045  
1046              $objWriter->endElement();
1047          }
1048      }
1049  
1050      /**
1051       * Write SheetData.
1052       *
1053       * @param string[] $stringTable String table
1054       */
1055      private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet, array $stringTable): void
1056      {
1057          // Flipped stringtable, for faster index searching
1058          $aFlippedStringTable = $this->getParentWriter()->getWriterPartstringtable()->flipStringTable($stringTable);
1059  
1060          // sheetData
1061          $objWriter->startElement('sheetData');
1062  
1063          // Get column count
1064          $colCount = Coordinate::columnIndexFromString($worksheet->getHighestColumn());
1065  
1066          // Highest row number
1067          $highestRow = $worksheet->getHighestRow();
1068  
1069          // Loop through cells
1070          $cellsByRow = [];
1071          foreach ($worksheet->getCoordinates() as $coordinate) {
1072              $cellAddress = Coordinate::coordinateFromString($coordinate);
1073              $cellsByRow[$cellAddress[1]][] = $coordinate;
1074          }
1075  
1076          $currentRow = 0;
1077          while ($currentRow++ < $highestRow) {
1078              // Get row dimension
1079              $rowDimension = $worksheet->getRowDimension($currentRow);
1080  
1081              // Write current row?
1082              $writeCurrentRow = isset($cellsByRow[$currentRow]) || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() == false || $rowDimension->getCollapsed() == true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null;
1083  
1084              if ($writeCurrentRow) {
1085                  // Start a new row
1086                  $objWriter->startElement('row');
1087                  $objWriter->writeAttribute('r', $currentRow);
1088                  $objWriter->writeAttribute('spans', '1:' . $colCount);
1089  
1090                  // Row dimensions
1091                  if ($rowDimension->getRowHeight() >= 0) {
1092                      $objWriter->writeAttribute('customHeight', '1');
1093                      $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight()));
1094                  }
1095  
1096                  // Row visibility
1097                  if (!$rowDimension->getVisible() === true) {
1098                      $objWriter->writeAttribute('hidden', 'true');
1099                  }
1100  
1101                  // Collapsed
1102                  if ($rowDimension->getCollapsed() === true) {
1103                      $objWriter->writeAttribute('collapsed', 'true');
1104                  }
1105  
1106                  // Outline level
1107                  if ($rowDimension->getOutlineLevel() > 0) {
1108                      $objWriter->writeAttribute('outlineLevel', $rowDimension->getOutlineLevel());
1109                  }
1110  
1111                  // Style
1112                  if ($rowDimension->getXfIndex() !== null) {
1113                      $objWriter->writeAttribute('s', $rowDimension->getXfIndex());
1114                      $objWriter->writeAttribute('customFormat', '1');
1115                  }
1116  
1117                  // Write cells
1118                  if (isset($cellsByRow[$currentRow])) {
1119                      foreach ($cellsByRow[$currentRow] as $cellAddress) {
1120                          // Write cell
1121                          $this->writeCell($objWriter, $worksheet, $cellAddress, $aFlippedStringTable);
1122                      }
1123                  }
1124  
1125                  // End row
1126                  $objWriter->endElement();
1127              }
1128          }
1129  
1130          $objWriter->endElement();
1131      }
1132  
1133      /**
1134       * @param RichText|string $cellValue
1135       */
1136      private function writeCellInlineStr(XMLWriter $objWriter, string $mappedType, $cellValue): void
1137      {
1138          $objWriter->writeAttribute('t', $mappedType);
1139          if (!$cellValue instanceof RichText) {
1140              $objWriter->writeElement(
1141                  't',
1142                  StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue, Settings::htmlEntityFlags()))
1143              );
1144          } elseif ($cellValue instanceof RichText) {
1145              $objWriter->startElement('is');
1146              $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $cellValue);
1147              $objWriter->endElement();
1148          }
1149      }
1150  
1151      /**
1152       * @param RichText|string $cellValue
1153       * @param string[] $flippedStringTable
1154       */
1155      private function writeCellString(XMLWriter $objWriter, string $mappedType, $cellValue, array $flippedStringTable): void
1156      {
1157          $objWriter->writeAttribute('t', $mappedType);
1158          if (!$cellValue instanceof RichText) {
1159              self::writeElementIf($objWriter, isset($flippedStringTable[$cellValue]), 'v', $flippedStringTable[$cellValue] ?? '');
1160          } else {
1161              $objWriter->writeElement('v', $flippedStringTable[$cellValue->getHashCode()]);
1162          }
1163      }
1164  
1165      /**
1166       * @param float|int $cellValue
1167       */
1168      private function writeCellNumeric(XMLWriter $objWriter, $cellValue): void
1169      {
1170          //force a decimal to be written if the type is float
1171          if (is_float($cellValue)) {
1172              // force point as decimal separator in case current locale uses comma
1173              $cellValue = str_replace(',', '.', (string) $cellValue);
1174              if (strpos($cellValue, '.') === false) {
1175                  $cellValue = $cellValue . '.0';
1176              }
1177          }
1178          $objWriter->writeElement('v', $cellValue);
1179      }
1180  
1181      private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void
1182      {
1183          $objWriter->writeAttribute('t', $mappedType);
1184          $objWriter->writeElement('v', $cellValue ? '1' : '0');
1185      }
1186  
1187      private function writeCellError(XMLWriter $objWriter, string $mappedType, string $cellValue, string $formulaerr = '#NULL!'): void
1188      {
1189          $objWriter->writeAttribute('t', $mappedType);
1190          $cellIsFormula = substr($cellValue, 0, 1) === '=';
1191          self::writeElementIf($objWriter, $cellIsFormula, 'f', Xlfn::addXlfnStripEquals($cellValue));
1192          $objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue);
1193      }
1194  
1195      private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell $cell): void
1196      {
1197          $calculatedValue = $this->getParentWriter()->getPreCalculateFormulas() ? $cell->getCalculatedValue() : $cellValue;
1198          if (is_string($calculatedValue)) {
1199              if (\PhpOffice\PhpSpreadsheet\Calculation\Functions::isError($calculatedValue)) {
1200                  $this->writeCellError($objWriter, 'e', $cellValue, $calculatedValue);
1201  
1202                  return;
1203              }
1204              $objWriter->writeAttribute('t', 'str');
1205              $calculatedValue = StringHelper::controlCharacterPHP2OOXML($calculatedValue);
1206          } elseif (is_bool($calculatedValue)) {
1207              $objWriter->writeAttribute('t', 'b');
1208              $calculatedValue = (int) $calculatedValue;
1209          }
1210  
1211          $attributes = $cell->getFormulaAttributes();
1212          if (($attributes['t'] ?? null) === 'array') {
1213              $objWriter->startElement('f');
1214              $objWriter->writeAttribute('t', 'array');
1215              $objWriter->writeAttribute('ref', $cell->getCoordinate());
1216              $objWriter->writeAttribute('aca', '1');
1217              $objWriter->writeAttribute('ca', '1');
1218              $objWriter->text(substr($cellValue, 1));
1219              $objWriter->endElement();
1220          } else {
1221              $objWriter->writeElement('f', Xlfn::addXlfnStripEquals($cellValue));
1222              self::writeElementIf(
1223                  $objWriter,
1224                  $this->getParentWriter()->getOffice2003Compatibility() === false,
1225                  'v',
1226                  ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue, 0, 1) !== '#')
1227                      ? StringHelper::formatNumber($calculatedValue) : '0'
1228              );
1229          }
1230      }
1231  
1232      /**
1233       * Write Cell.
1234       *
1235       * @param string $cellAddress Cell Address
1236       * @param string[] $flippedStringTable String table (flipped), for faster index searching
1237       */
1238      private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet, string $cellAddress, array $flippedStringTable): void
1239      {
1240          // Cell
1241          $pCell = $worksheet->getCell($cellAddress);
1242          $objWriter->startElement('c');
1243          $objWriter->writeAttribute('r', $cellAddress);
1244  
1245          // Sheet styles
1246          $xfi = $pCell->getXfIndex();
1247          self::writeAttributeIf($objWriter, $xfi, 's', $xfi);
1248  
1249          // If cell value is supplied, write cell value
1250          $cellValue = $pCell->getValue();
1251          if (is_object($cellValue) || $cellValue !== '') {
1252              // Map type
1253              $mappedType = $pCell->getDataType();
1254  
1255              // Write data depending on its type
1256              switch (strtolower($mappedType)) {
1257                  case 'inlinestr':    // Inline string
1258                      $this->writeCellInlineStr($objWriter, $mappedType, $cellValue);
1259  
1260                      break;
1261                  case 's':            // String
1262                      $this->writeCellString($objWriter, $mappedType, $cellValue, $flippedStringTable);
1263  
1264                      break;
1265                  case 'f':            // Formula
1266                      $this->writeCellFormula($objWriter, $cellValue, $pCell);
1267  
1268                      break;
1269                  case 'n':            // Numeric
1270                      $this->writeCellNumeric($objWriter, $cellValue);
1271  
1272                      break;
1273                  case 'b':            // Boolean
1274                      $this->writeCellBoolean($objWriter, $mappedType, $cellValue);
1275  
1276                      break;
1277                  case 'e':            // Error
1278                      $this->writeCellError($objWriter, $mappedType, $cellValue);
1279              }
1280          }
1281  
1282          $objWriter->endElement();
1283      }
1284  
1285      /**
1286       * Write Drawings.
1287       *
1288       * @param bool $includeCharts Flag indicating if we should include drawing details for charts
1289       */
1290      private function writeDrawings(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet, $includeCharts = false): void
1291      {
1292          $unparsedLoadedData = $worksheet->getParent()->getUnparsedLoadedData();
1293          $hasUnparsedDrawing = isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds']);
1294          $chartCount = ($includeCharts) ? $worksheet->getChartCollection()->count() : 0;
1295          if ($chartCount == 0 && $worksheet->getDrawingCollection()->count() == 0 && !$hasUnparsedDrawing) {
1296              return;
1297          }
1298  
1299          // If sheet contains drawings, add the relationships
1300          $objWriter->startElement('drawing');
1301  
1302          $rId = 'rId1';
1303          if (isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds'])) {
1304              $drawingOriginalIds = $unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds'];
1305              // take first. In future can be overriten
1306              // (! synchronize with \PhpOffice\PhpSpreadsheet\Writer\Xlsx\Rels::writeWorksheetRelationships)
1307              $rId = reset($drawingOriginalIds);
1308          }
1309  
1310          $objWriter->writeAttribute('r:id', $rId);
1311          $objWriter->endElement();
1312      }
1313  
1314      /**
1315       * Write LegacyDrawing.
1316       */
1317      private function writeLegacyDrawing(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1318      {
1319          // If sheet contains comments, add the relationships
1320          if (count($worksheet->getComments()) > 0) {
1321              $objWriter->startElement('legacyDrawing');
1322              $objWriter->writeAttribute('r:id', 'rId_comments_vml1');
1323              $objWriter->endElement();
1324          }
1325      }
1326  
1327      /**
1328       * Write LegacyDrawingHF.
1329       */
1330      private function writeLegacyDrawingHF(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1331      {
1332          // If sheet contains images, add the relationships
1333          if (count($worksheet->getHeaderFooter()->getImages()) > 0) {
1334              $objWriter->startElement('legacyDrawingHF');
1335              $objWriter->writeAttribute('r:id', 'rId_headerfooter_vml1');
1336              $objWriter->endElement();
1337          }
1338      }
1339  
1340      private function writeAlternateContent(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1341      {
1342          if (empty($worksheet->getParent()->getUnparsedLoadedData()['sheets'][$worksheet->getCodeName()]['AlternateContents'])) {
1343              return;
1344          }
1345  
1346          foreach ($worksheet->getParent()->getUnparsedLoadedData()['sheets'][$worksheet->getCodeName()]['AlternateContents'] as $alternateContent) {
1347              $objWriter->writeRaw($alternateContent);
1348          }
1349      }
1350  
1351      /**
1352       * write <ExtLst>
1353       * only implementation conditionalFormattings.
1354       *
1355       * @url https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/07d607af-5618-4ca2-b683-6a78dc0d9627
1356       */
1357      private function writeExtLst(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1358      {
1359          $conditionalFormattingRuleExtList = [];
1360          foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
1361              /** @var Conditional $conditional */
1362              foreach ($conditionalStyles as $conditional) {
1363                  $dataBar = $conditional->getDataBar();
1364                  // @phpstan-ignore-next-line
1365                  if ($dataBar && $dataBar->getConditionalFormattingRuleExt()) {
1366                      $conditionalFormattingRuleExtList[] = $dataBar->getConditionalFormattingRuleExt();
1367                  }
1368              }
1369          }
1370  
1371          if (count($conditionalFormattingRuleExtList) > 0) {
1372              $conditionalFormattingRuleExtNsPrefix = 'x14';
1373              $objWriter->startElement('extLst');
1374              $objWriter->startElement('ext');
1375              $objWriter->writeAttribute('uri', '{78C0D931-6437-407d-A8EE-F0AAD7539E65}');
1376              $objWriter->startElementNs($conditionalFormattingRuleExtNsPrefix, 'conditionalFormattings', null);
1377              foreach ($conditionalFormattingRuleExtList as $extension) {
1378                  self::writeExtConditionalFormattingElements($objWriter, $extension);
1379              }
1380              $objWriter->endElement(); //end conditionalFormattings
1381              $objWriter->endElement(); //end ext
1382              $objWriter->endElement(); //end extLst
1383          }
1384      }
1385  }