Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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\Style\Fill;
  11  use PhpOffice\PhpSpreadsheet\Style\Font;
  12  use PhpOffice\PhpSpreadsheet\Worksheet\Row;
  13  use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
  14  use PhpOffice\PhpSpreadsheet\Writer\Exception;
  15  use PhpOffice\PhpSpreadsheet\Writer\Ods;
  16  use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Comment;
  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      const CELL_STYLE_PREFIX = 'ce';
  26  
  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()
  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          // 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 ($i = 0; $i < $sheetCount; ++$i) {
 124              $objWriter->startElement('table:table');
 125              $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($i)->getTitle());
 126              $objWriter->writeElement('office:forms');
 127              $objWriter->startElement('table:table-column');
 128              $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
 129              $objWriter->endElement();
 130              $this->writeRows($objWriter, $spreadsheet->getSheet($i));
 131              $objWriter->endElement();
 132          }
 133      }
 134  
 135      /**
 136       * Write rows of the specified sheet.
 137       */
 138      private function writeRows(XMLWriter $objWriter, Worksheet $sheet): void
 139      {
 140          $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX;
 141          $span_row = 0;
 142          $rows = $sheet->getRowIterator();
 143          while ($rows->valid()) {
 144              --$numberRowsRepeated;
 145              $row = $rows->current();
 146              if ($row->getCellIterator()->valid()) {
 147                  if ($span_row) {
 148                      $objWriter->startElement('table:table-row');
 149                      if ($span_row > 1) {
 150                          $objWriter->writeAttribute('table:number-rows-repeated', $span_row);
 151                      }
 152                      $objWriter->startElement('table:table-cell');
 153                      $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
 154                      $objWriter->endElement();
 155                      $objWriter->endElement();
 156                      $span_row = 0;
 157                  }
 158                  $objWriter->startElement('table:table-row');
 159                  $this->writeCells($objWriter, $row);
 160                  $objWriter->endElement();
 161              } else {
 162                  ++$span_row;
 163              }
 164              $rows->next();
 165          }
 166      }
 167  
 168      /**
 169       * Write cells of the specified row.
 170       */
 171      private function writeCells(XMLWriter $objWriter, Row $row): void
 172      {
 173          $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX;
 174          $prevColumn = -1;
 175          $cells = $row->getCellIterator();
 176          while ($cells->valid()) {
 177              /** @var \PhpOffice\PhpSpreadsheet\Cell\Cell $cell */
 178              $cell = $cells->current();
 179              $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
 180  
 181              $this->writeCellSpan($objWriter, $column, $prevColumn);
 182              $objWriter->startElement('table:table-cell');
 183              $this->writeCellMerge($objWriter, $cell);
 184  
 185              // Style XF
 186              $style = $cell->getXfIndex();
 187              if ($style !== null) {
 188                  $objWriter->writeAttribute('table:style-name', self::CELL_STYLE_PREFIX . $style);
 189              }
 190  
 191              switch ($cell->getDataType()) {
 192                  case DataType::TYPE_BOOL:
 193                      $objWriter->writeAttribute('office:value-type', 'boolean');
 194                      $objWriter->writeAttribute('office:value', $cell->getValue());
 195                      $objWriter->writeElement('text:p', $cell->getValue());
 196  
 197                      break;
 198                  case DataType::TYPE_ERROR:
 199                      throw new Exception('Writing of error not implemented yet.');
 200  
 201                      break;
 202                  case DataType::TYPE_FORMULA:
 203                      $formulaValue = $cell->getValue();
 204                      if ($this->getParentWriter()->getPreCalculateFormulas()) {
 205                          try {
 206                              $formulaValue = $cell->getCalculatedValue();
 207                          } catch (Exception $e) {
 208                              // don't do anything
 209                          }
 210                      }
 211                      $objWriter->writeAttribute('table:formula', $this->formulaConvertor->convertFormula($cell->getValue()));
 212                      if (is_numeric($formulaValue)) {
 213                          $objWriter->writeAttribute('office:value-type', 'float');
 214                      } else {
 215                          $objWriter->writeAttribute('office:value-type', 'string');
 216                      }
 217                      $objWriter->writeAttribute('office:value', $formulaValue);
 218                      $objWriter->writeElement('text:p', $formulaValue);
 219  
 220                      break;
 221                  case DataType::TYPE_INLINE:
 222                      throw new Exception('Writing of inline not implemented yet.');
 223  
 224                      break;
 225                  case DataType::TYPE_NUMERIC:
 226                      $objWriter->writeAttribute('office:value-type', 'float');
 227                      $objWriter->writeAttribute('office:value', $cell->getValue());
 228                      $objWriter->writeElement('text:p', $cell->getValue());
 229  
 230                      break;
 231                  case DataType::TYPE_STRING:
 232                      $objWriter->writeAttribute('office:value-type', 'string');
 233                      $objWriter->writeElement('text:p', $cell->getValue());
 234  
 235                      break;
 236              }
 237              Comment::write($objWriter, $cell);
 238              $objWriter->endElement();
 239              $prevColumn = $column;
 240              $cells->next();
 241          }
 242          $numberColsRepeated = $numberColsRepeated - $prevColumn - 1;
 243          if ($numberColsRepeated > 0) {
 244              if ($numberColsRepeated > 1) {
 245                  $objWriter->startElement('table:table-cell');
 246                  $objWriter->writeAttribute('table:number-columns-repeated', $numberColsRepeated);
 247                  $objWriter->endElement();
 248              } else {
 249                  $objWriter->writeElement('table:table-cell');
 250              }
 251          }
 252      }
 253  
 254      /**
 255       * Write span.
 256       *
 257       * @param int $curColumn
 258       * @param int $prevColumn
 259       */
 260      private function writeCellSpan(XMLWriter $objWriter, $curColumn, $prevColumn): void
 261      {
 262          $diff = $curColumn - $prevColumn - 1;
 263          if (1 === $diff) {
 264              $objWriter->writeElement('table:table-cell');
 265          } elseif ($diff > 1) {
 266              $objWriter->startElement('table:table-cell');
 267              $objWriter->writeAttribute('table:number-columns-repeated', $diff);
 268              $objWriter->endElement();
 269          }
 270      }
 271  
 272      /**
 273       * Write XF cell styles.
 274       */
 275      private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void
 276      {
 277          foreach ($spreadsheet->getCellXfCollection() as $style) {
 278              $writer->startElement('style:style');
 279              $writer->writeAttribute('style:name', self::CELL_STYLE_PREFIX . $style->getIndex());
 280              $writer->writeAttribute('style:family', 'table-cell');
 281              $writer->writeAttribute('style:parent-style-name', 'Default');
 282  
 283              // style:text-properties
 284  
 285              // Font
 286              $writer->startElement('style:text-properties');
 287  
 288              $font = $style->getFont();
 289  
 290              if ($font->getBold()) {
 291                  $writer->writeAttribute('fo:font-weight', 'bold');
 292                  $writer->writeAttribute('style:font-weight-complex', 'bold');
 293                  $writer->writeAttribute('style:font-weight-asian', 'bold');
 294              }
 295  
 296              if ($font->getItalic()) {
 297                  $writer->writeAttribute('fo:font-style', 'italic');
 298              }
 299  
 300              if ($color = $font->getColor()) {
 301                  $writer->writeAttribute('fo:color', sprintf('#%s', $color->getRGB()));
 302              }
 303  
 304              if ($family = $font->getName()) {
 305                  $writer->writeAttribute('fo:font-family', $family);
 306              }
 307  
 308              if ($size = $font->getSize()) {
 309                  $writer->writeAttribute('fo:font-size', sprintf('%.1Fpt', $size));
 310              }
 311  
 312              if ($font->getUnderline() && $font->getUnderline() != Font::UNDERLINE_NONE) {
 313                  $writer->writeAttribute('style:text-underline-style', 'solid');
 314                  $writer->writeAttribute('style:text-underline-width', 'auto');
 315                  $writer->writeAttribute('style:text-underline-color', 'font-color');
 316  
 317                  switch ($font->getUnderline()) {
 318                      case Font::UNDERLINE_DOUBLE:
 319                          $writer->writeAttribute('style:text-underline-type', 'double');
 320  
 321                          break;
 322                      case Font::UNDERLINE_SINGLE:
 323                          $writer->writeAttribute('style:text-underline-type', 'single');
 324  
 325                          break;
 326                  }
 327              }
 328  
 329              $writer->endElement(); // Close style:text-properties
 330  
 331              // style:table-cell-properties
 332  
 333              $writer->startElement('style:table-cell-properties');
 334              $writer->writeAttribute('style:rotation-align', 'none');
 335  
 336              // Fill
 337              if ($fill = $style->getFill()) {
 338                  switch ($fill->getFillType()) {
 339                      case Fill::FILL_SOLID:
 340                          $writer->writeAttribute('fo:background-color', sprintf(
 341                              '#%s',
 342                              strtolower($fill->getStartColor()->getRGB())
 343                          ));
 344  
 345                          break;
 346                      case Fill::FILL_GRADIENT_LINEAR:
 347                      case Fill::FILL_GRADIENT_PATH:
 348                          /// TODO :: To be implemented
 349                          break;
 350                      case Fill::FILL_NONE:
 351                      default:
 352                  }
 353              }
 354  
 355              $writer->endElement(); // Close style:table-cell-properties
 356  
 357              // End
 358  
 359              $writer->endElement(); // Close style:style
 360          }
 361      }
 362  
 363      /**
 364       * Write attributes for merged cell.
 365       */
 366      private function writeCellMerge(XMLWriter $objWriter, Cell $cell): void
 367      {
 368          if (!$cell->isMergeRangeValueCell()) {
 369              return;
 370          }
 371  
 372          $mergeRange = Coordinate::splitRange($cell->getMergeRange());
 373          [$startCell, $endCell] = $mergeRange[0];
 374          $start = Coordinate::coordinateFromString($startCell);
 375          $end = Coordinate::coordinateFromString($endCell);
 376          $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1;
 377          $rowSpan = $end[1] - $start[1] + 1;
 378  
 379          $objWriter->writeAttribute('table:number-columns-spanned', $columnSpan);
 380          $objWriter->writeAttribute('table:number-rows-spanned', $rowSpan);
 381      }
 382  }