| 1 |
<?php |
| 2 |
|
| 3 |
namespace Box\Spout\Writer\XLSX\Helper; |
| 4 |
|
| 5 |
use Box\Spout\Writer\Common\Helper\AbstractStyleHelper; |
| 6 |
use Box\Spout\Writer\Style\Color; |
| 7 |
use Box\Spout\Writer\Style\Style; |
| 8 |
|
| 9 |
/** |
| 10 |
* Class StyleHelper |
| 11 |
* This class provides helper functions to manage styles |
| 12 |
* |
| 13 |
* @package Box\Spout\Writer\XLSX\Helper |
| 14 |
*/ |
| 15 |
class StyleHelper extends AbstractStyleHelper |
| 16 |
{ |
| 17 |
/** |
| 18 |
* @var array |
| 19 |
*/ |
| 20 |
protected $registeredFills = []; |
| 21 |
|
| 22 |
/** |
| 23 |
* @var array [STYLE_ID] => [FILL_ID] maps a style to a fill declaration |
| 24 |
*/ |
| 25 |
protected $styleIdToFillMappingTable = []; |
| 26 |
|
| 27 |
/** |
| 28 |
* Excel preserves two default fills with index 0 and 1 |
| 29 |
* Since Excel is the dominant vendor - we play along here |
| 30 |
* |
| 31 |
* @var int The fill index counter for custom fills. |
| 32 |
*/ |
| 33 |
protected $fillIndex = 2; |
| 34 |
|
| 35 |
/** |
| 36 |
* @var array |
| 37 |
*/ |
| 38 |
protected $registeredBorders = []; |
| 39 |
|
| 40 |
/** |
| 41 |
* @var array [STYLE_ID] => [BORDER_ID] maps a style to a border declaration |
| 42 |
*/ |
| 43 |
protected $styleIdToBorderMappingTable = []; |
| 44 |
|
| 45 |
/** |
| 46 |
* XLSX specific operations on the registered styles |
| 47 |
* |
| 48 |
* @param \Box\Spout\Writer\Style\Style $style |
| 49 |
* @return \Box\Spout\Writer\Style\Style |
| 50 |
*/ |
| 51 |
public function registerStyle($style) |
| 52 |
{ |
| 53 |
$registeredStyle = parent::registerStyle($style); |
| 54 |
$this->registerFill($registeredStyle); |
| 55 |
$this->registerBorder($registeredStyle); |
| 56 |
return $registeredStyle; |
| 57 |
} |
| 58 |
|
| 59 |
/** |
| 60 |
* Register a fill definition |
| 61 |
* |
| 62 |
* @param \Box\Spout\Writer\Style\Style $style |
| 63 |
*/ |
| 64 |
protected function registerFill($style) |
| 65 |
{ |
| 66 |
$styleId = $style->getId(); |
| 67 |
|
| 68 |
// Currently - only solid backgrounds are supported |
| 69 |
// so $backgroundColor is a scalar value (RGB Color) |
| 70 |
$backgroundColor = $style->getBackgroundColor(); |
| 71 |
|
| 72 |
if ($backgroundColor) { |
| 73 |
$isBackgroundColorRegistered = isset($this->registeredFills[$backgroundColor]); |
| 74 |
|
| 75 |
// We need to track the already registered background definitions |
| 76 |
if ($isBackgroundColorRegistered) { |
| 77 |
$registeredStyleId = $this->registeredFills[$backgroundColor]; |
| 78 |
$registeredFillId = $this->styleIdToFillMappingTable[$registeredStyleId]; |
| 79 |
$this->styleIdToFillMappingTable[$styleId] = $registeredFillId; |
| 80 |
} else { |
| 81 |
$this->registeredFills[$backgroundColor] = $styleId; |
| 82 |
$this->styleIdToFillMappingTable[$styleId] = $this->fillIndex++; |
| 83 |
} |
| 84 |
|
| 85 |
} else { |
| 86 |
// The fillId maps a style to a fill declaration |
| 87 |
// When there is no background color definition - we default to 0 |
| 88 |
$this->styleIdToFillMappingTable[$styleId] = 0; |
| 89 |
} |
| 90 |
} |
| 91 |
|
| 92 |
/** |
| 93 |
* Register a border definition |
| 94 |
* |
| 95 |
* @param \Box\Spout\Writer\Style\Style $style |
| 96 |
*/ |
| 97 |
protected function registerBorder($style) |
| 98 |
{ |
| 99 |
$styleId = $style->getId(); |
| 100 |
|
| 101 |
if ($style->shouldApplyBorder()) { |
| 102 |
$border = $style->getBorder(); |
| 103 |
$serializedBorder = serialize($border); |
| 104 |
|
| 105 |
$isBorderAlreadyRegistered = isset($this->registeredBorders[$serializedBorder]); |
| 106 |
|
| 107 |
if ($isBorderAlreadyRegistered) { |
| 108 |
$registeredStyleId = $this->registeredBorders[$serializedBorder]; |
| 109 |
$registeredBorderId = $this->styleIdToBorderMappingTable[$registeredStyleId]; |
| 110 |
$this->styleIdToBorderMappingTable[$styleId] = $registeredBorderId; |
| 111 |
} else { |
| 112 |
$this->registeredBorders[$serializedBorder] = $styleId; |
| 113 |
$this->styleIdToBorderMappingTable[$styleId] = count($this->registeredBorders); |
| 114 |
} |
| 115 |
|
| 116 |
} else { |
| 117 |
// If no border should be applied - the mapping is the default border: 0 |
| 118 |
$this->styleIdToBorderMappingTable[$styleId] = 0; |
| 119 |
} |
| 120 |
} |
| 121 |
|
| 122 |
|
| 123 |
/** |
| 124 |
* For empty cells, we can specify a style or not. If no style are specified, |
| 125 |
* then the software default will be applied. But sometimes, it may be useful |
| 126 |
* to override this default style, for instance if the cell should have a |
| 127 |
* background color different than the default one or some borders |
| 128 |
* (fonts property don't really matter here). |
| 129 |
* |
| 130 |
* @param int $styleId |
| 131 |
* @return bool Whether the cell should define a custom style |
| 132 |
*/ |
| 133 |
public function shouldApplyStyleOnEmptyCell($styleId) |
| 134 |
{ |
| 135 |
$hasStyleCustomFill = (isset($this->styleIdToFillMappingTable[$styleId]) && $this->styleIdToFillMappingTable[$styleId] !== 0); |
| 136 |
$hasStyleCustomBorders = (isset($this->styleIdToBorderMappingTable[$styleId]) && $this->styleIdToBorderMappingTable[$styleId] !== 0); |
| 137 |
|
| 138 |
return ($hasStyleCustomFill || $hasStyleCustomBorders); |
| 139 |
} |
| 140 |
|
| 141 |
|
| 142 |
/** |
| 143 |
* Returns the content of the "styles.xml" file, given a list of styles. |
| 144 |
* |
| 145 |
* @return string |
| 146 |
*/ |
| 147 |
public function getStylesXMLFileContent() |
| 148 |
{ |
| 149 |
$content = <<<EOD |
| 150 |
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
| 151 |
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> |
| 152 |
EOD; |
| 153 |
|
| 154 |
$content .= $this->getFontsSectionContent(); |
| 155 |
$content .= $this->getFillsSectionContent(); |
| 156 |
$content .= $this->getBordersSectionContent(); |
| 157 |
$content .= $this->getCellStyleXfsSectionContent(); |
| 158 |
$content .= $this->getCellXfsSectionContent(); |
| 159 |
$content .= $this->getCellStylesSectionContent(); |
| 160 |
|
| 161 |
$content .= <<<EOD |
| 162 |
</styleSheet> |
| 163 |
EOD; |
| 164 |
|
| 165 |
return $content; |
| 166 |
} |
| 167 |
|
| 168 |
/** |
| 169 |
* Returns the content of the "<fonts>" section. |
| 170 |
* |
| 171 |
* @return string |
| 172 |
*/ |
| 173 |
protected function getFontsSectionContent() |
| 174 |
{ |
| 175 |
$content = '<fonts count="' . count($this->styleIdToStyleMappingTable) . '">'; |
| 176 |
|
| 177 |
/** @var \Box\Spout\Writer\Style\Style $style */ |
| 178 |
foreach ($this->getRegisteredStyles() as $style) { |
| 179 |
$content .= '<font>'; |
| 180 |
|
| 181 |
$content .= '<sz val="' . $style->getFontSize() . '"/>'; |
| 182 |
$content .= '<color rgb="' . Color::toARGB($style->getFontColor()) . '"/>'; |
| 183 |
$content .= '<name val="' . $style->getFontName() . '"/>'; |
| 184 |
|
| 185 |
if ($style->isFontBold()) { |
| 186 |
$content .= '<b/>'; |
| 187 |
} |
| 188 |
if ($style->isFontItalic()) { |
| 189 |
$content .= '<i/>'; |
| 190 |
} |
| 191 |
if ($style->isFontUnderline()) { |
| 192 |
$content .= '<u/>'; |
| 193 |
} |
| 194 |
if ($style->isFontStrikethrough()) { |
| 195 |
$content .= '<strike/>'; |
| 196 |
} |
| 197 |
|
| 198 |
$content .= '</font>'; |
| 199 |
} |
| 200 |
|
| 201 |
$content .= '</fonts>'; |
| 202 |
|
| 203 |
return $content; |
| 204 |
} |
| 205 |
|
| 206 |
/** |
| 207 |
* Returns the content of the "<fills>" section. |
| 208 |
* |
| 209 |
* @return string |
| 210 |
*/ |
| 211 |
protected function getFillsSectionContent() |
| 212 |
{ |
| 213 |
// Excel reserves two default fills |
| 214 |
$fillsCount = count($this->registeredFills) + 2; |
| 215 |
$content = sprintf('<fills count="%d">', $fillsCount); |
| 216 |
|
| 217 |
$content .= '<fill><patternFill patternType="none"/></fill>'; |
| 218 |
$content .= '<fill><patternFill patternType="gray125"/></fill>'; |
| 219 |
|
| 220 |
// The other fills are actually registered by setting a background color |
| 221 |
foreach ($this->registeredFills as $styleId) { |
| 222 |
/** @var Style $style */ |
| 223 |
$style = $this->styleIdToStyleMappingTable[$styleId]; |
| 224 |
|
| 225 |
$backgroundColor = $style->getBackgroundColor(); |
| 226 |
$content .= sprintf( |
| 227 |
'<fill><patternFill patternType="solid"><fgColor rgb="%s"/></patternFill></fill>', |
| 228 |
$backgroundColor |
| 229 |
); |
| 230 |
} |
| 231 |
|
| 232 |
$content .= '</fills>'; |
| 233 |
|
| 234 |
return $content; |
| 235 |
} |
| 236 |
|
| 237 |
/** |
| 238 |
* Returns the content of the "<borders>" section. |
| 239 |
* |
| 240 |
* @return string |
| 241 |
*/ |
| 242 |
protected function getBordersSectionContent() |
| 243 |
{ |
| 244 |
|
| 245 |
// There is one default border with index 0 |
| 246 |
$borderCount = count($this->registeredBorders) + 1; |
| 247 |
|
| 248 |
$content = '<borders count="' . $borderCount . '">'; |
| 249 |
|
| 250 |
// Default border starting at index 0 |
| 251 |
$content .= '<border><left/><right/><top/><bottom/></border>'; |
| 252 |
|
| 253 |
foreach ($this->registeredBorders as $styleId) { |
| 254 |
/** @var \Box\Spout\Writer\Style\Style $style */ |
| 255 |
$style = $this->styleIdToStyleMappingTable[$styleId]; |
| 256 |
$border = $style->getBorder(); |
| 257 |
$content .= '<border>'; |
| 258 |
|
| 259 |
// @link https://github.com/box/spout/issues/271 |
| 260 |
$sortOrder = ['left', 'right', 'top', 'bottom']; |
| 261 |
|
| 262 |
foreach ($sortOrder as $partName) { |
| 263 |
if ($border->hasPart($partName)) { |
| 264 |
/** @var $part \Box\Spout\Writer\Style\BorderPart */ |
| 265 |
$part = $border->getPart($partName); |
| 266 |
$content .= BorderHelper::serializeBorderPart($part); |
| 267 |
} |
| 268 |
|
| 269 |
} |
| 270 |
|
| 271 |
$content .= '</border>'; |
| 272 |
} |
| 273 |
|
| 274 |
$content .= '</borders>'; |
| 275 |
|
| 276 |
return $content; |
| 277 |
} |
| 278 |
|
| 279 |
/** |
| 280 |
* Returns the content of the "<cellStyleXfs>" section. |
| 281 |
* |
| 282 |
* @return string |
| 283 |
*/ |
| 284 |
protected function getCellStyleXfsSectionContent() |
| 285 |
{ |
| 286 |
return <<<EOD |
| 287 |
<cellStyleXfs count="1"> |
| 288 |
<xf borderId="0" fillId="0" fontId="0" numFmtId="0"/> |
| 289 |
</cellStyleXfs> |
| 290 |
EOD; |
| 291 |
} |
| 292 |
|
| 293 |
/** |
| 294 |
* Returns the content of the "<cellXfs>" section. |
| 295 |
* |
| 296 |
* @return string |
| 297 |
*/ |
| 298 |
protected function getCellXfsSectionContent() |
| 299 |
{ |
| 300 |
$registeredStyles = $this->getRegisteredStyles(); |
| 301 |
|
| 302 |
$content = '<cellXfs count="' . count($registeredStyles) . '">'; |
| 303 |
|
| 304 |
foreach ($registeredStyles as $style) { |
| 305 |
$styleId = $style->getId(); |
| 306 |
$fillId = $this->styleIdToFillMappingTable[$styleId]; |
| 307 |
$borderId = $this->styleIdToBorderMappingTable[$styleId]; |
| 308 |
|
| 309 |
$content .= '<xf numFmtId="0" fontId="' . $styleId . '" fillId="' . $fillId . '" borderId="' . $borderId . '" xfId="0"'; |
| 310 |
|
| 311 |
if ($style->shouldApplyFont()) { |
| 312 |
$content .= ' applyFont="1"'; |
| 313 |
} |
| 314 |
|
| 315 |
$content .= sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0); |
| 316 |
|
| 317 |
if ($style->shouldWrapText()) { |
| 318 |
$content .= ' applyAlignment="1">'; |
| 319 |
$content .= '<alignment wrapText="1"/>'; |
| 320 |
$content .= '</xf>'; |
| 321 |
} else { |
| 322 |
$content .= '/>'; |
| 323 |
} |
| 324 |
} |
| 325 |
|
| 326 |
$content .= '</cellXfs>'; |
| 327 |
|
| 328 |
return $content; |
| 329 |
} |
| 330 |
|
| 331 |
/** |
| 332 |
* Returns the content of the "<cellStyles>" section. |
| 333 |
* |
| 334 |
* @return string |
| 335 |
*/ |
| 336 |
protected function getCellStylesSectionContent() |
| 337 |
{ |
| 338 |
return <<<EOD |
| 339 |
<cellStyles count="1"> |
| 340 |
<cellStyle builtinId="0" name="Normal" xfId="0"/> |
| 341 |
</cellStyles> |
| 342 |
EOD; |
| 343 |
} |
| 344 |
} |
| 345 |
|