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