Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [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              foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) {
 130                  $objWriter->startElement('table:table-column');
 131                  $objWriter->writeAttribute(
 132                      'table:style-name',
 133                      sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric())
 134                  );
 135                  $objWriter->writeAttribute('table:default-cell-style-name', 'ce0');
 136  //                $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
 137                  $objWriter->endElement();
 138              }
 139              $this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex);
 140              $objWriter->endElement();
 141          }
 142      }
 143  
 144      /**
 145       * Write rows of the specified sheet.
 146       */
 147      private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void
 148      {
 149          $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX;
 150          $span_row = 0;
 151          $rows = $sheet->getRowIterator();
 152          foreach ($rows as $row) {
 153              $cellIterator = $row->getCellIterator();
 154              --$numberRowsRepeated;
 155              if ($cellIterator->valid()) {
 156                  $objWriter->startElement('table:table-row');
 157                  if ($span_row) {
 158                      if ($span_row > 1) {
 159                          $objWriter->writeAttribute('table:number-rows-repeated', (string) $span_row);
 160                      }
 161                      $objWriter->startElement('table:table-cell');
 162                      $objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX);
 163                      $objWriter->endElement();
 164                      $span_row = 0;
 165                  } else {
 166                      if ($sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) {
 167                          $objWriter->writeAttribute(
 168                              'table:style-name',
 169                              sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex())
 170                          );
 171                      }
 172                      $this->writeCells($objWriter, $cellIterator);
 173                  }
 174                  $objWriter->endElement();
 175              } else {
 176                  ++$span_row;
 177              }
 178          }
 179      }
 180  
 181      /**
 182       * Write cells of the specified row.
 183       */
 184      private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void
 185      {
 186          $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX;
 187          $prevColumn = -1;
 188          foreach ($cells as $cell) {
 189              /** @var \PhpOffice\PhpSpreadsheet\Cell\Cell $cell */
 190              $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
 191  
 192              $this->writeCellSpan($objWriter, $column, $prevColumn);
 193              $objWriter->startElement('table:table-cell');
 194              $this->writeCellMerge($objWriter, $cell);
 195  
 196              // Style XF
 197              $style = $cell->getXfIndex();
 198              if ($style !== null) {
 199                  $objWriter->writeAttribute('table:style-name', Style::CELL_STYLE_PREFIX . $style);
 200              }
 201  
 202              switch ($cell->getDataType()) {
 203                  case DataType::TYPE_BOOL:
 204                      $objWriter->writeAttribute('office:value-type', 'boolean');
 205                      $objWriter->writeAttribute('office:value', $cell->getValue());
 206                      $objWriter->writeElement('text:p', $cell->getValue());
 207  
 208                      break;
 209                  case DataType::TYPE_ERROR:
 210                      $objWriter->writeAttribute('table:formula', 'of:=#NULL!');
 211                      $objWriter->writeAttribute('office:value-type', 'string');
 212                      $objWriter->writeAttribute('office:string-value', '');
 213                      $objWriter->writeElement('text:p', '#NULL!');
 214  
 215                      break;
 216                  case DataType::TYPE_FORMULA:
 217                      $formulaValue = $cell->getValue();
 218                      if ($this->getParentWriter()->getPreCalculateFormulas()) {
 219                          try {
 220                              $formulaValue = $cell->getCalculatedValue();
 221                          } catch (Exception $e) {
 222                              // don't do anything
 223                          }
 224                      }
 225                      $objWriter->writeAttribute('table:formula', $this->formulaConvertor->convertFormula($cell->getValue()));
 226                      if (is_numeric($formulaValue)) {
 227                          $objWriter->writeAttribute('office:value-type', 'float');
 228                      } else {
 229                          $objWriter->writeAttribute('office:value-type', 'string');
 230                      }
 231                      $objWriter->writeAttribute('office:value', $formulaValue);
 232                      $objWriter->writeElement('text:p', $formulaValue);
 233  
 234                      break;
 235                  case DataType::TYPE_NUMERIC:
 236                      $objWriter->writeAttribute('office:value-type', 'float');
 237                      $objWriter->writeAttribute('office:value', $cell->getValue());
 238                      $objWriter->writeElement('text:p', $cell->getValue());
 239  
 240                      break;
 241                  case DataType::TYPE_INLINE:
 242                      // break intentionally omitted
 243                  case DataType::TYPE_STRING:
 244                      $objWriter->writeAttribute('office:value-type', 'string');
 245                      $objWriter->writeElement('text:p', $cell->getValue());
 246  
 247                      break;
 248              }
 249              Comment::write($objWriter, $cell);
 250              $objWriter->endElement();
 251              $prevColumn = $column;
 252          }
 253  
 254          $numberColsRepeated = $numberColsRepeated - $prevColumn - 1;
 255          if ($numberColsRepeated > 0) {
 256              if ($numberColsRepeated > 1) {
 257                  $objWriter->startElement('table:table-cell');
 258                  $objWriter->writeAttribute('table:number-columns-repeated', (string) $numberColsRepeated);
 259                  $objWriter->endElement();
 260              } else {
 261                  $objWriter->writeElement('table:table-cell');
 262              }
 263          }
 264      }
 265  
 266      /**
 267       * Write span.
 268       *
 269       * @param int $curColumn
 270       * @param int $prevColumn
 271       */
 272      private function writeCellSpan(XMLWriter $objWriter, $curColumn, $prevColumn): void
 273      {
 274          $diff = $curColumn - $prevColumn - 1;
 275          if (1 === $diff) {
 276              $objWriter->writeElement('table:table-cell');
 277          } elseif ($diff > 1) {
 278              $objWriter->startElement('table:table-cell');
 279              $objWriter->writeAttribute('table:number-columns-repeated', (string) $diff);
 280              $objWriter->endElement();
 281          }
 282      }
 283  
 284      /**
 285       * Write XF cell styles.
 286       */
 287      private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void
 288      {
 289          $styleWriter = new Style($writer);
 290  
 291          $sheetCount = $spreadsheet->getSheetCount();
 292          for ($i = 0; $i < $sheetCount; ++$i) {
 293              $worksheet = $spreadsheet->getSheet($i);
 294              $styleWriter->writeTableStyle($worksheet, $i + 1);
 295  
 296              $worksheet->calculateColumnWidths();
 297              foreach ($worksheet->getColumnDimensions() as $columnDimension) {
 298                  if ($columnDimension->getWidth() !== -1.0) {
 299                      $styleWriter->writeColumnStyles($columnDimension, $i);
 300                  }
 301              }
 302          }
 303          for ($i = 0; $i < $sheetCount; ++$i) {
 304              $worksheet = $spreadsheet->getSheet($i);
 305              foreach ($worksheet->getRowDimensions() as $rowDimension) {
 306                  if ($rowDimension->getRowHeight() > 0.0) {
 307                      $styleWriter->writeRowStyles($rowDimension, $i);
 308                  }
 309              }
 310          }
 311  
 312          foreach ($spreadsheet->getCellXfCollection() as $style) {
 313              $styleWriter->write($style);
 314          }
 315      }
 316  
 317      /**
 318       * Write attributes for merged cell.
 319       */
 320      private function writeCellMerge(XMLWriter $objWriter, Cell $cell): void
 321      {
 322          if (!$cell->isMergeRangeValueCell()) {
 323              return;
 324          }
 325  
 326          $mergeRange = Coordinate::splitRange((string) $cell->getMergeRange());
 327          [$startCell, $endCell] = $mergeRange[0];
 328          $start = Coordinate::coordinateFromString($startCell);
 329          $end = Coordinate::coordinateFromString($endCell);
 330          $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1;
 331          $rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1;
 332  
 333          $objWriter->writeAttribute('table:number-columns-spanned', (string) $columnSpan);
 334          $objWriter->writeAttribute('table:number-rows-spanned', (string) $rowSpan);
 335      }
 336  }