1 <?php 2 3 declare(strict_types=1); 4 5 namespace OpenSpout\Writer\XLSX\Manager\Style; 6 7 use OpenSpout\Common\Entity\Style\BorderPart; 8 use OpenSpout\Common\Entity\Style\Color; 9 use OpenSpout\Common\Entity\Style\Style; 10 use OpenSpout\Writer\Common\Manager\Style\AbstractStyleManager as CommonStyleManager; 11 use OpenSpout\Writer\XLSX\Helper\BorderHelper; 12 13 /** 14 * @internal 15 * 16 * @property StyleRegistry $styleRegistry 17 */ 18 final class StyleManager extends CommonStyleManager 19 { 20 public function __construct(StyleRegistry $styleRegistry) 21 { 22 parent::__construct($styleRegistry); 23 } 24 25 /** 26 * For empty cells, we can specify a style or not. If no style are specified, 27 * then the software default will be applied. But sometimes, it may be useful 28 * to override this default style, for instance if the cell should have a 29 * background color different than the default one or some borders 30 * (fonts property don't really matter here). 31 * 32 * @return bool Whether the cell should define a custom style 33 */ 34 public function shouldApplyStyleOnEmptyCell(?int $styleId): bool 35 { 36 if (null === $styleId) { 37 return false; 38 } 39 $associatedFillId = $this->styleRegistry->getFillIdForStyleId($styleId); 40 $hasStyleCustomFill = (null !== $associatedFillId && 0 !== $associatedFillId); 41 42 $associatedBorderId = $this->styleRegistry->getBorderIdForStyleId($styleId); 43 $hasStyleCustomBorders = (null !== $associatedBorderId && 0 !== $associatedBorderId); 44 45 $associatedFormatId = $this->styleRegistry->getFormatIdForStyleId($styleId); 46 $hasStyleCustomFormats = (null !== $associatedFormatId && 0 !== $associatedFormatId); 47 48 return $hasStyleCustomFill || $hasStyleCustomBorders || $hasStyleCustomFormats; 49 } 50 51 /** 52 * Returns the content of the "styles.xml" file, given a list of styles. 53 */ 54 public function getStylesXMLFileContent(): string 55 { 56 $content = <<<'EOD' 57 <?xml version="1.0" encoding="UTF-8" standalone="yes"?> 58 <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> 59 EOD; 60 61 $content .= $this->getFormatsSectionContent(); 62 $content .= $this->getFontsSectionContent(); 63 $content .= $this->getFillsSectionContent(); 64 $content .= $this->getBordersSectionContent(); 65 $content .= $this->getCellStyleXfsSectionContent(); 66 $content .= $this->getCellXfsSectionContent(); 67 $content .= $this->getCellStylesSectionContent(); 68 69 $content .= <<<'EOD' 70 </styleSheet> 71 EOD; 72 73 return $content; 74 } 75 76 /** 77 * Returns the content of the "<numFmts>" section. 78 */ 79 private function getFormatsSectionContent(): string 80 { 81 $tags = []; 82 $registeredFormats = $this->styleRegistry->getRegisteredFormats(); 83 foreach ($registeredFormats as $styleId) { 84 $numFmtId = $this->styleRegistry->getFormatIdForStyleId($styleId); 85 86 // Built-in formats do not need to be declared, skip them 87 if ($numFmtId < 164) { 88 continue; 89 } 90 91 /** @var Style $style */ 92 $style = $this->styleRegistry->getStyleFromStyleId($styleId); 93 $format = $style->getFormat(); 94 $tags[] = '<numFmt numFmtId="'.$numFmtId.'" formatCode="'.$format.'"/>'; 95 } 96 $content = '<numFmts count="'.\count($tags).'">'; 97 $content .= implode('', $tags); 98 $content .= '</numFmts>'; 99 100 return $content; 101 } 102 103 /** 104 * Returns the content of the "<fonts>" section. 105 */ 106 private function getFontsSectionContent(): string 107 { 108 $registeredStyles = $this->styleRegistry->getRegisteredStyles(); 109 110 $content = '<fonts count="'.\count($registeredStyles).'">'; 111 112 /** @var Style $style */ 113 foreach ($registeredStyles as $style) { 114 $content .= '<font>'; 115 116 $content .= '<sz val="'.$style->getFontSize().'"/>'; 117 $content .= '<color rgb="'.Color::toARGB($style->getFontColor()).'"/>'; 118 $content .= '<name val="'.$style->getFontName().'"/>'; 119 120 if ($style->isFontBold()) { 121 $content .= '<b/>'; 122 } 123 if ($style->isFontItalic()) { 124 $content .= '<i/>'; 125 } 126 if ($style->isFontUnderline()) { 127 $content .= '<u/>'; 128 } 129 if ($style->isFontStrikethrough()) { 130 $content .= '<strike/>'; 131 } 132 133 $content .= '</font>'; 134 } 135 136 $content .= '</fonts>'; 137 138 return $content; 139 } 140 141 /** 142 * Returns the content of the "<fills>" section. 143 */ 144 private function getFillsSectionContent(): string 145 { 146 $registeredFills = $this->styleRegistry->getRegisteredFills(); 147 148 // Excel reserves two default fills 149 $fillsCount = \count($registeredFills) + 2; 150 $content = sprintf('<fills count="%d">', $fillsCount); 151 152 $content .= '<fill><patternFill patternType="none"/></fill>'; 153 $content .= '<fill><patternFill patternType="gray125"/></fill>'; 154 155 // The other fills are actually registered by setting a background color 156 foreach ($registeredFills as $styleId) { 157 /** @var Style $style */ 158 $style = $this->styleRegistry->getStyleFromStyleId($styleId); 159 160 $backgroundColor = $style->getBackgroundColor(); 161 $content .= sprintf( 162 '<fill><patternFill patternType="solid"><fgColor rgb="%s"/></patternFill></fill>', 163 $backgroundColor 164 ); 165 } 166 167 $content .= '</fills>'; 168 169 return $content; 170 } 171 172 /** 173 * Returns the content of the "<borders>" section. 174 */ 175 private function getBordersSectionContent(): string 176 { 177 $registeredBorders = $this->styleRegistry->getRegisteredBorders(); 178 179 // There is one default border with index 0 180 $borderCount = \count($registeredBorders) + 1; 181 182 $content = '<borders count="'.$borderCount.'">'; 183 184 // Default border starting at index 0 185 $content .= '<border><left/><right/><top/><bottom/></border>'; 186 187 foreach ($registeredBorders as $styleId) { 188 $style = $this->styleRegistry->getStyleFromStyleId($styleId); 189 $border = $style->getBorder(); 190 \assert(null !== $border); 191 $content .= '<border>'; 192 193 // @see https://github.com/box/spout/issues/271 194 foreach (BorderPart::allowedNames as $partName) { 195 $content .= BorderHelper::serializeBorderPart($border->getPart($partName)); 196 } 197 198 $content .= '</border>'; 199 } 200 201 $content .= '</borders>'; 202 203 return $content; 204 } 205 206 /** 207 * Returns the content of the "<cellStyleXfs>" section. 208 */ 209 private function getCellStyleXfsSectionContent(): string 210 { 211 return <<<'EOD' 212 <cellStyleXfs count="1"> 213 <xf borderId="0" fillId="0" fontId="0" numFmtId="0"/> 214 </cellStyleXfs> 215 EOD; 216 } 217 218 /** 219 * Returns the content of the "<cellXfs>" section. 220 */ 221 private function getCellXfsSectionContent(): string 222 { 223 $registeredStyles = $this->styleRegistry->getRegisteredStyles(); 224 225 $content = '<cellXfs count="'.\count($registeredStyles).'">'; 226 227 foreach ($registeredStyles as $style) { 228 $styleId = $style->getId(); 229 $fillId = $this->getFillIdForStyleId($styleId); 230 $borderId = $this->getBorderIdForStyleId($styleId); 231 $numFmtId = $this->getFormatIdForStyleId($styleId); 232 233 $content .= '<xf numFmtId="'.$numFmtId.'" fontId="'.$styleId.'" fillId="'.$fillId.'" borderId="'.$borderId.'" xfId="0"'; 234 235 if ($style->shouldApplyFont()) { 236 $content .= ' applyFont="1"'; 237 } 238 239 $content .= sprintf(' applyBorder="%d"', (bool) $style->getBorder()); 240 241 if ($style->shouldApplyCellAlignment() || $style->shouldApplyCellVerticalAlignment() || $style->hasSetWrapText() || $style->shouldShrinkToFit()) { 242 $content .= ' applyAlignment="1">'; 243 $content .= '<alignment'; 244 if ($style->shouldApplyCellAlignment()) { 245 $content .= sprintf(' horizontal="%s"', $style->getCellAlignment()); 246 } 247 if ($style->shouldApplyCellVerticalAlignment()) { 248 $content .= sprintf(' vertical="%s"', $style->getCellVerticalAlignment()); 249 } 250 if ($style->hasSetWrapText()) { 251 $content .= ' wrapText="'.($style->shouldWrapText() ? '1' : '0').'"'; 252 } 253 if ($style->shouldShrinkToFit()) { 254 $content .= ' shrinkToFit="true"'; 255 } 256 257 $content .= '/>'; 258 $content .= '</xf>'; 259 } else { 260 $content .= '/>'; 261 } 262 } 263 264 $content .= '</cellXfs>'; 265 266 return $content; 267 } 268 269 /** 270 * Returns the content of the "<cellStyles>" section. 271 */ 272 private function getCellStylesSectionContent(): string 273 { 274 return <<<'EOD' 275 <cellStyles count="1"> 276 <cellStyle builtinId="0" name="Normal" xfId="0"/> 277 </cellStyles> 278 EOD; 279 } 280 281 /** 282 * Returns the fill ID associated to the given style ID. 283 * For the default style, we don't a fill. 284 */ 285 private function getFillIdForStyleId(int $styleId): int 286 { 287 // For the default style (ID = 0), we don't want to override the fill. 288 // Otherwise all cells of the spreadsheet will have a background color. 289 $isDefaultStyle = (0 === $styleId); 290 291 return $isDefaultStyle ? 0 : ($this->styleRegistry->getFillIdForStyleId($styleId) ?? 0); 292 } 293 294 /** 295 * Returns the fill ID associated to the given style ID. 296 * For the default style, we don't a border. 297 */ 298 private function getBorderIdForStyleId(int $styleId): int 299 { 300 // For the default style (ID = 0), we don't want to override the border. 301 // Otherwise all cells of the spreadsheet will have a border. 302 $isDefaultStyle = (0 === $styleId); 303 304 return $isDefaultStyle ? 0 : ($this->styleRegistry->getBorderIdForStyleId($styleId) ?? 0); 305 } 306 307 /** 308 * Returns the format ID associated to the given style ID. 309 * For the default style use general format. 310 */ 311 private function getFormatIdForStyleId(int $styleId): int 312 { 313 // For the default style (ID = 0), we don't want to override the format. 314 // Otherwise all cells of the spreadsheet will have a format. 315 $isDefaultStyle = (0 === $styleId); 316 317 return $isDefaultStyle ? 0 : ($this->styleRegistry->getFormatIdForStyleId($styleId) ?? 0); 318 } 319 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body