Worksheet::writeCellError()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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