Passed
Pull Request — master (#3834)
by Shinji
21:30
created

Worksheet::buildNullRow()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 13.575

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 18
ccs 3
cts 10
cp 0.3
rs 9.6111
cc 5
nc 4
nop 5
crap 13.575
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4
5
use ArrayObject;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
8
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
9
use PhpOffice\PhpSpreadsheet\Cell\Cell;
10
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
11
use PhpOffice\PhpSpreadsheet\Cell\CellRange;
12
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
13
use PhpOffice\PhpSpreadsheet\Cell\DataType;
14
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
15
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
16
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
17
use PhpOffice\PhpSpreadsheet\Chart\Chart;
18
use PhpOffice\PhpSpreadsheet\Collection\Cells;
19
use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
20
use PhpOffice\PhpSpreadsheet\Comment;
21
use PhpOffice\PhpSpreadsheet\DefinedName;
22
use PhpOffice\PhpSpreadsheet\Exception;
23
use PhpOffice\PhpSpreadsheet\IComparable;
24
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
25
use PhpOffice\PhpSpreadsheet\RichText\RichText;
26
use PhpOffice\PhpSpreadsheet\Shared;
27
use PhpOffice\PhpSpreadsheet\Spreadsheet;
28
use PhpOffice\PhpSpreadsheet\Style\Alignment;
29
use PhpOffice\PhpSpreadsheet\Style\Color;
30
use PhpOffice\PhpSpreadsheet\Style\Conditional;
31
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
32
use PhpOffice\PhpSpreadsheet\Style\Protection as StyleProtection;
33
use PhpOffice\PhpSpreadsheet\Style\Style;
34
35
class Worksheet implements IComparable
36
{
37
    // Break types
38
    public const BREAK_NONE = 0;
39
    public const BREAK_ROW = 1;
40
    public const BREAK_COLUMN = 2;
41
    // Maximum column for row break
42
    public const BREAK_ROW_MAX_COLUMN = 16383;
43
44
    // Sheet state
45
    public const SHEETSTATE_VISIBLE = 'visible';
46
    public const SHEETSTATE_HIDDEN = 'hidden';
47
    public const SHEETSTATE_VERYHIDDEN = 'veryHidden';
48
49
    public const MERGE_CELL_CONTENT_EMPTY = 'empty';
50
    public const MERGE_CELL_CONTENT_HIDE = 'hide';
51
    public const MERGE_CELL_CONTENT_MERGE = 'merge';
52
53
    protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui';
54
55
    /**
56
     * Maximum 31 characters allowed for sheet title.
57
     *
58
     * @var int
59
     */
60
    const SHEET_TITLE_MAXIMUM_LENGTH = 31;
61
62
    /**
63
     * Invalid characters in sheet title.
64
     *
65
     * @var array
66
     */
67
    private static $invalidCharacters = ['*', ':', '/', '\\', '?', '[', ']'];
68
69
    /**
70
     * Parent spreadsheet.
71
     */
72
    private ?Spreadsheet $parent;
73
74
    /**
75
     * Collection of cells.
76
     */
77
    private ?Cells $cellCollection;
78
79
    /**
80
     * Collection of row dimensions.
81
     *
82
     * @var RowDimension[]
83
     */
84
    private $rowDimensions = [];
85
86
    /**
87
     * Default row dimension.
88
     */
89
    private RowDimension $defaultRowDimension;
90
91
    /**
92
     * Collection of column dimensions.
93
     *
94
     * @var ColumnDimension[]
95
     */
96
    private $columnDimensions = [];
97
98
    /**
99
     * Default column dimension.
100
     */
101
    private ColumnDimension $defaultColumnDimension;
102
103
    /**
104
     * Collection of drawings.
105
     *
106
     * @var ArrayObject<int, BaseDrawing>
107
     */
108
    private ArrayObject $drawingCollection;
109
110
    /**
111
     * Collection of Chart objects.
112
     *
113
     * @var ArrayObject<int, Chart>
114
     */
115
    private ArrayObject $chartCollection;
116
117
    /**
118
     * Collection of Table objects.
119
     *
120
     * @var ArrayObject<int, Table>
121
     */
122
    private ArrayObject $tableCollection;
123
124
    /**
125
     * Worksheet title.
126
     *
127
     * @var string
128
     */
129
    private $title;
130
131
    /**
132
     * Sheet state.
133
     *
134
     * @var string
135
     */
136
    private $sheetState;
137
138
    /**
139
     * Page setup.
140
     */
141
    private PageSetup $pageSetup;
142
143
    /**
144
     * Page margins.
145
     */
146
    private PageMargins $pageMargins;
147
148
    /**
149
     * Page header/footer.
150
     */
151
    private HeaderFooter $headerFooter;
152
153
    /**
154
     * Sheet view.
155
     */
156
    private SheetView $sheetView;
157
158
    /**
159
     * Protection.
160
     */
161
    private Protection $protection;
162
163
    /**
164
     * Collection of styles.
165
     *
166
     * @var Style[]
167
     */
168
    private $styles = [];
169
170
    /**
171
     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
172
     *
173
     * @var array
174
     */
175
    private $conditionalStylesCollection = [];
176
177
    /**
178
     * Collection of row breaks.
179
     *
180
     * @var PageBreak[]
181
     */
182
    private $rowBreaks = [];
183
184
    /**
185
     * Collection of column breaks.
186
     *
187
     * @var PageBreak[]
188
     */
189
    private $columnBreaks = [];
190
191
    /**
192
     * Collection of merged cell ranges.
193
     *
194
     * @var string[]
195
     */
196
    private $mergeCells = [];
197
198
    /**
199
     * Collection of protected cell ranges.
200
     *
201
     * @var string[]
202
     */
203
    private $protectedCells = [];
204
205
    /**
206
     * Autofilter Range and selection.
207
     */
208
    private AutoFilter $autoFilter;
209
210
    /**
211
     * Freeze pane.
212
     *
213
     * @var null|string
214
     */
215
    private $freezePane;
216
217
    /**
218
     * Default position of the right bottom pane.
219
     *
220
     * @var null|string
221
     */
222
    private $topLeftCell;
223
224
    private string $paneTopLeftCell = '';
225
226
    private string $activePane = '';
227
228
    private int $xSplit = 0;
229
230
    private int $ySplit = 0;
231
232
    private string $paneState = '';
233
234
    /**
235
     * Properties of the 4 panes.
236
     *
237
     * @var (null|Pane)[]
238
     */
239
    private $panes = [
240
        'bottomRight' => null,
241
        'bottomLeft' => null,
242
        'topRight' => null,
243
        'topLeft' => null,
244
    ];
245
246
    /**
247
     * Show gridlines?
248
     *
249
     * @var bool
250
     */
251
    private $showGridlines = true;
252
253
    /**
254
     * Print gridlines?
255
     *
256
     * @var bool
257
     */
258
    private $printGridlines = false;
259
260
    /**
261
     * Show row and column headers?
262
     *
263
     * @var bool
264
     */
265
    private $showRowColHeaders = true;
266
267
    /**
268
     * Show summary below? (Row/Column outline).
269
     *
270
     * @var bool
271
     */
272
    private $showSummaryBelow = true;
273
274
    /**
275
     * Show summary right? (Row/Column outline).
276
     *
277
     * @var bool
278
     */
279
    private $showSummaryRight = true;
280
281
    /**
282
     * Collection of comments.
283
     *
284
     * @var Comment[]
285
     */
286
    private $comments = [];
287
288
    /**
289
     * Active cell. (Only one!).
290
     *
291
     * @var string
292
     */
293
    private $activeCell = 'A1';
294
295
    /**
296
     * Selected cells.
297
     *
298
     * @var string
299
     */
300
    private $selectedCells = 'A1';
301
302
    /**
303
     * Cached highest column.
304
     *
305
     * @var int
306
     */
307
    private $cachedHighestColumn = 1;
308
309
    /**
310
     * Cached highest row.
311
     *
312
     * @var int
313
     */
314
    private $cachedHighestRow = 1;
315
316
    /**
317
     * Right-to-left?
318
     *
319
     * @var bool
320
     */
321
    private $rightToLeft = false;
322
323
    /**
324
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
325
     *
326
     * @var array
327
     */
328
    private $hyperlinkCollection = [];
329
330
    /**
331
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
332
     *
333
     * @var array
334
     */
335
    private $dataValidationCollection = [];
336
337
    /**
338
     * Tab color.
339
     *
340
     * @var null|Color
341
     */
342
    private $tabColor;
343
344
    /**
345
     * Dirty flag.
346
     *
347
     * @var bool
348
     */
349
    private $dirty = true;
350
351
    /**
352
     * Hash.
353
     *
354
     * @var string
355
     */
356
    private $hash;
357
358
    /**
359
     * CodeName.
360
     *
361
     * @var string
362
     */
363
    private $codeName;
364
365
    /**
366
     * Create a new worksheet.
367
     *
368
     * @param string $title
369
     */
370 9921
    public function __construct(?Spreadsheet $parent = null, $title = 'Worksheet')
371
    {
372
        // Set parent and title
373 9921
        $this->parent = $parent;
374 9921
        $this->setTitle($title, false);
375
        // setTitle can change $pTitle
376 9921
        $this->setCodeName($this->getTitle());
377 9921
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
378
379 9921
        $this->cellCollection = CellsFactory::getInstance($this);
380
        // Set page setup
381 9921
        $this->pageSetup = new PageSetup();
382
        // Set page margins
383 9921
        $this->pageMargins = new PageMargins();
384
        // Set page header/footer
385 9921
        $this->headerFooter = new HeaderFooter();
386
        // Set sheet view
387 9921
        $this->sheetView = new SheetView();
388
        // Drawing collection
389 9921
        $this->drawingCollection = new ArrayObject();
390
        // Chart collection
391 9921
        $this->chartCollection = new ArrayObject();
392
        // Protection
393 9921
        $this->protection = new Protection();
394
        // Default row dimension
395 9921
        $this->defaultRowDimension = new RowDimension(null);
396
        // Default column dimension
397 9921
        $this->defaultColumnDimension = new ColumnDimension(null);
398
        // AutoFilter
399 9921
        $this->autoFilter = new AutoFilter('', $this);
400
        // Table collection
401 9921
        $this->tableCollection = new ArrayObject();
402
    }
403
404
    /**
405
     * Disconnect all cells from this Worksheet object,
406
     * typically so that the worksheet object can be unset.
407
     */
408 8457
    public function disconnectCells(): void
409
    {
410 8457
        if ($this->cellCollection !== null) {
411 8457
            $this->cellCollection->unsetWorksheetCells();
412 8457
            $this->cellCollection = null;
413
        }
414
        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
415 8457
        $this->parent = null;
416
    }
417
418
    /**
419
     * Code to execute when this worksheet is unset().
420
     */
421 121
    public function __destruct()
422
    {
423 121
        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
424
425 121
        $this->disconnectCells();
426 121
        unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->drawingCollection, $this->chartCollection, $this->autoFilter);
427
    }
428
429
    /**
430
     * Return the cell collection.
431
     *
432
     * @return Cells
433
     */
434 9625
    public function getCellCollection()
435
    {
436
        // @phpstan-ignore-next-line
437 9625
        return $this->cellCollection;
438
    }
439
440
    /**
441
     * Get array of invalid characters for sheet title.
442
     *
443
     * @return array
444
     */
445 1
    public static function getInvalidCharacters()
446
    {
447 1
        return self::$invalidCharacters;
448
    }
449
450
    /**
451
     * Check sheet code name for valid Excel syntax.
452
     *
453
     * @param string $sheetCodeName The string to check
454
     *
455
     * @return string The valid string
456
     */
457 9921
    private static function checkSheetCodeName($sheetCodeName): string
458
    {
459 9921
        $charCount = Shared\StringHelper::countCharacters($sheetCodeName);
460 9921
        if ($charCount == 0) {
461 1
            throw new Exception('Sheet code name cannot be empty.');
462
        }
463
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
464
        if (
465 9921
            (str_replace(self::$invalidCharacters, '', $sheetCodeName) !== $sheetCodeName)
466 9921
            || (Shared\StringHelper::substring($sheetCodeName, -1, 1) == '\'')
467 9921
            || (Shared\StringHelper::substring($sheetCodeName, 0, 1) == '\'')
468
        ) {
469 1
            throw new Exception('Invalid character found in sheet code name');
470
        }
471
472
        // Enforce maximum characters allowed for sheet title
473 9921
        if ($charCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
474 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
475
        }
476
477 9921
        return $sheetCodeName;
478
    }
479
480
    /**
481
     * Check sheet title for valid Excel syntax.
482
     *
483
     * @param string $sheetTitle The string to check
484
     *
485
     * @return string The valid string
486
     */
487 9921
    private static function checkSheetTitle($sheetTitle): string
488
    {
489
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
490 9921
        if (str_replace(self::$invalidCharacters, '', $sheetTitle) !== $sheetTitle) {
491 1
            throw new Exception('Invalid character found in sheet title');
492
        }
493
494
        // Enforce maximum characters allowed for sheet title
495 9921
        if (Shared\StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
496 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
497
        }
498
499 9921
        return $sheetTitle;
500
    }
501
502
    /**
503
     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
504
     *
505
     * @param bool $sorted Also sort the cell collection?
506
     *
507
     * @return string[]
508
     */
509 1204
    public function getCoordinates($sorted = true): array
510
    {
511 1204
        if ($this->cellCollection == null) {
512 1
            return [];
513
        }
514
515 1203
        if ($sorted) {
516 411
            return $this->cellCollection->getSortedCoordinates();
517
        }
518
519 1119
        return $this->cellCollection->getCoordinates();
520
    }
521
522
    /**
523
     * Get collection of row dimensions.
524
     *
525
     * @return RowDimension[]
526
     */
527 947
    public function getRowDimensions()
528
    {
529 947
        return $this->rowDimensions;
530
    }
531
532
    /**
533
     * Get default row dimension.
534
     *
535
     * @return RowDimension
536
     */
537 917
    public function getDefaultRowDimension()
538
    {
539 917
        return $this->defaultRowDimension;
540
    }
541
542
    /**
543
     * Get collection of column dimensions.
544
     *
545
     * @return ColumnDimension[]
546
     */
547 952
    public function getColumnDimensions()
548
    {
549
        /** @var callable */
550 952
        $callable = [self::class, 'columnDimensionCompare'];
551 952
        uasort($this->columnDimensions, $callable);
552
553 952
        return $this->columnDimensions;
554
    }
555
556 74
    private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int
557
    {
558 74
        return $a->getColumnNumeric() - $b->getColumnNumeric();
559
    }
560
561
    /**
562
     * Get default column dimension.
563
     *
564
     * @return ColumnDimension
565
     */
566 440
    public function getDefaultColumnDimension()
567
    {
568 440
        return $this->defaultColumnDimension;
569
    }
570
571
    /**
572
     * Get collection of drawings.
573
     *
574
     * @return ArrayObject<int, BaseDrawing>
575
     */
576 934
    public function getDrawingCollection()
577
    {
578 934
        return $this->drawingCollection;
579
    }
580
581
    /**
582
     * Get collection of charts.
583
     *
584
     * @return ArrayObject<int, Chart>
585
     */
586 92
    public function getChartCollection()
587
    {
588 92
        return $this->chartCollection;
589
    }
590
591 98
    public function addChart(Chart $chart): Chart
592
    {
593 98
        $chart->setWorksheet($this);
594 98
        $this->chartCollection[] = $chart;
595
596 98
        return $chart;
597
    }
598
599
    /**
600
     * Return the count of charts on this worksheet.
601
     *
602
     * @return int The number of charts
603
     */
604 75
    public function getChartCount(): int
605
    {
606 75
        return count($this->chartCollection);
607
    }
608
609
    /**
610
     * Get a chart by its index position.
611
     *
612
     * @param ?string $index Chart index position
613
     *
614
     * @return Chart|false
615
     */
616 69
    public function getChartByIndex($index)
617
    {
618 69
        $chartCount = count($this->chartCollection);
619 69
        if ($chartCount == 0) {
620
            return false;
621
        }
622 69
        if ($index === null) {
623
            $index = --$chartCount;
624
        }
625 69
        if (!isset($this->chartCollection[$index])) {
626
            return false;
627
        }
628
629 69
        return $this->chartCollection[$index];
630
    }
631
632
    /**
633
     * Return an array of the names of charts on this worksheet.
634
     *
635
     * @return string[] The names of charts
636
     */
637 6
    public function getChartNames(): array
638
    {
639 6
        $chartNames = [];
640 6
        foreach ($this->chartCollection as $chart) {
641 6
            $chartNames[] = $chart->getName();
642
        }
643
644 6
        return $chartNames;
645
    }
646
647
    /**
648
     * Get a chart by name.
649
     *
650
     * @param string $chartName Chart name
651
     *
652
     * @return Chart|false
653
     */
654 7
    public function getChartByName($chartName)
655
    {
656 7
        foreach ($this->chartCollection as $index => $chart) {
657 7
            if ($chart->getName() == $chartName) {
658 7
                return $chart;
659
            }
660
        }
661
662 1
        return false;
663
    }
664
665 7
    public function getChartByNameOrThrow(string $chartName): Chart
666
    {
667 7
        $chart = $this->getChartByName($chartName);
668 7
        if ($chart !== false) {
669 7
            return $chart;
670
        }
671
672 1
        throw new Exception("Sheet does not have a chart named $chartName.");
673
    }
674
675
    /**
676
     * Refresh column dimensions.
677
     *
678
     * @return $this
679
     */
680 24
    public function refreshColumnDimensions(): static
681
    {
682 24
        $newColumnDimensions = [];
683 24
        foreach ($this->getColumnDimensions() as $objColumnDimension) {
684 24
            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
685
        }
686
687 24
        $this->columnDimensions = $newColumnDimensions;
688
689 24
        return $this;
690
    }
691
692
    /**
693
     * Refresh row dimensions.
694
     *
695
     * @return $this
696
     */
697 6
    public function refreshRowDimensions(): static
698
    {
699 6
        $newRowDimensions = [];
700 6
        foreach ($this->getRowDimensions() as $objRowDimension) {
701 6
            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
702
        }
703
704 6
        $this->rowDimensions = $newRowDimensions;
705
706 6
        return $this;
707
    }
708
709
    /**
710
     * Calculate worksheet dimension.
711
     *
712
     * @return string String containing the dimension of this worksheet
713
     */
714 368
    public function calculateWorksheetDimension(): string
715
    {
716
        // Return
717 368
        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
718
    }
719
720
    /**
721
     * Calculate worksheet data dimension.
722
     *
723
     * @return string String containing the dimension of this worksheet that actually contain data
724
     */
725 480
    public function calculateWorksheetDataDimension(): string
726
    {
727
        // Return
728 480
        return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
729
    }
730
731
    /**
732
     * Calculate widths for auto-size columns.
733
     *
734
     * @return $this
735
     */
736 647
    public function calculateColumnWidths(): static
737
    {
738
        // initialize $autoSizes array
739 647
        $autoSizes = [];
740 647
        foreach ($this->getColumnDimensions() as $colDimension) {
741 113
            if ($colDimension->getAutoSize()) {
742 51
                $autoSizes[$colDimension->getColumnIndex()] = -1;
743
            }
744
        }
745
746
        // There is only something to do if there are some auto-size columns
747 647
        if (!empty($autoSizes)) {
748
            // build list of cells references that participate in a merge
749 51
            $isMergeCell = [];
750 51
            foreach ($this->getMergeCells() as $cells) {
751 16
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
752 16
                    $isMergeCell[$cellReference] = true;
753
                }
754
            }
755
756 51
            $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
757
758
            // loop through all cells in the worksheet
759 51
            foreach ($this->getCoordinates(false) as $coordinate) {
760 51
                $cell = $this->getCellOrNull($coordinate);
761
762 51
                if ($cell !== null && isset($autoSizes[$this->getCellCollection()->getCurrentColumn()])) {
763
                    //Determine if cell is in merge range
764 51
                    $isMerged = isset($isMergeCell[$this->getCellCollection()->getCurrentCoordinate()]);
765
766
                    //By default merged cells should be ignored
767 51
                    $isMergedButProceed = false;
768
769
                    //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide)
770 51
                    if ($isMerged && $cell->isMergeRangeValueCell()) {
771
                        $range = (string) $cell->getMergeRange();
772
                        $rangeBoundaries = Coordinate::rangeDimension($range);
773
                        if ($rangeBoundaries[0] === 1) {
774
                            $isMergedButProceed = true;
775
                        }
776
                    }
777
778
                    // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range
779 51
                    if (!$isMerged || $isMergedButProceed) {
780
                        // Determine if we need to make an adjustment for the first row in an AutoFilter range that
781
                        //    has a column filter dropdown
782 51
                        $filterAdjustment = false;
783 51
                        if (!empty($autoFilterIndentRanges)) {
784 4
                            foreach ($autoFilterIndentRanges as $autoFilterFirstRowRange) {
785 4
                                if ($cell->isInRange($autoFilterFirstRowRange)) {
786 4
                                    $filterAdjustment = true;
787
788 4
                                    break;
789
                                }
790
                            }
791
                        }
792
793 51
                        $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
794 51
                        $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
795
796
                        // Calculated value
797
                        // To formatted string
798 51
                        $cellValue = NumberFormat::toFormattedString(
799 51
                            $cell->getCalculatedValue(),
800 51
                            (string) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
801 51
                                ->getNumberFormat()->getFormatCode()
802 51
                        );
803
804 51
                        if ($cellValue !== null && $cellValue !== '') {
805 51
                            $autoSizes[$this->getCellCollection()->getCurrentColumn()] = max(
806 51
                                $autoSizes[$this->getCellCollection()->getCurrentColumn()],
807 51
                                round(
808 51
                                    Shared\Font::calculateColumnWidth(
809 51
                                        $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont(),
810 51
                                        $cellValue,
811 51
                                        (int) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
812 51
                                            ->getAlignment()->getTextRotation(),
813 51
                                        $this->getParentOrThrow()->getDefaultStyle()->getFont(),
814 51
                                        $filterAdjustment,
815 51
                                        $indentAdjustment
816 51
                                    ),
817 51
                                    3
818 51
                                )
819 51
                            );
820
                        }
821
                    }
822
                }
823
            }
824
825
            // adjust column widths
826 51
            foreach ($autoSizes as $columnIndex => $width) {
827 51
                if ($width == -1) {
828
                    $width = $this->getDefaultColumnDimension()->getWidth();
829
                }
830 51
                $this->getColumnDimension($columnIndex)->setWidth($width);
831
            }
832
        }
833
834 647
        return $this;
835
    }
836
837
    /**
838
     * Get parent or null.
839
     */
840 8963
    public function getParent(): ?Spreadsheet
841
    {
842 8963
        return $this->parent;
843
    }
844
845
    /**
846
     * Get parent, throw exception if null.
847
     */
848 9213
    public function getParentOrThrow(): Spreadsheet
849
    {
850 9213
        if ($this->parent !== null) {
851 9212
            return $this->parent;
852
        }
853
854 1
        throw new Exception('Sheet does not have a parent.');
855
    }
856
857
    /**
858
     * Re-bind parent.
859
     *
860
     * @return $this
861
     */
862 54
    public function rebindParent(Spreadsheet $parent): static
863
    {
864 54
        if ($this->parent !== null) {
865 4
            $definedNames = $this->parent->getDefinedNames();
866 4
            foreach ($definedNames as $definedName) {
867
                $parent->addDefinedName($definedName);
868
            }
869
870 4
            $this->parent->removeSheetByIndex(
871 4
                $this->parent->getIndex($this)
872 4
            );
873
        }
874 54
        $this->parent = $parent;
875
876 54
        return $this;
877
    }
878
879
    /**
880
     * Get title.
881
     *
882
     * @return string
883
     */
884 9921
    public function getTitle()
885
    {
886 9921
        return $this->title;
887
    }
888
889
    /**
890
     * Set title.
891
     *
892
     * @param string $title String containing the dimension of this worksheet
893
     * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
894
     *            be updated to reflect the new sheet name.
895
     *          This should be left as the default true, unless you are
896
     *          certain that no formula cells on any worksheet contain
897
     *          references to this worksheet
898
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
899
     *                       at parse time (by Readers), where titles can be assumed to be valid.
900
     *
901
     * @return $this
902
     */
903 9921
    public function setTitle($title, $updateFormulaCellReferences = true, $validate = true): static
904
    {
905
        // Is this a 'rename' or not?
906 9921
        if ($this->getTitle() == $title) {
907 225
            return $this;
908
        }
909
910
        // Old title
911 9921
        $oldTitle = $this->getTitle();
912
913 9921
        if ($validate) {
914
            // Syntax check
915 9921
            self::checkSheetTitle($title);
916
917 9921
            if ($this->parent) {
918
                // Is there already such sheet name?
919 9884
                if ($this->parent->sheetNameExists($title)) {
920
                    // Use name, but append with lowest possible integer
921
922 147
                    if (Shared\StringHelper::countCharacters($title) > 29) {
923
                        $title = Shared\StringHelper::substring($title, 0, 29);
924
                    }
925 147
                    $i = 1;
926 147
                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
927 21
                        ++$i;
928 21
                        if ($i == 10) {
929
                            if (Shared\StringHelper::countCharacters($title) > 28) {
930
                                $title = Shared\StringHelper::substring($title, 0, 28);
931
                            }
932 21
                        } elseif ($i == 100) {
933
                            if (Shared\StringHelper::countCharacters($title) > 27) {
934
                                $title = Shared\StringHelper::substring($title, 0, 27);
935
                            }
936
                        }
937
                    }
938
939 147
                    $title .= " $i";
940
                }
941
            }
942
        }
943
944
        // Set title
945 9921
        $this->title = $title;
946 9921
        $this->dirty = true;
947
948 9921
        if ($this->parent && $this->parent->getCalculationEngine()) {
949
            // New title
950 9884
            $newTitle = $this->getTitle();
951 9884
            $this->parent->getCalculationEngine()
952 9884
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
953 9884
            if ($updateFormulaCellReferences) {
954 723
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
955
            }
956
        }
957
958 9921
        return $this;
959
    }
960
961
    /**
962
     * Get sheet state.
963
     *
964
     * @return string Sheet state (visible, hidden, veryHidden)
965
     */
966 389
    public function getSheetState()
967
    {
968 389
        return $this->sheetState;
969
    }
970
971
    /**
972
     * Set sheet state.
973
     *
974
     * @param string $value Sheet state (visible, hidden, veryHidden)
975
     *
976
     * @return $this
977
     */
978 9921
    public function setSheetState($value): static
979
    {
980 9921
        $this->sheetState = $value;
981
982 9921
        return $this;
983
    }
984
985
    /**
986
     * Get page setup.
987
     *
988
     * @return PageSetup
989
     */
990 1278
    public function getPageSetup()
991
    {
992 1278
        return $this->pageSetup;
993
    }
994
995
    /**
996
     * Set page setup.
997
     *
998
     * @return $this
999
     */
1000 1
    public function setPageSetup(PageSetup $pageSetup): static
1001
    {
1002 1
        $this->pageSetup = $pageSetup;
1003
1004 1
        return $this;
1005
    }
1006
1007
    /**
1008
     * Get page margins.
1009
     *
1010
     * @return PageMargins
1011
     */
1012 1301
    public function getPageMargins()
1013
    {
1014 1301
        return $this->pageMargins;
1015
    }
1016
1017
    /**
1018
     * Set page margins.
1019
     *
1020
     * @return $this
1021
     */
1022 1
    public function setPageMargins(PageMargins $pageMargins): static
1023
    {
1024 1
        $this->pageMargins = $pageMargins;
1025
1026 1
        return $this;
1027
    }
1028
1029
    /**
1030
     * Get page header/footer.
1031
     *
1032
     * @return HeaderFooter
1033
     */
1034 449
    public function getHeaderFooter()
1035
    {
1036 449
        return $this->headerFooter;
1037
    }
1038
1039
    /**
1040
     * Set page header/footer.
1041
     *
1042
     * @return $this
1043
     */
1044 1
    public function setHeaderFooter(HeaderFooter $headerFooter): static
1045
    {
1046 1
        $this->headerFooter = $headerFooter;
1047
1048 1
        return $this;
1049
    }
1050
1051
    /**
1052
     * Get sheet view.
1053
     *
1054
     * @return SheetView
1055
     */
1056 463
    public function getSheetView()
1057
    {
1058 463
        return $this->sheetView;
1059
    }
1060
1061
    /**
1062
     * Set sheet view.
1063
     *
1064
     * @return $this
1065
     */
1066 1
    public function setSheetView(SheetView $sheetView): static
1067
    {
1068 1
        $this->sheetView = $sheetView;
1069
1070 1
        return $this;
1071
    }
1072
1073
    /**
1074
     * Get Protection.
1075
     *
1076
     * @return Protection
1077
     */
1078 493
    public function getProtection()
1079
    {
1080 493
        return $this->protection;
1081
    }
1082
1083
    /**
1084
     * Set Protection.
1085
     *
1086
     * @return $this
1087
     */
1088 1
    public function setProtection(Protection $protection): static
1089
    {
1090 1
        $this->protection = $protection;
1091 1
        $this->dirty = true;
1092
1093 1
        return $this;
1094
    }
1095
1096
    /**
1097
     * Get highest worksheet column.
1098
     *
1099
     * @param null|int|string $row Return the data highest column for the specified row,
1100
     *                                     or the highest column of any row if no row number is passed
1101
     *
1102
     * @return string Highest column name
1103
     */
1104 1227
    public function getHighestColumn($row = null)
1105
    {
1106 1227
        if ($row === null) {
1107 1226
            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1108
        }
1109
1110 1
        return $this->getHighestDataColumn($row);
1111
    }
1112
1113
    /**
1114
     * Get highest worksheet column that contains data.
1115
     *
1116
     * @param null|int|string $row Return the highest data column for the specified row,
1117
     *                                     or the highest data column of any row if no row number is passed
1118
     *
1119
     * @return string Highest column name that contains data
1120
     */
1121 536
    public function getHighestDataColumn($row = null)
1122
    {
1123 536
        return $this->getCellCollection()->getHighestColumn($row);
1124
    }
1125
1126
    /**
1127
     * Get highest worksheet row.
1128
     *
1129
     * @param null|string $column Return the highest data row for the specified column,
1130
     *                                     or the highest row of any column if no column letter is passed
1131
     *
1132
     * @return int Highest row number
1133
     */
1134 774
    public function getHighestRow($column = null)
1135
    {
1136 774
        if ($column === null) {
1137 773
            return $this->cachedHighestRow;
1138
        }
1139
1140 1
        return $this->getHighestDataRow($column);
1141
    }
1142
1143
    /**
1144
     * Get highest worksheet row that contains data.
1145
     *
1146
     * @param null|string $column Return the highest data row for the specified column,
1147
     *                                     or the highest data row of any column if no column letter is passed
1148
     *
1149
     * @return int Highest row number that contains data
1150
     */
1151 542
    public function getHighestDataRow($column = null)
1152
    {
1153 542
        return $this->getCellCollection()->getHighestRow($column);
1154
    }
1155
1156
    /**
1157
     * Get highest worksheet column and highest row that have cell records.
1158
     *
1159
     * @return array Highest column name and highest row number
1160
     */
1161 1
    public function getHighestRowAndColumn(): array
1162
    {
1163 1
        return $this->getCellCollection()->getHighestRowAndColumn();
1164
    }
1165
1166
    /**
1167
     * Set a cell value.
1168
     *
1169
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1170
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1171
     * @param mixed $value Value for the cell
1172
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
1173
     *
1174
     * @return $this
1175
     */
1176 4473
    public function setCellValue($coordinate, mixed $value, ?IValueBinder $binder = null): static
1177
    {
1178 4473
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1179 4473
        $this->getCell($cellAddress)->setValue($value, $binder);
1180
1181 4473
        return $this;
1182
    }
1183
1184
    /**
1185
     * Set a cell value by using numeric cell coordinates.
1186
     *
1187
     * @deprecated 1.23.0
1188
     *      Use the setCellValue() method with a cell address such as 'C5' instead;,
1189
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1190
     * @see Worksheet::setCellValue()
1191
     *
1192
     * @param int $columnIndex Numeric column coordinate of the cell
1193
     * @param int $row Numeric row coordinate of the cell
1194
     * @param mixed $value Value of the cell
1195
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
1196
     *
1197
     * @return $this
1198
     */
1199 1
    public function setCellValueByColumnAndRow($columnIndex, $row, mixed $value, ?IValueBinder $binder = null): static
1200
    {
1201 1
        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValue($value, $binder);
1202
1203 1
        return $this;
1204
    }
1205
1206
    /**
1207
     * Set a cell value.
1208
     *
1209
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1210
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1211
     * @param mixed $value Value of the cell
1212
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1213
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1214
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1215
     *             the datatype match.
1216
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1217
     *          that you specify.
1218
     *
1219
     * @see DataType
1220
     *
1221
     * @return $this
1222
     */
1223 98
    public function setCellValueExplicit($coordinate, mixed $value, string $dataType): static
1224
    {
1225 98
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1226 98
        $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
1227
1228 98
        return $this;
1229
    }
1230
1231
    /**
1232
     * Set a cell value by using numeric cell coordinates.
1233
     *
1234
     * @deprecated 1.23.0
1235
     *      Use the setCellValueExplicit() method with a cell address such as 'C5' instead;,
1236
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1237
     * @see Worksheet::setCellValueExplicit()
1238
     *
1239
     * @param int $columnIndex Numeric column coordinate of the cell
1240
     * @param int $row Numeric row coordinate of the cell
1241
     * @param mixed $value Value of the cell
1242
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1243
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1244
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1245
     *             the datatype match.
1246
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1247
     *          that you specify.
1248
     *
1249
     * @see DataType
1250
     *
1251
     * @return $this
1252
     */
1253 1
    public function setCellValueExplicitByColumnAndRow($columnIndex, $row, mixed $value, string $dataType): static
1254
    {
1255 1
        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValueExplicit($value, $dataType);
1256
1257 1
        return $this;
1258
    }
1259
1260
    /**
1261
     * Get cell at a specific coordinate.
1262
     *
1263
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1264
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1265
     *
1266
     * @return Cell Cell that was found or created
1267
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1268
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1269
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1270
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1271
     *              the active cell has changed.
1272
     */
1273 9591
    public function getCell($coordinate): Cell
1274
    {
1275 9591
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1276
1277
        // Shortcut for increased performance for the vast majority of simple cases
1278 9591
        if ($this->getCellCollection()->has($cellAddress)) {
1279
            /** @var Cell $cell */
1280 9195
            $cell = $this->getCellCollection()->get($cellAddress);
1281
1282 9195
            return $cell;
1283
        }
1284
1285
        /** @var Worksheet $sheet */
1286 9587
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1287 9587
        $cell = $sheet->getCellCollection()->get($finalCoordinate);
1288
1289 9587
        return $cell ?? $sheet->createNewCell($finalCoordinate);
1290
    }
1291
1292
    /**
1293
     * Get the correct Worksheet and coordinate from a coordinate that may
1294
     * contains reference to another sheet or a named range.
1295
     *
1296
     * @return array{0: Worksheet, 1: string}
1297
     */
1298 9588
    private function getWorksheetAndCoordinate(string $coordinate): array
1299
    {
1300 9588
        $sheet = null;
1301 9588
        $finalCoordinate = null;
1302
1303
        // Worksheet reference?
1304 9588
        if (str_contains($coordinate, '!')) {
1305
            $worksheetReference = self::extractSheetTitle($coordinate, true);
1306
1307
            $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
1308
            $finalCoordinate = strtoupper($worksheetReference[1]);
1309
1310
            if ($sheet === null) {
1311
                throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
1312
            }
1313
        } elseif (
1314 9588
            !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
1315 9588
            && preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
1316
        ) {
1317
            // Named range?
1318 14
            $namedRange = $this->validateNamedRange($coordinate, true);
1319 14
            if ($namedRange !== null) {
1320 10
                $sheet = $namedRange->getWorksheet();
1321 10
                if ($sheet === null) {
1322
                    throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
1323
                }
1324
1325
                /** @phpstan-ignore-next-line */
1326 10
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
1327 10
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
1328
            }
1329
        }
1330
1331 9588
        if ($sheet === null || $finalCoordinate === null) {
1332 9588
            $sheet = $this;
1333 9588
            $finalCoordinate = strtoupper($coordinate);
1334
        }
1335
1336 9588
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
1337 2
            throw new Exception('Cell coordinate string can not be a range of cells.');
1338 9588
        } elseif (str_contains($finalCoordinate, '$')) {
1339
            throw new Exception('Cell coordinate must not be absolute.');
1340
        }
1341
1342 9588
        return [$sheet, $finalCoordinate];
1343
    }
1344
1345
    /**
1346
     * Get an existing cell at a specific coordinate, or null.
1347
     *
1348
     * @param string $coordinate Coordinate of the cell, eg: 'A1'
1349
     *
1350
     * @return null|Cell Cell that was found or null
1351
     */
1352 51
    private function getCellOrNull($coordinate): ?Cell
1353
    {
1354
        // Check cell collection
1355 51
        if ($this->getCellCollection()->has($coordinate)) {
1356 51
            return $this->getCellCollection()->get($coordinate);
1357
        }
1358
1359
        return null;
1360
    }
1361
1362
    /**
1363
     * Get cell at a specific coordinate by using numeric cell coordinates.
1364
     *
1365
     * @deprecated 1.23.0
1366
     *      Use the getCell() method with a cell address such as 'C5' instead;,
1367
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1368
     * @see Worksheet::getCell()
1369
     *
1370
     * @param int $columnIndex Numeric column coordinate of the cell
1371
     * @param int $row Numeric row coordinate of the cell
1372
     *
1373
     * @return Cell Cell that was found/created or null
1374
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1375
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1376
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1377
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1378
     *              the active cell has changed.
1379
     */
1380 1
    public function getCellByColumnAndRow($columnIndex, $row): Cell
1381
    {
1382 1
        return $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1383
    }
1384
1385
    /**
1386
     * Create a new cell at the specified coordinate.
1387
     *
1388
     * @param string $coordinate Coordinate of the cell
1389
     *
1390
     * @return Cell Cell that was created
1391
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1392
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1393
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1394
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1395
     *              the active cell has changed.
1396
     */
1397 9593
    public function createNewCell(string $coordinate): Cell
1398
    {
1399 9593
        [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate);
1400 9593
        $cell = new Cell(null, DataType::TYPE_NULL, $this);
1401 9593
        $this->getCellCollection()->add($coordinate, $cell);
1402
1403
        // Coordinates
1404 9593
        if ($column > $this->cachedHighestColumn) {
1405 6570
            $this->cachedHighestColumn = $column;
1406
        }
1407 9593
        if ($row > $this->cachedHighestRow) {
1408 8110
            $this->cachedHighestRow = $row;
1409
        }
1410
1411
        // Cell needs appropriate xfIndex from dimensions records
1412
        //    but don't create dimension records if they don't already exist
1413 9593
        $rowDimension = $this->rowDimensions[$row] ?? null;
1414 9593
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
1415
1416 9593
        $xfSet = false;
1417 9593
        if ($rowDimension !== null) {
1418 357
            $rowXf = (int) $rowDimension->getXfIndex();
1419 357
            if ($rowXf > 0) {
1420
                // then there is a row dimension with explicit style, assign it to the cell
1421 199
                $cell->setXfIndex($rowXf);
1422 199
                $xfSet = true;
1423
            }
1424
        }
1425 9593
        if (!$xfSet && $columnDimension !== null) {
1426 473
            $colXf = (int) $columnDimension->getXfIndex();
1427 473
            if ($colXf > 0) {
1428
                // then there is a column dimension, assign it to the cell
1429 209
                $cell->setXfIndex($colXf);
1430
            }
1431
        }
1432
1433 9593
        return $cell;
1434
    }
1435
1436
    /**
1437
     * Does the cell at a specific coordinate exist?
1438
     *
1439
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1440
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1441
     */
1442 8181
    public function cellExists($coordinate): bool
1443
    {
1444 8181
        $cellAddress = Validations::validateCellAddress($coordinate);
1445 8181
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1446
1447 8180
        return $sheet->getCellCollection()->has($finalCoordinate);
1448
    }
1449
1450
    /**
1451
     * Cell at a specific coordinate by using numeric cell coordinates exists?
1452
     *
1453
     * @deprecated 1.23.0
1454
     *      Use the cellExists() method with a cell address such as 'C5' instead;,
1455
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1456
     * @see Worksheet::cellExists()
1457
     *
1458
     * @param int $columnIndex Numeric column coordinate of the cell
1459
     * @param int $row Numeric row coordinate of the cell
1460
     */
1461 1
    public function cellExistsByColumnAndRow($columnIndex, $row): bool
1462
    {
1463 1
        return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1464
    }
1465
1466
    /**
1467
     * Get row dimension at a specific row.
1468
     *
1469
     * @param int $row Numeric index of the row
1470
     */
1471 506
    public function getRowDimension(int $row): RowDimension
1472
    {
1473
        // Get row dimension
1474 506
        if (!isset($this->rowDimensions[$row])) {
1475 506
            $this->rowDimensions[$row] = new RowDimension($row);
1476
1477 506
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
1478
        }
1479
1480 506
        return $this->rowDimensions[$row];
1481
    }
1482
1483 480
    public function rowDimensionExists(int $row): bool
1484
    {
1485 480
        return isset($this->rowDimensions[$row]);
1486
    }
1487
1488 2
    public function columnDimensionExists(string $column): bool
1489
    {
1490 2
        return isset($this->columnDimensions[$column]);
1491
    }
1492
1493
    /**
1494
     * Get column dimension at a specific column.
1495
     *
1496
     * @param string $column String index of the column eg: 'A'
1497
     */
1498 548
    public function getColumnDimension(string $column): ColumnDimension
1499
    {
1500
        // Uppercase coordinate
1501 548
        $column = strtoupper($column);
1502
1503
        // Fetch dimensions
1504 548
        if (!isset($this->columnDimensions[$column])) {
1505 548
            $this->columnDimensions[$column] = new ColumnDimension($column);
1506
1507 548
            $columnIndex = Coordinate::columnIndexFromString($column);
1508 548
            if ($this->cachedHighestColumn < $columnIndex) {
1509 390
                $this->cachedHighestColumn = $columnIndex;
1510
            }
1511
        }
1512
1513 548
        return $this->columnDimensions[$column];
1514
    }
1515
1516
    /**
1517
     * Get column dimension at a specific column by using numeric cell coordinates.
1518
     *
1519
     * @param int $columnIndex Numeric column coordinate of the cell
1520
     */
1521 80
    public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
1522
    {
1523 80
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
1524
    }
1525
1526
    /**
1527
     * Get styles.
1528
     *
1529
     * @return Style[]
1530
     */
1531 1
    public function getStyles()
1532
    {
1533 1
        return $this->styles;
1534
    }
1535
1536
    /**
1537
     * Get style for cell.
1538
     *
1539
     * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellCoordinate
1540
     *              A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
1541
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1542
     *              or a CellAddress or AddressRange object.
1543
     */
1544 8711
    public function getStyle($cellCoordinate): Style
1545
    {
1546 8711
        $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
1547
1548
        // set this sheet as active
1549 8711
        $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
1550
1551
        // set cell coordinate as active
1552 8711
        $this->setSelectedCells($cellCoordinate);
1553
1554 8711
        return $this->getParentOrThrow()->getCellXfSupervisor();
1555
    }
1556
1557
    /**
1558
     * Get style for cell by using numeric cell coordinates.
1559
     *
1560
     * @deprecated 1.23.0
1561
     *      Use the getStyle() method with a cell address range such as 'C5:F8' instead;,
1562
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1563
     *          or an AddressRange object.
1564
     * @see Worksheet::getStyle()
1565
     *
1566
     * @param int $columnIndex1 Numeric column coordinate of the cell
1567
     * @param int $row1 Numeric row coordinate of the cell
1568
     * @param null|int $columnIndex2 Numeric column coordinate of the range cell
1569
     * @param null|int $row2 Numeric row coordinate of the range cell
1570
     */
1571 1
    public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null): Style
1572
    {
1573 1
        if ($columnIndex2 !== null && $row2 !== null) {
1574 1
            $cellRange = new CellRange(
1575 1
                CellAddress::fromColumnAndRow($columnIndex1, $row1),
1576 1
                CellAddress::fromColumnAndRow($columnIndex2, $row2)
1577 1
            );
1578
1579 1
            return $this->getStyle($cellRange);
1580
        }
1581
1582 1
        return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1));
1583
    }
1584
1585
    /**
1586
     * Get conditional styles for a cell.
1587
     *
1588
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1589
     *          If a single cell is referenced, then the array of conditional styles will be returned if the cell is
1590
     *               included in a conditional style range.
1591
     *          If a range of cells is specified, then the styles will only be returned if the range matches the entire
1592
     *               range of the conditional.
1593
     *
1594
     * @return Conditional[]
1595
     */
1596 228
    public function getConditionalStyles(string $coordinate): array
1597
    {
1598 228
        $coordinate = strtoupper($coordinate);
1599 228
        if (str_contains($coordinate, ':')) {
1600 47
            return $this->conditionalStylesCollection[$coordinate] ?? [];
1601
        }
1602
1603 206
        $cell = $this->getCell($coordinate);
1604 206
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1605 194
            if ($cell->isInRange($conditionalRange)) {
1606 190
                return $this->conditionalStylesCollection[$conditionalRange];
1607
            }
1608
        }
1609
1610 35
        return [];
1611
    }
1612
1613 178
    public function getConditionalRange(string $coordinate): ?string
1614
    {
1615 178
        $coordinate = strtoupper($coordinate);
1616 178
        $cell = $this->getCell($coordinate);
1617 178
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1618 178
            if ($cell->isInRange($conditionalRange)) {
1619 177
                return $conditionalRange;
1620
            }
1621
        }
1622
1623 1
        return null;
1624
    }
1625
1626
    /**
1627
     * Do conditional styles exist for this cell?
1628
     *
1629
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1630
     *          If a single cell is specified, then this method will return true if that cell is included in a
1631
     *               conditional style range.
1632
     *          If a range of cells is specified, then true will only be returned if the range matches the entire
1633
     *               range of the conditional.
1634
     */
1635 22
    public function conditionalStylesExists($coordinate): bool
1636
    {
1637 22
        $coordinate = strtoupper($coordinate);
1638 22
        if (str_contains($coordinate, ':')) {
1639 11
            return isset($this->conditionalStylesCollection[$coordinate]);
1640
        }
1641
1642 11
        $cell = $this->getCell($coordinate);
1643 11
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1644 11
            if ($cell->isInRange($conditionalRange)) {
1645 7
                return true;
1646
            }
1647
        }
1648
1649 4
        return false;
1650
    }
1651
1652
    /**
1653
     * Removes conditional styles for a cell.
1654
     *
1655
     * @param string $coordinate eg: 'A1'
1656
     *
1657
     * @return $this
1658
     */
1659 42
    public function removeConditionalStyles($coordinate): static
1660
    {
1661 42
        unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
1662
1663 42
        return $this;
1664
    }
1665
1666
    /**
1667
     * Get collection of conditional styles.
1668
     *
1669
     * @return array
1670
     */
1671 456
    public function getConditionalStylesCollection()
1672
    {
1673 456
        return $this->conditionalStylesCollection;
1674
    }
1675
1676
    /**
1677
     * Set conditional styles.
1678
     *
1679
     * @param string $coordinate eg: 'A1'
1680
     * @param Conditional[] $styles
1681
     *
1682
     * @return $this
1683
     */
1684 287
    public function setConditionalStyles($coordinate, $styles): static
1685
    {
1686 287
        $this->conditionalStylesCollection[strtoupper($coordinate)] = $styles;
1687
1688 287
        return $this;
1689
    }
1690
1691
    /**
1692
     * Duplicate cell style to a range of cells.
1693
     *
1694
     * Please note that this will overwrite existing cell styles for cells in range!
1695
     *
1696
     * @param Style $style Cell style to duplicate
1697
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1698
     *
1699
     * @return $this
1700
     */
1701 2
    public function duplicateStyle(Style $style, string $range): static
1702
    {
1703
        // Add the style to the workbook if necessary
1704 2
        $workbook = $this->getParentOrThrow();
1705 2
        if ($existingStyle = $workbook->getCellXfByHashCode($style->getHashCode())) {
1706
            // there is already such cell Xf in our collection
1707 1
            $xfIndex = $existingStyle->getIndex();
1708
        } else {
1709
            // we don't have such a cell Xf, need to add
1710 2
            $workbook->addCellXf($style);
1711 2
            $xfIndex = $style->getIndex();
1712
        }
1713
1714
        // Calculate range outer borders
1715 2
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1716
1717
        // Make sure we can loop upwards on rows and columns
1718 2
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1719
            $tmp = $rangeStart;
1720
            $rangeStart = $rangeEnd;
1721
            $rangeEnd = $tmp;
1722
        }
1723
1724
        // Loop through cells and apply styles
1725 2
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1726 2
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1727 2
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
1728
            }
1729
        }
1730
1731 2
        return $this;
1732
    }
1733
1734
    /**
1735
     * Duplicate conditional style to a range of cells.
1736
     *
1737
     * Please note that this will overwrite existing cell styles for cells in range!
1738
     *
1739
     * @param Conditional[] $styles Cell style to duplicate
1740
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1741
     *
1742
     * @return $this
1743
     */
1744 18
    public function duplicateConditionalStyle(array $styles, string $range = ''): static
1745
    {
1746 18
        foreach ($styles as $cellStyle) {
1747 18
            if (!($cellStyle instanceof Conditional)) {
1748
                throw new Exception('Style is not a conditional style');
1749
            }
1750
        }
1751
1752
        // Calculate range outer borders
1753 18
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1754
1755
        // Make sure we can loop upwards on rows and columns
1756 18
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1757
            $tmp = $rangeStart;
1758
            $rangeStart = $rangeEnd;
1759
            $rangeEnd = $tmp;
1760
        }
1761
1762
        // Loop through cells and apply styles
1763 18
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1764 18
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1765 18
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
1766
            }
1767
        }
1768
1769 18
        return $this;
1770
    }
1771
1772
    /**
1773
     * Set break on a cell.
1774
     *
1775
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1776
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1777
     * @param int $break Break type (type of Worksheet::BREAK_*)
1778
     *
1779
     * @return $this
1780
     */
1781 26
    public function setBreak($coordinate, $break, int $max = -1): static
1782
    {
1783 26
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1784
1785 26
        if ($break === self::BREAK_NONE) {
1786 7
            unset($this->rowBreaks[$cellAddress], $this->columnBreaks[$cellAddress]);
1787 26
        } elseif ($break === self::BREAK_ROW) {
1788 18
            $this->rowBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
1789 14
        } elseif ($break === self::BREAK_COLUMN) {
1790 14
            $this->columnBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
1791
        }
1792
1793 26
        return $this;
1794
    }
1795
1796
    /**
1797
     * Set break on a cell by using numeric cell coordinates.
1798
     *
1799
     * @deprecated 1.23.0
1800
     *      Use the setBreak() method with a cell address such as 'C5' instead;,
1801
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1802
     * @see Worksheet::setBreak()
1803
     *
1804
     * @param int $columnIndex Numeric column coordinate of the cell
1805
     * @param int $row Numeric row coordinate of the cell
1806
     * @param int $break Break type (type of Worksheet::BREAK_*)
1807
     *
1808
     * @return $this
1809
     */
1810 1
    public function setBreakByColumnAndRow($columnIndex, $row, $break): static
1811
    {
1812 1
        return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break);
1813
    }
1814
1815
    /**
1816
     * Get breaks.
1817
     *
1818
     * @return int[]
1819
     */
1820 545
    public function getBreaks(): array
1821
    {
1822 545
        $breaks = [];
1823
        /** @var callable */
1824 545
        $compareFunction = [self::class, 'compareRowBreaks'];
1825 545
        uksort($this->rowBreaks, $compareFunction);
1826 545
        foreach ($this->rowBreaks as $break) {
1827 10
            $breaks[$break->getCoordinate()] = self::BREAK_ROW;
1828
        }
1829
        /** @var callable */
1830 545
        $compareFunction = [self::class, 'compareColumnBreaks'];
1831 545
        uksort($this->columnBreaks, $compareFunction);
1832 545
        foreach ($this->columnBreaks as $break) {
1833 8
            $breaks[$break->getCoordinate()] = self::BREAK_COLUMN;
1834
        }
1835
1836 545
        return $breaks;
1837
    }
1838
1839
    /**
1840
     * Get row breaks.
1841
     *
1842
     * @return PageBreak[]
1843
     */
1844 400
    public function getRowBreaks()
1845
    {
1846
        /** @var callable */
1847 400
        $compareFunction = [self::class, 'compareRowBreaks'];
1848 400
        uksort($this->rowBreaks, $compareFunction);
1849
1850 400
        return $this->rowBreaks;
1851
    }
1852
1853 9
    protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
1854
    {
1855 9
        $row1 = Coordinate::indexesFromString($coordinate1)[1];
1856 9
        $row2 = Coordinate::indexesFromString($coordinate2)[1];
1857
1858 9
        return $row1 - $row2;
1859
    }
1860
1861 5
    protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
1862
    {
1863 5
        $column1 = Coordinate::indexesFromString($coordinate1)[0];
1864 5
        $column2 = Coordinate::indexesFromString($coordinate2)[0];
1865
1866 5
        return $column1 - $column2;
1867
    }
1868
1869
    /**
1870
     * Get column breaks.
1871
     *
1872
     * @return PageBreak[]
1873
     */
1874 399
    public function getColumnBreaks()
1875
    {
1876
        /** @var callable */
1877 399
        $compareFunction = [self::class, 'compareColumnBreaks'];
1878 399
        uksort($this->columnBreaks, $compareFunction);
1879
1880 399
        return $this->columnBreaks;
1881
    }
1882
1883
    /**
1884
     * Set merge on a cell range.
1885
     *
1886
     * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10'
1887
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1888
     *              or an AddressRange.
1889
     * @param string $behaviour How the merged cells should behave.
1890
     *               Possible values are:
1891
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1892
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1893
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1894
     *
1895
     * @return $this
1896
     */
1897 148
    public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY): static
1898
    {
1899 148
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
1900
1901 147
        if (!str_contains($range, ':')) {
1902 1
            $range .= ":{$range}";
1903
        }
1904
1905 147
        if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) !== 1) {
1906 1
            throw new Exception('Merge must be on a valid range of cells.');
1907
        }
1908
1909 146
        $this->mergeCells[$range] = $range;
1910 146
        $firstRow = (int) $matches[2];
1911 146
        $lastRow = (int) $matches[4];
1912 146
        $firstColumn = $matches[1];
1913 146
        $lastColumn = $matches[3];
1914 146
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
1915 146
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
1916 146
        $numberRows = $lastRow - $firstRow;
1917 146
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
1918
1919 146
        if ($numberRows === 1 && $numberColumns === 1) {
1920 30
            return $this;
1921
        }
1922
1923
        // create upper left cell if it does not already exist
1924 139
        $upperLeft = "{$firstColumn}{$firstRow}";
1925 139
        if (!$this->cellExists($upperLeft)) {
1926 28
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
1927
        }
1928
1929 139
        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
1930
            // Blank out the rest of the cells in the range (if they exist)
1931 46
            if ($numberRows > $numberColumns) {
1932 10
                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
1933
            } else {
1934 36
                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
1935
            }
1936
        }
1937
1938 139
        return $this;
1939
    }
1940
1941 10
    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1942
    {
1943 10
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1944
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1945 10
            : [];
1946
1947 10
        foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) {
1948 10
            $iterator = $column->getCellIterator($firstRow);
1949 10
            $iterator->setIterateOnlyExistingCells(true);
1950 10
            foreach ($iterator as $cell) {
1951 10
                if ($cell !== null) {
1952 10
                    $row = $cell->getRow();
1953 10
                    if ($row > $lastRow) {
1954 7
                        break;
1955
                    }
1956 10
                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1957
                }
1958
            }
1959
        }
1960
1961 10
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1962
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1963
        }
1964
    }
1965
1966 36
    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1967
    {
1968 36
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1969 4
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1970 32
            : [];
1971
1972 36
        foreach ($this->getRowIterator($firstRow, $lastRow) as $row) {
1973 36
            $iterator = $row->getCellIterator($firstColumn);
1974 36
            $iterator->setIterateOnlyExistingCells(true);
1975 36
            foreach ($iterator as $cell) {
1976 36
                if ($cell !== null) {
1977 36
                    $column = $cell->getColumn();
1978 36
                    $columnIndex = Coordinate::columnIndexFromString($column);
1979 36
                    if ($columnIndex > $lastColumnIndex) {
1980 7
                        break;
1981
                    }
1982 36
                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1983
                }
1984
            }
1985
        }
1986
1987 36
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1988 4
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1989
        }
1990
    }
1991
1992 46
    public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array
1993
    {
1994 46
        if ($cell->getCoordinate() !== $upperLeft) {
1995 22
            Calculation::getInstance($cell->getWorksheet()->getParentOrThrow())->flushInstance();
1996 22
            if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1997 4
                $cellValue = $cell->getFormattedValue();
1998 4
                if ($cellValue !== '') {
1999 4
                    $leftCellValue[] = $cellValue;
2000
                }
2001
            }
2002 22
            $cell->setValueExplicit(null, DataType::TYPE_NULL);
2003
        }
2004
2005 46
        return $leftCellValue;
2006
    }
2007
2008
    /**
2009
     * Set merge on a cell range by using numeric cell coordinates.
2010
     *
2011
     * @deprecated 1.23.0
2012
     *      Use the mergeCells() method with a cell address range such as 'C5:F8' instead;,
2013
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2014
     *          or an AddressRange object.
2015
     * @see Worksheet::mergeCells()
2016
     *
2017
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2018
     * @param int $row1 Numeric row coordinate of the first cell
2019
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2020
     * @param int $row2 Numeric row coordinate of the last cell
2021
     * @param string $behaviour How the merged cells should behave.
2022
     *               Possible values are:
2023
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
2024
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
2025
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
2026
     *
2027
     * @return $this
2028
     */
2029 1
    public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY): static
2030
    {
2031 1
        $cellRange = new CellRange(
2032 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2033 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2034 1
        );
2035
2036 1
        return $this->mergeCells($cellRange, $behaviour);
2037
    }
2038
2039
    /**
2040
     * Remove merge on a cell range.
2041
     *
2042
     * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10'
2043
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2044
     *              or an AddressRange.
2045
     *
2046
     * @return $this
2047
     */
2048 23
    public function unmergeCells($range): static
2049
    {
2050 23
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
2051
2052 23
        if (str_contains($range, ':')) {
2053 22
            if (isset($this->mergeCells[$range])) {
2054 22
                unset($this->mergeCells[$range]);
2055
            } else {
2056 22
                throw new Exception('Cell range ' . $range . ' not known as merged.');
2057
            }
2058
        } else {
2059 1
            throw new Exception('Merge can only be removed from a range of cells.');
2060
        }
2061
2062 22
        return $this;
2063
    }
2064
2065
    /**
2066
     * Remove merge on a cell range by using numeric cell coordinates.
2067
     *
2068
     * @deprecated 1.23.0
2069
     *      Use the unmergeCells() method with a cell address range such as 'C5:F8' instead;,
2070
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2071
     *          or an AddressRange object.
2072
     * @see Worksheet::unmergeCells()
2073
     *
2074
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2075
     * @param int $row1 Numeric row coordinate of the first cell
2076
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2077
     * @param int $row2 Numeric row coordinate of the last cell
2078
     *
2079
     * @return $this
2080
     */
2081 1
    public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2): static
2082
    {
2083 1
        $cellRange = new CellRange(
2084 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2085 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2086 1
        );
2087
2088 1
        return $this->unmergeCells($cellRange);
2089
    }
2090
2091
    /**
2092
     * Get merge cells array.
2093
     *
2094
     * @return string[]
2095
     */
2096 959
    public function getMergeCells()
2097
    {
2098 959
        return $this->mergeCells;
2099
    }
2100
2101
    /**
2102
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
2103
     * a single cell range.
2104
     *
2105
     * @param string[] $mergeCells
2106
     *
2107
     * @return $this
2108
     */
2109 74
    public function setMergeCells(array $mergeCells): static
2110
    {
2111 74
        $this->mergeCells = $mergeCells;
2112
2113 74
        return $this;
2114
    }
2115
2116
    /**
2117
     * Set protection on a cell or cell range.
2118
     *
2119
     * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2120
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2121
     *              or a CellAddress or AddressRange object.
2122
     * @param string $password Password to unlock the protection
2123
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
2124
     *
2125
     * @return $this
2126
     */
2127 21
    public function protectCells($range, $password, $alreadyHashed = false): static
2128
    {
2129 21
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2130
2131 21
        if (!$alreadyHashed) {
2132 21
            $password = Shared\PasswordHasher::hashPassword($password);
2133
        }
2134 21
        $this->protectedCells[$range] = $password;
2135
2136 21
        return $this;
2137
    }
2138
2139
    /**
2140
     * Set protection on a cell range by using numeric cell coordinates.
2141
     *
2142
     * @deprecated 1.23.0
2143
     *      Use the protectCells() method with a cell address range such as 'C5:F8' instead;,
2144
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2145
     *          or an AddressRange object.
2146
     * @see Worksheet::protectCells()
2147
     *
2148
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2149
     * @param int $row1 Numeric row coordinate of the first cell
2150
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2151
     * @param int $row2 Numeric row coordinate of the last cell
2152
     * @param string $password Password to unlock the protection
2153
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
2154
     *
2155
     * @return $this
2156
     */
2157 1
    public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false): static
2158
    {
2159 1
        $cellRange = new CellRange(
2160 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2161 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2162 1
        );
2163
2164 1
        return $this->protectCells($cellRange, $password, $alreadyHashed);
2165
    }
2166
2167
    /**
2168
     * Remove protection on a cell or cell range.
2169
     *
2170
     * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2171
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2172
     *              or a CellAddress or AddressRange object.
2173
     *
2174
     * @return $this
2175
     */
2176 20
    public function unprotectCells($range): static
2177
    {
2178 20
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2179
2180 20
        if (isset($this->protectedCells[$range])) {
2181 19
            unset($this->protectedCells[$range]);
2182
        } else {
2183 1
            throw new Exception('Cell range ' . $range . ' not known as protected.');
2184
        }
2185
2186 19
        return $this;
2187
    }
2188
2189
    /**
2190
     * Remove protection on a cell range by using numeric cell coordinates.
2191
     *
2192
     * @deprecated 1.23.0
2193
     *      Use the unprotectCells() method with a cell address range such as 'C5:F8' instead;,
2194
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2195
     *          or an AddressRange object.
2196
     * @see Worksheet::unprotectCells()
2197
     *
2198
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2199
     * @param int $row1 Numeric row coordinate of the first cell
2200
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2201
     * @param int $row2 Numeric row coordinate of the last cell
2202
     *
2203
     * @return $this
2204
     */
2205 1
    public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2): static
2206
    {
2207 1
        $cellRange = new CellRange(
2208 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2209 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2210 1
        );
2211
2212 1
        return $this->unprotectCells($cellRange);
2213
    }
2214
2215
    /**
2216
     * Get protected cells.
2217
     *
2218
     * @return string[]
2219
     */
2220 459
    public function getProtectedCells()
2221
    {
2222 459
        return $this->protectedCells;
2223
    }
2224
2225
    /**
2226
     * Get Autofilter.
2227
     *
2228
     * @return AutoFilter
2229
     */
2230 636
    public function getAutoFilter()
2231
    {
2232 636
        return $this->autoFilter;
2233
    }
2234
2235
    /**
2236
     * Set AutoFilter.
2237
     *
2238
     * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|AutoFilter|string $autoFilterOrRange
2239
     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
2240
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2241
     *              or an AddressRange.
2242
     *
2243
     * @return $this
2244
     */
2245 15
    public function setAutoFilter($autoFilterOrRange): static
2246
    {
2247 15
        if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
2248
            $this->autoFilter = $autoFilterOrRange;
2249
        } else {
2250 15
            $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
2251
2252 15
            $this->autoFilter->setRange($cellRange);
2253
        }
2254
2255 15
        return $this;
2256
    }
2257
2258
    /**
2259
     * Set Autofilter Range by using numeric cell coordinates.
2260
     *
2261
     * @deprecated 1.23.0
2262
     *      Use the setAutoFilter() method with a cell address range such as 'C5:F8' instead;,
2263
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2264
     *          or an AddressRange object or AutoFilter object.
2265
     * @see Worksheet::setAutoFilter()
2266
     *
2267
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2268
     * @param int $row1 Numeric row coordinate of the first cell
2269
     * @param int $columnIndex2 Numeric column coordinate of the second cell
2270
     * @param int $row2 Numeric row coordinate of the second cell
2271
     *
2272
     * @return $this
2273
     */
2274 1
    public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2): static
2275
    {
2276 1
        $cellRange = new CellRange(
2277 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2278 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2279 1
        );
2280
2281 1
        return $this->setAutoFilter($cellRange);
2282
    }
2283
2284
    /**
2285
     * Remove autofilter.
2286
     */
2287 1
    public function removeAutoFilter(): self
2288
    {
2289 1
        $this->autoFilter->setRange('');
2290
2291 1
        return $this;
2292
    }
2293
2294
    /**
2295
     * Get collection of Tables.
2296
     *
2297
     * @return ArrayObject<int, Table>
2298
     */
2299 9548
    public function getTableCollection()
2300
    {
2301 9548
        return $this->tableCollection;
2302
    }
2303
2304
    /**
2305
     * Add Table.
2306
     *
2307
     * @return $this
2308
     */
2309 93
    public function addTable(Table $table): self
2310
    {
2311 93
        $table->setWorksheet($this);
2312 93
        $this->tableCollection[] = $table;
2313
2314 93
        return $this;
2315
    }
2316
2317
    /**
2318
     * @return string[] array of Table names
2319
     */
2320 1
    public function getTableNames(): array
2321
    {
2322 1
        $tableNames = [];
2323
2324 1
        foreach ($this->tableCollection as $table) {
2325
            /** @var Table $table */
2326 1
            $tableNames[] = $table->getName();
2327
        }
2328
2329 1
        return $tableNames;
2330
    }
2331
2332
    /**
2333
     * @param string $name the table name to search
2334
     *
2335
     * @return null|Table The table from the tables collection, or null if not found
2336
     */
2337 89
    public function getTableByName(string $name): ?Table
2338
    {
2339 89
        $tableIndex = $this->getTableIndexByName($name);
2340
2341 89
        return ($tableIndex === null) ? null : $this->tableCollection[$tableIndex];
2342
    }
2343
2344
    /**
2345
     * @param string $name the table name to search
2346
     *
2347
     * @return null|int The index of the located table in the tables collection, or null if not found
2348
     */
2349 90
    protected function getTableIndexByName(string $name): ?int
2350
    {
2351 90
        $name = Shared\StringHelper::strToUpper($name);
2352 90
        foreach ($this->tableCollection as $index => $table) {
2353
            /** @var Table $table */
2354 61
            if (Shared\StringHelper::strToUpper($table->getName()) === $name) {
2355 60
                return $index;
2356
            }
2357
        }
2358
2359 34
        return null;
2360
    }
2361
2362
    /**
2363
     * Remove Table by name.
2364
     *
2365
     * @param string $name Table name
2366
     *
2367
     * @return $this
2368
     */
2369 1
    public function removeTableByName(string $name): self
2370
    {
2371 1
        $tableIndex = $this->getTableIndexByName($name);
2372
2373 1
        if ($tableIndex !== null) {
2374 1
            unset($this->tableCollection[$tableIndex]);
2375
        }
2376
2377 1
        return $this;
2378
    }
2379
2380
    /**
2381
     * Remove collection of Tables.
2382
     */
2383 1
    public function removeTableCollection(): self
2384
    {
2385 1
        $this->tableCollection = new ArrayObject();
2386
2387 1
        return $this;
2388
    }
2389
2390
    /**
2391
     * Get Freeze Pane.
2392
     *
2393
     * @return null|string
2394
     */
2395 195
    public function getFreezePane()
2396
    {
2397 195
        return $this->freezePane;
2398
    }
2399
2400
    /**
2401
     * Freeze Pane.
2402
     *
2403
     * Examples:
2404
     *
2405
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
2406
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
2407
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
2408
     *
2409
     * @param null|array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
2410
     *            or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2411
     *        Passing a null value for this argument will clear any existing freeze pane for this worksheet.
2412
     * @param null|array<int>|CellAddress|string $topLeftCell default position of the right bottom pane
2413
     *            Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
2414
     *            or a CellAddress object.
2415
     *
2416
     * @return $this
2417
     */
2418 47
    public function freezePane($coordinate, $topLeftCell = null, bool $frozenSplit = false): static
2419
    {
2420 47
        $this->panes = [
2421 47
            'bottomRight' => null,
2422 47
            'bottomLeft' => null,
2423 47
            'topRight' => null,
2424 47
            'topLeft' => null,
2425 47
        ];
2426 47
        $cellAddress = ($coordinate !== null)
2427 47
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
2428 1
            : null;
2429 47
        if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
2430 1
            throw new Exception('Freeze pane can not be set on a range of cells.');
2431
        }
2432 46
        $topLeftCell = ($topLeftCell !== null)
2433 37
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
2434 34
            : null;
2435
2436 46
        if ($cellAddress !== null && $topLeftCell === null) {
2437 34
            $coordinate = Coordinate::coordinateFromString($cellAddress);
2438 34
            $topLeftCell = $coordinate[0] . $coordinate[1];
2439
        }
2440
2441 46
        $topLeftCell = "$topLeftCell";
2442 46
        $this->paneTopLeftCell = $topLeftCell;
2443
2444 46
        $this->freezePane = $cellAddress;
2445 46
        $this->topLeftCell = $topLeftCell;
2446 46
        if ($cellAddress === null) {
2447 1
            $this->paneState = '';
2448 1
            $this->xSplit = $this->ySplit = 0;
2449 1
            $this->activePane = '';
2450
        } else {
2451 46
            $coordinates = Coordinate::indexesFromString($cellAddress);
2452 46
            $this->xSplit = $coordinates[0] - 1;
2453 46
            $this->ySplit = $coordinates[1] - 1;
2454 46
            if ($this->xSplit > 0 || $this->ySplit > 0) {
2455 45
                $this->paneState = $frozenSplit ? self::PANE_FROZENSPLIT : self::PANE_FROZEN;
2456 45
                $this->setSelectedCellsActivePane();
2457
            } else {
2458 1
                $this->paneState = '';
2459 1
                $this->freezePane = null;
2460 1
                $this->activePane = '';
2461
            }
2462
        }
2463
2464 46
        return $this;
2465
    }
2466
2467 40
    public function setTopLeftCell(string $topLeftCell): self
2468
    {
2469 40
        $this->topLeftCell = $topLeftCell;
2470
2471 40
        return $this;
2472
    }
2473
2474
    /**
2475
     * Freeze Pane by using numeric cell coordinates.
2476
     *
2477
     * @deprecated 1.23.0
2478
     *      Use the freezePane() method with a cell address such as 'C5' instead;,
2479
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2480
     * @see Worksheet::freezePane()
2481
     *
2482
     * @param int $columnIndex Numeric column coordinate of the cell
2483
     * @param int $row Numeric row coordinate of the cell
2484
     *
2485
     * @return $this
2486
     */
2487 1
    public function freezePaneByColumnAndRow($columnIndex, $row): static
2488
    {
2489 1
        return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2490
    }
2491
2492
    /**
2493
     * Unfreeze Pane.
2494
     *
2495
     * @return $this
2496
     */
2497 1
    public function unfreezePane(): static
2498
    {
2499 1
        return $this->freezePane(null);
2500
    }
2501
2502
    /**
2503
     * Get the default position of the right bottom pane.
2504
     *
2505
     * @return null|string
2506
     */
2507 362
    public function getTopLeftCell()
2508
    {
2509 362
        return $this->topLeftCell;
2510
    }
2511
2512 10
    public function getPaneTopLeftCell(): string
2513
    {
2514 10
        return $this->paneTopLeftCell;
2515
    }
2516
2517 26
    public function setPaneTopLeftCell(string $paneTopLeftCell): self
2518
    {
2519 26
        $this->paneTopLeftCell = $paneTopLeftCell;
2520
2521 26
        return $this;
2522
    }
2523
2524 350
    public function usesPanes(): bool
2525
    {
2526 350
        return $this->xSplit > 0 || $this->ySplit > 0;
2527
    }
2528
2529 2
    public function getPane(string $position): ?Pane
2530
    {
2531 2
        return $this->panes[$position] ?? null;
2532
    }
2533
2534 34
    public function setPane(string $position, ?Pane $pane): self
2535
    {
2536 34
        if (array_key_exists($position, $this->panes)) {
2537 34
            $this->panes[$position] = $pane;
2538
        }
2539
2540 34
        return $this;
2541
    }
2542
2543
    /** @return (null|Pane)[] */
2544 11
    public function getPanes()
2545
    {
2546 11
        return $this->panes;
2547
    }
2548
2549 12
    public function getActivePane(): string
2550
    {
2551 12
        return $this->activePane;
2552
    }
2553
2554 46
    public function setActivePane(string $activePane): self
2555
    {
2556 46
        $this->activePane = array_key_exists($activePane, $this->panes) ? $activePane : '';
2557
2558 46
        return $this;
2559
    }
2560
2561 10
    public function getXSplit(): int
2562
    {
2563 10
        return $this->xSplit;
2564
    }
2565
2566 11
    public function setXSplit(int $xSplit): self
2567
    {
2568 11
        $this->xSplit = $xSplit;
2569 11
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
2570 1
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
2571
        }
2572
2573 11
        return $this;
2574
    }
2575
2576 10
    public function getYSplit(): int
2577
    {
2578 10
        return $this->ySplit;
2579
    }
2580
2581 26
    public function setYSplit(int $ySplit): self
2582
    {
2583 26
        $this->ySplit = $ySplit;
2584 26
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
2585 1
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
2586
        }
2587
2588 26
        return $this;
2589
    }
2590
2591 15
    public function getPaneState(): string
2592
    {
2593 15
        return $this->paneState;
2594
    }
2595
2596
    public const PANE_FROZEN = 'frozen';
2597
    public const PANE_FROZENSPLIT = 'frozenSplit';
2598
    public const PANE_SPLIT = 'split';
2599
    private const VALIDPANESTATE = [self::PANE_FROZEN, self::PANE_SPLIT, self::PANE_FROZENSPLIT];
2600
    private const VALIDFROZENSTATE = [self::PANE_FROZEN, self::PANE_FROZENSPLIT];
2601
2602 26
    public function setPaneState(string $paneState): self
2603
    {
2604 26
        $this->paneState = in_array($paneState, self::VALIDPANESTATE, true) ? $paneState : '';
2605 26
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
2606 25
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
2607
        } else {
2608 3
            $this->freezePane = null;
2609
        }
2610
2611 26
        return $this;
2612
    }
2613
2614
    /**
2615
     * Insert a new row, updating all possible related data.
2616
     *
2617
     * @param int $before Insert before this row number
2618
     * @param int $numberOfRows Number of new rows to insert
2619
     *
2620
     * @return $this
2621
     */
2622 35
    public function insertNewRowBefore(int $before, int $numberOfRows = 1): static
2623
    {
2624 35
        if ($before >= 1) {
2625 34
            $objReferenceHelper = ReferenceHelper::getInstance();
2626 34
            $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
2627
        } else {
2628 1
            throw new Exception('Rows can only be inserted before at least row 1.');
2629
        }
2630
2631 34
        return $this;
2632
    }
2633
2634
    /**
2635
     * Insert a new column, updating all possible related data.
2636
     *
2637
     * @param string $before Insert before this column Name, eg: 'A'
2638
     * @param int $numberOfColumns Number of new columns to insert
2639
     *
2640
     * @return $this
2641
     */
2642 30
    public function insertNewColumnBefore(string $before, int $numberOfColumns = 1): static
2643
    {
2644 30
        if (!is_numeric($before)) {
2645 29
            $objReferenceHelper = ReferenceHelper::getInstance();
2646 29
            $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
2647
        } else {
2648 1
            throw new Exception('Column references should not be numeric.');
2649
        }
2650
2651 29
        return $this;
2652
    }
2653
2654
    /**
2655
     * Insert a new column, updating all possible related data.
2656
     *
2657
     * @param int $beforeColumnIndex Insert before this column ID (numeric column coordinate of the cell)
2658
     * @param int $numberOfColumns Number of new columns to insert
2659
     *
2660
     * @return $this
2661
     */
2662 2
    public function insertNewColumnBeforeByIndex(int $beforeColumnIndex, int $numberOfColumns = 1): static
2663
    {
2664 2
        if ($beforeColumnIndex >= 1) {
2665 1
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
2666
        }
2667
2668 1
        throw new Exception('Columns can only be inserted before at least column A (1).');
2669
    }
2670
2671
    /**
2672
     * Delete a row, updating all possible related data.
2673
     *
2674
     * @param int $row Remove rows, starting with this row number
2675
     * @param int $numberOfRows Number of rows to remove
2676
     *
2677
     * @return $this
2678
     */
2679 40
    public function removeRow(int $row, int $numberOfRows = 1): static
2680
    {
2681 40
        if ($row < 1) {
2682 1
            throw new Exception('Rows to be deleted should at least start from row 1.');
2683
        }
2684
2685 39
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
2686 39
        $highestRow = $this->getHighestDataRow();
2687 39
        $removedRowsCounter = 0;
2688
2689 39
        for ($r = 0; $r < $numberOfRows; ++$r) {
2690 39
            if ($row + $r <= $highestRow) {
2691 35
                $this->getCellCollection()->removeRow($row + $r);
2692 35
                ++$removedRowsCounter;
2693
            }
2694
        }
2695
2696 39
        $objReferenceHelper = ReferenceHelper::getInstance();
2697 39
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
2698 39
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
2699 35
            $this->getCellCollection()->removeRow($highestRow);
2700 35
            --$highestRow;
2701
        }
2702
2703 39
        $this->rowDimensions = $holdRowDimensions;
2704
2705 39
        return $this;
2706
    }
2707
2708 39
    private function removeRowDimensions(int $row, int $numberOfRows): array
2709
    {
2710 39
        $highRow = $row + $numberOfRows - 1;
2711 39
        $holdRowDimensions = [];
2712 39
        foreach ($this->rowDimensions as $rowDimension) {
2713 4
            $num = $rowDimension->getRowIndex();
2714 4
            if ($num < $row) {
2715 3
                $holdRowDimensions[$num] = $rowDimension;
2716 4
            } elseif ($num > $highRow) {
2717 4
                $num -= $numberOfRows;
2718 4
                $cloneDimension = clone $rowDimension;
2719 4
                $cloneDimension->setRowIndex($num);
2720 4
                $holdRowDimensions[$num] = $cloneDimension;
2721
            }
2722
        }
2723
2724 39
        return $holdRowDimensions;
2725
    }
2726
2727
    /**
2728
     * Remove a column, updating all possible related data.
2729
     *
2730
     * @param string $column Remove columns starting with this column name, eg: 'A'
2731
     * @param int $numberOfColumns Number of columns to remove
2732
     *
2733
     * @return $this
2734
     */
2735 32
    public function removeColumn(string $column, int $numberOfColumns = 1): static
2736
    {
2737 32
        if (is_numeric($column)) {
2738 1
            throw new Exception('Column references should not be numeric.');
2739
        }
2740
2741 31
        $highestColumn = $this->getHighestDataColumn();
2742 31
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
2743 31
        $pColumnIndex = Coordinate::columnIndexFromString($column);
2744
2745 31
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
2746
2747 31
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
2748 31
        $objReferenceHelper = ReferenceHelper::getInstance();
2749 31
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
2750
2751 31
        $this->columnDimensions = $holdColumnDimensions;
2752
2753 31
        if ($pColumnIndex > $highestColumnIndex) {
2754 2
            return $this;
2755
        }
2756
2757 29
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
2758
2759 29
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
2760 29
            $this->getCellCollection()->removeColumn($highestColumn);
2761 29
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
2762
        }
2763
2764 29
        $this->garbageCollect();
2765
2766 29
        return $this;
2767
    }
2768
2769 31
    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
2770
    {
2771 31
        $highCol = $pColumnIndex + $numberOfColumns - 1;
2772 31
        $holdColumnDimensions = [];
2773 31
        foreach ($this->columnDimensions as $columnDimension) {
2774 18
            $num = $columnDimension->getColumnNumeric();
2775 18
            if ($num < $pColumnIndex) {
2776 18
                $str = $columnDimension->getColumnIndex();
2777 18
                $holdColumnDimensions[$str] = $columnDimension;
2778 18
            } elseif ($num > $highCol) {
2779 18
                $cloneDimension = clone $columnDimension;
2780 18
                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
2781 18
                $str = $cloneDimension->getColumnIndex();
2782 18
                $holdColumnDimensions[$str] = $cloneDimension;
2783
            }
2784
        }
2785
2786 31
        return $holdColumnDimensions;
2787
    }
2788
2789
    /**
2790
     * Remove a column, updating all possible related data.
2791
     *
2792
     * @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
2793
     * @param int $numColumns Number of columns to remove
2794
     *
2795
     * @return $this
2796
     */
2797 2
    public function removeColumnByIndex(int $columnIndex, int $numColumns = 1): static
2798
    {
2799 2
        if ($columnIndex >= 1) {
2800 1
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2801
        }
2802
2803 1
        throw new Exception('Columns to be deleted should at least start from column A (1)');
2804
    }
2805
2806
    /**
2807
     * Show gridlines?
2808
     */
2809 858
    public function getShowGridlines(): bool
2810
    {
2811 858
        return $this->showGridlines;
2812
    }
2813
2814
    /**
2815
     * Set show gridlines.
2816
     *
2817
     * @param bool $showGridLines Show gridlines (true/false)
2818
     *
2819
     * @return $this
2820
     */
2821 307
    public function setShowGridlines(bool $showGridLines): self
2822
    {
2823 307
        $this->showGridlines = $showGridLines;
2824
2825 307
        return $this;
2826
    }
2827
2828
    /**
2829
     * Print gridlines?
2830
     */
2831 861
    public function getPrintGridlines(): bool
2832
    {
2833 861
        return $this->printGridlines;
2834
    }
2835
2836
    /**
2837
     * Set print gridlines.
2838
     *
2839
     * @param bool $printGridLines Print gridlines (true/false)
2840
     *
2841
     * @return $this
2842
     */
2843 102
    public function setPrintGridlines(bool $printGridLines): self
2844
    {
2845 102
        $this->printGridlines = $printGridLines;
2846
2847 102
        return $this;
2848
    }
2849
2850
    /**
2851
     * Show row and column headers?
2852
     */
2853 397
    public function getShowRowColHeaders(): bool
2854
    {
2855 397
        return $this->showRowColHeaders;
2856
    }
2857
2858
    /**
2859
     * Set show row and column headers.
2860
     *
2861
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2862
     *
2863
     * @return $this
2864
     */
2865 293
    public function setShowRowColHeaders(bool $showRowColHeaders): self
2866
    {
2867 293
        $this->showRowColHeaders = $showRowColHeaders;
2868
2869 293
        return $this;
2870
    }
2871
2872
    /**
2873
     * Show summary below? (Row/Column outlining).
2874
     */
2875 398
    public function getShowSummaryBelow(): bool
2876
    {
2877 398
        return $this->showSummaryBelow;
2878
    }
2879
2880
    /**
2881
     * Set show summary below.
2882
     *
2883
     * @param bool $showSummaryBelow Show summary below (true/false)
2884
     *
2885
     * @return $this
2886
     */
2887 299
    public function setShowSummaryBelow(bool $showSummaryBelow): self
2888
    {
2889 299
        $this->showSummaryBelow = $showSummaryBelow;
2890
2891 299
        return $this;
2892
    }
2893
2894
    /**
2895
     * Show summary right? (Row/Column outlining).
2896
     */
2897 398
    public function getShowSummaryRight(): bool
2898
    {
2899 398
        return $this->showSummaryRight;
2900
    }
2901
2902
    /**
2903
     * Set show summary right.
2904
     *
2905
     * @param bool $showSummaryRight Show summary right (true/false)
2906
     *
2907
     * @return $this
2908
     */
2909 299
    public function setShowSummaryRight(bool $showSummaryRight): self
2910
    {
2911 299
        $this->showSummaryRight = $showSummaryRight;
2912
2913 299
        return $this;
2914
    }
2915
2916
    /**
2917
     * Get comments.
2918
     *
2919
     * @return Comment[]
2920
     */
2921 879
    public function getComments()
2922
    {
2923 879
        return $this->comments;
2924
    }
2925
2926
    /**
2927
     * Set comments array for the entire sheet.
2928
     *
2929
     * @param Comment[] $comments
2930
     *
2931
     * @return $this
2932
     */
2933 74
    public function setComments(array $comments): self
2934
    {
2935 74
        $this->comments = $comments;
2936
2937 74
        return $this;
2938
    }
2939
2940
    /**
2941
     * Remove comment from cell.
2942
     *
2943
     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2944
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2945
     *
2946
     * @return $this
2947
     */
2948 44
    public function removeComment($cellCoordinate): self
2949
    {
2950 44
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
2951
2952 44
        if (Coordinate::coordinateIsRange($cellAddress)) {
2953 1
            throw new Exception('Cell coordinate string can not be a range of cells.');
2954 43
        } elseif (str_contains($cellAddress, '$')) {
2955 1
            throw new Exception('Cell coordinate string must not be absolute.');
2956 42
        } elseif ($cellAddress == '') {
2957 1
            throw new Exception('Cell coordinate can not be zero-length string.');
2958
        }
2959
        // Check if we have a comment for this cell and delete it
2960 41
        if (isset($this->comments[$cellAddress])) {
2961 2
            unset($this->comments[$cellAddress]);
2962
        }
2963
2964 41
        return $this;
2965
    }
2966
2967
    /**
2968
     * Get comment for cell.
2969
     *
2970
     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2971
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2972
     */
2973 91
    public function getComment($cellCoordinate): Comment
2974
    {
2975 91
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
2976
2977 91
        if (Coordinate::coordinateIsRange($cellAddress)) {
2978 1
            throw new Exception('Cell coordinate string can not be a range of cells.');
2979 90
        } elseif (str_contains($cellAddress, '$')) {
2980 1
            throw new Exception('Cell coordinate string must not be absolute.');
2981 89
        } elseif ($cellAddress == '') {
2982 1
            throw new Exception('Cell coordinate can not be zero-length string.');
2983
        }
2984
2985
        // Check if we already have a comment for this cell.
2986 88
        if (isset($this->comments[$cellAddress])) {
2987 59
            return $this->comments[$cellAddress];
2988
        }
2989
2990
        // If not, create a new comment.
2991 88
        $newComment = new Comment();
2992 88
        $this->comments[$cellAddress] = $newComment;
2993
2994 88
        return $newComment;
2995
    }
2996
2997
    /**
2998
     * Get comment for cell by using numeric cell coordinates.
2999
     *
3000
     * @deprecated 1.23.0
3001
     *      Use the getComment() method with a cell address such as 'C5' instead;,
3002
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
3003
     * @see Worksheet::getComment()
3004
     *
3005
     * @param int $columnIndex Numeric column coordinate of the cell
3006
     * @param int $row Numeric row coordinate of the cell
3007
     */
3008 1
    public function getCommentByColumnAndRow($columnIndex, $row): Comment
3009
    {
3010 1
        return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row);
3011
    }
3012
3013
    /**
3014
     * Get active cell.
3015
     *
3016
     * @return string Example: 'A1'
3017
     */
3018 8329
    public function getActiveCell()
3019
    {
3020 8329
        return $this->activeCell;
3021
    }
3022
3023
    /**
3024
     * Get selected cells.
3025
     *
3026
     * @return string
3027
     */
3028 8980
    public function getSelectedCells()
3029
    {
3030 8980
        return $this->selectedCells;
3031
    }
3032
3033
    /**
3034
     * Selected cell.
3035
     *
3036
     * @param string $coordinate Cell (i.e. A1)
3037
     *
3038
     * @return $this
3039
     */
3040 24
    public function setSelectedCell($coordinate): static
3041
    {
3042 24
        return $this->setSelectedCells($coordinate);
3043
    }
3044
3045
    /**
3046
     * Select a range of cells.
3047
     *
3048
     * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10'
3049
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
3050
     *              or a CellAddress or AddressRange object.
3051
     *
3052
     * @return $this
3053
     */
3054 9002
    public function setSelectedCells($coordinate): static
3055
    {
3056 9002
        if (is_string($coordinate)) {
3057 9001
            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
3058
        }
3059 9002
        $coordinate = Validations::validateCellOrCellRange($coordinate);
3060
3061 9002
        if (Coordinate::coordinateIsRange($coordinate)) {
3062 433
            [$first] = Coordinate::splitRange($coordinate);
3063 433
            $this->activeCell = $first[0];
3064
        } else {
3065 8937
            $this->activeCell = $coordinate;
3066
        }
3067 9002
        $this->selectedCells = $coordinate;
3068 9002
        $this->setSelectedCellsActivePane();
3069
3070 9002
        return $this;
3071
    }
3072
3073 9005
    private function setSelectedCellsActivePane(): void
3074
    {
3075 9005
        if (!empty($this->freezePane)) {
3076 45
            $coordinateC = Coordinate::indexesFromString($this->freezePane);
3077 45
            $coordinateT = Coordinate::indexesFromString($this->activeCell);
3078 45
            if ($coordinateC[0] === 1) {
3079 24
                $activePane = ($coordinateT[1] <= $coordinateC[1]) ? 'topLeft' : 'bottomLeft';
3080 22
            } elseif ($coordinateC[1] === 1) {
3081 2
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
3082 21
            } elseif ($coordinateT[1] <= $coordinateC[1]) {
3083 21
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
3084
            } else {
3085 6
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'bottomLeft' : 'bottomRight';
3086
            }
3087 45
            $this->setActivePane($activePane);
3088 45
            $this->panes[$activePane] = new Pane($activePane, $this->selectedCells, $this->activeCell);
3089
        }
3090
    }
3091
3092
    /**
3093
     * Selected cell by using numeric cell coordinates.
3094
     *
3095
     * @deprecated 1.23.0
3096
     *      Use the setSelectedCells() method with a cell address such as 'C5' instead;,
3097
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
3098
     * @see Worksheet::setSelectedCells()
3099
     *
3100
     * @param int $columnIndex Numeric column coordinate of the cell
3101
     * @param int $row Numeric row coordinate of the cell
3102
     *
3103
     * @return $this
3104
     */
3105
    public function setSelectedCellByColumnAndRow($columnIndex, $row): static
3106
    {
3107
        return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row);
3108
    }
3109
3110
    /**
3111
     * Get right-to-left.
3112
     *
3113
     * @return bool
3114
     */
3115 397
    public function getRightToLeft()
3116
    {
3117 397
        return $this->rightToLeft;
3118
    }
3119
3120
    /**
3121
     * Set right-to-left.
3122
     *
3123
     * @param bool $value Right-to-left true/false
3124
     *
3125
     * @return $this
3126
     */
3127 103
    public function setRightToLeft($value): static
3128
    {
3129 103
        $this->rightToLeft = $value;
3130
3131 103
        return $this;
3132
    }
3133
3134
    /**
3135
     * Fill worksheet from values in array.
3136
     *
3137
     * @param array $source Source array
3138
     * @param mixed $nullValue Value in source array that stands for blank cell
3139
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
3140
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
3141
     *
3142
     * @return $this
3143
     */
3144 602
    public function fromArray(array $source, mixed $nullValue = null, $startCell = 'A1', $strictNullComparison = false): static
3145
    {
3146
        //    Convert a 1-D array to 2-D (for ease of looping)
3147 602
        if (!is_array(end($source))) {
3148 39
            $source = [$source];
3149
        }
3150
3151
        // start coordinate
3152 602
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
3153
3154
        // Loop through $source
3155 602
        foreach ($source as $rowData) {
3156 602
            $currentColumn = $startColumn;
3157 602
            foreach ($rowData as $cellValue) {
3158 601
                if ($strictNullComparison) {
3159 254
                    if ($cellValue !== $nullValue) {
3160
                        // Set cell value
3161 254
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
3162
                    }
3163
                } else {
3164 348
                    if ($cellValue != $nullValue) {
3165
                        // Set cell value
3166 342
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
3167
                    }
3168
                }
3169 601
                ++$currentColumn;
3170
            }
3171 602
            ++$startRow;
3172
        }
3173
3174 602
        return $this;
3175
    }
3176
3177
    /**
3178
     * @throws Exception
3179
     * @throws \PhpOffice\PhpSpreadsheet\Calculation\Exception
3180
     *
3181
     * @return mixed
3182
     */
3183 116
    protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, mixed $nullValue)
3184
    {
3185 116
        $returnValue = $nullValue;
3186
3187 116
        if ($cell->getValue() !== null) {
3188 116
            if ($cell->getValue() instanceof RichText) {
3189 3
                $returnValue = $cell->getValue()->getPlainText();
3190
            } else {
3191 116
                $returnValue = ($calculateFormulas) ? $cell->getCalculatedValue() : $cell->getValue();
3192
            }
3193
3194 116
            if ($formatData) {
3195 89
                $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
3196 89
                $returnValue = NumberFormat::toFormattedString(
3197 89
                    $returnValue,
3198 89
                    $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL
3199 89
                );
3200
            }
3201
        }
3202
3203 116
        return $returnValue;
3204
    }
3205
3206
    /**
3207
     * Create array from a range of cells.
3208
     *
3209
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3210
     * @param bool $calculateFormulas Should formulas be calculated?
3211
     * @param bool $formatData Should formatting be applied to cell values?
3212
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3213
     *                             True - Return rows and columns indexed by their actual row and column IDs
3214
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3215
     *                            True - Don't return values for rows/columns that are defined as hidden.
3216
     */
3217 116
    public function rangeToArray(
3218
        string $range,
3219
        mixed $nullValue = null,
3220
        bool $calculateFormulas = true,
3221
        bool $formatData = true,
3222
        bool $returnCellRef = false,
3223
        bool $ignoreHidden = false
3224
    ): array {
3225 116
        $range = Validations::validateCellOrCellRange($range);
3226
3227 116
        $returnValue = [];
3228
        //    Identify the range that we need to extract from the worksheet
3229 116
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
3230 116
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
3231 116
        $minRow = $rangeStart[1];
3232 116
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
3233 116
        $maxRow = $rangeEnd[1];
3234
3235 116
        ++$maxCol;
3236
        $nullRow = $this->buildNullRow($nullValue, $minCol, $maxCol, $returnCellRef, $ignoreHidden);
3237 116
3238 116
        // Loop through rows
3239 116
        $r = -1;
3240 4
        for ($row = $minRow; $row <= $maxRow; ++$row) {
3241
            if (($ignoreHidden === true) && ($this->getRowDimension($row)->getVisible() === false)) {
3242 116
                continue;
3243 116
            }
3244
            $rowRef = $returnCellRef ? $row : ++$r;
3245 116
            $returnValue[$rowRef] = $nullRow;
3246 116
            $c = -1;
3247 2
            // Loop through columns in the current row
3248
            for ($col = $minCol; $col !== $maxCol; ++$col) {
3249 116
                if (($ignoreHidden === true) && ($this->getColumnDimension($col)->getVisible() === false)) {
3250
                    continue;
3251
                }
3252 116
                $columnRef = $returnCellRef ? $col : ++$c;
3253 116
                //    Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen
3254 116
                //        so we test and retrieve directly against cellCollection
3255 116
                $cell = $this->getCellCollection()->get("{$col}{$row}");
3256
                if ($cell !== null) {
3257
                    $returnValue[$rowRef][$columnRef] = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue);
3258
                }
3259
            }
3260
        }
3261 116
3262
        // Return
3263
        return $returnValue;
3264 16
    }
3265
3266 16
    /**
3267 16
     * Prepare a row data filled with null values to deduplicate the memory areas for empty rows.
3268 5
     *
3269 4
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3270
     * @param string $minCol Start column of the range
3271
     * @param string $maxCol End column of the range
3272 1
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3273
     *                              True - Return rows and columns indexed by their actual row and column IDs
3274
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3275 11
     *                             True - Don't return values for rows/columns that are defined as hidden.
3276
     */
3277
    private function buildNullRow(
3278
        mixed $nullValue,
3279
        string $minCol,
3280
        string $maxCol,
3281
        bool $returnCellRef,
3282
        bool $ignoreHidden,
3283 11
    ): array {
3284 2
        $nullRow = [];
3285 2
        $c = -1;
3286
        for ($col = $minCol; $col !== $maxCol; ++$col) {
3287
            if (($ignoreHidden === true) && ($this->getColumnDimension($col)->getVisible() === false)) {
3288
                continue;
3289
            }
3290
            $columnRef = $returnCellRef ? $col : ++$c;
3291
            $nullRow[$columnRef] = $nullValue;
3292
        }
3293
3294
        return $nullRow;
3295
    }
3296 11
3297
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
3298
    {
3299
        $namedRange = DefinedName::resolveName($definedName, $this);
3300
        if ($namedRange === null) {
3301
            if ($returnNullIfInvalid) {
3302
                return null;
3303
            }
3304
3305
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
3306
        }
3307
3308
        if ($namedRange->isFormula()) {
3309
            if ($returnNullIfInvalid) {
3310
                return null;
3311 2
            }
3312
3313
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
3314
        }
3315
3316
        if ($namedRange->getLocalOnly()) {
3317
            $worksheet = $namedRange->getWorksheet();
3318
            if ($worksheet === null || $this->getHashCode() !== $worksheet->getHashCode()) {
3319 2
                if ($returnNullIfInvalid) {
3320 2
                    return null;
3321 1
                }
3322 1
3323 1
                throw new Exception(
3324 1
                    'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
3325 1
                );
3326 1
            }
3327
        }
3328
3329
        return $namedRange;
3330 1
    }
3331
3332
    /**
3333
     * Create array from a range of cells.
3334
     *
3335
     * @param string $definedName The Named Range that should be returned
3336
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3337
     * @param bool $calculateFormulas Should formulas be calculated?
3338
     * @param bool $formatData Should formatting be applied to cell values?
3339
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3340
     *                             True - Return rows and columns indexed by their actual row and column IDs
3341
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3342
     *                            True - Don't return values for rows/columns that are defined as hidden.
3343
     */
3344 54
    public function namedRangeToArray(
3345
        string $definedName,
3346
        mixed $nullValue = null,
3347
        bool $calculateFormulas = true,
3348
        bool $formatData = true,
3349
        bool $returnCellRef = false,
3350
        bool $ignoreHidden = false
3351
    ): array {
3352 54
        $retVal = [];
3353
        $namedRange = $this->validateNamedRange($definedName);
3354
        if ($namedRange !== null) {
3355 54
            $cellRange = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
3356 54
            $cellRange = str_replace('$', '', $cellRange);
3357
            $workSheet = $namedRange->getWorksheet();
3358
            if ($workSheet !== null) {
3359 54
                $retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden);
3360
            }
3361
        }
3362
3363
        return $retVal;
3364
    }
3365
3366
    /**
3367
     * Create array from worksheet.
3368 67
     *
3369
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3370 67
     * @param bool $calculateFormulas Should formulas be calculated?
3371
     * @param bool $formatData Should formatting be applied to cell values?
3372
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3373
     *                             True - Return rows and columns indexed by their actual row and column IDs
3374
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3375
     *                            True - Don't return values for rows/columns that are defined as hidden.
3376
     */
3377
    public function toArray(
3378
        mixed $nullValue = null,
3379 16
        bool $calculateFormulas = true,
3380
        bool $formatData = true,
3381 16
        bool $returnCellRef = false,
3382
        bool $ignoreHidden = false
3383
    ): array {
3384
        // Garbage collect...
3385
        $this->garbageCollect();
3386
3387
        //    Identify the range that we need to extract from the worksheet
3388
        $maxCol = $this->getHighestColumn();
3389 927
        $maxRow = $this->getHighestRow();
3390
3391
        // Return
3392 927
        return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden);
3393
    }
3394
3395 927
    /**
3396 927
     * Get row iterator.
3397 927
     *
3398
     * @param int $startRow The row number at which to start iterating
3399
     * @param int $endRow The row number at which to stop iterating
3400 927
     */
3401 125
    public function getRowIterator($startRow = 1, $endRow = null): RowIterator
3402
    {
3403
        return new RowIterator($this, $startRow, $endRow);
3404
    }
3405 927
3406 76
    /**
3407
     * Get column iterator.
3408
     *
3409
     * @param string $startColumn The column address at which to start iterating
3410 927
     * @param string $endColumn The column address at which to stop iterating
3411
     */
3412
    public function getColumnIterator($startColumn = 'A', $endColumn = null): ColumnIterator
3413 927
    {
3414
        return new ColumnIterator($this, $startColumn, $endColumn);
3415 927
    }
3416
3417
    /**
3418 927
     * Run PhpSpreadsheet garbage collector.
3419
     *
3420
     * @return $this
3421
     */
3422
    public function garbageCollect(): static
3423
    {
3424
        // Flush cache
3425
        $this->getCellCollection()->get('A1');
3426 8992
3427
        // Lookup highest column and highest row if cells are cleaned
3428 8992
        $colRow = $this->getCellCollection()->getHighestRowAndColumn();
3429 8992
        $highestRow = $colRow['row'];
3430 8992
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
3431
3432
        // Loop through column dimensions
3433 8992
        foreach ($this->columnDimensions as $dimension) {
3434
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
3435
        }
3436
3437
        // Loop through row dimensions
3438
        foreach ($this->rowDimensions as $dimension) {
3439
            $highestRow = max($highestRow, $dimension->getRowIndex());
3440
        }
3441
3442
        // Cache values
3443
        if ($highestColumn < 1) {
3444
            $this->cachedHighestColumn = 1;
3445
        } else {
3446
            $this->cachedHighestColumn = $highestColumn;
3447
        }
3448
        $this->cachedHighestRow = $highestRow;
3449
3450
        // Return
3451 9796
        return $this;
3452
    }
3453 9796
3454 13
    /**
3455
     * Get hash code.
3456
     *
3457
     * @return string Hash code
3458 9794
     */
3459 9768
    public function getHashCode()
3460
    {
3461
        if ($this->dirty) {
3462 1188
            $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__);
3463 1188
            $this->dirty = false;
3464
        }
3465
3466 7
        return $this->hash;
3467
    }
3468
3469
    /**
3470
     * Extract worksheet title from range.
3471
     *
3472
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3473
     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3474
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3475
     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3476 78
     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3477
     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3478
     *
3479 78
     * @param string $range Range to extract title from
3480 37
     * @param bool $returnRange Return range? (see example)
3481
     *
3482
     * @return ($range is non-empty-string ? ($returnRange is true ? array{0: string, 1: string} : string) : ($returnRange is true ? array{0: null, 1: null} : null))
3483
     */
3484 78
    public static function extractSheetTitle($range, $returnRange = false): array|null|string
3485
    {
3486 78
        if (empty($range)) {
3487
            return $returnRange ? [null, null] : null;
3488
        }
3489
3490
        // Sheet title included?
3491
        if (($sep = strrpos($range, '!')) === false) {
3492
            return $returnRange ? ['', $range] : '';
3493
        }
3494
3495
        if ($returnRange) {
3496 41
            return [substr($range, 0, $sep), substr($range, $sep + 1)];
3497
        }
3498 41
3499 41
        return substr($range, $sep + 1);
3500
    }
3501 20
3502
    /**
3503
     * Get hyperlink.
3504 41
     *
3505
     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3506
     *
3507
     * @return Hyperlink
3508
     */
3509
    public function getHyperlink($cellCoordinate)
3510
    {
3511
        // return hyperlink if we already have one
3512 472
        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
3513
            return $this->hyperlinkCollection[$cellCoordinate];
3514 472
        }
3515
3516
        // else create hyperlink
3517
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
3518
3519
        return $this->hyperlinkCollection[$cellCoordinate];
3520
    }
3521
3522 455
    /**
3523
     * Set hyperlink.
3524 455
     *
3525
     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3526
     *
3527
     * @return $this
3528
     */
3529
    public function setHyperlink($cellCoordinate, ?Hyperlink $hyperlink = null): static
3530
    {
3531
        if ($hyperlink === null) {
3532
            unset($this->hyperlinkCollection[$cellCoordinate]);
3533
        } else {
3534 27
            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
3535
        }
3536
3537 27
        return $this;
3538 17
    }
3539
3540
    /**
3541
     * Hyperlink at a specific coordinate exists?
3542 26
     *
3543
     * @param string $coordinate eg: 'A1'
3544 26
     */
3545
    public function hyperlinkExists($coordinate): bool
3546
    {
3547
        return isset($this->hyperlinkCollection[$coordinate]);
3548
    }
3549
3550
    /**
3551
     * Get collection of hyperlinks.
3552
     *
3553
     * @return Hyperlink[]
3554 46
     */
3555
    public function getHyperlinkCollection()
3556 46
    {
3557 43
        return $this->hyperlinkCollection;
3558
    }
3559 7
3560
    /**
3561
     * Get data validation.
3562 46
     *
3563
     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3564
     *
3565
     * @return DataValidation
3566
     */
3567
    public function getDataValidation($cellCoordinate)
3568
    {
3569
        // return data validation if we already have one
3570 17
        if (isset($this->dataValidationCollection[$cellCoordinate])) {
3571
            return $this->dataValidationCollection[$cellCoordinate];
3572 17
        }
3573
3574
        // else create data validation
3575
        $this->dataValidationCollection[$cellCoordinate] = new DataValidation();
3576
3577
        return $this->dataValidationCollection[$cellCoordinate];
3578
    }
3579
3580 455
    /**
3581
     * Set data validation.
3582 455
     *
3583
     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3584
     *
3585
     * @return $this
3586
     */
3587
    public function setDataValidation($cellCoordinate, ?DataValidation $dataValidation = null): static
3588
    {
3589
        if ($dataValidation === null) {
3590
            unset($this->dataValidationCollection[$cellCoordinate]);
3591
        } else {
3592 18
            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
3593
        }
3594 18
3595 18
        return $this;
3596 18
    }
3597
3598 18
    /**
3599 18
     * Data validation at a specific coordinate exists?
3600 18
     *
3601
     * @param string $coordinate eg: 'A1'
3602 18
     */
3603
    public function dataValidationExists($coordinate): bool
3604
    {
3605 18
        return isset($this->dataValidationCollection[$coordinate]);
3606
    }
3607
3608 18
    /**
3609
     * Get collection of data validations.
3610
     *
3611 18
     * @return DataValidation[]
3612 4
     */
3613
    public function getDataValidationCollection()
3614 18
    {
3615
        return $this->dataValidationCollection;
3616 18
    }
3617
3618 18
    /**
3619
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3620
     *
3621
     * @param string $range
3622
     *
3623
     * @return string Adjusted range value
3624
     */
3625
    public function shrinkRangeToFit($range): string
3626 22
    {
3627
        $maxCol = $this->getHighestColumn();
3628 22
        $maxRow = $this->getHighestRow();
3629 22
        $maxCol = Coordinate::columnIndexFromString($maxCol);
3630
3631
        $rangeBlocks = explode(' ', $range);
3632 22
        foreach ($rangeBlocks as &$rangeSet) {
3633
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
3634
3635
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
3636
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
3637
            }
3638
            if ($rangeBoundaries[0][1] > $maxRow) {
3639
                $rangeBoundaries[0][1] = $maxRow;
3640 1
            }
3641
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
3642 1
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
3643
            }
3644 1
            if ($rangeBoundaries[1][1] > $maxRow) {
3645
                $rangeBoundaries[1][1] = $maxRow;
3646
            }
3647
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
3648
        }
3649
        unset($rangeSet);
3650 399
3651
        return implode(' ', $rangeBlocks);
3652 399
    }
3653
3654
    /**
3655
     * Get tab color.
3656
     *
3657
     * @return Color
3658
     */
3659
    public function getTabColor()
3660
    {
3661
        if ($this->tabColor === null) {
3662
            $this->tabColor = new Color();
3663
        }
3664
3665
        return $this->tabColor;
3666
    }
3667
3668
    /**
3669
     * Reset tab color.
3670
     *
3671
     * @return $this
3672
     */
3673
    public function resetTabColor(): static
3674
    {
3675
        $this->tabColor = null;
3676
3677
        return $this;
3678
    }
3679
3680 9
    /**
3681
     * Tab color set?
3682
     */
3683 9
    public function isTabColorSet(): bool
3684 8
    {
3685 8
        return $this->tabColor !== null;
3686 1
    }
3687 1
3688
    /**
3689
     * Copy worksheet (!= clone!).
3690 8
     */
3691
    public function copy(): static
3692
    {
3693
        return clone $this;
3694
    }
3695
3696
    /**
3697
     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3698
     *          exist in the collection for this row. false will be returned otherwise.
3699
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3700
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3701
     *                  cells, then the row will be considered empty.
3702
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3703
     *                  string value cells, then the row will be considered empty.
3704
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3705
     *                  If the only cells in the collection are null value or empty string value cells, then the row
3706
     *                  will be considered empty.
3707
     *
3708
     * @param int $definitionOfEmptyFlags
3709
     *              Possible Flag Values are:
3710 9
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3711
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3712
     */
3713 9
    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
3714 8
    {
3715 8
        try {
3716 1
            $iterator = new RowIterator($this, $rowId, $rowId);
3717 1
            $iterator->seek($rowId);
3718
            $row = $iterator->current();
3719
        } catch (Exception) {
3720 8
            return true;
3721
        }
3722
3723
        return $row->isEmpty($definitionOfEmptyFlags);
3724
    }
3725
3726 11
    /**
3727
     * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
3728
     *          exist in the collection for this column. false will be returned otherwise.
3729 11
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3730 11
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3731 11
     *                  cells, then the column will be considered empty.
3732
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3733
     *                  string value cells, then the column will be considered empty.
3734 11
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3735 11
     *                  If the only cells in the collection are null value or empty string value cells, then the column
3736 11
     *                  will be considered empty.
3737 11
     *
3738 11
     * @param int $definitionOfEmptyFlags
3739 11
     *              Possible Flag Values are:
3740 11
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3741 11
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3742 4
     */
3743 4
    public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
3744
    {
3745 11
        try {
3746 11
            $iterator = new ColumnIterator($this, $columnId, $columnId);
3747 11
            $iterator->seek($columnId);
3748 11
            $column = $iterator->current();
3749 1
        } catch (Exception) {
3750 1
            return true;
3751 1
        }
3752
3753 11
        return $column->isEmpty($definitionOfEmptyFlags);
3754 11
    }
3755 11
3756 11
    /**
3757 5
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3758 5
     */
3759
    public function __clone()
3760 11
    {
3761 11
        // @phpstan-ignore-next-line
3762 11
        foreach ($this as $key => $val) {
3763 11
            if ($key == 'parent') {
3764
                continue;
3765 11
            }
3766
3767
            if (is_object($val) || (is_array($val))) {
3768
                if ($key == 'cellCollection') {
3769
                    $newCollection = $this->getCellCollection()->cloneCellCollection($this);
3770
                    $this->cellCollection = $newCollection;
3771
                } elseif ($key == 'drawingCollection') {
3772
                    $currentCollection = $this->drawingCollection;
3773
                    $this->drawingCollection = new ArrayObject();
3774
                    foreach ($currentCollection as $item) {
3775
                        $newDrawing = clone $item;
3776
                        $newDrawing->setWorksheet($this);
3777
                    }
3778
                } elseif ($key == 'tableCollection') {
3779
                    $currentCollection = $this->tableCollection;
3780
                    $this->tableCollection = new ArrayObject();
3781 9921
                    foreach ($currentCollection as $item) {
3782
                        $newTable = clone $item;
3783
                        $newTable->setName($item->getName() . 'clone');
3784 9921
                        $this->addTable($newTable);
3785
                    }
3786
                } elseif ($key == 'chartCollection') {
3787
                    $currentCollection = $this->chartCollection;
3788 9921
                    $this->chartCollection = new ArrayObject();
3789 9921
                    foreach ($currentCollection as $item) {
3790
                        $newChart = clone $item;
3791
                        $this->addChart($newChart);
3792
                    }
3793 9921
                } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) {
3794
                    $newAutoFilter = clone $this->autoFilter;
3795
                    $this->autoFilter = $newAutoFilter;
3796
                    $this->autoFilter->setParent($this);
3797 9921
                } else {
3798
                    $this->{$key} = unserialize(serialize($val));
3799 9884
                }
3800
            }
3801
        }
3802 485
    }
3803
3804
    /**
3805 485
     * Define the code name of the sheet.
3806 485
     *
3807 240
     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
3808 240
     *                       silently space to underscore)
3809 2
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
3810 2
     *                       at parse time (by Readers), where titles can be assumed to be valid.
3811
     *
3812 240
     * @return $this
3813
     */
3814
    public function setCodeName($codeName, $validate = true): static
3815
    {
3816
        // Is this a 'rename' or not?
3817
        if ($this->getCodeName() == $codeName) {
3818
            return $this;
3819 485
        }
3820
3821
        if ($validate) {
3822
            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
3823
3824 9921
            // Syntax check
3825
            // throw an exception if not valid
3826 9921
            self::checkSheetCodeName($codeName);
3827
3828
            // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
3829
3830
            if ($this->parent !== null) {
3831
                // Is there already such sheet name?
3832
                if ($this->parent->sheetCodeNameExists($codeName)) {
3833
                    // Use name, but append with lowest possible integer
3834 9921
3835
                    if (Shared\StringHelper::countCharacters($codeName) > 29) {
3836 9921
                        $codeName = Shared\StringHelper::substring($codeName, 0, 29);
3837
                    }
3838
                    $i = 1;
3839
                    while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) {
3840
                        ++$i;
3841
                        if ($i == 10) {
3842 2
                            if (Shared\StringHelper::countCharacters($codeName) > 28) {
3843
                                $codeName = Shared\StringHelper::substring($codeName, 0, 28);
3844 2
                            }
3845
                        } elseif ($i == 100) {
3846
                            if (Shared\StringHelper::countCharacters($codeName) > 27) {
3847 4
                                $codeName = Shared\StringHelper::substring($codeName, 0, 27);
3848
                            }
3849 4
                        }
3850
                    }
3851
3852 102
                    $codeName .= '_' . $i; // ok, we have a valid name
3853
                }
3854 102
            }
3855
        }
3856
3857
        $this->codeName = $codeName;
3858
3859
        return $this;
3860 1
    }
3861
3862 1
    /**
3863 1
     * Return the code name of the sheet.
3864
     *
3865 1
     * @return null|string
3866 1
     */
3867
    public function getCodeName()
3868 1
    {
3869 1
        return $this->codeName;
3870 1
    }
3871 1
3872
    /**
3873
     * Sheet has a code name ?
3874
     */
3875
    public function hasCodeName(): bool
3876
    {
3877
        return $this->codeName !== null;
3878
    }
3879
3880 1
    public static function nameRequiresQuotes(string $sheetName): bool
3881
    {
3882 1
        return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1;
3883 1
    }
3884
3885
    public function isRowVisible(int $row): bool
3886
    {
3887
        return !$this->rowDimensionExists($row) || $this->getRowDimension($row)->getVisible();
3888 1
    }
3889
3890
    /**
3891 1
     * Same as Cell->isLocked, but without creating cell if it doesn't exist.
3892
     */
3893 1
    public function isCellLocked(string $coordinate): bool
3894 1
    {
3895 1
        if ($this->getProtection()->getsheet() !== true) {
3896 1
            return false;
3897
        }
3898
        if ($this->cellExists($coordinate)) {
3899 1
            return $this->getCell($coordinate)->isLocked();
3900
        }
3901
        $spreadsheet = $this->parent;
3902
        $xfIndex = $this->getXfIndex($coordinate);
3903 1
        if ($spreadsheet === null || $xfIndex === null) {
3904
            return true;
3905
        }
3906
3907
        return $spreadsheet->getCellXfByIndex($xfIndex)->getProtection()->getLocked() !== StyleProtection::PROTECTION_UNPROTECTED;
3908
    }
3909
3910
    /**
3911
     * Same as Cell->isHiddenOnFormulaBar, but without creating cell if it doesn't exist.
3912 817
     */
3913
    public function isCellHiddenOnFormulaBar(string $coordinate): bool
3914 817
    {
3915
        if ($this->cellExists($coordinate)) {
3916
            return $this->getCell($coordinate)->isHiddenOnFormulaBar();
3917 314
        }
3918
3919 314
        // cell doesn't exist, therefore isn't a formula,
3920
        // therefore isn't hidden on formula bar.
3921
        return false;
3922 314
    }
3923
3924 314
    private function getXfIndex(string $coordinate): ?int
3925
    {
3926
        [$column, $row] = Coordinate::coordinateFromString($coordinate);
3927
        $row = (int) $row;
3928
        $xfIndex = null;
3929
        if ($this->rowDimensionExists($row)) {
3930
            $xfIndex = $this->getRowDimension($row)->getXfIndex();
3931
        }
3932
        if ($xfIndex === null && $this->ColumnDimensionExists($column)) {
3933
            $xfIndex = $this->getColumnDimension($column)->getXfIndex();
3934 3
        }
3935
3936 3
        return $xfIndex;
3937 3
    }
3938 3
3939 2
    private string $backgroundImage = '';
3940 2
3941 2
    private string $backgroundMime = '';
3942 2
3943 2
    private string $backgroundExtension = '';
3944
3945
    public function getBackgroundImage(): string
3946 3
    {
3947
        return $this->backgroundImage;
3948
    }
3949
3950
    public function getBackgroundMime(): string
3951
    {
3952
        return $this->backgroundMime;
3953
    }
3954
3955
    public function getBackgroundExtension(): string
3956
    {
3957
        return $this->backgroundExtension;
3958
    }
3959
3960
    /**
3961
     * Set background image.
3962
     * Used on read/write for Xlsx.
3963
     * Used on write for Html.
3964
     *
3965
     * @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents
3966
     */
3967
    public function setBackgroundImage(string $backgroundImage): self
3968
    {
3969
        $imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => ''];
3970
        $mime = $imageArray['mime'];
3971
        if ($mime !== '') {
3972
            $extension = explode('/', $mime);
3973
            $extension = $extension[1];
3974
            $this->backgroundImage = $backgroundImage;
3975
            $this->backgroundMime = $mime;
3976
            $this->backgroundExtension = $extension;
3977
        }
3978
3979
        return $this;
3980
    }
3981
}
3982