Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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\Writer\Ods;
   4  
   5  use PhpOffice\PhpSpreadsheet\Cell\Cell;
   6  use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
   7  use PhpOffice\PhpSpreadsheet\Cell\DataType;
   8  use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
   9  use PhpOffice\PhpSpreadsheet\Spreadsheet;
  10  use PhpOffice\PhpSpreadsheet\Worksheet\Row;
  11  use PhpOffice\PhpSpreadsheet\Worksheet\RowCellIterator;
  12  use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
  13  use PhpOffice\PhpSpreadsheet\Writer\Exception;
  14  use PhpOffice\PhpSpreadsheet\Writer\Ods;
  15  use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Comment;
  16  use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Style;
  17  
  18  /**
  19   * @author     Alexander Pervakov <frost-nzcr4@jagmort.com>
  20   */
  21  class Content extends WriterPart
  22  {
  23      const NUMBER_COLS_REPEATED_MAX = 1024;
  24      const NUMBER_ROWS_REPEATED_MAX = 1048576;
  25  
  26      private $formulaConvertor;
  27  
  28      /**
  29       * Set parent Ods writer.
  30       */
  31      public function __construct(Ods $writer)
  32      {
  33          parent::__construct($writer);
  34  
  35          $this->formulaConvertor = new Formula($this->getParentWriter()->getSpreadsheet()->getDefinedNames());
  36      }
  37  
  38      /**
  39       * Write content.xml to XML format.
  40       *
  41       * @return string XML Output
  42       */
  43      public function write(): string
  44      {
  45          $objWriter = null;
  46          if ($this->getParentWriter()->getUseDiskCaching()) {
  47              $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
  48          } else {
  49              $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
  50          }
  51  
  52          // XML header
  53          $objWriter->startDocument('1.0', 'UTF-8');
  54  
  55          // Content
  56          $objWriter->startElement('office:document-content');
  57          $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
  58          $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0');
  59          $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
  60          $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0');
  61          $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0');
  62          $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
  63          $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
  64          $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
  65          $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0');
  66          $objWriter->writeAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0');
  67          $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0');
  68          $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0');
  69          $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0');
  70          $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0');
  71          $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML');
  72          $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0');
  73          $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0');
  74          $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
  75          $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer');
  76          $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc');
  77          $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events');
  78          $objWriter->writeAttribute('xmlns:xforms', 'http://www.w3.org/2002/xforms');
  79          $objWriter->writeAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema');
  80          $objWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
  81          $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report');
  82          $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2');
  83          $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
  84          $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#');
  85          $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table');
  86          $objWriter->writeAttribute('xmlns:field', 'urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0');
  87          $objWriter->writeAttribute('xmlns:formx', 'urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0');
  88          $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/');
  89          $objWriter->writeAttribute('office:version', '1.2');
  90  
  91          $objWriter->writeElement('office:scripts');
  92          $objWriter->writeElement('office:font-face-decls');
  93  
  94          // Styles XF
  95          $objWriter->startElement('office:automatic-styles');
  96          $this->writeXfStyles($objWriter, $this->getParentWriter()->getSpreadsheet());
  97          $objWriter->endElement();
  98  
  99          $objWriter->startElement('office:body');
 100          $objWriter->startElement('office:spreadsheet');
 101          $objWriter->writeElement('table:calculation-settings');
 102  
 103          $this->writeSheets($objWriter);
 104  
 105          (new AutoFilters($objWriter, $this->getParentWriter()->getSpreadsheet()))->write();
 106          // Defined names (ranges and formulae)
 107          (new NamedExpressions($objWriter, $this->getParentWriter()->getSpreadsheet(), $this->formulaConvertor))->write();
 108  
 109          $objWriter->endElement();
 110          $objWriter->endElement();
 111          $objWriter->endElement();
 112  
 113          return $objWriter->getData();
 114      }
 115  
 116      /**
 117       * Write sheets.
 118       */
 119      private function writeSheets(XMLWriter $objWriter): void
 120      {
 121          $spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */
 122          $sheetCount = $spreadsheet->getSheetCount();
 123          for ($sheetIndex = 0; $sheetIndex < $sheetCount; ++$sheetIndex) {
 124              $objWriter->startElement('table:table');
 125              $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle());
 126              $objWriter->writeAttribute('table:style-name', Style::TABLE_STYLE_PREFIX . (string) ($sheetIndex + 1));
 127              $objWriter->writeElement('office:forms');
 128              foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) {
 129                  $objWriter->startElement('table:table-column');
 130                  $objWriter->writeAttribute(
 131                      'table:style-name',
 132                      sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric())
 133                  );
 134                  $objWriter->writeAttribute('table:default-cell-style-name', 'ce0');
 135  //                $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
 136                  $objWriter->endElement();
 137              }
 138              $this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex);
 139              $objWriter->endElement();
 140          }
 141      }
 142  
 143      /**
 144       * Write rows of the specified sheet.
 145       */
 146      private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void
 147      {
 148          $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX;
 149          $span_row = 0;
 150          $rows = $sheet->getRowIterator();
 151          foreach ($rows as $row) {
 152              $cellIterator = $row->getCellIterator();
 153              --$numberRowsRepeated;
 154              if ($cellIterator->valid()) {
 155                  $objWriter->startElement('table:table-row');
 156                  if ($span_row) {
 157                      if ($span_row > 1) {
 158                          $objWriter->writeAttribute('table:number-rows-repeated', $span_row);
 159                      }
 160                      $objWriter->startElement('table:table-cell');
 161                      $objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX);
 162                      $objWriter->endElement();
 163                      $span_row = 0;
 164                  } else {
 165                      if ($sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) {
 166                          $objWriter->writeAttribute(
 167                              'table:style-name',
 168                              sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex())
 169                          );
 170                      }
 171                      $this->writeCells($objWriter, $cellIterator);
 172                  }
 173                  $objWriter->endElement();
 174              } else {
 175                  ++$span_row;
 176              }
 177          }
 178      }
 179  
 180      /**
 181       * Write cells of the specified row.
 182       */
 183      private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void
 184      {
 185          $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX;
 186          $prevColumn = -1;
 187          foreach ($cells as $cell) {
 188              /** @var \PhpOffice\PhpSpreadsheet\Cell\Cell $cell */
 189              $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
 190  
 191              $this->writeCellSpan($objWriter, $column, $prevColumn);
 192              $objWriter->startElement('table:table-cell');
 193              $this->writeCellMerge($objWriter, $cell);
 194  
 195              // Style XF
 196              $style = $cell->getXfIndex();
 197              if ($style !== null) {
 198                  $objWriter->writeAttribute('table:style-name', Style::CELL_STYLE_PREFIX . $style);
 199              }
 200  
 201              switch ($cell->getDataType()) {
 202                  case DataType::TYPE_BOOL:
 203                      $objWriter->writeAttribute('office:value-type', 'boolean');
 204                      $objWriter->writeAttribute('office:value', $cell->getValue());
 205                      $objWriter->writeElement('text:p', $cell->getValue());
 206  
 207                      break;
 208                  case DataType::TYPE_ERROR:
 209                      $objWriter->writeAttribute('table:formula', 'of:=#NULL!');
 210                      $objWriter->writeAttribute('office:value-type', 'string');
 211                      $objWriter->writeAttribute('office:string-value', '');
 212                      $objWriter->writeElement('text:p', '#NULL!');
 213  
 214                      break;
 215                  case DataType::TYPE_FORMULA:
 216                      $formulaValue = $cell->getValue();
 217                      if ($this->getParentWriter()->getPreCalculateFormulas()) {
 218                          try {
 219                              $formulaValue = $cell->getCalculatedValue();
 220                          } catch (Exception $e) {
 221                              // don't do anything
 222                          }
 223                      }
 224                      $objWriter->writeAttribute('table:formula', $this->formulaConvertor->convertFormula($cell->getValue()));
 225                      if (is_numeric($formulaValue)) {
 226                          $objWriter->writeAttribute('office:value-type', 'float');
 227                      } else {
 228                          $objWriter->writeAttribute('office:value-type', 'string');
 229                      }
 230                      $objWriter->writeAttribute('office:value', $formulaValue);
 231                      $objWriter->writeElement('text:p', $formulaValue);
 232  
 233                      break;
 234                  case DataType::TYPE_NUMERIC:
 235                      $objWriter->writeAttribute('office:value-type', 'float');
 236                      $objWriter->writeAttribute('office:value', $cell->getValue());
 237                      $objWriter->writeElement('text:p', $cell->getValue());
 238  
 239                      break;
 240                  case DataType::TYPE_INLINE:
 241                      // break intentionally omitted
 242                  case DataType::TYPE_STRING:
 243                      $objWriter->writeAttribute('office:value-type', 'string');
 244                      $objWriter->writeElement('text:p', $cell->getValue());
 245  
 246                      break;
 247              }
 248              Comment::write($objWriter, $cell);
 249              $objWriter->endElement();
 250              $prevColumn = $column;
 251          }
 252  
 253          $numberColsRepeated = $numberColsRepeated - $prevColumn - 1;
 254          if ($numberColsRepeated > 0) {
 255              if ($numberColsRepeated > 1) {
 256                  $objWriter->startElement('table:table-cell');
 257                  $objWriter->writeAttribute('table:number-columns-repeated', $numberColsRepeated);
 258                  $objWriter->endElement();
 259              } else {
 260                  $objWriter->writeElement('table:table-cell');
 261              }
 262          }
 263      }
 264  
 265      /**
 266       * Write span.
 267       *
 268       * @param int $curColumn
 269       * @param int $prevColumn
 270       */
 271      private function writeCellSpan(XMLWriter $objWriter, $curColumn, $prevColumn): void
 272      {
 273          $diff = $curColumn - $prevColumn - 1;
 274          if (1 === $diff) {
 275              $objWriter->writeElement('table:table-cell');
 276          } elseif ($diff > 1) {
 277              $objWriter->startElement('table:table-cell');
 278              $objWriter->writeAttribute('table:number-columns-repeated', $diff);
 279              $objWriter->endElement();
 280          }
 281      }
 282  
 283      /**
 284       * Write XF cell styles.
 285       */
 286      private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void
 287      {
 288          $styleWriter = new Style($writer);
 289  
 290          $sheetCount = $spreadsheet->getSheetCount();
 291          for ($i = 0; $i < $sheetCount; ++$i) {
 292              $worksheet = $spreadsheet->getSheet($i);
 293              $styleWriter->writeTableStyle($worksheet, $i + 1);
 294  
 295              $worksheet->calculateColumnWidths();
 296              foreach ($worksheet->getColumnDimensions() as $columnDimension) {
 297                  if ($columnDimension->getWidth() !== -1.0) {
 298                      $styleWriter->writeColumnStyles($columnDimension, $i);
 299                  }
 300              }
 301          }
 302          for ($i = 0; $i < $sheetCount; ++$i) {
 303              $worksheet = $spreadsheet->getSheet($i);
 304              foreach ($worksheet->getRowDimensions() as $rowDimension) {
 305                  if ($rowDimension->getRowHeight() > 0.0) {
 306                      $styleWriter->writeRowStyles($rowDimension, $i);
 307                  }
 308              }
 309          }
 310  
 311          foreach ($spreadsheet->getCellXfCollection() as $style) {
 312              $styleWriter->write($style);
 313          }
 314      }
 315  
 316      /**
 317       * Write attributes for merged cell.
 318       */
 319      private function writeCellMerge(XMLWriter $objWriter, Cell $cell): void
 320      {
 321          if (!$cell->isMergeRangeValueCell()) {
 322              return;
 323          }
 324  
 325          $mergeRange = Coordinate::splitRange($cell->getMergeRange());
 326          [$startCell, $endCell] = $mergeRange[0];
 327          $start = Coordinate::coordinateFromString($startCell);
 328          $end = Coordinate::coordinateFromString($endCell);
 329          $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1;
 330          $rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1;
 331  
 332          $objWriter->writeAttribute('table:number-columns-spanned', (string) $columnSpan);
 333          $objWriter->writeAttribute('table:number-rows-spanned', (string) $rowSpan);
 334      }
 335  }