1<?php 2 3namespace PhpOffice\PhpSpreadsheet\Style; 4 5use PhpOffice\PhpSpreadsheet\Cell\Coordinate; 6use PhpOffice\PhpSpreadsheet\Spreadsheet; 7 8class Style extends Supervisor 9{ 10 /** 11 * Font. 12 * 13 * @var Font 14 */ 15 protected $font; 16 17 /** 18 * Fill. 19 * 20 * @var Fill 21 */ 22 protected $fill; 23 24 /** 25 * Borders. 26 * 27 * @var Borders 28 */ 29 protected $borders; 30 31 /** 32 * Alignment. 33 * 34 * @var Alignment 35 */ 36 protected $alignment; 37 38 /** 39 * Number Format. 40 * 41 * @var NumberFormat 42 */ 43 protected $numberFormat; 44 45 /** 46 * Protection. 47 * 48 * @var Protection 49 */ 50 protected $protection; 51 52 /** 53 * Index of style in collection. Only used for real style. 54 * 55 * @var int 56 */ 57 protected $index; 58 59 /** 60 * Use Quote Prefix when displaying in cell editor. Only used for real style. 61 * 62 * @var bool 63 */ 64 protected $quotePrefix = false; 65 66 /** 67 * Create a new Style. 68 * 69 * @param bool $isSupervisor Flag indicating if this is a supervisor or not 70 * Leave this value at default unless you understand exactly what 71 * its ramifications are 72 * @param bool $isConditional Flag indicating if this is a conditional style or not 73 * Leave this value at default unless you understand exactly what 74 * its ramifications are 75 */ 76 public function __construct($isSupervisor = false, $isConditional = false) 77 { 78 parent::__construct($isSupervisor); 79 80 // Initialise values 81 $this->font = new Font($isSupervisor, $isConditional); 82 $this->fill = new Fill($isSupervisor, $isConditional); 83 $this->borders = new Borders($isSupervisor); 84 $this->alignment = new Alignment($isSupervisor, $isConditional); 85 $this->numberFormat = new NumberFormat($isSupervisor, $isConditional); 86 $this->protection = new Protection($isSupervisor, $isConditional); 87 88 // bind parent if we are a supervisor 89 if ($isSupervisor) { 90 $this->font->bindParent($this); 91 $this->fill->bindParent($this); 92 $this->borders->bindParent($this); 93 $this->alignment->bindParent($this); 94 $this->numberFormat->bindParent($this); 95 $this->protection->bindParent($this); 96 } 97 } 98 99 /** 100 * Get the shared style component for the currently active cell in currently active sheet. 101 * Only used for style supervisor. 102 * 103 * @return Style 104 */ 105 public function getSharedComponent() 106 { 107 $activeSheet = $this->getActiveSheet(); 108 $selectedCell = $this->getActiveCell(); // e.g. 'A1' 109 110 if ($activeSheet->cellExists($selectedCell)) { 111 $xfIndex = $activeSheet->getCell($selectedCell)->getXfIndex(); 112 } else { 113 $xfIndex = 0; 114 } 115 116 return $this->parent->getCellXfByIndex($xfIndex); 117 } 118 119 /** 120 * Get parent. Only used for style supervisor. 121 * 122 * @return Spreadsheet 123 */ 124 public function getParent() 125 { 126 return $this->parent; 127 } 128 129 /** 130 * Build style array from subcomponents. 131 * 132 * @param array $array 133 * 134 * @return array 135 */ 136 public function getStyleArray($array) 137 { 138 return ['quotePrefix' => $array]; 139 } 140 141 /** 142 * Apply styles from array. 143 * 144 * <code> 145 * $spreadsheet->getActiveSheet()->getStyle('B2')->applyFromArray( 146 * [ 147 * 'font' => [ 148 * 'name' => 'Arial', 149 * 'bold' => true, 150 * 'italic' => false, 151 * 'underline' => Font::UNDERLINE_DOUBLE, 152 * 'strikethrough' => false, 153 * 'color' => [ 154 * 'rgb' => '808080' 155 * ] 156 * ], 157 * 'borders' => [ 158 * 'bottom' => [ 159 * 'borderStyle' => Border::BORDER_DASHDOT, 160 * 'color' => [ 161 * 'rgb' => '808080' 162 * ] 163 * ], 164 * 'top' => [ 165 * 'borderStyle' => Border::BORDER_DASHDOT, 166 * 'color' => [ 167 * 'rgb' => '808080' 168 * ] 169 * ] 170 * ], 171 * 'alignment' => [ 172 * 'horizontal' => Alignment::HORIZONTAL_CENTER, 173 * 'vertical' => Alignment::VERTICAL_CENTER, 174 * 'wrapText' => true, 175 * ], 176 * 'quotePrefix' => true 177 * ] 178 * ); 179 * </code> 180 * 181 * @param array $pStyles Array containing style information 182 * @param bool $pAdvanced advanced mode for setting borders 183 * 184 * @return $this 185 */ 186 public function applyFromArray(array $pStyles, $pAdvanced = true) 187 { 188 if ($this->isSupervisor) { 189 $pRange = $this->getSelectedCells(); 190 191 // Uppercase coordinate 192 $pRange = strtoupper($pRange); 193 194 // Is it a cell range or a single cell? 195 if (strpos($pRange, ':') === false) { 196 $rangeA = $pRange; 197 $rangeB = $pRange; 198 } else { 199 [$rangeA, $rangeB] = explode(':', $pRange); 200 } 201 202 // Calculate range outer borders 203 $rangeStart = Coordinate::coordinateFromString($rangeA); 204 $rangeEnd = Coordinate::coordinateFromString($rangeB); 205 $rangeStartIndexes = Coordinate::indexesFromString($rangeA); 206 $rangeEndIndexes = Coordinate::indexesFromString($rangeB); 207 208 $columnStart = $rangeStart[0]; 209 $columnEnd = $rangeEnd[0]; 210 211 // Make sure we can loop upwards on rows and columns 212 if ($rangeStartIndexes[0] > $rangeEndIndexes[0] && $rangeStartIndexes[1] > $rangeEndIndexes[1]) { 213 $tmp = $rangeStartIndexes; 214 $rangeStartIndexes = $rangeEndIndexes; 215 $rangeEndIndexes = $tmp; 216 } 217 218 // ADVANCED MODE: 219 if ($pAdvanced && isset($pStyles['borders'])) { 220 // 'allBorders' is a shorthand property for 'outline' and 'inside' and 221 // it applies to components that have not been set explicitly 222 if (isset($pStyles['borders']['allBorders'])) { 223 foreach (['outline', 'inside'] as $component) { 224 if (!isset($pStyles['borders'][$component])) { 225 $pStyles['borders'][$component] = $pStyles['borders']['allBorders']; 226 } 227 } 228 unset($pStyles['borders']['allBorders']); // not needed any more 229 } 230 // 'outline' is a shorthand property for 'top', 'right', 'bottom', 'left' 231 // it applies to components that have not been set explicitly 232 if (isset($pStyles['borders']['outline'])) { 233 foreach (['top', 'right', 'bottom', 'left'] as $component) { 234 if (!isset($pStyles['borders'][$component])) { 235 $pStyles['borders'][$component] = $pStyles['borders']['outline']; 236 } 237 } 238 unset($pStyles['borders']['outline']); // not needed any more 239 } 240 // 'inside' is a shorthand property for 'vertical' and 'horizontal' 241 // it applies to components that have not been set explicitly 242 if (isset($pStyles['borders']['inside'])) { 243 foreach (['vertical', 'horizontal'] as $component) { 244 if (!isset($pStyles['borders'][$component])) { 245 $pStyles['borders'][$component] = $pStyles['borders']['inside']; 246 } 247 } 248 unset($pStyles['borders']['inside']); // not needed any more 249 } 250 // width and height characteristics of selection, 1, 2, or 3 (for 3 or more) 251 $xMax = min($rangeEndIndexes[0] - $rangeStartIndexes[0] + 1, 3); 252 $yMax = min($rangeEndIndexes[1] - $rangeStartIndexes[1] + 1, 3); 253 254 // loop through up to 3 x 3 = 9 regions 255 for ($x = 1; $x <= $xMax; ++$x) { 256 // start column index for region 257 $colStart = ($x == 3) ? 258 Coordinate::stringFromColumnIndex($rangeEndIndexes[0]) 259 : Coordinate::stringFromColumnIndex($rangeStartIndexes[0] + $x - 1); 260 // end column index for region 261 $colEnd = ($x == 1) ? 262 Coordinate::stringFromColumnIndex($rangeStartIndexes[0]) 263 : Coordinate::stringFromColumnIndex($rangeEndIndexes[0] - $xMax + $x); 264 265 for ($y = 1; $y <= $yMax; ++$y) { 266 // which edges are touching the region 267 $edges = []; 268 if ($x == 1) { 269 // are we at left edge 270 $edges[] = 'left'; 271 } 272 if ($x == $xMax) { 273 // are we at right edge 274 $edges[] = 'right'; 275 } 276 if ($y == 1) { 277 // are we at top edge? 278 $edges[] = 'top'; 279 } 280 if ($y == $yMax) { 281 // are we at bottom edge? 282 $edges[] = 'bottom'; 283 } 284 285 // start row index for region 286 $rowStart = ($y == 3) ? 287 $rangeEndIndexes[1] : $rangeStartIndexes[1] + $y - 1; 288 289 // end row index for region 290 $rowEnd = ($y == 1) ? 291 $rangeStartIndexes[1] : $rangeEndIndexes[1] - $yMax + $y; 292 293 // build range for region 294 $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd; 295 296 // retrieve relevant style array for region 297 $regionStyles = $pStyles; 298 unset($regionStyles['borders']['inside']); 299 300 // what are the inner edges of the region when looking at the selection 301 $innerEdges = array_diff(['top', 'right', 'bottom', 'left'], $edges); 302 303 // inner edges that are not touching the region should take the 'inside' border properties if they have been set 304 foreach ($innerEdges as $innerEdge) { 305 switch ($innerEdge) { 306 case 'top': 307 case 'bottom': 308 // should pick up 'horizontal' border property if set 309 if (isset($pStyles['borders']['horizontal'])) { 310 $regionStyles['borders'][$innerEdge] = $pStyles['borders']['horizontal']; 311 } else { 312 unset($regionStyles['borders'][$innerEdge]); 313 } 314 315 break; 316 case 'left': 317 case 'right': 318 // should pick up 'vertical' border property if set 319 if (isset($pStyles['borders']['vertical'])) { 320 $regionStyles['borders'][$innerEdge] = $pStyles['borders']['vertical']; 321 } else { 322 unset($regionStyles['borders'][$innerEdge]); 323 } 324 325 break; 326 } 327 } 328 329 // apply region style to region by calling applyFromArray() in simple mode 330 $this->getActiveSheet()->getStyle($range)->applyFromArray($regionStyles, false); 331 } 332 } 333 334 // restore initial cell selection range 335 $this->getActiveSheet()->getStyle($pRange); 336 337 return $this; 338 } 339 340 // SIMPLE MODE: 341 // Selection type, inspect 342 if (preg_match('/^[A-Z]+1:[A-Z]+1048576$/', $pRange)) { 343 $selectionType = 'COLUMN'; 344 } elseif (preg_match('/^A\d+:XFD\d+$/', $pRange)) { 345 $selectionType = 'ROW'; 346 } else { 347 $selectionType = 'CELL'; 348 } 349 350 // First loop through columns, rows, or cells to find out which styles are affected by this operation 351 $oldXfIndexes = $this->getOldXfIndexes($selectionType, $rangeStartIndexes, $rangeEndIndexes, $columnStart, $columnEnd, $pStyles); 352 353 // clone each of the affected styles, apply the style array, and add the new styles to the workbook 354 $workbook = $this->getActiveSheet()->getParent(); 355 $newXfIndexes = []; 356 foreach ($oldXfIndexes as $oldXfIndex => $dummy) { 357 $style = $workbook->getCellXfByIndex($oldXfIndex); 358 $newStyle = clone $style; 359 $newStyle->applyFromArray($pStyles); 360 361 if ($existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode())) { 362 // there is already such cell Xf in our collection 363 $newXfIndexes[$oldXfIndex] = $existingStyle->getIndex(); 364 } else { 365 // we don't have such a cell Xf, need to add 366 $workbook->addCellXf($newStyle); 367 $newXfIndexes[$oldXfIndex] = $newStyle->getIndex(); 368 } 369 } 370 371 // Loop through columns, rows, or cells again and update the XF index 372 switch ($selectionType) { 373 case 'COLUMN': 374 for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) { 375 $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col); 376 $oldXfIndex = $columnDimension->getXfIndex(); 377 $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]); 378 } 379 380 break; 381 case 'ROW': 382 for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) { 383 $rowDimension = $this->getActiveSheet()->getRowDimension($row); 384 // row without explicit style should be formatted based on default style 385 $oldXfIndex = $rowDimension->getXfIndex() ?? 0; 386 $rowDimension->setXfIndex($newXfIndexes[$oldXfIndex]); 387 } 388 389 break; 390 case 'CELL': 391 for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) { 392 for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) { 393 $cell = $this->getActiveSheet()->getCellByColumnAndRow($col, $row); 394 $oldXfIndex = $cell->getXfIndex(); 395 $cell->setXfIndex($newXfIndexes[$oldXfIndex]); 396 } 397 } 398 399 break; 400 } 401 } else { 402 // not a supervisor, just apply the style array directly on style object 403 if (isset($pStyles['fill'])) { 404 $this->getFill()->applyFromArray($pStyles['fill']); 405 } 406 if (isset($pStyles['font'])) { 407 $this->getFont()->applyFromArray($pStyles['font']); 408 } 409 if (isset($pStyles['borders'])) { 410 $this->getBorders()->applyFromArray($pStyles['borders']); 411 } 412 if (isset($pStyles['alignment'])) { 413 $this->getAlignment()->applyFromArray($pStyles['alignment']); 414 } 415 if (isset($pStyles['numberFormat'])) { 416 $this->getNumberFormat()->applyFromArray($pStyles['numberFormat']); 417 } 418 if (isset($pStyles['protection'])) { 419 $this->getProtection()->applyFromArray($pStyles['protection']); 420 } 421 if (isset($pStyles['quotePrefix'])) { 422 $this->quotePrefix = $pStyles['quotePrefix']; 423 } 424 } 425 426 return $this; 427 } 428 429 private function getOldXfIndexes(string $selectionType, array $rangeStart, array $rangeEnd, string $columnStart, string $columnEnd, array $pStyles): array 430 { 431 $oldXfIndexes = []; 432 switch ($selectionType) { 433 case 'COLUMN': 434 for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { 435 $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true; 436 } 437 foreach ($this->getActiveSheet()->getColumnIterator($columnStart, $columnEnd) as $columnIterator) { 438 $cellIterator = $columnIterator->getCellIterator(); 439 $cellIterator->setIterateOnlyExistingCells(true); 440 foreach ($cellIterator as $columnCell) { 441 if ($columnCell !== null) { 442 $columnCell->getStyle()->applyFromArray($pStyles); 443 } 444 } 445 } 446 447 break; 448 case 'ROW': 449 for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { 450 if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() === null) { 451 $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style 452 } else { 453 $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true; 454 } 455 } 456 foreach ($this->getActiveSheet()->getRowIterator((int) $rangeStart[1], (int) $rangeEnd[1]) as $rowIterator) { 457 $cellIterator = $rowIterator->getCellIterator(); 458 $cellIterator->setIterateOnlyExistingCells(true); 459 foreach ($cellIterator as $rowCell) { 460 if ($rowCell !== null) { 461 $rowCell->getStyle()->applyFromArray($pStyles); 462 } 463 } 464 } 465 466 break; 467 case 'CELL': 468 for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { 469 for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { 470 $oldXfIndexes[$this->getActiveSheet()->getCellByColumnAndRow($col, $row)->getXfIndex()] = true; 471 } 472 } 473 474 break; 475 } 476 477 return $oldXfIndexes; 478 } 479 480 /** 481 * Get Fill. 482 * 483 * @return Fill 484 */ 485 public function getFill() 486 { 487 return $this->fill; 488 } 489 490 /** 491 * Get Font. 492 * 493 * @return Font 494 */ 495 public function getFont() 496 { 497 return $this->font; 498 } 499 500 /** 501 * Set font. 502 * 503 * @return $this 504 */ 505 public function setFont(Font $font) 506 { 507 $this->font = $font; 508 509 return $this; 510 } 511 512 /** 513 * Get Borders. 514 * 515 * @return Borders 516 */ 517 public function getBorders() 518 { 519 return $this->borders; 520 } 521 522 /** 523 * Get Alignment. 524 * 525 * @return Alignment 526 */ 527 public function getAlignment() 528 { 529 return $this->alignment; 530 } 531 532 /** 533 * Get Number Format. 534 * 535 * @return NumberFormat 536 */ 537 public function getNumberFormat() 538 { 539 return $this->numberFormat; 540 } 541 542 /** 543 * Get Conditional Styles. Only used on supervisor. 544 * 545 * @return Conditional[] 546 */ 547 public function getConditionalStyles() 548 { 549 return $this->getActiveSheet()->getConditionalStyles($this->getActiveCell()); 550 } 551 552 /** 553 * Set Conditional Styles. Only used on supervisor. 554 * 555 * @param Conditional[] $pValue Array of conditional styles 556 * 557 * @return $this 558 */ 559 public function setConditionalStyles(array $pValue) 560 { 561 $this->getActiveSheet()->setConditionalStyles($this->getSelectedCells(), $pValue); 562 563 return $this; 564 } 565 566 /** 567 * Get Protection. 568 * 569 * @return Protection 570 */ 571 public function getProtection() 572 { 573 return $this->protection; 574 } 575 576 /** 577 * Get quote prefix. 578 * 579 * @return bool 580 */ 581 public function getQuotePrefix() 582 { 583 if ($this->isSupervisor) { 584 return $this->getSharedComponent()->getQuotePrefix(); 585 } 586 587 return $this->quotePrefix; 588 } 589 590 /** 591 * Set quote prefix. 592 * 593 * @param bool $pValue 594 * 595 * @return $this 596 */ 597 public function setQuotePrefix($pValue) 598 { 599 if ($pValue == '') { 600 $pValue = false; 601 } 602 if ($this->isSupervisor) { 603 $styleArray = ['quotePrefix' => $pValue]; 604 $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); 605 } else { 606 $this->quotePrefix = (bool) $pValue; 607 } 608 609 return $this; 610 } 611 612 /** 613 * Get hash code. 614 * 615 * @return string Hash code 616 */ 617 public function getHashCode() 618 { 619 return md5( 620 $this->fill->getHashCode() . 621 $this->font->getHashCode() . 622 $this->borders->getHashCode() . 623 $this->alignment->getHashCode() . 624 $this->numberFormat->getHashCode() . 625 $this->protection->getHashCode() . 626 ($this->quotePrefix ? 't' : 'f') . 627 __CLASS__ 628 ); 629 } 630 631 /** 632 * Get own index in style collection. 633 * 634 * @return int 635 */ 636 public function getIndex() 637 { 638 return $this->index; 639 } 640 641 /** 642 * Set own index in style collection. 643 * 644 * @param int $pValue 645 */ 646 public function setIndex($pValue): void 647 { 648 $this->index = $pValue; 649 } 650 651 protected function exportArray1(): array 652 { 653 $exportedArray = []; 654 $this->exportArray2($exportedArray, 'alignment', $this->getAlignment()); 655 $this->exportArray2($exportedArray, 'borders', $this->getBorders()); 656 $this->exportArray2($exportedArray, 'fill', $this->getFill()); 657 $this->exportArray2($exportedArray, 'font', $this->getFont()); 658 $this->exportArray2($exportedArray, 'numberFormat', $this->getNumberFormat()); 659 $this->exportArray2($exportedArray, 'protection', $this->getProtection()); 660 $this->exportArray2($exportedArray, 'quotePrefx', $this->getQuotePrefix()); 661 662 return $exportedArray; 663 } 664} 665