Worksheet::writeSheetViews()   F
last analyzed

Complexity

Conditions 38
Paths > 20000

Size

Total Lines 153
Code Lines 95

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 90
CRAP Score 38.0147

Importance

Changes 0
Metric Value
eloc 95
c 0
b 0
f 0
dl 0
loc 153
ccs 90
cts 92
cp 0.9783
rs 0
cc 38
nc 9840640
nop 2
crap 38.0147

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
4
5
use Composer\Pcre\Preg;
6
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
7
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
8
use PhpOffice\PhpSpreadsheet\Cell\Cell;
9
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
10
use PhpOffice\PhpSpreadsheet\Cell\DataType;
11
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
12
use PhpOffice\PhpSpreadsheet\RichText\RichText;
13
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
14
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
15
use PhpOffice\PhpSpreadsheet\Style\Conditional;
16
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale;
17
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
18
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension;
19
use PhpOffice\PhpSpreadsheet\Style\Font;
20
use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension;
21
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
22
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as PhpspreadsheetWorksheet;
23
24
class Worksheet extends WriterPart
25
{
26
    private string $numberStoredAsText = '';
27
28
    private string $formula = '';
29
30
    private string $formulaRange = '';
31
32
    private string $twoDigitTextYear = '';
33
34
    private string $evalError = '';
35
36
    private bool $explicitStyle0;
37
38
    private bool $useDynamicArrays = false;
39
40
    /**
41
     * Write worksheet to XML format.
42
     *
43
     * @param string[] $stringTable
44
     * @param bool $includeCharts Flag indicating if we should write charts
45
     *
46
     * @return string XML Output
47
     */
48 438
    public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, array $stringTable = [], bool $includeCharts = false): string
49
    {
50 438
        $this->useDynamicArrays = $this->getParentWriter()->useDynamicArrays();
51 438
        $this->explicitStyle0 = $this->getParentWriter()->getExplicitStyle0();
52 438
        $worksheet->calculateArrays($this->getParentWriter()->getPreCalculateFormulas());
53 438
        $this->numberStoredAsText = '';
54 438
        $this->formula = '';
55 438
        $this->formulaRange = '';
56 438
        $this->twoDigitTextYear = '';
57 438
        $this->evalError = '';
58
        // Create XML writer
59 438
        $objWriter = null;
60 438
        if ($this->getParentWriter()->getUseDiskCaching()) {
61
            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
62
        } else {
63 438
            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
64
        }
65
66
        // XML header
67 438
        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
68
69
        // Worksheet
70 438
        $objWriter->startElement('worksheet');
71 438
        $objWriter->writeAttribute('xmlns', Namespaces::MAIN);
72 438
        $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
73
74 438
        $objWriter->writeAttribute('xmlns:xdr', Namespaces::SPREADSHEET_DRAWING);
75 438
        $objWriter->writeAttribute('xmlns:x14', Namespaces::DATA_VALIDATIONS1);
76 438
        $objWriter->writeAttribute('xmlns:xm', Namespaces::DATA_VALIDATIONS2);
77 438
        $objWriter->writeAttribute('xmlns:mc', Namespaces::COMPATIBILITY);
78 438
        $objWriter->writeAttribute('mc:Ignorable', 'x14ac');
79 438
        $objWriter->writeAttribute('xmlns:x14ac', Namespaces::SPREADSHEETML_AC);
80
81
        // sheetPr
82 438
        $this->writeSheetPr($objWriter, $worksheet);
83
84
        // Dimension
85 438
        $this->writeDimension($objWriter, $worksheet);
86
87
        // sheetViews
88 438
        $this->writeSheetViews($objWriter, $worksheet);
89
90
        // sheetFormatPr
91 438
        $this->writeSheetFormatPr($objWriter, $worksheet);
92
93
        // cols
94 438
        $this->writeCols($objWriter, $worksheet);
95
96
        // sheetData
97 438
        $this->writeSheetData($objWriter, $worksheet, $stringTable);
98
99
        // sheetProtection
100 437
        $this->writeSheetProtection($objWriter, $worksheet);
101
102
        // protectedRanges
103 437
        $this->writeProtectedRanges($objWriter, $worksheet);
104
105
        // autoFilter
106 437
        $this->writeAutoFilter($objWriter, $worksheet);
107
108
        // mergeCells
109 437
        $this->writeMergeCells($objWriter, $worksheet);
110
111
        // conditionalFormatting
112 437
        $this->writeConditionalFormatting($objWriter, $worksheet);
113
114
        // dataValidations
115 437
        $this->writeDataValidations($objWriter, $worksheet);
116
117
        // hyperlinks
118 437
        $this->writeHyperlinks($objWriter, $worksheet);
119
120
        // Print options
121 437
        $this->writePrintOptions($objWriter, $worksheet);
122
123
        // Page margins
124 437
        $this->writePageMargins($objWriter, $worksheet);
125
126
        // Page setup
127 437
        $this->writePageSetup($objWriter, $worksheet);
128
129
        // Header / footer
130 437
        $this->writeHeaderFooter($objWriter, $worksheet);
131
132
        // Breaks
133 437
        $this->writeBreaks($objWriter, $worksheet);
134
135
        // IgnoredErrors
136 437
        $this->writeIgnoredErrors($objWriter);
137
138
        // Drawings and/or Charts
139 437
        $this->writeDrawings($objWriter, $worksheet, $includeCharts);
140
141
        // LegacyDrawing
142 437
        $this->writeLegacyDrawing($objWriter, $worksheet);
143
144
        // LegacyDrawingHF
145 437
        $this->writeLegacyDrawingHF($objWriter, $worksheet);
146
147
        // AlternateContent
148 437
        $this->writeAlternateContent($objWriter, $worksheet);
149
150
        // BackgroundImage must come after ignored, before table
151 437
        $this->writeBackgroundImage($objWriter, $worksheet);
152
153
        // Table
154 437
        $this->writeTable($objWriter, $worksheet);
155
156
        // ConditionalFormattingRuleExtensionList
157
        // (Must be inserted last. Not insert last, an Excel parse error will occur)
158 437
        $this->writeExtLst($objWriter, $worksheet);
159
160 437
        $objWriter->endElement();
161
162
        // Return
163 437
        return $objWriter->getData();
164
    }
165
166 437
    private function writeIgnoredError(XMLWriter $objWriter, bool &$started, string $attr, string $cells): void
167
    {
168 437
        if ($cells !== '') {
169 4
            if (!$started) {
170 4
                $objWriter->startElement('ignoredErrors');
171 4
                $started = true;
172
            }
173 4
            $objWriter->startElement('ignoredError');
174 4
            $objWriter->writeAttribute('sqref', substr($cells, 1));
175 4
            $objWriter->writeAttribute($attr, '1');
176 4
            $objWriter->endElement();
177
        }
178
    }
179
180 437
    private function writeIgnoredErrors(XMLWriter $objWriter): void
181
    {
182 437
        $started = false;
183 437
        $this->writeIgnoredError($objWriter, $started, 'numberStoredAsText', $this->numberStoredAsText);
184 437
        $this->writeIgnoredError($objWriter, $started, 'formula', $this->formula);
185 437
        $this->writeIgnoredError($objWriter, $started, 'formulaRange', $this->formulaRange);
186 437
        $this->writeIgnoredError($objWriter, $started, 'twoDigitTextYear', $this->twoDigitTextYear);
187 437
        $this->writeIgnoredError($objWriter, $started, 'evalError', $this->evalError);
188 437
        if ($started) {
189 4
            $objWriter->endElement();
190
        }
191
    }
192
193
    /**
194
     * Write SheetPr.
195
     */
196 438
    private function writeSheetPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
197
    {
198
        // sheetPr
199 438
        $objWriter->startElement('sheetPr');
200 438
        if ($worksheet->getParentOrThrow()->hasMacros()) {
201
            //if the workbook have macros, we need to have codeName for the sheet
202 2
            if (!$worksheet->hasCodeName()) {
203
                $worksheet->setCodeName($worksheet->getTitle());
204
            }
205 2
            self::writeAttributeNotNull($objWriter, 'codeName', $worksheet->getCodeName());
206
        }
207 438
        $autoFilterRange = $worksheet->getAutoFilter()->getRange();
208 438
        if (!empty($autoFilterRange)) {
209 10
            $objWriter->writeAttribute('filterMode', '1');
210 10
            if (!$worksheet->getAutoFilter()->getEvaluated()) {
211 6
                $worksheet->getAutoFilter()->showHideRows();
212
            }
213
        }
214 438
        $tables = $worksheet->getTableCollection();
215 438
        if (count($tables)) {
216 8
            foreach ($tables as $table) {
217 8
                if (!$table->getAutoFilter()->getEvaluated()) {
218 8
                    $table->getAutoFilter()->showHideRows();
219
                }
220
            }
221
        }
222
223
        // tabColor
224 438
        if ($worksheet->isTabColorSet()) {
225 8
            $objWriter->startElement('tabColor');
226 8
            $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB() ?? '');
227 8
            $objWriter->endElement();
228
        }
229
230
        // outlinePr
231 438
        $objWriter->startElement('outlinePr');
232 438
        $objWriter->writeAttribute('summaryBelow', ($worksheet->getShowSummaryBelow() ? '1' : '0'));
233 438
        $objWriter->writeAttribute('summaryRight', ($worksheet->getShowSummaryRight() ? '1' : '0'));
234 438
        $objWriter->endElement();
235
236
        // pageSetUpPr
237 438
        if ($worksheet->getPageSetup()->getFitToPage()) {
238 6
            $objWriter->startElement('pageSetUpPr');
239 6
            $objWriter->writeAttribute('fitToPage', '1');
240 6
            $objWriter->endElement();
241
        }
242
243 438
        $objWriter->endElement();
244
    }
245
246
    /**
247
     * Write Dimension.
248
     */
249 438
    private function writeDimension(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
250
    {
251
        // dimension
252 438
        $objWriter->startElement('dimension');
253 438
        $objWriter->writeAttribute('ref', $worksheet->calculateWorksheetDimension());
254 438
        $objWriter->endElement();
255
    }
256
257
    /**
258
     * Write SheetViews.
259
     */
260 438
    private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
261
    {
262
        // sheetViews
263 438
        $objWriter->startElement('sheetViews');
264
265
        // Sheet selected?
266 438
        $sheetSelected = false;
267 438
        if ($this->getParentWriter()->getSpreadsheet()->getIndex($worksheet) == $this->getParentWriter()->getSpreadsheet()->getActiveSheetIndex()) {
268 433
            $sheetSelected = true;
269
        }
270
271
        // sheetView
272 438
        $objWriter->startElement('sheetView');
273 438
        $objWriter->writeAttribute('tabSelected', $sheetSelected ? '1' : '0');
274 438
        $objWriter->writeAttribute('workbookViewId', '0');
275
276
        // Zoom scales
277 438
        $zoomScale = $worksheet->getSheetView()->getZoomScale();
278 438
        if ($zoomScale !== 100 && $zoomScale !== null) {
279 9
            $objWriter->writeAttribute('zoomScale', (string) $zoomScale);
280
        }
281 438
        $zoomScale = $worksheet->getSheetView()->getZoomScaleNormal();
282 438
        if ($zoomScale !== 100 && $zoomScale !== null) {
283 6
            $objWriter->writeAttribute('zoomScaleNormal', (string) $zoomScale);
284
        }
285 438
        $zoomScale = $worksheet->getSheetView()->getZoomScalePageLayoutView();
286 438
        if ($zoomScale !== 100) {
287 4
            $objWriter->writeAttribute('zoomScalePageLayoutView', (string) $zoomScale);
288
        }
289 438
        $zoomScale = $worksheet->getSheetView()->getZoomScaleSheetLayoutView();
290 438
        if ($zoomScale !== 100) {
291 4
            $objWriter->writeAttribute('zoomScaleSheetLayoutView', (string) $zoomScale);
292
        }
293
294
        // Show zeros (Excel also writes this attribute only if set to false)
295 438
        if ($worksheet->getSheetView()->getShowZeros() === false) {
296
            $objWriter->writeAttribute('showZeros', '0');
297
        }
298
299
        // View Layout Type
300 438
        if ($worksheet->getSheetView()->getView() !== SheetView::SHEETVIEW_NORMAL) {
301 6
            $objWriter->writeAttribute('view', $worksheet->getSheetView()->getView());
302
        }
303
304
        // Gridlines
305 438
        if ($worksheet->getShowGridlines()) {
306 434
            $objWriter->writeAttribute('showGridLines', 'true');
307
        } else {
308 9
            $objWriter->writeAttribute('showGridLines', 'false');
309
        }
310
311
        // Row and column headers
312 438
        if ($worksheet->getShowRowColHeaders()) {
313 438
            $objWriter->writeAttribute('showRowColHeaders', '1');
314
        } else {
315
            $objWriter->writeAttribute('showRowColHeaders', '0');
316
        }
317
318
        // Right-to-left
319 438
        if ($worksheet->getRightToLeft()) {
320 1
            $objWriter->writeAttribute('rightToLeft', 'true');
321
        }
322
323 438
        $topLeftCell = $worksheet->getTopLeftCell();
324 438
        if (!empty($topLeftCell) && $worksheet->getPaneState() !== PhpspreadsheetWorksheet::PANE_FROZEN && $worksheet->getPaneState() !== PhpspreadsheetWorksheet::PANE_FROZENSPLIT) {
325 12
            $objWriter->writeAttribute('topLeftCell', $topLeftCell);
326
        }
327 438
        $activeCell = $worksheet->getActiveCell();
328 438
        $sqref = $worksheet->getSelectedCells();
329
330
        // Pane
331 438
        if ($worksheet->usesPanes()) {
332 10
            $objWriter->startElement('pane');
333 10
            $xSplit = $worksheet->getXSplit();
334 10
            $ySplit = $worksheet->getYSplit();
335 10
            $pane = $worksheet->getActivePane();
336 10
            $paneTopLeftCell = $worksheet->getPaneTopLeftCell();
337 10
            $paneState = $worksheet->getPaneState();
338 10
            $normalFreeze = '';
339 10
            if ($paneState === PhpspreadsheetWorksheet::PANE_FROZEN) {
340 10
                if ($ySplit > 0) {
341 10
                    $normalFreeze = ($xSplit <= 0) ? 'bottomLeft' : 'bottomRight';
342
                } else {
343 1
                    $normalFreeze = 'topRight';
344
                }
345
            }
346 10
            if ($xSplit > 0) {
347 4
                $objWriter->writeAttribute('xSplit', "$xSplit");
348
            }
349 10
            if ($ySplit > 0) {
350 10
                $objWriter->writeAttribute('ySplit', "$ySplit");
351
            }
352 10
            if ($normalFreeze !== '') {
353 10
                $objWriter->writeAttribute('activePane', $normalFreeze);
354 1
            } elseif ($pane !== '') {
355 1
                $objWriter->writeAttribute('activePane', $pane);
356
            }
357 10
            if ($paneState !== '') {
358 10
                $objWriter->writeAttribute('state', $paneState);
359
            }
360 10
            if ($paneTopLeftCell !== '') {
361 10
                $objWriter->writeAttribute('topLeftCell', $paneTopLeftCell);
362
            }
363 10
            $objWriter->endElement(); // pane
364
365 10
            if ($normalFreeze !== '') {
366 10
                $objWriter->startElement('selection');
367 10
                $objWriter->writeAttribute('pane', $normalFreeze);
368 10
                if ($activeCell !== '') {
369 10
                    $objWriter->writeAttribute('activeCell', $activeCell);
370
                }
371 10
                if ($sqref !== '') {
372 10
                    $objWriter->writeAttribute('sqref', $sqref);
373
                }
374 10
                $objWriter->endElement(); // selection
375 10
                $sqref = $activeCell = '';
376
            } else {
377 1
                foreach ($worksheet->getPanes() as $panex) {
378 1
                    if ($panex !== null) {
379 1
                        $sqref = $activeCell = '';
380 1
                        $objWriter->startElement('selection');
381 1
                        $objWriter->writeAttribute('pane', $panex->getPosition());
382 1
                        $activeCellPane = $panex->getActiveCell();
383 1
                        if ($activeCellPane !== '') {
384 1
                            $objWriter->writeAttribute('activeCell', $activeCellPane);
385
                        }
386 1
                        $sqrefPane = $panex->getSqref();
387 1
                        if ($sqrefPane !== '') {
388 1
                            $objWriter->writeAttribute('sqref', $sqrefPane);
389
                        }
390 1
                        $objWriter->endElement(); // selection
391
                    }
392
                }
393
            }
394
        }
395
396
        // Selection
397
        // Only need to write selection element if we have a split pane
398
        // We cheat a little by over-riding the active cell selection, setting it to the split cell
399 438
        if (!empty($sqref) || !empty($activeCell)) {
400 431
            $objWriter->startElement('selection');
401 431
            if (!empty($activeCell)) {
402 431
                $objWriter->writeAttribute('activeCell', $activeCell);
403
            }
404 431
            if (!empty($sqref)) {
405 431
                $objWriter->writeAttribute('sqref', $sqref);
406
            }
407 431
            $objWriter->endElement(); // selection
408
        }
409
410 438
        $objWriter->endElement();
411
412 438
        $objWriter->endElement();
413
    }
414
415
    /**
416
     * Write SheetFormatPr.
417
     */
418 438
    private function writeSheetFormatPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
419
    {
420
        // sheetFormatPr
421 438
        $objWriter->startElement('sheetFormatPr');
422
423
        // Default row height
424 438
        if ($worksheet->getDefaultRowDimension()->getRowHeight() >= 0) {
425 16
            $objWriter->writeAttribute('customHeight', 'true');
426 16
            $objWriter->writeAttribute('defaultRowHeight', StringHelper::formatNumber($worksheet->getDefaultRowDimension()->getRowHeight()));
427
        } else {
428 423
            $objWriter->writeAttribute('defaultRowHeight', '14.4');
429
        }
430
431
        // Set Zero Height row
432 438
        if ($worksheet->getDefaultRowDimension()->getZeroHeight()) {
433
            $objWriter->writeAttribute('zeroHeight', '1');
434
        }
435
436
        // Default column width
437 438
        if ($worksheet->getDefaultColumnDimension()->getWidth() >= 0) {
438 28
            $objWriter->writeAttribute('defaultColWidth', StringHelper::formatNumber($worksheet->getDefaultColumnDimension()->getWidth()));
439
        }
440
441
        // Outline level - row
442 438
        $outlineLevelRow = 0;
443 438
        foreach ($worksheet->getRowDimensions() as $dimension) {
444 58
            if ($dimension->getOutlineLevel() > $outlineLevelRow) {
445
                $outlineLevelRow = $dimension->getOutlineLevel();
446
            }
447
        }
448 438
        $objWriter->writeAttribute('outlineLevelRow', (string) (int) $outlineLevelRow);
449
450
        // Outline level - column
451 438
        $outlineLevelCol = 0;
452 438
        foreach ($worksheet->getColumnDimensions() as $dimension) {
453 94
            if ($dimension->getOutlineLevel() > $outlineLevelCol) {
454 1
                $outlineLevelCol = $dimension->getOutlineLevel();
455
            }
456
        }
457 438
        $objWriter->writeAttribute('outlineLevelCol', (string) (int) $outlineLevelCol);
458
459 438
        $objWriter->endElement();
460
    }
461
462
    /**
463
     * Write Cols.
464
     */
465 438
    private function writeCols(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
466
    {
467
        // cols
468 438
        if (count($worksheet->getColumnDimensions()) > 0) {
469 94
            $objWriter->startElement('cols');
470
471 94
            $worksheet->calculateColumnWidths();
472
473
            // Loop through column dimensions
474 94
            foreach ($worksheet->getColumnDimensions() as $colDimension) {
475
                // col
476 94
                $objWriter->startElement('col');
477 94
                $objWriter->writeAttribute('min', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex()));
478 94
                $objWriter->writeAttribute('max', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex()));
479
480 94
                if ($colDimension->getWidth() < 0) {
481
                    // No width set, apply default of 10
482 3
                    $objWriter->writeAttribute('width', '9.10');
483
                } else {
484
                    // Width set
485 93
                    $objWriter->writeAttribute('width', StringHelper::formatNumber($colDimension->getWidth()));
486
                }
487
488
                // Column visibility
489 94
                if ($colDimension->getVisible() === false) {
490 8
                    $objWriter->writeAttribute('hidden', 'true');
491
                }
492
493
                // Auto size?
494 94
                if ($colDimension->getAutoSize()) {
495 34
                    $objWriter->writeAttribute('bestFit', 'true');
496
                }
497
498
                // Custom width?
499 94
                if ($colDimension->getWidth() != $worksheet->getDefaultColumnDimension()->getWidth()) {
500 91
                    $objWriter->writeAttribute('customWidth', 'true');
501
                }
502
503
                // Collapsed
504 94
                if ($colDimension->getCollapsed() === true) {
505 1
                    $objWriter->writeAttribute('collapsed', 'true');
506
                }
507
508
                // Outline level
509 94
                if ($colDimension->getOutlineLevel() > 0) {
510 1
                    $objWriter->writeAttribute('outlineLevel', (string) $colDimension->getOutlineLevel());
511
                }
512
513
                // Style
514 94
                $objWriter->writeAttribute('style', (string) $colDimension->getXfIndex());
515
516 94
                $objWriter->endElement();
517
            }
518
519 94
            $objWriter->endElement();
520
        }
521
    }
522
523
    /**
524
     * Write SheetProtection.
525
     */
526 437
    private function writeSheetProtection(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
527
    {
528 437
        $protection = $worksheet->getProtection();
529 437
        if (!$protection->isProtectionEnabled()) {
530 414
            return;
531
        }
532
        // sheetProtection
533 33
        $objWriter->startElement('sheetProtection');
534
535 33
        if ($protection->getAlgorithm()) {
536 2
            $objWriter->writeAttribute('algorithmName', $protection->getAlgorithm());
537 2
            $objWriter->writeAttribute('hashValue', $protection->getPassword());
538 2
            $objWriter->writeAttribute('saltValue', $protection->getSalt());
539 2
            $objWriter->writeAttribute('spinCount', (string) $protection->getSpinCount());
540 32
        } elseif ($protection->getPassword() !== '') {
541 5
            $objWriter->writeAttribute('password', $protection->getPassword());
542
        }
543
544 33
        self::writeProtectionAttribute($objWriter, 'sheet', $protection->getSheet());
545 33
        self::writeProtectionAttribute($objWriter, 'objects', $protection->getObjects());
546 33
        self::writeProtectionAttribute($objWriter, 'scenarios', $protection->getScenarios());
547 33
        self::writeProtectionAttribute($objWriter, 'formatCells', $protection->getFormatCells());
548 33
        self::writeProtectionAttribute($objWriter, 'formatColumns', $protection->getFormatColumns());
549 33
        self::writeProtectionAttribute($objWriter, 'formatRows', $protection->getFormatRows());
550 33
        self::writeProtectionAttribute($objWriter, 'insertColumns', $protection->getInsertColumns());
551 33
        self::writeProtectionAttribute($objWriter, 'insertRows', $protection->getInsertRows());
552 33
        self::writeProtectionAttribute($objWriter, 'insertHyperlinks', $protection->getInsertHyperlinks());
553 33
        self::writeProtectionAttribute($objWriter, 'deleteColumns', $protection->getDeleteColumns());
554 33
        self::writeProtectionAttribute($objWriter, 'deleteRows', $protection->getDeleteRows());
555 33
        self::writeProtectionAttribute($objWriter, 'sort', $protection->getSort());
556 33
        self::writeProtectionAttribute($objWriter, 'autoFilter', $protection->getAutoFilter());
557 33
        self::writeProtectionAttribute($objWriter, 'pivotTables', $protection->getPivotTables());
558 33
        self::writeProtectionAttribute($objWriter, 'selectLockedCells', $protection->getSelectLockedCells());
559 33
        self::writeProtectionAttribute($objWriter, 'selectUnlockedCells', $protection->getSelectUnlockedCells());
560 33
        $objWriter->endElement();
561
    }
562
563 33
    private static function writeProtectionAttribute(XMLWriter $objWriter, string $name, ?bool $value): void
564
    {
565 33
        if ($value === true) {
566 22
            $objWriter->writeAttribute($name, '1');
567 33
        } elseif ($value === false) {
568 20
            $objWriter->writeAttribute($name, '0');
569
        }
570
    }
571
572 75
    private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition, string $attr, string $val): void
573
    {
574 75
        if ($condition) {
575 74
            $objWriter->writeAttribute($attr, $val);
576
        }
577
    }
578
579 2
    private static function writeAttributeNotNull(XMLWriter $objWriter, string $attr, ?string $val): void
580
    {
581 2
        if ($val !== null) {
582 2
            $objWriter->writeAttribute($attr, $val);
583
        }
584
    }
585
586 275
    private static function writeElementIf(XMLWriter $objWriter, bool $condition, string $attr, string $val): void
587
    {
588 275
        if ($condition) {
589 257
            $objWriter->writeElement($attr, $val);
590
        }
591
    }
592
593 44
    private static function writeOtherCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
594
    {
595 44
        $conditions = $conditional->getConditions();
596
        if (
597 44
            $conditional->getConditionType() == Conditional::CONDITION_CELLIS
598 44
            || $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION
599 44
            || !empty($conditions)
600
        ) {
601 33
            foreach ($conditions as $formula) {
602
                // Formula
603 33
                if (is_bool($formula)) {
604 1
                    $formula = $formula ? 'TRUE' : 'FALSE';
605
                }
606 33
                $objWriter->writeElement('formula', FunctionPrefix::addFunctionPrefix("$formula"));
607
            }
608
        } else {
609 11
            if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) {
610
                // formula copied from ms xlsx xml source file
611 2
                $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))=0');
612 9
            } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSBLANKS) {
613
                // formula copied from ms xlsx xml source file
614 1
                $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))>0');
615 8
            } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSERRORS) {
616
                // formula copied from ms xlsx xml source file
617 1
                $objWriter->writeElement('formula', 'ISERROR(' . $cellCoordinate . ')');
618 7
            } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSERRORS) {
619
                // formula copied from ms xlsx xml source file
620 1
                $objWriter->writeElement('formula', 'NOT(ISERROR(' . $cellCoordinate . '))');
621
            }
622
        }
623
    }
624
625 12
    private static function writeTimePeriodCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
626
    {
627 12
        $txt = $conditional->getText();
628 12
        if (!empty($txt)) {
629 12
            $objWriter->writeAttribute('timePeriod', $txt);
630 12
            if (empty($conditional->getConditions())) {
631 10
                if ($conditional->getOperatorType() == Conditional::TIMEPERIOD_TODAY) {
632 1
                    $objWriter->writeElement('formula', 'FLOOR(' . $cellCoordinate . ')=TODAY()');
633 9
                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_TOMORROW) {
634 1
                    $objWriter->writeElement('formula', 'FLOOR(' . $cellCoordinate . ')=TODAY()+1');
635 8
                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_YESTERDAY) {
636 1
                    $objWriter->writeElement('formula', 'FLOOR(' . $cellCoordinate . ')=TODAY()-1');
637 7
                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_LAST_7_DAYS) {
638 1
                    $objWriter->writeElement('formula', 'AND(TODAY()-FLOOR(' . $cellCoordinate . ',1)<=6,FLOOR(' . $cellCoordinate . ',1)<=TODAY())');
639 6
                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_LAST_WEEK) {
640 1
                    $objWriter->writeElement('formula', 'AND(TODAY()-ROUNDDOWN(' . $cellCoordinate . ',0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(' . $cellCoordinate . ',0)<(WEEKDAY(TODAY())+7))');
641 5
                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_THIS_WEEK) {
642 1
                    $objWriter->writeElement('formula', 'AND(TODAY()-ROUNDDOWN(' . $cellCoordinate . ',0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(' . $cellCoordinate . ',0)-TODAY()<=7-WEEKDAY(TODAY()))');
643 4
                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_NEXT_WEEK) {
644 1
                    $objWriter->writeElement('formula', 'AND(ROUNDDOWN(' . $cellCoordinate . ',0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(' . $cellCoordinate . ',0)-TODAY()<(15-WEEKDAY(TODAY())))');
645 3
                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_LAST_MONTH) {
646 1
                    $objWriter->writeElement('formula', 'AND(MONTH(' . $cellCoordinate . ')=MONTH(EDATE(TODAY(),0-1)),YEAR(' . $cellCoordinate . ')=YEAR(EDATE(TODAY(),0-1)))');
647 2
                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_THIS_MONTH) {
648 1
                    $objWriter->writeElement('formula', 'AND(MONTH(' . $cellCoordinate . ')=MONTH(TODAY()),YEAR(' . $cellCoordinate . ')=YEAR(TODAY()))');
649 1
                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_NEXT_MONTH) {
650 1
                    $objWriter->writeElement('formula', 'AND(MONTH(' . $cellCoordinate . ')=MONTH(EDATE(TODAY(),0+1)),YEAR(' . $cellCoordinate . ')=YEAR(EDATE(TODAY(),0+1)))');
651
                }
652
            } else {
653 2
                $objWriter->writeElement('formula', (string) ($conditional->getConditions()[0]));
654
            }
655
        }
656
    }
657
658 9
    private static function writeTextCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
659
    {
660 9
        $txt = $conditional->getText();
661 9
        if (!empty($txt)) {
662 8
            $objWriter->writeAttribute('text', $txt);
663 8
            if (empty($conditional->getConditions())) {
664 5
                if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) {
665 2
                    $objWriter->writeElement('formula', 'NOT(ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . ')))');
666 4
                } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_BEGINSWITH) {
667 2
                    $objWriter->writeElement('formula', 'LEFT(' . $cellCoordinate . ',LEN("' . $txt . '"))="' . $txt . '"');
668 3
                } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_ENDSWITH) {
669 2
                    $objWriter->writeElement('formula', 'RIGHT(' . $cellCoordinate . ',LEN("' . $txt . '"))="' . $txt . '"');
670 2
                } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_NOTCONTAINS) {
671 2
                    $objWriter->writeElement('formula', 'ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . '))');
672
                }
673
            } else {
674 3
                $objWriter->writeElement('formula', (string) ($conditional->getConditions()[0]));
675
            }
676
        }
677
    }
678
679 1
    private static function writeExtConditionalFormattingElements(XMLWriter $objWriter, ConditionalFormattingRuleExtension $ruleExtension): void
680
    {
681 1
        $prefix = 'x14';
682 1
        $objWriter->startElementNs($prefix, 'conditionalFormatting', null);
683
684 1
        $objWriter->startElementNs($prefix, 'cfRule', null);
685 1
        $objWriter->writeAttribute('type', $ruleExtension->getCfRule());
686 1
        $objWriter->writeAttribute('id', $ruleExtension->getId());
687 1
        $objWriter->startElementNs($prefix, 'dataBar', null);
688 1
        $dataBar = $ruleExtension->getDataBarExt();
689 1
        foreach ($dataBar->getXmlAttributes() as $attrKey => $val) {
690
            /** @var string $val */
691 1
            $objWriter->writeAttribute($attrKey, $val);
692
        }
693 1
        $minCfvo = $dataBar->getMinimumConditionalFormatValueObject();
694
        // Phpstan is wrong about the next statement.
695 1
        if ($minCfvo !== null) { // @phpstan-ignore-line
696 1
            $objWriter->startElementNs($prefix, 'cfvo', null);
697 1
            $objWriter->writeAttribute('type', $minCfvo->getType());
698 1
            if ($minCfvo->getCellFormula()) {
699 1
                $objWriter->writeElement('xm:f', $minCfvo->getCellFormula());
700
            }
701 1
            $objWriter->endElement(); //end cfvo
702
        }
703
704 1
        $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject();
705
        // Phpstan is wrong about the next statement.
706 1
        if ($maxCfvo !== null) { // @phpstan-ignore-line
707 1
            $objWriter->startElementNs($prefix, 'cfvo', null);
708 1
            $objWriter->writeAttribute('type', $maxCfvo->getType());
709 1
            if ($maxCfvo->getCellFormula()) {
710 1
                $objWriter->writeElement('xm:f', $maxCfvo->getCellFormula());
711
            }
712 1
            $objWriter->endElement(); //end cfvo
713
        }
714
715 1
        foreach ($dataBar->getXmlElements() as $elmKey => $elmAttr) {
716
            /** @var string[] $elmAttr */
717 1
            $objWriter->startElementNs($prefix, $elmKey, null);
718 1
            foreach ($elmAttr as $attrKey => $attrVal) {
719 1
                $objWriter->writeAttribute($attrKey, $attrVal);
720
            }
721 1
            $objWriter->endElement(); //end elmKey
722
        }
723 1
        $objWriter->endElement(); //end dataBar
724 1
        $objWriter->endElement(); //end cfRule
725 1
        $objWriter->writeElement('xm:sqref', $ruleExtension->getSqref());
726 1
        $objWriter->endElement(); //end conditionalFormatting
727
    }
728
729 66
    private static function writeDataBarElements(XMLWriter $objWriter, ?ConditionalDataBar $dataBar): void
730
    {
731 66
        if ($dataBar) {
732 2
            $objWriter->startElement('dataBar');
733 2
            self::writeAttributeIf($objWriter, null !== $dataBar->getShowValue(), 'showValue', $dataBar->getShowValue() ? '1' : '0');
734
735 2
            $minCfvo = $dataBar->getMinimumConditionalFormatValueObject();
736 2
            if ($minCfvo) {
737 2
                $objWriter->startElement('cfvo');
738 2
                $objWriter->writeAttribute('type', $minCfvo->getType());
739 2
                self::writeAttributeIf($objWriter, $minCfvo->getValue() !== null, 'val', (string) $minCfvo->getValue());
740 2
                $objWriter->endElement();
741
            }
742 2
            $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject();
743 2
            if ($maxCfvo) {
744 2
                $objWriter->startElement('cfvo');
745 2
                $objWriter->writeAttribute('type', $maxCfvo->getType());
746 2
                self::writeAttributeIf($objWriter, $maxCfvo->getValue() !== null, 'val', (string) $maxCfvo->getValue());
747 2
                $objWriter->endElement();
748
            }
749 2
            if ($dataBar->getColor()) {
750 2
                $objWriter->startElement('color');
751 2
                $objWriter->writeAttribute('rgb', $dataBar->getColor());
752 2
                $objWriter->endElement();
753
            }
754 2
            $objWriter->endElement(); // end dataBar
755
756 2
            if ($dataBar->getConditionalFormattingRuleExt()) {
757 1
                $objWriter->startElement('extLst');
758 1
                $extension = $dataBar->getConditionalFormattingRuleExt();
759 1
                $objWriter->startElement('ext');
760 1
                $objWriter->writeAttribute('uri', '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}');
761 1
                $objWriter->startElementNs('x14', 'id', null);
762 1
                $objWriter->text($extension->getId());
763 1
                $objWriter->endElement();
764 1
                $objWriter->endElement();
765 1
                $objWriter->endElement(); //end extLst
766
            }
767
        }
768
    }
769
770 3
    private static function writeColorScaleElements(XMLWriter $objWriter, ?ConditionalColorScale $colorScale): void
771
    {
772 3
        if ($colorScale) {
773 3
            $objWriter->startElement('colorScale');
774
775 3
            $minCfvo = $colorScale->getMinimumConditionalFormatValueObject();
776 3
            $minArgb = $colorScale->getMinimumColor()?->getARGB();
777 3
            $useMin = $minCfvo !== null || $minArgb !== null;
778 3
            if ($useMin) {
779 3
                $objWriter->startElement('cfvo');
780 3
                $type = 'min';
781 3
                $value = null;
782 3
                if ($minCfvo !== null) {
783 3
                    $typex = $minCfvo->getType();
784 3
                    if ($typex === 'formula') {
785 1
                        $value = $minCfvo->getCellFormula();
786 1
                        if ($value !== null) {
787 1
                            $type = $typex;
788
                        }
789
                    } else {
790 2
                        $type = $typex;
791 2
                        $defaults = ['number' => '0', 'percent' => '0', 'percentile' => '10'];
792 2
                        $value = $minCfvo->getValue() ?? $defaults[$type] ?? null;
793
                    }
794
                }
795 3
                $objWriter->writeAttribute('type', $type);
796 3
                self::writeAttributeIf($objWriter, $value !== null, 'val', (string) $value);
797 3
                $objWriter->endElement();
798
            }
799 3
            $midCfvo = $colorScale->getMidpointConditionalFormatValueObject();
800 3
            $midArgb = $colorScale->getMidpointColor()?->getARGB();
801 3
            $useMid = $midCfvo !== null || $midArgb !== null;
802 3
            if ($useMid) {
803 2
                $objWriter->startElement('cfvo');
804 2
                $type = 'percentile';
805 2
                $value = '50';
806 2
                if ($midCfvo !== null) {
807 2
                    $type = $midCfvo->getType();
808 2
                    if ($type === 'formula') {
809
                        $value = $midCfvo->getCellFormula();
810
                        if ($value === null) {
811
                            $type = 'percentile';
812
                            $value = '50';
813
                        }
814
                    } else {
815 2
                        $defaults = ['number' => '0', 'percent' => '50', 'percentile' => '50'];
816 2
                        $value = $midCfvo->getValue() ?? $defaults[$type] ?? null;
817
                    }
818
                }
819 2
                $objWriter->writeAttribute('type', $type);
820 2
                self::writeAttributeIf($objWriter, $value !== null, 'val', (string) $value);
821 2
                $objWriter->endElement();
822
            }
823 3
            $maxCfvo = $colorScale->getMaximumConditionalFormatValueObject();
824 3
            $maxArgb = $colorScale->getMaximumColor()?->getARGB();
825 3
            $useMax = $maxCfvo !== null || $maxArgb !== null;
826 3
            if ($useMax) {
827 3
                $objWriter->startElement('cfvo');
828 3
                $type = 'max';
829 3
                $value = null;
830 3
                if ($maxCfvo !== null) {
831 3
                    $typex = $maxCfvo->getType();
832 3
                    if ($typex === 'formula') {
833
                        $value = $maxCfvo->getCellFormula();
834
                        if ($value !== null) {
835
                            $type = $typex;
836
                        }
837
                    } else {
838 3
                        $type = $typex;
839 3
                        $defaults = ['number' => '0', 'percent' => '100', 'percentile' => '90'];
840 3
                        $value = $maxCfvo->getValue() ?? $defaults[$type] ?? null;
841
                    }
842
                }
843 3
                $objWriter->writeAttribute('type', $type);
844 3
                self::writeAttributeIf($objWriter, $value !== null, 'val', (string) $value);
845 3
                $objWriter->endElement();
846
            }
847 3
            if ($useMin) {
848 3
                $objWriter->startElement('color');
849 3
                self::writeAttributeIf($objWriter, $minArgb !== null, 'rgb', "$minArgb");
850 3
                $objWriter->endElement();
851
            }
852 3
            if ($useMid) {
853 2
                $objWriter->startElement('color');
854 2
                self::writeAttributeIf($objWriter, $midArgb !== null, 'rgb', "$midArgb");
855 2
                $objWriter->endElement();
856
            }
857 3
            if ($useMax) {
858 3
                $objWriter->startElement('color');
859 3
                self::writeAttributeIf($objWriter, $maxArgb !== null, 'rgb', "$maxArgb");
860 3
                $objWriter->endElement();
861
            }
862 3
            $objWriter->endElement(); // end colorScale
863
        }
864
    }
865
866
    /**
867
     * Write ConditionalFormatting.
868
     */
869 437
    private function writeConditionalFormatting(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
870
    {
871
        // Conditional id
872 437
        $id = 0;
873 437
        foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) {
874 66
            foreach ($conditionalStyles as $conditional) {
875 66
                $id = max($id, $conditional->getPriority());
876
            }
877
        }
878
879
        // Loop through styles in the current worksheet
880 437
        foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
881 66
            $objWriter->startElement('conditionalFormatting');
882
            // N.B. In Excel UI, intersection is space and union is comma.
883
            // But in Xml, intersection is comma and union is space.
884
            // Anyhow, I don't think Excel handles intersection correctly when reading.
885 66
            $outCoordinate = Coordinate::resolveUnionAndIntersection(str_replace('$', '', $cellCoordinate), ' ');
886 66
            $objWriter->writeAttribute('sqref', $outCoordinate);
887
888 66
            foreach ($conditionalStyles as $conditional) {
889
                // WHY was this again?
890
                // if ($this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) == '') {
891
                //    continue;
892
                // }
893
                // cfRule
894 66
                $objWriter->startElement('cfRule');
895 66
                $objWriter->writeAttribute('type', $conditional->getConditionType());
896 66
                self::writeAttributeIf(
897 66
                    $objWriter,
898 66
                    ($conditional->getConditionType() !== Conditional::CONDITION_COLORSCALE
899 66
                        && $conditional->getConditionType() !== Conditional::CONDITION_DATABAR
900 66
                        && $conditional->getNoFormatSet() === false),
901 66
                    'dxfId',
902 66
                    (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())
903 66
                );
904 66
                $priority = $conditional->getPriority() ?: ++$id;
905 66
                $objWriter->writeAttribute('priority', (string) $priority);
906
907 66
                self::writeAttributeif(
908 66
                    $objWriter,
909 66
                    (
910 66
                        $conditional->getConditionType() === Conditional::CONDITION_CELLIS
911 66
                        || $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT
912 66
                        || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT
913 66
                        || $conditional->getConditionType() === Conditional::CONDITION_BEGINSWITH
914 66
                        || $conditional->getConditionType() === Conditional::CONDITION_ENDSWITH
915 66
                    ) && $conditional->getOperatorType() !== Conditional::OPERATOR_NONE,
916 66
                    'operator',
917 66
                    $conditional->getOperatorType()
918 66
                );
919
920 66
                self::writeAttributeIf($objWriter, $conditional->getStopIfTrue(), 'stopIfTrue', '1');
921
922 66
                $cellRange = Coordinate::splitRange(str_replace('$', '', strtoupper($cellCoordinate)));
923 66
                [$topLeftCell] = $cellRange[0];
924
925
                if (
926 66
                    $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT
927 66
                    || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT
928 66
                    || $conditional->getConditionType() === Conditional::CONDITION_BEGINSWITH
929 66
                    || $conditional->getConditionType() === Conditional::CONDITION_ENDSWITH
930
                ) {
931 9
                    self::writeTextCondElements($objWriter, $conditional, $topLeftCell);
932 59
                } elseif ($conditional->getConditionType() === Conditional::CONDITION_TIMEPERIOD) {
933 12
                    self::writeTimePeriodCondElements($objWriter, $conditional, $topLeftCell);
934 47
                } elseif ($conditional->getConditionType() === Conditional::CONDITION_COLORSCALE) {
935 3
                    self::writeColorScaleElements($objWriter, $conditional->getColorScale());
936
                } else {
937 44
                    self::writeOtherCondElements($objWriter, $conditional, $topLeftCell);
938
                }
939
940
                //<dataBar>
941 66
                self::writeDataBarElements($objWriter, $conditional->getDataBar());
942
943 66
                $objWriter->endElement(); //end cfRule
944
            }
945
946 66
            $objWriter->endElement(); //end conditionalFormatting
947
        }
948
    }
949
950
    /**
951
     * Write DataValidations.
952
     */
953 437
    private function writeDataValidations(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
954
    {
955
        // Datavalidation collection
956 437
        $dataValidationCollection = $worksheet->getDataValidationCollection();
957
958
        // Write data validations?
959 437
        if (!empty($dataValidationCollection)) {
960 16
            $objWriter->startElement('dataValidations');
961 16
            $objWriter->writeAttribute('count', (string) count($dataValidationCollection));
962
963 16
            foreach ($dataValidationCollection as $coordinate => $dv) {
964 16
                $objWriter->startElement('dataValidation');
965
966 16
                if ($dv->getType() != '') {
967 16
                    $objWriter->writeAttribute('type', $dv->getType());
968
                }
969
970 16
                if ($dv->getErrorStyle() != '') {
971 9
                    $objWriter->writeAttribute('errorStyle', $dv->getErrorStyle());
972
                }
973
974 16
                if ($dv->getOperator() != '') {
975 16
                    $objWriter->writeAttribute('operator', $dv->getOperator());
976
                }
977
978 16
                $objWriter->writeAttribute('allowBlank', ($dv->getAllowBlank() ? '1' : '0'));
979 16
                $objWriter->writeAttribute('showDropDown', (!$dv->getShowDropDown() ? '1' : '0'));
980 16
                $objWriter->writeAttribute('showInputMessage', ($dv->getShowInputMessage() ? '1' : '0'));
981 16
                $objWriter->writeAttribute('showErrorMessage', ($dv->getShowErrorMessage() ? '1' : '0'));
982
983 16
                if ($dv->getErrorTitle() !== '') {
984 7
                    $objWriter->writeAttribute('errorTitle', $dv->getErrorTitle());
985
                }
986 16
                if ($dv->getError() !== '') {
987 9
                    $objWriter->writeAttribute('error', $dv->getError());
988
                }
989 16
                if ($dv->getPromptTitle() !== '') {
990 6
                    $objWriter->writeAttribute('promptTitle', $dv->getPromptTitle());
991
                }
992 16
                if ($dv->getPrompt() !== '') {
993 5
                    $objWriter->writeAttribute('prompt', $dv->getPrompt());
994
                }
995
996 16
                $objWriter->writeAttribute('sqref', $dv->getSqref() ?? $coordinate);
997
998 16
                if ($dv->getFormula1() !== '') {
999 16
                    $objWriter->writeElement('formula1', FunctionPrefix::addFunctionPrefix($dv->getFormula1()));
1000
                }
1001 16
                if ($dv->getFormula2() !== '') {
1002 3
                    $objWriter->writeElement('formula2', FunctionPrefix::addFunctionPrefix($dv->getFormula2()));
1003
                }
1004
1005 16
                $objWriter->endElement();
1006
            }
1007
1008 16
            $objWriter->endElement();
1009
        }
1010
    }
1011
1012
    /**
1013
     * Write Hyperlinks.
1014
     */
1015 437
    private function writeHyperlinks(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1016
    {
1017
        // Hyperlink collection
1018 437
        $hyperlinkCollection = $worksheet->getHyperlinkCollection();
1019
1020
        // Relation ID
1021 437
        $relationId = 1;
1022
1023
        // Write hyperlinks?
1024 437
        if (!empty($hyperlinkCollection)) {
1025 16
            $objWriter->startElement('hyperlinks');
1026
1027 16
            foreach ($hyperlinkCollection as $coordinate => $hyperlink) {
1028 16
                $objWriter->startElement('hyperlink');
1029
1030 16
                $objWriter->writeAttribute('ref', $coordinate);
1031 16
                if (!$hyperlink->isInternal()) {
1032 16
                    $objWriter->writeAttribute('r:id', 'rId_hyperlink_' . $relationId);
1033 16
                    ++$relationId;
1034
                } else {
1035 10
                    $objWriter->writeAttribute('location', str_replace('sheet://', '', $hyperlink->getUrl()));
1036
                }
1037
1038 16
                if ($hyperlink->getTooltip() !== '') {
1039 10
                    $objWriter->writeAttribute('tooltip', $hyperlink->getTooltip());
1040 10
                    $objWriter->writeAttribute('display', $hyperlink->getTooltip());
1041
                }
1042
1043 16
                $objWriter->endElement();
1044
            }
1045
1046 16
            $objWriter->endElement();
1047
        }
1048
    }
1049
1050
    /**
1051
     * Write ProtectedRanges.
1052
     */
1053 437
    private function writeProtectedRanges(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1054
    {
1055 437
        if (count($worksheet->getProtectedCellRanges()) > 0) {
1056
            // protectedRanges
1057 9
            $objWriter->startElement('protectedRanges');
1058
1059
            // Loop protectedRanges
1060 9
            foreach ($worksheet->getProtectedCellRanges() as $protectedCell => $protectedRange) {
1061
                // protectedRange
1062 9
                $objWriter->startElement('protectedRange');
1063 9
                $objWriter->writeAttribute('name', $protectedRange->getName());
1064 9
                $objWriter->writeAttribute('sqref', $protectedCell);
1065 9
                $passwordHash = $protectedRange->getPassword();
1066 9
                $this->writeAttributeIf($objWriter, $passwordHash !== '', 'password', $passwordHash);
1067 9
                $securityDescriptor = $protectedRange->getSecurityDescriptor();
1068 9
                $this->writeAttributeIf($objWriter, $securityDescriptor !== '', 'securityDescriptor', $securityDescriptor);
1069 9
                $objWriter->endElement();
1070
            }
1071
1072 9
            $objWriter->endElement();
1073
        }
1074
    }
1075
1076
    /**
1077
     * Write MergeCells.
1078
     */
1079 437
    private function writeMergeCells(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1080
    {
1081 437
        if (count($worksheet->getMergeCells()) > 0) {
1082
            // mergeCells
1083 38
            $objWriter->startElement('mergeCells');
1084
1085
            // Loop mergeCells
1086 38
            foreach ($worksheet->getMergeCells() as $mergeCell) {
1087
                // mergeCell
1088 38
                $objWriter->startElement('mergeCell');
1089 38
                $objWriter->writeAttribute('ref', $mergeCell);
1090 38
                $objWriter->endElement();
1091
            }
1092
1093 38
            $objWriter->endElement();
1094
        }
1095
    }
1096
1097
    /**
1098
     * Write PrintOptions.
1099
     */
1100 437
    private function writePrintOptions(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1101
    {
1102
        // printOptions
1103 437
        $objWriter->startElement('printOptions');
1104
1105 437
        $objWriter->writeAttribute('gridLines', ($worksheet->getPrintGridlines() ? 'true' : 'false'));
1106 437
        $objWriter->writeAttribute('gridLinesSet', 'true');
1107
1108 437
        if ($worksheet->getPageSetup()->getHorizontalCentered()) {
1109 4
            $objWriter->writeAttribute('horizontalCentered', 'true');
1110
        }
1111
1112 437
        if ($worksheet->getPageSetup()->getVerticalCentered()) {
1113 2
            $objWriter->writeAttribute('verticalCentered', 'true');
1114
        }
1115
1116 437
        $objWriter->endElement();
1117
    }
1118
1119
    /**
1120
     * Write PageMargins.
1121
     */
1122 437
    private function writePageMargins(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1123
    {
1124
        // pageMargins
1125 437
        $objWriter->startElement('pageMargins');
1126 437
        $objWriter->writeAttribute('left', StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()));
1127 437
        $objWriter->writeAttribute('right', StringHelper::formatNumber($worksheet->getPageMargins()->getRight()));
1128 437
        $objWriter->writeAttribute('top', StringHelper::formatNumber($worksheet->getPageMargins()->getTop()));
1129 437
        $objWriter->writeAttribute('bottom', StringHelper::formatNumber($worksheet->getPageMargins()->getBottom()));
1130 437
        $objWriter->writeAttribute('header', StringHelper::formatNumber($worksheet->getPageMargins()->getHeader()));
1131 437
        $objWriter->writeAttribute('footer', StringHelper::formatNumber($worksheet->getPageMargins()->getFooter()));
1132 437
        $objWriter->endElement();
1133
    }
1134
1135
    /**
1136
     * Write AutoFilter.
1137
     */
1138 437
    private function writeAutoFilter(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1139
    {
1140 437
        AutoFilter::writeAutoFilter($objWriter, $worksheet);
1141
    }
1142
1143
    /**
1144
     * Write Table.
1145
     */
1146 437
    private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1147
    {
1148 437
        $tableCount = $worksheet->getTableCollection()->count();
1149 437
        if ($tableCount === 0) {
1150 430
            return;
1151
        }
1152
1153 8
        $objWriter->startElement('tableParts');
1154 8
        $objWriter->writeAttribute('count', (string) $tableCount);
1155
1156 8
        for ($t = 1; $t <= $tableCount; ++$t) {
1157 8
            $objWriter->startElement('tablePart');
1158 8
            $objWriter->writeAttribute('r:id', 'rId_table_' . $t);
1159 8
            $objWriter->endElement();
1160
        }
1161
1162 8
        $objWriter->endElement();
1163
    }
1164
1165
    /**
1166
     * Write Background Image.
1167
     */
1168 437
    private function writeBackgroundImage(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1169
    {
1170 437
        if ($worksheet->getBackgroundImage() !== '') {
1171 2
            $objWriter->startElement('picture');
1172 2
            $objWriter->writeAttribute('r:id', 'rIdBg');
1173 2
            $objWriter->endElement();
1174
        }
1175
    }
1176
1177
    /**
1178
     * Write PageSetup.
1179
     */
1180 437
    private function writePageSetup(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1181
    {
1182
        // pageSetup
1183 437
        $objWriter->startElement('pageSetup');
1184 437
        $objWriter->writeAttribute('paperSize', (string) $worksheet->getPageSetup()->getPaperSize());
1185 437
        $objWriter->writeAttribute('orientation', $worksheet->getPageSetup()->getOrientation());
1186
1187 437
        if ($worksheet->getPageSetup()->getScale() !== null) {
1188 437
            $objWriter->writeAttribute('scale', (string) $worksheet->getPageSetup()->getScale());
1189
        }
1190 437
        if ($worksheet->getPageSetup()->getFitToHeight() !== null) {
1191 437
            $objWriter->writeAttribute('fitToHeight', (string) $worksheet->getPageSetup()->getFitToHeight());
1192
        } else {
1193
            $objWriter->writeAttribute('fitToHeight', '0');
1194
        }
1195 437
        if ($worksheet->getPageSetup()->getFitToWidth() !== null) {
1196 437
            $objWriter->writeAttribute('fitToWidth', (string) $worksheet->getPageSetup()->getFitToWidth());
1197
        } else {
1198
            $objWriter->writeAttribute('fitToWidth', '0');
1199
        }
1200 437
        if (!empty($worksheet->getPageSetup()->getFirstPageNumber())) {
1201 1
            $objWriter->writeAttribute('firstPageNumber', (string) $worksheet->getPageSetup()->getFirstPageNumber());
1202 1
            $objWriter->writeAttribute('useFirstPageNumber', '1');
1203
        }
1204 437
        $objWriter->writeAttribute('pageOrder', $worksheet->getPageSetup()->getPageOrder());
1205
1206
        /** @var string[][][] */
1207 437
        $getUnparsedLoadedData = $worksheet->getParentOrThrow()->getUnparsedLoadedData();
1208 437
        if (isset($getUnparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId'])) {
1209 36
            $objWriter->writeAttribute('r:id', $getUnparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId']);
1210
        }
1211
1212 437
        $objWriter->endElement();
1213
    }
1214
1215
    /**
1216
     * Write Header / Footer.
1217
     */
1218 437
    private function writeHeaderFooter(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1219
    {
1220
        // headerFooter
1221 437
        $headerFooter = $worksheet->getHeaderFooter();
1222 437
        $oddHeader = $headerFooter->getOddHeader();
1223 437
        $oddFooter = $headerFooter->getOddFooter();
1224 437
        $evenHeader = $headerFooter->getEvenHeader();
1225 437
        $evenFooter = $headerFooter->getEvenFooter();
1226 437
        $firstHeader = $headerFooter->getFirstHeader();
1227 437
        $firstFooter = $headerFooter->getFirstFooter();
1228 437
        if ("$oddHeader$oddFooter$evenHeader$evenFooter$firstHeader$firstFooter" === '') {
1229 426
            return;
1230
        }
1231
1232 21
        $objWriter->startElement('headerFooter');
1233 21
        $objWriter->writeAttribute('differentOddEven', ($worksheet->getHeaderFooter()->getDifferentOddEven() ? 'true' : 'false'));
1234 21
        $objWriter->writeAttribute('differentFirst', ($worksheet->getHeaderFooter()->getDifferentFirst() ? 'true' : 'false'));
1235 21
        $objWriter->writeAttribute('scaleWithDoc', ($worksheet->getHeaderFooter()->getScaleWithDocument() ? 'true' : 'false'));
1236 21
        $objWriter->writeAttribute('alignWithMargins', ($worksheet->getHeaderFooter()->getAlignWithMargins() ? 'true' : 'false'));
1237
1238 21
        self::writeElementIf($objWriter, $oddHeader !== '', 'oddHeader', $oddHeader);
1239 21
        self::writeElementIf($objWriter, $oddFooter !== '', 'oddFooter', $oddFooter);
1240 21
        self::writeElementIf($objWriter, $evenHeader !== '', 'evenHeader', $evenHeader);
1241 21
        self::writeElementIf($objWriter, $evenFooter !== '', 'evenFooter', $evenFooter);
1242 21
        self::writeElementIf($objWriter, $firstHeader !== '', 'firstHeader', $firstHeader);
1243 21
        self::writeElementIf($objWriter, $firstFooter !== '', 'firstFooter', $firstFooter);
1244
1245 21
        $objWriter->endElement(); // headerFooter
1246
    }
1247
1248
    /**
1249
     * Write Breaks.
1250
     */
1251 437
    private function writeBreaks(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1252
    {
1253
        // Get row and column breaks
1254 437
        $aRowBreaks = [];
1255 437
        $aColumnBreaks = [];
1256 437
        foreach ($worksheet->getRowBreaks() as $cell => $break) {
1257 9
            $aRowBreaks[$cell] = $break;
1258
        }
1259 437
        foreach ($worksheet->getColumnBreaks() as $cell => $break) {
1260 3
            $aColumnBreaks[$cell] = $break;
1261
        }
1262
1263
        // rowBreaks
1264 437
        if (!empty($aRowBreaks)) {
1265 9
            $objWriter->startElement('rowBreaks');
1266 9
            $objWriter->writeAttribute('count', (string) count($aRowBreaks));
1267 9
            $objWriter->writeAttribute('manualBreakCount', (string) count($aRowBreaks));
1268
1269 9
            foreach ($aRowBreaks as $cell => $break) {
1270 9
                $coords = Coordinate::coordinateFromString($cell);
1271
1272 9
                $objWriter->startElement('brk');
1273 9
                $objWriter->writeAttribute('id', $coords[1]);
1274 9
                $objWriter->writeAttribute('man', '1');
1275 9
                $rowBreakMax = $break->getMaxColOrRow();
1276 9
                if ($rowBreakMax >= 0) {
1277 1
                    $objWriter->writeAttribute('max', "$rowBreakMax");
1278 8
                } elseif ($worksheet->getPageSetup()->getPrintArea() !== '') {
1279 5
                    $maxCol = Coordinate::columnIndexFromString($worksheet->getHighestColumn());
1280 5
                    $objWriter->writeAttribute('max', "$maxCol");
1281
                }
1282 9
                $objWriter->endElement();
1283
            }
1284
1285 9
            $objWriter->endElement();
1286
        }
1287
1288
        // Second, write column breaks
1289 437
        if (!empty($aColumnBreaks)) {
1290 3
            $objWriter->startElement('colBreaks');
1291 3
            $objWriter->writeAttribute('count', (string) count($aColumnBreaks));
1292 3
            $objWriter->writeAttribute('manualBreakCount', (string) count($aColumnBreaks));
1293
1294 3
            foreach ($aColumnBreaks as $cell => $break) {
1295 3
                $coords = Coordinate::indexesFromString($cell);
1296
1297 3
                $objWriter->startElement('brk');
1298 3
                $objWriter->writeAttribute('id', (string) ((int) $coords[0] - 1));
1299 3
                $objWriter->writeAttribute('man', '1');
1300 3
                $colBreakMax = $break->getMaxColOrRow();
1301 3
                if ($colBreakMax >= 0) {
1302
                    $objWriter->writeAttribute('max', "$colBreakMax");
1303 3
                } elseif ($worksheet->getPageSetup()->getPrintArea() !== '') {
1304 1
                    $maxRow = $worksheet->getHighestRow();
1305 1
                    $objWriter->writeAttribute('max', "$maxRow");
1306
                }
1307 3
                $objWriter->endElement();
1308
            }
1309
1310 3
            $objWriter->endElement();
1311
        }
1312
    }
1313
1314
    /**
1315
     * Write SheetData.
1316
     *
1317
     * @param string[] $stringTable String table
1318
     */
1319 438
    private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet, array $stringTable): void
1320
    {
1321
        // Flipped stringtable, for faster index searching
1322 438
        $aFlippedStringTable = $this->getParentWriter()->getWriterPartstringtable()->flipStringTable($stringTable);
1323
1324
        // sheetData
1325 438
        $objWriter->startElement('sheetData');
1326
1327
        // Get column count
1328 438
        $colCount = Coordinate::columnIndexFromString($worksheet->getHighestColumn());
1329
1330
        // Highest row number
1331 438
        $highestRow = $worksheet->getHighestRow();
1332
1333
        // Loop through cells building a comma-separated list of the columns in each row
1334
        // This is a trade-off between the memory usage that is required for a full array of columns,
1335
        //      and execution speed
1336
        /** @var array<int, string> $cellsByRow */
1337 438
        $cellsByRow = [];
1338 438
        foreach ($worksheet->getCoordinates() as $coordinate) {
1339 382
            [$column, $row] = Coordinate::coordinateFromString($coordinate);
1340 382
            if (!isset($cellsByRow[$row])) {
1341 382
                $pCell = $worksheet->getCell("$column$row");
1342 382
                $xfi = $pCell->getXfIndex();
1343 382
                $cellValue = $pCell->getValue();
1344 382
                $writeValue = $cellValue !== '' && $cellValue !== null;
1345 382
                if (!empty($xfi) || $writeValue) {
1346 365
                    $cellsByRow[$row] = "{$column},";
1347
                }
1348
            } else {
1349 232
                $cellsByRow[$row] .= "{$column},";
1350
            }
1351
        }
1352
1353 438
        $currentRow = 0;
1354 438
        $emptyDimension = new RowDimension();
1355 438
        while ($currentRow++ < $highestRow) {
1356 438
            $isRowSet = isset($cellsByRow[$currentRow]);
1357 438
            if ($isRowSet || $worksheet->rowDimensionExists($currentRow)) {
1358
                // Get row dimension
1359 365
                $rowDimension = $worksheet->rowDimensionExists($currentRow) ? $worksheet->getRowDimension($currentRow) : $emptyDimension;
1360
1361
                // Write current row?
1362 365
                $writeCurrentRow = $isRowSet || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() === false || $rowDimension->getCollapsed() === true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null;
1363
1364 365
                if ($writeCurrentRow) {
1365
                    // Start a new row
1366 365
                    $objWriter->startElement('row');
1367 365
                    $objWriter->writeAttribute('r', "$currentRow");
1368 365
                    $objWriter->writeAttribute('spans', '1:' . $colCount);
1369
1370
                    // Row dimensions
1371 365
                    if ($rowDimension->getRowHeight() >= 0) {
1372 34
                        $objWriter->writeAttribute('customHeight', '1');
1373 34
                        $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight()));
1374
                    }
1375
1376
                    // Row visibility
1377 365
                    if (!$rowDimension->getVisible() === true) {
1378 16
                        $objWriter->writeAttribute('hidden', 'true');
1379
                    }
1380
1381
                    // Collapsed
1382 365
                    if ($rowDimension->getCollapsed() === true) {
1383
                        $objWriter->writeAttribute('collapsed', 'true');
1384
                    }
1385
1386
                    // Outline level
1387 365
                    if ($rowDimension->getOutlineLevel() > 0) {
1388
                        $objWriter->writeAttribute('outlineLevel', (string) $rowDimension->getOutlineLevel());
1389
                    }
1390
1391
                    // Style
1392 365
                    if ($rowDimension->getXfIndex() !== null) {
1393 8
                        $objWriter->writeAttribute('s', (string) $rowDimension->getXfIndex());
1394 8
                        $objWriter->writeAttribute('customFormat', '1');
1395
                    }
1396
1397
                    // Write cells
1398 365
                    if (isset($cellsByRow[$currentRow])) {
1399
                        // We have a comma-separated list of column names (with a trailing entry); split to an array
1400 365
                        $columnsInRow = explode(',', $cellsByRow[$currentRow]);
1401 365
                        array_pop($columnsInRow);
1402 365
                        foreach ($columnsInRow as $column) {
1403
                            // Write cell
1404 365
                            $coord = "$column$currentRow";
1405 365
                            if ($worksheet->getCell($coord)->getIgnoredErrors()->getNumberStoredAsText()) {
1406 4
                                $this->numberStoredAsText .= " $coord";
1407
                            }
1408 365
                            if ($worksheet->getCell($coord)->getIgnoredErrors()->getFormula()) {
1409 1
                                $this->formula .= " $coord";
1410
                            }
1411 365
                            if ($worksheet->getCell($coord)->getIgnoredErrors()->getFormulaRange()) {
1412 1
                                $this->formulaRange .= " $coord";
1413
                            }
1414 365
                            if ($worksheet->getCell($coord)->getIgnoredErrors()->getTwoDigitTextYear()) {
1415 1
                                $this->twoDigitTextYear .= " $coord";
1416
                            }
1417 365
                            if ($worksheet->getCell($coord)->getIgnoredErrors()->getEvalError()) {
1418 1
                                $this->evalError .= " $coord";
1419
                            }
1420 365
                            $this->writeCell($objWriter, $worksheet, $coord, $aFlippedStringTable);
1421
                        }
1422
                    }
1423
1424
                    // End row
1425 364
                    $objWriter->endElement();
1426
                }
1427
            }
1428
        }
1429
1430 437
        $objWriter->endElement();
1431
    }
1432
1433 14
    private function writeCellInlineStr(XMLWriter $objWriter, string $mappedType, RichText|string $cellValue, ?Font $font): void
1434
    {
1435 14
        $objWriter->writeAttribute('t', $mappedType);
1436 14
        if (!$cellValue instanceof RichText) {
1437 3
            $objWriter->startElement('is');
1438 3
            $objWriter->startElement('t');
1439 3
            $textToWrite = StringHelper::controlCharacterPHP2OOXML(
1440 3
                $cellValue
1441 3
            );
1442 3
            if ($textToWrite !== trim($textToWrite)) {
1443 1
                $objWriter->writeAttribute('xml:space', 'preserve');
1444
            }
1445 3
            $objWriter->writeRawData($textToWrite);
1446 3
            $objWriter->endElement(); // t
1447 3
            $objWriter->endElement(); // is
1448
        } else {
1449 13
            $objWriter->startElement('is');
1450 13
            $this->getParentWriter()
1451 13
                ->getWriterPartstringtable()
1452 13
                ->writeRichText($objWriter, $cellValue, null, $font);
1453 13
            $objWriter->endElement();
1454
        }
1455
    }
1456
1457
    /**
1458
     * @param string[] $flippedStringTable
1459
     */
1460 253
    private function writeCellString(XMLWriter $objWriter, string $mappedType, RichText|string $cellValue, array $flippedStringTable): void
1461
    {
1462 253
        $objWriter->writeAttribute('t', $mappedType);
1463 253
        if (!$cellValue instanceof RichText) {
1464 252
            self::writeElementIf($objWriter, isset($flippedStringTable[$cellValue]), 'v', $flippedStringTable[$cellValue] ?? '');
1465
        } else {
1466 9
            $objWriter->writeElement('v', $flippedStringTable[$cellValue->getHashCode()]);
1467
        }
1468
    }
1469
1470 231
    private function writeCellNumeric(XMLWriter $objWriter, float|int $cellValue): void
1471
    {
1472 231
        $result = StringHelper::convertToString($cellValue);
1473 231
        if (is_float($cellValue) && !str_contains($result, '.')) {
1474 29
            $result .= '.0';
1475
        }
1476 231
        $objWriter->writeElement('v', $result);
1477
    }
1478
1479 14
    private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void
1480
    {
1481 14
        $objWriter->writeAttribute('t', $mappedType);
1482 14
        $objWriter->writeElement('v', $cellValue ? '1' : '0');
1483
    }
1484
1485 12
    private function writeCellError(XMLWriter $objWriter, string $mappedType, string $cellValue, string $formulaerr = '#NULL!'): void
1486
    {
1487 12
        $objWriter->writeAttribute('t', $mappedType);
1488 12
        $cellIsFormula = str_starts_with($cellValue, '=');
1489 12
        self::writeElementIf($objWriter, $cellIsFormula, 'f', FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
1490 12
        $objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue);
1491
    }
1492
1493 113
    private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell $cell): void
1494
    {
1495 113
        $attributes = $cell->getFormulaAttributes() ?? [];
1496 113
        $coordinate = $cell->getCoordinate();
1497 113
        $calculatedValue = $this->getParentWriter()->getPreCalculateFormulas() ? $cell->getCalculatedValue() : $cellValue;
1498 112
        if ($calculatedValue === ExcelError::SPILL()) {
1499 1
            $objWriter->writeAttribute('t', 'e');
1500
            //$objWriter->writeAttribute('cm', '1'); // already added
1501 1
            $objWriter->writeAttribute('vm', '1');
1502 1
            $objWriter->startElement('f');
1503 1
            $objWriter->writeAttribute('t', 'array');
1504 1
            $objWriter->writeAttribute('aca', '1');
1505 1
            $objWriter->writeAttribute('ref', $coordinate);
1506 1
            $objWriter->writeAttribute('ca', '1');
1507 1
            $objWriter->text(FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
1508 1
            $objWriter->endElement(); // f
1509 1
            $objWriter->writeElement('v', ExcelError::VALUE()); // note #VALUE! in xml even though error is #SPILL!
1510
1511 1
            return;
1512
        }
1513 111
        $calculatedValueString = $this->getParentWriter()->getPreCalculateFormulas() ? $cell->getCalculatedValueString() : $cellValue;
1514 111
        $result = $calculatedValue;
1515 111
        while (is_array($result)) {
1516 11
            $result = array_shift($result);
1517
        }
1518 111
        if (is_string($result)) {
1519 45
            if (ErrorValue::isError($result)) {
1520 11
                $this->writeCellError($objWriter, 'e', $cellValue, $result);
1521
1522 11
                return;
1523
            }
1524 43
            $objWriter->writeAttribute('t', 'str');
1525 43
            $result = $calculatedValueString = StringHelper::controlCharacterPHP2OOXML($result);
1526 43
            if (is_string($calculatedValue)) {
1527 42
                $calculatedValue = $calculatedValueString;
1528
            }
1529 89
        } elseif (is_bool($result)) {
1530 8
            $objWriter->writeAttribute('t', 'b');
1531 8
            if (is_bool($calculatedValue)) {
1532 8
                $calculatedValue = $result;
1533
            }
1534 8
            $result = (int) $result;
1535 8
            $calculatedValueString = (string) $result;
1536
        }
1537
1538 111
        if (isset($attributes['ref'])) {
1539 28
            $ref = $this->parseRef($coordinate, $attributes['ref']);
1540 28
            if ($ref === "$coordinate:$coordinate") {
1541
                $ref = $coordinate;
1542
            }
1543
        } else {
1544 100
            $ref = $coordinate;
1545
        }
1546 111
        if (is_array($calculatedValue)) {
1547 11
            $attributes['t'] = 'array';
1548
        }
1549 111
        if (($attributes['t'] ?? null) === 'array') {
1550 14
            $objWriter->startElement('f');
1551 14
            $objWriter->writeAttribute('t', 'array');
1552 14
            $objWriter->writeAttribute('ref', $ref);
1553 14
            $objWriter->writeAttribute('aca', '1');
1554 14
            $objWriter->writeAttribute('ca', '1');
1555 14
            $objWriter->text(FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
1556 14
            $objWriter->endElement();
1557
            if (
1558 14
                is_scalar($result)
1559 14
                && $this->getParentWriter()->getOffice2003Compatibility() === false
1560 14
                && $this->getParentWriter()->getPreCalculateFormulas()
1561
            ) {
1562 14
                $objWriter->writeElement('v', (string) $result);
1563
            }
1564
        } else {
1565 98
            $objWriter->writeElement('f', FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
1566 98
            self::writeElementIf(
1567 98
                $objWriter,
1568 98
                $this->getParentWriter()->getOffice2003Compatibility() === false
1569 98
                && $this->getParentWriter()->getPreCalculateFormulas()
1570 98
                && $calculatedValue !== null,
1571 98
                'v',
1572 98
                (!is_array($calculatedValue) && !str_starts_with($calculatedValueString, '#'))
1573 98
                    ? StringHelper::formatNumber($calculatedValueString) : '0'
1574 98
            );
1575
        }
1576
    }
1577
1578 28
    private function parseRef(string $coordinate, string $ref): string
1579
    {
1580 28
        if (!Preg::isMatch('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches)) {
1581
            return $ref;
1582
        }
1583 28
        if (!isset($matches[3])) { // single cell, not range
1584 2
            return $coordinate;
1585
        }
1586 28
        $minRow = (int) $matches[2];
1587 28
        $maxRow = (int) $matches[5];
1588 28
        $rows = $maxRow - $minRow + 1;
1589 28
        $minCol = Coordinate::columnIndexFromString($matches[1]);
1590 28
        $maxCol = Coordinate::columnIndexFromString($matches[4]);
1591 28
        $cols = $maxCol - $minCol + 1;
1592 28
        $firstCellArray = Coordinate::indexesFromString($coordinate);
1593 28
        $lastRow = $firstCellArray[1] + $rows - 1;
1594 28
        $lastColumn = $firstCellArray[0] + $cols - 1;
1595 28
        $lastColumnString = Coordinate::stringFromColumnIndex($lastColumn);
1596
1597 28
        return "$coordinate:$lastColumnString$lastRow";
1598
    }
1599
1600
    /**
1601
     * Write Cell.
1602
     *
1603
     * @param string $cellAddress Cell Address
1604
     * @param string[] $flippedStringTable String table (flipped), for faster index searching
1605
     */
1606 365
    private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet, string $cellAddress, array $flippedStringTable): void
1607
    {
1608
        // Cell
1609 365
        $pCell = $worksheet->getCell($cellAddress);
1610 365
        $xfi = $pCell->getXfIndex();
1611 365
        $cellValue = $pCell->getValue();
1612 365
        $cellValueString = $pCell->getValueString();
1613 365
        $writeValue = $cellValue !== '' && $cellValue !== null;
1614 365
        if (empty($xfi) && !$writeValue) {
1615 27
            return;
1616
        }
1617 365
        $styleArray = $this->getParentWriter()
1618 365
            ->getSpreadsheet()
1619 365
            ->getCellXfCollection();
1620 365
        $font = $styleArray[$xfi] ?? null;
1621 365
        if ($font !== null) {
1622 365
            $font = $font->getFont();
1623
        }
1624 365
        $objWriter->startElement('c');
1625 365
        $objWriter->writeAttribute('r', $cellAddress);
1626 365
        $mappedType = $pCell->getDataType();
1627 365
        if ($mappedType === DataType::TYPE_FORMULA) {
1628 113
            if ($this->useDynamicArrays) {
1629 11
                if (preg_match(PhpspreadsheetWorksheet::FUNCTION_LIKE_GROUPBY, $cellValueString) === 1) {
1630 1
                    $tempCalc = [];
1631
                } else {
1632 10
                    $tempCalc = $pCell->getCalculatedValue();
1633
                }
1634 11
                if (is_array($tempCalc)) {
1635 10
                    $objWriter->writeAttribute('cm', '1');
1636
                }
1637
            }
1638
        }
1639
1640
        // Sheet styles
1641 365
        if ($xfi) {
1642 127
            $objWriter->writeAttribute('s', "$xfi");
1643 330
        } elseif ($this->explicitStyle0) {
1644 1
            $objWriter->writeAttribute('s', '0');
1645
        }
1646
1647
        // If cell value is supplied, write cell value
1648 365
        if ($writeValue) {
1649
            // Write data depending on its type
1650 358
            switch (strtolower($mappedType)) {
1651 358
                case 'inlinestr':    // Inline string
1652
                    /** @var RichText|string */
1653 14
                    $richText = $cellValue;
1654 14
                    $this->writeCellInlineStr($objWriter, $mappedType, $richText, $font);
1655
1656 14
                    break;
1657 354
                case 's':            // String
1658 253
                    $this->writeCellString($objWriter, $mappedType, ($cellValue instanceof RichText) ? $cellValue : $cellValueString, $flippedStringTable);
1659
1660 253
                    break;
1661 266
                case 'f':            // Formula
1662 113
                    $this->writeCellFormula($objWriter, $cellValueString, $pCell);
1663
1664 112
                    break;
1665 235
                case 'n':            // Numeric
1666 231
                    $cellValueNumeric = is_numeric($cellValue) ? ($cellValue + 0) : 0;
1667 231
                    $this->writeCellNumeric($objWriter, $cellValueNumeric);
1668
1669 231
                    break;
1670 14
                case 'b':            // Boolean
1671 14
                    $this->writeCellBoolean($objWriter, $mappedType, (bool) $cellValue);
1672
1673 14
                    break;
1674 1
                case 'e':            // Error
1675 1
                    $this->writeCellError($objWriter, $mappedType, $cellValueString);
1676
            }
1677
        }
1678
1679 364
        $objWriter->endElement(); // c
1680
    }
1681
1682
    /**
1683
     * Write Drawings.
1684
     *
1685
     * @param bool $includeCharts Flag indicating if we should include drawing details for charts
1686
     */
1687 437
    private function writeDrawings(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet, bool $includeCharts = false): void
1688
    {
1689
        /** @var mixed[][][][] */
1690 437
        $unparsedLoadedData = $worksheet->getParentOrThrow()->getUnparsedLoadedData();
1691 437
        $hasUnparsedDrawing = isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds']);
1692 437
        $chartCount = ($includeCharts) ? $worksheet->getChartCollection()->count() : 0;
1693 437
        if ($chartCount == 0 && $worksheet->getDrawingCollection()->count() == 0 && !$hasUnparsedDrawing) {
1694 329
            return;
1695
        }
1696
1697
        // If sheet contains drawings, add the relationships
1698 127
        $objWriter->startElement('drawing');
1699
1700 127
        $rId = 'rId1';
1701 127
        if (isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds'])) {
1702 49
            $drawingOriginalIds = $unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds'];
1703
            // take first. In future can be overriten
1704
            // (! synchronize with \PhpOffice\PhpSpreadsheet\Writer\Xlsx\Rels::writeWorksheetRelationships)
1705 49
            $rId = reset($drawingOriginalIds);
1706
        }
1707
1708
        /** @var string $rId */
1709 127
        $objWriter->writeAttribute('r:id', $rId);
1710 127
        $objWriter->endElement();
1711
    }
1712
1713
    /**
1714
     * Write LegacyDrawing.
1715
     */
1716 437
    private function writeLegacyDrawing(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1717
    {
1718
        // If sheet contains comments, add the relationships
1719
        /** @var mixed[][][][] */
1720 437
        $unparsedLoadedData = $worksheet->getParentOrThrow()->getUnparsedLoadedData();
1721 437
        if (count($worksheet->getComments()) > 0 || isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'])) {
1722 28
            $objWriter->startElement('legacyDrawing');
1723 28
            $objWriter->writeAttribute('r:id', 'rId_comments_vml1');
1724 28
            $objWriter->endElement();
1725
        }
1726
    }
1727
1728
    /**
1729
     * Write LegacyDrawingHF.
1730
     */
1731 437
    private function writeLegacyDrawingHF(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1732
    {
1733
        // If sheet contains images, add the relationships
1734 437
        if (count($worksheet->getHeaderFooter()->getImages()) > 0) {
1735 4
            $objWriter->startElement('legacyDrawingHF');
1736 4
            $objWriter->writeAttribute('r:id', 'rId_headerfooter_vml1');
1737 4
            $objWriter->endElement();
1738
        }
1739
    }
1740
1741 437
    private function writeAlternateContent(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1742
    {
1743
        /** @var string[][][] */
1744 437
        $unparsedSheet = $worksheet->getParentOrThrow()->getUnparsedLoadedData()['sheets'] ?? [];
1745 437
        $unparsedSheet = $unparsedSheet[$worksheet->getCodeName()] ?? [];
1746 437
        $unparsedSheet = $unparsedSheet['AlternateContents'] ?? [];
1747
1748 437
        foreach ($unparsedSheet as $alternateContent) {
1749 4
            $objWriter->writeRaw($alternateContent);
1750
        }
1751
    }
1752
1753
    /**
1754
     * write <ExtLst>
1755
     * only implementation conditionalFormattings.
1756
     *
1757
     * @url https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/07d607af-5618-4ca2-b683-6a78dc0d9627
1758
     */
1759 437
    private function writeExtLst(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
1760
    {
1761 437
        $conditionalFormattingRuleExtList = [];
1762 437
        foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
1763
            /** @var Conditional $conditional */
1764 66
            foreach ($conditionalStyles as $conditional) {
1765 66
                $dataBar = $conditional->getDataBar();
1766 66
                if ($dataBar && $dataBar->getConditionalFormattingRuleExt()) {
1767 1
                    $conditionalFormattingRuleExtList[] = $dataBar->getConditionalFormattingRuleExt();
1768
                }
1769
            }
1770
        }
1771
1772 437
        if (count($conditionalFormattingRuleExtList) > 0) {
1773 1
            $conditionalFormattingRuleExtNsPrefix = 'x14';
1774 1
            $objWriter->startElement('extLst');
1775 1
            $objWriter->startElement('ext');
1776 1
            $objWriter->writeAttribute('uri', '{78C0D931-6437-407d-A8EE-F0AAD7539E65}');
1777 1
            $objWriter->startElementNs($conditionalFormattingRuleExtNsPrefix, 'conditionalFormattings', null);
1778 1
            foreach ($conditionalFormattingRuleExtList as $extension) {
1779 1
                self::writeExtConditionalFormattingElements($objWriter, $extension);
1780
            }
1781 1
            $objWriter->endElement(); //end conditionalFormattings
1782 1
            $objWriter->endElement(); //end ext
1783 1
            $objWriter->endElement(); //end extLst
1784
        }
1785
    }
1786
}
1787