Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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