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\Reader\Xlsx;
   4  
   5  use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
   6  use PhpOffice\PhpSpreadsheet\Style\Alignment;
   7  use PhpOffice\PhpSpreadsheet\Style\Border;
   8  use PhpOffice\PhpSpreadsheet\Style\Borders;
   9  use PhpOffice\PhpSpreadsheet\Style\Color;
  10  use PhpOffice\PhpSpreadsheet\Style\Fill;
  11  use PhpOffice\PhpSpreadsheet\Style\Font;
  12  use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
  13  use PhpOffice\PhpSpreadsheet\Style\Protection;
  14  use PhpOffice\PhpSpreadsheet\Style\Style;
  15  use SimpleXMLElement;
  16  use stdClass;
  17  
  18  class Styles extends BaseParserClass
  19  {
  20      /**
  21       * Theme instance.
  22       *
  23       * @var ?Theme
  24       */
  25      private $theme;
  26  
  27      /** @var array */
  28      private $workbookPalette = [];
  29  
  30      /** @var array */
  31      private $styles = [];
  32  
  33      /** @var array */
  34      private $cellStyles = [];
  35  
  36      /** @var SimpleXMLElement */
  37      private $styleXml;
  38  
  39      /** @var string */
  40      private $namespace = '';
  41  
  42      public function setNamespace(string $namespace): void
  43      {
  44          $this->namespace = $namespace;
  45      }
  46  
  47      public function setWorkbookPalette(array $palette): void
  48      {
  49          $this->workbookPalette = $palette;
  50      }
  51  
  52      /**
  53       * Cast SimpleXMLElement to bool to overcome Scrutinizer problem.
  54       *
  55       * @param mixed $value
  56       */
  57      private static function castBool($value): bool
  58      {
  59          return (bool) $value;
  60      }
  61  
  62      private function getStyleAttributes(SimpleXMLElement $value): SimpleXMLElement
  63      {
  64          $attr = null;
  65          if (self::castBool($value)) {
  66              $attr = $value->attributes('');
  67              if ($attr === null || count($attr) === 0) {
  68                  $attr = $value->attributes($this->namespace);
  69              }
  70          }
  71  
  72          return Xlsx::testSimpleXml($attr);
  73      }
  74  
  75      public function setStyleXml(SimpleXmlElement $styleXml): void
  76      {
  77          $this->styleXml = $styleXml;
  78      }
  79  
  80      public function setTheme(Theme $theme): void
  81      {
  82          $this->theme = $theme;
  83      }
  84  
  85      public function setStyleBaseData(?Theme $theme = null, array $styles = [], array $cellStyles = []): void
  86      {
  87          $this->theme = $theme;
  88          $this->styles = $styles;
  89          $this->cellStyles = $cellStyles;
  90      }
  91  
  92      public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void
  93      {
  94          if (isset($fontStyleXml->name)) {
  95              $attr = $this->getStyleAttributes($fontStyleXml->name);
  96              if (isset($attr['val'])) {
  97                  $fontStyle->setName((string) $attr['val']);
  98              }
  99          }
 100          if (isset($fontStyleXml->sz)) {
 101              $attr = $this->getStyleAttributes($fontStyleXml->sz);
 102              if (isset($attr['val'])) {
 103                  $fontStyle->setSize((float) $attr['val']);
 104              }
 105          }
 106          if (isset($fontStyleXml->b)) {
 107              $attr = $this->getStyleAttributes($fontStyleXml->b);
 108              $fontStyle->setBold(!isset($attr['val']) || self::boolean((string) $attr['val']));
 109          }
 110          if (isset($fontStyleXml->i)) {
 111              $attr = $this->getStyleAttributes($fontStyleXml->i);
 112              $fontStyle->setItalic(!isset($attr['val']) || self::boolean((string) $attr['val']));
 113          }
 114          if (isset($fontStyleXml->strike)) {
 115              $attr = $this->getStyleAttributes($fontStyleXml->strike);
 116              $fontStyle->setStrikethrough(!isset($attr['val']) || self::boolean((string) $attr['val']));
 117          }
 118          $fontStyle->getColor()->setARGB($this->readColor($fontStyleXml->color));
 119  
 120          if (isset($fontStyleXml->u)) {
 121              $attr = $this->getStyleAttributes($fontStyleXml->u);
 122              if (!isset($attr['val'])) {
 123                  $fontStyle->setUnderline(Font::UNDERLINE_SINGLE);
 124              } else {
 125                  $fontStyle->setUnderline((string) $attr['val']);
 126              }
 127          }
 128          if (isset($fontStyleXml->vertAlign)) {
 129              $attr = $this->getStyleAttributes($fontStyleXml->vertAlign);
 130              if (isset($attr['val'])) {
 131                  $verticalAlign = strtolower((string) $attr['val']);
 132                  if ($verticalAlign === 'superscript') {
 133                      $fontStyle->setSuperscript(true);
 134                  } elseif ($verticalAlign === 'subscript') {
 135                      $fontStyle->setSubscript(true);
 136                  }
 137              }
 138          }
 139      }
 140  
 141      private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void
 142      {
 143          if ((string) $numfmtStyleXml['formatCode'] !== '') {
 144              $numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmtStyleXml['formatCode']));
 145  
 146              return;
 147          }
 148          $numfmt = $this->getStyleAttributes($numfmtStyleXml);
 149          if (isset($numfmt['formatCode'])) {
 150              $numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmt['formatCode']));
 151          }
 152      }
 153  
 154      public function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyleXml): void
 155      {
 156          if ($fillStyleXml->gradientFill) {
 157              /** @var SimpleXMLElement $gradientFill */
 158              $gradientFill = $fillStyleXml->gradientFill[0];
 159              $attr = $this->getStyleAttributes($gradientFill);
 160              if (!empty($attr['type'])) {
 161                  $fillStyle->setFillType((string) $attr['type']);
 162              }
 163              $fillStyle->setRotation((float) ($attr['degree']));
 164              $gradientFill->registerXPathNamespace('sml', Namespaces::MAIN);
 165              $fillStyle->getStartColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color));
 166              $fillStyle->getEndColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color));
 167          } elseif ($fillStyleXml->patternFill) {
 168              $defaultFillStyle = Fill::FILL_NONE;
 169              if ($fillStyleXml->patternFill->fgColor) {
 170                  $fillStyle->getStartColor()->setARGB($this->readColor($fillStyleXml->patternFill->fgColor, true));
 171                  $defaultFillStyle = Fill::FILL_SOLID;
 172              }
 173              if ($fillStyleXml->patternFill->bgColor) {
 174                  $fillStyle->getEndColor()->setARGB($this->readColor($fillStyleXml->patternFill->bgColor, true));
 175                  $defaultFillStyle = Fill::FILL_SOLID;
 176              }
 177  
 178              $type = '';
 179              if ((string) $fillStyleXml->patternFill['patternType'] !== '') {
 180                  $type = (string) $fillStyleXml->patternFill['patternType'];
 181              } else {
 182                  $attr = $this->getStyleAttributes($fillStyleXml->patternFill);
 183                  $type = (string) $attr['patternType'];
 184              }
 185              $patternType = ($type === '') ? $defaultFillStyle : $type;
 186  
 187              $fillStyle->setFillType($patternType);
 188          }
 189      }
 190  
 191      public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderStyleXml): void
 192      {
 193          $diagonalUp = $this->getAttribute($borderStyleXml, 'diagonalUp');
 194          $diagonalUp = self::boolean($diagonalUp);
 195          $diagonalDown = $this->getAttribute($borderStyleXml, 'diagonalDown');
 196          $diagonalDown = self::boolean($diagonalDown);
 197          if ($diagonalUp === false) {
 198              if ($diagonalDown === false) {
 199                  $borderStyle->setDiagonalDirection(Borders::DIAGONAL_NONE);
 200              } else {
 201                  $borderStyle->setDiagonalDirection(Borders::DIAGONAL_DOWN);
 202              }
 203          } elseif ($diagonalDown === false) {
 204              $borderStyle->setDiagonalDirection(Borders::DIAGONAL_UP);
 205          } else {
 206              $borderStyle->setDiagonalDirection(Borders::DIAGONAL_BOTH);
 207          }
 208  
 209          $this->readBorder($borderStyle->getLeft(), $borderStyleXml->left);
 210          $this->readBorder($borderStyle->getRight(), $borderStyleXml->right);
 211          $this->readBorder($borderStyle->getTop(), $borderStyleXml->top);
 212          $this->readBorder($borderStyle->getBottom(), $borderStyleXml->bottom);
 213          $this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal);
 214      }
 215  
 216      private function getAttribute(SimpleXMLElement $xml, string $attribute): string
 217      {
 218          $style = '';
 219          if ((string) $xml[$attribute] !== '') {
 220              $style = (string) $xml[$attribute];
 221          } else {
 222              $attr = $this->getStyleAttributes($xml);
 223              if (isset($attr[$attribute])) {
 224                  $style = (string) $attr[$attribute];
 225              }
 226          }
 227  
 228          return $style;
 229      }
 230  
 231      private function readBorder(Border $border, SimpleXMLElement $borderXml): void
 232      {
 233          $style = $this->getAttribute($borderXml, 'style');
 234          if ($style !== '') {
 235              $border->setBorderStyle((string) $style);
 236          }
 237          if (isset($borderXml->color)) {
 238              $border->getColor()->setARGB($this->readColor($borderXml->color));
 239          }
 240      }
 241  
 242      public function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void
 243      {
 244          $horizontal = $this->getAttribute($alignmentXml, 'horizontal');
 245          $alignment->setHorizontal($horizontal);
 246          $vertical = $this->getAttribute($alignmentXml, 'vertical');
 247          $alignment->setVertical((string) $vertical);
 248  
 249          $textRotation = (int) $this->getAttribute($alignmentXml, 'textRotation');
 250          if ($textRotation > 90) {
 251              $textRotation = 90 - $textRotation;
 252          }
 253          $alignment->setTextRotation($textRotation);
 254  
 255          $wrapText = $this->getAttribute($alignmentXml, 'wrapText');
 256          $alignment->setWrapText(self::boolean((string) $wrapText));
 257          $shrinkToFit = $this->getAttribute($alignmentXml, 'shrinkToFit');
 258          $alignment->setShrinkToFit(self::boolean((string) $shrinkToFit));
 259          $indent = (int) $this->getAttribute($alignmentXml, 'indent');
 260          $alignment->setIndent(max($indent, 0));
 261          $readingOrder = (int) $this->getAttribute($alignmentXml, 'readingOrder');
 262          $alignment->setReadOrder(max($readingOrder, 0));
 263      }
 264  
 265      private static function formatGeneral(string $formatString): string
 266      {
 267          if ($formatString === 'GENERAL') {
 268              $formatString = NumberFormat::FORMAT_GENERAL;
 269          }
 270  
 271          return $formatString;
 272      }
 273  
 274      /**
 275       * Read style.
 276       *
 277       * @param SimpleXMLElement|stdClass $style
 278       */
 279      public function readStyle(Style $docStyle, $style): void
 280      {
 281          if ($style->numFmt instanceof SimpleXMLElement) {
 282              $this->readNumberFormat($docStyle->getNumberFormat(), $style->numFmt);
 283          } else {
 284              $docStyle->getNumberFormat()->setFormatCode(self::formatGeneral((string) $style->numFmt));
 285          }
 286  
 287          if (isset($style->font)) {
 288              $this->readFontStyle($docStyle->getFont(), $style->font);
 289          }
 290  
 291          if (isset($style->fill)) {
 292              $this->readFillStyle($docStyle->getFill(), $style->fill);
 293          }
 294  
 295          if (isset($style->border)) {
 296              $this->readBorderStyle($docStyle->getBorders(), $style->border);
 297          }
 298  
 299          if (isset($style->alignment)) {
 300              $this->readAlignmentStyle($docStyle->getAlignment(), $style->alignment);
 301          }
 302  
 303          // protection
 304          if (isset($style->protection)) {
 305              $this->readProtectionLocked($docStyle, $style->protection);
 306              $this->readProtectionHidden($docStyle, $style->protection);
 307          }
 308  
 309          // top-level style settings
 310          if (isset($style->quotePrefix)) {
 311              $docStyle->setQuotePrefix((bool) $style->quotePrefix);
 312          }
 313      }
 314  
 315      /**
 316       * Read protection locked attribute.
 317       */
 318      public function readProtectionLocked(Style $docStyle, SimpleXMLElement $style): void
 319      {
 320          $locked = '';
 321          if ((string) $style['locked'] !== '') {
 322              $locked = (string) $style['locked'];
 323          } else {
 324              $attr = $this->getStyleAttributes($style);
 325              if (isset($attr['locked'])) {
 326                  $locked = (string) $attr['locked'];
 327              }
 328          }
 329          if ($locked !== '') {
 330              if (self::boolean($locked)) {
 331                  $docStyle->getProtection()->setLocked(Protection::PROTECTION_PROTECTED);
 332              } else {
 333                  $docStyle->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED);
 334              }
 335          }
 336      }
 337  
 338      /**
 339       * Read protection hidden attribute.
 340       */
 341      public function readProtectionHidden(Style $docStyle, SimpleXMLElement $style): void
 342      {
 343          $hidden = '';
 344          if ((string) $style['hidden'] !== '') {
 345              $hidden = (string) $style['hidden'];
 346          } else {
 347              $attr = $this->getStyleAttributes($style);
 348              if (isset($attr['hidden'])) {
 349                  $hidden = (string) $attr['hidden'];
 350              }
 351          }
 352          if ($hidden !== '') {
 353              if (self::boolean((string) $hidden)) {
 354                  $docStyle->getProtection()->setHidden(Protection::PROTECTION_PROTECTED);
 355              } else {
 356                  $docStyle->getProtection()->setHidden(Protection::PROTECTION_UNPROTECTED);
 357              }
 358          }
 359      }
 360  
 361      public function readColor(SimpleXMLElement $color, bool $background = false): string
 362      {
 363          $attr = $this->getStyleAttributes($color);
 364          if (isset($attr['rgb'])) {
 365              return (string) $attr['rgb'];
 366          }
 367          if (isset($attr['indexed'])) {
 368              if (empty($this->workbookPalette)) {
 369                  return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? '';
 370              }
 371  
 372              return Color::indexedColor((int) ($attr['indexed']), $background, $this->workbookPalette)->getARGB() ?? '';
 373          }
 374          if (isset($attr['theme'])) {
 375              if ($this->theme !== null) {
 376                  $returnColour = $this->theme->getColourByIndex((int) $attr['theme']);
 377                  if (isset($attr['tint'])) {
 378                      $tintAdjust = (float) $attr['tint'];
 379                      $returnColour = Color::changeBrightness($returnColour ?? '', $tintAdjust);
 380                  }
 381  
 382                  return 'FF' . $returnColour;
 383              }
 384          }
 385  
 386          return ($background) ? 'FFFFFFFF' : 'FF000000';
 387      }
 388  
 389      public function dxfs(bool $readDataOnly = false): array
 390      {
 391          $dxfs = [];
 392          if (!$readDataOnly && $this->styleXml) {
 393              //    Conditional Styles
 394              if ($this->styleXml->dxfs) {
 395                  foreach ($this->styleXml->dxfs->dxf as $dxf) {
 396                      $style = new Style(false, true);
 397                      $this->readStyle($style, $dxf);
 398                      $dxfs[] = $style;
 399                  }
 400              }
 401              //    Cell Styles
 402              if ($this->styleXml->cellStyles) {
 403                  foreach ($this->styleXml->cellStyles->cellStyle as $cellStylex) {
 404                      $cellStyle = Xlsx::getAttributes($cellStylex);
 405                      if ((int) ($cellStyle['builtinId']) == 0) {
 406                          if (isset($this->cellStyles[(int) ($cellStyle['xfId'])])) {
 407                              // Set default style
 408                              $style = new Style();
 409                              $this->readStyle($style, $this->cellStyles[(int) ($cellStyle['xfId'])]);
 410  
 411                              // normal style, currently not using it for anything
 412                          }
 413                      }
 414                  }
 415              }
 416          }
 417  
 418          return $dxfs;
 419      }
 420  
 421      public function styles(): array
 422      {
 423          return $this->styles;
 424      }
 425  
 426      /**
 427       * Get array item.
 428       *
 429       * @param mixed $array (usually array, in theory can be false)
 430       *
 431       * @return stdClass
 432       */
 433      private static function getArrayItem($array, int $key = 0)
 434      {
 435          return is_array($array) ? ($array[$key] ?? null) : null;
 436      }
 437  }