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