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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body