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.
   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  namespace OpenSpout\Writer\ODS\Manager\Style;
   6  
   7  use OpenSpout\Common\Entity\Style\Border;
   8  use OpenSpout\Common\Entity\Style\BorderPart;
   9  use OpenSpout\Common\Entity\Style\CellAlignment;
  10  use OpenSpout\Common\Entity\Style\CellVerticalAlignment;
  11  use OpenSpout\Common\Entity\Style\Style;
  12  use OpenSpout\Writer\Common\AbstractOptions;
  13  use OpenSpout\Writer\Common\ColumnWidth;
  14  use OpenSpout\Writer\Common\Entity\Worksheet;
  15  use OpenSpout\Writer\Common\Manager\Style\AbstractStyleManager as CommonStyleManager;
  16  use OpenSpout\Writer\ODS\Helper\BorderHelper;
  17  
  18  /**
  19   * @internal
  20   *
  21   * @property StyleRegistry $styleRegistry
  22   */
  23  final class StyleManager extends CommonStyleManager
  24  {
  25      private AbstractOptions $options;
  26  
  27      public function __construct(StyleRegistry $styleRegistry, AbstractOptions $options)
  28      {
  29          parent::__construct($styleRegistry);
  30          $this->options = $options;
  31      }
  32  
  33      /**
  34       * Returns the content of the "styles.xml" file, given a list of styles.
  35       *
  36       * @param int $numWorksheets Number of worksheets created
  37       */
  38      public function getStylesXMLFileContent(int $numWorksheets): string
  39      {
  40          $content = <<<'EOD'
  41              <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  42              <office:document-styles office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
  43              EOD;
  44  
  45          $content .= $this->getFontFaceSectionContent();
  46          $content .= $this->getStylesSectionContent();
  47          $content .= $this->getAutomaticStylesSectionContent($numWorksheets);
  48          $content .= $this->getMasterStylesSectionContent($numWorksheets);
  49  
  50          $content .= <<<'EOD'
  51              </office:document-styles>
  52              EOD;
  53  
  54          return $content;
  55      }
  56  
  57      /**
  58       * Returns the contents of the "<office:font-face-decls>" section, inside "content.xml" file.
  59       */
  60      public function getContentXmlFontFaceSectionContent(): string
  61      {
  62          $content = '<office:font-face-decls>';
  63          foreach ($this->styleRegistry->getUsedFonts() as $fontName) {
  64              $content .= '<style:font-face style:name="'.$fontName.'" svg:font-family="'.$fontName.'"/>';
  65          }
  66          $content .= '</office:font-face-decls>';
  67  
  68          return $content;
  69      }
  70  
  71      /**
  72       * Returns the contents of the "<office:automatic-styles>" section, inside "content.xml" file.
  73       *
  74       * @param Worksheet[] $worksheets
  75       */
  76      public function getContentXmlAutomaticStylesSectionContent(array $worksheets): string
  77      {
  78          $content = '<office:automatic-styles>';
  79  
  80          foreach ($this->styleRegistry->getRegisteredStyles() as $style) {
  81              $content .= $this->getStyleSectionContent($style);
  82          }
  83  
  84          $useOptimalRowHeight = null === $this->options->DEFAULT_ROW_HEIGHT ? 'true' : 'false';
  85          $defaultRowHeight = null === $this->options->DEFAULT_ROW_HEIGHT ? '15pt' : "{$this->options->DEFAULT_ROW_HEIGHT}pt";
  86          $defaultColumnWidth = null === $this->options->DEFAULT_COLUMN_WIDTH ? '' : "style:column-width=\"{$this->options->DEFAULT_COLUMN_WIDTH}pt\"";
  87  
  88          $content .= <<<EOD
  89              <style:style style:family="table-column" style:name="default-column-style">
  90                  <style:table-column-properties fo:break-before="auto" {$defaultColumnWidth}/>
  91              </style:style>
  92              <style:style style:family="table-row" style:name="ro1">
  93                  <style:table-row-properties fo:break-before="auto" style:row-height="{$defaultRowHeight}" style:use-optimal-row-height="{$useOptimalRowHeight}"/>
  94              </style:style>
  95              EOD;
  96  
  97          foreach ($worksheets as $worksheet) {
  98              $worksheetId = $worksheet->getId();
  99              $isSheetVisible = $worksheet->getExternalSheet()->isVisible() ? 'true' : 'false';
 100  
 101              $content .= <<<EOD
 102                  <style:style style:family="table" style:master-page-name="mp{$worksheetId}" style:name="ta{$worksheetId}">
 103                      <style:table-properties style:writing-mode="lr-tb" table:display="{$isSheetVisible}"/>
 104                  </style:style>
 105                  EOD;
 106          }
 107  
 108          // Sort column widths since ODS cares about order
 109          $columnWidths = $this->options->getColumnWidths();
 110          usort($columnWidths, static function (ColumnWidth $a, ColumnWidth $b): int {
 111              return $a->start <=> $b->start;
 112          });
 113          $content .= $this->getTableColumnStylesXMLContent();
 114  
 115          $content .= '</office:automatic-styles>';
 116  
 117          return $content;
 118      }
 119  
 120      public function getTableColumnStylesXMLContent(): string
 121      {
 122          if ([] === $this->options->getColumnWidths()) {
 123              return '';
 124          }
 125  
 126          $content = '';
 127          foreach ($this->options->getColumnWidths() as $styleIndex => $columnWidth) {
 128              $content .= <<<EOD
 129                  <style:style style:family="table-column" style:name="co{$styleIndex}">
 130                      <style:table-column-properties fo:break-before="auto" style:use-optimal-column-width="false" style:column-width="{$columnWidth->width}pt"/>
 131                  </style:style>
 132                  EOD;
 133          }
 134  
 135          return $content;
 136      }
 137  
 138      public function getStyledTableColumnXMLContent(int $maxNumColumns): string
 139      {
 140          if ([] === $this->options->getColumnWidths()) {
 141              return '';
 142          }
 143  
 144          $content = '';
 145          foreach ($this->options->getColumnWidths() as $styleIndex => $columnWidth) {
 146              $numCols = $columnWidth->end - $columnWidth->start + 1;
 147              $content .= <<<EOD
 148                  <table:table-column table:default-cell-style-name='Default' table:style-name="co{$styleIndex}" table:number-columns-repeated="{$numCols}"/>
 149                  EOD;
 150          }
 151          \assert(isset($columnWidth));
 152          // Note: This assumes the column widths are contiguous and default width is
 153          // only applied to columns after the last custom column with a custom width
 154          $content .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="default-column-style" table:number-columns-repeated="'.($maxNumColumns - $columnWidth->end).'"/>';
 155  
 156          return $content;
 157      }
 158  
 159      /**
 160       * Returns the content of the "<office:styles>" section, inside "styles.xml" file.
 161       */
 162      private function getStylesSectionContent(): string
 163      {
 164          $defaultStyle = $this->getDefaultStyle();
 165  
 166          return <<<EOD
 167              <office:styles>
 168                  <number:number-style style:name="N0">
 169                      <number:number number:min-integer-digits="1"/>
 170                  </number:number-style>
 171                  <style:style style:data-style-name="N0" style:family="table-cell" style:name="Default">
 172                      <style:table-cell-properties fo:background-color="transparent" style:vertical-align="automatic"/>
 173                      <style:text-properties fo:color="#{$defaultStyle->getFontColor()}"
 174                                             fo:font-size="{$defaultStyle->getFontSize()}pt" style:font-size-asian="{$defaultStyle->getFontSize()}pt" style:font-size-complex="{$defaultStyle->getFontSize()}pt"
 175                                             style:font-name="{$defaultStyle->getFontName()}" style:font-name-asian="{$defaultStyle->getFontName()}" style:font-name-complex="{$defaultStyle->getFontName()}"/>
 176                  </style:style>
 177              </office:styles>
 178              EOD;
 179      }
 180  
 181      /**
 182       * Returns the content of the "<office:master-styles>" section, inside "styles.xml" file.
 183       *
 184       * @param int $numWorksheets Number of worksheets created
 185       */
 186      private function getMasterStylesSectionContent(int $numWorksheets): string
 187      {
 188          $content = '<office:master-styles>';
 189  
 190          for ($i = 1; $i <= $numWorksheets; ++$i) {
 191              $content .= <<<EOD
 192                  <style:master-page style:name="mp{$i}" style:page-layout-name="pm{$i}">
 193                      <style:header/>
 194                      <style:header-left style:display="false"/>
 195                      <style:footer/>
 196                      <style:footer-left style:display="false"/>
 197                  </style:master-page>
 198                  EOD;
 199          }
 200  
 201          $content .= '</office:master-styles>';
 202  
 203          return $content;
 204      }
 205  
 206      /**
 207       * Returns the content of the "<office:font-face-decls>" section, inside "styles.xml" file.
 208       */
 209      private function getFontFaceSectionContent(): string
 210      {
 211          $content = '<office:font-face-decls>';
 212          foreach ($this->styleRegistry->getUsedFonts() as $fontName) {
 213              $content .= '<style:font-face style:name="'.$fontName.'" svg:font-family="'.$fontName.'"/>';
 214          }
 215          $content .= '</office:font-face-decls>';
 216  
 217          return $content;
 218      }
 219  
 220      /**
 221       * Returns the content of the "<office:automatic-styles>" section, inside "styles.xml" file.
 222       *
 223       * @param int $numWorksheets Number of worksheets created
 224       */
 225      private function getAutomaticStylesSectionContent(int $numWorksheets): string
 226      {
 227          $content = '<office:automatic-styles>';
 228  
 229          for ($i = 1; $i <= $numWorksheets; ++$i) {
 230              $content .= <<<EOD
 231                  <style:page-layout style:name="pm{$i}">
 232                      <style:page-layout-properties style:first-page-number="continue" style:print="objects charts drawings" style:table-centering="none"/>
 233                      <style:header-style/>
 234                      <style:footer-style/>
 235                  </style:page-layout>
 236                  EOD;
 237          }
 238  
 239          $content .= '</office:automatic-styles>';
 240  
 241          return $content;
 242      }
 243  
 244      /**
 245       * Returns the contents of the "<style:style>" section, inside "<office:automatic-styles>" section.
 246       */
 247      private function getStyleSectionContent(Style $style): string
 248      {
 249          $styleIndex = $style->getId() + 1; // 1-based
 250  
 251          $content = '<style:style style:data-style-name="N0" style:family="table-cell" style:name="ce'.$styleIndex.'" style:parent-style-name="Default">';
 252  
 253          $content .= $this->getTextPropertiesSectionContent($style);
 254          $content .= $this->getParagraphPropertiesSectionContent($style);
 255          $content .= $this->getTableCellPropertiesSectionContent($style);
 256  
 257          $content .= '</style:style>';
 258  
 259          return $content;
 260      }
 261  
 262      /**
 263       * Returns the contents of the "<style:text-properties>" section, inside "<style:style>" section.
 264       */
 265      private function getTextPropertiesSectionContent(Style $style): string
 266      {
 267          if (!$style->shouldApplyFont()) {
 268              return '';
 269          }
 270  
 271          return '<style:text-properties '
 272              .$this->getFontSectionContent($style)
 273              .'/>';
 274      }
 275  
 276      /**
 277       * Returns the contents of the fonts definition section, inside "<style:text-properties>" section.
 278       */
 279      private function getFontSectionContent(Style $style): string
 280      {
 281          $defaultStyle = $this->getDefaultStyle();
 282          $content = '';
 283  
 284          $fontColor = $style->getFontColor();
 285          if ($fontColor !== $defaultStyle->getFontColor()) {
 286              $content .= ' fo:color="#'.$fontColor.'"';
 287          }
 288  
 289          $fontName = $style->getFontName();
 290          if ($fontName !== $defaultStyle->getFontName()) {
 291              $content .= ' style:font-name="'.$fontName.'" style:font-name-asian="'.$fontName.'" style:font-name-complex="'.$fontName.'"';
 292          }
 293  
 294          $fontSize = $style->getFontSize();
 295          if ($fontSize !== $defaultStyle->getFontSize()) {
 296              $content .= ' fo:font-size="'.$fontSize.'pt" style:font-size-asian="'.$fontSize.'pt" style:font-size-complex="'.$fontSize.'pt"';
 297          }
 298  
 299          if ($style->isFontBold()) {
 300              $content .= ' fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"';
 301          }
 302          if ($style->isFontItalic()) {
 303              $content .= ' fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"';
 304          }
 305          if ($style->isFontUnderline()) {
 306              $content .= ' style:text-underline-style="solid" style:text-underline-type="single"';
 307          }
 308          if ($style->isFontStrikethrough()) {
 309              $content .= ' style:text-line-through-style="solid"';
 310          }
 311  
 312          return $content;
 313      }
 314  
 315      /**
 316       * Returns the contents of the "<style:paragraph-properties>" section, inside "<style:style>" section.
 317       */
 318      private function getParagraphPropertiesSectionContent(Style $style): string
 319      {
 320          if (!$style->shouldApplyCellAlignment() && !$style->shouldApplyCellVerticalAlignment()) {
 321              return '';
 322          }
 323  
 324          return '<style:paragraph-properties '
 325              .$this->getCellAlignmentSectionContent($style)
 326              .$this->getCellVerticalAlignmentSectionContent($style)
 327              .'/>';
 328      }
 329  
 330      /**
 331       * Returns the contents of the cell alignment definition for the "<style:paragraph-properties>" section.
 332       */
 333      private function getCellAlignmentSectionContent(Style $style): string
 334      {
 335          if (!$style->hasSetCellAlignment()) {
 336              return '';
 337          }
 338  
 339          return sprintf(
 340              ' fo:text-align="%s" ',
 341              $this->transformCellAlignment($style->getCellAlignment())
 342          );
 343      }
 344  
 345      /**
 346       * Returns the contents of the cell vertical alignment definition for the "<style:paragraph-properties>" section.
 347       */
 348      private function getCellVerticalAlignmentSectionContent(Style $style): string
 349      {
 350          if (!$style->hasSetCellVerticalAlignment()) {
 351              return '';
 352          }
 353  
 354          return sprintf(
 355              ' fo:vertical-align="%s" ',
 356              $this->transformCellVerticalAlignment($style->getCellVerticalAlignment())
 357          );
 358      }
 359  
 360      /**
 361       * Even though "left" and "right" alignments are part of the spec, and interpreted
 362       * respectively as "start" and "end", using the recommended values increase compatibility
 363       * with software that will read the created ODS file.
 364       */
 365      private function transformCellAlignment(string $cellAlignment): string
 366      {
 367          return match ($cellAlignment) {
 368              CellAlignment::LEFT => 'start',
 369              CellAlignment::RIGHT => 'end',
 370              default => $cellAlignment,
 371          };
 372      }
 373  
 374      /**
 375       * Spec uses 'middle' rather than 'center'
 376       * http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1420236_253892949.
 377       */
 378      private function transformCellVerticalAlignment(string $cellVerticalAlignment): string
 379      {
 380          return (CellVerticalAlignment::CENTER === $cellVerticalAlignment)
 381              ? 'middle'
 382              : $cellVerticalAlignment;
 383      }
 384  
 385      /**
 386       * Returns the contents of the "<style:table-cell-properties>" section, inside "<style:style>" section.
 387       */
 388      private function getTableCellPropertiesSectionContent(Style $style): string
 389      {
 390          $content = '<style:table-cell-properties ';
 391  
 392          if ($style->hasSetWrapText()) {
 393              $content .= $this->getWrapTextXMLContent($style->shouldWrapText());
 394          }
 395  
 396          if (null !== ($border = $style->getBorder())) {
 397              $content .= $this->getBorderXMLContent($border);
 398          }
 399  
 400          if (null !== ($bgColor = $style->getBackgroundColor())) {
 401              $content .= $this->getBackgroundColorXMLContent($bgColor);
 402          }
 403  
 404          $content .= '/>';
 405  
 406          return $content;
 407      }
 408  
 409      /**
 410       * Returns the contents of the wrap text definition for the "<style:table-cell-properties>" section.
 411       */
 412      private function getWrapTextXMLContent(bool $shouldWrapText): string
 413      {
 414          return ' fo:wrap-option="'.($shouldWrapText ? '' : 'no-').'wrap" style:vertical-align="automatic" ';
 415      }
 416  
 417      /**
 418       * Returns the contents of the borders definition for the "<style:table-cell-properties>" section.
 419       */
 420      private function getBorderXMLContent(Border $border): string
 421      {
 422          $borders = array_map(static function (BorderPart $borderPart) {
 423              return BorderHelper::serializeBorderPart($borderPart);
 424          }, $border->getParts());
 425  
 426          return sprintf(' %s ', implode(' ', $borders));
 427      }
 428  
 429      /**
 430       * Returns the contents of the background color definition for the "<style:table-cell-properties>" section.
 431       */
 432      private function getBackgroundColorXMLContent(string $bgColor): string
 433      {
 434          return sprintf(' fo:background-color="#%s" ', $bgColor);
 435      }
 436  }