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;
   4  
   5  use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
   6  use PhpOffice\PhpSpreadsheet\Calculation\Functions;
   7  use PhpOffice\PhpSpreadsheet\HashTable;
   8  use PhpOffice\PhpSpreadsheet\Shared\File;
   9  use PhpOffice\PhpSpreadsheet\Spreadsheet;
  10  use PhpOffice\PhpSpreadsheet\Worksheet\Drawing as WorksheetDrawing;
  11  use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
  12  use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
  13  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Chart;
  14  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Comments;
  15  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\ContentTypes;
  16  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\DocProps;
  17  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Drawing;
  18  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Rels;
  19  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsRibbon;
  20  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsVBA;
  21  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\StringTable;
  22  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Style;
  23  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Theme;
  24  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Workbook;
  25  use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet;
  26  use ZipArchive;
  27  use ZipStream\Exception\OverflowException;
  28  use ZipStream\Option\Archive;
  29  use ZipStream\ZipStream;
  30  
  31  class Xlsx extends BaseWriter
  32  {
  33      /**
  34       * Office2003 compatibility.
  35       *
  36       * @var bool
  37       */
  38      private $office2003compatibility = false;
  39  
  40      /**
  41       * Private writer parts.
  42       *
  43       * @var Xlsx\WriterPart[]
  44       */
  45      private $writerParts = [];
  46  
  47      /**
  48       * Private Spreadsheet.
  49       *
  50       * @var Spreadsheet
  51       */
  52      private $spreadSheet;
  53  
  54      /**
  55       * Private string table.
  56       *
  57       * @var string[]
  58       */
  59      private $stringTable = [];
  60  
  61      /**
  62       * Private unique Conditional HashTable.
  63       *
  64       * @var HashTable
  65       */
  66      private $stylesConditionalHashTable;
  67  
  68      /**
  69       * Private unique Style HashTable.
  70       *
  71       * @var HashTable
  72       */
  73      private $styleHashTable;
  74  
  75      /**
  76       * Private unique Fill HashTable.
  77       *
  78       * @var HashTable
  79       */
  80      private $fillHashTable;
  81  
  82      /**
  83       * Private unique \PhpOffice\PhpSpreadsheet\Style\Font HashTable.
  84       *
  85       * @var HashTable
  86       */
  87      private $fontHashTable;
  88  
  89      /**
  90       * Private unique Borders HashTable.
  91       *
  92       * @var HashTable
  93       */
  94      private $bordersHashTable;
  95  
  96      /**
  97       * Private unique NumberFormat HashTable.
  98       *
  99       * @var HashTable
 100       */
 101      private $numFmtHashTable;
 102  
 103      /**
 104       * Private unique \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\BaseDrawing HashTable.
 105       *
 106       * @var HashTable
 107       */
 108      private $drawingHashTable;
 109  
 110      /**
 111       * Private handle for zip stream.
 112       *
 113       * @var ZipStream
 114       */
 115      private $zip;
 116  
 117      /**
 118       * Create a new Xlsx Writer.
 119       */
 120      public function __construct(Spreadsheet $spreadsheet)
 121      {
 122          // Assign PhpSpreadsheet
 123          $this->setSpreadsheet($spreadsheet);
 124  
 125          $writerPartsArray = [
 126              'stringtable' => StringTable::class,
 127              'contenttypes' => ContentTypes::class,
 128              'docprops' => DocProps::class,
 129              'rels' => Rels::class,
 130              'theme' => Theme::class,
 131              'style' => Style::class,
 132              'workbook' => Workbook::class,
 133              'worksheet' => Worksheet::class,
 134              'drawing' => Drawing::class,
 135              'comments' => Comments::class,
 136              'chart' => Chart::class,
 137              'relsvba' => RelsVBA::class,
 138              'relsribbonobjects' => RelsRibbon::class,
 139          ];
 140  
 141          //    Initialise writer parts
 142          //        and Assign their parent IWriters
 143          foreach ($writerPartsArray as $writer => $class) {
 144              $this->writerParts[$writer] = new $class($this);
 145          }
 146  
 147          $hashTablesArray = ['stylesConditionalHashTable', 'fillHashTable', 'fontHashTable',
 148              'bordersHashTable', 'numFmtHashTable', 'drawingHashTable',
 149              'styleHashTable',
 150          ];
 151  
 152          // Set HashTable variables
 153          foreach ($hashTablesArray as $tableName) {
 154              $this->$tableName = new HashTable();
 155          }
 156      }
 157  
 158      /**
 159       * Get writer part.
 160       *
 161       * @param string $pPartName Writer part name
 162       *
 163       * @return \PhpOffice\PhpSpreadsheet\Writer\Xlsx\WriterPart
 164       */
 165      public function getWriterPart($pPartName)
 166      {
 167          if ($pPartName != '' && isset($this->writerParts[strtolower($pPartName)])) {
 168              return $this->writerParts[strtolower($pPartName)];
 169          }
 170  
 171          return null;
 172      }
 173  
 174      /**
 175       * Save PhpSpreadsheet to file.
 176       *
 177       * @param resource|string $pFilename
 178       */
 179      public function save($pFilename): void
 180      {
 181          // garbage collect
 182          $this->pathNames = [];
 183          $this->spreadSheet->garbageCollect();
 184  
 185          $this->openFileHandle($pFilename);
 186  
 187          $saveDebugLog = Calculation::getInstance($this->spreadSheet)->getDebugLog()->getWriteDebugLog();
 188          Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog(false);
 189          $saveDateReturnType = Functions::getReturnDateType();
 190          Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
 191  
 192          // Create string lookup table
 193          $this->stringTable = [];
 194          for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
 195              $this->stringTable = $this->getWriterPart('StringTable')->createStringTable($this->spreadSheet->getSheet($i), $this->stringTable);
 196          }
 197  
 198          // Create styles dictionaries
 199          $this->styleHashTable->addFromSource($this->getWriterPart('Style')->allStyles($this->spreadSheet));
 200          $this->stylesConditionalHashTable->addFromSource($this->getWriterPart('Style')->allConditionalStyles($this->spreadSheet));
 201          $this->fillHashTable->addFromSource($this->getWriterPart('Style')->allFills($this->spreadSheet));
 202          $this->fontHashTable->addFromSource($this->getWriterPart('Style')->allFonts($this->spreadSheet));
 203          $this->bordersHashTable->addFromSource($this->getWriterPart('Style')->allBorders($this->spreadSheet));
 204          $this->numFmtHashTable->addFromSource($this->getWriterPart('Style')->allNumberFormats($this->spreadSheet));
 205  
 206          // Create drawing dictionary
 207          $this->drawingHashTable->addFromSource($this->getWriterPart('Drawing')->allDrawings($this->spreadSheet));
 208  
 209          $options = new Archive();
 210          $options->setEnableZip64(false);
 211          $options->setOutputStream($this->fileHandle);
 212  
 213          $this->zip = new ZipStream(null, $options);
 214  
 215          // Add [Content_Types].xml to ZIP file
 216          $this->addZipFile('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts));
 217  
 218          //if hasMacros, add the vbaProject.bin file, Certificate file(if exists)
 219          if ($this->spreadSheet->hasMacros()) {
 220              $macrosCode = $this->spreadSheet->getMacrosCode();
 221              if ($macrosCode !== null) {
 222                  // we have the code ?
 223                  $this->addZipFile('xl/vbaProject.bin', $macrosCode); //allways in 'xl', allways named vbaProject.bin
 224                  if ($this->spreadSheet->hasMacrosCertificate()) {
 225                      //signed macros ?
 226                      // Yes : add the certificate file and the related rels file
 227                      $this->addZipFile('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate());
 228                      $this->addZipFile('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet));
 229                  }
 230              }
 231          }
 232          //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels)
 233          if ($this->spreadSheet->hasRibbon()) {
 234              $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target');
 235              $this->addZipFile($tmpRibbonTarget, $this->spreadSheet->getRibbonXMLData('data'));
 236              if ($this->spreadSheet->hasRibbonBinObjects()) {
 237                  $tmpRootPath = dirname($tmpRibbonTarget) . '/';
 238                  $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write
 239                  foreach ($ribbonBinObjects as $aPath => $aContent) {
 240                      $this->addZipFile($tmpRootPath . $aPath, $aContent);
 241                  }
 242                  //the rels for files
 243                  $this->addZipFile($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet));
 244              }
 245          }
 246  
 247          // Add relationships to ZIP file
 248          $this->addZipFile('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet));
 249          $this->addZipFile('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet));
 250  
 251          // Add document properties to ZIP file
 252          $this->addZipFile('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet));
 253          $this->addZipFile('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet));
 254          $customPropertiesPart = $this->getWriterPart('DocProps')->writeDocPropsCustom($this->spreadSheet);
 255          if ($customPropertiesPart !== null) {
 256              $this->addZipFile('docProps/custom.xml', $customPropertiesPart);
 257          }
 258  
 259          // Add theme to ZIP file
 260          $this->addZipFile('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet));
 261  
 262          // Add string table to ZIP file
 263          $this->addZipFile('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable));
 264  
 265          // Add styles to ZIP file
 266          $this->addZipFile('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet));
 267  
 268          // Add workbook to ZIP file
 269          $this->addZipFile('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas));
 270  
 271          $chartCount = 0;
 272          // Add worksheets
 273          for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
 274              $this->addZipFile('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts));
 275              if ($this->includeCharts) {
 276                  $charts = $this->spreadSheet->getSheet($i)->getChartCollection();
 277                  if (count($charts) > 0) {
 278                      foreach ($charts as $chart) {
 279                          $this->addZipFile('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas));
 280                          ++$chartCount;
 281                      }
 282                  }
 283              }
 284          }
 285  
 286          $chartRef1 = 0;
 287          // Add worksheet relationships (drawings, ...)
 288          for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
 289              // Add relationships
 290              $this->addZipFile('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts));
 291  
 292              // Add unparsedLoadedData
 293              $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName();
 294              $unparsedLoadedData = $this->spreadSheet->getUnparsedLoadedData();
 295              if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'])) {
 296                  foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'] as $ctrlProp) {
 297                      $this->addZipFile($ctrlProp['filePath'], $ctrlProp['content']);
 298                  }
 299              }
 300              if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'])) {
 301                  foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'] as $ctrlProp) {
 302                      $this->addZipFile($ctrlProp['filePath'], $ctrlProp['content']);
 303                  }
 304              }
 305  
 306              $drawings = $this->spreadSheet->getSheet($i)->getDrawingCollection();
 307              $drawingCount = count($drawings);
 308              if ($this->includeCharts) {
 309                  $chartCount = $this->spreadSheet->getSheet($i)->getChartCount();
 310              }
 311  
 312              // Add drawing and image relationship parts
 313              if (($drawingCount > 0) || ($chartCount > 0)) {
 314                  // Drawing relationships
 315                  $this->addZipFile('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts));
 316  
 317                  // Drawings
 318                  $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts));
 319              } elseif (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingAlternateContents'])) {
 320                  // Drawings
 321                  $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts));
 322              }
 323  
 324              // Add unparsed drawings
 325              if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['Drawings'])) {
 326                  foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['Drawings'] as $relId => $drawingXml) {
 327                      $drawingFile = array_search($relId, $unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']);
 328                      if ($drawingFile !== false) {
 329                          $drawingFile = ltrim($drawingFile, '.');
 330                          $this->addZipFile('xl' . $drawingFile, $drawingXml);
 331                      }
 332                  }
 333              }
 334  
 335              // Add comment relationship parts
 336              if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) {
 337                  // VML Comments
 338                  $this->addZipFile('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i)));
 339  
 340                  // Comments
 341                  $this->addZipFile('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i)));
 342              }
 343  
 344              // Add unparsed relationship parts
 345              if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'])) {
 346                  foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'] as $vmlDrawing) {
 347                      $this->addZipFile($vmlDrawing['filePath'], $vmlDrawing['content']);
 348                  }
 349              }
 350  
 351              // Add header/footer relationship parts
 352              if (count($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) {
 353                  // VML Drawings
 354                  $this->addZipFile('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i)));
 355  
 356                  // VML Drawing relationships
 357                  $this->addZipFile('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i)));
 358  
 359                  // Media
 360                  foreach ($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages() as $image) {
 361                      $this->addZipFile('xl/media/' . $image->getIndexedFilename(), file_get_contents($image->getPath()));
 362                  }
 363              }
 364          }
 365  
 366          // Add media
 367          for ($i = 0; $i < $this->getDrawingHashTable()->count(); ++$i) {
 368              if ($this->getDrawingHashTable()->getByIndex($i) instanceof WorksheetDrawing) {
 369                  $imageContents = null;
 370                  $imagePath = $this->getDrawingHashTable()->getByIndex($i)->getPath();
 371                  if (strpos($imagePath, 'zip://') !== false) {
 372                      $imagePath = substr($imagePath, 6);
 373                      $imagePathSplitted = explode('#', $imagePath);
 374  
 375                      $imageZip = new ZipArchive();
 376                      $imageZip->open($imagePathSplitted[0]);
 377                      $imageContents = $imageZip->getFromName($imagePathSplitted[1]);
 378                      $imageZip->close();
 379                      unset($imageZip);
 380                  } else {
 381                      $imageContents = file_get_contents($imagePath);
 382                  }
 383  
 384                  $this->addZipFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents);
 385              } elseif ($this->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) {
 386                  ob_start();
 387                  call_user_func(
 388                      $this->getDrawingHashTable()->getByIndex($i)->getRenderingFunction(),
 389                      $this->getDrawingHashTable()->getByIndex($i)->getImageResource()
 390                  );
 391                  $imageContents = ob_get_contents();
 392                  ob_end_clean();
 393  
 394                  $this->addZipFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents);
 395              }
 396          }
 397  
 398          Functions::setReturnDateType($saveDateReturnType);
 399          Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
 400  
 401          // Close file
 402          try {
 403              $this->zip->finish();
 404          } catch (OverflowException $e) {
 405              throw new WriterException('Could not close resource.');
 406          }
 407  
 408          $this->maybeCloseFileHandle();
 409      }
 410  
 411      /**
 412       * Get Spreadsheet object.
 413       *
 414       * @return Spreadsheet
 415       */
 416      public function getSpreadsheet()
 417      {
 418          return $this->spreadSheet;
 419      }
 420  
 421      /**
 422       * Set Spreadsheet object.
 423       *
 424       * @param Spreadsheet $spreadsheet PhpSpreadsheet object
 425       *
 426       * @return $this
 427       */
 428      public function setSpreadsheet(Spreadsheet $spreadsheet)
 429      {
 430          $this->spreadSheet = $spreadsheet;
 431  
 432          return $this;
 433      }
 434  
 435      /**
 436       * Get string table.
 437       *
 438       * @return string[]
 439       */
 440      public function getStringTable()
 441      {
 442          return $this->stringTable;
 443      }
 444  
 445      /**
 446       * Get Style HashTable.
 447       *
 448       * @return HashTable
 449       */
 450      public function getStyleHashTable()
 451      {
 452          return $this->styleHashTable;
 453      }
 454  
 455      /**
 456       * Get Conditional HashTable.
 457       *
 458       * @return HashTable
 459       */
 460      public function getStylesConditionalHashTable()
 461      {
 462          return $this->stylesConditionalHashTable;
 463      }
 464  
 465      /**
 466       * Get Fill HashTable.
 467       *
 468       * @return HashTable
 469       */
 470      public function getFillHashTable()
 471      {
 472          return $this->fillHashTable;
 473      }
 474  
 475      /**
 476       * Get \PhpOffice\PhpSpreadsheet\Style\Font HashTable.
 477       *
 478       * @return HashTable
 479       */
 480      public function getFontHashTable()
 481      {
 482          return $this->fontHashTable;
 483      }
 484  
 485      /**
 486       * Get Borders HashTable.
 487       *
 488       * @return HashTable
 489       */
 490      public function getBordersHashTable()
 491      {
 492          return $this->bordersHashTable;
 493      }
 494  
 495      /**
 496       * Get NumberFormat HashTable.
 497       *
 498       * @return HashTable
 499       */
 500      public function getNumFmtHashTable()
 501      {
 502          return $this->numFmtHashTable;
 503      }
 504  
 505      /**
 506       * Get \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\BaseDrawing HashTable.
 507       *
 508       * @return HashTable
 509       */
 510      public function getDrawingHashTable()
 511      {
 512          return $this->drawingHashTable;
 513      }
 514  
 515      /**
 516       * Get Office2003 compatibility.
 517       *
 518       * @return bool
 519       */
 520      public function getOffice2003Compatibility()
 521      {
 522          return $this->office2003compatibility;
 523      }
 524  
 525      /**
 526       * Set Office2003 compatibility.
 527       *
 528       * @param bool $pValue Office2003 compatibility?
 529       *
 530       * @return $this
 531       */
 532      public function setOffice2003Compatibility($pValue)
 533      {
 534          $this->office2003compatibility = $pValue;
 535  
 536          return $this;
 537      }
 538  
 539      private $pathNames = [];
 540  
 541      private function addZipFile(string $path, string $content): void
 542      {
 543          if (!in_array($path, $this->pathNames)) {
 544              $this->pathNames[] = $path;
 545              $this->zip->addFile($path, $content);
 546          }
 547      }
 548  }