Passed
Pull Request — master (#4450)
by Owen
17:01 queued 04:52
created

Worksheet::writeTable()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

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