Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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