Browse Source

版本构建

bingqishi 1 hour ago
parent
commit
670577808c
100 changed files with 36260 additions and 0 deletions
  1. 37 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Locale.php
  2. 57 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Number.php
  3. 80 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/NumberBase.php
  4. 40 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Percentage.php
  5. 33 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Scientific.php
  6. 105 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Time.php
  7. 8 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Wizard.php
  8. 198 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Protection.php
  9. 175 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/RgbTint.php
  10. 745 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php
  11. 175 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Supervisor.php
  12. 269 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Theme.php
  13. 1118 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php
  14. 404 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php
  15. 426 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php
  16. 51 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFit.php
  17. 543 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php
  18. 94 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/CellIterator.php
  19. 121 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php
  20. 205 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php
  21. 137 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php
  22. 174 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnIterator.php
  23. 134 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php
  24. 272 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php
  25. 287 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing/Shadow.php
  26. 490 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooter.php
  27. 24 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooterDrawing.php
  28. 74 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Iterator.php
  29. 356 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php
  30. 58 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageBreak.php
  31. 229 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageMargins.php
  32. 888 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php
  33. 517 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Protection.php
  34. 120 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Row.php
  35. 195 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php
  36. 118 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowDimension.php
  37. 163 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowIterator.php
  38. 178 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/SheetView.php
  39. 585 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php
  40. 254 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/Column.php
  41. 230 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php
  42. 118 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php
  43. 3708 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php
  44. 148 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php
  45. 326 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Csv.php
  46. 9 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Exception.php
  47. 1935 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php
  48. 98 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php
  49. 186 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods.php
  50. 66 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php
  51. 30 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php
  52. 259 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php
  53. 345 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php
  54. 120 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Formula.php
  55. 122 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Meta.php
  56. 60 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/MetaInf.php
  57. 16 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Mimetype.php
  58. 140 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php
  59. 152 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Settings.php
  60. 65 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Styles.php
  61. 16 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Thumbnails.php
  62. 35 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/WriterPart.php
  63. 251 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf.php
  64. 60 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php
  65. 93 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php
  66. 84 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php
  67. 924 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls.php
  68. 224 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/BIFFwriter.php
  69. 78 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/CellDataValidation.php
  70. 76 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php
  71. 28 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/ErrorCode.php
  72. 524 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Escher.php
  73. 146 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Font.php
  74. 1664 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php
  75. 59 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellAlignment.php
  76. 40 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellBorder.php
  77. 46 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellFill.php
  78. 90 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php
  79. 1190 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php
  80. 3218 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Worksheet.php
  81. 415 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Xf.php
  82. 763 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php
  83. 125 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/AutoFilter.php
  84. 1842 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php
  85. 236 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php
  86. 272 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php
  87. 244 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php
  88. 250 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php
  89. 571 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
  90. 194 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php
  91. 498 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Rels.php
  92. 46 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsRibbon.php
  93. 40 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsVBA.php
  94. 346 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php
  95. 734 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Style.php
  96. 115 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Table.php
  97. 744 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Theme.php
  98. 214 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php
  99. 1462 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
  100. 33 0
      vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/WriterPart.php

+ 37 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Locale.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
+
+use NumberFormatter;
+use PhpOffice\PhpSpreadsheet\Exception;
+
+final class Locale
+{
+    /**
+     * Language code: ISO-639 2 character, alpha.
+     * Optional script code: ISO-15924 4 alpha.
+     * Optional country code: ISO-3166-1, 2 character alpha.
+     * Separated by underscores or dashes.
+     */
+    public const STRUCTURE = '/^(?P<language>[a-z]{2})([-_](?P<script>[a-z]{4}))?([-_](?P<country>[a-z]{2}))?$/i';
+
+    private NumberFormatter $formatter;
+
+    public function __construct(?string $locale, int $style)
+    {
+        if (class_exists(NumberFormatter::class) === false) {
+            throw new Exception();
+        }
+
+        $formatterLocale = str_replace('-', '_', $locale ?? '');
+        $this->formatter = new NumberFormatter($formatterLocale, $style);
+        if ($this->formatter->getLocale() !== $formatterLocale) {
+            throw new Exception("Unable to read locale data for '{$locale}'");
+        }
+    }
+
+    public function format(): string
+    {
+        return $this->formatter->getPattern();
+    }
+}

+ 57 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Number.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
+
+use PhpOffice\PhpSpreadsheet\Exception;
+
+class Number extends NumberBase implements Wizard
+{
+    public const WITH_THOUSANDS_SEPARATOR = true;
+
+    public const WITHOUT_THOUSANDS_SEPARATOR = false;
+
+    protected bool $thousandsSeparator = true;
+
+    /**
+     * @param int $decimals number of decimal places to display, in the range 0-30
+     * @param bool $thousandsSeparator indicator whether the thousands separator should be used, or not
+     * @param ?string $locale Set the locale for the number format; or leave as the default null.
+     *          Locale has no effect for Number Format values, and is retained here only for compatibility
+     *              with the other Wizards.
+     *          If provided, Locale values must be a valid formatted locale string (e.g. 'en-GB', 'fr', uz-Arab-AF).
+     *
+     * @throws Exception If a provided locale code is not a valid format
+     */
+    public function __construct(
+        int $decimals = 2,
+        bool $thousandsSeparator = self::WITH_THOUSANDS_SEPARATOR,
+        ?string $locale = null
+    ) {
+        $this->setDecimals($decimals);
+        $this->setThousandsSeparator($thousandsSeparator);
+        $this->setLocale($locale);
+    }
+
+    public function setThousandsSeparator(bool $thousandsSeparator = self::WITH_THOUSANDS_SEPARATOR): void
+    {
+        $this->thousandsSeparator = $thousandsSeparator;
+    }
+
+    /**
+     * As MS Excel cannot easily handle Lakh, which is the only locale-specific Number format variant,
+     *       we don't use locale with Numbers.
+     */
+    protected function getLocaleFormat(): string
+    {
+        return $this->format();
+    }
+
+    public function format(): string
+    {
+        return sprintf(
+            '%s0%s',
+            $this->thousandsSeparator ? '#,##' : null,
+            $this->decimals > 0 ? '.' . str_repeat('0', $this->decimals) : null
+        );
+    }
+}

+ 80 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/NumberBase.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
+
+use NumberFormatter;
+use PhpOffice\PhpSpreadsheet\Exception;
+use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
+
+abstract class NumberBase
+{
+    protected const MAX_DECIMALS = 30;
+
+    protected int $decimals = 2;
+
+    protected ?string $locale = null;
+
+    protected ?string $fullLocale = null;
+
+    protected ?string $localeFormat = null;
+
+    public function setDecimals(int $decimals = 2): void
+    {
+        $this->decimals = ($decimals > self::MAX_DECIMALS) ? self::MAX_DECIMALS : max($decimals, 0);
+    }
+
+    /**
+     * Setting a locale will override any settings defined in this class.
+     *
+     * @throws Exception If the locale code is not a valid format
+     */
+    public function setLocale(?string $locale = null): void
+    {
+        if ($locale === null) {
+            $this->localeFormat = $this->locale = $this->fullLocale = null;
+
+            return;
+        }
+
+        $this->locale = $this->validateLocale($locale);
+
+        if (class_exists(NumberFormatter::class)) {
+            $this->localeFormat = $this->getLocaleFormat();
+        }
+    }
+
+    /**
+     * Stub: should be implemented as a concrete method in concrete wizards.
+     */
+    abstract protected function getLocaleFormat(): string;
+
+    /**
+     * @throws Exception If the locale code is not a valid format
+     */
+    private function validateLocale(string $locale): string
+    {
+        if (preg_match(Locale::STRUCTURE, $locale, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
+            throw new Exception("Invalid locale code '{$locale}'");
+        }
+
+        ['language' => $language, 'script' => $script, 'country' => $country] = $matches;
+        // Set case and separator to match standardised locale case
+        $language = strtolower($language ?? '');
+        $script = ($script === null) ? null : ucfirst(strtolower($script));
+        $country = ($country === null) ? null : strtoupper($country);
+
+        $this->fullLocale = implode('-', array_filter([$language, $script, $country]));
+
+        return $country === null ? $language : "{$language}-{$country}";
+    }
+
+    public function format(): string
+    {
+        return NumberFormat::FORMAT_GENERAL;
+    }
+
+    public function __toString(): string
+    {
+        return $this->format();
+    }
+}

+ 40 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Percentage.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
+
+use NumberFormatter;
+use PhpOffice\PhpSpreadsheet\Exception;
+
+class Percentage extends NumberBase implements Wizard
+{
+    /**
+     * @param int $decimals number of decimal places to display, in the range 0-30
+     * @param ?string $locale Set the locale for the percentage format; or leave as the default null.
+     *          If provided, Locale values must be a valid formatted locale string (e.g. 'en-GB', 'fr', uz-Arab-AF).
+     *
+     * @throws Exception If a provided locale code is not a valid format
+     */
+    public function __construct(int $decimals = 2, ?string $locale = null)
+    {
+        $this->setDecimals($decimals);
+        $this->setLocale($locale);
+    }
+
+    protected function getLocaleFormat(): string
+    {
+        $formatter = new Locale($this->fullLocale, NumberFormatter::PERCENT);
+
+        return $this->decimals > 0
+            ? str_replace('0', '0.' . str_repeat('0', $this->decimals), $formatter->format())
+            : $formatter->format();
+    }
+
+    public function format(): string
+    {
+        if ($this->localeFormat !== null) {
+            return $this->localeFormat;
+        }
+
+        return sprintf('0%s%%', $this->decimals > 0 ? '.' . str_repeat('0', $this->decimals) : null);
+    }
+}

+ 33 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Scientific.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
+
+use PhpOffice\PhpSpreadsheet\Exception;
+
+class Scientific extends NumberBase implements Wizard
+{
+    /**
+     * @param int $decimals number of decimal places to display, in the range 0-30
+     * @param ?string $locale Set the locale for the scientific format; or leave as the default null.
+     *          Locale has no effect for Scientific Format values, and is retained here for compatibility
+     *              with the other Wizards.
+     *          If provided, Locale values must be a valid formatted locale string (e.g. 'en-GB', 'fr', uz-Arab-AF).
+     *
+     * @throws Exception If a provided locale code is not a valid format
+     */
+    public function __construct(int $decimals = 2, ?string $locale = null)
+    {
+        $this->setDecimals($decimals);
+        $this->setLocale($locale);
+    }
+
+    protected function getLocaleFormat(): string
+    {
+        return $this->format();
+    }
+
+    public function format(): string
+    {
+        return sprintf('0%sE+00', $this->decimals > 0 ? '.' . str_repeat('0', $this->decimals) : null);
+    }
+}

+ 105 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Time.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
+
+class Time extends DateTimeWizard
+{
+    /**
+     * Hours without a leading zero, e.g. 9.
+     */
+    public const HOURS_SHORT = 'h';
+
+    /**
+     * Hours with a leading zero, e.g. 09.
+     */
+    public const HOURS_LONG = 'hh';
+
+    /**
+     * Minutes without a leading zero, e.g. 5.
+     */
+    public const MINUTES_SHORT = 'm';
+
+    /**
+     * Minutes with a leading zero, e.g. 05.
+     */
+    public const MINUTES_LONG = 'mm';
+
+    /**
+     * Seconds without a leading zero, e.g. 2.
+     */
+    public const SECONDS_SHORT = 's';
+
+    /**
+     * Seconds with a leading zero, e.g. 02.
+     */
+    public const SECONDS_LONG = 'ss';
+
+    public const MORNING_AFTERNOON = 'AM/PM';
+
+    protected const TIME_BLOCKS = [
+        self::HOURS_LONG,
+        self::HOURS_SHORT,
+        self::MINUTES_LONG,
+        self::MINUTES_SHORT,
+        self::SECONDS_LONG,
+        self::SECONDS_SHORT,
+        self::MORNING_AFTERNOON,
+    ];
+
+    public const SEPARATOR_COLON = ':';
+    public const SEPARATOR_SPACE_NONBREAKING = "\u{a0}";
+    public const SEPARATOR_SPACE = ' ';
+
+    protected const TIME_DEFAULT = [
+        self::HOURS_LONG,
+        self::MINUTES_LONG,
+        self::SECONDS_LONG,
+    ];
+
+    /**
+     * @var string[]
+     */
+    protected array $separators;
+
+    /**
+     * @var string[]
+     */
+    protected array $formatBlocks;
+
+    /**
+     * @param null|string|string[] $separators
+     *        If you want to use the same separator for all format blocks, then it can be passed as a string literal;
+     *           if you wish to use different separators, then they should be passed as an array.
+     *        If you want to use only a single format block, then pass a null as the separator argument
+     */
+    public function __construct($separators = self::SEPARATOR_COLON, string ...$formatBlocks)
+    {
+        $separators ??= self::SEPARATOR_COLON;
+        $formatBlocks = (count($formatBlocks) === 0) ? self::TIME_DEFAULT : $formatBlocks;
+
+        $this->separators = $this->padSeparatorArray(
+            is_array($separators) ? $separators : [$separators],
+            count($formatBlocks) - 1
+        );
+        $this->formatBlocks = array_map([$this, 'mapFormatBlocks'], $formatBlocks);
+    }
+
+    private function mapFormatBlocks(string $value): string
+    {
+        // Any date masking codes are returned as lower case values
+        //     except for AM/PM, which is set to uppercase
+        if (in_array(mb_strtolower($value), self::TIME_BLOCKS, true)) {
+            return mb_strtolower($value);
+        } elseif (mb_strtoupper($value) === self::MORNING_AFTERNOON) {
+            return mb_strtoupper($value);
+        }
+
+        // Wrap any string literals in quotes, so that they're clearly defined as string literals
+        return $this->wrapLiteral($value);
+    }
+
+    public function format(): string
+    {
+        return implode('', array_map([$this, 'intersperse'], $this->formatBlocks, $this->separators));
+    }
+}

+ 8 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Wizard.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
+
+interface Wizard
+{
+    public function format(): string;
+}

+ 198 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Protection.php

@@ -0,0 +1,198 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style;
+
+class Protection extends Supervisor
+{
+    /** Protection styles */
+    const PROTECTION_INHERIT = 'inherit';
+    const PROTECTION_PROTECTED = 'protected';
+    const PROTECTION_UNPROTECTED = 'unprotected';
+
+    /**
+     * Locked.
+     *
+     * @var string
+     */
+    protected $locked;
+
+    /**
+     * Hidden.
+     *
+     * @var string
+     */
+    protected $hidden;
+
+    /**
+     * Create a new Protection.
+     *
+     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
+     *                                    Leave this value at default unless you understand exactly what
+     *                                        its ramifications are
+     * @param bool $isConditional Flag indicating if this is a conditional style or not
+     *                                    Leave this value at default unless you understand exactly what
+     *                                        its ramifications are
+     */
+    public function __construct($isSupervisor = false, $isConditional = false)
+    {
+        // Supervisor?
+        parent::__construct($isSupervisor);
+
+        // Initialise values
+        if (!$isConditional) {
+            $this->locked = self::PROTECTION_INHERIT;
+            $this->hidden = self::PROTECTION_INHERIT;
+        }
+    }
+
+    /**
+     * Get the shared style component for the currently active cell in currently active sheet.
+     * Only used for style supervisor.
+     *
+     * @return Protection
+     */
+    public function getSharedComponent()
+    {
+        /** @var Style */
+        $parent = $this->parent;
+
+        return $parent->getSharedComponent()->getProtection();
+    }
+
+    /**
+     * Build style array from subcomponents.
+     *
+     * @param array $array
+     *
+     * @return array
+     */
+    public function getStyleArray($array)
+    {
+        return ['protection' => $array];
+    }
+
+    /**
+     * Apply styles from array.
+     *
+     * <code>
+     * $spreadsheet->getActiveSheet()->getStyle('B2')->getLocked()->applyFromArray(
+     *     [
+     *         'locked' => TRUE,
+     *         'hidden' => FALSE
+     *     ]
+     * );
+     * </code>
+     *
+     * @param array $styleArray Array containing style information
+     *
+     * @return $this
+     */
+    public function applyFromArray(array $styleArray)
+    {
+        if ($this->isSupervisor) {
+            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($styleArray));
+        } else {
+            if (isset($styleArray['locked'])) {
+                $this->setLocked($styleArray['locked']);
+            }
+            if (isset($styleArray['hidden'])) {
+                $this->setHidden($styleArray['hidden']);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get locked.
+     *
+     * @return string
+     */
+    public function getLocked()
+    {
+        if ($this->isSupervisor) {
+            return $this->getSharedComponent()->getLocked();
+        }
+
+        return $this->locked;
+    }
+
+    /**
+     * Set locked.
+     *
+     * @param string $lockType see self::PROTECTION_*
+     *
+     * @return $this
+     */
+    public function setLocked($lockType)
+    {
+        if ($this->isSupervisor) {
+            $styleArray = $this->getStyleArray(['locked' => $lockType]);
+            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+        } else {
+            $this->locked = $lockType;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get hidden.
+     *
+     * @return string
+     */
+    public function getHidden()
+    {
+        if ($this->isSupervisor) {
+            return $this->getSharedComponent()->getHidden();
+        }
+
+        return $this->hidden;
+    }
+
+    /**
+     * Set hidden.
+     *
+     * @param string $hiddenType see self::PROTECTION_*
+     *
+     * @return $this
+     */
+    public function setHidden($hiddenType)
+    {
+        if ($this->isSupervisor) {
+            $styleArray = $this->getStyleArray(['hidden' => $hiddenType]);
+            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+        } else {
+            $this->hidden = $hiddenType;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get hash code.
+     *
+     * @return string Hash code
+     */
+    public function getHashCode()
+    {
+        if ($this->isSupervisor) {
+            return $this->getSharedComponent()->getHashCode();
+        }
+
+        return md5(
+            $this->locked .
+            $this->hidden .
+            __CLASS__
+        );
+    }
+
+    protected function exportArray1(): array
+    {
+        $exportedArray = [];
+        $this->exportArray2($exportedArray, 'locked', $this->getLocked());
+        $this->exportArray2($exportedArray, 'hidden', $this->getHidden());
+
+        return $exportedArray;
+    }
+}

+ 175 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/RgbTint.php

@@ -0,0 +1,175 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style;
+
+/**
+ * Class to handle tint applied to color.
+ * Code borrows heavily from some Python projects.
+ *
+ * @see https://docs.python.org/3/library/colorsys.html
+ * @see https://gist.github.com/Mike-Honey/b36e651e9a7f1d2e1d60ce1c63b9b633
+ */
+class RgbTint
+{
+    private const ONE_THIRD = 1.0 / 3.0;
+    private const ONE_SIXTH = 1.0 / 6.0;
+    private const TWO_THIRD = 2.0 / 3.0;
+    private const RGBMAX = 255.0;
+    /**
+     * MS excel's tint function expects that HLS is base 240.
+     *
+     * @see https://social.msdn.microsoft.com/Forums/en-US/e9d8c136-6d62-4098-9b1b-dac786149f43/excel-color-tint-algorithm-incorrect?forum=os_binaryfile#d3c2ac95-52e0-476b-86f1-e2a697f24969
+     */
+    private const HLSMAX = 240.0;
+
+    /**
+     * Convert red/green/blue to hue/luminance/saturation.
+     *
+     * @param float $red 0.0 through 1.0
+     * @param float $green 0.0 through 1.0
+     * @param float $blue 0.0 through 1.0
+     *
+     * @return float[]
+     */
+    private static function rgbToHls(float $red, float $green, float $blue): array
+    {
+        $maxc = max($red, $green, $blue);
+        $minc = min($red, $green, $blue);
+        $luminance = ($minc + $maxc) / 2.0;
+        if ($minc === $maxc) {
+            return [0.0, $luminance, 0.0];
+        }
+        $maxMinusMin = $maxc - $minc;
+        if ($luminance <= 0.5) {
+            $s = $maxMinusMin / ($maxc + $minc);
+        } else {
+            $s = $maxMinusMin / (2.0 - $maxc - $minc);
+        }
+        $rc = ($maxc - $red) / $maxMinusMin;
+        $gc = ($maxc - $green) / $maxMinusMin;
+        $bc = ($maxc - $blue) / $maxMinusMin;
+        if ($red === $maxc) {
+            $h = $bc - $gc;
+        } elseif ($green === $maxc) {
+            $h = 2.0 + $rc - $bc;
+        } else {
+            $h = 4.0 + $gc - $rc;
+        }
+        $h = self::positiveDecimalPart($h / 6.0);
+
+        return [$h, $luminance, $s];
+    }
+
+    /** @var mixed */
+    private static $scrutinizerZeroPointZero = 0.0;
+
+    /**
+     * Convert hue/luminance/saturation to red/green/blue.
+     *
+     * @param float $hue 0.0 through 1.0
+     * @param float $luminance 0.0 through 1.0
+     * @param float $saturation 0.0 through 1.0
+     *
+     * @return float[]
+     */
+    private static function hlsToRgb($hue, $luminance, $saturation): array
+    {
+        if ($saturation === self::$scrutinizerZeroPointZero) {
+            return [$luminance, $luminance, $luminance];
+        }
+        if ($luminance <= 0.5) {
+            $m2 = $luminance * (1.0 + $saturation);
+        } else {
+            $m2 = $luminance + $saturation - ($luminance * $saturation);
+        }
+        $m1 = 2.0 * $luminance - $m2;
+
+        return [
+            self::vFunction($m1, $m2, $hue + self::ONE_THIRD),
+            self::vFunction($m1, $m2, $hue),
+            self::vFunction($m1, $m2, $hue - self::ONE_THIRD),
+        ];
+    }
+
+    private static function vFunction(float $m1, float $m2, float $hue): float
+    {
+        $hue = self::positiveDecimalPart($hue);
+        if ($hue < self::ONE_SIXTH) {
+            return $m1 + ($m2 - $m1) * $hue * 6.0;
+        }
+        if ($hue < 0.5) {
+            return $m2;
+        }
+        if ($hue < self::TWO_THIRD) {
+            return $m1 + ($m2 - $m1) * (self::TWO_THIRD - $hue) * 6.0;
+        }
+
+        return $m1;
+    }
+
+    private static function positiveDecimalPart(float $hue): float
+    {
+        $hue = fmod($hue, 1.0);
+
+        return ($hue >= 0.0) ? $hue : (1.0 + $hue);
+    }
+
+    /**
+     * Convert red/green/blue to HLSMAX-based hue/luminance/saturation.
+     *
+     * @return int[]
+     */
+    private static function rgbToMsHls(int $red, int $green, int $blue): array
+    {
+        $red01 = $red / self::RGBMAX;
+        $green01 = $green / self::RGBMAX;
+        $blue01 = $blue / self::RGBMAX;
+        [$hue, $luminance, $saturation] = self::rgbToHls($red01, $green01, $blue01);
+
+        return [
+            (int) round($hue * self::HLSMAX),
+            (int) round($luminance * self::HLSMAX),
+            (int) round($saturation * self::HLSMAX),
+        ];
+    }
+
+    /**
+     * Converts HLSMAX based HLS values to rgb values in the range (0,1).
+     *
+     * @return float[]
+     */
+    private static function msHlsToRgb(int $hue, int $lightness, int $saturation): array
+    {
+        return self::hlsToRgb($hue / self::HLSMAX, $lightness / self::HLSMAX, $saturation / self::HLSMAX);
+    }
+
+    /**
+     * Tints HLSMAX based luminance.
+     *
+     * @see http://ciintelligence.blogspot.co.uk/2012/02/converting-excel-theme-color-and-tint.html
+     */
+    private static function tintLuminance(float $tint, float $luminance): int
+    {
+        if ($tint < 0) {
+            return (int) round($luminance * (1.0 + $tint));
+        }
+
+        return (int) round($luminance * (1.0 - $tint) + (self::HLSMAX - self::HLSMAX * (1.0 - $tint)));
+    }
+
+    /**
+     * Return result of tinting supplied rgb as 6 hex digits.
+     */
+    public static function rgbAndTintToRgb(int $red, int $green, int $blue, float $tint): string
+    {
+        [$hue, $luminance, $saturation] = self::rgbToMsHls($red, $green, $blue);
+        [$red, $green, $blue] = self::msHlsToRgb($hue, self::tintLuminance($tint, $luminance), $saturation);
+
+        return sprintf(
+            '%02X%02X%02X',
+            (int) round($red * self::RGBMAX),
+            (int) round($green * self::RGBMAX),
+            (int) round($blue * self::RGBMAX)
+        );
+    }
+}

+ 745 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php

@@ -0,0 +1,745 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style;
+
+use PhpOffice\PhpSpreadsheet\Calculation\Functions;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Exception;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+
+class Style extends Supervisor
+{
+    /**
+     * Font.
+     *
+     * @var Font
+     */
+    protected $font;
+
+    /**
+     * Fill.
+     *
+     * @var Fill
+     */
+    protected $fill;
+
+    /**
+     * Borders.
+     *
+     * @var Borders
+     */
+    protected $borders;
+
+    /**
+     * Alignment.
+     *
+     * @var Alignment
+     */
+    protected $alignment;
+
+    /**
+     * Number Format.
+     *
+     * @var NumberFormat
+     */
+    protected $numberFormat;
+
+    /**
+     * Protection.
+     *
+     * @var Protection
+     */
+    protected $protection;
+
+    /**
+     * Index of style in collection. Only used for real style.
+     *
+     * @var int
+     */
+    protected $index;
+
+    /**
+     * Use Quote Prefix when displaying in cell editor. Only used for real style.
+     *
+     * @var bool
+     */
+    protected $quotePrefix = false;
+
+    /**
+     * Internal cache for styles
+     * Used when applying style on range of cells (column or row) and cleared when
+     * all cells in range is styled.
+     *
+     * PhpSpreadsheet will always minimize the amount of styles used. So cells with
+     * same styles will reference the same Style instance. To check if two styles
+     * are similar Style::getHashCode() is used. This call is expensive. To minimize
+     * the need to call this method we can cache the internal PHP object id of the
+     * Style in the range. Style::getHashCode() will then only be called when we
+     * encounter a unique style.
+     *
+     * @see Style::applyFromArray()
+     * @see Style::getHashCode()
+     *
+     * @var null|array<string, array>
+     */
+    private static $cachedStyles;
+
+    /**
+     * Create a new Style.
+     *
+     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
+     *         Leave this value at default unless you understand exactly what
+     *    its ramifications are
+     * @param bool $isConditional Flag indicating if this is a conditional style or not
+     *       Leave this value at default unless you understand exactly what
+     *    its ramifications are
+     */
+    public function __construct($isSupervisor = false, $isConditional = false)
+    {
+        parent::__construct($isSupervisor);
+
+        // Initialise values
+        $this->font = new Font($isSupervisor, $isConditional);
+        $this->fill = new Fill($isSupervisor, $isConditional);
+        $this->borders = new Borders($isSupervisor, $isConditional);
+        $this->alignment = new Alignment($isSupervisor, $isConditional);
+        $this->numberFormat = new NumberFormat($isSupervisor, $isConditional);
+        $this->protection = new Protection($isSupervisor, $isConditional);
+
+        // bind parent if we are a supervisor
+        if ($isSupervisor) {
+            $this->font->bindParent($this);
+            $this->fill->bindParent($this);
+            $this->borders->bindParent($this);
+            $this->alignment->bindParent($this);
+            $this->numberFormat->bindParent($this);
+            $this->protection->bindParent($this);
+        }
+    }
+
+    /**
+     * Get the shared style component for the currently active cell in currently active sheet.
+     * Only used for style supervisor.
+     */
+    public function getSharedComponent(): self
+    {
+        $activeSheet = $this->getActiveSheet();
+        $selectedCell = Functions::trimSheetFromCellReference($this->getActiveCell()); // e.g. 'A1'
+
+        if ($activeSheet->cellExists($selectedCell)) {
+            $xfIndex = $activeSheet->getCell($selectedCell)->getXfIndex();
+        } else {
+            $xfIndex = 0;
+        }
+
+        return $activeSheet->getParentOrThrow()->getCellXfByIndex($xfIndex);
+    }
+
+    /**
+     * Get parent. Only used for style supervisor.
+     */
+    public function getParent(): Spreadsheet
+    {
+        return $this->getActiveSheet()->getParentOrThrow();
+    }
+
+    /**
+     * Build style array from subcomponents.
+     *
+     * @param array $array
+     *
+     * @return array
+     */
+    public function getStyleArray($array)
+    {
+        return ['quotePrefix' => $array];
+    }
+
+    /**
+     * Apply styles from array.
+     *
+     * <code>
+     * $spreadsheet->getActiveSheet()->getStyle('B2')->applyFromArray(
+     *     [
+     *         'font' => [
+     *             'name' => 'Arial',
+     *             'bold' => true,
+     *             'italic' => false,
+     *             'underline' => Font::UNDERLINE_DOUBLE,
+     *             'strikethrough' => false,
+     *             'color' => [
+     *                 'rgb' => '808080'
+     *             ]
+     *         ],
+     *         'borders' => [
+     *             'bottom' => [
+     *                 'borderStyle' => Border::BORDER_DASHDOT,
+     *                 'color' => [
+     *                     'rgb' => '808080'
+     *                 ]
+     *             ],
+     *             'top' => [
+     *                 'borderStyle' => Border::BORDER_DASHDOT,
+     *                 'color' => [
+     *                     'rgb' => '808080'
+     *                 ]
+     *             ]
+     *         ],
+     *         'alignment' => [
+     *             'horizontal' => Alignment::HORIZONTAL_CENTER,
+     *             'vertical' => Alignment::VERTICAL_CENTER,
+     *             'wrapText' => true,
+     *         ],
+     *         'quotePrefix'    => true
+     *     ]
+     * );
+     * </code>
+     *
+     * @param array $styleArray Array containing style information
+     * @param bool $advancedBorders advanced mode for setting borders
+     *
+     * @return $this
+     */
+    public function applyFromArray(array $styleArray, $advancedBorders = true)
+    {
+        if ($this->isSupervisor) {
+            $pRange = $this->getSelectedCells();
+
+            // Uppercase coordinate and strip any Worksheet reference from the selected range
+            $pRange = strtoupper($pRange);
+            if (strpos($pRange, '!') !== false) {
+                $pRangeWorksheet = StringHelper::strToUpper(trim(substr($pRange, 0, (int) strrpos($pRange, '!')), "'"));
+                if ($pRangeWorksheet !== '' && StringHelper::strToUpper($this->getActiveSheet()->getTitle()) !== $pRangeWorksheet) {
+                    throw new Exception('Invalid Worksheet for specified Range');
+                }
+                $pRange = strtoupper(Functions::trimSheetFromCellReference($pRange));
+            }
+
+            // Is it a cell range or a single cell?
+            if (strpos($pRange, ':') === false) {
+                $rangeA = $pRange;
+                $rangeB = $pRange;
+            } else {
+                [$rangeA, $rangeB] = explode(':', $pRange);
+            }
+
+            // Calculate range outer borders
+            $rangeStart = Coordinate::coordinateFromString($rangeA);
+            $rangeEnd = Coordinate::coordinateFromString($rangeB);
+            $rangeStartIndexes = Coordinate::indexesFromString($rangeA);
+            $rangeEndIndexes = Coordinate::indexesFromString($rangeB);
+
+            $columnStart = $rangeStart[0];
+            $columnEnd = $rangeEnd[0];
+
+            // Make sure we can loop upwards on rows and columns
+            if ($rangeStartIndexes[0] > $rangeEndIndexes[0] && $rangeStartIndexes[1] > $rangeEndIndexes[1]) {
+                $tmp = $rangeStartIndexes;
+                $rangeStartIndexes = $rangeEndIndexes;
+                $rangeEndIndexes = $tmp;
+            }
+
+            // ADVANCED MODE:
+            if ($advancedBorders && isset($styleArray['borders'])) {
+                // 'allBorders' is a shorthand property for 'outline' and 'inside' and
+                //        it applies to components that have not been set explicitly
+                if (isset($styleArray['borders']['allBorders'])) {
+                    foreach (['outline', 'inside'] as $component) {
+                        if (!isset($styleArray['borders'][$component])) {
+                            $styleArray['borders'][$component] = $styleArray['borders']['allBorders'];
+                        }
+                    }
+                    unset($styleArray['borders']['allBorders']); // not needed any more
+                }
+                // 'outline' is a shorthand property for 'top', 'right', 'bottom', 'left'
+                //        it applies to components that have not been set explicitly
+                if (isset($styleArray['borders']['outline'])) {
+                    foreach (['top', 'right', 'bottom', 'left'] as $component) {
+                        if (!isset($styleArray['borders'][$component])) {
+                            $styleArray['borders'][$component] = $styleArray['borders']['outline'];
+                        }
+                    }
+                    unset($styleArray['borders']['outline']); // not needed any more
+                }
+                // 'inside' is a shorthand property for 'vertical' and 'horizontal'
+                //        it applies to components that have not been set explicitly
+                if (isset($styleArray['borders']['inside'])) {
+                    foreach (['vertical', 'horizontal'] as $component) {
+                        if (!isset($styleArray['borders'][$component])) {
+                            $styleArray['borders'][$component] = $styleArray['borders']['inside'];
+                        }
+                    }
+                    unset($styleArray['borders']['inside']); // not needed any more
+                }
+                // width and height characteristics of selection, 1, 2, or 3 (for 3 or more)
+                $xMax = min($rangeEndIndexes[0] - $rangeStartIndexes[0] + 1, 3);
+                $yMax = min($rangeEndIndexes[1] - $rangeStartIndexes[1] + 1, 3);
+
+                // loop through up to 3 x 3 = 9 regions
+                for ($x = 1; $x <= $xMax; ++$x) {
+                    // start column index for region
+                    $colStart = ($x == 3) ?
+                        Coordinate::stringFromColumnIndex($rangeEndIndexes[0])
+                        : Coordinate::stringFromColumnIndex($rangeStartIndexes[0] + $x - 1);
+                    // end column index for region
+                    $colEnd = ($x == 1) ?
+                        Coordinate::stringFromColumnIndex($rangeStartIndexes[0])
+                        : Coordinate::stringFromColumnIndex($rangeEndIndexes[0] - $xMax + $x);
+
+                    for ($y = 1; $y <= $yMax; ++$y) {
+                        // which edges are touching the region
+                        $edges = [];
+                        if ($x == 1) {
+                            // are we at left edge
+                            $edges[] = 'left';
+                        }
+                        if ($x == $xMax) {
+                            // are we at right edge
+                            $edges[] = 'right';
+                        }
+                        if ($y == 1) {
+                            // are we at top edge?
+                            $edges[] = 'top';
+                        }
+                        if ($y == $yMax) {
+                            // are we at bottom edge?
+                            $edges[] = 'bottom';
+                        }
+
+                        // start row index for region
+                        $rowStart = ($y == 3) ?
+                            $rangeEndIndexes[1] : $rangeStartIndexes[1] + $y - 1;
+
+                        // end row index for region
+                        $rowEnd = ($y == 1) ?
+                            $rangeStartIndexes[1] : $rangeEndIndexes[1] - $yMax + $y;
+
+                        // build range for region
+                        $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd;
+
+                        // retrieve relevant style array for region
+                        $regionStyles = $styleArray;
+                        unset($regionStyles['borders']['inside']);
+
+                        // what are the inner edges of the region when looking at the selection
+                        $innerEdges = array_diff(['top', 'right', 'bottom', 'left'], $edges);
+
+                        // inner edges that are not touching the region should take the 'inside' border properties if they have been set
+                        foreach ($innerEdges as $innerEdge) {
+                            switch ($innerEdge) {
+                                case 'top':
+                                case 'bottom':
+                                    // should pick up 'horizontal' border property if set
+                                    if (isset($styleArray['borders']['horizontal'])) {
+                                        $regionStyles['borders'][$innerEdge] = $styleArray['borders']['horizontal'];
+                                    } else {
+                                        unset($regionStyles['borders'][$innerEdge]);
+                                    }
+
+                                    break;
+                                case 'left':
+                                case 'right':
+                                    // should pick up 'vertical' border property if set
+                                    if (isset($styleArray['borders']['vertical'])) {
+                                        $regionStyles['borders'][$innerEdge] = $styleArray['borders']['vertical'];
+                                    } else {
+                                        unset($regionStyles['borders'][$innerEdge]);
+                                    }
+
+                                    break;
+                            }
+                        }
+
+                        // apply region style to region by calling applyFromArray() in simple mode
+                        $this->getActiveSheet()->getStyle($range)->applyFromArray($regionStyles, false);
+                    }
+                }
+
+                // restore initial cell selection range
+                $this->getActiveSheet()->getStyle($pRange);
+
+                return $this;
+            }
+
+            // SIMPLE MODE:
+            // Selection type, inspect
+            if (preg_match('/^[A-Z]+1:[A-Z]+1048576$/', $pRange)) {
+                $selectionType = 'COLUMN';
+
+                // Enable caching of styles
+                self::$cachedStyles = ['hashByObjId' => [], 'styleByHash' => []];
+            } elseif (preg_match('/^A\d+:XFD\d+$/', $pRange)) {
+                $selectionType = 'ROW';
+
+                // Enable caching of styles
+                self::$cachedStyles = ['hashByObjId' => [], 'styleByHash' => []];
+            } else {
+                $selectionType = 'CELL';
+            }
+
+            // First loop through columns, rows, or cells to find out which styles are affected by this operation
+            $oldXfIndexes = $this->getOldXfIndexes($selectionType, $rangeStartIndexes, $rangeEndIndexes, $columnStart, $columnEnd, $styleArray);
+
+            // clone each of the affected styles, apply the style array, and add the new styles to the workbook
+            $workbook = $this->getActiveSheet()->getParentOrThrow();
+            $newXfIndexes = [];
+            foreach ($oldXfIndexes as $oldXfIndex => $dummy) {
+                $style = $workbook->getCellXfByIndex($oldXfIndex);
+
+                // $cachedStyles is set when applying style for a range of cells, either column or row
+                if (self::$cachedStyles === null) {
+                    // Clone the old style and apply style-array
+                    $newStyle = clone $style;
+                    $newStyle->applyFromArray($styleArray);
+
+                    // Look for existing style we can use instead (reduce memory usage)
+                    $existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode());
+                } else {
+                    // Style cache is stored by Style::getHashCode(). But calling this method is
+                    // expensive. So we cache the php obj id -> hash.
+                    $objId = spl_object_id($style);
+
+                    // Look for the original HashCode
+                    $styleHash = self::$cachedStyles['hashByObjId'][$objId] ?? null;
+                    if ($styleHash === null) {
+                        // This object_id is not cached, store the hashcode in case encounter again
+                        $styleHash = self::$cachedStyles['hashByObjId'][$objId] = $style->getHashCode();
+                    }
+
+                    // Find existing style by hash.
+                    $existingStyle = self::$cachedStyles['styleByHash'][$styleHash] ?? null;
+
+                    if (!$existingStyle) {
+                        // The old style combined with the new style array is not cached, so we create it now
+                        $newStyle = clone $style;
+                        $newStyle->applyFromArray($styleArray);
+
+                        // Look for similar style in workbook to reduce memory usage
+                        $existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode());
+
+                        // Cache the new style by original hashcode
+                        self::$cachedStyles['styleByHash'][$styleHash] = $existingStyle instanceof self ? $existingStyle : $newStyle;
+                    }
+                }
+
+                if ($existingStyle) {
+                    // there is already such cell Xf in our collection
+                    $newXfIndexes[$oldXfIndex] = $existingStyle->getIndex();
+                } else {
+                    if (!isset($newStyle)) {
+                        // Handle bug in PHPStan, see https://github.com/phpstan/phpstan/issues/5805
+                        // $newStyle should always be defined.
+                        // This block might not be needed in the future
+                        // @codeCoverageIgnoreStart
+                        $newStyle = clone $style;
+                        $newStyle->applyFromArray($styleArray);
+                        // @codeCoverageIgnoreEnd
+                    }
+
+                    // we don't have such a cell Xf, need to add
+                    $workbook->addCellXf($newStyle);
+                    $newXfIndexes[$oldXfIndex] = $newStyle->getIndex();
+                }
+            }
+
+            // Loop through columns, rows, or cells again and update the XF index
+            switch ($selectionType) {
+                case 'COLUMN':
+                    for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) {
+                        $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col);
+                        $oldXfIndex = $columnDimension->getXfIndex();
+                        $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
+                    }
+
+                    // Disable caching of styles
+                    self::$cachedStyles = null;
+
+                    break;
+                case 'ROW':
+                    for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) {
+                        $rowDimension = $this->getActiveSheet()->getRowDimension($row);
+                        // row without explicit style should be formatted based on default style
+                        $oldXfIndex = $rowDimension->getXfIndex() ?? 0;
+                        $rowDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
+                    }
+
+                    // Disable caching of styles
+                    self::$cachedStyles = null;
+
+                    break;
+                case 'CELL':
+                    for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) {
+                        for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) {
+                            $cell = $this->getActiveSheet()->getCell([$col, $row]);
+                            $oldXfIndex = $cell->getXfIndex();
+                            $cell->setXfIndex($newXfIndexes[$oldXfIndex]);
+                        }
+                    }
+
+                    break;
+            }
+        } else {
+            // not a supervisor, just apply the style array directly on style object
+            if (isset($styleArray['fill'])) {
+                $this->getFill()->applyFromArray($styleArray['fill']);
+            }
+            if (isset($styleArray['font'])) {
+                $this->getFont()->applyFromArray($styleArray['font']);
+            }
+            if (isset($styleArray['borders'])) {
+                $this->getBorders()->applyFromArray($styleArray['borders']);
+            }
+            if (isset($styleArray['alignment'])) {
+                $this->getAlignment()->applyFromArray($styleArray['alignment']);
+            }
+            if (isset($styleArray['numberFormat'])) {
+                $this->getNumberFormat()->applyFromArray($styleArray['numberFormat']);
+            }
+            if (isset($styleArray['protection'])) {
+                $this->getProtection()->applyFromArray($styleArray['protection']);
+            }
+            if (isset($styleArray['quotePrefix'])) {
+                $this->quotePrefix = $styleArray['quotePrefix'];
+            }
+        }
+
+        return $this;
+    }
+
+    private function getOldXfIndexes(string $selectionType, array $rangeStart, array $rangeEnd, string $columnStart, string $columnEnd, array $styleArray): array
+    {
+        $oldXfIndexes = [];
+        switch ($selectionType) {
+            case 'COLUMN':
+                for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
+                    $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true;
+                }
+                foreach ($this->getActiveSheet()->getColumnIterator($columnStart, $columnEnd) as $columnIterator) {
+                    $cellIterator = $columnIterator->getCellIterator();
+                    $cellIterator->setIterateOnlyExistingCells(true);
+                    foreach ($cellIterator as $columnCell) {
+                        if ($columnCell !== null) {
+                            $columnCell->getStyle()->applyFromArray($styleArray);
+                        }
+                    }
+                }
+
+                break;
+            case 'ROW':
+                for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
+                    if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() === null) {
+                        $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style
+                    } else {
+                        $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true;
+                    }
+                }
+                foreach ($this->getActiveSheet()->getRowIterator((int) $rangeStart[1], (int) $rangeEnd[1]) as $rowIterator) {
+                    $cellIterator = $rowIterator->getCellIterator();
+                    $cellIterator->setIterateOnlyExistingCells(true);
+                    foreach ($cellIterator as $rowCell) {
+                        if ($rowCell !== null) {
+                            $rowCell->getStyle()->applyFromArray($styleArray);
+                        }
+                    }
+                }
+
+                break;
+            case 'CELL':
+                for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
+                    for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
+                        $oldXfIndexes[$this->getActiveSheet()->getCell([$col, $row])->getXfIndex()] = true;
+                    }
+                }
+
+                break;
+        }
+
+        return $oldXfIndexes;
+    }
+
+    /**
+     * Get Fill.
+     *
+     * @return Fill
+     */
+    public function getFill()
+    {
+        return $this->fill;
+    }
+
+    /**
+     * Get Font.
+     *
+     * @return Font
+     */
+    public function getFont()
+    {
+        return $this->font;
+    }
+
+    /**
+     * Set font.
+     *
+     * @return $this
+     */
+    public function setFont(Font $font)
+    {
+        $this->font = $font;
+
+        return $this;
+    }
+
+    /**
+     * Get Borders.
+     *
+     * @return Borders
+     */
+    public function getBorders()
+    {
+        return $this->borders;
+    }
+
+    /**
+     * Get Alignment.
+     *
+     * @return Alignment
+     */
+    public function getAlignment()
+    {
+        return $this->alignment;
+    }
+
+    /**
+     * Get Number Format.
+     *
+     * @return NumberFormat
+     */
+    public function getNumberFormat()
+    {
+        return $this->numberFormat;
+    }
+
+    /**
+     * Get Conditional Styles. Only used on supervisor.
+     *
+     * @return Conditional[]
+     */
+    public function getConditionalStyles()
+    {
+        return $this->getActiveSheet()->getConditionalStyles($this->getActiveCell());
+    }
+
+    /**
+     * Set Conditional Styles. Only used on supervisor.
+     *
+     * @param Conditional[] $conditionalStyleArray Array of conditional styles
+     *
+     * @return $this
+     */
+    public function setConditionalStyles(array $conditionalStyleArray)
+    {
+        $this->getActiveSheet()->setConditionalStyles($this->getSelectedCells(), $conditionalStyleArray);
+
+        return $this;
+    }
+
+    /**
+     * Get Protection.
+     *
+     * @return Protection
+     */
+    public function getProtection()
+    {
+        return $this->protection;
+    }
+
+    /**
+     * Get quote prefix.
+     *
+     * @return bool
+     */
+    public function getQuotePrefix()
+    {
+        if ($this->isSupervisor) {
+            return $this->getSharedComponent()->getQuotePrefix();
+        }
+
+        return $this->quotePrefix;
+    }
+
+    /**
+     * Set quote prefix.
+     *
+     * @param bool $quotePrefix
+     *
+     * @return $this
+     */
+    public function setQuotePrefix($quotePrefix)
+    {
+        if ($quotePrefix == '') {
+            $quotePrefix = false;
+        }
+        if ($this->isSupervisor) {
+            $styleArray = ['quotePrefix' => $quotePrefix];
+            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
+        } else {
+            $this->quotePrefix = (bool) $quotePrefix;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get hash code.
+     *
+     * @return string Hash code
+     */
+    public function getHashCode()
+    {
+        return md5(
+            $this->fill->getHashCode() .
+            $this->font->getHashCode() .
+            $this->borders->getHashCode() .
+            $this->alignment->getHashCode() .
+            $this->numberFormat->getHashCode() .
+            $this->protection->getHashCode() .
+            ($this->quotePrefix ? 't' : 'f') .
+            __CLASS__
+        );
+    }
+
+    /**
+     * Get own index in style collection.
+     *
+     * @return int
+     */
+    public function getIndex()
+    {
+        return $this->index;
+    }
+
+    /**
+     * Set own index in style collection.
+     *
+     * @param int $index
+     */
+    public function setIndex($index): void
+    {
+        $this->index = $index;
+    }
+
+    protected function exportArray1(): array
+    {
+        $exportedArray = [];
+        $this->exportArray2($exportedArray, 'alignment', $this->getAlignment());
+        $this->exportArray2($exportedArray, 'borders', $this->getBorders());
+        $this->exportArray2($exportedArray, 'fill', $this->getFill());
+        $this->exportArray2($exportedArray, 'font', $this->getFont());
+        $this->exportArray2($exportedArray, 'numberFormat', $this->getNumberFormat());
+        $this->exportArray2($exportedArray, 'protection', $this->getProtection());
+        $this->exportArray2($exportedArray, 'quotePrefx', $this->getQuotePrefix());
+
+        return $exportedArray;
+    }
+}

+ 175 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Supervisor.php

@@ -0,0 +1,175 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Style;
+
+use PhpOffice\PhpSpreadsheet\IComparable;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+
+abstract class Supervisor implements IComparable
+{
+    /**
+     * Supervisor?
+     *
+     * @var bool
+     */
+    protected $isSupervisor;
+
+    /**
+     * Parent. Only used for supervisor.
+     *
+     * @var Spreadsheet|Supervisor
+     */
+    protected $parent;
+
+    /**
+     * Parent property name.
+     *
+     * @var null|string
+     */
+    protected $parentPropertyName;
+
+    /**
+     * Create a new Supervisor.
+     *
+     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
+     *                                    Leave this value at default unless you understand exactly what
+     *                                        its ramifications are
+     */
+    public function __construct($isSupervisor = false)
+    {
+        // Supervisor?
+        $this->isSupervisor = $isSupervisor;
+    }
+
+    /**
+     * Bind parent. Only used for supervisor.
+     *
+     * @param Spreadsheet|Supervisor $parent
+     * @param null|string $parentPropertyName
+     *
+     * @return $this
+     */
+    public function bindParent($parent, $parentPropertyName = null)
+    {
+        $this->parent = $parent;
+        $this->parentPropertyName = $parentPropertyName;
+
+        return $this;
+    }
+
+    /**
+     * Is this a supervisor or a cell style component?
+     *
+     * @return bool
+     */
+    public function getIsSupervisor()
+    {
+        return $this->isSupervisor;
+    }
+
+    /**
+     * Get the currently active sheet. Only used for supervisor.
+     *
+     * @return Worksheet
+     */
+    public function getActiveSheet()
+    {
+        return $this->parent->getActiveSheet();
+    }
+
+    /**
+     * Get the currently active cell coordinate in currently active sheet.
+     * Only used for supervisor.
+     *
+     * @return string E.g. 'A1'
+     */
+    public function getSelectedCells()
+    {
+        return $this->getActiveSheet()->getSelectedCells();
+    }
+
+    /**
+     * Get the currently active cell coordinate in currently active sheet.
+     * Only used for supervisor.
+     *
+     * @return string E.g. 'A1'
+     */
+    public function getActiveCell()
+    {
+        return $this->getActiveSheet()->getActiveCell();
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $value) {
+            if ((is_object($value)) && ($key != 'parent')) {
+                $this->$key = clone $value;
+            } else {
+                $this->$key = $value;
+            }
+        }
+    }
+
+    /**
+     * Export style as array.
+     *
+     * Available to anything which extends this class:
+     * Alignment, Border, Borders, Color, Fill, Font,
+     * NumberFormat, Protection, and Style.
+     */
+    final public function exportArray(): array
+    {
+        return $this->exportArray1();
+    }
+
+    /**
+     * Abstract method to be implemented in anything which
+     * extends this class.
+     *
+     * This method invokes exportArray2 with the names and values
+     * of all properties to be included in output array,
+     * returning that array to exportArray, then to caller.
+     */
+    abstract protected function exportArray1(): array;
+
+    /**
+     * Populate array from exportArray1.
+     * This method is available to anything which extends this class.
+     * The parameter index is the key to be added to the array.
+     * The parameter objOrValue is either a primitive type,
+     * which is the value added to the array,
+     * or a Style object to be recursively added via exportArray.
+     *
+     * @param mixed $objOrValue
+     */
+    final protected function exportArray2(array &$exportedArray, string $index, $objOrValue): void
+    {
+        if ($objOrValue instanceof self) {
+            $exportedArray[$index] = $objOrValue->exportArray();
+        } else {
+            $exportedArray[$index] = $objOrValue;
+        }
+    }
+
+    /**
+     * Get the shared style component for the currently active cell in currently active sheet.
+     * Only used for style supervisor.
+     *
+     * @return mixed
+     */
+    abstract public function getSharedComponent();
+
+    /**
+     * Build style array from subcomponents.
+     *
+     * @param array $array
+     *
+     * @return array
+     */
+    abstract public function getStyleArray($array);
+}

+ 269 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Theme.php

@@ -0,0 +1,269 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet;
+
+class Theme
+{
+    /** @var string */
+    private $themeColorName = 'Office';
+
+    /** @var string */
+    private $themeFontName = 'Office';
+
+    public const COLOR_SCHEME_2013_PLUS_NAME = 'Office 2013+';
+    public const COLOR_SCHEME_2013_PLUS = [
+        'dk1' => '000000',
+        'lt1' => 'FFFFFF',
+        'dk2' => '44546A',
+        'lt2' => 'E7E6E6',
+        'accent1' => '4472C4',
+        'accent2' => 'ED7D31',
+        'accent3' => 'A5A5A5',
+        'accent4' => 'FFC000',
+        'accent5' => '5B9BD5',
+        'accent6' => '70AD47',
+        'hlink' => '0563C1',
+        'folHlink' => '954F72',
+    ];
+
+    public const COLOR_SCHEME_2007_2010_NAME = 'Office 2007-2010';
+    public const COLOR_SCHEME_2007_2010 = [
+        'dk1' => '000000',
+        'lt1' => 'FFFFFF',
+        'dk2' => '1F497D',
+        'lt2' => 'EEECE1',
+        'accent1' => '4F81BD',
+        'accent2' => 'C0504D',
+        'accent3' => '9BBB59',
+        'accent4' => '8064A2',
+        'accent5' => '4BACC6',
+        'accent6' => 'F79646',
+        'hlink' => '0000FF',
+        'folHlink' => '800080',
+    ];
+
+    /** @var string[] */
+    private $themeColors = self::COLOR_SCHEME_2007_2010;
+
+    /** @var string */
+    private $majorFontLatin = 'Cambria';
+
+    /** @var string */
+    private $majorFontEastAsian = '';
+
+    /** @var string */
+    private $majorFontComplexScript = '';
+
+    /** @var string */
+    private $minorFontLatin = 'Calibri';
+
+    /** @var string */
+    private $minorFontEastAsian = '';
+
+    /** @var string */
+    private $minorFontComplexScript = '';
+
+    /**
+     * Map of Major (header) fonts to write.
+     *
+     * @var string[]
+     */
+    private $majorFontSubstitutions = self::FONTS_TIMES_SUBSTITUTIONS;
+
+    /**
+     * Map of Minor (body) fonts to write.
+     *
+     * @var string[]
+     */
+    private $minorFontSubstitutions = self::FONTS_ARIAL_SUBSTITUTIONS;
+
+    public const FONTS_TIMES_SUBSTITUTIONS = [
+        'Jpan' => 'MS Pゴシック',
+        'Hang' => '맑은 고딕',
+        'Hans' => '宋体',
+        'Hant' => '新細明體',
+        'Arab' => 'Times New Roman',
+        'Hebr' => 'Times New Roman',
+        'Thai' => 'Tahoma',
+        'Ethi' => 'Nyala',
+        'Beng' => 'Vrinda',
+        'Gujr' => 'Shruti',
+        'Khmr' => 'MoolBoran',
+        'Knda' => 'Tunga',
+        'Guru' => 'Raavi',
+        'Cans' => 'Euphemia',
+        'Cher' => 'Plantagenet Cherokee',
+        'Yiii' => 'Microsoft Yi Baiti',
+        'Tibt' => 'Microsoft Himalaya',
+        'Thaa' => 'MV Boli',
+        'Deva' => 'Mangal',
+        'Telu' => 'Gautami',
+        'Taml' => 'Latha',
+        'Syrc' => 'Estrangelo Edessa',
+        'Orya' => 'Kalinga',
+        'Mlym' => 'Kartika',
+        'Laoo' => 'DokChampa',
+        'Sinh' => 'Iskoola Pota',
+        'Mong' => 'Mongolian Baiti',
+        'Viet' => 'Times New Roman',
+        'Uigh' => 'Microsoft Uighur',
+        'Geor' => 'Sylfaen',
+    ];
+
+    public const FONTS_ARIAL_SUBSTITUTIONS = [
+        'Jpan' => 'MS Pゴシック',
+        'Hang' => '맑은 고딕',
+        'Hans' => '宋体',
+        'Hant' => '新細明體',
+        'Arab' => 'Arial',
+        'Hebr' => 'Arial',
+        'Thai' => 'Tahoma',
+        'Ethi' => 'Nyala',
+        'Beng' => 'Vrinda',
+        'Gujr' => 'Shruti',
+        'Khmr' => 'DaunPenh',
+        'Knda' => 'Tunga',
+        'Guru' => 'Raavi',
+        'Cans' => 'Euphemia',
+        'Cher' => 'Plantagenet Cherokee',
+        'Yiii' => 'Microsoft Yi Baiti',
+        'Tibt' => 'Microsoft Himalaya',
+        'Thaa' => 'MV Boli',
+        'Deva' => 'Mangal',
+        'Telu' => 'Gautami',
+        'Taml' => 'Latha',
+        'Syrc' => 'Estrangelo Edessa',
+        'Orya' => 'Kalinga',
+        'Mlym' => 'Kartika',
+        'Laoo' => 'DokChampa',
+        'Sinh' => 'Iskoola Pota',
+        'Mong' => 'Mongolian Baiti',
+        'Viet' => 'Arial',
+        'Uigh' => 'Microsoft Uighur',
+        'Geor' => 'Sylfaen',
+    ];
+
+    public function getThemeColors(): array
+    {
+        return $this->themeColors;
+    }
+
+    public function setThemeColor(string $key, string $value): self
+    {
+        $this->themeColors[$key] = $value;
+
+        return $this;
+    }
+
+    public function getThemeColorName(): string
+    {
+        return $this->themeColorName;
+    }
+
+    public function setThemeColorName(string $name, ?array $themeColors = null): self
+    {
+        $this->themeColorName = $name;
+        if ($name === self::COLOR_SCHEME_2007_2010_NAME) {
+            $themeColors = $themeColors ?? self::COLOR_SCHEME_2007_2010;
+        } elseif ($name === self::COLOR_SCHEME_2013_PLUS_NAME) {
+            $themeColors = $themeColors ?? self::COLOR_SCHEME_2013_PLUS;
+        }
+        if ($themeColors !== null) {
+            $this->themeColors = $themeColors;
+        }
+
+        return $this;
+    }
+
+    public function getMajorFontLatin(): string
+    {
+        return $this->majorFontLatin;
+    }
+
+    public function getMajorFontEastAsian(): string
+    {
+        return $this->majorFontEastAsian;
+    }
+
+    public function getMajorFontComplexScript(): string
+    {
+        return $this->majorFontComplexScript;
+    }
+
+    public function getMajorFontSubstitutions(): array
+    {
+        return $this->majorFontSubstitutions;
+    }
+
+    /** @param null|array $substitutions */
+    public function setMajorFontValues(?string $latin, ?string $eastAsian, ?string $complexScript, $substitutions): self
+    {
+        if (!empty($latin)) {
+            $this->majorFontLatin = $latin;
+        }
+        if ($eastAsian !== null) {
+            $this->majorFontEastAsian = $eastAsian;
+        }
+        if ($complexScript !== null) {
+            $this->majorFontComplexScript = $complexScript;
+        }
+        if ($substitutions !== null) {
+            $this->majorFontSubstitutions = $substitutions;
+        }
+
+        return $this;
+    }
+
+    public function getMinorFontLatin(): string
+    {
+        return $this->minorFontLatin;
+    }
+
+    public function getMinorFontEastAsian(): string
+    {
+        return $this->minorFontEastAsian;
+    }
+
+    public function getMinorFontComplexScript(): string
+    {
+        return $this->minorFontComplexScript;
+    }
+
+    public function getMinorFontSubstitutions(): array
+    {
+        return $this->minorFontSubstitutions;
+    }
+
+    /** @param null|array $substitutions */
+    public function setMinorFontValues(?string $latin, ?string $eastAsian, ?string $complexScript, $substitutions): self
+    {
+        if (!empty($latin)) {
+            $this->minorFontLatin = $latin;
+        }
+        if ($eastAsian !== null) {
+            $this->minorFontEastAsian = $eastAsian;
+        }
+        if ($complexScript !== null) {
+            $this->minorFontComplexScript = $complexScript;
+        }
+        if ($substitutions !== null) {
+            $this->minorFontSubstitutions = $substitutions;
+        }
+
+        return $this;
+    }
+
+    public function getThemeFontName(): string
+    {
+        return $this->themeFontName;
+    }
+
+    public function setThemeFontName(?string $name): self
+    {
+        if (!empty($name)) {
+            $this->themeFontName = $name;
+        }
+
+        return $this;
+    }
+}

+ 1118 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php

@@ -0,0 +1,1118 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use DateTime;
+use DateTimeZone;
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Calculation\Functions;
+use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
+use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Exception;
+use PhpOffice\PhpSpreadsheet\Shared\Date;
+use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
+
+class AutoFilter
+{
+    /**
+     * Autofilter Worksheet.
+     *
+     * @var null|Worksheet
+     */
+    private $workSheet;
+
+    /**
+     * Autofilter Range.
+     *
+     * @var string
+     */
+    private $range = '';
+
+    /**
+     * Autofilter Column Ruleset.
+     *
+     * @var AutoFilter\Column[]
+     */
+    private $columns = [];
+
+    /** @var bool */
+    private $evaluated = false;
+
+    public function getEvaluated(): bool
+    {
+        return $this->evaluated;
+    }
+
+    public function setEvaluated(bool $value): void
+    {
+        $this->evaluated = $value;
+    }
+
+    /**
+     * Create a new AutoFilter.
+     *
+     * @param AddressRange|array<int>|string $range
+     *            A simple string containing a Cell range like 'A1:E10' is permitted
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or an AddressRange object.
+     */
+    public function __construct($range = '', ?Worksheet $worksheet = null)
+    {
+        if ($range !== '') {
+            [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
+        }
+
+        $this->range = $range;
+        $this->workSheet = $worksheet;
+    }
+
+    /**
+     * Get AutoFilter Parent Worksheet.
+     *
+     * @return null|Worksheet
+     */
+    public function getParent()
+    {
+        return $this->workSheet;
+    }
+
+    /**
+     * Set AutoFilter Parent Worksheet.
+     *
+     * @return $this
+     */
+    public function setParent(?Worksheet $worksheet = null)
+    {
+        $this->evaluated = false;
+        $this->workSheet = $worksheet;
+
+        return $this;
+    }
+
+    /**
+     * Get AutoFilter Range.
+     *
+     * @return string
+     */
+    public function getRange()
+    {
+        return $this->range;
+    }
+
+    /**
+     * Set AutoFilter Cell Range.
+     *
+     * @param AddressRange|array<int>|string $range
+     *            A simple string containing a Cell range like 'A1:E10' or a Cell address like 'A1' is permitted
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or an AddressRange object.
+     */
+    public function setRange($range = ''): self
+    {
+        $this->evaluated = false;
+        // extract coordinate
+        if ($range !== '') {
+            [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
+        }
+
+        if (empty($range)) {
+            //    Discard all column rules
+            $this->columns = [];
+            $this->range = '';
+
+            return $this;
+        }
+
+        if (ctype_digit($range) || ctype_alpha($range)) {
+            throw new Exception("{$range} is an invalid range for AutoFilter");
+        }
+
+        $this->range = $range;
+        //    Discard any column rules that are no longer valid within this range
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+        foreach ($this->columns as $key => $value) {
+            $colIndex = Coordinate::columnIndexFromString($key);
+            if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
+                unset($this->columns[$key]);
+            }
+        }
+
+        return $this;
+    }
+
+    public function setRangeToMaxRow(): self
+    {
+        $this->evaluated = false;
+        if ($this->workSheet !== null) {
+            $thisrange = $this->range;
+            $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange);
+            if ($range !== $thisrange) {
+                $this->setRange($range);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get all AutoFilter Columns.
+     *
+     * @return AutoFilter\Column[]
+     */
+    public function getColumns()
+    {
+        return $this->columns;
+    }
+
+    /**
+     * Validate that the specified column is in the AutoFilter range.
+     *
+     * @param string $column Column name (e.g. A)
+     *
+     * @return int The column offset within the autofilter range
+     */
+    public function testColumnInRange($column)
+    {
+        if (empty($this->range)) {
+            throw new Exception('No autofilter range is defined.');
+        }
+
+        $columnIndex = Coordinate::columnIndexFromString($column);
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+        if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
+            throw new Exception('Column is outside of current autofilter range.');
+        }
+
+        return $columnIndex - $rangeStart[0];
+    }
+
+    /**
+     * Get a specified AutoFilter Column Offset within the defined AutoFilter range.
+     *
+     * @param string $column Column name (e.g. A)
+     *
+     * @return int The offset of the specified column within the autofilter range
+     */
+    public function getColumnOffset($column)
+    {
+        return $this->testColumnInRange($column);
+    }
+
+    /**
+     * Get a specified AutoFilter Column.
+     *
+     * @param string $column Column name (e.g. A)
+     *
+     * @return AutoFilter\Column
+     */
+    public function getColumn($column)
+    {
+        $this->testColumnInRange($column);
+
+        if (!isset($this->columns[$column])) {
+            $this->columns[$column] = new AutoFilter\Column($column, $this);
+        }
+
+        return $this->columns[$column];
+    }
+
+    /**
+     * Get a specified AutoFilter Column by it's offset.
+     *
+     * @param int $columnOffset Column offset within range (starting from 0)
+     *
+     * @return AutoFilter\Column
+     */
+    public function getColumnByOffset($columnOffset)
+    {
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+        $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset);
+
+        return $this->getColumn($pColumn);
+    }
+
+    /**
+     * Set AutoFilter.
+     *
+     * @param AutoFilter\Column|string $columnObjectOrString
+     *            A simple string containing a Column ID like 'A' is permitted
+     *
+     * @return $this
+     */
+    public function setColumn($columnObjectOrString)
+    {
+        $this->evaluated = false;
+        if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {
+            $column = $columnObjectOrString;
+        } elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof AutoFilter\Column)) {
+            $column = $columnObjectOrString->getColumnIndex();
+        } else {
+            throw new Exception('Column is not within the autofilter range.');
+        }
+        $this->testColumnInRange($column);
+
+        if (is_string($columnObjectOrString)) {
+            $this->columns[$columnObjectOrString] = new AutoFilter\Column($columnObjectOrString, $this);
+        } else {
+            $columnObjectOrString->setParent($this);
+            $this->columns[$column] = $columnObjectOrString;
+        }
+        ksort($this->columns);
+
+        return $this;
+    }
+
+    /**
+     * Clear a specified AutoFilter Column.
+     *
+     * @param string $column Column name (e.g. A)
+     *
+     * @return $this
+     */
+    public function clearColumn($column)
+    {
+        $this->evaluated = false;
+        $this->testColumnInRange($column);
+
+        if (isset($this->columns[$column])) {
+            unset($this->columns[$column]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Shift an AutoFilter Column Rule to a different column.
+     *
+     * Note: This method bypasses validation of the destination column to ensure it is within this AutoFilter range.
+     *        Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
+     *        Use with caution.
+     *
+     * @param string $fromColumn Column name (e.g. A)
+     * @param string $toColumn Column name (e.g. B)
+     *
+     * @return $this
+     */
+    public function shiftColumn($fromColumn, $toColumn)
+    {
+        $this->evaluated = false;
+        $fromColumn = strtoupper($fromColumn);
+        $toColumn = strtoupper($toColumn);
+
+        if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) {
+            $this->columns[$fromColumn]->setParent();
+            $this->columns[$fromColumn]->setColumnIndex($toColumn);
+            $this->columns[$toColumn] = $this->columns[$fromColumn];
+            $this->columns[$toColumn]->setParent($this);
+            unset($this->columns[$fromColumn]);
+
+            ksort($this->columns);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Test if cell value is in the defined set of values.
+     *
+     * @param mixed $cellValue
+     * @param mixed[] $dataSet
+     *
+     * @return bool
+     */
+    protected static function filterTestInSimpleDataSet($cellValue, $dataSet)
+    {
+        $dataSetValues = $dataSet['filterValues'];
+        $blanks = $dataSet['blanks'];
+        if (($cellValue == '') || ($cellValue === null)) {
+            return $blanks;
+        }
+
+        return in_array($cellValue, $dataSetValues);
+    }
+
+    /**
+     * Test if cell value is in the defined set of Excel date values.
+     *
+     * @param mixed $cellValue
+     * @param mixed[] $dataSet
+     *
+     * @return bool
+     */
+    protected static function filterTestInDateGroupSet($cellValue, $dataSet)
+    {
+        $dateSet = $dataSet['filterValues'];
+        $blanks = $dataSet['blanks'];
+        if (($cellValue == '') || ($cellValue === null)) {
+            return $blanks;
+        }
+        $timeZone = new DateTimeZone('UTC');
+
+        if (is_numeric($cellValue)) {
+            $dateTime = Date::excelToDateTimeObject((float) $cellValue, $timeZone);
+            $cellValue = (float) $cellValue;
+            if ($cellValue < 1) {
+                //    Just the time part
+                $dtVal = $dateTime->format('His');
+                $dateSet = $dateSet['time'];
+            } elseif ($cellValue == floor($cellValue)) {
+                //    Just the date part
+                $dtVal = $dateTime->format('Ymd');
+                $dateSet = $dateSet['date'];
+            } else {
+                //    date and time parts
+                $dtVal = $dateTime->format('YmdHis');
+                $dateSet = $dateSet['dateTime'];
+            }
+            foreach ($dateSet as $dateValue) {
+                //    Use of substr to extract value at the appropriate group level
+                if (substr($dtVal, 0, strlen($dateValue)) == $dateValue) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Test if cell value is within a set of values defined by a ruleset.
+     *
+     * @param mixed $cellValue
+     * @param mixed[] $ruleSet
+     *
+     * @return bool
+     */
+    protected static function filterTestInCustomDataSet($cellValue, $ruleSet)
+    {
+        /** @var array[] */
+        $dataSet = $ruleSet['filterRules'];
+        $join = $ruleSet['join'];
+        $customRuleForBlanks = $ruleSet['customRuleForBlanks'] ?? false;
+
+        if (!$customRuleForBlanks) {
+            //    Blank cells are always ignored, so return a FALSE
+            if (($cellValue == '') || ($cellValue === null)) {
+                return false;
+            }
+        }
+        $returnVal = ($join == AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND);
+        foreach ($dataSet as $rule) {
+            /** @var string */
+            $ruleValue = $rule['value'];
+            /** @var string */
+            $ruleOperator = $rule['operator'];
+            /** @var string */
+            $cellValueString = $cellValue ?? '';
+            $retVal = false;
+
+            if (is_numeric($ruleValue)) {
+                //    Numeric values are tested using the appropriate operator
+                $numericTest = is_numeric($cellValue);
+                switch ($ruleOperator) {
+                    case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
+                        $retVal = $numericTest && ($cellValue == $ruleValue);
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
+                        $retVal = !$numericTest || ($cellValue != $ruleValue);
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
+                        $retVal = $numericTest && ($cellValue > $ruleValue);
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
+                        $retVal = $numericTest && ($cellValue >= $ruleValue);
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
+                        $retVal = $numericTest && ($cellValue < $ruleValue);
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
+                        $retVal = $numericTest && ($cellValue <= $ruleValue);
+
+                        break;
+                }
+            } elseif ($ruleValue == '') {
+                switch ($ruleOperator) {
+                    case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
+                        $retVal = (($cellValue == '') || ($cellValue === null));
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
+                        $retVal = (($cellValue != '') && ($cellValue !== null));
+
+                        break;
+                    default:
+                        $retVal = true;
+
+                        break;
+                }
+            } else {
+                //    String values are always tested for equality, factoring in for wildcards (hence a regexp test)
+                switch ($ruleOperator) {
+                    case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
+                        $retVal = (bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString);
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
+                        $retVal = !((bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString));
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
+                        $retVal = strcasecmp($cellValueString, $ruleValue) > 0;
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
+                        $retVal = strcasecmp($cellValueString, $ruleValue) >= 0;
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
+                        $retVal = strcasecmp($cellValueString, $ruleValue) < 0;
+
+                        break;
+                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
+                        $retVal = strcasecmp($cellValueString, $ruleValue) <= 0;
+
+                        break;
+                }
+            }
+            //    If there are multiple conditions, then we need to test both using the appropriate join operator
+            switch ($join) {
+                case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR:
+                    $returnVal = $returnVal || $retVal;
+                    //    Break as soon as we have a TRUE match for OR joins,
+                    //        to avoid unnecessary additional code execution
+                    if ($returnVal) {
+                        return $returnVal;
+                    }
+
+                    break;
+                case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND:
+                    $returnVal = $returnVal && $retVal;
+
+                    break;
+            }
+        }
+
+        return $returnVal;
+    }
+
+    /**
+     * Test if cell date value is matches a set of values defined by a set of months.
+     *
+     * @param mixed $cellValue
+     * @param mixed[] $monthSet
+     *
+     * @return bool
+     */
+    protected static function filterTestInPeriodDateSet($cellValue, $monthSet)
+    {
+        //    Blank cells are always ignored, so return a FALSE
+        if (($cellValue == '') || ($cellValue === null)) {
+            return false;
+        }
+
+        if (is_numeric($cellValue)) {
+            $dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC'));
+            $dateValue = (int) $dateObject->format('m');
+            if (in_array($dateValue, $monthSet)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private static function makeDateObject(int $year, int $month, int $day, int $hour = 0, int $minute = 0, int $second = 0): DateTime
+    {
+        $baseDate = new DateTime();
+        $baseDate->setDate($year, $month, $day);
+        $baseDate->setTime($hour, $minute, $second);
+
+        return $baseDate;
+    }
+
+    private const DATE_FUNCTIONS = [
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH => 'dynamicLastMonth',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER => 'dynamicLastQuarter',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK => 'dynamicLastWeek',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR => 'dynamicLastYear',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH => 'dynamicNextMonth',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER => 'dynamicNextQuarter',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK => 'dynamicNextWeek',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR => 'dynamicNextYear',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH => 'dynamicThisMonth',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER => 'dynamicThisQuarter',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK => 'dynamicThisWeek',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR => 'dynamicThisYear',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY => 'dynamicToday',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW => 'dynamicTomorrow',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE => 'dynamicYearToDate',
+        Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY => 'dynamicYesterday',
+    ];
+
+    private static function dynamicLastMonth(): array
+    {
+        $maxval = new DateTime();
+        $year = (int) $maxval->format('Y');
+        $month = (int) $maxval->format('m');
+        $maxval->setDate($year, $month, 1);
+        $maxval->setTime(0, 0, 0);
+        $val = clone $maxval;
+        $val->modify('-1 month');
+
+        return [$val, $maxval];
+    }
+
+    private static function firstDayOfQuarter(): DateTime
+    {
+        $val = new DateTime();
+        $year = (int) $val->format('Y');
+        $month = (int) $val->format('m');
+        $month = 3 * intdiv($month - 1, 3) + 1;
+        $val->setDate($year, $month, 1);
+        $val->setTime(0, 0, 0);
+
+        return $val;
+    }
+
+    private static function dynamicLastQuarter(): array
+    {
+        $maxval = self::firstDayOfQuarter();
+        $val = clone $maxval;
+        $val->modify('-3 months');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicLastWeek(): array
+    {
+        $val = new DateTime();
+        $val->setTime(0, 0, 0);
+        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
+        $subtract = $dayOfWeek + 7; // revert to prior Sunday
+        $val->modify("-$subtract days");
+        $maxval = clone $val;
+        $maxval->modify('+7 days');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicLastYear(): array
+    {
+        $val = new DateTime();
+        $year = (int) $val->format('Y');
+        $val = self::makeDateObject($year - 1, 1, 1);
+        $maxval = self::makeDateObject($year, 1, 1);
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicNextMonth(): array
+    {
+        $val = new DateTime();
+        $year = (int) $val->format('Y');
+        $month = (int) $val->format('m');
+        $val->setDate($year, $month, 1);
+        $val->setTime(0, 0, 0);
+        $val->modify('+1 month');
+        $maxval = clone $val;
+        $maxval->modify('+1 month');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicNextQuarter(): array
+    {
+        $val = self::firstDayOfQuarter();
+        $val->modify('+3 months');
+        $maxval = clone $val;
+        $maxval->modify('+3 months');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicNextWeek(): array
+    {
+        $val = new DateTime();
+        $val->setTime(0, 0, 0);
+        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
+        $add = 7 - $dayOfWeek; // move to next Sunday
+        $val->modify("+$add days");
+        $maxval = clone $val;
+        $maxval->modify('+7 days');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicNextYear(): array
+    {
+        $val = new DateTime();
+        $year = (int) $val->format('Y');
+        $val = self::makeDateObject($year + 1, 1, 1);
+        $maxval = self::makeDateObject($year + 2, 1, 1);
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicThisMonth(): array
+    {
+        $baseDate = new DateTime();
+        $baseDate->setTime(0, 0, 0);
+        $year = (int) $baseDate->format('Y');
+        $month = (int) $baseDate->format('m');
+        $val = self::makeDateObject($year, $month, 1);
+        $maxval = clone $val;
+        $maxval->modify('+1 month');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicThisQuarter(): array
+    {
+        $val = self::firstDayOfQuarter();
+        $maxval = clone $val;
+        $maxval->modify('+3 months');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicThisWeek(): array
+    {
+        $val = new DateTime();
+        $val->setTime(0, 0, 0);
+        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
+        $subtract = $dayOfWeek; // revert to Sunday
+        $val->modify("-$subtract days");
+        $maxval = clone $val;
+        $maxval->modify('+7 days');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicThisYear(): array
+    {
+        $val = new DateTime();
+        $year = (int) $val->format('Y');
+        $val = self::makeDateObject($year, 1, 1);
+        $maxval = self::makeDateObject($year + 1, 1, 1);
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicToday(): array
+    {
+        $val = new DateTime();
+        $val->setTime(0, 0, 0);
+        $maxval = clone $val;
+        $maxval->modify('+1 day');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicTomorrow(): array
+    {
+        $val = new DateTime();
+        $val->setTime(0, 0, 0);
+        $val->modify('+1 day');
+        $maxval = clone $val;
+        $maxval->modify('+1 day');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicYearToDate(): array
+    {
+        $maxval = new DateTime();
+        $maxval->setTime(0, 0, 0);
+        $val = self::makeDateObject((int) $maxval->format('Y'), 1, 1);
+        $maxval->modify('+1 day');
+
+        return [$val, $maxval];
+    }
+
+    private static function dynamicYesterday(): array
+    {
+        $maxval = new DateTime();
+        $maxval->setTime(0, 0, 0);
+        $val = clone $maxval;
+        $val->modify('-1 day');
+
+        return [$val, $maxval];
+    }
+
+    /**
+     * Convert a dynamic rule daterange to a custom filter range expression for ease of calculation.
+     *
+     * @param string $dynamicRuleType
+     *
+     * @return mixed[]
+     */
+    private function dynamicFilterDateRange($dynamicRuleType, AutoFilter\Column &$filterColumn)
+    {
+        $ruleValues = [];
+        $callBack = [__CLASS__, self::DATE_FUNCTIONS[$dynamicRuleType]]; // What if not found?
+        //    Calculate start/end dates for the required date range based on current date
+        //    Val is lowest permitted value.
+        //    Maxval is greater than highest permitted value
+        $val = $maxval = 0;
+        if (is_callable($callBack)) {
+            [$val, $maxval] = $callBack();
+        }
+        $val = Date::dateTimeToExcel($val);
+        $maxval = Date::dateTimeToExcel($maxval);
+
+        //    Set the filter column rule attributes ready for writing
+        $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxval]);
+
+        //    Set the rules for identifying rows for hide/show
+        $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val];
+        $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxval];
+
+        return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]];
+    }
+
+    /**
+     * Apply the AutoFilter rules to the AutoFilter Range.
+     *
+     * @param string $columnID
+     * @param int $startRow
+     * @param int $endRow
+     * @param ?string $ruleType
+     * @param mixed $ruleValue
+     *
+     * @return mixed
+     */
+    private function calculateTopTenValue($columnID, $startRow, $endRow, $ruleType, $ruleValue)
+    {
+        $range = $columnID . $startRow . ':' . $columnID . $endRow;
+        $retVal = null;
+        if ($this->workSheet !== null) {
+            $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false));
+            $dataValues = array_filter($dataValues);
+
+            if ($ruleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) {
+                rsort($dataValues);
+            } else {
+                sort($dataValues);
+            }
+
+            $slice = array_slice($dataValues, 0, $ruleValue);
+
+            $retVal = array_pop($slice);
+        }
+
+        return $retVal;
+    }
+
+    /**
+     * Apply the AutoFilter rules to the AutoFilter Range.
+     *
+     * @return $this
+     */
+    public function showHideRows()
+    {
+        if ($this->workSheet === null) {
+            return $this;
+        }
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+
+        //    The heading row should always be visible
+        $this->workSheet->getRowDimension($rangeStart[1])->setVisible(true);
+
+        $columnFilterTests = [];
+        foreach ($this->columns as $columnID => $filterColumn) {
+            $rules = $filterColumn->getRules();
+            switch ($filterColumn->getFilterType()) {
+                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER:
+                    $ruleType = null;
+                    $ruleValues = [];
+                    //    Build a list of the filter value selections
+                    foreach ($rules as $rule) {
+                        $ruleType = $rule->getRuleType();
+                        $ruleValues[] = $rule->getValue();
+                    }
+                    //    Test if we want to include blanks in our filter criteria
+                    $blanks = false;
+                    $ruleDataSet = array_filter($ruleValues);
+                    if (count($ruleValues) != count($ruleDataSet)) {
+                        $blanks = true;
+                    }
+                    if ($ruleType == Rule::AUTOFILTER_RULETYPE_FILTER) {
+                        //    Filter on absolute values
+                        $columnFilterTests[$columnID] = [
+                            'method' => 'filterTestInSimpleDataSet',
+                            'arguments' => ['filterValues' => $ruleDataSet, 'blanks' => $blanks],
+                        ];
+                    } else {
+                        //    Filter on date group values
+                        $arguments = [
+                            'date' => [],
+                            'time' => [],
+                            'dateTime' => [],
+                        ];
+                        foreach ($ruleDataSet as $ruleValue) {
+                            if (!is_array($ruleValue)) {
+                                continue;
+                            }
+                            $date = $time = '';
+                            if (
+                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) &&
+                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '')
+                            ) {
+                                $date .= sprintf('%04d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]);
+                            }
+                            if (
+                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) &&
+                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '')
+                            ) {
+                                $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]);
+                            }
+                            if (
+                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) &&
+                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '')
+                            ) {
+                                $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]);
+                            }
+                            if (
+                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) &&
+                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '')
+                            ) {
+                                $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]);
+                            }
+                            if (
+                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) &&
+                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '')
+                            ) {
+                                $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]);
+                            }
+                            if (
+                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) &&
+                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '')
+                            ) {
+                                $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]);
+                            }
+                            $dateTime = $date . $time;
+                            $arguments['date'][] = $date;
+                            $arguments['time'][] = $time;
+                            $arguments['dateTime'][] = $dateTime;
+                        }
+                        //    Remove empty elements
+                        $arguments['date'] = array_filter($arguments['date']);
+                        $arguments['time'] = array_filter($arguments['time']);
+                        $arguments['dateTime'] = array_filter($arguments['dateTime']);
+                        $columnFilterTests[$columnID] = [
+                            'method' => 'filterTestInDateGroupSet',
+                            'arguments' => ['filterValues' => $arguments, 'blanks' => $blanks],
+                        ];
+                    }
+
+                    break;
+                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER:
+                    $customRuleForBlanks = true;
+                    $ruleValues = [];
+                    //    Build a list of the filter value selections
+                    foreach ($rules as $rule) {
+                        $ruleValue = $rule->getValue();
+                        if (!is_array($ruleValue) && !is_numeric($ruleValue)) {
+                            //    Convert to a regexp allowing for regexp reserved characters, wildcards and escaped wildcards
+                            $ruleValue = WildcardMatch::wildcard($ruleValue);
+                            if (trim($ruleValue) == '') {
+                                $customRuleForBlanks = true;
+                                $ruleValue = trim($ruleValue);
+                            }
+                        }
+                        $ruleValues[] = ['operator' => $rule->getOperator(), 'value' => $ruleValue];
+                    }
+                    $join = $filterColumn->getJoin();
+                    $columnFilterTests[$columnID] = [
+                        'method' => 'filterTestInCustomDataSet',
+                        'arguments' => ['filterRules' => $ruleValues, 'join' => $join, 'customRuleForBlanks' => $customRuleForBlanks],
+                    ];
+
+                    break;
+                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER:
+                    $ruleValues = [];
+                    foreach ($rules as $rule) {
+                        //    We should only ever have one Dynamic Filter Rule anyway
+                        $dynamicRuleType = $rule->getGrouping();
+                        if (
+                            ($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) ||
+                            ($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE)
+                        ) {
+                            //    Number (Average) based
+                            //    Calculate the average
+                            $averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')';
+                            $spreadsheet = ($this->workSheet === null) ? null : $this->workSheet->getParent();
+                            $average = Calculation::getInstance($spreadsheet)->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1'));
+                            while (is_array($average)) {
+                                $average = array_pop($average);
+                            }
+                            //    Set above/below rule based on greaterThan or LessTan
+                            $operator = ($dynamicRuleType === Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)
+                                ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN
+                                : Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN;
+                            $ruleValues[] = [
+                                'operator' => $operator,
+                                'value' => $average,
+                            ];
+                            $columnFilterTests[$columnID] = [
+                                'method' => 'filterTestInCustomDataSet',
+                                'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
+                            ];
+                        } else {
+                            //    Date based
+                            if ($dynamicRuleType[0] == 'M' || $dynamicRuleType[0] == 'Q') {
+                                $periodType = '';
+                                $period = 0;
+                                //    Month or Quarter
+                                sscanf($dynamicRuleType, '%[A-Z]%d', $periodType, $period);
+                                if ($periodType == 'M') {
+                                    $ruleValues = [$period];
+                                } else {
+                                    --$period;
+                                    $periodEnd = (1 + $period) * 3;
+                                    $periodStart = 1 + $period * 3;
+                                    $ruleValues = range($periodStart, $periodEnd);
+                                }
+                                $columnFilterTests[$columnID] = [
+                                    'method' => 'filterTestInPeriodDateSet',
+                                    'arguments' => $ruleValues,
+                                ];
+                                $filterColumn->setAttributes([]);
+                            } else {
+                                //    Date Range
+                                $columnFilterTests[$columnID] = $this->dynamicFilterDateRange($dynamicRuleType, $filterColumn);
+
+                                break;
+                            }
+                        }
+                    }
+
+                    break;
+                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER:
+                    $ruleValues = [];
+                    $dataRowCount = $rangeEnd[1] - $rangeStart[1];
+                    $toptenRuleType = null;
+                    $ruleValue = 0;
+                    $ruleOperator = null;
+                    foreach ($rules as $rule) {
+                        //    We should only ever have one Dynamic Filter Rule anyway
+                        $toptenRuleType = $rule->getGrouping();
+                        $ruleValue = $rule->getValue();
+                        $ruleOperator = $rule->getOperator();
+                    }
+                    if (is_numeric($ruleValue) && $ruleOperator === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) {
+                        $ruleValue = floor((float) $ruleValue * ($dataRowCount / 100));
+                    }
+                    if (!is_array($ruleValue) && $ruleValue < 1) {
+                        $ruleValue = 1;
+                    }
+                    if (!is_array($ruleValue) && $ruleValue > 500) {
+                        $ruleValue = 500;
+                    }
+
+                    $maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, (int) $rangeEnd[1], $toptenRuleType, $ruleValue);
+
+                    $operator = ($toptenRuleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP)
+                        ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL
+                        : Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL;
+                    $ruleValues[] = ['operator' => $operator, 'value' => $maxVal];
+                    $columnFilterTests[$columnID] = [
+                        'method' => 'filterTestInCustomDataSet',
+                        'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
+                    ];
+                    $filterColumn->setAttributes(['maxVal' => $maxVal]);
+
+                    break;
+            }
+        }
+
+        $rangeEnd[1] = $this->autoExtendRange($rangeStart[1], $rangeEnd[1]);
+
+        //    Execute the column tests for each row in the autoFilter range to determine show/hide,
+        for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) {
+            $result = true;
+            foreach ($columnFilterTests as $columnID => $columnFilterTest) {
+                $cellValue = $this->workSheet->getCell($columnID . $row)->getCalculatedValue();
+                //    Execute the filter test
+                $result = // $result && // phpstan says $result is always true here
+                    // @phpstan-ignore-next-line
+                    call_user_func_array([self::class, $columnFilterTest['method']], [$cellValue, $columnFilterTest['arguments']]);
+                //    If filter test has resulted in FALSE, exit the loop straightaway rather than running any more tests
+                if (!$result) {
+                    break;
+                }
+            }
+            //    Set show/hide for the row based on the result of the autoFilter result
+            $this->workSheet->getRowDimension((int) $row)->setVisible($result);
+        }
+        $this->evaluated = true;
+
+        return $this;
+    }
+
+    /**
+     * Magic Range Auto-sizing.
+     * For a single row rangeSet, we follow MS Excel rules, and search for the first empty row to determine our range.
+     */
+    public function autoExtendRange(int $startRow, int $endRow): int
+    {
+        if ($startRow === $endRow && $this->workSheet !== null) {
+            try {
+                $rowIterator = $this->workSheet->getRowIterator($startRow + 1);
+            } catch (Exception $e) {
+                // If there are no rows below $startRow
+                return $startRow;
+            }
+            foreach ($rowIterator as $row) {
+                if ($row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL) === true) {
+                    return $row->getRowIndex() - 1;
+                }
+            }
+        }
+
+        return $endRow;
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $value) {
+            if (is_object($value)) {
+                if ($key === 'workSheet') {
+                    //    Detach from worksheet
+                    $this->{$key} = null;
+                } else {
+                    $this->{$key} = clone $value;
+                }
+            } elseif ((is_array($value)) && ($key == 'columns')) {
+                //    The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects
+                $this->{$key} = [];
+                foreach ($value as $k => $v) {
+                    $this->{$key}[$k] = clone $v;
+                    // attach the new cloned Column to this new cloned Autofilter object
+                    $this->{$key}[$k]->setParent($this);
+                }
+            } else {
+                $this->{$key} = $value;
+            }
+        }
+    }
+
+    /**
+     * toString method replicates previous behavior by returning the range if object is
+     * referenced as a property of its parent.
+     */
+    public function __toString()
+    {
+        return (string) $this->range;
+    }
+}

+ 404 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php

@@ -0,0 +1,404 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
+
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
+
+class Column
+{
+    const AUTOFILTER_FILTERTYPE_FILTER = 'filters';
+    const AUTOFILTER_FILTERTYPE_CUSTOMFILTER = 'customFilters';
+    //    Supports no more than 2 rules, with an And/Or join criteria
+    //        if more than 1 rule is defined
+    const AUTOFILTER_FILTERTYPE_DYNAMICFILTER = 'dynamicFilter';
+    //    Even though the filter rule is constant, the filtered data can vary
+    //        e.g. filtered by date = TODAY
+    const AUTOFILTER_FILTERTYPE_TOPTENFILTER = 'top10';
+
+    /**
+     * Types of autofilter rules.
+     *
+     * @var string[]
+     */
+    private static $filterTypes = [
+        //    Currently we're not handling
+        //        colorFilter
+        //        extLst
+        //        iconFilter
+        self::AUTOFILTER_FILTERTYPE_FILTER,
+        self::AUTOFILTER_FILTERTYPE_CUSTOMFILTER,
+        self::AUTOFILTER_FILTERTYPE_DYNAMICFILTER,
+        self::AUTOFILTER_FILTERTYPE_TOPTENFILTER,
+    ];
+
+    // Multiple Rule Connections
+    const AUTOFILTER_COLUMN_JOIN_AND = 'and';
+    const AUTOFILTER_COLUMN_JOIN_OR = 'or';
+
+    /**
+     * Join options for autofilter rules.
+     *
+     * @var string[]
+     */
+    private static $ruleJoins = [
+        self::AUTOFILTER_COLUMN_JOIN_AND,
+        self::AUTOFILTER_COLUMN_JOIN_OR,
+    ];
+
+    /**
+     * Autofilter.
+     *
+     * @var null|AutoFilter
+     */
+    private $parent;
+
+    /**
+     * Autofilter Column Index.
+     *
+     * @var string
+     */
+    private $columnIndex = '';
+
+    /**
+     * Autofilter Column Filter Type.
+     *
+     * @var string
+     */
+    private $filterType = self::AUTOFILTER_FILTERTYPE_FILTER;
+
+    /**
+     * Autofilter Multiple Rules And/Or.
+     *
+     * @var string
+     */
+    private $join = self::AUTOFILTER_COLUMN_JOIN_OR;
+
+    /**
+     * Autofilter Column Rules.
+     *
+     * @var Column\Rule[]
+     */
+    private $ruleset = [];
+
+    /**
+     * Autofilter Column Dynamic Attributes.
+     *
+     * @var mixed[]
+     */
+    private $attributes = [];
+
+    /**
+     * Create a new Column.
+     *
+     * @param string $column Column (e.g. A)
+     * @param AutoFilter $parent Autofilter for this column
+     */
+    public function __construct($column, ?AutoFilter $parent = null)
+    {
+        $this->columnIndex = $column;
+        $this->parent = $parent;
+    }
+
+    public function setEvaluatedFalse(): void
+    {
+        if ($this->parent !== null) {
+            $this->parent->setEvaluated(false);
+        }
+    }
+
+    /**
+     * Get AutoFilter column index as string eg: 'A'.
+     *
+     * @return string
+     */
+    public function getColumnIndex()
+    {
+        return $this->columnIndex;
+    }
+
+    /**
+     * Set AutoFilter column index as string eg: 'A'.
+     *
+     * @param string $column Column (e.g. A)
+     *
+     * @return $this
+     */
+    public function setColumnIndex($column)
+    {
+        $this->setEvaluatedFalse();
+        // Uppercase coordinate
+        $column = strtoupper($column);
+        if ($this->parent !== null) {
+            $this->parent->testColumnInRange($column);
+        }
+
+        $this->columnIndex = $column;
+
+        return $this;
+    }
+
+    /**
+     * Get this Column's AutoFilter Parent.
+     *
+     * @return null|AutoFilter
+     */
+    public function getParent()
+    {
+        return $this->parent;
+    }
+
+    /**
+     * Set this Column's AutoFilter Parent.
+     *
+     * @return $this
+     */
+    public function setParent(?AutoFilter $parent = null)
+    {
+        $this->setEvaluatedFalse();
+        $this->parent = $parent;
+
+        return $this;
+    }
+
+    /**
+     * Get AutoFilter Type.
+     *
+     * @return string
+     */
+    public function getFilterType()
+    {
+        return $this->filterType;
+    }
+
+    /**
+     * Set AutoFilter Type.
+     *
+     * @param string $filterType
+     *
+     * @return $this
+     */
+    public function setFilterType($filterType)
+    {
+        $this->setEvaluatedFalse();
+        if (!in_array($filterType, self::$filterTypes)) {
+            throw new PhpSpreadsheetException('Invalid filter type for column AutoFilter.');
+        }
+        if ($filterType === self::AUTOFILTER_FILTERTYPE_CUSTOMFILTER && count($this->ruleset) > 2) {
+            throw new PhpSpreadsheetException('No more than 2 rules are allowed in a Custom Filter');
+        }
+
+        $this->filterType = $filterType;
+
+        return $this;
+    }
+
+    /**
+     * Get AutoFilter Multiple Rules And/Or Join.
+     *
+     * @return string
+     */
+    public function getJoin()
+    {
+        return $this->join;
+    }
+
+    /**
+     * Set AutoFilter Multiple Rules And/Or.
+     *
+     * @param string $join And/Or
+     *
+     * @return $this
+     */
+    public function setJoin($join)
+    {
+        $this->setEvaluatedFalse();
+        // Lowercase And/Or
+        $join = strtolower($join);
+        if (!in_array($join, self::$ruleJoins)) {
+            throw new PhpSpreadsheetException('Invalid rule connection for column AutoFilter.');
+        }
+
+        $this->join = $join;
+
+        return $this;
+    }
+
+    /**
+     * Set AutoFilter Attributes.
+     *
+     * @param mixed[] $attributes
+     *
+     * @return $this
+     */
+    public function setAttributes($attributes)
+    {
+        $this->setEvaluatedFalse();
+        $this->attributes = $attributes;
+
+        return $this;
+    }
+
+    /**
+     * Set An AutoFilter Attribute.
+     *
+     * @param string $name Attribute Name
+     * @param int|string $value Attribute Value
+     *
+     * @return $this
+     */
+    public function setAttribute($name, $value)
+    {
+        $this->setEvaluatedFalse();
+        $this->attributes[$name] = $value;
+
+        return $this;
+    }
+
+    /**
+     * Get AutoFilter Column Attributes.
+     *
+     * @return int[]|string[]
+     */
+    public function getAttributes()
+    {
+        return $this->attributes;
+    }
+
+    /**
+     * Get specific AutoFilter Column Attribute.
+     *
+     * @param string $name Attribute Name
+     *
+     * @return null|int|string
+     */
+    public function getAttribute($name)
+    {
+        if (isset($this->attributes[$name])) {
+            return $this->attributes[$name];
+        }
+
+        return null;
+    }
+
+    public function ruleCount(): int
+    {
+        return count($this->ruleset);
+    }
+
+    /**
+     * Get all AutoFilter Column Rules.
+     *
+     * @return Column\Rule[]
+     */
+    public function getRules()
+    {
+        return $this->ruleset;
+    }
+
+    /**
+     * Get a specified AutoFilter Column Rule.
+     *
+     * @param int $index Rule index in the ruleset array
+     *
+     * @return Column\Rule
+     */
+    public function getRule($index)
+    {
+        if (!isset($this->ruleset[$index])) {
+            $this->ruleset[$index] = new Column\Rule($this);
+        }
+
+        return $this->ruleset[$index];
+    }
+
+    /**
+     * Create a new AutoFilter Column Rule in the ruleset.
+     *
+     * @return Column\Rule
+     */
+    public function createRule()
+    {
+        $this->setEvaluatedFalse();
+        if ($this->filterType === self::AUTOFILTER_FILTERTYPE_CUSTOMFILTER && count($this->ruleset) >= 2) {
+            throw new PhpSpreadsheetException('No more than 2 rules are allowed in a Custom Filter');
+        }
+        $this->ruleset[] = new Column\Rule($this);
+
+        return end($this->ruleset);
+    }
+
+    /**
+     * Add a new AutoFilter Column Rule to the ruleset.
+     *
+     * @return $this
+     */
+    public function addRule(Column\Rule $rule)
+    {
+        $this->setEvaluatedFalse();
+        $rule->setParent($this);
+        $this->ruleset[] = $rule;
+
+        return $this;
+    }
+
+    /**
+     * Delete a specified AutoFilter Column Rule
+     * If the number of rules is reduced to 1, then we reset And/Or logic to Or.
+     *
+     * @param int $index Rule index in the ruleset array
+     *
+     * @return $this
+     */
+    public function deleteRule($index)
+    {
+        $this->setEvaluatedFalse();
+        if (isset($this->ruleset[$index])) {
+            unset($this->ruleset[$index]);
+            //    If we've just deleted down to a single rule, then reset And/Or joining to Or
+            if (count($this->ruleset) <= 1) {
+                $this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Delete all AutoFilter Column Rules.
+     *
+     * @return $this
+     */
+    public function clearRules()
+    {
+        $this->setEvaluatedFalse();
+        $this->ruleset = [];
+        $this->setJoin(self::AUTOFILTER_COLUMN_JOIN_OR);
+
+        return $this;
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $value) {
+            if ($key === 'parent') {
+                // Detach from autofilter parent
+                $this->parent = null;
+            } elseif ($key === 'ruleset') {
+                // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects
+                $this->ruleset = [];
+                foreach ($value as $k => $v) {
+                    $cloned = clone $v;
+                    $cloned->setParent($this); // attach the new cloned Rule to this new cloned Autofilter Cloned object
+                    $this->ruleset[$k] = $cloned;
+                }
+            } else {
+                $this->$key = $value;
+            }
+        }
+    }
+}

+ 426 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php

@@ -0,0 +1,426 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column;
+
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column;
+
+class Rule
+{
+    const AUTOFILTER_RULETYPE_FILTER = 'filter';
+    const AUTOFILTER_RULETYPE_DATEGROUP = 'dateGroupItem';
+    const AUTOFILTER_RULETYPE_CUSTOMFILTER = 'customFilter';
+    const AUTOFILTER_RULETYPE_DYNAMICFILTER = 'dynamicFilter';
+    const AUTOFILTER_RULETYPE_TOPTENFILTER = 'top10Filter';
+
+    private const RULE_TYPES = [
+        //    Currently we're not handling
+        //        colorFilter
+        //        extLst
+        //        iconFilter
+        self::AUTOFILTER_RULETYPE_FILTER,
+        self::AUTOFILTER_RULETYPE_DATEGROUP,
+        self::AUTOFILTER_RULETYPE_CUSTOMFILTER,
+        self::AUTOFILTER_RULETYPE_DYNAMICFILTER,
+        self::AUTOFILTER_RULETYPE_TOPTENFILTER,
+    ];
+
+    const AUTOFILTER_RULETYPE_DATEGROUP_YEAR = 'year';
+    const AUTOFILTER_RULETYPE_DATEGROUP_MONTH = 'month';
+    const AUTOFILTER_RULETYPE_DATEGROUP_DAY = 'day';
+    const AUTOFILTER_RULETYPE_DATEGROUP_HOUR = 'hour';
+    const AUTOFILTER_RULETYPE_DATEGROUP_MINUTE = 'minute';
+    const AUTOFILTER_RULETYPE_DATEGROUP_SECOND = 'second';
+
+    private const DATE_TIME_GROUPS = [
+        self::AUTOFILTER_RULETYPE_DATEGROUP_YEAR,
+        self::AUTOFILTER_RULETYPE_DATEGROUP_MONTH,
+        self::AUTOFILTER_RULETYPE_DATEGROUP_DAY,
+        self::AUTOFILTER_RULETYPE_DATEGROUP_HOUR,
+        self::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE,
+        self::AUTOFILTER_RULETYPE_DATEGROUP_SECOND,
+    ];
+
+    const AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY = 'yesterday';
+    const AUTOFILTER_RULETYPE_DYNAMIC_TODAY = 'today';
+    const AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW = 'tomorrow';
+    const AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE = 'yearToDate';
+    const AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR = 'thisYear';
+    const AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER = 'thisQuarter';
+    const AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH = 'thisMonth';
+    const AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK = 'thisWeek';
+    const AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR = 'lastYear';
+    const AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER = 'lastQuarter';
+    const AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH = 'lastMonth';
+    const AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK = 'lastWeek';
+    const AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR = 'nextYear';
+    const AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER = 'nextQuarter';
+    const AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH = 'nextMonth';
+    const AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK = 'nextWeek';
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_1 = 'M1';
+    const AUTOFILTER_RULETYPE_DYNAMIC_JANUARY = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_1;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2 = 'M2';
+    const AUTOFILTER_RULETYPE_DYNAMIC_FEBRUARY = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_3 = 'M3';
+    const AUTOFILTER_RULETYPE_DYNAMIC_MARCH = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_3;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_4 = 'M4';
+    const AUTOFILTER_RULETYPE_DYNAMIC_APRIL = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_4;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_5 = 'M5';
+    const AUTOFILTER_RULETYPE_DYNAMIC_MAY = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_5;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_6 = 'M6';
+    const AUTOFILTER_RULETYPE_DYNAMIC_JUNE = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_6;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_7 = 'M7';
+    const AUTOFILTER_RULETYPE_DYNAMIC_JULY = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_7;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_8 = 'M8';
+    const AUTOFILTER_RULETYPE_DYNAMIC_AUGUST = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_8;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_9 = 'M9';
+    const AUTOFILTER_RULETYPE_DYNAMIC_SEPTEMBER = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_9;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_10 = 'M10';
+    const AUTOFILTER_RULETYPE_DYNAMIC_OCTOBER = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_10;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_11 = 'M11';
+    const AUTOFILTER_RULETYPE_DYNAMIC_NOVEMBER = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_11;
+    const AUTOFILTER_RULETYPE_DYNAMIC_MONTH_12 = 'M12';
+    const AUTOFILTER_RULETYPE_DYNAMIC_DECEMBER = self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_12;
+    const AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_1 = 'Q1';
+    const AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_2 = 'Q2';
+    const AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_3 = 'Q3';
+    const AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_4 = 'Q4';
+    const AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE = 'aboveAverage';
+    const AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE = 'belowAverage';
+
+    private const DYNAMIC_TYPES = [
+        self::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_TODAY,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_1,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_3,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_4,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_5,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_6,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_7,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_8,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_9,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_10,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_11,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_12,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_1,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_2,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_3,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_4,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE,
+        self::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE,
+    ];
+
+    // Filter rule operators for filter and customFilter types.
+    const AUTOFILTER_COLUMN_RULE_EQUAL = 'equal';
+    const AUTOFILTER_COLUMN_RULE_NOTEQUAL = 'notEqual';
+    const AUTOFILTER_COLUMN_RULE_GREATERTHAN = 'greaterThan';
+    const AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL = 'greaterThanOrEqual';
+    const AUTOFILTER_COLUMN_RULE_LESSTHAN = 'lessThan';
+    const AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL = 'lessThanOrEqual';
+
+    private const OPERATORS = [
+        self::AUTOFILTER_COLUMN_RULE_EQUAL,
+        self::AUTOFILTER_COLUMN_RULE_NOTEQUAL,
+        self::AUTOFILTER_COLUMN_RULE_GREATERTHAN,
+        self::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL,
+        self::AUTOFILTER_COLUMN_RULE_LESSTHAN,
+        self::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL,
+    ];
+
+    const AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE = 'byValue';
+    const AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT = 'byPercent';
+
+    private const TOP_TEN_VALUE = [
+        self::AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE,
+        self::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT,
+    ];
+
+    const AUTOFILTER_COLUMN_RULE_TOPTEN_TOP = 'top';
+    const AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM = 'bottom';
+
+    private const TOP_TEN_TYPE = [
+        self::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP,
+        self::AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM,
+    ];
+
+    //  Unimplented Rule Operators (Numeric, Boolean etc)
+    //    const AUTOFILTER_COLUMN_RULE_BETWEEN = 'between';        //    greaterThanOrEqual 1 && lessThanOrEqual 2
+    // Rule Operators (Numeric Special) which are translated to standard numeric operators with calculated values
+    // Rule Operators (String) which are set as wild-carded values
+    //    const AUTOFILTER_COLUMN_RULE_BEGINSWITH            = 'beginsWith';            // A*
+    //    const AUTOFILTER_COLUMN_RULE_ENDSWITH            = 'endsWith';            // *Z
+    //    const AUTOFILTER_COLUMN_RULE_CONTAINS            = 'contains';            // *B*
+    //    const AUTOFILTER_COLUMN_RULE_DOESNTCONTAIN        = 'notEqual';            //    notEqual *B*
+    // Rule Operators (Date Special) which are translated to standard numeric operators with calculated values
+    //    const AUTOFILTER_COLUMN_RULE_BEFORE                = 'lessThan';
+    //    const AUTOFILTER_COLUMN_RULE_AFTER                = 'greaterThan';
+
+    /**
+     * Autofilter Column.
+     *
+     * @var ?Column
+     */
+    private $parent;
+
+    /**
+     * Autofilter Rule Type.
+     *
+     * @var string
+     */
+    private $ruleType = self::AUTOFILTER_RULETYPE_FILTER;
+
+    /**
+     * Autofilter Rule Value.
+     *
+     * @var int|int[]|string|string[]
+     */
+    private $value = '';
+
+    /**
+     * Autofilter Rule Operator.
+     *
+     * @var string
+     */
+    private $operator = self::AUTOFILTER_COLUMN_RULE_EQUAL;
+
+    /**
+     * DateTimeGrouping Group Value.
+     *
+     * @var string
+     */
+    private $grouping = '';
+
+    /**
+     * Create a new Rule.
+     */
+    public function __construct(?Column $parent = null)
+    {
+        $this->parent = $parent;
+    }
+
+    private function setEvaluatedFalse(): void
+    {
+        if ($this->parent !== null) {
+            $this->parent->setEvaluatedFalse();
+        }
+    }
+
+    /**
+     * Get AutoFilter Rule Type.
+     *
+     * @return string
+     */
+    public function getRuleType()
+    {
+        return $this->ruleType;
+    }
+
+    /**
+     * Set AutoFilter Rule Type.
+     *
+     * @param string $ruleType see self::AUTOFILTER_RULETYPE_*
+     *
+     * @return $this
+     */
+    public function setRuleType($ruleType)
+    {
+        $this->setEvaluatedFalse();
+        if (!in_array($ruleType, self::RULE_TYPES)) {
+            throw new PhpSpreadsheetException('Invalid rule type for column AutoFilter Rule.');
+        }
+
+        $this->ruleType = $ruleType;
+
+        return $this;
+    }
+
+    /**
+     * Get AutoFilter Rule Value.
+     *
+     * @return int|int[]|string|string[]
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * Set AutoFilter Rule Value.
+     *
+     * @param int|int[]|string|string[] $value
+     *
+     * @return $this
+     */
+    public function setValue($value)
+    {
+        $this->setEvaluatedFalse();
+        if (is_array($value)) {
+            $grouping = -1;
+            foreach ($value as $key => $v) {
+                //    Validate array entries
+                if (!in_array($key, self::DATE_TIME_GROUPS)) {
+                    //    Remove any invalid entries from the value array
+                    unset($value[$key]);
+                } else {
+                    //    Work out what the dateTime grouping will be
+                    $grouping = max($grouping, array_search($key, self::DATE_TIME_GROUPS));
+                }
+            }
+            if (count($value) == 0) {
+                throw new PhpSpreadsheetException('Invalid rule value for column AutoFilter Rule.');
+            }
+            //    Set the dateTime grouping that we've anticipated
+            $this->setGrouping(self::DATE_TIME_GROUPS[$grouping]);
+        }
+        $this->value = $value;
+
+        return $this;
+    }
+
+    /**
+     * Get AutoFilter Rule Operator.
+     *
+     * @return string
+     */
+    public function getOperator()
+    {
+        return $this->operator;
+    }
+
+    /**
+     * Set AutoFilter Rule Operator.
+     *
+     * @param string $operator see self::AUTOFILTER_COLUMN_RULE_*
+     *
+     * @return $this
+     */
+    public function setOperator($operator)
+    {
+        $this->setEvaluatedFalse();
+        if (empty($operator)) {
+            $operator = self::AUTOFILTER_COLUMN_RULE_EQUAL;
+        }
+        if (
+            (!in_array($operator, self::OPERATORS)) &&
+            (!in_array($operator, self::TOP_TEN_VALUE))
+        ) {
+            throw new PhpSpreadsheetException('Invalid operator for column AutoFilter Rule.');
+        }
+        $this->operator = $operator;
+
+        return $this;
+    }
+
+    /**
+     * Get AutoFilter Rule Grouping.
+     *
+     * @return string
+     */
+    public function getGrouping()
+    {
+        return $this->grouping;
+    }
+
+    /**
+     * Set AutoFilter Rule Grouping.
+     *
+     * @param string $grouping
+     *
+     * @return $this
+     */
+    public function setGrouping($grouping)
+    {
+        $this->setEvaluatedFalse();
+        if (
+            ($grouping !== null) &&
+            (!in_array($grouping, self::DATE_TIME_GROUPS)) &&
+            (!in_array($grouping, self::DYNAMIC_TYPES)) &&
+            (!in_array($grouping, self::TOP_TEN_TYPE))
+        ) {
+            throw new PhpSpreadsheetException('Invalid grouping for column AutoFilter Rule.');
+        }
+        $this->grouping = $grouping;
+
+        return $this;
+    }
+
+    /**
+     * Set AutoFilter Rule.
+     *
+     * @param string $operator see self::AUTOFILTER_COLUMN_RULE_*
+     * @param int|int[]|string|string[] $value
+     * @param string $grouping
+     *
+     * @return $this
+     */
+    public function setRule($operator, $value, $grouping = null)
+    {
+        $this->setEvaluatedFalse();
+        $this->setOperator($operator);
+        $this->setValue($value);
+        //  Only set grouping if it's been passed in as a user-supplied argument,
+        //      otherwise we're calculating it when we setValue() and don't want to overwrite that
+        //      If the user supplies an argumnet for grouping, then on their own head be it
+        if ($grouping !== null) {
+            $this->setGrouping($grouping);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get this Rule's AutoFilter Column Parent.
+     *
+     * @return ?Column
+     */
+    public function getParent()
+    {
+        return $this->parent;
+    }
+
+    /**
+     * Set this Rule's AutoFilter Column Parent.
+     *
+     * @return $this
+     */
+    public function setParent(?Column $parent = null)
+    {
+        $this->setEvaluatedFalse();
+        $this->parent = $parent;
+
+        return $this;
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $value) {
+            if (is_object($value)) {
+                if ($key == 'parent') { // this is only object
+                    //    Detach from autofilter column parent
+                    $this->$key = null;
+                }
+            } else {
+                $this->$key = $value;
+            }
+        }
+    }
+}

+ 51 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFit.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
+use PhpOffice\PhpSpreadsheet\Cell\CellRange;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+
+class AutoFit
+{
+    protected Worksheet $worksheet;
+
+    public function __construct(Worksheet $worksheet)
+    {
+        $this->worksheet = $worksheet;
+    }
+
+    public function getAutoFilterIndentRanges(): array
+    {
+        $autoFilterIndentRanges = [];
+        $autoFilterIndentRanges[] = $this->getAutoFilterIndentRange($this->worksheet->getAutoFilter());
+
+        foreach ($this->worksheet->getTableCollection() as $table) {
+            /** @var Table $table */
+            if ($table->getShowHeaderRow() === true && $table->getAllowFilter() === true) {
+                $autoFilter = $table->getAutoFilter();
+                if ($autoFilter !== null) {
+                    $autoFilterIndentRanges[] = $this->getAutoFilterIndentRange($autoFilter);
+                }
+            }
+        }
+
+        return array_filter($autoFilterIndentRanges);
+    }
+
+    private function getAutoFilterIndentRange(AutoFilter $autoFilter): ?string
+    {
+        $autoFilterRange = $autoFilter->getRange();
+        $autoFilterIndentRange = null;
+
+        if (!empty($autoFilterRange)) {
+            $autoFilterRangeBoundaries = Coordinate::rangeBoundaries($autoFilterRange);
+            $autoFilterIndentRange = (string) new CellRange(
+                CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[0][0], $autoFilterRangeBoundaries[0][1]),
+                CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[1][0], $autoFilterRangeBoundaries[0][1])
+            );
+        }
+
+        return $autoFilterIndentRange;
+    }
+}

+ 543 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php

@@ -0,0 +1,543 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+use PhpOffice\PhpSpreadsheet\IComparable;
+
+class BaseDrawing implements IComparable
+{
+    const EDIT_AS_ABSOLUTE = 'absolute';
+    const EDIT_AS_ONECELL = 'oneCell';
+    const EDIT_AS_TWOCELL = 'twoCell';
+    private const VALID_EDIT_AS = [
+        self::EDIT_AS_ABSOLUTE,
+        self::EDIT_AS_ONECELL,
+        self::EDIT_AS_TWOCELL,
+    ];
+
+    /**
+     * The editAs attribute, used only with two cell anchor.
+     *
+     * @var string
+     */
+    protected $editAs = '';
+
+    /**
+     * Image counter.
+     *
+     * @var int
+     */
+    private static $imageCounter = 0;
+
+    /**
+     * Image index.
+     *
+     * @var int
+     */
+    private $imageIndex = 0;
+
+    /**
+     * Name.
+     *
+     * @var string
+     */
+    protected $name = '';
+
+    /**
+     * Description.
+     *
+     * @var string
+     */
+    protected $description = '';
+
+    /**
+     * Worksheet.
+     *
+     * @var null|Worksheet
+     */
+    protected $worksheet;
+
+    /**
+     * Coordinates.
+     *
+     * @var string
+     */
+    protected $coordinates = 'A1';
+
+    /**
+     * Offset X.
+     *
+     * @var int
+     */
+    protected $offsetX = 0;
+
+    /**
+     * Offset Y.
+     *
+     * @var int
+     */
+    protected $offsetY = 0;
+
+    /**
+     * Coordinates2.
+     *
+     * @var string
+     */
+    protected $coordinates2 = '';
+
+    /**
+     * Offset X2.
+     *
+     * @var int
+     */
+    protected $offsetX2 = 0;
+
+    /**
+     * Offset Y2.
+     *
+     * @var int
+     */
+    protected $offsetY2 = 0;
+
+    /**
+     * Width.
+     *
+     * @var int
+     */
+    protected $width = 0;
+
+    /**
+     * Height.
+     *
+     * @var int
+     */
+    protected $height = 0;
+
+    /**
+     * Pixel width of image. See $width for the size the Drawing will be in the sheet.
+     *
+     * @var int
+     */
+    protected $imageWidth = 0;
+
+    /**
+     * Pixel width of image. See $height for the size the Drawing will be in the sheet.
+     *
+     * @var int
+     */
+    protected $imageHeight = 0;
+
+    /**
+     * Proportional resize.
+     *
+     * @var bool
+     */
+    protected $resizeProportional = true;
+
+    /**
+     * Rotation.
+     *
+     * @var int
+     */
+    protected $rotation = 0;
+
+    /**
+     * Shadow.
+     *
+     * @var Drawing\Shadow
+     */
+    protected $shadow;
+
+    /**
+     * Image hyperlink.
+     *
+     * @var null|Hyperlink
+     */
+    private $hyperlink;
+
+    /**
+     * Image type.
+     *
+     * @var int
+     */
+    protected $type = IMAGETYPE_UNKNOWN;
+
+    /**
+     * Create a new BaseDrawing.
+     */
+    public function __construct()
+    {
+        // Initialise values
+        $this->setShadow();
+
+        // Set image index
+        ++self::$imageCounter;
+        $this->imageIndex = self::$imageCounter;
+    }
+
+    public function getImageIndex(): int
+    {
+        return $this->imageIndex;
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function setName(string $name): self
+    {
+        $this->name = $name;
+
+        return $this;
+    }
+
+    public function getDescription(): string
+    {
+        return $this->description;
+    }
+
+    public function setDescription(string $description): self
+    {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    public function getWorksheet(): ?Worksheet
+    {
+        return $this->worksheet;
+    }
+
+    /**
+     * Set Worksheet.
+     *
+     * @param bool $overrideOld If a Worksheet has already been assigned, overwrite it and remove image from old Worksheet?
+     */
+    public function setWorksheet(?Worksheet $worksheet = null, bool $overrideOld = false): self
+    {
+        if ($this->worksheet === null) {
+            // Add drawing to Worksheet
+            if ($worksheet !== null) {
+                $this->worksheet = $worksheet;
+                if (!($this instanceof Drawing && $this->getPath() === '')) {
+                    $this->worksheet->getCell($this->coordinates);
+                }
+                $this->worksheet->getDrawingCollection()
+                    ->append($this);
+            }
+        } else {
+            if ($overrideOld) {
+                // Remove drawing from old Worksheet
+                $iterator = $this->worksheet->getDrawingCollection()->getIterator();
+
+                while ($iterator->valid()) {
+                    if ($iterator->current()->getHashCode() === $this->getHashCode()) {
+                        $this->worksheet->getDrawingCollection()->offsetUnset(/** @scrutinizer ignore-type */ $iterator->key());
+                        $this->worksheet = null;
+
+                        break;
+                    }
+                }
+
+                // Set new Worksheet
+                $this->setWorksheet($worksheet);
+            } else {
+                throw new PhpSpreadsheetException('A Worksheet has already been assigned. Drawings can only exist on one Worksheet.');
+            }
+        }
+
+        return $this;
+    }
+
+    public function getCoordinates(): string
+    {
+        return $this->coordinates;
+    }
+
+    public function setCoordinates(string $coordinates): self
+    {
+        $this->coordinates = $coordinates;
+        if ($this->worksheet !== null) {
+            if (!($this instanceof Drawing && $this->getPath() === '')) {
+                $this->worksheet->getCell($this->coordinates);
+            }
+        }
+
+        return $this;
+    }
+
+    public function getOffsetX(): int
+    {
+        return $this->offsetX;
+    }
+
+    public function setOffsetX(int $offsetX): self
+    {
+        $this->offsetX = $offsetX;
+
+        return $this;
+    }
+
+    public function getOffsetY(): int
+    {
+        return $this->offsetY;
+    }
+
+    public function setOffsetY(int $offsetY): self
+    {
+        $this->offsetY = $offsetY;
+
+        return $this;
+    }
+
+    public function getCoordinates2(): string
+    {
+        return $this->coordinates2;
+    }
+
+    public function setCoordinates2(string $coordinates2): self
+    {
+        $this->coordinates2 = $coordinates2;
+
+        return $this;
+    }
+
+    public function getOffsetX2(): int
+    {
+        return $this->offsetX2;
+    }
+
+    public function setOffsetX2(int $offsetX2): self
+    {
+        $this->offsetX2 = $offsetX2;
+
+        return $this;
+    }
+
+    public function getOffsetY2(): int
+    {
+        return $this->offsetY2;
+    }
+
+    public function setOffsetY2(int $offsetY2): self
+    {
+        $this->offsetY2 = $offsetY2;
+
+        return $this;
+    }
+
+    public function getWidth(): int
+    {
+        return $this->width;
+    }
+
+    public function setWidth(int $width): self
+    {
+        // Resize proportional?
+        if ($this->resizeProportional && $width != 0) {
+            $ratio = $this->height / ($this->width != 0 ? $this->width : 1);
+            $this->height = (int) round($ratio * $width);
+        }
+
+        // Set width
+        $this->width = $width;
+
+        return $this;
+    }
+
+    public function getHeight(): int
+    {
+        return $this->height;
+    }
+
+    public function setHeight(int $height): self
+    {
+        // Resize proportional?
+        if ($this->resizeProportional && $height != 0) {
+            $ratio = $this->width / ($this->height != 0 ? $this->height : 1);
+            $this->width = (int) round($ratio * $height);
+        }
+
+        // Set height
+        $this->height = $height;
+
+        return $this;
+    }
+
+    /**
+     * Set width and height with proportional resize.
+     *
+     * Example:
+     * <code>
+     * $objDrawing->setResizeProportional(true);
+     * $objDrawing->setWidthAndHeight(160,120);
+     * </code>
+     *
+     * @author Vincent@luo MSN:kele_100@hotmail.com
+     */
+    public function setWidthAndHeight(int $width, int $height): self
+    {
+        $xratio = $width / ($this->width != 0 ? $this->width : 1);
+        $yratio = $height / ($this->height != 0 ? $this->height : 1);
+        if ($this->resizeProportional && !($width == 0 || $height == 0)) {
+            if (($xratio * $this->height) < $height) {
+                $this->height = (int) ceil($xratio * $this->height);
+                $this->width = $width;
+            } else {
+                $this->width = (int) ceil($yratio * $this->width);
+                $this->height = $height;
+            }
+        } else {
+            $this->width = $width;
+            $this->height = $height;
+        }
+
+        return $this;
+    }
+
+    public function getResizeProportional(): bool
+    {
+        return $this->resizeProportional;
+    }
+
+    public function setResizeProportional(bool $resizeProportional): self
+    {
+        $this->resizeProportional = $resizeProportional;
+
+        return $this;
+    }
+
+    public function getRotation(): int
+    {
+        return $this->rotation;
+    }
+
+    public function setRotation(int $rotation): self
+    {
+        $this->rotation = $rotation;
+
+        return $this;
+    }
+
+    public function getShadow(): Drawing\Shadow
+    {
+        return $this->shadow;
+    }
+
+    public function setShadow(?Drawing\Shadow $shadow = null): self
+    {
+        $this->shadow = $shadow ?? new Drawing\Shadow();
+
+        return $this;
+    }
+
+    /**
+     * Get hash code.
+     *
+     * @return string Hash code
+     */
+    public function getHashCode()
+    {
+        return md5(
+            $this->name .
+            $this->description .
+            (($this->worksheet === null) ? '' : (string) $this->worksheet->getHashInt()) .
+            $this->coordinates .
+            $this->offsetX .
+            $this->offsetY .
+            $this->coordinates2 .
+            $this->offsetX2 .
+            $this->offsetY2 .
+            $this->width .
+            $this->height .
+            $this->rotation .
+            $this->shadow->getHashCode() .
+            __CLASS__
+        );
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $value) {
+            if ($key == 'worksheet') {
+                $this->worksheet = null;
+            } elseif (is_object($value)) {
+                $this->$key = clone $value;
+            } else {
+                $this->$key = $value;
+            }
+        }
+    }
+
+    public function setHyperlink(?Hyperlink $hyperlink = null): void
+    {
+        $this->hyperlink = $hyperlink;
+    }
+
+    public function getHyperlink(): ?Hyperlink
+    {
+        return $this->hyperlink;
+    }
+
+    /**
+     * Set Fact Sizes and Type of Image.
+     */
+    protected function setSizesAndType(string $path): void
+    {
+        if ($this->imageWidth === 0 && $this->imageHeight === 0 && $this->type === IMAGETYPE_UNKNOWN) {
+            $imageData = getimagesize($path);
+
+            if (!empty($imageData)) {
+                $this->imageWidth = $imageData[0];
+                $this->imageHeight = $imageData[1];
+                $this->type = $imageData[2];
+            }
+        }
+        if ($this->width === 0 && $this->height === 0) {
+            $this->width = $this->imageWidth;
+            $this->height = $this->imageHeight;
+        }
+    }
+
+    /**
+     * Get Image Type.
+     */
+    public function getType(): int
+    {
+        return $this->type;
+    }
+
+    public function getImageWidth(): int
+    {
+        return $this->imageWidth;
+    }
+
+    public function getImageHeight(): int
+    {
+        return $this->imageHeight;
+    }
+
+    public function getEditAs(): string
+    {
+        return $this->editAs;
+    }
+
+    public function setEditAs(string $editAs): self
+    {
+        $this->editAs = $editAs;
+
+        return $this;
+    }
+
+    public function validEditAs(): bool
+    {
+        return in_array($this->editAs, self::VALID_EDIT_AS, true);
+    }
+}

+ 94 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/CellIterator.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use Iterator as NativeIterator;
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Collection\Cells;
+
+/**
+ * @template TKey
+ *
+ * @implements NativeIterator<TKey, Cell>
+ */
+abstract class CellIterator implements NativeIterator
+{
+    public const TREAT_NULL_VALUE_AS_EMPTY_CELL = 1;
+
+    public const TREAT_EMPTY_STRING_AS_EMPTY_CELL = 2;
+
+    public const IF_NOT_EXISTS_RETURN_NULL = false;
+
+    public const IF_NOT_EXISTS_CREATE_NEW = true;
+
+    /**
+     * Worksheet to iterate.
+     *
+     * @var Worksheet
+     */
+    protected $worksheet;
+
+    /**
+     * Cell Collection to iterate.
+     *
+     * @var Cells
+     */
+    protected $cellCollection;
+
+    /**
+     * Iterate only existing cells.
+     *
+     * @var bool
+     */
+    protected $onlyExistingCells = false;
+
+    /**
+     * If iterating all cells, and a cell doesn't exist, identifies whether a new cell should be created,
+     *    or if the iterator should return a null value.
+     *
+     * @var bool
+     */
+    protected $ifNotExists = self::IF_NOT_EXISTS_CREATE_NEW;
+
+    /**
+     * Destructor.
+     */
+    public function __destruct()
+    {
+        // @phpstan-ignore-next-line
+        $this->worksheet = $this->cellCollection = null;
+    }
+
+    public function getIfNotExists(): bool
+    {
+        return $this->ifNotExists;
+    }
+
+    public function setIfNotExists(bool $ifNotExists = self::IF_NOT_EXISTS_CREATE_NEW): void
+    {
+        $this->ifNotExists = $ifNotExists;
+    }
+
+    /**
+     * Get loop only existing cells.
+     */
+    public function getIterateOnlyExistingCells(): bool
+    {
+        return $this->onlyExistingCells;
+    }
+
+    /**
+     * Validate start/end values for 'IterateOnlyExistingCells' mode, and adjust if necessary.
+     */
+    abstract protected function adjustForExistingOnlyRange(): void;
+
+    /**
+     * Set the iterator to loop only existing cells.
+     */
+    public function setIterateOnlyExistingCells(bool $value): void
+    {
+        $this->onlyExistingCells = (bool) $value;
+
+        $this->adjustForExistingOnlyRange();
+    }
+}

+ 121 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php

@@ -0,0 +1,121 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+class Column
+{
+    /**
+     * \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet.
+     *
+     * @var Worksheet
+     */
+    private $worksheet;
+
+    /**
+     * Column index.
+     *
+     * @var string
+     */
+    private $columnIndex;
+
+    /**
+     * Create a new column.
+     *
+     * @param string $columnIndex
+     */
+    public function __construct(Worksheet $worksheet, $columnIndex = 'A')
+    {
+        // Set parent and column index
+        $this->worksheet = $worksheet;
+        $this->columnIndex = $columnIndex;
+    }
+
+    /**
+     * Destructor.
+     */
+    public function __destruct()
+    {
+        // @phpstan-ignore-next-line
+        $this->worksheet = null;
+    }
+
+    /**
+     * Get column index as string eg: 'A'.
+     */
+    public function getColumnIndex(): string
+    {
+        return $this->columnIndex;
+    }
+
+    /**
+     * Get cell iterator.
+     *
+     * @param int $startRow The row number at which to start iterating
+     * @param int $endRow Optionally, the row number at which to stop iterating
+     */
+    public function getCellIterator($startRow = 1, $endRow = null): ColumnCellIterator
+    {
+        return new ColumnCellIterator($this->worksheet, $this->columnIndex, $startRow, $endRow);
+    }
+
+    /**
+     * Get row iterator. Synonym for getCellIterator().
+     *
+     * @param int $startRow The row number at which to start iterating
+     * @param int $endRow Optionally, the row number at which to stop iterating
+     */
+    public function getRowIterator($startRow = 1, $endRow = null): ColumnCellIterator
+    {
+        return $this->getCellIterator($startRow, $endRow);
+    }
+
+    /**
+     * Returns a boolean true if the column contains no cells. By default, this means that no cell records exist in the
+     *         collection for this column. false will be returned otherwise.
+     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
+     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
+     *                  cells, then the column will be considered empty.
+     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
+     *                  string value cells, then the column will be considered empty.
+     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
+     *                  If the only cells in the collection are null value or empty string value cells, then the column
+     *                  will be considered empty.
+     *
+     * @param int $definitionOfEmptyFlags
+     *              Possible Flag Values are:
+     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
+     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
+     * @param int $startRow The row number at which to start checking if cells are empty
+     * @param int $endRow Optionally, the row number at which to stop checking if cells are empty
+     */
+    public function isEmpty(int $definitionOfEmptyFlags = 0, $startRow = 1, $endRow = null): bool
+    {
+        $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL);
+        $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL);
+
+        $cellIterator = $this->getCellIterator($startRow, $endRow);
+        $cellIterator->setIterateOnlyExistingCells(true);
+        foreach ($cellIterator as $cell) {
+            /** @scrutinizer ignore-call */
+            $value = $cell->getValue();
+            if ($value === null && $nullValueCellIsEmpty === true) {
+                continue;
+            }
+            if ($value === '' && $emptyStringCellIsEmpty === true) {
+                continue;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns bound worksheet.
+     */
+    public function getWorksheet(): Worksheet
+    {
+        return $this->worksheet;
+    }
+}

+ 205 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php

@@ -0,0 +1,205 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+
+/**
+ * @extends CellIterator<int>
+ */
+class ColumnCellIterator extends CellIterator
+{
+    /**
+     * Current iterator position.
+     *
+     * @var int
+     */
+    private $currentRow;
+
+    /**
+     * Column index.
+     *
+     * @var int
+     */
+    private $columnIndex;
+
+    /**
+     * Start position.
+     *
+     * @var int
+     */
+    private $startRow = 1;
+
+    /**
+     * End position.
+     *
+     * @var int
+     */
+    private $endRow = 1;
+
+    /**
+     * Create a new row iterator.
+     *
+     * @param Worksheet $worksheet The worksheet to iterate over
+     * @param string $columnIndex The column that we want to iterate
+     * @param int $startRow The row number at which to start iterating
+     * @param int $endRow Optionally, the row number at which to stop iterating
+     */
+    public function __construct(Worksheet $worksheet, $columnIndex = 'A', $startRow = 1, $endRow = null)
+    {
+        // Set subject
+        $this->worksheet = $worksheet;
+        $this->cellCollection = $worksheet->getCellCollection();
+        $this->columnIndex = Coordinate::columnIndexFromString($columnIndex);
+        $this->resetEnd($endRow);
+        $this->resetStart($startRow);
+    }
+
+    /**
+     * (Re)Set the start row and the current row pointer.
+     *
+     * @param int $startRow The row number at which to start iterating
+     *
+     * @return $this
+     */
+    public function resetStart(int $startRow = 1)
+    {
+        $this->startRow = $startRow;
+        $this->adjustForExistingOnlyRange();
+        $this->seek($startRow);
+
+        return $this;
+    }
+
+    /**
+     * (Re)Set the end row.
+     *
+     * @param int $endRow The row number at which to stop iterating
+     *
+     * @return $this
+     */
+    public function resetEnd($endRow = null)
+    {
+        $this->endRow = $endRow ?: $this->worksheet->getHighestRow();
+        $this->adjustForExistingOnlyRange();
+
+        return $this;
+    }
+
+    /**
+     * Set the row pointer to the selected row.
+     *
+     * @param int $row The row number to set the current pointer at
+     *
+     * @return $this
+     */
+    public function seek(int $row = 1)
+    {
+        if (
+            $this->onlyExistingCells &&
+            (!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->columnIndex) . $row))
+        ) {
+            throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist');
+        }
+        if (($row < $this->startRow) || ($row > $this->endRow)) {
+            throw new PhpSpreadsheetException("Row $row is out of range ({$this->startRow} - {$this->endRow})");
+        }
+        $this->currentRow = $row;
+
+        return $this;
+    }
+
+    /**
+     * Rewind the iterator to the starting row.
+     */
+    public function rewind(): void
+    {
+        $this->currentRow = $this->startRow;
+    }
+
+    /**
+     * Return the current cell in this worksheet column.
+     */
+    public function current(): ?Cell
+    {
+        $cellAddress = Coordinate::stringFromColumnIndex($this->columnIndex) . $this->currentRow;
+
+        return $this->cellCollection->has($cellAddress)
+            ? $this->cellCollection->get($cellAddress)
+            : (
+                $this->ifNotExists === self::IF_NOT_EXISTS_CREATE_NEW
+                ? $this->worksheet->createNewCell($cellAddress)
+                : null
+            );
+    }
+
+    /**
+     * Return the current iterator key.
+     */
+    public function key(): int
+    {
+        return $this->currentRow;
+    }
+
+    /**
+     * Set the iterator to its next value.
+     */
+    public function next(): void
+    {
+        $columnAddress = Coordinate::stringFromColumnIndex($this->columnIndex);
+        do {
+            ++$this->currentRow;
+        } while (
+            ($this->onlyExistingCells) &&
+            ($this->currentRow <= $this->endRow) &&
+            (!$this->cellCollection->has($columnAddress . $this->currentRow))
+        );
+    }
+
+    /**
+     * Set the iterator to its previous value.
+     */
+    public function prev(): void
+    {
+        $columnAddress = Coordinate::stringFromColumnIndex($this->columnIndex);
+        do {
+            --$this->currentRow;
+        } while (
+            ($this->onlyExistingCells) &&
+            ($this->currentRow >= $this->startRow) &&
+            (!$this->cellCollection->has($columnAddress . $this->currentRow))
+        );
+    }
+
+    /**
+     * Indicate if more rows exist in the worksheet range of rows that we're iterating.
+     */
+    public function valid(): bool
+    {
+        return $this->currentRow <= $this->endRow && $this->currentRow >= $this->startRow;
+    }
+
+    /**
+     * Validate start/end values for "IterateOnlyExistingCells" mode, and adjust if necessary.
+     */
+    protected function adjustForExistingOnlyRange(): void
+    {
+        if ($this->onlyExistingCells) {
+            $columnAddress = Coordinate::stringFromColumnIndex($this->columnIndex);
+            while (
+                (!$this->cellCollection->has($columnAddress . $this->startRow)) &&
+                ($this->startRow <= $this->endRow)
+            ) {
+                ++$this->startRow;
+            }
+            while (
+                (!$this->cellCollection->has($columnAddress . $this->endRow)) &&
+                ($this->endRow >= $this->startRow)
+            ) {
+                --$this->endRow;
+            }
+        }
+    }
+}

+ 137 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension;
+
+class ColumnDimension extends Dimension
+{
+    /**
+     * Column index.
+     *
+     * @var ?string
+     */
+    private $columnIndex;
+
+    /**
+     * Column width.
+     *
+     * When this is set to a negative value, the column width should be ignored by IWriter
+     *
+     * @var float
+     */
+    private $width = -1;
+
+    /**
+     * Auto size?
+     *
+     * @var bool
+     */
+    private $autoSize = false;
+
+    /**
+     * Create a new ColumnDimension.
+     *
+     * @param ?string $index Character column index
+     */
+    public function __construct($index = 'A')
+    {
+        // Initialise values
+        $this->columnIndex = $index;
+
+        // set dimension as unformatted by default
+        parent::__construct(0);
+    }
+
+    /**
+     * Get column index as string eg: 'A'.
+     */
+    public function getColumnIndex(): ?string
+    {
+        return $this->columnIndex;
+    }
+
+    /**
+     * Set column index as string eg: 'A'.
+     */
+    public function setColumnIndex(string $index): self
+    {
+        $this->columnIndex = $index;
+
+        return $this;
+    }
+
+    /**
+     * Get column index as numeric.
+     */
+    public function getColumnNumeric(): int
+    {
+        return Coordinate::columnIndexFromString($this->columnIndex ?? '');
+    }
+
+    /**
+     * Set column index as numeric.
+     */
+    public function setColumnNumeric(int $index): self
+    {
+        $this->columnIndex = Coordinate::stringFromColumnIndex($index);
+
+        return $this;
+    }
+
+    /**
+     * Get Width.
+     *
+     * Each unit of column width is equal to the width of one character in the default font size. A value of -1
+     *      tells Excel to display this column in its default width.
+     * By default, this will be the return value; but this method also accepts an optional unit of measure argument
+     *    and will convert the returned value to the specified UoM..
+     */
+    public function getWidth(?string $unitOfMeasure = null): float
+    {
+        return ($unitOfMeasure === null || $this->width < 0)
+            ? $this->width
+            : (new CssDimension((string) $this->width))->toUnit($unitOfMeasure);
+    }
+
+    /**
+     * Set Width.
+     *
+     * Each unit of column width is equal to the width of one character in the default font size. A value of -1
+     *      tells Excel to display this column in its default width.
+     * By default, this will be the unit of measure for the passed value; but this method also accepts an
+     *    optional unit of measure argument, and will convert the value from the specified UoM using an
+     *    approximation method.
+     *
+     * @return $this
+     */
+    public function setWidth(float $width, ?string $unitOfMeasure = null)
+    {
+        $this->width = ($unitOfMeasure === null || $width < 0)
+            ? $width
+            : (new CssDimension("{$width}{$unitOfMeasure}"))->width();
+
+        return $this;
+    }
+
+    /**
+     * Get Auto Size.
+     */
+    public function getAutoSize(): bool
+    {
+        return $this->autoSize;
+    }
+
+    /**
+     * Set Auto Size.
+     *
+     * @return $this
+     */
+    public function setAutoSize(bool $autosizeEnabled)
+    {
+        $this->autoSize = $autosizeEnabled;
+
+        return $this;
+    }
+}

+ 174 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnIterator.php

@@ -0,0 +1,174 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use Iterator as NativeIterator;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Exception;
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+
+/**
+ * @implements NativeIterator<string, Column>
+ */
+class ColumnIterator implements NativeIterator
+{
+    /**
+     * Worksheet to iterate.
+     *
+     * @var Worksheet
+     */
+    private $worksheet;
+
+    /**
+     * Current iterator position.
+     *
+     * @var int
+     */
+    private $currentColumnIndex = 1;
+
+    /**
+     * Start position.
+     *
+     * @var int
+     */
+    private $startColumnIndex = 1;
+
+    /**
+     * End position.
+     *
+     * @var int
+     */
+    private $endColumnIndex = 1;
+
+    /**
+     * Create a new column iterator.
+     *
+     * @param Worksheet $worksheet The worksheet to iterate over
+     * @param string $startColumn The column address at which to start iterating
+     * @param string $endColumn Optionally, the column address at which to stop iterating
+     */
+    public function __construct(Worksheet $worksheet, $startColumn = 'A', $endColumn = null)
+    {
+        // Set subject
+        $this->worksheet = $worksheet;
+        $this->resetEnd($endColumn);
+        $this->resetStart($startColumn);
+    }
+
+    /**
+     * Destructor.
+     */
+    public function __destruct()
+    {
+        // @phpstan-ignore-next-line
+        $this->worksheet = null;
+    }
+
+    /**
+     * (Re)Set the start column and the current column pointer.
+     *
+     * @param string $startColumn The column address at which to start iterating
+     *
+     * @return $this
+     */
+    public function resetStart(string $startColumn = 'A')
+    {
+        $startColumnIndex = Coordinate::columnIndexFromString($startColumn);
+        if ($startColumnIndex > Coordinate::columnIndexFromString($this->worksheet->getHighestColumn())) {
+            throw new Exception(
+                "Start column ({$startColumn}) is beyond highest column ({$this->worksheet->getHighestColumn()})"
+            );
+        }
+
+        $this->startColumnIndex = $startColumnIndex;
+        if ($this->endColumnIndex < $this->startColumnIndex) {
+            $this->endColumnIndex = $this->startColumnIndex;
+        }
+        $this->seek($startColumn);
+
+        return $this;
+    }
+
+    /**
+     * (Re)Set the end column.
+     *
+     * @param string $endColumn The column address at which to stop iterating
+     *
+     * @return $this
+     */
+    public function resetEnd($endColumn = null)
+    {
+        $endColumn = $endColumn ?: $this->worksheet->getHighestColumn();
+        $this->endColumnIndex = Coordinate::columnIndexFromString($endColumn);
+
+        return $this;
+    }
+
+    /**
+     * Set the column pointer to the selected column.
+     *
+     * @param string $column The column address to set the current pointer at
+     *
+     * @return $this
+     */
+    public function seek(string $column = 'A')
+    {
+        $column = Coordinate::columnIndexFromString($column);
+        if (($column < $this->startColumnIndex) || ($column > $this->endColumnIndex)) {
+            throw new PhpSpreadsheetException(
+                "Column $column is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})"
+            );
+        }
+        $this->currentColumnIndex = $column;
+
+        return $this;
+    }
+
+    /**
+     * Rewind the iterator to the starting column.
+     */
+    public function rewind(): void
+    {
+        $this->currentColumnIndex = $this->startColumnIndex;
+    }
+
+    /**
+     * Return the current column in this worksheet.
+     */
+    public function current(): Column
+    {
+        return new Column($this->worksheet, Coordinate::stringFromColumnIndex($this->currentColumnIndex));
+    }
+
+    /**
+     * Return the current iterator key.
+     */
+    public function key(): string
+    {
+        return Coordinate::stringFromColumnIndex($this->currentColumnIndex);
+    }
+
+    /**
+     * Set the iterator to its next value.
+     */
+    public function next(): void
+    {
+        ++$this->currentColumnIndex;
+    }
+
+    /**
+     * Set the iterator to its previous value.
+     */
+    public function prev(): void
+    {
+        --$this->currentColumnIndex;
+    }
+
+    /**
+     * Indicate if more columns exist in the worksheet range of columns that we're iterating.
+     */
+    public function valid(): bool
+    {
+        return $this->currentColumnIndex <= $this->endColumnIndex && $this->currentColumnIndex >= $this->startColumnIndex;
+    }
+}

+ 134 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+
+abstract class Dimension
+{
+    /**
+     * Visible?
+     *
+     * @var bool
+     */
+    private $visible = true;
+
+    /**
+     * Outline level.
+     *
+     * @var int
+     */
+    private $outlineLevel = 0;
+
+    /**
+     * Collapsed.
+     *
+     * @var bool
+     */
+    private $collapsed = false;
+
+    /**
+     * Index to cellXf. Null value means row has no explicit cellXf format.
+     *
+     * @var null|int
+     */
+    private $xfIndex;
+
+    /**
+     * Create a new Dimension.
+     *
+     * @param int $initialValue Numeric row index
+     */
+    public function __construct($initialValue = null)
+    {
+        // set dimension as unformatted by default
+        $this->xfIndex = $initialValue;
+    }
+
+    /**
+     * Get Visible.
+     */
+    public function getVisible(): bool
+    {
+        return $this->visible;
+    }
+
+    /**
+     * Set Visible.
+     *
+     * @return $this
+     */
+    public function setVisible(bool $visible)
+    {
+        $this->visible = $visible;
+
+        return $this;
+    }
+
+    /**
+     * Get Outline Level.
+     */
+    public function getOutlineLevel(): int
+    {
+        return $this->outlineLevel;
+    }
+
+    /**
+     * Set Outline Level.
+     * Value must be between 0 and 7.
+     *
+     * @return $this
+     */
+    public function setOutlineLevel(int $level)
+    {
+        if ($level < 0 || $level > 7) {
+            throw new PhpSpreadsheetException('Outline level must range between 0 and 7.');
+        }
+
+        $this->outlineLevel = $level;
+
+        return $this;
+    }
+
+    /**
+     * Get Collapsed.
+     */
+    public function getCollapsed(): bool
+    {
+        return $this->collapsed;
+    }
+
+    /**
+     * Set Collapsed.
+     *
+     * @return $this
+     */
+    public function setCollapsed(bool $collapsed)
+    {
+        $this->collapsed = $collapsed;
+
+        return $this;
+    }
+
+    /**
+     * Get index to cellXf.
+     *
+     * @return int
+     */
+    public function getXfIndex(): ?int
+    {
+        return $this->xfIndex;
+    }
+
+    /**
+     * Set index to cellXf.
+     *
+     * @return $this
+     */
+    public function setXfIndex(int $XfIndex)
+    {
+        $this->xfIndex = $XfIndex;
+
+        return $this;
+    }
+}

+ 272 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php

@@ -0,0 +1,272 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+use ZipArchive;
+
+class Drawing extends BaseDrawing
+{
+    const IMAGE_TYPES_CONVERTION_MAP = [
+        IMAGETYPE_GIF => IMAGETYPE_PNG,
+        IMAGETYPE_JPEG => IMAGETYPE_JPEG,
+        IMAGETYPE_PNG => IMAGETYPE_PNG,
+        IMAGETYPE_BMP => IMAGETYPE_PNG,
+    ];
+
+    /**
+     * Path.
+     *
+     * @var string
+     */
+    private $path;
+
+    /**
+     * Whether or not we are dealing with a URL.
+     *
+     * @var bool
+     */
+    private $isUrl;
+
+    /**
+     * Create a new Drawing.
+     */
+    public function __construct()
+    {
+        // Initialise values
+        $this->path = '';
+        $this->isUrl = false;
+
+        // Initialize parent
+        parent::__construct();
+    }
+
+    /**
+     * Get Filename.
+     *
+     * @return string
+     */
+    public function getFilename()
+    {
+        return basename($this->path);
+    }
+
+    /**
+     * Get indexed filename (using image index).
+     */
+    public function getIndexedFilename(): string
+    {
+        return md5($this->path) . '.' . $this->getExtension();
+    }
+
+    /**
+     * Get Extension.
+     *
+     * @return string
+     */
+    public function getExtension()
+    {
+        $exploded = explode('.', basename($this->path));
+
+        return $exploded[count($exploded) - 1];
+    }
+
+    /**
+     * Get full filepath to store drawing in zip archive.
+     *
+     * @return string
+     */
+    public function getMediaFilename()
+    {
+        if (!array_key_exists($this->type, self::IMAGE_TYPES_CONVERTION_MAP)) {
+            throw new PhpSpreadsheetException('Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.');
+        }
+
+        return sprintf('image%d%s', $this->getImageIndex(), $this->getImageFileExtensionForSave());
+    }
+
+    /**
+     * Get Path.
+     *
+     * @return string
+     */
+    public function getPath()
+    {
+        return $this->path;
+    }
+
+    /**
+     * Set Path.
+     *
+     * @param string $path File path
+     * @param bool $verifyFile Verify file
+     * @param ZipArchive $zip Zip archive instance
+     *
+     * @return $this
+     */
+    public function setPath($path, $verifyFile = true, $zip = null)
+    {
+        $this->isUrl = false;
+        if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) {
+            $this->path = $path;
+
+            return $this;
+        }
+
+        $this->path = '';
+        // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979
+        if (filter_var($path, FILTER_VALIDATE_URL) || (preg_match('/^([\\w\\s\\x00-\\x1f]+):/u', $path) && !preg_match('/^([\\w]+):/u', $path))) {
+            if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) {
+                throw new PhpSpreadsheetException('Invalid protocol for linked drawing');
+            }
+            // Implicit that it is a URL, rather store info than running check above on value in other places.
+            $this->isUrl = true;
+            $ctx = null;
+            // https://github.com/php/php-src/issues/16023
+            // https://github.com/php/php-src/issues/17121
+            if (preg_match('/^https?:/', $path) === 1) {
+                $ctxArray = [
+                    'http' => [
+                        'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
+                        'header' => [
+                            //'Connection: keep-alive', // unacceptable performance
+                            'Accept: image/*;q=0.9,*/*;q=0.8',
+                        ],
+                    ],
+                ];
+                if (preg_match('/^https:/', $path) === 1) {
+                    $ctxArray['ssl'] = ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT];
+                }
+                $ctx = stream_context_create($ctxArray);
+            }
+            $imageContents = @file_get_contents($path, false, $ctx);
+            if ($imageContents !== false) {
+                $filePath = tempnam(sys_get_temp_dir(), 'Drawing');
+                if ($filePath) {
+                    $put = @file_put_contents($filePath, $imageContents);
+                    if ($put !== false) {
+                        if ($this->isImage($filePath)) {
+                            $this->path = $path;
+                            $this->setSizesAndType($filePath);
+                        }
+                        unlink($filePath);
+                    }
+                }
+            }
+        } elseif ($zip instanceof ZipArchive) {
+            $zipPath = explode('#', $path)[1];
+            $locate = @$zip->locateName($zipPath);
+            if ($locate !== false) {
+                if ($this->isImage($path)) {
+                    $this->path = $path;
+                    $this->setSizesAndType($path);
+                }
+            }
+        } else {
+            $exists = @file_exists($path);
+            if ($exists !== false && $this->isImage($path)) {
+                $this->path = $path;
+                $this->setSizesAndType($path);
+            }
+        }
+        if ($this->path === '' && $verifyFile) {
+            throw new PhpSpreadsheetException("File $path not found!");
+        }
+
+        if ($this->worksheet !== null) {
+            if ($this->path !== '') {
+                $this->worksheet->getCell($this->coordinates);
+            }
+        }
+
+        return $this;
+    }
+
+    private function isImage(string $path): bool
+    {
+        $mime = (string) @mime_content_type($path);
+        $retVal = false;
+        if (strpos($mime, 'image/') === 0) {
+            $retVal = true;
+        } elseif ($mime === 'application/octet-stream') {
+            $extension = pathinfo($path, PATHINFO_EXTENSION);
+            $retVal = in_array($extension, ['bin', 'emf'], true);
+        }
+
+        return $retVal;
+    }
+
+    /**
+     * Get isURL.
+     */
+    public function getIsURL(): bool
+    {
+        return $this->isUrl;
+    }
+
+    /**
+     * Set isURL.
+     *
+     * @return $this
+     *
+     * @deprecated 3.7.0 not needed, property is set by setPath
+     */
+    public function setIsURL(bool $isUrl): self
+    {
+        $this->isUrl = $isUrl;
+
+        return $this;
+    }
+
+    /**
+     * Get hash code.
+     *
+     * @return string Hash code
+     */
+    public function getHashCode()
+    {
+        return md5(
+            $this->path .
+            parent::getHashCode() .
+            __CLASS__
+        );
+    }
+
+    /**
+     * Get Image Type for Save.
+     */
+    public function getImageTypeForSave(): int
+    {
+        if (!array_key_exists($this->type, self::IMAGE_TYPES_CONVERTION_MAP)) {
+            throw new PhpSpreadsheetException('Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.');
+        }
+
+        return self::IMAGE_TYPES_CONVERTION_MAP[$this->type];
+    }
+
+    /**
+     * Get Image file extention for Save.
+     */
+    public function getImageFileExtensionForSave(bool $includeDot = true): string
+    {
+        if (!array_key_exists($this->type, self::IMAGE_TYPES_CONVERTION_MAP)) {
+            throw new PhpSpreadsheetException('Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.');
+        }
+
+        $result = image_type_to_extension(self::IMAGE_TYPES_CONVERTION_MAP[$this->type], $includeDot);
+
+        return "$result";
+    }
+
+    /**
+     * Get Image mime type.
+     */
+    public function getImageMimeType(): string
+    {
+        if (!array_key_exists($this->type, self::IMAGE_TYPES_CONVERTION_MAP)) {
+            throw new PhpSpreadsheetException('Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.');
+        }
+
+        return image_type_to_mime_type(self::IMAGE_TYPES_CONVERTION_MAP[$this->type]);
+    }
+}

+ 287 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing/Shadow.php

@@ -0,0 +1,287 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
+
+use PhpOffice\PhpSpreadsheet\IComparable;
+use PhpOffice\PhpSpreadsheet\Style\Color;
+
+class Shadow implements IComparable
+{
+    // Shadow alignment
+    const SHADOW_BOTTOM = 'b';
+    const SHADOW_BOTTOM_LEFT = 'bl';
+    const SHADOW_BOTTOM_RIGHT = 'br';
+    const SHADOW_CENTER = 'ctr';
+    const SHADOW_LEFT = 'l';
+    const SHADOW_TOP = 't';
+    const SHADOW_TOP_LEFT = 'tl';
+    const SHADOW_TOP_RIGHT = 'tr';
+
+    /**
+     * Visible.
+     *
+     * @var bool
+     */
+    private $visible;
+
+    /**
+     * Blur radius.
+     *
+     * Defaults to 6
+     *
+     * @var int
+     */
+    private $blurRadius;
+
+    /**
+     * Shadow distance.
+     *
+     * Defaults to 2
+     *
+     * @var int
+     */
+    private $distance;
+
+    /**
+     * Shadow direction (in degrees).
+     *
+     * @var int
+     */
+    private $direction;
+
+    /**
+     * Shadow alignment.
+     *
+     * @var string
+     */
+    private $alignment;
+
+    /**
+     * Color.
+     *
+     * @var Color
+     */
+    private $color;
+
+    /**
+     * Alpha.
+     *
+     * @var int
+     */
+    private $alpha;
+
+    /**
+     * Create a new Shadow.
+     */
+    public function __construct()
+    {
+        // Initialise values
+        $this->visible = false;
+        $this->blurRadius = 6;
+        $this->distance = 2;
+        $this->direction = 0;
+        $this->alignment = self::SHADOW_BOTTOM_RIGHT;
+        $this->color = new Color(Color::COLOR_BLACK);
+        $this->alpha = 50;
+    }
+
+    /**
+     * Get Visible.
+     *
+     * @return bool
+     */
+    public function getVisible()
+    {
+        return $this->visible;
+    }
+
+    /**
+     * Set Visible.
+     *
+     * @param bool $visible
+     *
+     * @return $this
+     */
+    public function setVisible($visible)
+    {
+        $this->visible = $visible;
+
+        return $this;
+    }
+
+    /**
+     * Get Blur radius.
+     *
+     * @return int
+     */
+    public function getBlurRadius()
+    {
+        return $this->blurRadius;
+    }
+
+    /**
+     * Set Blur radius.
+     *
+     * @param int $blurRadius
+     *
+     * @return $this
+     */
+    public function setBlurRadius($blurRadius)
+    {
+        $this->blurRadius = $blurRadius;
+
+        return $this;
+    }
+
+    /**
+     * Get Shadow distance.
+     *
+     * @return int
+     */
+    public function getDistance()
+    {
+        return $this->distance;
+    }
+
+    /**
+     * Set Shadow distance.
+     *
+     * @param int $distance
+     *
+     * @return $this
+     */
+    public function setDistance($distance)
+    {
+        $this->distance = $distance;
+
+        return $this;
+    }
+
+    /**
+     * Get Shadow direction (in degrees).
+     *
+     * @return int
+     */
+    public function getDirection()
+    {
+        return $this->direction;
+    }
+
+    /**
+     * Set Shadow direction (in degrees).
+     *
+     * @param int $direction
+     *
+     * @return $this
+     */
+    public function setDirection($direction)
+    {
+        $this->direction = $direction;
+
+        return $this;
+    }
+
+    /**
+     * Get Shadow alignment.
+     *
+     * @return string
+     */
+    public function getAlignment()
+    {
+        return $this->alignment;
+    }
+
+    /**
+     * Set Shadow alignment.
+     *
+     * @param string $alignment
+     *
+     * @return $this
+     */
+    public function setAlignment($alignment)
+    {
+        $this->alignment = $alignment;
+
+        return $this;
+    }
+
+    /**
+     * Get Color.
+     *
+     * @return Color
+     */
+    public function getColor()
+    {
+        return $this->color;
+    }
+
+    /**
+     * Set Color.
+     *
+     * @return $this
+     */
+    public function setColor(Color $color)
+    {
+        $this->color = $color;
+
+        return $this;
+    }
+
+    /**
+     * Get Alpha.
+     *
+     * @return int
+     */
+    public function getAlpha()
+    {
+        return $this->alpha;
+    }
+
+    /**
+     * Set Alpha.
+     *
+     * @param int $alpha
+     *
+     * @return $this
+     */
+    public function setAlpha($alpha)
+    {
+        $this->alpha = $alpha;
+
+        return $this;
+    }
+
+    /**
+     * Get hash code.
+     *
+     * @return string Hash code
+     */
+    public function getHashCode()
+    {
+        return md5(
+            ($this->visible ? 't' : 'f') .
+            $this->blurRadius .
+            $this->distance .
+            $this->direction .
+            $this->alignment .
+            $this->color->getHashCode() .
+            $this->alpha .
+            __CLASS__
+        );
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $value) {
+            if (is_object($value)) {
+                $this->$key = clone $value;
+            } else {
+                $this->$key = $value;
+            }
+        }
+    }
+}

+ 490 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooter.php

@@ -0,0 +1,490 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+/**
+ * <code>
+ * Header/Footer Formatting Syntax taken from Office Open XML Part 4 - Markup Language Reference, page 1970:.
+ *
+ * There are a number of formatting codes that can be written inline with the actual header / footer text, which
+ * affect the formatting in the header or footer.
+ *
+ * Example: This example shows the text "Center Bold Header" on the first line (center section), and the date on
+ * the second line (center section).
+ *         &CCenter &"-,Bold"Bold&"-,Regular"Header_x000A_&D
+ *
+ * General Rules:
+ * There is no required order in which these codes must appear.
+ *
+ * The first occurrence of the following codes turns the formatting ON, the second occurrence turns it OFF again:
+ * - strikethrough
+ * - superscript
+ * - subscript
+ * Superscript and subscript cannot both be ON at same time. Whichever comes first wins and the other is ignored,
+ * while the first is ON.
+ * &L - code for "left section" (there are three header / footer locations, "left", "center", and "right"). When
+ * two or more occurrences of this section marker exist, the contents from all markers are concatenated, in the
+ * order of appearance, and placed into the left section.
+ * &P - code for "current page #"
+ * &N - code for "total pages"
+ * &font size - code for "text font size", where font size is a font size in points.
+ * &K - code for "text font color"
+ * RGB Color is specified as RRGGBB
+ * Theme Color is specifed as TTSNN where TT is the theme color Id, S is either "+" or "-" of the tint/shade
+ * value, NN is the tint/shade value.
+ * &S - code for "text strikethrough" on / off
+ * &X - code for "text super script" on / off
+ * &Y - code for "text subscript" on / off
+ * &C - code for "center section". When two or more occurrences of this section marker exist, the contents
+ * from all markers are concatenated, in the order of appearance, and placed into the center section.
+ *
+ * &D - code for "date"
+ * &T - code for "time"
+ * &G - code for "picture as background"
+ * &U - code for "text single underline"
+ * &E - code for "double underline"
+ * &R - code for "right section". When two or more occurrences of this section marker exist, the contents
+ * from all markers are concatenated, in the order of appearance, and placed into the right section.
+ * &Z - code for "this workbook's file path"
+ * &F - code for "this workbook's file name"
+ * &A - code for "sheet tab name"
+ * &+ - code for add to page #.
+ * &- - code for subtract from page #.
+ * &"font name,font type" - code for "text font name" and "text font type", where font name and font type
+ * are strings specifying the name and type of the font, separated by a comma. When a hyphen appears in font
+ * name, it means "none specified". Both of font name and font type can be localized values.
+ * &"-,Bold" - code for "bold font style"
+ * &B - also means "bold font style".
+ * &"-,Regular" - code for "regular font style"
+ * &"-,Italic" - code for "italic font style"
+ * &I - also means "italic font style"
+ * &"-,Bold Italic" code for "bold italic font style"
+ * &O - code for "outline style"
+ * &H - code for "shadow style"
+ * </code>
+ */
+class HeaderFooter
+{
+    // Header/footer image location
+    const IMAGE_HEADER_LEFT = 'LH';
+    const IMAGE_HEADER_CENTER = 'CH';
+    const IMAGE_HEADER_RIGHT = 'RH';
+    const IMAGE_FOOTER_LEFT = 'LF';
+    const IMAGE_FOOTER_CENTER = 'CF';
+    const IMAGE_FOOTER_RIGHT = 'RF';
+
+    /**
+     * OddHeader.
+     *
+     * @var string
+     */
+    private $oddHeader = '';
+
+    /**
+     * OddFooter.
+     *
+     * @var string
+     */
+    private $oddFooter = '';
+
+    /**
+     * EvenHeader.
+     *
+     * @var string
+     */
+    private $evenHeader = '';
+
+    /**
+     * EvenFooter.
+     *
+     * @var string
+     */
+    private $evenFooter = '';
+
+    /**
+     * FirstHeader.
+     *
+     * @var string
+     */
+    private $firstHeader = '';
+
+    /**
+     * FirstFooter.
+     *
+     * @var string
+     */
+    private $firstFooter = '';
+
+    /**
+     * Different header for Odd/Even, defaults to false.
+     *
+     * @var bool
+     */
+    private $differentOddEven = false;
+
+    /**
+     * Different header for first page, defaults to false.
+     *
+     * @var bool
+     */
+    private $differentFirst = false;
+
+    /**
+     * Scale with document, defaults to true.
+     *
+     * @var bool
+     */
+    private $scaleWithDocument = true;
+
+    /**
+     * Align with margins, defaults to true.
+     *
+     * @var bool
+     */
+    private $alignWithMargins = true;
+
+    /**
+     * Header/footer images.
+     *
+     * @var HeaderFooterDrawing[]
+     */
+    private $headerFooterImages = [];
+
+    /**
+     * Create a new HeaderFooter.
+     */
+    public function __construct()
+    {
+    }
+
+    /**
+     * Get OddHeader.
+     *
+     * @return string
+     */
+    public function getOddHeader()
+    {
+        return $this->oddHeader;
+    }
+
+    /**
+     * Set OddHeader.
+     *
+     * @param string $oddHeader
+     *
+     * @return $this
+     */
+    public function setOddHeader($oddHeader)
+    {
+        $this->oddHeader = $oddHeader;
+
+        return $this;
+    }
+
+    /**
+     * Get OddFooter.
+     *
+     * @return string
+     */
+    public function getOddFooter()
+    {
+        return $this->oddFooter;
+    }
+
+    /**
+     * Set OddFooter.
+     *
+     * @param string $oddFooter
+     *
+     * @return $this
+     */
+    public function setOddFooter($oddFooter)
+    {
+        $this->oddFooter = $oddFooter;
+
+        return $this;
+    }
+
+    /**
+     * Get EvenHeader.
+     *
+     * @return string
+     */
+    public function getEvenHeader()
+    {
+        return $this->evenHeader;
+    }
+
+    /**
+     * Set EvenHeader.
+     *
+     * @param string $eventHeader
+     *
+     * @return $this
+     */
+    public function setEvenHeader($eventHeader)
+    {
+        $this->evenHeader = $eventHeader;
+
+        return $this;
+    }
+
+    /**
+     * Get EvenFooter.
+     *
+     * @return string
+     */
+    public function getEvenFooter()
+    {
+        return $this->evenFooter;
+    }
+
+    /**
+     * Set EvenFooter.
+     *
+     * @param string $evenFooter
+     *
+     * @return $this
+     */
+    public function setEvenFooter($evenFooter)
+    {
+        $this->evenFooter = $evenFooter;
+
+        return $this;
+    }
+
+    /**
+     * Get FirstHeader.
+     *
+     * @return string
+     */
+    public function getFirstHeader()
+    {
+        return $this->firstHeader;
+    }
+
+    /**
+     * Set FirstHeader.
+     *
+     * @param string $firstHeader
+     *
+     * @return $this
+     */
+    public function setFirstHeader($firstHeader)
+    {
+        $this->firstHeader = $firstHeader;
+
+        return $this;
+    }
+
+    /**
+     * Get FirstFooter.
+     *
+     * @return string
+     */
+    public function getFirstFooter()
+    {
+        return $this->firstFooter;
+    }
+
+    /**
+     * Set FirstFooter.
+     *
+     * @param string $firstFooter
+     *
+     * @return $this
+     */
+    public function setFirstFooter($firstFooter)
+    {
+        $this->firstFooter = $firstFooter;
+
+        return $this;
+    }
+
+    /**
+     * Get DifferentOddEven.
+     *
+     * @return bool
+     */
+    public function getDifferentOddEven()
+    {
+        return $this->differentOddEven;
+    }
+
+    /**
+     * Set DifferentOddEven.
+     *
+     * @param bool $differentOddEvent
+     *
+     * @return $this
+     */
+    public function setDifferentOddEven($differentOddEvent)
+    {
+        $this->differentOddEven = $differentOddEvent;
+
+        return $this;
+    }
+
+    /**
+     * Get DifferentFirst.
+     *
+     * @return bool
+     */
+    public function getDifferentFirst()
+    {
+        return $this->differentFirst;
+    }
+
+    /**
+     * Set DifferentFirst.
+     *
+     * @param bool $differentFirst
+     *
+     * @return $this
+     */
+    public function setDifferentFirst($differentFirst)
+    {
+        $this->differentFirst = $differentFirst;
+
+        return $this;
+    }
+
+    /**
+     * Get ScaleWithDocument.
+     *
+     * @return bool
+     */
+    public function getScaleWithDocument()
+    {
+        return $this->scaleWithDocument;
+    }
+
+    /**
+     * Set ScaleWithDocument.
+     *
+     * @param bool $scaleWithDocument
+     *
+     * @return $this
+     */
+    public function setScaleWithDocument($scaleWithDocument)
+    {
+        $this->scaleWithDocument = $scaleWithDocument;
+
+        return $this;
+    }
+
+    /**
+     * Get AlignWithMargins.
+     *
+     * @return bool
+     */
+    public function getAlignWithMargins()
+    {
+        return $this->alignWithMargins;
+    }
+
+    /**
+     * Set AlignWithMargins.
+     *
+     * @param bool $alignWithMargins
+     *
+     * @return $this
+     */
+    public function setAlignWithMargins($alignWithMargins)
+    {
+        $this->alignWithMargins = $alignWithMargins;
+
+        return $this;
+    }
+
+    /**
+     * Add header/footer image.
+     *
+     * @param string $location
+     *
+     * @return $this
+     */
+    public function addImage(HeaderFooterDrawing $image, $location = self::IMAGE_HEADER_LEFT)
+    {
+        $this->headerFooterImages[$location] = $image;
+
+        return $this;
+    }
+
+    /**
+     * Remove header/footer image.
+     *
+     * @param string $location
+     *
+     * @return $this
+     */
+    public function removeImage($location = self::IMAGE_HEADER_LEFT)
+    {
+        if (isset($this->headerFooterImages[$location])) {
+            unset($this->headerFooterImages[$location]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set header/footer images.
+     *
+     * @param HeaderFooterDrawing[] $images
+     *
+     * @return $this
+     */
+    public function setImages(array $images)
+    {
+        $this->headerFooterImages = $images;
+
+        return $this;
+    }
+
+    /**
+     * Get header/footer images.
+     *
+     * @return HeaderFooterDrawing[]
+     */
+    public function getImages()
+    {
+        // Sort array
+        $images = [];
+        if (isset($this->headerFooterImages[self::IMAGE_HEADER_LEFT])) {
+            $images[self::IMAGE_HEADER_LEFT] = $this->headerFooterImages[self::IMAGE_HEADER_LEFT];
+        }
+        if (isset($this->headerFooterImages[self::IMAGE_HEADER_CENTER])) {
+            $images[self::IMAGE_HEADER_CENTER] = $this->headerFooterImages[self::IMAGE_HEADER_CENTER];
+        }
+        if (isset($this->headerFooterImages[self::IMAGE_HEADER_RIGHT])) {
+            $images[self::IMAGE_HEADER_RIGHT] = $this->headerFooterImages[self::IMAGE_HEADER_RIGHT];
+        }
+        if (isset($this->headerFooterImages[self::IMAGE_FOOTER_LEFT])) {
+            $images[self::IMAGE_FOOTER_LEFT] = $this->headerFooterImages[self::IMAGE_FOOTER_LEFT];
+        }
+        if (isset($this->headerFooterImages[self::IMAGE_FOOTER_CENTER])) {
+            $images[self::IMAGE_FOOTER_CENTER] = $this->headerFooterImages[self::IMAGE_FOOTER_CENTER];
+        }
+        if (isset($this->headerFooterImages[self::IMAGE_FOOTER_RIGHT])) {
+            $images[self::IMAGE_FOOTER_RIGHT] = $this->headerFooterImages[self::IMAGE_FOOTER_RIGHT];
+        }
+        $this->headerFooterImages = $images;
+
+        return $this->headerFooterImages;
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $value) {
+            if (is_object($value)) {
+                $this->$key = clone $value;
+            } else {
+                $this->$key = $value;
+            }
+        }
+    }
+}

+ 24 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/HeaderFooterDrawing.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+class HeaderFooterDrawing extends Drawing
+{
+    /**
+     * Get hash code.
+     *
+     * @return string Hash code
+     */
+    public function getHashCode()
+    {
+        return md5(
+            $this->getPath() .
+            $this->name .
+            $this->offsetX .
+            $this->offsetY .
+            $this->width .
+            $this->height .
+            __CLASS__
+        );
+    }
+}

+ 74 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Iterator.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+
+/**
+ * @implements \Iterator<int, Worksheet>
+ */
+class Iterator implements \Iterator
+{
+    /**
+     * Spreadsheet to iterate.
+     *
+     * @var Spreadsheet
+     */
+    private $subject;
+
+    /**
+     * Current iterator position.
+     *
+     * @var int
+     */
+    private $position = 0;
+
+    /**
+     * Create a new worksheet iterator.
+     */
+    public function __construct(Spreadsheet $subject)
+    {
+        // Set subject
+        $this->subject = $subject;
+    }
+
+    /**
+     * Rewind iterator.
+     */
+    public function rewind(): void
+    {
+        $this->position = 0;
+    }
+
+    /**
+     * Current Worksheet.
+     */
+    public function current(): Worksheet
+    {
+        return $this->subject->getSheet($this->position);
+    }
+
+    /**
+     * Current key.
+     */
+    public function key(): int
+    {
+        return $this->position;
+    }
+
+    /**
+     * Next value.
+     */
+    public function next(): void
+    {
+        ++$this->position;
+    }
+
+    /**
+     * Are there more Worksheet instances available?
+     */
+    public function valid(): bool
+    {
+        return $this->position < $this->subject->getSheetCount() && $this->position >= 0;
+    }
+}

+ 356 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php

@@ -0,0 +1,356 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use GdImage;
+use PhpOffice\PhpSpreadsheet\Exception;
+use PhpOffice\PhpSpreadsheet\Shared\File;
+
+class MemoryDrawing extends BaseDrawing
+{
+    // Rendering functions
+    const RENDERING_DEFAULT = 'imagepng';
+    const RENDERING_PNG = 'imagepng';
+    const RENDERING_GIF = 'imagegif';
+    const RENDERING_JPEG = 'imagejpeg';
+
+    // MIME types
+    const MIMETYPE_DEFAULT = 'image/png';
+    const MIMETYPE_PNG = 'image/png';
+    const MIMETYPE_GIF = 'image/gif';
+    const MIMETYPE_JPEG = 'image/jpeg';
+
+    const SUPPORTED_MIME_TYPES = [
+        self::MIMETYPE_GIF,
+        self::MIMETYPE_JPEG,
+        self::MIMETYPE_PNG,
+    ];
+
+    /**
+     * Image resource.
+     *
+     * @var null|GdImage|resource
+     */
+    private $imageResource;
+
+    /**
+     * Rendering function.
+     *
+     * @var string
+     */
+    private $renderingFunction;
+
+    /**
+     * Mime type.
+     *
+     * @var string
+     */
+    private $mimeType;
+
+    /**
+     * Unique name.
+     *
+     * @var string
+     */
+    private $uniqueName;
+
+    /** @var null|resource */
+    private $alwaysNull;
+
+    /**
+     * Create a new MemoryDrawing.
+     */
+    public function __construct()
+    {
+        // Initialise values
+        $this->renderingFunction = self::RENDERING_DEFAULT;
+        $this->mimeType = self::MIMETYPE_DEFAULT;
+        $this->uniqueName = md5(mt_rand(0, 9999) . time() . mt_rand(0, 9999));
+        $this->alwaysNull = null;
+
+        // Initialize parent
+        parent::__construct();
+    }
+
+    public function __destruct()
+    {
+        if ($this->imageResource) {
+            $rslt = @imagedestroy($this->imageResource);
+            // "Fix" for Scrutinizer
+            $this->imageResource = $rslt ? null : $this->alwaysNull;
+        }
+    }
+
+    public function __clone()
+    {
+        parent::__clone();
+        $this->cloneResource();
+    }
+
+    private function cloneResource(): void
+    {
+        if (!$this->imageResource) {
+            return;
+        }
+
+        $width = (int) imagesx($this->imageResource);
+        $height = (int) imagesy($this->imageResource);
+
+        if (imageistruecolor($this->imageResource)) {
+            $clone = imagecreatetruecolor($width, $height);
+            if (!$clone) {
+                throw new Exception('Could not clone image resource');
+            }
+
+            imagealphablending($clone, false);
+            imagesavealpha($clone, true);
+        } else {
+            $clone = imagecreate($width, $height);
+            if (!$clone) {
+                throw new Exception('Could not clone image resource');
+            }
+
+            // If the image has transparency...
+            $transparent = imagecolortransparent($this->imageResource);
+            if ($transparent >= 0) {
+                $rgb = imagecolorsforindex($this->imageResource, $transparent);
+                if (empty($rgb)) {
+                    throw new Exception('Could not get image colors');
+                }
+
+                imagesavealpha($clone, true);
+                $color = imagecolorallocatealpha($clone, $rgb['red'], $rgb['green'], $rgb['blue'], $rgb['alpha']);
+                if ($color === false) {
+                    throw new Exception('Could not get image alpha color');
+                }
+
+                imagefill($clone, 0, 0, $color);
+            }
+        }
+
+        //Create the Clone!!
+        imagecopy($clone, $this->imageResource, 0, 0, 0, 0, $width, $height);
+
+        $this->imageResource = $clone;
+    }
+
+    /**
+     * @param resource $imageStream Stream data to be converted to a Memory Drawing
+     *
+     * @throws Exception
+     */
+    public static function fromStream($imageStream): self
+    {
+        $streamValue = stream_get_contents($imageStream);
+        if ($streamValue === false) {
+            throw new Exception('Unable to read data from stream');
+        }
+
+        return self::fromString($streamValue);
+    }
+
+    /**
+     * @param string $imageString String data to be converted to a Memory Drawing
+     *
+     * @throws Exception
+     */
+    public static function fromString(string $imageString): self
+    {
+        $gdImage = @imagecreatefromstring($imageString);
+        if ($gdImage === false) {
+            throw new Exception('Value cannot be converted to an image');
+        }
+
+        $mimeType = self::identifyMimeType($imageString);
+        $renderingFunction = self::identifyRenderingFunction($mimeType);
+
+        $drawing = new self();
+        $drawing->setImageResource($gdImage);
+        $drawing->setRenderingFunction($renderingFunction);
+        $drawing->setMimeType($mimeType);
+
+        return $drawing;
+    }
+
+    private static function identifyRenderingFunction(string $mimeType): string
+    {
+        switch ($mimeType) {
+            case self::MIMETYPE_PNG:
+                return self::RENDERING_PNG;
+            case self::MIMETYPE_JPEG:
+                return self::RENDERING_JPEG;
+            case self::MIMETYPE_GIF:
+                return self::RENDERING_GIF;
+        }
+
+        return self::RENDERING_DEFAULT;
+    }
+
+    /**
+     * @throws Exception
+     */
+    private static function identifyMimeType(string $imageString): string
+    {
+        $temporaryFileName = File::temporaryFilename();
+        file_put_contents($temporaryFileName, $imageString);
+
+        $mimeType = self::identifyMimeTypeUsingExif($temporaryFileName);
+        if ($mimeType !== null) {
+            unlink($temporaryFileName);
+
+            return $mimeType;
+        }
+
+        $mimeType = self::identifyMimeTypeUsingGd($temporaryFileName);
+        if ($mimeType !== null) {
+            unlink($temporaryFileName);
+
+            return $mimeType;
+        }
+
+        unlink($temporaryFileName);
+
+        return self::MIMETYPE_DEFAULT;
+    }
+
+    private static function identifyMimeTypeUsingExif(string $temporaryFileName): ?string
+    {
+        if (function_exists('exif_imagetype')) {
+            $imageType = @exif_imagetype($temporaryFileName);
+            $mimeType = ($imageType) ? image_type_to_mime_type($imageType) : null;
+
+            return self::supportedMimeTypes($mimeType);
+        }
+
+        return null;
+    }
+
+    private static function identifyMimeTypeUsingGd(string $temporaryFileName): ?string
+    {
+        if (function_exists('getimagesize')) {
+            $imageSize = @getimagesize($temporaryFileName);
+            if (is_array($imageSize)) {
+                $mimeType = $imageSize['mime'] ?? null;
+
+                return self::supportedMimeTypes($mimeType);
+            }
+        }
+
+        return null;
+    }
+
+    private static function supportedMimeTypes(?string $mimeType = null): ?string
+    {
+        if (in_array($mimeType, self::SUPPORTED_MIME_TYPES, true)) {
+            return $mimeType;
+        }
+
+        return null;
+    }
+
+    /**
+     * Get image resource.
+     *
+     * @return null|GdImage|resource
+     */
+    public function getImageResource()
+    {
+        return $this->imageResource;
+    }
+
+    /**
+     * Set image resource.
+     *
+     * @param GdImage|resource $value
+     *
+     * @return $this
+     */
+    public function setImageResource($value)
+    {
+        $this->imageResource = $value;
+
+        if ($this->imageResource !== null) {
+            // Get width/height
+            $this->width = (int) imagesx($this->imageResource);
+            $this->height = (int) imagesy($this->imageResource);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get rendering function.
+     *
+     * @return string
+     */
+    public function getRenderingFunction()
+    {
+        return $this->renderingFunction;
+    }
+
+    /**
+     * Set rendering function.
+     *
+     * @param string $value see self::RENDERING_*
+     *
+     * @return $this
+     */
+    public function setRenderingFunction($value)
+    {
+        $this->renderingFunction = $value;
+
+        return $this;
+    }
+
+    /**
+     * Get mime type.
+     *
+     * @return string
+     */
+    public function getMimeType()
+    {
+        return $this->mimeType;
+    }
+
+    /**
+     * Set mime type.
+     *
+     * @param string $value see self::MIMETYPE_*
+     *
+     * @return $this
+     */
+    public function setMimeType($value)
+    {
+        $this->mimeType = $value;
+
+        return $this;
+    }
+
+    /**
+     * Get indexed filename (using image index).
+     */
+    public function getIndexedFilename(): string
+    {
+        $extension = strtolower($this->getMimeType());
+        $extension = explode('/', $extension);
+        $extension = $extension[1];
+
+        return $this->uniqueName . $this->getImageIndex() . '.' . $extension;
+    }
+
+    /**
+     * Get hash code.
+     *
+     * @return string Hash code
+     */
+    public function getHashCode()
+    {
+        return md5(
+            $this->renderingFunction .
+            $this->mimeType .
+            $this->uniqueName .
+            parent::getHashCode() .
+            __CLASS__
+        );
+    }
+}

+ 58 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageBreak.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Calculation\Functions;
+use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+
+class PageBreak
+{
+    /** @var int */
+    private $breakType;
+
+    /** @var string */
+    private $coordinate;
+
+    /** @var int */
+    private $maxColOrRow;
+
+    /** @param array|CellAddress|string $coordinate */
+    public function __construct(int $breakType, $coordinate, int $maxColOrRow = -1)
+    {
+        $coordinate = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
+        $this->breakType = $breakType;
+        $this->coordinate = $coordinate;
+        $this->maxColOrRow = $maxColOrRow;
+    }
+
+    public function getBreakType(): int
+    {
+        return $this->breakType;
+    }
+
+    public function getCoordinate(): string
+    {
+        return $this->coordinate;
+    }
+
+    public function getMaxColOrRow(): int
+    {
+        return $this->maxColOrRow;
+    }
+
+    public function getColumnInt(): int
+    {
+        return Coordinate::indexesFromString($this->coordinate)[0];
+    }
+
+    public function getRow(): int
+    {
+        return Coordinate::indexesFromString($this->coordinate)[1];
+    }
+
+    public function getColumnString(): string
+    {
+        return Coordinate::indexesFromString($this->coordinate)[2];
+    }
+}

+ 229 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageMargins.php

@@ -0,0 +1,229 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+class PageMargins
+{
+    /**
+     * Left.
+     *
+     * @var float
+     */
+    private $left = 0.7;
+
+    /**
+     * Right.
+     *
+     * @var float
+     */
+    private $right = 0.7;
+
+    /**
+     * Top.
+     *
+     * @var float
+     */
+    private $top = 0.75;
+
+    /**
+     * Bottom.
+     *
+     * @var float
+     */
+    private $bottom = 0.75;
+
+    /**
+     * Header.
+     *
+     * @var float
+     */
+    private $header = 0.3;
+
+    /**
+     * Footer.
+     *
+     * @var float
+     */
+    private $footer = 0.3;
+
+    /**
+     * Create a new PageMargins.
+     */
+    public function __construct()
+    {
+    }
+
+    /**
+     * Get Left.
+     *
+     * @return float
+     */
+    public function getLeft()
+    {
+        return $this->left;
+    }
+
+    /**
+     * Set Left.
+     *
+     * @param float $left
+     *
+     * @return $this
+     */
+    public function setLeft($left)
+    {
+        $this->left = $left;
+
+        return $this;
+    }
+
+    /**
+     * Get Right.
+     *
+     * @return float
+     */
+    public function getRight()
+    {
+        return $this->right;
+    }
+
+    /**
+     * Set Right.
+     *
+     * @param float $right
+     *
+     * @return $this
+     */
+    public function setRight($right)
+    {
+        $this->right = $right;
+
+        return $this;
+    }
+
+    /**
+     * Get Top.
+     *
+     * @return float
+     */
+    public function getTop()
+    {
+        return $this->top;
+    }
+
+    /**
+     * Set Top.
+     *
+     * @param float $top
+     *
+     * @return $this
+     */
+    public function setTop($top)
+    {
+        $this->top = $top;
+
+        return $this;
+    }
+
+    /**
+     * Get Bottom.
+     *
+     * @return float
+     */
+    public function getBottom()
+    {
+        return $this->bottom;
+    }
+
+    /**
+     * Set Bottom.
+     *
+     * @param float $bottom
+     *
+     * @return $this
+     */
+    public function setBottom($bottom)
+    {
+        $this->bottom = $bottom;
+
+        return $this;
+    }
+
+    /**
+     * Get Header.
+     *
+     * @return float
+     */
+    public function getHeader()
+    {
+        return $this->header;
+    }
+
+    /**
+     * Set Header.
+     *
+     * @param float $header
+     *
+     * @return $this
+     */
+    public function setHeader($header)
+    {
+        $this->header = $header;
+
+        return $this;
+    }
+
+    /**
+     * Get Footer.
+     *
+     * @return float
+     */
+    public function getFooter()
+    {
+        return $this->footer;
+    }
+
+    /**
+     * Set Footer.
+     *
+     * @param float $footer
+     *
+     * @return $this
+     */
+    public function setFooter($footer)
+    {
+        $this->footer = $footer;
+
+        return $this;
+    }
+
+    public static function fromCentimeters(float $value): float
+    {
+        return $value / 2.54;
+    }
+
+    public static function toCentimeters(float $value): float
+    {
+        return $value * 2.54;
+    }
+
+    public static function fromMillimeters(float $value): float
+    {
+        return $value / 25.4;
+    }
+
+    public static function toMillimeters(float $value): float
+    {
+        return $value * 25.4;
+    }
+
+    public static function fromPoints(float $value): float
+    {
+        return $value / 72;
+    }
+
+    public static function toPoints(float $value): float
+    {
+        return $value * 72;
+    }
+}

+ 888 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php

@@ -0,0 +1,888 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+
+/**
+ * <code>
+ * Paper size taken from Office Open XML Part 4 - Markup Language Reference, page 1988:.
+ *
+ * 1 = Letter paper (8.5 in. by 11 in.)
+ * 2 = Letter small paper (8.5 in. by 11 in.)
+ * 3 = Tabloid paper (11 in. by 17 in.)
+ * 4 = Ledger paper (17 in. by 11 in.)
+ * 5 = Legal paper (8.5 in. by 14 in.)
+ * 6 = Statement paper (5.5 in. by 8.5 in.)
+ * 7 = Executive paper (7.25 in. by 10.5 in.)
+ * 8 = A3 paper (297 mm by 420 mm)
+ * 9 = A4 paper (210 mm by 297 mm)
+ * 10 = A4 small paper (210 mm by 297 mm)
+ * 11 = A5 paper (148 mm by 210 mm)
+ * 12 = B4 paper (250 mm by 353 mm)
+ * 13 = B5 paper (176 mm by 250 mm)
+ * 14 = Folio paper (8.5 in. by 13 in.)
+ * 15 = Quarto paper (215 mm by 275 mm)
+ * 16 = Standard paper (10 in. by 14 in.)
+ * 17 = Standard paper (11 in. by 17 in.)
+ * 18 = Note paper (8.5 in. by 11 in.)
+ * 19 = #9 envelope (3.875 in. by 8.875 in.)
+ * 20 = #10 envelope (4.125 in. by 9.5 in.)
+ * 21 = #11 envelope (4.5 in. by 10.375 in.)
+ * 22 = #12 envelope (4.75 in. by 11 in.)
+ * 23 = #14 envelope (5 in. by 11.5 in.)
+ * 24 = C paper (17 in. by 22 in.)
+ * 25 = D paper (22 in. by 34 in.)
+ * 26 = E paper (34 in. by 44 in.)
+ * 27 = DL envelope (110 mm by 220 mm)
+ * 28 = C5 envelope (162 mm by 229 mm)
+ * 29 = C3 envelope (324 mm by 458 mm)
+ * 30 = C4 envelope (229 mm by 324 mm)
+ * 31 = C6 envelope (114 mm by 162 mm)
+ * 32 = C65 envelope (114 mm by 229 mm)
+ * 33 = B4 envelope (250 mm by 353 mm)
+ * 34 = B5 envelope (176 mm by 250 mm)
+ * 35 = B6 envelope (176 mm by 125 mm)
+ * 36 = Italy envelope (110 mm by 230 mm)
+ * 37 = Monarch envelope (3.875 in. by 7.5 in.).
+ * 38 = 6 3/4 envelope (3.625 in. by 6.5 in.)
+ * 39 = US standard fanfold (14.875 in. by 11 in.)
+ * 40 = German standard fanfold (8.5 in. by 12 in.)
+ * 41 = German legal fanfold (8.5 in. by 13 in.)
+ * 42 = ISO B4 (250 mm by 353 mm)
+ * 43 = Japanese double postcard (200 mm by 148 mm)
+ * 44 = Standard paper (9 in. by 11 in.)
+ * 45 = Standard paper (10 in. by 11 in.)
+ * 46 = Standard paper (15 in. by 11 in.)
+ * 47 = Invite envelope (220 mm by 220 mm)
+ * 50 = Letter extra paper (9.275 in. by 12 in.)
+ * 51 = Legal extra paper (9.275 in. by 15 in.)
+ * 52 = Tabloid extra paper (11.69 in. by 18 in.)
+ * 53 = A4 extra paper (236 mm by 322 mm)
+ * 54 = Letter transverse paper (8.275 in. by 11 in.)
+ * 55 = A4 transverse paper (210 mm by 297 mm)
+ * 56 = Letter extra transverse paper (9.275 in. by 12 in.)
+ * 57 = SuperA/SuperA/A4 paper (227 mm by 356 mm)
+ * 58 = SuperB/SuperB/A3 paper (305 mm by 487 mm)
+ * 59 = Letter plus paper (8.5 in. by 12.69 in.)
+ * 60 = A4 plus paper (210 mm by 330 mm)
+ * 61 = A5 transverse paper (148 mm by 210 mm)
+ * 62 = JIS B5 transverse paper (182 mm by 257 mm)
+ * 63 = A3 extra paper (322 mm by 445 mm)
+ * 64 = A5 extra paper (174 mm by 235 mm)
+ * 65 = ISO B5 extra paper (201 mm by 276 mm)
+ * 66 = A2 paper (420 mm by 594 mm)
+ * 67 = A3 transverse paper (297 mm by 420 mm)
+ * 68 = A3 extra transverse paper (322 mm by 445 mm)
+ * </code>
+ */
+class PageSetup
+{
+    // Paper size
+    const PAPERSIZE_LETTER = 1;
+    const PAPERSIZE_LETTER_SMALL = 2;
+    const PAPERSIZE_TABLOID = 3;
+    const PAPERSIZE_LEDGER = 4;
+    const PAPERSIZE_LEGAL = 5;
+    const PAPERSIZE_STATEMENT = 6;
+    const PAPERSIZE_EXECUTIVE = 7;
+    const PAPERSIZE_A3 = 8;
+    const PAPERSIZE_A4 = 9;
+    const PAPERSIZE_A4_SMALL = 10;
+    const PAPERSIZE_A5 = 11;
+    const PAPERSIZE_B4 = 12;
+    const PAPERSIZE_B5 = 13;
+    const PAPERSIZE_FOLIO = 14;
+    const PAPERSIZE_QUARTO = 15;
+    const PAPERSIZE_STANDARD_1 = 16;
+    const PAPERSIZE_STANDARD_2 = 17;
+    const PAPERSIZE_NOTE = 18;
+    const PAPERSIZE_NO9_ENVELOPE = 19;
+    const PAPERSIZE_NO10_ENVELOPE = 20;
+    const PAPERSIZE_NO11_ENVELOPE = 21;
+    const PAPERSIZE_NO12_ENVELOPE = 22;
+    const PAPERSIZE_NO14_ENVELOPE = 23;
+    const PAPERSIZE_C = 24;
+    const PAPERSIZE_D = 25;
+    const PAPERSIZE_E = 26;
+    const PAPERSIZE_DL_ENVELOPE = 27;
+    const PAPERSIZE_C5_ENVELOPE = 28;
+    const PAPERSIZE_C3_ENVELOPE = 29;
+    const PAPERSIZE_C4_ENVELOPE = 30;
+    const PAPERSIZE_C6_ENVELOPE = 31;
+    const PAPERSIZE_C65_ENVELOPE = 32;
+    const PAPERSIZE_B4_ENVELOPE = 33;
+    const PAPERSIZE_B5_ENVELOPE = 34;
+    const PAPERSIZE_B6_ENVELOPE = 35;
+    const PAPERSIZE_ITALY_ENVELOPE = 36;
+    const PAPERSIZE_MONARCH_ENVELOPE = 37;
+    const PAPERSIZE_6_3_4_ENVELOPE = 38;
+    const PAPERSIZE_US_STANDARD_FANFOLD = 39;
+    const PAPERSIZE_GERMAN_STANDARD_FANFOLD = 40;
+    const PAPERSIZE_GERMAN_LEGAL_FANFOLD = 41;
+    const PAPERSIZE_ISO_B4 = 42;
+    const PAPERSIZE_JAPANESE_DOUBLE_POSTCARD = 43;
+    const PAPERSIZE_STANDARD_PAPER_1 = 44;
+    const PAPERSIZE_STANDARD_PAPER_2 = 45;
+    const PAPERSIZE_STANDARD_PAPER_3 = 46;
+    const PAPERSIZE_INVITE_ENVELOPE = 47;
+    const PAPERSIZE_LETTER_EXTRA_PAPER = 48;
+    const PAPERSIZE_LEGAL_EXTRA_PAPER = 49;
+    const PAPERSIZE_TABLOID_EXTRA_PAPER = 50;
+    const PAPERSIZE_A4_EXTRA_PAPER = 51;
+    const PAPERSIZE_LETTER_TRANSVERSE_PAPER = 52;
+    const PAPERSIZE_A4_TRANSVERSE_PAPER = 53;
+    const PAPERSIZE_LETTER_EXTRA_TRANSVERSE_PAPER = 54;
+    const PAPERSIZE_SUPERA_SUPERA_A4_PAPER = 55;
+    const PAPERSIZE_SUPERB_SUPERB_A3_PAPER = 56;
+    const PAPERSIZE_LETTER_PLUS_PAPER = 57;
+    const PAPERSIZE_A4_PLUS_PAPER = 58;
+    const PAPERSIZE_A5_TRANSVERSE_PAPER = 59;
+    const PAPERSIZE_JIS_B5_TRANSVERSE_PAPER = 60;
+    const PAPERSIZE_A3_EXTRA_PAPER = 61;
+    const PAPERSIZE_A5_EXTRA_PAPER = 62;
+    const PAPERSIZE_ISO_B5_EXTRA_PAPER = 63;
+    const PAPERSIZE_A2_PAPER = 64;
+    const PAPERSIZE_A3_TRANSVERSE_PAPER = 65;
+    const PAPERSIZE_A3_EXTRA_TRANSVERSE_PAPER = 66;
+
+    // Page orientation
+    const ORIENTATION_DEFAULT = 'default';
+    const ORIENTATION_LANDSCAPE = 'landscape';
+    const ORIENTATION_PORTRAIT = 'portrait';
+
+    // Print Range Set Method
+    const SETPRINTRANGE_OVERWRITE = 'O';
+    const SETPRINTRANGE_INSERT = 'I';
+
+    const PAGEORDER_OVER_THEN_DOWN = 'overThenDown';
+    const PAGEORDER_DOWN_THEN_OVER = 'downThenOver';
+
+    /**
+     * Paper size default.
+     *
+     * @var int
+     */
+    private static $paperSizeDefault = self::PAPERSIZE_LETTER;
+
+    /**
+     * Paper size.
+     *
+     * @var ?int
+     */
+    private $paperSize;
+
+    /**
+     * Orientation default.
+     *
+     * @var string
+     */
+    private static $orientationDefault = self::ORIENTATION_DEFAULT;
+
+    /**
+     * Orientation.
+     *
+     * @var string
+     */
+    private $orientation;
+
+    /**
+     * Scale (Print Scale).
+     *
+     * Print scaling. Valid values range from 10 to 400
+     * This setting is overridden when fitToWidth and/or fitToHeight are in use
+     *
+     * @var null|int
+     */
+    private $scale = 100;
+
+    /**
+     * Fit To Page
+     * Whether scale or fitToWith / fitToHeight applies.
+     *
+     * @var bool
+     */
+    private $fitToPage = false;
+
+    /**
+     * Fit To Height
+     * Number of vertical pages to fit on.
+     *
+     * @var null|int
+     */
+    private $fitToHeight = 1;
+
+    /**
+     * Fit To Width
+     * Number of horizontal pages to fit on.
+     *
+     * @var null|int
+     */
+    private $fitToWidth = 1;
+
+    /**
+     * Columns to repeat at left.
+     *
+     * @var array Containing start column and end column, empty array if option unset
+     */
+    private $columnsToRepeatAtLeft = ['', ''];
+
+    /**
+     * Rows to repeat at top.
+     *
+     * @var array Containing start row number and end row number, empty array if option unset
+     */
+    private $rowsToRepeatAtTop = [0, 0];
+
+    /**
+     * Center page horizontally.
+     *
+     * @var bool
+     */
+    private $horizontalCentered = false;
+
+    /**
+     * Center page vertically.
+     *
+     * @var bool
+     */
+    private $verticalCentered = false;
+
+    /**
+     * Print area.
+     *
+     * @var null|string
+     */
+    private $printArea;
+
+    /**
+     * First page number.
+     *
+     * @var ?int
+     */
+    private $firstPageNumber;
+
+    /** @var string */
+    private $pageOrder = self::PAGEORDER_DOWN_THEN_OVER;
+
+    /**
+     * Create a new PageSetup.
+     */
+    public function __construct()
+    {
+        $this->orientation = self::$orientationDefault;
+    }
+
+    /**
+     * Get Paper Size.
+     *
+     * @return int
+     */
+    public function getPaperSize()
+    {
+        return $this->paperSize ?? self::$paperSizeDefault;
+    }
+
+    /**
+     * Set Paper Size.
+     *
+     * @param int $paperSize see self::PAPERSIZE_*
+     *
+     * @return $this
+     */
+    public function setPaperSize($paperSize)
+    {
+        $this->paperSize = $paperSize;
+
+        return $this;
+    }
+
+    /**
+     * Get Paper Size default.
+     */
+    public static function getPaperSizeDefault(): int
+    {
+        return self::$paperSizeDefault;
+    }
+
+    /**
+     * Set Paper Size Default.
+     */
+    public static function setPaperSizeDefault(int $paperSize): void
+    {
+        self::$paperSizeDefault = $paperSize;
+    }
+
+    /**
+     * Get Orientation.
+     *
+     * @return string
+     */
+    public function getOrientation()
+    {
+        return $this->orientation;
+    }
+
+    /**
+     * Set Orientation.
+     *
+     * @param string $orientation see self::ORIENTATION_*
+     *
+     * @return $this
+     */
+    public function setOrientation($orientation)
+    {
+        if ($orientation === self::ORIENTATION_LANDSCAPE || $orientation === self::ORIENTATION_PORTRAIT || $orientation === self::ORIENTATION_DEFAULT) {
+            $this->orientation = $orientation;
+        }
+
+        return $this;
+    }
+
+    public static function getOrientationDefault(): string
+    {
+        return self::$orientationDefault;
+    }
+
+    public static function setOrientationDefault(string $orientation): void
+    {
+        if ($orientation === self::ORIENTATION_LANDSCAPE || $orientation === self::ORIENTATION_PORTRAIT || $orientation === self::ORIENTATION_DEFAULT) {
+            self::$orientationDefault = $orientation;
+        }
+    }
+
+    /**
+     * Get Scale.
+     *
+     * @return null|int
+     */
+    public function getScale()
+    {
+        return $this->scale;
+    }
+
+    /**
+     * Set Scale.
+     * Print scaling. Valid values range from 10 to 400
+     * This setting is overridden when fitToWidth and/or fitToHeight are in use.
+     *
+     * @param null|int $scale
+     * @param bool $update Update fitToPage so scaling applies rather than fitToHeight / fitToWidth
+     *
+     * @return $this
+     */
+    public function setScale($scale, $update = true)
+    {
+        // Microsoft Office Excel 2007 only allows setting a scale between 10 and 400 via the user interface,
+        // but it is apparently still able to handle any scale >= 0, where 0 results in 100
+        if ($scale === null || $scale >= 0) {
+            $this->scale = $scale;
+            if ($update) {
+                $this->fitToPage = false;
+            }
+        } else {
+            throw new PhpSpreadsheetException('Scale must not be negative');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get Fit To Page.
+     *
+     * @return bool
+     */
+    public function getFitToPage()
+    {
+        return $this->fitToPage;
+    }
+
+    /**
+     * Set Fit To Page.
+     *
+     * @param bool $fitToPage
+     *
+     * @return $this
+     */
+    public function setFitToPage($fitToPage)
+    {
+        $this->fitToPage = $fitToPage;
+
+        return $this;
+    }
+
+    /**
+     * Get Fit To Height.
+     *
+     * @return null|int
+     */
+    public function getFitToHeight()
+    {
+        return $this->fitToHeight;
+    }
+
+    /**
+     * Set Fit To Height.
+     *
+     * @param null|int $fitToHeight
+     * @param bool $update Update fitToPage so it applies rather than scaling
+     *
+     * @return $this
+     */
+    public function setFitToHeight($fitToHeight, $update = true)
+    {
+        $this->fitToHeight = $fitToHeight;
+        if ($update) {
+            $this->fitToPage = true;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get Fit To Width.
+     *
+     * @return null|int
+     */
+    public function getFitToWidth()
+    {
+        return $this->fitToWidth;
+    }
+
+    /**
+     * Set Fit To Width.
+     *
+     * @param null|int $value
+     * @param bool $update Update fitToPage so it applies rather than scaling
+     *
+     * @return $this
+     */
+    public function setFitToWidth($value, $update = true)
+    {
+        $this->fitToWidth = $value;
+        if ($update) {
+            $this->fitToPage = true;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Is Columns to repeat at left set?
+     *
+     * @return bool
+     */
+    public function isColumnsToRepeatAtLeftSet()
+    {
+        if (!empty($this->columnsToRepeatAtLeft)) {
+            if ($this->columnsToRepeatAtLeft[0] != '' && $this->columnsToRepeatAtLeft[1] != '') {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Get Columns to repeat at left.
+     *
+     * @return array Containing start column and end column, empty array if option unset
+     */
+    public function getColumnsToRepeatAtLeft()
+    {
+        return $this->columnsToRepeatAtLeft;
+    }
+
+    /**
+     * Set Columns to repeat at left.
+     *
+     * @param array $columnsToRepeatAtLeft Containing start column and end column, empty array if option unset
+     *
+     * @return $this
+     */
+    public function setColumnsToRepeatAtLeft(array $columnsToRepeatAtLeft)
+    {
+        $this->columnsToRepeatAtLeft = $columnsToRepeatAtLeft;
+
+        return $this;
+    }
+
+    /**
+     * Set Columns to repeat at left by start and end.
+     *
+     * @param string $start eg: 'A'
+     * @param string $end eg: 'B'
+     *
+     * @return $this
+     */
+    public function setColumnsToRepeatAtLeftByStartAndEnd($start, $end)
+    {
+        $this->columnsToRepeatAtLeft = [$start, $end];
+
+        return $this;
+    }
+
+    /**
+     * Is Rows to repeat at top set?
+     *
+     * @return bool
+     */
+    public function isRowsToRepeatAtTopSet()
+    {
+        if (!empty($this->rowsToRepeatAtTop)) {
+            if ($this->rowsToRepeatAtTop[0] != 0 && $this->rowsToRepeatAtTop[1] != 0) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Get Rows to repeat at top.
+     *
+     * @return array Containing start column and end column, empty array if option unset
+     */
+    public function getRowsToRepeatAtTop()
+    {
+        return $this->rowsToRepeatAtTop;
+    }
+
+    /**
+     * Set Rows to repeat at top.
+     *
+     * @param array $rowsToRepeatAtTop Containing start column and end column, empty array if option unset
+     *
+     * @return $this
+     */
+    public function setRowsToRepeatAtTop(array $rowsToRepeatAtTop)
+    {
+        $this->rowsToRepeatAtTop = $rowsToRepeatAtTop;
+
+        return $this;
+    }
+
+    /**
+     * Set Rows to repeat at top by start and end.
+     *
+     * @param int $start eg: 1
+     * @param int $end eg: 1
+     *
+     * @return $this
+     */
+    public function setRowsToRepeatAtTopByStartAndEnd($start, $end)
+    {
+        $this->rowsToRepeatAtTop = [$start, $end];
+
+        return $this;
+    }
+
+    /**
+     * Get center page horizontally.
+     *
+     * @return bool
+     */
+    public function getHorizontalCentered()
+    {
+        return $this->horizontalCentered;
+    }
+
+    /**
+     * Set center page horizontally.
+     *
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function setHorizontalCentered($value)
+    {
+        $this->horizontalCentered = $value;
+
+        return $this;
+    }
+
+    /**
+     * Get center page vertically.
+     *
+     * @return bool
+     */
+    public function getVerticalCentered()
+    {
+        return $this->verticalCentered;
+    }
+
+    /**
+     * Set center page vertically.
+     *
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function setVerticalCentered($value)
+    {
+        $this->verticalCentered = $value;
+
+        return $this;
+    }
+
+    /**
+     * Get print area.
+     *
+     * @param int $index Identifier for a specific print area range if several ranges have been set
+     *                            Default behaviour, or a index value of 0, will return all ranges as a comma-separated string
+     *                            Otherwise, the specific range identified by the value of $index will be returned
+     *                            Print areas are numbered from 1
+     *
+     * @return string
+     */
+    public function getPrintArea($index = 0)
+    {
+        if ($index == 0) {
+            return (string) $this->printArea;
+        }
+        $printAreas = explode(',', (string) $this->printArea);
+        if (isset($printAreas[$index - 1])) {
+            return $printAreas[$index - 1];
+        }
+
+        throw new PhpSpreadsheetException('Requested Print Area does not exist');
+    }
+
+    /**
+     * Is print area set?
+     *
+     * @param int $index Identifier for a specific print area range if several ranges have been set
+     *                            Default behaviour, or an index value of 0, will identify whether any print range is set
+     *                            Otherwise, existence of the range identified by the value of $index will be returned
+     *                            Print areas are numbered from 1
+     *
+     * @return bool
+     */
+    public function isPrintAreaSet($index = 0)
+    {
+        if ($index == 0) {
+            return $this->printArea !== null;
+        }
+        $printAreas = explode(',', (string) $this->printArea);
+
+        return isset($printAreas[$index - 1]);
+    }
+
+    /**
+     * Clear a print area.
+     *
+     * @param int $index Identifier for a specific print area range if several ranges have been set
+     *                            Default behaviour, or an index value of 0, will clear all print ranges that are set
+     *                            Otherwise, the range identified by the value of $index will be removed from the series
+     *                            Print areas are numbered from 1
+     *
+     * @return $this
+     */
+    public function clearPrintArea($index = 0)
+    {
+        if ($index == 0) {
+            $this->printArea = null;
+        } else {
+            $printAreas = explode(',', (string) $this->printArea);
+            if (isset($printAreas[$index - 1])) {
+                unset($printAreas[$index - 1]);
+                $this->printArea = implode(',', $printAreas);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set print area. e.g. 'A1:D10' or 'A1:D10,G5:M20'.
+     *
+     * @param string $value
+     * @param int $index Identifier for a specific print area range allowing several ranges to be set
+     *                            When the method is "O"verwrite, then a positive integer index will overwrite that indexed
+     *                                entry in the print areas list; a negative index value will identify which entry to
+     *                                overwrite working bacward through the print area to the list, with the last entry as -1.
+     *                                Specifying an index value of 0, will overwrite <b>all</b> existing print ranges.
+     *                            When the method is "I"nsert, then a positive index will insert after that indexed entry in
+     *                                the print areas list, while a negative index will insert before the indexed entry.
+     *                                Specifying an index value of 0, will always append the new print range at the end of the
+     *                                list.
+     *                            Print areas are numbered from 1
+     * @param string $method Determines the method used when setting multiple print areas
+     *                            Default behaviour, or the "O" method, overwrites existing print area
+     *                            The "I" method, inserts the new print area before any specified index, or at the end of the list
+     *
+     * @return $this
+     */
+    public function setPrintArea($value, $index = 0, $method = self::SETPRINTRANGE_OVERWRITE)
+    {
+        if (strpos($value, '!') !== false) {
+            throw new PhpSpreadsheetException('Cell coordinate must not specify a worksheet.');
+        } elseif (strpos($value, ':') === false) {
+            throw new PhpSpreadsheetException('Cell coordinate must be a range of cells.');
+        } elseif (strpos($value, '$') !== false) {
+            throw new PhpSpreadsheetException('Cell coordinate must not be absolute.');
+        }
+        $value = strtoupper($value);
+        if (!$this->printArea) {
+            $index = 0;
+        }
+
+        if ($method == self::SETPRINTRANGE_OVERWRITE) {
+            if ($index == 0) {
+                $this->printArea = $value;
+            } else {
+                $printAreas = explode(',', (string) $this->printArea);
+                if ($index < 0) {
+                    $index = count($printAreas) - abs($index) + 1;
+                }
+                if (($index <= 0) || ($index > count($printAreas))) {
+                    throw new PhpSpreadsheetException('Invalid index for setting print range.');
+                }
+                $printAreas[$index - 1] = $value;
+                $this->printArea = implode(',', $printAreas);
+            }
+        } elseif ($method == self::SETPRINTRANGE_INSERT) {
+            if ($index == 0) {
+                $this->printArea = $this->printArea ? ($this->printArea . ',' . $value) : $value;
+            } else {
+                $printAreas = explode(',', (string) $this->printArea);
+                if ($index < 0) {
+                    $index = (int) abs($index) - 1;
+                }
+                if ($index > count($printAreas)) {
+                    throw new PhpSpreadsheetException('Invalid index for setting print range.');
+                }
+                $printAreas = array_merge(array_slice($printAreas, 0, $index), [$value], array_slice($printAreas, $index));
+                $this->printArea = implode(',', $printAreas);
+            }
+        } else {
+            throw new PhpSpreadsheetException('Invalid method for setting print range.');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add a new print area (e.g. 'A1:D10' or 'A1:D10,G5:M20') to the list of print areas.
+     *
+     * @param string $value
+     * @param int $index Identifier for a specific print area range allowing several ranges to be set
+     *                            A positive index will insert after that indexed entry in the print areas list, while a
+     *                                negative index will insert before the indexed entry.
+     *                                Specifying an index value of 0, will always append the new print range at the end of the
+     *                                list.
+     *                            Print areas are numbered from 1
+     *
+     * @return $this
+     */
+    public function addPrintArea($value, $index = -1)
+    {
+        return $this->setPrintArea($value, $index, self::SETPRINTRANGE_INSERT);
+    }
+
+    /**
+     * Set print area.
+     *
+     * @param int $column1 Column 1
+     * @param int $row1 Row 1
+     * @param int $column2 Column 2
+     * @param int $row2 Row 2
+     * @param int $index Identifier for a specific print area range allowing several ranges to be set
+     *                                When the method is "O"verwrite, then a positive integer index will overwrite that indexed
+     *                                    entry in the print areas list; a negative index value will identify which entry to
+     *                                    overwrite working backward through the print area to the list, with the last entry as -1.
+     *                                    Specifying an index value of 0, will overwrite <b>all</b> existing print ranges.
+     *                                When the method is "I"nsert, then a positive index will insert after that indexed entry in
+     *                                    the print areas list, while a negative index will insert before the indexed entry.
+     *                                    Specifying an index value of 0, will always append the new print range at the end of the
+     *                                    list.
+     *                                Print areas are numbered from 1
+     * @param string $method Determines the method used when setting multiple print areas
+     *                                Default behaviour, or the "O" method, overwrites existing print area
+     *                                The "I" method, inserts the new print area before any specified index, or at the end of the list
+     *
+     * @return $this
+     */
+    public function setPrintAreaByColumnAndRow($column1, $row1, $column2, $row2, $index = 0, $method = self::SETPRINTRANGE_OVERWRITE)
+    {
+        return $this->setPrintArea(
+            Coordinate::stringFromColumnIndex($column1) . $row1 . ':' . Coordinate::stringFromColumnIndex($column2) . $row2,
+            $index,
+            $method
+        );
+    }
+
+    /**
+     * Add a new print area to the list of print areas.
+     *
+     * @param int $column1 Start Column for the print area
+     * @param int $row1 Start Row for the print area
+     * @param int $column2 End Column for the print area
+     * @param int $row2 End Row for the print area
+     * @param int $index Identifier for a specific print area range allowing several ranges to be set
+     *                                A positive index will insert after that indexed entry in the print areas list, while a
+     *                                    negative index will insert before the indexed entry.
+     *                                    Specifying an index value of 0, will always append the new print range at the end of the
+     *                                    list.
+     *                                Print areas are numbered from 1
+     *
+     * @return $this
+     */
+    public function addPrintAreaByColumnAndRow($column1, $row1, $column2, $row2, $index = -1)
+    {
+        return $this->setPrintArea(
+            Coordinate::stringFromColumnIndex($column1) . $row1 . ':' . Coordinate::stringFromColumnIndex($column2) . $row2,
+            $index,
+            self::SETPRINTRANGE_INSERT
+        );
+    }
+
+    /**
+     * Get first page number.
+     *
+     * @return ?int
+     */
+    public function getFirstPageNumber()
+    {
+        return $this->firstPageNumber;
+    }
+
+    /**
+     * Set first page number.
+     *
+     * @param ?int $value
+     *
+     * @return $this
+     */
+    public function setFirstPageNumber($value)
+    {
+        $this->firstPageNumber = $value;
+
+        return $this;
+    }
+
+    /**
+     * Reset first page number.
+     *
+     * @return $this
+     */
+    public function resetFirstPageNumber()
+    {
+        return $this->setFirstPageNumber(null);
+    }
+
+    public function getPageOrder(): string
+    {
+        return $this->pageOrder;
+    }
+
+    public function setPageOrder(?string $pageOrder): self
+    {
+        if ($pageOrder === null || $pageOrder === self::PAGEORDER_DOWN_THEN_OVER || $pageOrder === self::PAGEORDER_OVER_THEN_DOWN) {
+            $this->pageOrder = $pageOrder ?? self::PAGEORDER_DOWN_THEN_OVER;
+        }
+
+        return $this;
+    }
+}

+ 517 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Protection.php

@@ -0,0 +1,517 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Shared\PasswordHasher;
+
+class Protection
+{
+    const ALGORITHM_MD2 = 'MD2';
+    const ALGORITHM_MD4 = 'MD4';
+    const ALGORITHM_MD5 = 'MD5';
+    const ALGORITHM_SHA_1 = 'SHA-1';
+    const ALGORITHM_SHA_256 = 'SHA-256';
+    const ALGORITHM_SHA_384 = 'SHA-384';
+    const ALGORITHM_SHA_512 = 'SHA-512';
+    const ALGORITHM_RIPEMD_128 = 'RIPEMD-128';
+    const ALGORITHM_RIPEMD_160 = 'RIPEMD-160';
+    const ALGORITHM_WHIRLPOOL = 'WHIRLPOOL';
+
+    /**
+     * Autofilters are locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $autoFilter;
+
+    /**
+     * Deleting columns is locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $deleteColumns;
+
+    /**
+     * Deleting rows is locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $deleteRows;
+
+    /**
+     * Formatting cells is locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $formatCells;
+
+    /**
+     * Formatting columns is locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $formatColumns;
+
+    /**
+     * Formatting rows is locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $formatRows;
+
+    /**
+     * Inserting columns is locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $insertColumns;
+
+    /**
+     * Inserting hyperlinks is locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $insertHyperlinks;
+
+    /**
+     * Inserting rows is locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $insertRows;
+
+    /**
+     * Objects are locked when sheet is protected, default false.
+     *
+     * @var ?bool
+     */
+    private $objects;
+
+    /**
+     * Pivot tables are locked when the sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $pivotTables;
+
+    /**
+     * Scenarios are locked when sheet is protected, default false.
+     *
+     * @var ?bool
+     */
+    private $scenarios;
+
+    /**
+     * Selection of locked cells is locked when sheet is protected, default false.
+     *
+     * @var ?bool
+     */
+    private $selectLockedCells;
+
+    /**
+     * Selection of unlocked cells is locked when sheet is protected, default false.
+     *
+     * @var ?bool
+     */
+    private $selectUnlockedCells;
+
+    /**
+     * Sheet is locked when sheet is protected, default false.
+     *
+     * @var ?bool
+     */
+    private $sheet;
+
+    /**
+     * Sorting is locked when sheet is protected, default true.
+     *
+     * @var ?bool
+     */
+    private $sort;
+
+    /**
+     * Hashed password.
+     *
+     * @var string
+     */
+    private $password = '';
+
+    /**
+     * Algorithm name.
+     *
+     * @var string
+     */
+    private $algorithm = '';
+
+    /**
+     * Salt value.
+     *
+     * @var string
+     */
+    private $salt = '';
+
+    /**
+     * Spin count.
+     *
+     * @var int
+     */
+    private $spinCount = 10000;
+
+    /**
+     * Create a new Protection.
+     */
+    public function __construct()
+    {
+    }
+
+    /**
+     * Is some sort of protection enabled?
+     */
+    public function isProtectionEnabled(): bool
+    {
+        return
+            $this->password !== '' ||
+            isset($this->sheet) ||
+            isset($this->objects) ||
+            isset($this->scenarios) ||
+            isset($this->formatCells) ||
+            isset($this->formatColumns) ||
+            isset($this->formatRows) ||
+            isset($this->insertColumns) ||
+            isset($this->insertRows) ||
+            isset($this->insertHyperlinks) ||
+            isset($this->deleteColumns) ||
+            isset($this->deleteRows) ||
+            isset($this->selectLockedCells) ||
+            isset($this->sort) ||
+            isset($this->autoFilter) ||
+            isset($this->pivotTables) ||
+            isset($this->selectUnlockedCells);
+    }
+
+    public function getSheet(): ?bool
+    {
+        return $this->sheet;
+    }
+
+    public function setSheet(?bool $sheet): self
+    {
+        $this->sheet = $sheet;
+
+        return $this;
+    }
+
+    public function getObjects(): ?bool
+    {
+        return $this->objects;
+    }
+
+    public function setObjects(?bool $objects): self
+    {
+        $this->objects = $objects;
+
+        return $this;
+    }
+
+    public function getScenarios(): ?bool
+    {
+        return $this->scenarios;
+    }
+
+    public function setScenarios(?bool $scenarios): self
+    {
+        $this->scenarios = $scenarios;
+
+        return $this;
+    }
+
+    public function getFormatCells(): ?bool
+    {
+        return $this->formatCells;
+    }
+
+    public function setFormatCells(?bool $formatCells): self
+    {
+        $this->formatCells = $formatCells;
+
+        return $this;
+    }
+
+    public function getFormatColumns(): ?bool
+    {
+        return $this->formatColumns;
+    }
+
+    public function setFormatColumns(?bool $formatColumns): self
+    {
+        $this->formatColumns = $formatColumns;
+
+        return $this;
+    }
+
+    public function getFormatRows(): ?bool
+    {
+        return $this->formatRows;
+    }
+
+    public function setFormatRows(?bool $formatRows): self
+    {
+        $this->formatRows = $formatRows;
+
+        return $this;
+    }
+
+    public function getInsertColumns(): ?bool
+    {
+        return $this->insertColumns;
+    }
+
+    public function setInsertColumns(?bool $insertColumns): self
+    {
+        $this->insertColumns = $insertColumns;
+
+        return $this;
+    }
+
+    public function getInsertRows(): ?bool
+    {
+        return $this->insertRows;
+    }
+
+    public function setInsertRows(?bool $insertRows): self
+    {
+        $this->insertRows = $insertRows;
+
+        return $this;
+    }
+
+    public function getInsertHyperlinks(): ?bool
+    {
+        return $this->insertHyperlinks;
+    }
+
+    public function setInsertHyperlinks(?bool $insertHyperLinks): self
+    {
+        $this->insertHyperlinks = $insertHyperLinks;
+
+        return $this;
+    }
+
+    public function getDeleteColumns(): ?bool
+    {
+        return $this->deleteColumns;
+    }
+
+    public function setDeleteColumns(?bool $deleteColumns): self
+    {
+        $this->deleteColumns = $deleteColumns;
+
+        return $this;
+    }
+
+    public function getDeleteRows(): ?bool
+    {
+        return $this->deleteRows;
+    }
+
+    public function setDeleteRows(?bool $deleteRows): self
+    {
+        $this->deleteRows = $deleteRows;
+
+        return $this;
+    }
+
+    public function getSelectLockedCells(): ?bool
+    {
+        return $this->selectLockedCells;
+    }
+
+    public function setSelectLockedCells(?bool $selectLockedCells): self
+    {
+        $this->selectLockedCells = $selectLockedCells;
+
+        return $this;
+    }
+
+    public function getSort(): ?bool
+    {
+        return $this->sort;
+    }
+
+    public function setSort(?bool $sort): self
+    {
+        $this->sort = $sort;
+
+        return $this;
+    }
+
+    public function getAutoFilter(): ?bool
+    {
+        return $this->autoFilter;
+    }
+
+    public function setAutoFilter(?bool $autoFilter): self
+    {
+        $this->autoFilter = $autoFilter;
+
+        return $this;
+    }
+
+    public function getPivotTables(): ?bool
+    {
+        return $this->pivotTables;
+    }
+
+    public function setPivotTables(?bool $pivotTables): self
+    {
+        $this->pivotTables = $pivotTables;
+
+        return $this;
+    }
+
+    public function getSelectUnlockedCells(): ?bool
+    {
+        return $this->selectUnlockedCells;
+    }
+
+    public function setSelectUnlockedCells(?bool $selectUnlockedCells): self
+    {
+        $this->selectUnlockedCells = $selectUnlockedCells;
+
+        return $this;
+    }
+
+    /**
+     * Get hashed password.
+     *
+     * @return string
+     */
+    public function getPassword()
+    {
+        return $this->password;
+    }
+
+    /**
+     * Set Password.
+     *
+     * @param string $password
+     * @param bool $alreadyHashed If the password has already been hashed, set this to true
+     *
+     * @return $this
+     */
+    public function setPassword($password, $alreadyHashed = false)
+    {
+        if (!$alreadyHashed) {
+            $salt = $this->generateSalt();
+            $this->setSalt($salt);
+            $password = PasswordHasher::hashPassword($password, $this->getAlgorithm(), $this->getSalt(), $this->getSpinCount());
+        }
+
+        $this->password = $password;
+
+        return $this;
+    }
+
+    public function setHashValue(string $password): self
+    {
+        return $this->setPassword($password, true);
+    }
+
+    /**
+     * Create a pseudorandom string.
+     */
+    private function generateSalt(): string
+    {
+        return base64_encode(random_bytes(16));
+    }
+
+    /**
+     * Get algorithm name.
+     */
+    public function getAlgorithm(): string
+    {
+        return $this->algorithm;
+    }
+
+    /**
+     * Set algorithm name.
+     */
+    public function setAlgorithm(string $algorithm): self
+    {
+        return $this->setAlgorithmName($algorithm);
+    }
+
+    /**
+     * Set algorithm name.
+     */
+    public function setAlgorithmName(string $algorithm): self
+    {
+        $this->algorithm = $algorithm;
+
+        return $this;
+    }
+
+    public function getSalt(): string
+    {
+        return $this->salt;
+    }
+
+    public function setSalt(string $salt): self
+    {
+        return $this->setSaltValue($salt);
+    }
+
+    public function setSaltValue(string $salt): self
+    {
+        $this->salt = $salt;
+
+        return $this;
+    }
+
+    /**
+     * Get spin count.
+     */
+    public function getSpinCount(): int
+    {
+        return $this->spinCount;
+    }
+
+    /**
+     * Set spin count.
+     */
+    public function setSpinCount(int $spinCount): self
+    {
+        $this->spinCount = $spinCount;
+
+        return $this;
+    }
+
+    /**
+     * Verify that the given non-hashed password can "unlock" the protection.
+     */
+    public function verify(string $password): bool
+    {
+        if ($this->password === '') {
+            return true;
+        }
+
+        $hash = PasswordHasher::hashPassword($password, $this->getAlgorithm(), $this->getSalt(), $this->getSpinCount());
+
+        return $this->getPassword() === $hash;
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $value) {
+            if (is_object($value)) {
+                $this->$key = clone $value;
+            } else {
+                $this->$key = $value;
+            }
+        }
+    }
+}

+ 120 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Row.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+class Row
+{
+    /**
+     * \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet.
+     *
+     * @var Worksheet
+     */
+    private $worksheet;
+
+    /**
+     * Row index.
+     *
+     * @var int
+     */
+    private $rowIndex = 0;
+
+    /**
+     * Create a new row.
+     *
+     * @param int $rowIndex
+     */
+    public function __construct(Worksheet $worksheet, $rowIndex = 1)
+    {
+        // Set parent and row index
+        $this->worksheet = $worksheet;
+        $this->rowIndex = $rowIndex;
+    }
+
+    /**
+     * Destructor.
+     */
+    public function __destruct()
+    {
+        $this->worksheet = null; // @phpstan-ignore-line
+    }
+
+    /**
+     * Get row index.
+     */
+    public function getRowIndex(): int
+    {
+        return $this->rowIndex;
+    }
+
+    /**
+     * Get cell iterator.
+     *
+     * @param string $startColumn The column address at which to start iterating
+     * @param string $endColumn Optionally, the column address at which to stop iterating
+     */
+    public function getCellIterator($startColumn = 'A', $endColumn = null): RowCellIterator
+    {
+        return new RowCellIterator($this->worksheet, $this->rowIndex, $startColumn, $endColumn);
+    }
+
+    /**
+     * Get column iterator. Synonym for getCellIterator().
+     *
+     * @param string $startColumn The column address at which to start iterating
+     * @param string $endColumn Optionally, the column address at which to stop iterating
+     */
+    public function getColumnIterator($startColumn = 'A', $endColumn = null): RowCellIterator
+    {
+        return $this->getCellIterator($startColumn, $endColumn);
+    }
+
+    /**
+     * Returns a boolean true if the row contains no cells. By default, this means that no cell records exist in the
+     *         collection for this row. false will be returned otherwise.
+     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
+     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
+     *                  cells, then the row will be considered empty.
+     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
+     *                  string value cells, then the row will be considered empty.
+     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
+     *                  If the only cells in the collection are null value or empty string value cells, then the row
+     *                  will be considered empty.
+     *
+     * @param int $definitionOfEmptyFlags
+     *              Possible Flag Values are:
+     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
+     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
+     * @param string $startColumn The column address at which to start checking if cells are empty
+     * @param string $endColumn Optionally, the column address at which to stop checking if cells are empty
+     */
+    public function isEmpty(int $definitionOfEmptyFlags = 0, $startColumn = 'A', $endColumn = null): bool
+    {
+        $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL);
+        $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL);
+
+        $cellIterator = $this->getCellIterator($startColumn, $endColumn);
+        $cellIterator->setIterateOnlyExistingCells(true);
+        foreach ($cellIterator as $cell) {
+            /** @scrutinizer ignore-call */
+            $value = $cell->getValue();
+            if ($value === null && $nullValueCellIsEmpty === true) {
+                continue;
+            }
+            if ($value === '' && $emptyStringCellIsEmpty === true) {
+                continue;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns bound worksheet.
+     */
+    public function getWorksheet(): Worksheet
+    {
+        return $this->worksheet;
+    }
+}

+ 195 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php

@@ -0,0 +1,195 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+
+/**
+ * @extends CellIterator<string>
+ */
+class RowCellIterator extends CellIterator
+{
+    /**
+     * Current iterator position.
+     *
+     * @var int
+     */
+    private $currentColumnIndex;
+
+    /**
+     * Row index.
+     *
+     * @var int
+     */
+    private $rowIndex = 1;
+
+    /**
+     * Start position.
+     *
+     * @var int
+     */
+    private $startColumnIndex = 1;
+
+    /**
+     * End position.
+     *
+     * @var int
+     */
+    private $endColumnIndex = 1;
+
+    /**
+     * Create a new column iterator.
+     *
+     * @param Worksheet $worksheet The worksheet to iterate over
+     * @param int $rowIndex The row that we want to iterate
+     * @param string $startColumn The column address at which to start iterating
+     * @param string $endColumn Optionally, the column address at which to stop iterating
+     */
+    public function __construct(Worksheet $worksheet, $rowIndex = 1, $startColumn = 'A', $endColumn = null)
+    {
+        // Set subject and row index
+        $this->worksheet = $worksheet;
+        $this->cellCollection = $worksheet->getCellCollection();
+        $this->rowIndex = $rowIndex;
+        $this->resetEnd($endColumn);
+        $this->resetStart($startColumn);
+    }
+
+    /**
+     * (Re)Set the start column and the current column pointer.
+     *
+     * @param string $startColumn The column address at which to start iterating
+     *
+     * @return $this
+     */
+    public function resetStart(string $startColumn = 'A')
+    {
+        $this->startColumnIndex = Coordinate::columnIndexFromString($startColumn);
+        $this->adjustForExistingOnlyRange();
+        $this->seek(Coordinate::stringFromColumnIndex($this->startColumnIndex));
+
+        return $this;
+    }
+
+    /**
+     * (Re)Set the end column.
+     *
+     * @param string $endColumn The column address at which to stop iterating
+     *
+     * @return $this
+     */
+    public function resetEnd($endColumn = null)
+    {
+        $endColumn = $endColumn ?: $this->worksheet->getHighestColumn();
+        $this->endColumnIndex = Coordinate::columnIndexFromString($endColumn);
+        $this->adjustForExistingOnlyRange();
+
+        return $this;
+    }
+
+    /**
+     * Set the column pointer to the selected column.
+     *
+     * @param string $column The column address to set the current pointer at
+     *
+     * @return $this
+     */
+    public function seek(string $column = 'A')
+    {
+        $columnId = Coordinate::columnIndexFromString($column);
+        if ($this->onlyExistingCells && !($this->cellCollection->has($column . $this->rowIndex))) {
+            throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist');
+        }
+        if (($columnId < $this->startColumnIndex) || ($columnId > $this->endColumnIndex)) {
+            throw new PhpSpreadsheetException("Column $column is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})");
+        }
+        $this->currentColumnIndex = $columnId;
+
+        return $this;
+    }
+
+    /**
+     * Rewind the iterator to the starting column.
+     */
+    public function rewind(): void
+    {
+        $this->currentColumnIndex = $this->startColumnIndex;
+    }
+
+    /**
+     * Return the current cell in this worksheet row.
+     */
+    public function current(): ?Cell
+    {
+        $cellAddress = Coordinate::stringFromColumnIndex($this->currentColumnIndex) . $this->rowIndex;
+
+        return $this->cellCollection->has($cellAddress)
+            ? $this->cellCollection->get($cellAddress)
+            : (
+                $this->ifNotExists === self::IF_NOT_EXISTS_CREATE_NEW
+                ? $this->worksheet->createNewCell($cellAddress)
+                : null
+            );
+    }
+
+    /**
+     * Return the current iterator key.
+     */
+    public function key(): string
+    {
+        return Coordinate::stringFromColumnIndex($this->currentColumnIndex);
+    }
+
+    /**
+     * Set the iterator to its next value.
+     */
+    public function next(): void
+    {
+        do {
+            ++$this->currentColumnIndex;
+        } while (($this->onlyExistingCells) && (!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->currentColumnIndex) . $this->rowIndex)) && ($this->currentColumnIndex <= $this->endColumnIndex));
+    }
+
+    /**
+     * Set the iterator to its previous value.
+     */
+    public function prev(): void
+    {
+        do {
+            --$this->currentColumnIndex;
+        } while (($this->onlyExistingCells) && (!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->currentColumnIndex) . $this->rowIndex)) && ($this->currentColumnIndex >= $this->startColumnIndex));
+    }
+
+    /**
+     * Indicate if more columns exist in the worksheet range of columns that we're iterating.
+     */
+    public function valid(): bool
+    {
+        return $this->currentColumnIndex <= $this->endColumnIndex && $this->currentColumnIndex >= $this->startColumnIndex;
+    }
+
+    /**
+     * Return the current iterator position.
+     */
+    public function getCurrentColumnIndex(): int
+    {
+        return $this->currentColumnIndex;
+    }
+
+    /**
+     * Validate start/end values for "IterateOnlyExistingCells" mode, and adjust if necessary.
+     */
+    protected function adjustForExistingOnlyRange(): void
+    {
+        if ($this->onlyExistingCells) {
+            while ((!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->startColumnIndex) . $this->rowIndex)) && ($this->startColumnIndex <= $this->endColumnIndex)) {
+                ++$this->startColumnIndex;
+            }
+            while ((!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->endColumnIndex) . $this->rowIndex)) && ($this->endColumnIndex >= $this->startColumnIndex)) {
+                --$this->endColumnIndex;
+            }
+        }
+    }
+}

+ 118 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowDimension.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension;
+
+class RowDimension extends Dimension
+{
+    /**
+     * Row index.
+     *
+     * @var ?int
+     */
+    private $rowIndex;
+
+    /**
+     * Row height (in pt).
+     *
+     * When this is set to a negative value, the row height should be ignored by IWriter
+     *
+     * @var float
+     */
+    private $height = -1;
+
+    /**
+     * ZeroHeight for Row?
+     *
+     * @var bool
+     */
+    private $zeroHeight = false;
+
+    /**
+     * Create a new RowDimension.
+     *
+     * @param ?int $index Numeric row index
+     */
+    public function __construct($index = 0)
+    {
+        // Initialise values
+        $this->rowIndex = $index;
+
+        // set dimension as unformatted by default
+        parent::__construct(null);
+    }
+
+    /**
+     * Get Row Index.
+     */
+    public function getRowIndex(): ?int
+    {
+        return $this->rowIndex;
+    }
+
+    /**
+     * Set Row Index.
+     *
+     * @return $this
+     */
+    public function setRowIndex(int $index)
+    {
+        $this->rowIndex = $index;
+
+        return $this;
+    }
+
+    /**
+     * Get Row Height.
+     * By default, this will be in points; but this method also accepts an optional unit of measure
+     *    argument, and will convert the value from points to the specified UoM.
+     *    A value of -1 tells Excel to display this column in its default height.
+     *
+     * @return float
+     */
+    public function getRowHeight(?string $unitOfMeasure = null)
+    {
+        return ($unitOfMeasure === null || $this->height < 0)
+            ? $this->height
+            : (new CssDimension($this->height . CssDimension::UOM_POINTS))->toUnit($unitOfMeasure);
+    }
+
+    /**
+     * Set Row Height.
+     *
+     * @param float $height in points. A value of -1 tells Excel to display this column in its default height.
+     * By default, this will be the passed argument value; but this method also accepts an optional unit of measure
+     *    argument, and will convert the passed argument value to points from the specified UoM
+     *
+     * @return $this
+     */
+    public function setRowHeight($height, ?string $unitOfMeasure = null)
+    {
+        $this->height = ($unitOfMeasure === null || $height < 0)
+            ? $height
+            : (new CssDimension("{$height}{$unitOfMeasure}"))->height();
+
+        return $this;
+    }
+
+    /**
+     * Get ZeroHeight.
+     */
+    public function getZeroHeight(): bool
+    {
+        return $this->zeroHeight;
+    }
+
+    /**
+     * Set ZeroHeight.
+     *
+     * @return $this
+     */
+    public function setZeroHeight(bool $zeroHeight)
+    {
+        $this->zeroHeight = $zeroHeight;
+
+        return $this;
+    }
+}

+ 163 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowIterator.php

@@ -0,0 +1,163 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use Iterator as NativeIterator;
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+
+/**
+ * @implements NativeIterator<int, Row>
+ */
+class RowIterator implements NativeIterator
+{
+    /**
+     * Worksheet to iterate.
+     *
+     * @var Worksheet
+     */
+    private $subject;
+
+    /**
+     * Current iterator position.
+     *
+     * @var int
+     */
+    private $position = 1;
+
+    /**
+     * Start position.
+     *
+     * @var int
+     */
+    private $startRow = 1;
+
+    /**
+     * End position.
+     *
+     * @var int
+     */
+    private $endRow = 1;
+
+    /**
+     * Create a new row iterator.
+     *
+     * @param Worksheet $subject The worksheet to iterate over
+     * @param int $startRow The row number at which to start iterating
+     * @param int $endRow Optionally, the row number at which to stop iterating
+     */
+    public function __construct(Worksheet $subject, $startRow = 1, $endRow = null)
+    {
+        // Set subject
+        $this->subject = $subject;
+        $this->resetEnd($endRow);
+        $this->resetStart($startRow);
+    }
+
+    public function __destruct()
+    {
+        $this->subject = null; // @phpstan-ignore-line
+    }
+
+    /**
+     * (Re)Set the start row and the current row pointer.
+     *
+     * @param int $startRow The row number at which to start iterating
+     *
+     * @return $this
+     */
+    public function resetStart(int $startRow = 1)
+    {
+        if ($startRow > $this->subject->getHighestRow()) {
+            throw new PhpSpreadsheetException(
+                "Start row ({$startRow}) is beyond highest row ({$this->subject->getHighestRow()})"
+            );
+        }
+
+        $this->startRow = $startRow;
+        if ($this->endRow < $this->startRow) {
+            $this->endRow = $this->startRow;
+        }
+        $this->seek($startRow);
+
+        return $this;
+    }
+
+    /**
+     * (Re)Set the end row.
+     *
+     * @param int $endRow The row number at which to stop iterating
+     *
+     * @return $this
+     */
+    public function resetEnd($endRow = null)
+    {
+        $this->endRow = $endRow ?: $this->subject->getHighestRow();
+
+        return $this;
+    }
+
+    /**
+     * Set the row pointer to the selected row.
+     *
+     * @param int $row The row number to set the current pointer at
+     *
+     * @return $this
+     */
+    public function seek(int $row = 1)
+    {
+        if (($row < $this->startRow) || ($row > $this->endRow)) {
+            throw new PhpSpreadsheetException("Row $row is out of range ({$this->startRow} - {$this->endRow})");
+        }
+        $this->position = $row;
+
+        return $this;
+    }
+
+    /**
+     * Rewind the iterator to the starting row.
+     */
+    public function rewind(): void
+    {
+        $this->position = $this->startRow;
+    }
+
+    /**
+     * Return the current row in this worksheet.
+     */
+    public function current(): Row
+    {
+        return new Row($this->subject, $this->position);
+    }
+
+    /**
+     * Return the current iterator key.
+     */
+    public function key(): int
+    {
+        return $this->position;
+    }
+
+    /**
+     * Set the iterator to its next value.
+     */
+    public function next(): void
+    {
+        ++$this->position;
+    }
+
+    /**
+     * Set the iterator to its previous value.
+     */
+    public function prev(): void
+    {
+        --$this->position;
+    }
+
+    /**
+     * Indicate if more rows exist in the worksheet range of rows that we're iterating.
+     */
+    public function valid(): bool
+    {
+        return $this->position <= $this->endRow && $this->position >= $this->startRow;
+    }
+}

+ 178 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/SheetView.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+
+class SheetView
+{
+    // Sheet View types
+    const SHEETVIEW_NORMAL = 'normal';
+    const SHEETVIEW_PAGE_LAYOUT = 'pageLayout';
+    const SHEETVIEW_PAGE_BREAK_PREVIEW = 'pageBreakPreview';
+
+    private const SHEET_VIEW_TYPES = [
+        self::SHEETVIEW_NORMAL,
+        self::SHEETVIEW_PAGE_LAYOUT,
+        self::SHEETVIEW_PAGE_BREAK_PREVIEW,
+    ];
+
+    /**
+     * ZoomScale.
+     *
+     * Valid values range from 10 to 400.
+     *
+     * @var ?int
+     */
+    private $zoomScale = 100;
+
+    /**
+     * ZoomScaleNormal.
+     *
+     * Valid values range from 10 to 400.
+     *
+     * @var ?int
+     */
+    private $zoomScaleNormal = 100;
+
+    /**
+     * ShowZeros.
+     *
+     * If true, "null" values from a calculation will be shown as "0". This is the default Excel behaviour and can be changed
+     * with the advanced worksheet option "Show a zero in cells that have zero value"
+     *
+     * @var bool
+     */
+    private $showZeros = true;
+
+    /**
+     * View.
+     *
+     * Valid values range from 10 to 400.
+     *
+     * @var string
+     */
+    private $sheetviewType = self::SHEETVIEW_NORMAL;
+
+    /**
+     * Create a new SheetView.
+     */
+    public function __construct()
+    {
+    }
+
+    /**
+     * Get ZoomScale.
+     *
+     * @return ?int
+     */
+    public function getZoomScale()
+    {
+        return $this->zoomScale;
+    }
+
+    /**
+     * Set ZoomScale.
+     * Valid values range from 10 to 400.
+     *
+     * @param ?int $zoomScale
+     *
+     * @return $this
+     */
+    public function setZoomScale($zoomScale)
+    {
+        // Microsoft Office Excel 2007 only allows setting a scale between 10 and 400 via the user interface,
+        // but it is apparently still able to handle any scale >= 1
+        if ($zoomScale === null || $zoomScale >= 1) {
+            $this->zoomScale = $zoomScale;
+        } else {
+            throw new PhpSpreadsheetException('Scale must be greater than or equal to 1.');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get ZoomScaleNormal.
+     *
+     * @return ?int
+     */
+    public function getZoomScaleNormal()
+    {
+        return $this->zoomScaleNormal;
+    }
+
+    /**
+     * Set ZoomScale.
+     * Valid values range from 10 to 400.
+     *
+     * @param ?int $zoomScaleNormal
+     *
+     * @return $this
+     */
+    public function setZoomScaleNormal($zoomScaleNormal)
+    {
+        if ($zoomScaleNormal === null || $zoomScaleNormal >= 1) {
+            $this->zoomScaleNormal = $zoomScaleNormal;
+        } else {
+            throw new PhpSpreadsheetException('Scale must be greater than or equal to 1.');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set ShowZeroes setting.
+     *
+     * @param bool $showZeros
+     */
+    public function setShowZeros($showZeros): void
+    {
+        $this->showZeros = $showZeros;
+    }
+
+    /**
+     * @return bool
+     */
+    public function getShowZeros()
+    {
+        return $this->showZeros;
+    }
+
+    /**
+     * Get View.
+     *
+     * @return string
+     */
+    public function getView()
+    {
+        return $this->sheetviewType;
+    }
+
+    /**
+     * Set View.
+     *
+     * Valid values are
+     *        'normal'            self::SHEETVIEW_NORMAL
+     *        'pageLayout'        self::SHEETVIEW_PAGE_LAYOUT
+     *        'pageBreakPreview'  self::SHEETVIEW_PAGE_BREAK_PREVIEW
+     *
+     * @param ?string $sheetViewType
+     *
+     * @return $this
+     */
+    public function setView($sheetViewType)
+    {
+        // MS Excel 2007 allows setting the view to 'normal', 'pageLayout' or 'pageBreakPreview' via the user interface
+        if ($sheetViewType === null) {
+            $sheetViewType = self::SHEETVIEW_NORMAL;
+        }
+        if (in_array($sheetViewType, self::SHEET_VIEW_TYPES)) {
+            $this->sheetviewType = $sheetViewType;
+        } else {
+            throw new PhpSpreadsheetException('Invalid sheetview layout type.');
+        }
+
+        return $this;
+    }
+}

+ 585 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php

@@ -0,0 +1,585 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
+
+class Table
+{
+    /**
+     * Table Name.
+     *
+     * @var string
+     */
+    private $name;
+
+    /**
+     * Show Header Row.
+     *
+     * @var bool
+     */
+    private $showHeaderRow = true;
+
+    /**
+     * Show Totals Row.
+     *
+     * @var bool
+     */
+    private $showTotalsRow = false;
+
+    /**
+     * Table Range.
+     *
+     * @var string
+     */
+    private $range = '';
+
+    /**
+     * Table Worksheet.
+     *
+     * @var null|Worksheet
+     */
+    private $workSheet;
+
+    /**
+     * Table allow filter.
+     *
+     * @var bool
+     */
+    private $allowFilter = true;
+
+    /**
+     * Table Column.
+     *
+     * @var Table\Column[]
+     */
+    private $columns = [];
+
+    /**
+     * Table Style.
+     *
+     * @var TableStyle
+     */
+    private $style;
+
+    /**
+     * Table AutoFilter.
+     *
+     * @var AutoFilter
+     */
+    private $autoFilter;
+
+    /**
+     * Create a new Table.
+     *
+     * @param AddressRange|array<int>|string $range
+     *            A simple string containing a Cell range like 'A1:E10' is permitted
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or an AddressRange object.
+     * @param string $name (e.g. Table1)
+     */
+    public function __construct($range = '', string $name = '')
+    {
+        $this->style = new TableStyle();
+        $this->autoFilter = new AutoFilter($range);
+        $this->setRange($range);
+        $this->setName($name);
+    }
+
+    /**
+     * Get Table name.
+     */
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    /**
+     * Set Table name.
+     *
+     * @throws PhpSpreadsheetException
+     */
+    public function setName(string $name): self
+    {
+        $name = trim($name);
+
+        if (!empty($name)) {
+            if (strlen($name) === 1 && in_array($name, ['C', 'c', 'R', 'r'])) {
+                throw new PhpSpreadsheetException('The table name is invalid');
+            }
+            if (StringHelper::countCharacters($name) > 255) {
+                throw new PhpSpreadsheetException('The table name cannot be longer than 255 characters');
+            }
+            // Check for A1 or R1C1 cell reference notation
+            if (
+                preg_match(Coordinate::A1_COORDINATE_REGEX, $name) ||
+                preg_match('/^R\[?\-?[0-9]*\]?C\[?\-?[0-9]*\]?$/i', $name)
+            ) {
+                throw new PhpSpreadsheetException('The table name can\'t be the same as a cell reference');
+            }
+            if (!preg_match('/^[\p{L}_\\\\]/iu', $name)) {
+                throw new PhpSpreadsheetException('The table name must begin a name with a letter, an underscore character (_), or a backslash (\)');
+            }
+            if (!preg_match('/^[\p{L}_\\\\][\p{L}\p{M}0-9\._]+$/iu', $name)) {
+                throw new PhpSpreadsheetException('The table name contains invalid characters');
+            }
+
+            $this->checkForDuplicateTableNames($name, $this->workSheet);
+            $this->updateStructuredReferences($name);
+        }
+
+        $this->name = $name;
+
+        return $this;
+    }
+
+    /**
+     * @throws PhpSpreadsheetException
+     */
+    private function checkForDuplicateTableNames(string $name, ?Worksheet $worksheet): void
+    {
+        // Remember that table names are case-insensitive
+        $tableName = StringHelper::strToLower($name);
+
+        if ($worksheet !== null && StringHelper::strToLower($this->name) !== $name) {
+            $spreadsheet = $worksheet->getParentOrThrow();
+
+            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
+                foreach ($sheet->getTableCollection() as $table) {
+                    if (StringHelper::strToLower($table->getName()) === $tableName && $table != $this) {
+                        throw new PhpSpreadsheetException("Spreadsheet already contains a table named '{$this->name}'");
+                    }
+                }
+            }
+        }
+    }
+
+    private function updateStructuredReferences(string $name): void
+    {
+        if ($this->workSheet === null || $this->name === null || $this->name === '') {
+            return;
+        }
+
+        // Remember that table names are case-insensitive
+        if (StringHelper::strToLower($this->name) !== StringHelper::strToLower($name)) {
+            // We need to check all formula cells that might contain fully-qualified Structured References
+            //    that refer to this table, and update those formulae to reference the new table name
+            $spreadsheet = $this->workSheet->getParentOrThrow();
+            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
+                $this->updateStructuredReferencesInCells($sheet, $name);
+            }
+            $this->updateStructuredReferencesInNamedFormulae($spreadsheet, $name);
+        }
+    }
+
+    private function updateStructuredReferencesInCells(Worksheet $worksheet, string $newName): void
+    {
+        $pattern = '/' . preg_quote($this->name, '/') . '\[/mui';
+
+        foreach ($worksheet->getCoordinates(false) as $coordinate) {
+            $cell = $worksheet->getCell($coordinate);
+            if ($cell->getDataType() === DataType::TYPE_FORMULA) {
+                $formula = $cell->getValue();
+                if (preg_match($pattern, $formula) === 1) {
+                    $formula = preg_replace($pattern, "{$newName}[", $formula);
+                    $cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
+                }
+            }
+        }
+    }
+
+    private function updateStructuredReferencesInNamedFormulae(Spreadsheet $spreadsheet, string $newName): void
+    {
+        $pattern = '/' . preg_quote($this->name, '/') . '\[/mui';
+
+        foreach ($spreadsheet->getNamedFormulae() as $namedFormula) {
+            $formula = $namedFormula->getValue();
+            if (preg_match($pattern, $formula) === 1) {
+                $formula = preg_replace($pattern, "{$newName}[", $formula);
+                $namedFormula->setValue($formula); // @phpstan-ignore-line
+            }
+        }
+    }
+
+    /**
+     * Get show Header Row.
+     */
+    public function getShowHeaderRow(): bool
+    {
+        return $this->showHeaderRow;
+    }
+
+    /**
+     * Set show Header Row.
+     */
+    public function setShowHeaderRow(bool $showHeaderRow): self
+    {
+        $this->showHeaderRow = $showHeaderRow;
+
+        return $this;
+    }
+
+    /**
+     * Get show Totals Row.
+     */
+    public function getShowTotalsRow(): bool
+    {
+        return $this->showTotalsRow;
+    }
+
+    /**
+     * Set show Totals Row.
+     */
+    public function setShowTotalsRow(bool $showTotalsRow): self
+    {
+        $this->showTotalsRow = $showTotalsRow;
+
+        return $this;
+    }
+
+    /**
+     * Get allow filter.
+     * If false, autofiltering is disabled for the table, if true it is enabled.
+     */
+    public function getAllowFilter(): bool
+    {
+        return $this->allowFilter;
+    }
+
+    /**
+     * Set show Autofiltering.
+     * Disabling autofiltering has the same effect as hiding the filter button on all the columns in the table.
+     */
+    public function setAllowFilter(bool $allowFilter): self
+    {
+        $this->allowFilter = $allowFilter;
+
+        return $this;
+    }
+
+    /**
+     * Get Table Range.
+     */
+    public function getRange(): string
+    {
+        return $this->range;
+    }
+
+    /**
+     * Set Table Cell Range.
+     *
+     * @param AddressRange|array<int>|string $range
+     *            A simple string containing a Cell range like 'A1:E10' is permitted
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or an AddressRange object.
+     */
+    public function setRange($range = ''): self
+    {
+        // extract coordinate
+        if ($range !== '') {
+            [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
+        }
+        if (empty($range)) {
+            //    Discard all column rules
+            $this->columns = [];
+            $this->range = '';
+
+            return $this;
+        }
+
+        if (strpos($range, ':') === false) {
+            throw new PhpSpreadsheetException('Table must be set on a range of cells.');
+        }
+
+        [$width, $height] = Coordinate::rangeDimension($range);
+        if ($width < 1 || $height < 1) {
+            throw new PhpSpreadsheetException('The table range must be at least 1 column and row');
+        }
+
+        $this->range = $range;
+        $this->autoFilter->setRange($range);
+
+        //    Discard any column rules that are no longer valid within this range
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+        foreach ($this->columns as $key => $value) {
+            $colIndex = Coordinate::columnIndexFromString($key);
+            if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
+                unset($this->columns[$key]);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set Table Cell Range to max row.
+     */
+    public function setRangeToMaxRow(): self
+    {
+        if ($this->workSheet !== null) {
+            $thisrange = $this->range;
+            $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange);
+            if ($range !== $thisrange) {
+                $this->setRange($range);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get Table's Worksheet.
+     */
+    public function getWorksheet(): ?Worksheet
+    {
+        return $this->workSheet;
+    }
+
+    /**
+     * Set Table's Worksheet.
+     */
+    public function setWorksheet(?Worksheet $worksheet = null): self
+    {
+        if ($this->name !== '' && $worksheet !== null) {
+            $spreadsheet = $worksheet->getParentOrThrow();
+            $tableName = StringHelper::strToUpper($this->name);
+
+            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
+                foreach ($sheet->getTableCollection() as $table) {
+                    if (StringHelper::strToUpper($table->getName()) === $tableName) {
+                        throw new PhpSpreadsheetException("Workbook already contains a table named '{$this->name}'");
+                    }
+                }
+            }
+        }
+
+        $this->workSheet = $worksheet;
+        $this->autoFilter->setParent($worksheet);
+
+        return $this;
+    }
+
+    /**
+     * Get all Table Columns.
+     *
+     * @return Table\Column[]
+     */
+    public function getColumns(): array
+    {
+        return $this->columns;
+    }
+
+    /**
+     * Validate that the specified column is in the Table range.
+     *
+     * @param string $column Column name (e.g. A)
+     *
+     * @return int The column offset within the table range
+     */
+    public function isColumnInRange(string $column): int
+    {
+        if (empty($this->range)) {
+            throw new PhpSpreadsheetException('No table range is defined.');
+        }
+
+        $columnIndex = Coordinate::columnIndexFromString($column);
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+        if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
+            throw new PhpSpreadsheetException('Column is outside of current table range.');
+        }
+
+        return $columnIndex - $rangeStart[0];
+    }
+
+    /**
+     * Get a specified Table Column Offset within the defined Table range.
+     *
+     * @param string $column Column name (e.g. A)
+     *
+     * @return int The offset of the specified column within the table range
+     */
+    public function getColumnOffset($column): int
+    {
+        return $this->isColumnInRange($column);
+    }
+
+    /**
+     * Get a specified Table Column.
+     *
+     * @param string $column Column name (e.g. A)
+     */
+    public function getColumn($column): Table\Column
+    {
+        $this->isColumnInRange($column);
+
+        if (!isset($this->columns[$column])) {
+            $this->columns[$column] = new Table\Column($column, $this);
+        }
+
+        return $this->columns[$column];
+    }
+
+    /**
+     * Get a specified Table Column by it's offset.
+     *
+     * @param int $columnOffset Column offset within range (starting from 0)
+     */
+    public function getColumnByOffset($columnOffset): Table\Column
+    {
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
+        $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset);
+
+        return $this->getColumn($pColumn);
+    }
+
+    /**
+     * Set Table.
+     *
+     * @param string|Table\Column $columnObjectOrString
+     *            A simple string containing a Column ID like 'A' is permitted
+     */
+    public function setColumn($columnObjectOrString): self
+    {
+        if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {
+            $column = $columnObjectOrString;
+        } elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof Table\Column)) {
+            $column = $columnObjectOrString->getColumnIndex();
+        } else {
+            throw new PhpSpreadsheetException('Column is not within the table range.');
+        }
+        $this->isColumnInRange($column);
+
+        if (is_string($columnObjectOrString)) {
+            $this->columns[$columnObjectOrString] = new Table\Column($columnObjectOrString, $this);
+        } else {
+            $columnObjectOrString->setTable($this);
+            $this->columns[$column] = $columnObjectOrString;
+        }
+        ksort($this->columns);
+
+        return $this;
+    }
+
+    /**
+     * Clear a specified Table Column.
+     *
+     * @param string $column Column name (e.g. A)
+     */
+    public function clearColumn($column): self
+    {
+        $this->isColumnInRange($column);
+
+        if (isset($this->columns[$column])) {
+            unset($this->columns[$column]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Shift an Table Column Rule to a different column.
+     *
+     * Note: This method bypasses validation of the destination column to ensure it is within this Table range.
+     *        Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
+     *        Use with caution.
+     *
+     * @param string $fromColumn Column name (e.g. A)
+     * @param string $toColumn Column name (e.g. B)
+     */
+    public function shiftColumn($fromColumn, $toColumn): self
+    {
+        $fromColumn = strtoupper($fromColumn);
+        $toColumn = strtoupper($toColumn);
+
+        if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) {
+            $this->columns[$fromColumn]->setTable();
+            $this->columns[$fromColumn]->setColumnIndex($toColumn);
+            $this->columns[$toColumn] = $this->columns[$fromColumn];
+            $this->columns[$toColumn]->setTable($this);
+            unset($this->columns[$fromColumn]);
+
+            ksort($this->columns);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get table Style.
+     */
+    public function getStyle(): Table\TableStyle
+    {
+        return $this->style;
+    }
+
+    /**
+     * Set table Style.
+     */
+    public function setStyle(TableStyle $style): self
+    {
+        $this->style = $style;
+
+        return $this;
+    }
+
+    /**
+     * Get AutoFilter.
+     */
+    public function getAutoFilter(): AutoFilter
+    {
+        return $this->autoFilter;
+    }
+
+    /**
+     * Set AutoFilter.
+     */
+    public function setAutoFilter(AutoFilter $autoFilter): self
+    {
+        $this->autoFilter = $autoFilter;
+
+        return $this;
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        $vars = get_object_vars($this);
+        foreach ($vars as $key => $value) {
+            if (is_object($value)) {
+                if ($key === 'workSheet') {
+                    //    Detach from worksheet
+                    $this->{$key} = null;
+                } else {
+                    $this->{$key} = clone $value;
+                }
+            } elseif ((is_array($value)) && ($key === 'columns')) {
+                //    The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table objects
+                $this->{$key} = [];
+                foreach ($value as $k => $v) {
+                    $this->{$key}[$k] = clone $v;
+                    // attach the new cloned Column to this new cloned Table object
+                    $this->{$key}[$k]->setTable($this);
+                }
+            } else {
+                $this->{$key} = $value;
+            }
+        }
+    }
+
+    /**
+     * toString method replicates previous behavior by returning the range if object is
+     * referenced as a property of its worksheet.
+     */
+    public function __toString()
+    {
+        return (string) $this->range;
+    }
+}

+ 254 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/Column.php

@@ -0,0 +1,254 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet\Table;
+
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Table;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+
+class Column
+{
+    /**
+     * Table Column Index.
+     *
+     * @var string
+     */
+    private $columnIndex = '';
+
+    /**
+     * Show Filter Button.
+     *
+     * @var bool
+     */
+    private $showFilterButton = true;
+
+    /**
+     * Total Row Label.
+     *
+     * @var string
+     */
+    private $totalsRowLabel;
+
+    /**
+     * Total Row Function.
+     *
+     * @var string
+     */
+    private $totalsRowFunction;
+
+    /**
+     * Total Row Formula.
+     *
+     * @var string
+     */
+    private $totalsRowFormula;
+
+    /**
+     * Column Formula.
+     *
+     * @var string
+     */
+    private $columnFormula;
+
+    /**
+     * Table.
+     *
+     * @var null|Table
+     */
+    private $table;
+
+    /**
+     * Create a new Column.
+     *
+     * @param string $column Column (e.g. A)
+     * @param Table $table Table for this column
+     */
+    public function __construct($column, ?Table $table = null)
+    {
+        $this->columnIndex = $column;
+        $this->table = $table;
+    }
+
+    /**
+     * Get Table column index as string eg: 'A'.
+     */
+    public function getColumnIndex(): string
+    {
+        return $this->columnIndex;
+    }
+
+    /**
+     * Set Table column index as string eg: 'A'.
+     *
+     * @param string $column Column (e.g. A)
+     */
+    public function setColumnIndex($column): self
+    {
+        // Uppercase coordinate
+        $column = strtoupper($column);
+        if ($this->table !== null) {
+            $this->table->isColumnInRange($column);
+        }
+
+        $this->columnIndex = $column;
+
+        return $this;
+    }
+
+    /**
+     * Get show Filter Button.
+     */
+    public function getShowFilterButton(): bool
+    {
+        return $this->showFilterButton;
+    }
+
+    /**
+     * Set show Filter Button.
+     */
+    public function setShowFilterButton(bool $showFilterButton): self
+    {
+        $this->showFilterButton = $showFilterButton;
+
+        return $this;
+    }
+
+    /**
+     * Get total Row Label.
+     */
+    public function getTotalsRowLabel(): ?string
+    {
+        return $this->totalsRowLabel;
+    }
+
+    /**
+     * Set total Row Label.
+     */
+    public function setTotalsRowLabel(string $totalsRowLabel): self
+    {
+        $this->totalsRowLabel = $totalsRowLabel;
+
+        return $this;
+    }
+
+    /**
+     * Get total Row Function.
+     */
+    public function getTotalsRowFunction(): ?string
+    {
+        return $this->totalsRowFunction;
+    }
+
+    /**
+     * Set total Row Function.
+     */
+    public function setTotalsRowFunction(string $totalsRowFunction): self
+    {
+        $this->totalsRowFunction = $totalsRowFunction;
+
+        return $this;
+    }
+
+    /**
+     * Get total Row Formula.
+     */
+    public function getTotalsRowFormula(): ?string
+    {
+        return $this->totalsRowFormula;
+    }
+
+    /**
+     * Set total Row Formula.
+     */
+    public function setTotalsRowFormula(string $totalsRowFormula): self
+    {
+        $this->totalsRowFormula = $totalsRowFormula;
+
+        return $this;
+    }
+
+    /**
+     * Get column Formula.
+     */
+    public function getColumnFormula(): ?string
+    {
+        return $this->columnFormula;
+    }
+
+    /**
+     * Set column Formula.
+     */
+    public function setColumnFormula(string $columnFormula): self
+    {
+        $this->columnFormula = $columnFormula;
+
+        return $this;
+    }
+
+    /**
+     * Get this Column's Table.
+     */
+    public function getTable(): ?Table
+    {
+        return $this->table;
+    }
+
+    /**
+     * Set this Column's Table.
+     */
+    public function setTable(?Table $table = null): self
+    {
+        $this->table = $table;
+
+        return $this;
+    }
+
+    public static function updateStructuredReferences(?Worksheet $workSheet, ?string $oldTitle, ?string $newTitle): void
+    {
+        if ($workSheet === null || $oldTitle === null || $oldTitle === '' || $newTitle === null) {
+            return;
+        }
+
+        // Remember that table headings are case-insensitive
+        if (StringHelper::strToLower($oldTitle) !== StringHelper::strToLower($newTitle)) {
+            // We need to check all formula cells that might contain Structured References that refer
+            //    to this column, and update those formulae to reference the new column text
+            $spreadsheet = $workSheet->getParentOrThrow();
+            foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
+                self::updateStructuredReferencesInCells($sheet, $oldTitle, $newTitle);
+            }
+            self::updateStructuredReferencesInNamedFormulae($spreadsheet, $oldTitle, $newTitle);
+        }
+    }
+
+    private static function updateStructuredReferencesInCells(Worksheet $worksheet, string $oldTitle, string $newTitle): void
+    {
+        $pattern = '/\[(@?)' . preg_quote($oldTitle, '/') . '\]/mui';
+
+        foreach ($worksheet->getCoordinates(false) as $coordinate) {
+            $cell = $worksheet->getCell($coordinate);
+            if ($cell->getDataType() === DataType::TYPE_FORMULA) {
+                $formula = $cell->getValue();
+                if (preg_match($pattern, $formula) === 1) {
+                    $formula = preg_replace($pattern, "[$1{$newTitle}]", $formula);
+                    $cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
+                }
+            }
+        }
+    }
+
+    private static function updateStructuredReferencesInNamedFormulae(Spreadsheet $spreadsheet, string $oldTitle, string $newTitle): void
+    {
+        $pattern = '/\[(@?)' . preg_quote($oldTitle, '/') . '\]/mui';
+
+        foreach ($spreadsheet->getNamedFormulae() as $namedFormula) {
+            $formula = $namedFormula->getValue();
+            if (preg_match($pattern, $formula) === 1) {
+                $formula = preg_replace($pattern, "[$1{$newTitle}]", $formula);
+                $namedFormula->setValue($formula); // @phpstan-ignore-line
+            }
+        }
+    }
+}

+ 230 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php

@@ -0,0 +1,230 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet\Table;
+
+use PhpOffice\PhpSpreadsheet\Worksheet\Table;
+
+class TableStyle
+{
+    const TABLE_STYLE_NONE = '';
+    const TABLE_STYLE_LIGHT1 = 'TableStyleLight1';
+    const TABLE_STYLE_LIGHT2 = 'TableStyleLight2';
+    const TABLE_STYLE_LIGHT3 = 'TableStyleLight3';
+    const TABLE_STYLE_LIGHT4 = 'TableStyleLight4';
+    const TABLE_STYLE_LIGHT5 = 'TableStyleLight5';
+    const TABLE_STYLE_LIGHT6 = 'TableStyleLight6';
+    const TABLE_STYLE_LIGHT7 = 'TableStyleLight7';
+    const TABLE_STYLE_LIGHT8 = 'TableStyleLight8';
+    const TABLE_STYLE_LIGHT9 = 'TableStyleLight9';
+    const TABLE_STYLE_LIGHT10 = 'TableStyleLight10';
+    const TABLE_STYLE_LIGHT11 = 'TableStyleLight11';
+    const TABLE_STYLE_LIGHT12 = 'TableStyleLight12';
+    const TABLE_STYLE_LIGHT13 = 'TableStyleLight13';
+    const TABLE_STYLE_LIGHT14 = 'TableStyleLight14';
+    const TABLE_STYLE_LIGHT15 = 'TableStyleLight15';
+    const TABLE_STYLE_LIGHT16 = 'TableStyleLight16';
+    const TABLE_STYLE_LIGHT17 = 'TableStyleLight17';
+    const TABLE_STYLE_LIGHT18 = 'TableStyleLight18';
+    const TABLE_STYLE_LIGHT19 = 'TableStyleLight19';
+    const TABLE_STYLE_LIGHT20 = 'TableStyleLight20';
+    const TABLE_STYLE_LIGHT21 = 'TableStyleLight21';
+    const TABLE_STYLE_MEDIUM1 = 'TableStyleMedium1';
+    const TABLE_STYLE_MEDIUM2 = 'TableStyleMedium2';
+    const TABLE_STYLE_MEDIUM3 = 'TableStyleMedium3';
+    const TABLE_STYLE_MEDIUM4 = 'TableStyleMedium4';
+    const TABLE_STYLE_MEDIUM5 = 'TableStyleMedium5';
+    const TABLE_STYLE_MEDIUM6 = 'TableStyleMedium6';
+    const TABLE_STYLE_MEDIUM7 = 'TableStyleMedium7';
+    const TABLE_STYLE_MEDIUM8 = 'TableStyleMedium8';
+    const TABLE_STYLE_MEDIUM9 = 'TableStyleMedium9';
+    const TABLE_STYLE_MEDIUM10 = 'TableStyleMedium10';
+    const TABLE_STYLE_MEDIUM11 = 'TableStyleMedium11';
+    const TABLE_STYLE_MEDIUM12 = 'TableStyleMedium12';
+    const TABLE_STYLE_MEDIUM13 = 'TableStyleMedium13';
+    const TABLE_STYLE_MEDIUM14 = 'TableStyleMedium14';
+    const TABLE_STYLE_MEDIUM15 = 'TableStyleMedium15';
+    const TABLE_STYLE_MEDIUM16 = 'TableStyleMedium16';
+    const TABLE_STYLE_MEDIUM17 = 'TableStyleMedium17';
+    const TABLE_STYLE_MEDIUM18 = 'TableStyleMedium18';
+    const TABLE_STYLE_MEDIUM19 = 'TableStyleMedium19';
+    const TABLE_STYLE_MEDIUM20 = 'TableStyleMedium20';
+    const TABLE_STYLE_MEDIUM21 = 'TableStyleMedium21';
+    const TABLE_STYLE_MEDIUM22 = 'TableStyleMedium22';
+    const TABLE_STYLE_MEDIUM23 = 'TableStyleMedium23';
+    const TABLE_STYLE_MEDIUM24 = 'TableStyleMedium24';
+    const TABLE_STYLE_MEDIUM25 = 'TableStyleMedium25';
+    const TABLE_STYLE_MEDIUM26 = 'TableStyleMedium26';
+    const TABLE_STYLE_MEDIUM27 = 'TableStyleMedium27';
+    const TABLE_STYLE_MEDIUM28 = 'TableStyleMedium28';
+    const TABLE_STYLE_DARK1 = 'TableStyleDark1';
+    const TABLE_STYLE_DARK2 = 'TableStyleDark2';
+    const TABLE_STYLE_DARK3 = 'TableStyleDark3';
+    const TABLE_STYLE_DARK4 = 'TableStyleDark4';
+    const TABLE_STYLE_DARK5 = 'TableStyleDark5';
+    const TABLE_STYLE_DARK6 = 'TableStyleDark6';
+    const TABLE_STYLE_DARK7 = 'TableStyleDark7';
+    const TABLE_STYLE_DARK8 = 'TableStyleDark8';
+    const TABLE_STYLE_DARK9 = 'TableStyleDark9';
+    const TABLE_STYLE_DARK10 = 'TableStyleDark10';
+    const TABLE_STYLE_DARK11 = 'TableStyleDark11';
+
+    /**
+     * Theme.
+     *
+     * @var string
+     */
+    private $theme;
+
+    /**
+     * Show First Column.
+     *
+     * @var bool
+     */
+    private $showFirstColumn = false;
+
+    /**
+     * Show Last Column.
+     *
+     * @var bool
+     */
+    private $showLastColumn = false;
+
+    /**
+     * Show Row Stripes.
+     *
+     * @var bool
+     */
+    private $showRowStripes = false;
+
+    /**
+     * Show Column Stripes.
+     *
+     * @var bool
+     */
+    private $showColumnStripes = false;
+
+    /**
+     * Table.
+     *
+     * @var null|Table
+     */
+    private $table;
+
+    /**
+     * Create a new Table Style.
+     *
+     * @param string $theme (e.g. TableStyle::TABLE_STYLE_MEDIUM2)
+     */
+    public function __construct(string $theme = self::TABLE_STYLE_MEDIUM2)
+    {
+        $this->theme = $theme;
+    }
+
+    /**
+     * Get theme.
+     */
+    public function getTheme(): string
+    {
+        return $this->theme;
+    }
+
+    /**
+     * Set theme.
+     */
+    public function setTheme(string $theme): self
+    {
+        $this->theme = $theme;
+
+        return $this;
+    }
+
+    /**
+     * Get show First Column.
+     */
+    public function getShowFirstColumn(): bool
+    {
+        return $this->showFirstColumn;
+    }
+
+    /**
+     * Set show First Column.
+     */
+    public function setShowFirstColumn(bool $showFirstColumn): self
+    {
+        $this->showFirstColumn = $showFirstColumn;
+
+        return $this;
+    }
+
+    /**
+     * Get show Last Column.
+     */
+    public function getShowLastColumn(): bool
+    {
+        return $this->showLastColumn;
+    }
+
+    /**
+     * Set show Last Column.
+     */
+    public function setShowLastColumn(bool $showLastColumn): self
+    {
+        $this->showLastColumn = $showLastColumn;
+
+        return $this;
+    }
+
+    /**
+     * Get show Row Stripes.
+     */
+    public function getShowRowStripes(): bool
+    {
+        return $this->showRowStripes;
+    }
+
+    /**
+     * Set show Row Stripes.
+     */
+    public function setShowRowStripes(bool $showRowStripes): self
+    {
+        $this->showRowStripes = $showRowStripes;
+
+        return $this;
+    }
+
+    /**
+     * Get show Column Stripes.
+     */
+    public function getShowColumnStripes(): bool
+    {
+        return $this->showColumnStripes;
+    }
+
+    /**
+     * Set show Column Stripes.
+     */
+    public function setShowColumnStripes(bool $showColumnStripes): self
+    {
+        $this->showColumnStripes = $showColumnStripes;
+
+        return $this;
+    }
+
+    /**
+     * Get this Style's Table.
+     */
+    public function getTable(): ?Table
+    {
+        return $this->table;
+    }
+
+    /**
+     * Set this Style's Table.
+     */
+    public function setTable(?Table $table = null): self
+    {
+        $this->table = $table;
+
+        return $this;
+    }
+}

+ 118 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
+use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
+use PhpOffice\PhpSpreadsheet\Cell\CellRange;
+use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
+
+class Validations
+{
+    /**
+     * Validate a cell address.
+     *
+     * @param null|array<int>|CellAddress|string $cellAddress Coordinate of the cell as a string, eg: 'C5';
+     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     */
+    public static function validateCellAddress($cellAddress): string
+    {
+        if (is_string($cellAddress)) {
+            [$worksheet, $address] = Worksheet::extractSheetTitle($cellAddress, true);
+//            if (!empty($worksheet) && $worksheet !== $this->getTitle()) {
+//                throw new Exception('Reference is not for this worksheet');
+//            }
+
+            return empty($worksheet) ? strtoupper("$address") : $worksheet . '!' . strtoupper("$address");
+        }
+
+        if (is_array($cellAddress)) {
+            $cellAddress = CellAddress::fromColumnRowArray($cellAddress);
+        }
+
+        return (string) $cellAddress;
+    }
+
+    /**
+     * Validate a cell address or cell range.
+     *
+     * @param AddressRange|array<int>|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12';
+     *               or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]),
+     *               or as a CellAddress or AddressRange object.
+     */
+    public static function validateCellOrCellRange($cellRange): string
+    {
+        if (is_string($cellRange) || is_numeric($cellRange)) {
+            // Convert a single column reference like 'A' to 'A:A',
+            //    a single row reference like '1' to '1:1'
+            $cellRange = (string) preg_replace('/^([A-Z]+|\d+)$/', '${1}:${1}', (string) $cellRange);
+        } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) {
+            $cellRange = new CellRange($cellRange, $cellRange);
+        }
+
+        return self::validateCellRange($cellRange);
+    }
+
+    private const SETMAXROW = '${1}1:${2}' . AddressRange::MAX_ROW;
+    private const SETMAXCOL = 'A${1}:' . AddressRange::MAX_COLUMN . '${2}';
+
+    /**
+     * Validate a cell range.
+     *
+     * @param AddressRange|array<int>|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12';
+     *               or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]),
+     *               or as an AddressRange object.
+     */
+    public static function validateCellRange($cellRange): string
+    {
+        if (is_string($cellRange)) {
+            [$worksheet, $addressRange] = Worksheet::extractSheetTitle($cellRange, true);
+
+            // Convert Column ranges like 'A:C' to 'A1:C1048576'
+            //      or Row ranges like '1:3' to 'A1:XFD3'
+            $addressRange = (string) preg_replace(
+                ['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'],
+                [self::SETMAXROW, self::SETMAXCOL],
+                $addressRange
+            );
+
+            return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange);
+        }
+
+        if (is_array($cellRange)) {
+            switch (count($cellRange)) {
+                case 2:
+                    $from = [$cellRange[0], $cellRange[1]];
+                    $to = [$cellRange[0], $cellRange[1]];
+
+                    break;
+                case 4:
+                    $from = [$cellRange[0], $cellRange[1]];
+                    $to = [$cellRange[2], $cellRange[3]];
+
+                    break;
+                default:
+                    throw new SpreadsheetException('CellRange array length must be 2 or 4');
+            }
+            $cellRange = new CellRange(CellAddress::fromColumnRowArray($from), CellAddress::fromColumnRowArray($to));
+        }
+
+        return (string) $cellRange;
+    }
+
+    public static function definedNameToCoordinate(string $coordinate, Worksheet $worksheet): string
+    {
+        // Uppercase coordinate
+        $coordinate = strtoupper($coordinate);
+        // Eliminate leading equal sign
+        $testCoordinate = (string) preg_replace('/^=/', '', $coordinate);
+        $defined = $worksheet->getParentOrThrow()->getDefinedName($testCoordinate, $worksheet);
+        if ($defined !== null) {
+            if ($defined->getWorksheet() === $worksheet && !$defined->isFormula()) {
+                $coordinate = (string) preg_replace('/^=/', '', $defined->getValue());
+            }
+        }
+
+        return $coordinate;
+    }
+}

+ 3708 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php

@@ -0,0 +1,3708 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Worksheet;
+
+use ArrayObject;
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Calculation\Functions;
+use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
+use PhpOffice\PhpSpreadsheet\Cell\CellRange;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
+use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
+use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
+use PhpOffice\PhpSpreadsheet\Chart\Chart;
+use PhpOffice\PhpSpreadsheet\Collection\Cells;
+use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
+use PhpOffice\PhpSpreadsheet\Comment;
+use PhpOffice\PhpSpreadsheet\DefinedName;
+use PhpOffice\PhpSpreadsheet\Exception;
+use PhpOffice\PhpSpreadsheet\ReferenceHelper;
+use PhpOffice\PhpSpreadsheet\RichText\RichText;
+use PhpOffice\PhpSpreadsheet\Shared;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Color;
+use PhpOffice\PhpSpreadsheet\Style\Conditional;
+use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
+use PhpOffice\PhpSpreadsheet\Style\Style;
+
+class Worksheet
+{
+    // Break types
+    public const BREAK_NONE = 0;
+    public const BREAK_ROW = 1;
+    public const BREAK_COLUMN = 2;
+    // Maximum column for row break
+    public const BREAK_ROW_MAX_COLUMN = 16383;
+
+    // Sheet state
+    public const SHEETSTATE_VISIBLE = 'visible';
+    public const SHEETSTATE_HIDDEN = 'hidden';
+    public const SHEETSTATE_VERYHIDDEN = 'veryHidden';
+
+    public const MERGE_CELL_CONTENT_EMPTY = 'empty';
+    public const MERGE_CELL_CONTENT_HIDE = 'hide';
+    public const MERGE_CELL_CONTENT_MERGE = 'merge';
+
+    protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui';
+
+    /**
+     * Maximum 31 characters allowed for sheet title.
+     *
+     * @var int
+     */
+    const SHEET_TITLE_MAXIMUM_LENGTH = 31;
+
+    /**
+     * Invalid characters in sheet title.
+     *
+     * @var array
+     */
+    private static $invalidCharacters = ['*', ':', '/', '\\', '?', '[', ']'];
+
+    /**
+     * Parent spreadsheet.
+     *
+     * @var ?Spreadsheet
+     */
+    private $parent;
+
+    /**
+     * Collection of cells.
+     *
+     * @var Cells
+     */
+    private $cellCollection;
+
+    /**
+     * Collection of row dimensions.
+     *
+     * @var RowDimension[]
+     */
+    private $rowDimensions = [];
+
+    /**
+     * Default row dimension.
+     *
+     * @var RowDimension
+     */
+    private $defaultRowDimension;
+
+    /**
+     * Collection of column dimensions.
+     *
+     * @var ColumnDimension[]
+     */
+    private $columnDimensions = [];
+
+    /**
+     * Default column dimension.
+     *
+     * @var ColumnDimension
+     */
+    private $defaultColumnDimension;
+
+    /**
+     * Collection of drawings.
+     *
+     * @var ArrayObject<int, BaseDrawing>
+     */
+    private $drawingCollection;
+
+    /**
+     * Collection of Chart objects.
+     *
+     * @var ArrayObject<int, Chart>
+     */
+    private $chartCollection;
+
+    /**
+     * Collection of Table objects.
+     *
+     * @var ArrayObject<int, Table>
+     */
+    private $tableCollection;
+
+    /**
+     * Worksheet title.
+     *
+     * @var string
+     */
+    private $title;
+
+    /**
+     * Sheet state.
+     *
+     * @var string
+     */
+    private $sheetState;
+
+    /**
+     * Page setup.
+     *
+     * @var PageSetup
+     */
+    private $pageSetup;
+
+    /**
+     * Page margins.
+     *
+     * @var PageMargins
+     */
+    private $pageMargins;
+
+    /**
+     * Page header/footer.
+     *
+     * @var HeaderFooter
+     */
+    private $headerFooter;
+
+    /**
+     * Sheet view.
+     *
+     * @var SheetView
+     */
+    private $sheetView;
+
+    /**
+     * Protection.
+     *
+     * @var Protection
+     */
+    private $protection;
+
+    /**
+     * Collection of styles.
+     *
+     * @var Style[]
+     */
+    private $styles = [];
+
+    /**
+     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
+     *
+     * @var array
+     */
+    private $conditionalStylesCollection = [];
+
+    /**
+     * Collection of row breaks.
+     *
+     * @var PageBreak[]
+     */
+    private $rowBreaks = [];
+
+    /**
+     * Collection of column breaks.
+     *
+     * @var PageBreak[]
+     */
+    private $columnBreaks = [];
+
+    /**
+     * Collection of merged cell ranges.
+     *
+     * @var string[]
+     */
+    private $mergeCells = [];
+
+    /**
+     * Collection of protected cell ranges.
+     *
+     * @var string[]
+     */
+    private $protectedCells = [];
+
+    /**
+     * Autofilter Range and selection.
+     *
+     * @var AutoFilter
+     */
+    private $autoFilter;
+
+    /**
+     * Freeze pane.
+     *
+     * @var null|string
+     */
+    private $freezePane;
+
+    /**
+     * Default position of the right bottom pane.
+     *
+     * @var null|string
+     */
+    private $topLeftCell;
+
+    /**
+     * Show gridlines?
+     *
+     * @var bool
+     */
+    private $showGridlines = true;
+
+    /**
+     * Print gridlines?
+     *
+     * @var bool
+     */
+    private $printGridlines = false;
+
+    /**
+     * Show row and column headers?
+     *
+     * @var bool
+     */
+    private $showRowColHeaders = true;
+
+    /**
+     * Show summary below? (Row/Column outline).
+     *
+     * @var bool
+     */
+    private $showSummaryBelow = true;
+
+    /**
+     * Show summary right? (Row/Column outline).
+     *
+     * @var bool
+     */
+    private $showSummaryRight = true;
+
+    /**
+     * Collection of comments.
+     *
+     * @var Comment[]
+     */
+    private $comments = [];
+
+    /**
+     * Active cell. (Only one!).
+     *
+     * @var string
+     */
+    private $activeCell = 'A1';
+
+    /**
+     * Selected cells.
+     *
+     * @var string
+     */
+    private $selectedCells = 'A1';
+
+    /**
+     * Cached highest column.
+     *
+     * @var int
+     */
+    private $cachedHighestColumn = 1;
+
+    /**
+     * Cached highest row.
+     *
+     * @var int
+     */
+    private $cachedHighestRow = 1;
+
+    /**
+     * Right-to-left?
+     *
+     * @var bool
+     */
+    private $rightToLeft = false;
+
+    /**
+     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
+     *
+     * @var array
+     */
+    private $hyperlinkCollection = [];
+
+    /**
+     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
+     *
+     * @var array
+     */
+    private $dataValidationCollection = [];
+
+    /**
+     * Tab color.
+     *
+     * @var null|Color
+     */
+    private $tabColor;
+
+    /**
+     * Hash.
+     *
+     * @var int
+     */
+    private $hash;
+
+    /**
+     * CodeName.
+     *
+     * @var string
+     */
+    private $codeName;
+
+    /**
+     * Create a new worksheet.
+     *
+     * @param string $title
+     */
+    public function __construct(?Spreadsheet $parent = null, $title = 'Worksheet')
+    {
+        // Set parent and title
+        $this->parent = $parent;
+        $this->hash = spl_object_id($this);
+        $this->setTitle($title, false);
+        // setTitle can change $pTitle
+        $this->setCodeName($this->getTitle());
+        $this->setSheetState(self::SHEETSTATE_VISIBLE);
+
+        $this->cellCollection = CellsFactory::getInstance($this);
+        // Set page setup
+        $this->pageSetup = new PageSetup();
+        // Set page margins
+        $this->pageMargins = new PageMargins();
+        // Set page header/footer
+        $this->headerFooter = new HeaderFooter();
+        // Set sheet view
+        $this->sheetView = new SheetView();
+        // Drawing collection
+        $this->drawingCollection = new ArrayObject();
+        // Chart collection
+        $this->chartCollection = new ArrayObject();
+        // Protection
+        $this->protection = new Protection();
+        // Default row dimension
+        $this->defaultRowDimension = new RowDimension(null);
+        // Default column dimension
+        $this->defaultColumnDimension = new ColumnDimension(null);
+        // AutoFilter
+        $this->autoFilter = new AutoFilter('', $this);
+        // Table collection
+        $this->tableCollection = new ArrayObject();
+    }
+
+    /**
+     * Disconnect all cells from this Worksheet object,
+     * typically so that the worksheet object can be unset.
+     */
+    public function disconnectCells(): void
+    {
+        if ($this->cellCollection !== null) {
+            $this->cellCollection->unsetWorksheetCells();
+            // @phpstan-ignore-next-line
+            $this->cellCollection = null;
+        }
+        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
+        $this->parent = null;
+    }
+
+    /**
+     * Code to execute when this worksheet is unset().
+     */
+    public function __destruct()
+    {
+        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
+
+        $this->disconnectCells();
+        $this->rowDimensions = [];
+    }
+
+    public function __wakeup(): void
+    {
+        $this->hash = spl_object_id($this);
+        $this->parent = null;
+    }
+
+    /**
+     * Return the cell collection.
+     *
+     * @return Cells
+     */
+    public function getCellCollection()
+    {
+        return $this->cellCollection;
+    }
+
+    /**
+     * Get array of invalid characters for sheet title.
+     *
+     * @return array
+     */
+    public static function getInvalidCharacters()
+    {
+        return self::$invalidCharacters;
+    }
+
+    /**
+     * Check sheet code name for valid Excel syntax.
+     *
+     * @param string $sheetCodeName The string to check
+     *
+     * @return string The valid string
+     */
+    private static function checkSheetCodeName($sheetCodeName)
+    {
+        $charCount = Shared\StringHelper::countCharacters($sheetCodeName);
+        if ($charCount == 0) {
+            throw new Exception('Sheet code name cannot be empty.');
+        }
+        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
+        if (
+            (str_replace(self::$invalidCharacters, '', $sheetCodeName) !== $sheetCodeName) ||
+            (Shared\StringHelper::substring($sheetCodeName, -1, 1) == '\'') ||
+            (Shared\StringHelper::substring($sheetCodeName, 0, 1) == '\'')
+        ) {
+            throw new Exception('Invalid character found in sheet code name');
+        }
+
+        // Enforce maximum characters allowed for sheet title
+        if ($charCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
+            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
+        }
+
+        return $sheetCodeName;
+    }
+
+    /**
+     * Check sheet title for valid Excel syntax.
+     *
+     * @param string $sheetTitle The string to check
+     *
+     * @return string The valid string
+     */
+    private static function checkSheetTitle($sheetTitle)
+    {
+        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
+        if (str_replace(self::$invalidCharacters, '', $sheetTitle) !== $sheetTitle) {
+            throw new Exception('Invalid character found in sheet title');
+        }
+
+        // Enforce maximum characters allowed for sheet title
+        if (Shared\StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
+            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
+        }
+
+        return $sheetTitle;
+    }
+
+    /**
+     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
+     *
+     * @param bool $sorted Also sort the cell collection?
+     *
+     * @return string[]
+     */
+    public function getCoordinates($sorted = true)
+    {
+        if ($this->cellCollection == null) {
+            return [];
+        }
+
+        if ($sorted) {
+            return $this->cellCollection->getSortedCoordinates();
+        }
+
+        return $this->cellCollection->getCoordinates();
+    }
+
+    /**
+     * Get collection of row dimensions.
+     *
+     * @return RowDimension[]
+     */
+    public function getRowDimensions()
+    {
+        return $this->rowDimensions;
+    }
+
+    /**
+     * Get default row dimension.
+     *
+     * @return RowDimension
+     */
+    public function getDefaultRowDimension()
+    {
+        return $this->defaultRowDimension;
+    }
+
+    /**
+     * Get collection of column dimensions.
+     *
+     * @return ColumnDimension[]
+     */
+    public function getColumnDimensions()
+    {
+        /** @var callable */
+        $callable = [self::class, 'columnDimensionCompare'];
+        uasort($this->columnDimensions, $callable);
+
+        return $this->columnDimensions;
+    }
+
+    private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int
+    {
+        return $a->getColumnNumeric() - $b->getColumnNumeric();
+    }
+
+    /**
+     * Get default column dimension.
+     *
+     * @return ColumnDimension
+     */
+    public function getDefaultColumnDimension()
+    {
+        return $this->defaultColumnDimension;
+    }
+
+    /**
+     * Get collection of drawings.
+     *
+     * @return ArrayObject<int, BaseDrawing>
+     */
+    public function getDrawingCollection()
+    {
+        return $this->drawingCollection;
+    }
+
+    /**
+     * Get collection of charts.
+     *
+     * @return ArrayObject<int, Chart>
+     */
+    public function getChartCollection()
+    {
+        return $this->chartCollection;
+    }
+
+    /**
+     * Add chart.
+     *
+     * @param null|int $chartIndex Index where chart should go (0,1,..., or null for last)
+     *
+     * @return Chart
+     */
+    public function addChart(Chart $chart, $chartIndex = null)
+    {
+        $chart->setWorksheet($this);
+        if ($chartIndex === null) {
+            $this->chartCollection[] = $chart;
+        } else {
+            // Insert the chart at the requested index
+            // @phpstan-ignore-next-line
+            array_splice(/** @scrutinizer ignore-type */ $this->chartCollection, $chartIndex, 0, [$chart]);
+        }
+
+        return $chart;
+    }
+
+    /**
+     * Return the count of charts on this worksheet.
+     *
+     * @return int The number of charts
+     */
+    public function getChartCount()
+    {
+        return count($this->chartCollection);
+    }
+
+    /**
+     * Get a chart by its index position.
+     *
+     * @param ?string $index Chart index position
+     *
+     * @return Chart|false
+     */
+    public function getChartByIndex($index)
+    {
+        $chartCount = count($this->chartCollection);
+        if ($chartCount == 0) {
+            return false;
+        }
+        if ($index === null) {
+            $index = --$chartCount;
+        }
+        if (!isset($this->chartCollection[$index])) {
+            return false;
+        }
+
+        return $this->chartCollection[$index];
+    }
+
+    /**
+     * Return an array of the names of charts on this worksheet.
+     *
+     * @return string[] The names of charts
+     */
+    public function getChartNames()
+    {
+        $chartNames = [];
+        foreach ($this->chartCollection as $chart) {
+            $chartNames[] = $chart->getName();
+        }
+
+        return $chartNames;
+    }
+
+    /**
+     * Get a chart by name.
+     *
+     * @param string $chartName Chart name
+     *
+     * @return Chart|false
+     */
+    public function getChartByName($chartName)
+    {
+        foreach ($this->chartCollection as $index => $chart) {
+            if ($chart->getName() == $chartName) {
+                return $chart;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Refresh column dimensions.
+     *
+     * @return $this
+     */
+    public function refreshColumnDimensions()
+    {
+        $newColumnDimensions = [];
+        foreach ($this->getColumnDimensions() as $objColumnDimension) {
+            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
+        }
+
+        $this->columnDimensions = $newColumnDimensions;
+
+        return $this;
+    }
+
+    /**
+     * Refresh row dimensions.
+     *
+     * @return $this
+     */
+    public function refreshRowDimensions()
+    {
+        $newRowDimensions = [];
+        foreach ($this->getRowDimensions() as $objRowDimension) {
+            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
+        }
+
+        $this->rowDimensions = $newRowDimensions;
+
+        return $this;
+    }
+
+    /**
+     * Calculate worksheet dimension.
+     *
+     * @return string String containing the dimension of this worksheet
+     */
+    public function calculateWorksheetDimension()
+    {
+        // Return
+        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
+    }
+
+    /**
+     * Calculate worksheet data dimension.
+     *
+     * @return string String containing the dimension of this worksheet that actually contain data
+     */
+    public function calculateWorksheetDataDimension()
+    {
+        // Return
+        return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
+    }
+
+    /**
+     * Calculate widths for auto-size columns.
+     *
+     * @return $this
+     */
+    public function calculateColumnWidths()
+    {
+        // initialize $autoSizes array
+        $autoSizes = [];
+        foreach ($this->getColumnDimensions() as $colDimension) {
+            if ($colDimension->getAutoSize()) {
+                $autoSizes[$colDimension->getColumnIndex()] = -1;
+            }
+        }
+
+        // There is only something to do if there are some auto-size columns
+        if (!empty($autoSizes)) {
+            // build list of cells references that participate in a merge
+            $isMergeCell = [];
+            foreach ($this->getMergeCells() as $cells) {
+                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
+                    $isMergeCell[$cellReference] = true;
+                }
+            }
+
+            $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
+
+            // loop through all cells in the worksheet
+            foreach ($this->getCoordinates(false) as $coordinate) {
+                $cell = $this->getCellOrNull($coordinate);
+
+                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
+                    //Determine if cell is in merge range
+                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
+
+                    //By default merged cells should be ignored
+                    $isMergedButProceed = false;
+
+                    //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide)
+                    if ($isMerged && $cell->isMergeRangeValueCell()) {
+                        $range = (string) $cell->getMergeRange();
+                        $rangeBoundaries = Coordinate::rangeDimension($range);
+                        if ($rangeBoundaries[0] === 1) {
+                            $isMergedButProceed = true;
+                        }
+                    }
+
+                    // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range
+                    if (!$isMerged || $isMergedButProceed) {
+                        // Determine if we need to make an adjustment for the first row in an AutoFilter range that
+                        //    has a column filter dropdown
+                        $filterAdjustment = false;
+                        if (!empty($autoFilterIndentRanges)) {
+                            foreach ($autoFilterIndentRanges as $autoFilterFirstRowRange) {
+                                if ($cell->isInRange($autoFilterFirstRowRange)) {
+                                    $filterAdjustment = true;
+
+                                    break;
+                                }
+                            }
+                        }
+
+                        $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
+                        $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
+
+                        // Calculated value
+                        // To formatted string
+                        $cellValue = NumberFormat::toFormattedString(
+                            $cell->getCalculatedValue(),
+                            (string) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
+                                ->getNumberFormat()->getFormatCode()
+                        );
+
+                        if ($cellValue !== null && $cellValue !== '') {
+                            $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
+                                $autoSizes[$this->cellCollection->getCurrentColumn()],
+                                round(
+                                    Shared\Font::calculateColumnWidth(
+                                        $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont(),
+                                        $cellValue,
+                                        (int) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
+                                            ->getAlignment()->getTextRotation(),
+                                        $this->getParentOrThrow()->getDefaultStyle()->getFont(),
+                                        $filterAdjustment,
+                                        $indentAdjustment
+                                    ),
+                                    3
+                                )
+                            );
+                        }
+                    }
+                }
+            }
+
+            // adjust column widths
+            foreach ($autoSizes as $columnIndex => $width) {
+                if ($width == -1) {
+                    $width = $this->getDefaultColumnDimension()->getWidth();
+                }
+                $this->getColumnDimension($columnIndex)->setWidth($width);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get parent or null.
+     */
+    public function getParent(): ?Spreadsheet
+    {
+        return $this->parent;
+    }
+
+    /**
+     * Get parent, throw exception if null.
+     */
+    public function getParentOrThrow(): Spreadsheet
+    {
+        if ($this->parent !== null) {
+            return $this->parent;
+        }
+
+        throw new Exception('Sheet does not have a parent.');
+    }
+
+    /**
+     * Re-bind parent.
+     *
+     * @return $this
+     */
+    public function rebindParent(Spreadsheet $parent)
+    {
+        if ($this->parent !== null) {
+            $definedNames = $this->parent->getDefinedNames();
+            foreach ($definedNames as $definedName) {
+                $parent->addDefinedName($definedName);
+            }
+
+            $this->parent->removeSheetByIndex(
+                $this->parent->getIndex($this)
+            );
+        }
+        $this->parent = $parent;
+
+        return $this;
+    }
+
+    /**
+     * Get title.
+     *
+     * @return string
+     */
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    /**
+     * Set title.
+     *
+     * @param string $title String containing the dimension of this worksheet
+     * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
+     *            be updated to reflect the new sheet name.
+     *          This should be left as the default true, unless you are
+     *          certain that no formula cells on any worksheet contain
+     *          references to this worksheet
+     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
+     *                       at parse time (by Readers), where titles can be assumed to be valid.
+     *
+     * @return $this
+     */
+    public function setTitle($title, $updateFormulaCellReferences = true, $validate = true)
+    {
+        // Is this a 'rename' or not?
+        if ($this->getTitle() == $title) {
+            return $this;
+        }
+
+        // Old title
+        $oldTitle = $this->getTitle();
+
+        if ($validate) {
+            // Syntax check
+            self::checkSheetTitle($title);
+
+            if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
+                // Is there already such sheet name?
+                if ($this->parent->sheetNameExists($title)) {
+                    // Use name, but append with lowest possible integer
+
+                    if (Shared\StringHelper::countCharacters($title) > 29) {
+                        $title = Shared\StringHelper::substring($title, 0, 29);
+                    }
+                    $i = 1;
+                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
+                        ++$i;
+                        if ($i == 10) {
+                            if (Shared\StringHelper::countCharacters($title) > 28) {
+                                $title = Shared\StringHelper::substring($title, 0, 28);
+                            }
+                        } elseif ($i == 100) {
+                            if (Shared\StringHelper::countCharacters($title) > 27) {
+                                $title = Shared\StringHelper::substring($title, 0, 27);
+                            }
+                        }
+                    }
+
+                    $title .= " $i";
+                }
+            }
+        }
+
+        // Set title
+        $this->title = $title;
+
+        if ($this->parent && $this->parent->getIndex($this, true) >= 0 && $this->parent->getCalculationEngine()) {
+            // New title
+            $newTitle = $this->getTitle();
+            $this->parent->getCalculationEngine()
+                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
+            if ($updateFormulaCellReferences) {
+                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get sheet state.
+     *
+     * @return string Sheet state (visible, hidden, veryHidden)
+     */
+    public function getSheetState()
+    {
+        return $this->sheetState;
+    }
+
+    /**
+     * Set sheet state.
+     *
+     * @param string $value Sheet state (visible, hidden, veryHidden)
+     *
+     * @return $this
+     */
+    public function setSheetState($value)
+    {
+        $this->sheetState = $value;
+
+        return $this;
+    }
+
+    /**
+     * Get page setup.
+     *
+     * @return PageSetup
+     */
+    public function getPageSetup()
+    {
+        return $this->pageSetup;
+    }
+
+    /**
+     * Set page setup.
+     *
+     * @return $this
+     */
+    public function setPageSetup(PageSetup $pageSetup)
+    {
+        $this->pageSetup = $pageSetup;
+
+        return $this;
+    }
+
+    /**
+     * Get page margins.
+     *
+     * @return PageMargins
+     */
+    public function getPageMargins()
+    {
+        return $this->pageMargins;
+    }
+
+    /**
+     * Set page margins.
+     *
+     * @return $this
+     */
+    public function setPageMargins(PageMargins $pageMargins)
+    {
+        $this->pageMargins = $pageMargins;
+
+        return $this;
+    }
+
+    /**
+     * Get page header/footer.
+     *
+     * @return HeaderFooter
+     */
+    public function getHeaderFooter()
+    {
+        return $this->headerFooter;
+    }
+
+    /**
+     * Set page header/footer.
+     *
+     * @return $this
+     */
+    public function setHeaderFooter(HeaderFooter $headerFooter)
+    {
+        $this->headerFooter = $headerFooter;
+
+        return $this;
+    }
+
+    /**
+     * Get sheet view.
+     *
+     * @return SheetView
+     */
+    public function getSheetView()
+    {
+        return $this->sheetView;
+    }
+
+    /**
+     * Set sheet view.
+     *
+     * @return $this
+     */
+    public function setSheetView(SheetView $sheetView)
+    {
+        $this->sheetView = $sheetView;
+
+        return $this;
+    }
+
+    /**
+     * Get Protection.
+     *
+     * @return Protection
+     */
+    public function getProtection()
+    {
+        return $this->protection;
+    }
+
+    /**
+     * Set Protection.
+     *
+     * @return $this
+     */
+    public function setProtection(Protection $protection)
+    {
+        $this->protection = $protection;
+
+        return $this;
+    }
+
+    /**
+     * Get highest worksheet column.
+     *
+     * @param null|int|string $row Return the data highest column for the specified row,
+     *                                     or the highest column of any row if no row number is passed
+     *
+     * @return string Highest column name
+     */
+    public function getHighestColumn($row = null)
+    {
+        if ($row === null) {
+            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
+        }
+
+        return $this->getHighestDataColumn($row);
+    }
+
+    /**
+     * Get highest worksheet column that contains data.
+     *
+     * @param null|int|string $row Return the highest data column for the specified row,
+     *                                     or the highest data column of any row if no row number is passed
+     *
+     * @return string Highest column name that contains data
+     */
+    public function getHighestDataColumn($row = null)
+    {
+        return $this->cellCollection->getHighestColumn($row);
+    }
+
+    /**
+     * Get highest worksheet row.
+     *
+     * @param null|string $column Return the highest data row for the specified column,
+     *                                     or the highest row of any column if no column letter is passed
+     *
+     * @return int Highest row number
+     */
+    public function getHighestRow($column = null)
+    {
+        if ($column === null) {
+            return $this->cachedHighestRow;
+        }
+
+        return $this->getHighestDataRow($column);
+    }
+
+    /**
+     * Get highest worksheet row that contains data.
+     *
+     * @param null|string $column Return the highest data row for the specified column,
+     *                                     or the highest data row of any column if no column letter is passed
+     *
+     * @return int Highest row number that contains data
+     */
+    public function getHighestDataRow($column = null)
+    {
+        return $this->cellCollection->getHighestRow($column);
+    }
+
+    /**
+     * Get highest worksheet column and highest row that have cell records.
+     *
+     * @return array Highest column name and highest row number
+     */
+    public function getHighestRowAndColumn()
+    {
+        return $this->cellCollection->getHighestRowAndColumn();
+    }
+
+    /**
+     * Set a cell value.
+     *
+     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
+     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @param mixed $value Value for the cell
+     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
+     *
+     * @return $this
+     */
+    public function setCellValue($coordinate, $value, ?IValueBinder $binder = null)
+    {
+        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
+        $this->getCell($cellAddress)->setValue($value, $binder);
+
+        return $this;
+    }
+
+    /**
+     * Set a cell value by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the setCellValue() method with a cell address such as 'C5' instead;,
+     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @see Worksheet::setCellValue()
+     *
+     * @param int $columnIndex Numeric column coordinate of the cell
+     * @param int $row Numeric row coordinate of the cell
+     * @param mixed $value Value of the cell
+     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
+     *
+     * @return $this
+     */
+    public function setCellValueByColumnAndRow($columnIndex, $row, $value, ?IValueBinder $binder = null)
+    {
+        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValue($value, $binder);
+
+        return $this;
+    }
+
+    /**
+     * Set a cell value.
+     *
+     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
+     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @param mixed $value Value of the cell
+     * @param string $dataType Explicit data type, see DataType::TYPE_*
+     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
+     *             method, then it is your responsibility as an end-user developer to validate that the value and
+     *             the datatype match.
+     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
+     *          that you specify.
+     *
+     * @see DataType
+     *
+     * @return $this
+     */
+    public function setCellValueExplicit($coordinate, $value, $dataType)
+    {
+        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
+        $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
+
+        return $this;
+    }
+
+    /**
+     * Set a cell value by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the setCellValueExplicit() method with a cell address such as 'C5' instead;,
+     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @see Worksheet::setCellValueExplicit()
+     *
+     * @param int $columnIndex Numeric column coordinate of the cell
+     * @param int $row Numeric row coordinate of the cell
+     * @param mixed $value Value of the cell
+     * @param string $dataType Explicit data type, see DataType::TYPE_*
+     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
+     *             method, then it is your responsibility as an end-user developer to validate that the value and
+     *             the datatype match.
+     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
+     *          that you specify.
+     *
+     * @see DataType
+     *
+     * @return $this
+     */
+    public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType)
+    {
+        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValueExplicit($value, $dataType);
+
+        return $this;
+    }
+
+    /**
+     * Get cell at a specific coordinate.
+     *
+     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
+     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     *
+     * @return Cell Cell that was found or created
+     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
+     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
+     *              another cell using getCell() or any of its variants, the newly selected cell becomes
+     *              the "active" cell, and any previous assignment becomes a disconnected reference because
+     *              the active cell has changed.
+     */
+    public function getCell($coordinate): Cell
+    {
+        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
+
+        // Shortcut for increased performance for the vast majority of simple cases
+        if ($this->cellCollection->has($cellAddress)) {
+            /** @var Cell $cell */
+            $cell = $this->cellCollection->get($cellAddress);
+
+            return $cell;
+        }
+
+        /** @var Worksheet $sheet */
+        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
+        $cell = $sheet->cellCollection->get($finalCoordinate);
+
+        return $cell ?? $sheet->createNewCell($finalCoordinate);
+    }
+
+    /**
+     * Get the correct Worksheet and coordinate from a coordinate that may
+     * contains reference to another sheet or a named range.
+     *
+     * @return array{0: Worksheet, 1: string}
+     */
+    private function getWorksheetAndCoordinate(string $coordinate): array
+    {
+        $sheet = null;
+        $finalCoordinate = null;
+
+        // Worksheet reference?
+        if (strpos($coordinate, '!') !== false) {
+            $worksheetReference = self::extractSheetTitle($coordinate, true);
+
+            $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
+            $finalCoordinate = strtoupper($worksheetReference[1]);
+
+            if ($sheet === null) {
+                throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
+            }
+        } elseif (
+            !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate) &&
+            preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
+        ) {
+            // Named range?
+            $namedRange = $this->validateNamedRange($coordinate, true);
+            if ($namedRange !== null) {
+                $sheet = $namedRange->getWorksheet();
+                if ($sheet === null) {
+                    throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
+                }
+
+                /** @phpstan-ignore-next-line */
+                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
+                $finalCoordinate = str_replace('$', '', $cellCoordinate);
+            }
+        }
+
+        if ($sheet === null || $finalCoordinate === null) {
+            $sheet = $this;
+            $finalCoordinate = strtoupper($coordinate);
+        }
+
+        if (Coordinate::coordinateIsRange($finalCoordinate)) {
+            throw new Exception('Cell coordinate string can not be a range of cells.');
+        } elseif (strpos($finalCoordinate, '$') !== false) {
+            throw new Exception('Cell coordinate must not be absolute.');
+        }
+
+        return [$sheet, $finalCoordinate];
+    }
+
+    /**
+     * Get an existing cell at a specific coordinate, or null.
+     *
+     * @param string $coordinate Coordinate of the cell, eg: 'A1'
+     *
+     * @return null|Cell Cell that was found or null
+     */
+    private function getCellOrNull($coordinate): ?Cell
+    {
+        // Check cell collection
+        if ($this->cellCollection->has($coordinate)) {
+            return $this->cellCollection->get($coordinate);
+        }
+
+        return null;
+    }
+
+    /**
+     * Get cell at a specific coordinate by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the getCell() method with a cell address such as 'C5' instead;,
+     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @see Worksheet::getCell()
+     *
+     * @param int $columnIndex Numeric column coordinate of the cell
+     * @param int $row Numeric row coordinate of the cell
+     *
+     * @return Cell Cell that was found/created or null
+     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
+     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
+     *              another cell using getCell() or any of its variants, the newly selected cell becomes
+     *              the "active" cell, and any previous assignment becomes a disconnected reference because
+     *              the active cell has changed.
+     */
+    public function getCellByColumnAndRow($columnIndex, $row): Cell
+    {
+        return $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row);
+    }
+
+    /**
+     * Create a new cell at the specified coordinate.
+     *
+     * @param string $coordinate Coordinate of the cell
+     *
+     * @return Cell Cell that was created
+     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
+     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
+     *              another cell using getCell() or any of its variants, the newly selected cell becomes
+     *              the "active" cell, and any previous assignment becomes a disconnected reference because
+     *              the active cell has changed.
+     */
+    public function createNewCell($coordinate): Cell
+    {
+        [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate);
+        $cell = new Cell(null, DataType::TYPE_NULL, $this);
+        $this->cellCollection->add($coordinate, $cell);
+
+        // Coordinates
+        if ($column > $this->cachedHighestColumn) {
+            $this->cachedHighestColumn = $column;
+        }
+        if ($row > $this->cachedHighestRow) {
+            $this->cachedHighestRow = $row;
+        }
+
+        // Cell needs appropriate xfIndex from dimensions records
+        //    but don't create dimension records if they don't already exist
+        $rowDimension = $this->rowDimensions[$row] ?? null;
+        $columnDimension = $this->columnDimensions[$columnString] ?? null;
+
+        if ($rowDimension !== null) {
+            $rowXf = (int) $rowDimension->getXfIndex();
+            if ($rowXf > 0) {
+                // then there is a row dimension with explicit style, assign it to the cell
+                $cell->setXfIndex($rowXf);
+            }
+        } elseif ($columnDimension !== null) {
+            $colXf = (int) $columnDimension->getXfIndex();
+            if ($colXf > 0) {
+                // then there is a column dimension, assign it to the cell
+                $cell->setXfIndex($colXf);
+            }
+        }
+
+        return $cell;
+    }
+
+    /**
+     * Does the cell at a specific coordinate exist?
+     *
+     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
+     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     */
+    public function cellExists($coordinate): bool
+    {
+        $cellAddress = Validations::validateCellAddress($coordinate);
+        /** @var Worksheet $sheet */
+        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
+
+        return $sheet->cellCollection->has($finalCoordinate);
+    }
+
+    /**
+     * Cell at a specific coordinate by using numeric cell coordinates exists?
+     *
+     * @deprecated 1.23.0
+     *      Use the cellExists() method with a cell address such as 'C5' instead;,
+     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @see Worksheet::cellExists()
+     *
+     * @param int $columnIndex Numeric column coordinate of the cell
+     * @param int $row Numeric row coordinate of the cell
+     */
+    public function cellExistsByColumnAndRow($columnIndex, $row): bool
+    {
+        return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
+    }
+
+    /**
+     * Get row dimension at a specific row.
+     *
+     * @param int $row Numeric index of the row
+     */
+    public function getRowDimension(int $row): RowDimension
+    {
+        // Get row dimension
+        if (!isset($this->rowDimensions[$row])) {
+            $this->rowDimensions[$row] = new RowDimension($row);
+
+            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
+        }
+
+        return $this->rowDimensions[$row];
+    }
+
+    public function rowDimensionExists(int $row): bool
+    {
+        return isset($this->rowDimensions[$row]);
+    }
+
+    /**
+     * Get column dimension at a specific column.
+     *
+     * @param string $column String index of the column eg: 'A'
+     */
+    public function getColumnDimension(string $column): ColumnDimension
+    {
+        // Uppercase coordinate
+        $column = strtoupper($column);
+
+        // Fetch dimensions
+        if (!isset($this->columnDimensions[$column])) {
+            $this->columnDimensions[$column] = new ColumnDimension($column);
+
+            $columnIndex = Coordinate::columnIndexFromString($column);
+            if ($this->cachedHighestColumn < $columnIndex) {
+                $this->cachedHighestColumn = $columnIndex;
+            }
+        }
+
+        return $this->columnDimensions[$column];
+    }
+
+    /**
+     * Get column dimension at a specific column by using numeric cell coordinates.
+     *
+     * @param int $columnIndex Numeric column coordinate of the cell
+     */
+    public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
+    {
+        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
+    }
+
+    /**
+     * Get styles.
+     *
+     * @return Style[]
+     */
+    public function getStyles()
+    {
+        return $this->styles;
+    }
+
+    /**
+     * Get style for cell.
+     *
+     * @param AddressRange|array<int>|CellAddress|int|string $cellCoordinate
+     *              A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or a CellAddress or AddressRange object.
+     */
+    public function getStyle($cellCoordinate): Style
+    {
+        $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
+
+        // set this sheet as active
+        $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
+
+        // set cell coordinate as active
+        $this->setSelectedCells($cellCoordinate);
+
+        return $this->getParentOrThrow()->getCellXfSupervisor();
+    }
+
+    /**
+     * Get style for cell by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the getStyle() method with a cell address range such as 'C5:F8' instead;,
+     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *          or an AddressRange object.
+     * @see Worksheet::getStyle()
+     *
+     * @param int $columnIndex1 Numeric column coordinate of the cell
+     * @param int $row1 Numeric row coordinate of the cell
+     * @param null|int $columnIndex2 Numeric column coordinate of the range cell
+     * @param null|int $row2 Numeric row coordinate of the range cell
+     *
+     * @return Style
+     */
+    public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
+    {
+        if ($columnIndex2 !== null && $row2 !== null) {
+            $cellRange = new CellRange(
+                CellAddress::fromColumnAndRow($columnIndex1, $row1),
+                CellAddress::fromColumnAndRow($columnIndex2, $row2)
+            );
+
+            return $this->getStyle($cellRange);
+        }
+
+        return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1));
+    }
+
+    /**
+     * Get conditional styles for a cell.
+     *
+     * @param string $coordinate eg: 'A1' or 'A1:A3'.
+     *          If a single cell is referenced, then the array of conditional styles will be returned if the cell is
+     *               included in a conditional style range.
+     *          If a range of cells is specified, then the styles will only be returned if the range matches the entire
+     *               range of the conditional.
+     *
+     * @return Conditional[]
+     */
+    public function getConditionalStyles(string $coordinate): array
+    {
+        $coordinate = strtoupper($coordinate);
+        if (strpos($coordinate, ':') !== false) {
+            return $this->conditionalStylesCollection[$coordinate] ?? [];
+        }
+
+        $cell = $this->getCell($coordinate);
+        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
+            if ($cell->isInRange($conditionalRange)) {
+                return $this->conditionalStylesCollection[$conditionalRange];
+            }
+        }
+
+        return [];
+    }
+
+    public function getConditionalRange(string $coordinate): ?string
+    {
+        $coordinate = strtoupper($coordinate);
+        $cell = $this->getCell($coordinate);
+        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
+            if ($cell->isInRange($conditionalRange)) {
+                return $conditionalRange;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Do conditional styles exist for this cell?
+     *
+     * @param string $coordinate eg: 'A1' or 'A1:A3'.
+     *          If a single cell is specified, then this method will return true if that cell is included in a
+     *               conditional style range.
+     *          If a range of cells is specified, then true will only be returned if the range matches the entire
+     *               range of the conditional.
+     */
+    public function conditionalStylesExists($coordinate): bool
+    {
+        $coordinate = strtoupper($coordinate);
+        if (strpos($coordinate, ':') !== false) {
+            return isset($this->conditionalStylesCollection[$coordinate]);
+        }
+
+        $cell = $this->getCell($coordinate);
+        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
+            if ($cell->isInRange($conditionalRange)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Removes conditional styles for a cell.
+     *
+     * @param string $coordinate eg: 'A1'
+     *
+     * @return $this
+     */
+    public function removeConditionalStyles($coordinate)
+    {
+        unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
+
+        return $this;
+    }
+
+    /**
+     * Get collection of conditional styles.
+     *
+     * @return array
+     */
+    public function getConditionalStylesCollection()
+    {
+        return $this->conditionalStylesCollection;
+    }
+
+    /**
+     * Set conditional styles.
+     *
+     * @param string $coordinate eg: 'A1'
+     * @param Conditional[] $styles
+     *
+     * @return $this
+     */
+    public function setConditionalStyles($coordinate, $styles)
+    {
+        $this->conditionalStylesCollection[strtoupper($coordinate)] = $styles;
+
+        return $this;
+    }
+
+    /**
+     * Duplicate cell style to a range of cells.
+     *
+     * Please note that this will overwrite existing cell styles for cells in range!
+     *
+     * @param Style $style Cell style to duplicate
+     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
+     *
+     * @return $this
+     */
+    public function duplicateStyle(Style $style, $range)
+    {
+        // Add the style to the workbook if necessary
+        $workbook = $this->getParentOrThrow();
+        if ($existingStyle = $workbook->getCellXfByHashCode($style->getHashCode())) {
+            // there is already such cell Xf in our collection
+            $xfIndex = $existingStyle->getIndex();
+        } else {
+            // we don't have such a cell Xf, need to add
+            $workbook->addCellXf($style);
+            $xfIndex = $style->getIndex();
+        }
+
+        // Calculate range outer borders
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
+
+        // Make sure we can loop upwards on rows and columns
+        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
+            $tmp = $rangeStart;
+            $rangeStart = $rangeEnd;
+            $rangeEnd = $tmp;
+        }
+
+        // Loop through cells and apply styles
+        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
+            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
+                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Duplicate conditional style to a range of cells.
+     *
+     * Please note that this will overwrite existing cell styles for cells in range!
+     *
+     * @param Conditional[] $styles Cell style to duplicate
+     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
+     *
+     * @return $this
+     */
+    public function duplicateConditionalStyle(array $styles, $range = '')
+    {
+        foreach ($styles as $cellStyle) {
+            if (!($cellStyle instanceof Conditional)) {
+                throw new Exception('Style is not a conditional style');
+            }
+        }
+
+        // Calculate range outer borders
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
+
+        // Make sure we can loop upwards on rows and columns
+        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
+            $tmp = $rangeStart;
+            $rangeStart = $rangeEnd;
+            $rangeEnd = $tmp;
+        }
+
+        // Loop through cells and apply styles
+        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
+            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
+                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set break on a cell.
+     *
+     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
+     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @param int $break Break type (type of Worksheet::BREAK_*)
+     *
+     * @return $this
+     */
+    public function setBreak($coordinate, $break, int $max = -1)
+    {
+        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
+
+        if ($break === self::BREAK_NONE) {
+            unset($this->rowBreaks[$cellAddress], $this->columnBreaks[$cellAddress]);
+        } elseif ($break === self::BREAK_ROW) {
+            $this->rowBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
+        } elseif ($break === self::BREAK_COLUMN) {
+            $this->columnBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set break on a cell by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the setBreak() method with a cell address such as 'C5' instead;,
+     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @see Worksheet::setBreak()
+     *
+     * @param int $columnIndex Numeric column coordinate of the cell
+     * @param int $row Numeric row coordinate of the cell
+     * @param int $break Break type (type of Worksheet::BREAK_*)
+     *
+     * @return $this
+     */
+    public function setBreakByColumnAndRow($columnIndex, $row, $break)
+    {
+        return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break);
+    }
+
+    /**
+     * Get breaks.
+     *
+     * @return int[]
+     */
+    public function getBreaks()
+    {
+        $breaks = [];
+        /** @var callable */
+        $compareFunction = [self::class, 'compareRowBreaks'];
+        uksort($this->rowBreaks, $compareFunction);
+        foreach ($this->rowBreaks as $break) {
+            $breaks[$break->getCoordinate()] = self::BREAK_ROW;
+        }
+        /** @var callable */
+        $compareFunction = [self::class, 'compareColumnBreaks'];
+        uksort($this->columnBreaks, $compareFunction);
+        foreach ($this->columnBreaks as $break) {
+            $breaks[$break->getCoordinate()] = self::BREAK_COLUMN;
+        }
+
+        return $breaks;
+    }
+
+    /**
+     * Get row breaks.
+     *
+     * @return PageBreak[]
+     */
+    public function getRowBreaks()
+    {
+        /** @var callable */
+        $compareFunction = [self::class, 'compareRowBreaks'];
+        uksort($this->rowBreaks, $compareFunction);
+
+        return $this->rowBreaks;
+    }
+
+    protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
+    {
+        $row1 = Coordinate::indexesFromString($coordinate1)[1];
+        $row2 = Coordinate::indexesFromString($coordinate2)[1];
+
+        return $row1 - $row2;
+    }
+
+    protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
+    {
+        $column1 = Coordinate::indexesFromString($coordinate1)[0];
+        $column2 = Coordinate::indexesFromString($coordinate2)[0];
+
+        return $column1 - $column2;
+    }
+
+    /**
+     * Get column breaks.
+     *
+     * @return PageBreak[]
+     */
+    public function getColumnBreaks()
+    {
+        /** @var callable */
+        $compareFunction = [self::class, 'compareColumnBreaks'];
+        uksort($this->columnBreaks, $compareFunction);
+
+        return $this->columnBreaks;
+    }
+
+    /**
+     * Set merge on a cell range.
+     *
+     * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or an AddressRange.
+     * @param string $behaviour How the merged cells should behave.
+     *               Possible values are:
+     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
+     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
+     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
+     *
+     * @return $this
+     */
+    public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY)
+    {
+        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
+
+        if (strpos($range, ':') === false) {
+            $range .= ":{$range}";
+        }
+
+        if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) !== 1) {
+            throw new Exception('Merge must be on a valid range of cells.');
+        }
+
+        $this->mergeCells[$range] = $range;
+        $firstRow = (int) $matches[2];
+        $lastRow = (int) $matches[4];
+        $firstColumn = $matches[1];
+        $lastColumn = $matches[3];
+        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
+        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
+        $numberRows = $lastRow - $firstRow;
+        $numberColumns = $lastColumnIndex - $firstColumnIndex;
+
+        if ($numberRows === 1 && $numberColumns === 1) {
+            return $this;
+        }
+
+        // create upper left cell if it does not already exist
+        $upperLeft = "{$firstColumn}{$firstRow}";
+        if (!$this->cellExists($upperLeft)) {
+            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
+        }
+
+        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
+            // Blank out the rest of the cells in the range (if they exist)
+            if ($numberRows > $numberColumns) {
+                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
+            } else {
+                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
+            }
+        }
+
+        return $this;
+    }
+
+    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
+    {
+        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
+            ? [$this->getCell($upperLeft)->getFormattedValue()]
+            : [];
+
+        foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) {
+            $iterator = $column->getCellIterator($firstRow);
+            $iterator->setIterateOnlyExistingCells(true);
+            foreach ($iterator as $cell) {
+                if ($cell !== null) {
+                    $row = $cell->getRow();
+                    if ($row > $lastRow) {
+                        break;
+                    }
+                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
+                }
+            }
+        }
+
+        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
+            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
+        }
+    }
+
+    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
+    {
+        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
+            ? [$this->getCell($upperLeft)->getFormattedValue()]
+            : [];
+
+        foreach ($this->getRowIterator($firstRow, $lastRow) as $row) {
+            $iterator = $row->getCellIterator($firstColumn);
+            $iterator->setIterateOnlyExistingCells(true);
+            foreach ($iterator as $cell) {
+                if ($cell !== null) {
+                    $column = $cell->getColumn();
+                    $columnIndex = Coordinate::columnIndexFromString($column);
+                    if ($columnIndex > $lastColumnIndex) {
+                        break;
+                    }
+                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
+                }
+            }
+        }
+
+        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
+            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
+        }
+    }
+
+    public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array
+    {
+        if ($cell->getCoordinate() !== $upperLeft) {
+            Calculation::getInstance($cell->getWorksheet()->getParentOrThrow())->flushInstance();
+            if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
+                $cellValue = $cell->getFormattedValue();
+                if ($cellValue !== '') {
+                    $leftCellValue[] = $cellValue;
+                }
+            }
+            $cell->setValueExplicit(null, DataType::TYPE_NULL);
+        }
+
+        return $leftCellValue;
+    }
+
+    /**
+     * Set merge on a cell range by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the mergeCells() method with a cell address range such as 'C5:F8' instead;,
+     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *          or an AddressRange object.
+     * @see Worksheet::mergeCells()
+     *
+     * @param int $columnIndex1 Numeric column coordinate of the first cell
+     * @param int $row1 Numeric row coordinate of the first cell
+     * @param int $columnIndex2 Numeric column coordinate of the last cell
+     * @param int $row2 Numeric row coordinate of the last cell
+     * @param string $behaviour How the merged cells should behave.
+     *               Possible values are:
+     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
+     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
+     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
+     *
+     * @return $this
+     */
+    public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY)
+    {
+        $cellRange = new CellRange(
+            CellAddress::fromColumnAndRow($columnIndex1, $row1),
+            CellAddress::fromColumnAndRow($columnIndex2, $row2)
+        );
+
+        return $this->mergeCells($cellRange, $behaviour);
+    }
+
+    /**
+     * Remove merge on a cell range.
+     *
+     * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or an AddressRange.
+     *
+     * @return $this
+     */
+    public function unmergeCells($range)
+    {
+        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
+
+        if (strpos($range, ':') !== false) {
+            if (isset($this->mergeCells[$range])) {
+                unset($this->mergeCells[$range]);
+            } else {
+                throw new Exception('Cell range ' . $range . ' not known as merged.');
+            }
+        } else {
+            throw new Exception('Merge can only be removed from a range of cells.');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Remove merge on a cell range by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the unmergeCells() method with a cell address range such as 'C5:F8' instead;,
+     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *          or an AddressRange object.
+     * @see Worksheet::unmergeCells()
+     *
+     * @param int $columnIndex1 Numeric column coordinate of the first cell
+     * @param int $row1 Numeric row coordinate of the first cell
+     * @param int $columnIndex2 Numeric column coordinate of the last cell
+     * @param int $row2 Numeric row coordinate of the last cell
+     *
+     * @return $this
+     */
+    public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
+    {
+        $cellRange = new CellRange(
+            CellAddress::fromColumnAndRow($columnIndex1, $row1),
+            CellAddress::fromColumnAndRow($columnIndex2, $row2)
+        );
+
+        return $this->unmergeCells($cellRange);
+    }
+
+    /**
+     * Get merge cells array.
+     *
+     * @return string[]
+     */
+    public function getMergeCells()
+    {
+        return $this->mergeCells;
+    }
+
+    /**
+     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
+     * a single cell range.
+     *
+     * @param string[] $mergeCells
+     *
+     * @return $this
+     */
+    public function setMergeCells(array $mergeCells)
+    {
+        $this->mergeCells = $mergeCells;
+
+        return $this;
+    }
+
+    /**
+     * Set protection on a cell or cell range.
+     *
+     * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or a CellAddress or AddressRange object.
+     * @param string $password Password to unlock the protection
+     * @param bool $alreadyHashed If the password has already been hashed, set this to true
+     *
+     * @return $this
+     */
+    public function protectCells($range, $password, $alreadyHashed = false)
+    {
+        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
+
+        if (!$alreadyHashed) {
+            $password = Shared\PasswordHasher::hashPassword($password);
+        }
+        $this->protectedCells[$range] = $password;
+
+        return $this;
+    }
+
+    /**
+     * Set protection on a cell range by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the protectCells() method with a cell address range such as 'C5:F8' instead;,
+     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *          or an AddressRange object.
+     * @see Worksheet::protectCells()
+     *
+     * @param int $columnIndex1 Numeric column coordinate of the first cell
+     * @param int $row1 Numeric row coordinate of the first cell
+     * @param int $columnIndex2 Numeric column coordinate of the last cell
+     * @param int $row2 Numeric row coordinate of the last cell
+     * @param string $password Password to unlock the protection
+     * @param bool $alreadyHashed If the password has already been hashed, set this to true
+     *
+     * @return $this
+     */
+    public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false)
+    {
+        $cellRange = new CellRange(
+            CellAddress::fromColumnAndRow($columnIndex1, $row1),
+            CellAddress::fromColumnAndRow($columnIndex2, $row2)
+        );
+
+        return $this->protectCells($cellRange, $password, $alreadyHashed);
+    }
+
+    /**
+     * Remove protection on a cell or cell range.
+     *
+     * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or a CellAddress or AddressRange object.
+     *
+     * @return $this
+     */
+    public function unprotectCells($range)
+    {
+        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
+
+        if (isset($this->protectedCells[$range])) {
+            unset($this->protectedCells[$range]);
+        } else {
+            throw new Exception('Cell range ' . $range . ' not known as protected.');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Remove protection on a cell range by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the unprotectCells() method with a cell address range such as 'C5:F8' instead;,
+     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *          or an AddressRange object.
+     * @see Worksheet::unprotectCells()
+     *
+     * @param int $columnIndex1 Numeric column coordinate of the first cell
+     * @param int $row1 Numeric row coordinate of the first cell
+     * @param int $columnIndex2 Numeric column coordinate of the last cell
+     * @param int $row2 Numeric row coordinate of the last cell
+     *
+     * @return $this
+     */
+    public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
+    {
+        $cellRange = new CellRange(
+            CellAddress::fromColumnAndRow($columnIndex1, $row1),
+            CellAddress::fromColumnAndRow($columnIndex2, $row2)
+        );
+
+        return $this->unprotectCells($cellRange);
+    }
+
+    /**
+     * Get protected cells.
+     *
+     * @return string[]
+     */
+    public function getProtectedCells()
+    {
+        return $this->protectedCells;
+    }
+
+    /**
+     * Get Autofilter.
+     *
+     * @return AutoFilter
+     */
+    public function getAutoFilter()
+    {
+        return $this->autoFilter;
+    }
+
+    /**
+     * Set AutoFilter.
+     *
+     * @param AddressRange|array<int>|AutoFilter|string $autoFilterOrRange
+     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or an AddressRange.
+     *
+     * @return $this
+     */
+    public function setAutoFilter($autoFilterOrRange)
+    {
+        if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
+            $this->autoFilter = $autoFilterOrRange;
+        } else {
+            $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
+
+            $this->autoFilter->setRange($cellRange);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set Autofilter Range by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the setAutoFilter() method with a cell address range such as 'C5:F8' instead;,
+     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *          or an AddressRange object or AutoFilter object.
+     * @see Worksheet::setAutoFilter()
+     *
+     * @param int $columnIndex1 Numeric column coordinate of the first cell
+     * @param int $row1 Numeric row coordinate of the first cell
+     * @param int $columnIndex2 Numeric column coordinate of the second cell
+     * @param int $row2 Numeric row coordinate of the second cell
+     *
+     * @return $this
+     */
+    public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
+    {
+        $cellRange = new CellRange(
+            CellAddress::fromColumnAndRow($columnIndex1, $row1),
+            CellAddress::fromColumnAndRow($columnIndex2, $row2)
+        );
+
+        return $this->setAutoFilter($cellRange);
+    }
+
+    /**
+     * Remove autofilter.
+     */
+    public function removeAutoFilter(): self
+    {
+        $this->autoFilter->setRange('');
+
+        return $this;
+    }
+
+    /**
+     * Get collection of Tables.
+     *
+     * @return ArrayObject<int, Table>
+     */
+    public function getTableCollection()
+    {
+        return $this->tableCollection;
+    }
+
+    /**
+     * Add Table.
+     *
+     * @return $this
+     */
+    public function addTable(Table $table): self
+    {
+        $table->setWorksheet($this);
+        $this->tableCollection[] = $table;
+
+        return $this;
+    }
+
+    /**
+     * @return string[] array of Table names
+     */
+    public function getTableNames(): array
+    {
+        $tableNames = [];
+
+        foreach ($this->tableCollection as $table) {
+            /** @var Table $table */
+            $tableNames[] = $table->getName();
+        }
+
+        return $tableNames;
+    }
+
+    /** @var null|Table */
+    private static $scrutinizerNullTable;
+
+    /** @var null|int */
+    private static $scrutinizerNullInt;
+
+    /**
+     * @param string $name the table name to search
+     *
+     * @return null|Table The table from the tables collection, or null if not found
+     */
+    public function getTableByName(string $name): ?Table
+    {
+        $tableIndex = $this->getTableIndexByName($name);
+
+        return ($tableIndex === null) ? self::$scrutinizerNullTable : $this->tableCollection[$tableIndex];
+    }
+
+    /**
+     * @param string $name the table name to search
+     *
+     * @return null|int The index of the located table in the tables collection, or null if not found
+     */
+    protected function getTableIndexByName(string $name): ?int
+    {
+        $name = Shared\StringHelper::strToUpper($name);
+        foreach ($this->tableCollection as $index => $table) {
+            /** @var Table $table */
+            if (Shared\StringHelper::strToUpper($table->getName()) === $name) {
+                return $index;
+            }
+        }
+
+        return self::$scrutinizerNullInt;
+    }
+
+    /**
+     * Remove Table by name.
+     *
+     * @param string $name Table name
+     *
+     * @return $this
+     */
+    public function removeTableByName(string $name): self
+    {
+        $tableIndex = $this->getTableIndexByName($name);
+
+        if ($tableIndex !== null) {
+            unset($this->tableCollection[$tableIndex]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Remove collection of Tables.
+     */
+    public function removeTableCollection(): self
+    {
+        $this->tableCollection = new ArrayObject();
+
+        return $this;
+    }
+
+    /**
+     * Get Freeze Pane.
+     *
+     * @return null|string
+     */
+    public function getFreezePane()
+    {
+        return $this->freezePane;
+    }
+
+    /**
+     * Freeze Pane.
+     *
+     * Examples:
+     *
+     *     - A2 will freeze the rows above cell A2 (i.e row 1)
+     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
+     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
+     *
+     * @param null|array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
+     *            or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     *        Passing a null value for this argument will clear any existing freeze pane for this worksheet.
+     * @param null|array<int>|CellAddress|string $topLeftCell default position of the right bottom pane
+     *            Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
+     *            or a CellAddress object.
+     *
+     * @return $this
+     */
+    public function freezePane($coordinate, $topLeftCell = null)
+    {
+        $cellAddress = ($coordinate !== null)
+            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
+            : null;
+        if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
+            throw new Exception('Freeze pane can not be set on a range of cells.');
+        }
+        $topLeftCell = ($topLeftCell !== null)
+            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
+            : null;
+
+        if ($cellAddress !== null && $topLeftCell === null) {
+            $coordinate = Coordinate::coordinateFromString($cellAddress);
+            $topLeftCell = $coordinate[0] . $coordinate[1];
+        }
+
+        $this->freezePane = $cellAddress;
+        $this->topLeftCell = $topLeftCell;
+
+        return $this;
+    }
+
+    public function setTopLeftCell(string $topLeftCell): self
+    {
+        $this->topLeftCell = $topLeftCell;
+
+        return $this;
+    }
+
+    /**
+     * Freeze Pane by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the freezePane() method with a cell address such as 'C5' instead;,
+     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @see Worksheet::freezePane()
+     *
+     * @param int $columnIndex Numeric column coordinate of the cell
+     * @param int $row Numeric row coordinate of the cell
+     *
+     * @return $this
+     */
+    public function freezePaneByColumnAndRow($columnIndex, $row)
+    {
+        return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row);
+    }
+
+    /**
+     * Unfreeze Pane.
+     *
+     * @return $this
+     */
+    public function unfreezePane()
+    {
+        return $this->freezePane(null);
+    }
+
+    /**
+     * Get the default position of the right bottom pane.
+     *
+     * @return null|string
+     */
+    public function getTopLeftCell()
+    {
+        return $this->topLeftCell;
+    }
+
+    /**
+     * Insert a new row, updating all possible related data.
+     *
+     * @param int $before Insert before this row number
+     * @param int $numberOfRows Number of new rows to insert
+     *
+     * @return $this
+     */
+    public function insertNewRowBefore(int $before, int $numberOfRows = 1)
+    {
+        if ($before >= 1) {
+            $objReferenceHelper = ReferenceHelper::getInstance();
+            $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
+        } else {
+            throw new Exception('Rows can only be inserted before at least row 1.');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Insert a new column, updating all possible related data.
+     *
+     * @param string $before Insert before this column Name, eg: 'A'
+     * @param int $numberOfColumns Number of new columns to insert
+     *
+     * @return $this
+     */
+    public function insertNewColumnBefore(string $before, int $numberOfColumns = 1)
+    {
+        if (!is_numeric($before)) {
+            $objReferenceHelper = ReferenceHelper::getInstance();
+            $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
+        } else {
+            throw new Exception('Column references should not be numeric.');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Insert a new column, updating all possible related data.
+     *
+     * @param int $beforeColumnIndex Insert before this column ID (numeric column coordinate of the cell)
+     * @param int $numberOfColumns Number of new columns to insert
+     *
+     * @return $this
+     */
+    public function insertNewColumnBeforeByIndex(int $beforeColumnIndex, int $numberOfColumns = 1)
+    {
+        if ($beforeColumnIndex >= 1) {
+            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
+        }
+
+        throw new Exception('Columns can only be inserted before at least column A (1).');
+    }
+
+    /**
+     * Delete a row, updating all possible related data.
+     *
+     * @param int $row Remove rows, starting with this row number
+     * @param int $numberOfRows Number of rows to remove
+     *
+     * @return $this
+     */
+    public function removeRow(int $row, int $numberOfRows = 1)
+    {
+        if ($row < 1) {
+            throw new Exception('Rows to be deleted should at least start from row 1.');
+        }
+
+        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
+        $highestRow = $this->getHighestDataRow();
+        $removedRowsCounter = 0;
+
+        for ($r = 0; $r < $numberOfRows; ++$r) {
+            if ($row + $r <= $highestRow) {
+                $this->getCellCollection()->removeRow($row + $r);
+                ++$removedRowsCounter;
+            }
+        }
+
+        $objReferenceHelper = ReferenceHelper::getInstance();
+        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
+        for ($r = 0; $r < $removedRowsCounter; ++$r) {
+            $this->getCellCollection()->removeRow($highestRow);
+            --$highestRow;
+        }
+
+        $this->rowDimensions = $holdRowDimensions;
+
+        return $this;
+    }
+
+    private function removeRowDimensions(int $row, int $numberOfRows): array
+    {
+        $highRow = $row + $numberOfRows - 1;
+        $holdRowDimensions = [];
+        foreach ($this->rowDimensions as $rowDimension) {
+            $num = $rowDimension->getRowIndex();
+            if ($num < $row) {
+                $holdRowDimensions[$num] = $rowDimension;
+            } elseif ($num > $highRow) {
+                $num -= $numberOfRows;
+                $cloneDimension = clone $rowDimension;
+                $cloneDimension->setRowIndex(/** @scrutinizer ignore-type */ $num);
+                $holdRowDimensions[$num] = $cloneDimension;
+            }
+        }
+
+        return $holdRowDimensions;
+    }
+
+    /**
+     * Remove a column, updating all possible related data.
+     *
+     * @param string $column Remove columns starting with this column name, eg: 'A'
+     * @param int $numberOfColumns Number of columns to remove
+     *
+     * @return $this
+     */
+    public function removeColumn(string $column, int $numberOfColumns = 1)
+    {
+        if (is_numeric($column)) {
+            throw new Exception('Column references should not be numeric.');
+        }
+
+        $highestColumn = $this->getHighestDataColumn();
+        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
+        $pColumnIndex = Coordinate::columnIndexFromString($column);
+
+        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
+
+        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
+        $objReferenceHelper = ReferenceHelper::getInstance();
+        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
+
+        $this->columnDimensions = $holdColumnDimensions;
+
+        if ($pColumnIndex > $highestColumnIndex) {
+            return $this;
+        }
+
+        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
+
+        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
+            $this->getCellCollection()->removeColumn($highestColumn);
+            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
+        }
+
+        $this->garbageCollect();
+
+        return $this;
+    }
+
+    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
+    {
+        $highCol = $pColumnIndex + $numberOfColumns - 1;
+        $holdColumnDimensions = [];
+        foreach ($this->columnDimensions as $columnDimension) {
+            $num = $columnDimension->getColumnNumeric();
+            if ($num < $pColumnIndex) {
+                $str = $columnDimension->getColumnIndex();
+                $holdColumnDimensions[$str] = $columnDimension;
+            } elseif ($num > $highCol) {
+                $cloneDimension = clone $columnDimension;
+                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
+                $str = $cloneDimension->getColumnIndex();
+                $holdColumnDimensions[$str] = $cloneDimension;
+            }
+        }
+
+        return $holdColumnDimensions;
+    }
+
+    /**
+     * Remove a column, updating all possible related data.
+     *
+     * @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
+     * @param int $numColumns Number of columns to remove
+     *
+     * @return $this
+     */
+    public function removeColumnByIndex(int $columnIndex, int $numColumns = 1)
+    {
+        if ($columnIndex >= 1) {
+            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
+        }
+
+        throw new Exception('Columns to be deleted should at least start from column A (1)');
+    }
+
+    /**
+     * Show gridlines?
+     */
+    public function getShowGridlines(): bool
+    {
+        return $this->showGridlines;
+    }
+
+    /**
+     * Set show gridlines.
+     *
+     * @param bool $showGridLines Show gridlines (true/false)
+     *
+     * @return $this
+     */
+    public function setShowGridlines(bool $showGridLines): self
+    {
+        $this->showGridlines = $showGridLines;
+
+        return $this;
+    }
+
+    /**
+     * Print gridlines?
+     */
+    public function getPrintGridlines(): bool
+    {
+        return $this->printGridlines;
+    }
+
+    /**
+     * Set print gridlines.
+     *
+     * @param bool $printGridLines Print gridlines (true/false)
+     *
+     * @return $this
+     */
+    public function setPrintGridlines(bool $printGridLines): self
+    {
+        $this->printGridlines = $printGridLines;
+
+        return $this;
+    }
+
+    /**
+     * Show row and column headers?
+     */
+    public function getShowRowColHeaders(): bool
+    {
+        return $this->showRowColHeaders;
+    }
+
+    /**
+     * Set show row and column headers.
+     *
+     * @param bool $showRowColHeaders Show row and column headers (true/false)
+     *
+     * @return $this
+     */
+    public function setShowRowColHeaders(bool $showRowColHeaders): self
+    {
+        $this->showRowColHeaders = $showRowColHeaders;
+
+        return $this;
+    }
+
+    /**
+     * Show summary below? (Row/Column outlining).
+     */
+    public function getShowSummaryBelow(): bool
+    {
+        return $this->showSummaryBelow;
+    }
+
+    /**
+     * Set show summary below.
+     *
+     * @param bool $showSummaryBelow Show summary below (true/false)
+     *
+     * @return $this
+     */
+    public function setShowSummaryBelow(bool $showSummaryBelow): self
+    {
+        $this->showSummaryBelow = $showSummaryBelow;
+
+        return $this;
+    }
+
+    /**
+     * Show summary right? (Row/Column outlining).
+     */
+    public function getShowSummaryRight(): bool
+    {
+        return $this->showSummaryRight;
+    }
+
+    /**
+     * Set show summary right.
+     *
+     * @param bool $showSummaryRight Show summary right (true/false)
+     *
+     * @return $this
+     */
+    public function setShowSummaryRight(bool $showSummaryRight): self
+    {
+        $this->showSummaryRight = $showSummaryRight;
+
+        return $this;
+    }
+
+    /**
+     * Get comments.
+     *
+     * @return Comment[]
+     */
+    public function getComments()
+    {
+        return $this->comments;
+    }
+
+    /**
+     * Set comments array for the entire sheet.
+     *
+     * @param Comment[] $comments
+     *
+     * @return $this
+     */
+    public function setComments(array $comments): self
+    {
+        $this->comments = $comments;
+
+        return $this;
+    }
+
+    /**
+     * Remove comment from cell.
+     *
+     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
+     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     *
+     * @return $this
+     */
+    public function removeComment($cellCoordinate): self
+    {
+        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
+
+        if (Coordinate::coordinateIsRange($cellAddress)) {
+            throw new Exception('Cell coordinate string can not be a range of cells.');
+        } elseif (strpos($cellAddress, '$') !== false) {
+            throw new Exception('Cell coordinate string must not be absolute.');
+        } elseif ($cellAddress == '') {
+            throw new Exception('Cell coordinate can not be zero-length string.');
+        }
+        // Check if we have a comment for this cell and delete it
+        if (isset($this->comments[$cellAddress])) {
+            unset($this->comments[$cellAddress]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get comment for cell.
+     *
+     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
+     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     */
+    public function getComment($cellCoordinate): Comment
+    {
+        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
+
+        if (Coordinate::coordinateIsRange($cellAddress)) {
+            throw new Exception('Cell coordinate string can not be a range of cells.');
+        } elseif (strpos($cellAddress, '$') !== false) {
+            throw new Exception('Cell coordinate string must not be absolute.');
+        } elseif ($cellAddress == '') {
+            throw new Exception('Cell coordinate can not be zero-length string.');
+        }
+
+        // Check if we already have a comment for this cell.
+        if (isset($this->comments[$cellAddress])) {
+            return $this->comments[$cellAddress];
+        }
+
+        // If not, create a new comment.
+        $newComment = new Comment();
+        $this->comments[$cellAddress] = $newComment;
+
+        return $newComment;
+    }
+
+    /**
+     * Get comment for cell by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the getComment() method with a cell address such as 'C5' instead;,
+     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @see Worksheet::getComment()
+     *
+     * @param int $columnIndex Numeric column coordinate of the cell
+     * @param int $row Numeric row coordinate of the cell
+     */
+    public function getCommentByColumnAndRow($columnIndex, $row): Comment
+    {
+        return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row);
+    }
+
+    /**
+     * Get active cell.
+     *
+     * @return string Example: 'A1'
+     */
+    public function getActiveCell()
+    {
+        return $this->activeCell;
+    }
+
+    /**
+     * Get selected cells.
+     *
+     * @return string
+     */
+    public function getSelectedCells()
+    {
+        return $this->selectedCells;
+    }
+
+    /**
+     * Selected cell.
+     *
+     * @param string $coordinate Cell (i.e. A1)
+     *
+     * @return $this
+     */
+    public function setSelectedCell($coordinate)
+    {
+        return $this->setSelectedCells($coordinate);
+    }
+
+    /**
+     * Select a range of cells.
+     *
+     * @param AddressRange|array<int>|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10'
+     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
+     *              or a CellAddress or AddressRange object.
+     *
+     * @return $this
+     */
+    public function setSelectedCells($coordinate)
+    {
+        if (is_string($coordinate)) {
+            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
+        }
+        $coordinate = Validations::validateCellOrCellRange($coordinate);
+
+        if (Coordinate::coordinateIsRange($coordinate)) {
+            [$first] = Coordinate::splitRange($coordinate);
+            $this->activeCell = $first[0];
+        } else {
+            $this->activeCell = $coordinate;
+        }
+        $this->selectedCells = $coordinate;
+
+        return $this;
+    }
+
+    /**
+     * Selected cell by using numeric cell coordinates.
+     *
+     * @deprecated 1.23.0
+     *      Use the setSelectedCells() method with a cell address such as 'C5' instead;,
+     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
+     * @see Worksheet::setSelectedCells()
+     *
+     * @param int $columnIndex Numeric column coordinate of the cell
+     * @param int $row Numeric row coordinate of the cell
+     *
+     * @return $this
+     */
+    public function setSelectedCellByColumnAndRow($columnIndex, $row)
+    {
+        return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row);
+    }
+
+    /**
+     * Get right-to-left.
+     *
+     * @return bool
+     */
+    public function getRightToLeft()
+    {
+        return $this->rightToLeft;
+    }
+
+    /**
+     * Set right-to-left.
+     *
+     * @param bool $value Right-to-left true/false
+     *
+     * @return $this
+     */
+    public function setRightToLeft($value)
+    {
+        $this->rightToLeft = $value;
+
+        return $this;
+    }
+
+    /**
+     * Fill worksheet from values in array.
+     *
+     * @param array $source Source array
+     * @param mixed $nullValue Value in source array that stands for blank cell
+     * @param string $startCell Insert array starting from this cell address as the top left coordinate
+     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
+     *
+     * @return $this
+     */
+    public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false)
+    {
+        //    Convert a 1-D array to 2-D (for ease of looping)
+        if (!is_array(end($source))) {
+            $source = [$source];
+        }
+
+        // start coordinate
+        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
+
+        // Loop through $source
+        foreach ($source as $rowData) {
+            $currentColumn = $startColumn;
+            foreach ($rowData as $cellValue) {
+                if ($strictNullComparison) {
+                    if ($cellValue !== $nullValue) {
+                        // Set cell value
+                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
+                    }
+                } else {
+                    if ($cellValue != $nullValue) {
+                        // Set cell value
+                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
+                    }
+                }
+                ++$currentColumn;
+            }
+            ++$startRow;
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param mixed $nullValue
+     *
+     * @throws Exception
+     * @throws \PhpOffice\PhpSpreadsheet\Calculation\Exception
+     *
+     * @return mixed
+     */
+    protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, $nullValue)
+    {
+        $returnValue = $nullValue;
+
+        if ($cell->getValue() !== null) {
+            if ($cell->getValue() instanceof RichText) {
+                $returnValue = $cell->getValue()->getPlainText();
+            } else {
+                $returnValue = ($calculateFormulas) ? $cell->getCalculatedValue() : $cell->getValue();
+            }
+
+            if ($formatData) {
+                $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
+                $returnValue = NumberFormat::toFormattedString(
+                    $returnValue,
+                    $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL
+                );
+            }
+        }
+
+        return $returnValue;
+    }
+
+    /**
+     * Create array from a range of cells.
+     *
+     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
+     * @param bool $calculateFormulas Should formulas be calculated?
+     * @param bool $formatData Should formatting be applied to cell values?
+     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
+     *                             True - Return rows and columns indexed by their actual row and column IDs
+     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
+     *                            True - Don't return values for rows/columns that are defined as hidden.
+     */
+    public function rangeToArray(
+        string $range,
+        $nullValue = null,
+        bool $calculateFormulas = true,
+        bool $formatData = true,
+        bool $returnCellRef = false,
+        bool $ignoreHidden = false
+    ): array {
+        $range = Validations::validateCellOrCellRange($range);
+
+        $returnValue = [];
+        //    Identify the range that we need to extract from the worksheet
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
+        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
+        $minRow = $rangeStart[1];
+        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
+        $maxRow = $rangeEnd[1];
+
+        ++$maxCol;
+        // Loop through rows
+        $r = -1;
+        for ($row = $minRow; $row <= $maxRow; ++$row) {
+            if (($ignoreHidden === true) && ($this->getRowDimension($row)->getVisible() === false)) {
+                continue;
+            }
+            $rowRef = $returnCellRef ? $row : ++$r;
+            $c = -1;
+            // Loop through columns in the current row
+            for ($col = $minCol; $col !== $maxCol; ++$col) {
+                if (($ignoreHidden === true) && ($this->getColumnDimension($col)->getVisible() === false)) {
+                    continue;
+                }
+                $columnRef = $returnCellRef ? $col : ++$c;
+                //    Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen
+                //        so we test and retrieve directly against cellCollection
+                $cell = $this->cellCollection->get("{$col}{$row}");
+                $returnValue[$rowRef][$columnRef] = $nullValue;
+                if ($cell !== null) {
+                    $returnValue[$rowRef][$columnRef] = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue);
+                }
+            }
+        }
+
+        // Return
+        return $returnValue;
+    }
+
+    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
+    {
+        $namedRange = DefinedName::resolveName($definedName, $this);
+        if ($namedRange === null) {
+            if ($returnNullIfInvalid) {
+                return null;
+            }
+
+            throw new Exception('Named Range ' . $definedName . ' does not exist.');
+        }
+
+        if ($namedRange->isFormula()) {
+            if ($returnNullIfInvalid) {
+                return null;
+            }
+
+            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
+        }
+
+        if ($namedRange->getLocalOnly()) {
+            $worksheet = $namedRange->getWorksheet();
+            if ($worksheet === null || $this->getHashInt() !== $worksheet->getHashInt()) {
+                if ($returnNullIfInvalid) {
+                    return null;
+                }
+
+                throw new Exception(
+                    'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
+                );
+            }
+        }
+
+        return $namedRange;
+    }
+
+    /**
+     * Create array from a range of cells.
+     *
+     * @param string $definedName The Named Range that should be returned
+     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
+     * @param bool $calculateFormulas Should formulas be calculated?
+     * @param bool $formatData Should formatting be applied to cell values?
+     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
+     *                             True - Return rows and columns indexed by their actual row and column IDs
+     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
+     *                            True - Don't return values for rows/columns that are defined as hidden.
+     */
+    public function namedRangeToArray(
+        string $definedName,
+        $nullValue = null,
+        bool $calculateFormulas = true,
+        bool $formatData = true,
+        bool $returnCellRef = false,
+        bool $ignoreHidden = false
+    ): array {
+        $retVal = [];
+        $namedRange = $this->validateNamedRange($definedName);
+        if ($namedRange !== null) {
+            $cellRange = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
+            $cellRange = str_replace('$', '', $cellRange);
+            $workSheet = $namedRange->getWorksheet();
+            if ($workSheet !== null) {
+                $retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden);
+            }
+        }
+
+        return $retVal;
+    }
+
+    /**
+     * Create array from worksheet.
+     *
+     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
+     * @param bool $calculateFormulas Should formulas be calculated?
+     * @param bool $formatData Should formatting be applied to cell values?
+     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
+     *                             True - Return rows and columns indexed by their actual row and column IDs
+     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
+     *                            True - Don't return values for rows/columns that are defined as hidden.
+     */
+    public function toArray(
+        $nullValue = null,
+        bool $calculateFormulas = true,
+        bool $formatData = true,
+        bool $returnCellRef = false,
+        bool $ignoreHidden = false
+    ): array {
+        // Garbage collect...
+        $this->garbageCollect();
+
+        //    Identify the range that we need to extract from the worksheet
+        $maxCol = $this->getHighestColumn();
+        $maxRow = $this->getHighestRow();
+
+        // Return
+        return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden);
+    }
+
+    /**
+     * Get row iterator.
+     *
+     * @param int $startRow The row number at which to start iterating
+     * @param int $endRow The row number at which to stop iterating
+     *
+     * @return RowIterator
+     */
+    public function getRowIterator($startRow = 1, $endRow = null)
+    {
+        return new RowIterator($this, $startRow, $endRow);
+    }
+
+    /**
+     * Get column iterator.
+     *
+     * @param string $startColumn The column address at which to start iterating
+     * @param string $endColumn The column address at which to stop iterating
+     *
+     * @return ColumnIterator
+     */
+    public function getColumnIterator($startColumn = 'A', $endColumn = null)
+    {
+        return new ColumnIterator($this, $startColumn, $endColumn);
+    }
+
+    /**
+     * Run PhpSpreadsheet garbage collector.
+     *
+     * @return $this
+     */
+    public function garbageCollect()
+    {
+        // Flush cache
+        $this->cellCollection->get('A1');
+
+        // Lookup highest column and highest row if cells are cleaned
+        $colRow = $this->cellCollection->getHighestRowAndColumn();
+        $highestRow = $colRow['row'];
+        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
+
+        // Loop through column dimensions
+        foreach ($this->columnDimensions as $dimension) {
+            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
+        }
+
+        // Loop through row dimensions
+        foreach ($this->rowDimensions as $dimension) {
+            $highestRow = max($highestRow, $dimension->getRowIndex());
+        }
+
+        // Cache values
+        if ($highestColumn < 1) {
+            $this->cachedHighestColumn = 1;
+        } else {
+            $this->cachedHighestColumn = $highestColumn;
+        }
+        $this->cachedHighestRow = $highestRow;
+
+        // Return
+        return $this;
+    }
+
+    /**
+     * @deprecated 3.5.0 use getHashInt instead.
+     *
+     * @return string Hash code
+     */
+    public function getHashCode()
+    {
+        return (string) $this->hash;
+    }
+
+    /**
+     * @return int Hash code
+     */
+    public function getHashInt()
+    {
+        return $this->hash;
+    }
+
+    /**
+     * Extract worksheet title from range.
+     *
+     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
+     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
+     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
+     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
+     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
+     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
+     *
+     * @param string $range Range to extract title from
+     * @param bool $returnRange Return range? (see example)
+     *
+     * @return mixed
+     */
+    public static function extractSheetTitle($range, $returnRange = false)
+    {
+        if (empty($range)) {
+            return $returnRange ? [null, null] : null;
+        }
+
+        // Sheet title included?
+        if (($sep = strrpos($range, '!')) === false) {
+            return $returnRange ? ['', $range] : '';
+        }
+
+        if ($returnRange) {
+            return [substr($range, 0, $sep), substr($range, $sep + 1)];
+        }
+
+        return substr($range, $sep + 1);
+    }
+
+    /**
+     * Get hyperlink.
+     *
+     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
+     *
+     * @return Hyperlink
+     */
+    public function getHyperlink($cellCoordinate)
+    {
+        // return hyperlink if we already have one
+        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
+            return $this->hyperlinkCollection[$cellCoordinate];
+        }
+
+        // else create hyperlink
+        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
+
+        return $this->hyperlinkCollection[$cellCoordinate];
+    }
+
+    /**
+     * Set hyperlink.
+     *
+     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
+     *
+     * @return $this
+     */
+    public function setHyperlink($cellCoordinate, ?Hyperlink $hyperlink = null)
+    {
+        if ($hyperlink === null) {
+            unset($this->hyperlinkCollection[$cellCoordinate]);
+        } else {
+            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Hyperlink at a specific coordinate exists?
+     *
+     * @param string $coordinate eg: 'A1'
+     *
+     * @return bool
+     */
+    public function hyperlinkExists($coordinate)
+    {
+        return isset($this->hyperlinkCollection[$coordinate]);
+    }
+
+    /**
+     * Get collection of hyperlinks.
+     *
+     * @return Hyperlink[]
+     */
+    public function getHyperlinkCollection()
+    {
+        return $this->hyperlinkCollection;
+    }
+
+    /**
+     * Get data validation.
+     *
+     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
+     *
+     * @return DataValidation
+     */
+    public function getDataValidation($cellCoordinate)
+    {
+        // return data validation if we already have one
+        if (isset($this->dataValidationCollection[$cellCoordinate])) {
+            return $this->dataValidationCollection[$cellCoordinate];
+        }
+
+        // else create data validation
+        $this->dataValidationCollection[$cellCoordinate] = new DataValidation();
+
+        return $this->dataValidationCollection[$cellCoordinate];
+    }
+
+    /**
+     * Set data validation.
+     *
+     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
+     *
+     * @return $this
+     */
+    public function setDataValidation($cellCoordinate, ?DataValidation $dataValidation = null)
+    {
+        if ($dataValidation === null) {
+            unset($this->dataValidationCollection[$cellCoordinate]);
+        } else {
+            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Data validation at a specific coordinate exists?
+     *
+     * @param string $coordinate eg: 'A1'
+     *
+     * @return bool
+     */
+    public function dataValidationExists($coordinate)
+    {
+        return isset($this->dataValidationCollection[$coordinate]);
+    }
+
+    /**
+     * Get collection of data validations.
+     *
+     * @return DataValidation[]
+     */
+    public function getDataValidationCollection()
+    {
+        return $this->dataValidationCollection;
+    }
+
+    /**
+     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
+     *
+     * @param string $range
+     *
+     * @return string Adjusted range value
+     */
+    public function shrinkRangeToFit($range)
+    {
+        $maxCol = $this->getHighestColumn();
+        $maxRow = $this->getHighestRow();
+        $maxCol = Coordinate::columnIndexFromString($maxCol);
+
+        $rangeBlocks = explode(' ', $range);
+        foreach ($rangeBlocks as &$rangeSet) {
+            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
+
+            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
+                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
+            }
+            if ($rangeBoundaries[0][1] > $maxRow) {
+                $rangeBoundaries[0][1] = $maxRow;
+            }
+            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
+                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
+            }
+            if ($rangeBoundaries[1][1] > $maxRow) {
+                $rangeBoundaries[1][1] = $maxRow;
+            }
+            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
+        }
+        unset($rangeSet);
+
+        return implode(' ', $rangeBlocks);
+    }
+
+    /**
+     * Get tab color.
+     *
+     * @return Color
+     */
+    public function getTabColor()
+    {
+        if ($this->tabColor === null) {
+            $this->tabColor = new Color();
+        }
+
+        return $this->tabColor;
+    }
+
+    /**
+     * Reset tab color.
+     *
+     * @return $this
+     */
+    public function resetTabColor()
+    {
+        $this->tabColor = null;
+
+        return $this;
+    }
+
+    /**
+     * Tab color set?
+     *
+     * @return bool
+     */
+    public function isTabColorSet()
+    {
+        return $this->tabColor !== null;
+    }
+
+    /**
+     * Copy worksheet (!= clone!).
+     *
+     * @return static
+     */
+    public function copy()
+    {
+        return clone $this;
+    }
+
+    /**
+     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
+     *          exist in the collection for this row. false will be returned otherwise.
+     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
+     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
+     *                  cells, then the row will be considered empty.
+     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
+     *                  string value cells, then the row will be considered empty.
+     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
+     *                  If the only cells in the collection are null value or empty string value cells, then the row
+     *                  will be considered empty.
+     *
+     * @param int $definitionOfEmptyFlags
+     *              Possible Flag Values are:
+     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
+     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
+     */
+    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
+    {
+        try {
+            $iterator = new RowIterator($this, $rowId, $rowId);
+            $iterator->seek($rowId);
+            $row = $iterator->current();
+        } catch (Exception $e) {
+            return true;
+        }
+
+        return $row->isEmpty($definitionOfEmptyFlags);
+    }
+
+    /**
+     * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
+     *          exist in the collection for this column. false will be returned otherwise.
+     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
+     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
+     *                  cells, then the column will be considered empty.
+     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
+     *                  string value cells, then the column will be considered empty.
+     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
+     *                  If the only cells in the collection are null value or empty string value cells, then the column
+     *                  will be considered empty.
+     *
+     * @param int $definitionOfEmptyFlags
+     *              Possible Flag Values are:
+     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
+     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
+     */
+    public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
+    {
+        try {
+            $iterator = new ColumnIterator($this, $columnId, $columnId);
+            $iterator->seek($columnId);
+            $column = $iterator->current();
+        } catch (Exception $e) {
+            return true;
+        }
+
+        return $column->isEmpty($definitionOfEmptyFlags);
+    }
+
+    /**
+     * Implement PHP __clone to create a deep clone, not just a shallow copy.
+     */
+    public function __clone()
+    {
+        // @phpstan-ignore-next-line
+        foreach ($this as $key => $val) {
+            if ($key == 'parent') {
+                continue;
+            }
+
+            if (is_object($val) || (is_array($val))) {
+                if ($key == 'cellCollection') {
+                    $newCollection = $this->cellCollection->cloneCellCollection($this);
+                    $this->cellCollection = $newCollection;
+                } elseif ($key == 'drawingCollection') {
+                    $currentCollection = $this->drawingCollection;
+                    $this->drawingCollection = new ArrayObject();
+                    foreach ($currentCollection as $item) {
+                        if (is_object($item)) {
+                            $newDrawing = clone $item;
+                            $newDrawing->setWorksheet($this);
+                        }
+                    }
+                } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) {
+                    $newAutoFilter = clone $this->autoFilter;
+                    $this->autoFilter = $newAutoFilter;
+                    $this->autoFilter->setParent($this);
+                } else {
+                    $this->{$key} = unserialize(serialize($val));
+                }
+            }
+        }
+        $this->hash = spl_object_id($this);
+    }
+
+    /**
+     * Define the code name of the sheet.
+     *
+     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
+     *                       silently space to underscore)
+     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
+     *                       at parse time (by Readers), where titles can be assumed to be valid.
+     *
+     * @return $this
+     */
+    public function setCodeName($codeName, $validate = true)
+    {
+        // Is this a 'rename' or not?
+        if ($this->getCodeName() == $codeName) {
+            return $this;
+        }
+
+        if ($validate) {
+            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
+
+            // Syntax check
+            // throw an exception if not valid
+            self::checkSheetCodeName($codeName);
+
+            // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
+
+            if ($this->parent !== null) {
+                // Is there already such sheet name?
+                if ($this->parent->sheetCodeNameExists($codeName)) {
+                    // Use name, but append with lowest possible integer
+
+                    if (Shared\StringHelper::countCharacters($codeName) > 29) {
+                        $codeName = Shared\StringHelper::substring($codeName, 0, 29);
+                    }
+                    $i = 1;
+                    while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) {
+                        ++$i;
+                        if ($i == 10) {
+                            if (Shared\StringHelper::countCharacters($codeName) > 28) {
+                                $codeName = Shared\StringHelper::substring($codeName, 0, 28);
+                            }
+                        } elseif ($i == 100) {
+                            if (Shared\StringHelper::countCharacters($codeName) > 27) {
+                                $codeName = Shared\StringHelper::substring($codeName, 0, 27);
+                            }
+                        }
+                    }
+
+                    $codeName .= '_' . $i; // ok, we have a valid name
+                }
+            }
+        }
+
+        $this->codeName = $codeName;
+
+        return $this;
+    }
+
+    /**
+     * Return the code name of the sheet.
+     *
+     * @return null|string
+     */
+    public function getCodeName()
+    {
+        return $this->codeName;
+    }
+
+    /**
+     * Sheet has a code name ?
+     *
+     * @return bool
+     */
+    public function hasCodeName()
+    {
+        return $this->codeName !== null;
+    }
+
+    public static function nameRequiresQuotes(string $sheetName): bool
+    {
+        return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1;
+    }
+}

+ 148 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php

@@ -0,0 +1,148 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer;
+
+abstract class BaseWriter implements IWriter
+{
+    /**
+     * Write charts that are defined in the workbook?
+     * Identifies whether the Writer should write definitions for any charts that exist in the PhpSpreadsheet object.
+     *
+     * @var bool
+     */
+    protected $includeCharts = false;
+
+    /**
+     * Pre-calculate formulas
+     * Forces PhpSpreadsheet to recalculate all formulae in a workbook when saving, so that the pre-calculated values are
+     * immediately available to MS Excel or other office spreadsheet viewer when opening the file.
+     *
+     * @var bool
+     */
+    protected $preCalculateFormulas = true;
+
+    /**
+     * Use disk caching where possible?
+     *
+     * @var bool
+     */
+    private $useDiskCaching = false;
+
+    /**
+     * Disk caching directory.
+     *
+     * @var string
+     */
+    private $diskCachingDirectory = './';
+
+    /**
+     * @var resource
+     */
+    protected $fileHandle;
+
+    /**
+     * @var bool
+     */
+    private $shouldCloseFile;
+
+    public function getIncludeCharts()
+    {
+        return $this->includeCharts;
+    }
+
+    public function setIncludeCharts($includeCharts)
+    {
+        $this->includeCharts = (bool) $includeCharts;
+
+        return $this;
+    }
+
+    public function getPreCalculateFormulas()
+    {
+        return $this->preCalculateFormulas;
+    }
+
+    public function setPreCalculateFormulas($precalculateFormulas)
+    {
+        $this->preCalculateFormulas = (bool) $precalculateFormulas;
+
+        return $this;
+    }
+
+    public function getUseDiskCaching()
+    {
+        return $this->useDiskCaching;
+    }
+
+    public function setUseDiskCaching($useDiskCache, $cacheDirectory = null)
+    {
+        $this->useDiskCaching = $useDiskCache;
+
+        if ($cacheDirectory !== null) {
+            if (is_dir($cacheDirectory)) {
+                $this->diskCachingDirectory = $cacheDirectory;
+            } else {
+                throw new Exception("Directory does not exist: $cacheDirectory");
+            }
+        }
+
+        return $this;
+    }
+
+    public function getDiskCachingDirectory()
+    {
+        return $this->diskCachingDirectory;
+    }
+
+    protected function processFlags(int $flags): void
+    {
+        if (((bool) ($flags & self::SAVE_WITH_CHARTS)) === true) {
+            $this->setIncludeCharts(true);
+        }
+        if (((bool) ($flags & self::DISABLE_PRECALCULATE_FORMULAE)) === true) {
+            $this->setPreCalculateFormulas(false);
+        }
+    }
+
+    /**
+     * Open file handle.
+     *
+     * @param resource|string $filename
+     */
+    public function openFileHandle($filename): void
+    {
+        if (is_resource($filename)) {
+            $this->fileHandle = $filename;
+            $this->shouldCloseFile = false;
+
+            return;
+        }
+
+        $mode = 'wb';
+        $scheme = parse_url($filename, PHP_URL_SCHEME);
+        if ($scheme === 's3') {
+            // @codeCoverageIgnoreStart
+            $mode = 'w';
+            // @codeCoverageIgnoreEnd
+        }
+        $fileHandle = $filename ? fopen($filename, $mode) : false;
+        if ($fileHandle === false) {
+            throw new Exception('Could not open file "' . $filename . '" for writing.');
+        }
+
+        $this->fileHandle = $fileHandle;
+        $this->shouldCloseFile = true;
+    }
+
+    /**
+     * Close file handle only if we opened it ourselves.
+     */
+    protected function maybeCloseFileHandle(): void
+    {
+        if ($this->shouldCloseFile) {
+            if (!fclose($this->fileHandle)) {
+                throw new Exception('Could not close file after writing.');
+            }
+        }
+    }
+}

+ 326 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Csv.php

@@ -0,0 +1,326 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer;
+
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+
+class Csv extends BaseWriter
+{
+    /**
+     * PhpSpreadsheet object.
+     *
+     * @var Spreadsheet
+     */
+    private $spreadsheet;
+
+    /**
+     * Delimiter.
+     *
+     * @var string
+     */
+    private $delimiter = ',';
+
+    /**
+     * Enclosure.
+     *
+     * @var string
+     */
+    private $enclosure = '"';
+
+    /**
+     * Line ending.
+     *
+     * @var string
+     */
+    private $lineEnding = PHP_EOL;
+
+    /**
+     * Sheet index to write.
+     *
+     * @var int
+     */
+    private $sheetIndex = 0;
+
+    /**
+     * Whether to write a UTF8 BOM.
+     *
+     * @var bool
+     */
+    private $useBOM = false;
+
+    /**
+     * Whether to write a Separator line as the first line of the file
+     *     sep=x.
+     *
+     * @var bool
+     */
+    private $includeSeparatorLine = false;
+
+    /**
+     * Whether to write a fully Excel compatible CSV file.
+     *
+     * @var bool
+     */
+    private $excelCompatibility = false;
+
+    /**
+     * Output encoding.
+     *
+     * @var string
+     */
+    private $outputEncoding = '';
+
+    /**
+     * Create a new CSV.
+     */
+    public function __construct(Spreadsheet $spreadsheet)
+    {
+        $this->spreadsheet = $spreadsheet;
+    }
+
+    /**
+     * Save PhpSpreadsheet to file.
+     *
+     * @param resource|string $filename
+     */
+    public function save($filename, int $flags = 0): void
+    {
+        $this->processFlags($flags);
+
+        // Fetch sheet
+        $sheet = $this->spreadsheet->getSheet($this->sheetIndex);
+
+        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
+        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
+        $saveArrayReturnType = Calculation::getArrayReturnType();
+        Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
+
+        // Open file
+        $this->openFileHandle($filename);
+
+        if ($this->excelCompatibility) {
+            $this->setUseBOM(true); //  Enforce UTF-8 BOM Header
+            $this->setIncludeSeparatorLine(true); //  Set separator line
+            $this->setEnclosure('"'); //  Set enclosure to "
+            $this->setDelimiter(';'); //  Set delimiter to a semi-colon
+            $this->setLineEnding("\r\n");
+        }
+
+        if ($this->useBOM) {
+            // Write the UTF-8 BOM code if required
+            fwrite($this->fileHandle, "\xEF\xBB\xBF");
+        }
+
+        if ($this->includeSeparatorLine) {
+            // Write the separator line if required
+            fwrite($this->fileHandle, 'sep=' . $this->getDelimiter() . $this->lineEnding);
+        }
+
+        //    Identify the range that we need to extract from the worksheet
+        $maxCol = $sheet->getHighestDataColumn();
+        $maxRow = $sheet->getHighestDataRow();
+
+        // Write rows to file
+        for ($row = 1; $row <= $maxRow; ++$row) {
+            // Convert the row to an array...
+            $cellsArray = $sheet->rangeToArray('A' . $row . ':' . $maxCol . $row, '', $this->preCalculateFormulas);
+            // ... and write to the file
+            $this->writeLine($this->fileHandle, $cellsArray[0]);
+        }
+
+        $this->maybeCloseFileHandle();
+        Calculation::setArrayReturnType($saveArrayReturnType);
+        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
+    }
+
+    public function getDelimiter(): string
+    {
+        return $this->delimiter;
+    }
+
+    public function setDelimiter(string $delimiter): self
+    {
+        $this->delimiter = $delimiter;
+
+        return $this;
+    }
+
+    public function getEnclosure(): string
+    {
+        return $this->enclosure;
+    }
+
+    public function setEnclosure(string $enclosure = '"'): self
+    {
+        $this->enclosure = $enclosure;
+
+        return $this;
+    }
+
+    public function getLineEnding(): string
+    {
+        return $this->lineEnding;
+    }
+
+    public function setLineEnding(string $lineEnding): self
+    {
+        $this->lineEnding = $lineEnding;
+
+        return $this;
+    }
+
+    /**
+     * Get whether BOM should be used.
+     */
+    public function getUseBOM(): bool
+    {
+        return $this->useBOM;
+    }
+
+    /**
+     * Set whether BOM should be used, typically when non-ASCII characters are used.
+     */
+    public function setUseBOM(bool $useBOM): self
+    {
+        $this->useBOM = $useBOM;
+
+        return $this;
+    }
+
+    /**
+     * Get whether a separator line should be included.
+     */
+    public function getIncludeSeparatorLine(): bool
+    {
+        return $this->includeSeparatorLine;
+    }
+
+    /**
+     * Set whether a separator line should be included as the first line of the file.
+     */
+    public function setIncludeSeparatorLine(bool $includeSeparatorLine): self
+    {
+        $this->includeSeparatorLine = $includeSeparatorLine;
+
+        return $this;
+    }
+
+    /**
+     * Get whether the file should be saved with full Excel Compatibility.
+     */
+    public function getExcelCompatibility(): bool
+    {
+        return $this->excelCompatibility;
+    }
+
+    /**
+     * Set whether the file should be saved with full Excel Compatibility.
+     *
+     * @param bool $excelCompatibility Set the file to be written as a fully Excel compatible csv file
+     *                                Note that this overrides other settings such as useBOM, enclosure and delimiter
+     */
+    public function setExcelCompatibility(bool $excelCompatibility): self
+    {
+        $this->excelCompatibility = $excelCompatibility;
+
+        return $this;
+    }
+
+    public function getSheetIndex(): int
+    {
+        return $this->sheetIndex;
+    }
+
+    public function setSheetIndex(int $sheetIndex): self
+    {
+        $this->sheetIndex = $sheetIndex;
+
+        return $this;
+    }
+
+    public function getOutputEncoding(): string
+    {
+        return $this->outputEncoding;
+    }
+
+    public function setOutputEncoding(string $outputEnconding): self
+    {
+        $this->outputEncoding = $outputEnconding;
+
+        return $this;
+    }
+
+    /** @var bool */
+    private $enclosureRequired = true;
+
+    public function setEnclosureRequired(bool $value): self
+    {
+        $this->enclosureRequired = $value;
+
+        return $this;
+    }
+
+    public function getEnclosureRequired(): bool
+    {
+        return $this->enclosureRequired;
+    }
+
+    /**
+     * Convert boolean to TRUE/FALSE; otherwise return element cast to string.
+     *
+     * @param mixed $element
+     */
+    private static function elementToString($element): string
+    {
+        if (is_bool($element)) {
+            return $element ? 'TRUE' : 'FALSE';
+        }
+
+        return (string) $element;
+    }
+
+    /**
+     * Write line to CSV file.
+     *
+     * @param resource $fileHandle PHP filehandle
+     * @param array $values Array containing values in a row
+     */
+    private function writeLine($fileHandle, array $values): void
+    {
+        // No leading delimiter
+        $delimiter = '';
+
+        // Build the line
+        $line = '';
+
+        foreach ($values as $element) {
+            $element = self::elementToString($element);
+            // Add delimiter
+            $line .= $delimiter;
+            $delimiter = $this->delimiter;
+            // Escape enclosures
+            $enclosure = $this->enclosure;
+            if ($enclosure) {
+                // If enclosure is not required, use enclosure only if
+                // element contains newline, delimiter, or enclosure.
+                if (!$this->enclosureRequired && strpbrk($element, "$delimiter$enclosure\n") === false) {
+                    $enclosure = '';
+                } else {
+                    $element = str_replace($enclosure, $enclosure . $enclosure, $element);
+                }
+            }
+            // Add enclosed string
+            $line .= $enclosure . $element . $enclosure;
+        }
+
+        // Add line ending
+        $line .= $this->lineEnding;
+
+        // Write to file
+        if ($this->outputEncoding != '') {
+            $line = mb_convert_encoding($line, $this->outputEncoding);
+        }
+        fwrite($fileHandle, /** @scrutinizer ignore-type */ $line);
+    }
+}

+ 9 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Exception.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer;
+
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+
+class Exception extends PhpSpreadsheetException
+{
+}

+ 1935 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php

@@ -0,0 +1,1935 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer;
+
+use HTMLPurifier;
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Chart\Chart;
+use PhpOffice\PhpSpreadsheet\Document\Properties;
+use PhpOffice\PhpSpreadsheet\RichText\RichText;
+use PhpOffice\PhpSpreadsheet\RichText\Run;
+use PhpOffice\PhpSpreadsheet\Settings;
+use PhpOffice\PhpSpreadsheet\Shared\Date;
+use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
+use PhpOffice\PhpSpreadsheet\Shared\File;
+use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Borders;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+use PhpOffice\PhpSpreadsheet\Style\Font;
+use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
+use PhpOffice\PhpSpreadsheet\Style\Style;
+use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
+use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
+use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+
+class Html extends BaseWriter
+{
+    /**
+     * Spreadsheet object.
+     *
+     * @var Spreadsheet
+     */
+    protected $spreadsheet;
+
+    /**
+     * Sheet index to write.
+     *
+     * @var null|int
+     */
+    private $sheetIndex = 0;
+
+    /**
+     * Images root.
+     *
+     * @var string
+     */
+    private $imagesRoot = '';
+
+    /**
+     * embed images, or link to images.
+     *
+     * @var bool
+     */
+    protected $embedImages = false;
+
+    /**
+     * Use inline CSS?
+     *
+     * @var bool
+     */
+    private $useInlineCss = false;
+
+    /**
+     * Use embedded CSS?
+     *
+     * @var bool
+     */
+    private $useEmbeddedCSS = true;
+
+    /**
+     * Array of CSS styles.
+     *
+     * @var array
+     */
+    private $cssStyles;
+
+    /**
+     * Array of column widths in points.
+     *
+     * @var array
+     */
+    private $columnWidths;
+
+    /**
+     * Default font.
+     *
+     * @var Font
+     */
+    private $defaultFont;
+
+    /**
+     * Flag whether spans have been calculated.
+     *
+     * @var bool
+     */
+    private $spansAreCalculated = false;
+
+    /**
+     * Excel cells that should not be written as HTML cells.
+     *
+     * @var array
+     */
+    private $isSpannedCell = [];
+
+    /**
+     * Excel cells that are upper-left corner in a cell merge.
+     *
+     * @var array
+     */
+    private $isBaseCell = [];
+
+    /**
+     * Excel rows that should not be written as HTML rows.
+     *
+     * @var array
+     */
+    private $isSpannedRow = [];
+
+    /**
+     * Is the current writer creating PDF?
+     *
+     * @var bool
+     */
+    protected $isPdf = false;
+
+    /**
+     * Is the current writer creating mPDF?
+     *
+     * @var bool
+     */
+    protected $isMPdf = false;
+
+    /**
+     * Generate the Navigation block.
+     *
+     * @var bool
+     */
+    private $generateSheetNavigationBlock = true;
+
+    /**
+     * Callback for editing generated html.
+     *
+     * @var null|callable
+     */
+    private $editHtmlCallback;
+
+    /**
+     * Create a new HTML.
+     */
+    public function __construct(Spreadsheet $spreadsheet)
+    {
+        $this->spreadsheet = $spreadsheet;
+        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
+    }
+
+    /**
+     * Save Spreadsheet to file.
+     *
+     * @param resource|string $filename
+     */
+    public function save($filename, int $flags = 0): void
+    {
+        $this->processFlags($flags);
+
+        // Open file
+        $this->openFileHandle($filename);
+
+        // Write html
+        fwrite($this->fileHandle, $this->generateHTMLAll());
+
+        // Close file
+        $this->maybeCloseFileHandle();
+    }
+
+    /**
+     * Save Spreadsheet as html to variable.
+     *
+     * @return string
+     */
+    public function generateHtmlAll()
+    {
+        // garbage collect
+        $this->spreadsheet->garbageCollect();
+
+        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
+        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
+        $saveArrayReturnType = Calculation::getArrayReturnType();
+        Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
+
+        // Build CSS
+        $this->buildCSS(!$this->useInlineCss);
+
+        $html = '';
+
+        // Write headers
+        $html .= $this->generateHTMLHeader(!$this->useInlineCss);
+
+        // Write navigation (tabs)
+        if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
+            $html .= $this->generateNavigation();
+        }
+
+        // Write data
+        $html .= $this->generateSheetData();
+
+        // Write footer
+        $html .= $this->generateHTMLFooter();
+        $callback = $this->editHtmlCallback;
+        if ($callback) {
+            $html = $callback($html);
+        }
+
+        Calculation::setArrayReturnType($saveArrayReturnType);
+        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
+
+        return $html;
+    }
+
+    /**
+     * Set a callback to edit the entire HTML.
+     *
+     * The callback must accept the HTML as string as first parameter,
+     * and it must return the edited HTML as string.
+     */
+    public function setEditHtmlCallback(?callable $callback): void
+    {
+        $this->editHtmlCallback = $callback;
+    }
+
+    /**
+     * Map VAlign.
+     *
+     * @param string $vAlign Vertical alignment
+     *
+     * @return string
+     */
+    private function mapVAlign($vAlign)
+    {
+        return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? '';
+    }
+
+    /**
+     * Map HAlign.
+     *
+     * @param string $hAlign Horizontal alignment
+     *
+     * @return string
+     */
+    private function mapHAlign($hAlign)
+    {
+        return Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$hAlign] ?? '';
+    }
+
+    const BORDER_ARR = [
+        Border::BORDER_NONE => 'none',
+        Border::BORDER_DASHDOT => '1px dashed',
+        Border::BORDER_DASHDOTDOT => '1px dotted',
+        Border::BORDER_DASHED => '1px dashed',
+        Border::BORDER_DOTTED => '1px dotted',
+        Border::BORDER_DOUBLE => '3px double',
+        Border::BORDER_HAIR => '1px solid',
+        Border::BORDER_MEDIUM => '2px solid',
+        Border::BORDER_MEDIUMDASHDOT => '2px dashed',
+        Border::BORDER_MEDIUMDASHDOTDOT => '2px dotted',
+        Border::BORDER_SLANTDASHDOT => '2px dashed',
+        Border::BORDER_THICK => '3px solid',
+    ];
+
+    /**
+     * Map border style.
+     *
+     * @param int|string $borderStyle Sheet index
+     *
+     * @return string
+     */
+    private function mapBorderStyle($borderStyle)
+    {
+        return array_key_exists($borderStyle, self::BORDER_ARR) ? self::BORDER_ARR[$borderStyle] : '1px solid';
+    }
+
+    /**
+     * Get sheet index.
+     */
+    public function getSheetIndex(): ?int
+    {
+        return $this->sheetIndex;
+    }
+
+    /**
+     * Set sheet index.
+     *
+     * @param int $sheetIndex Sheet index
+     *
+     * @return $this
+     */
+    public function setSheetIndex($sheetIndex)
+    {
+        $this->sheetIndex = $sheetIndex;
+
+        return $this;
+    }
+
+    /**
+     * Get sheet index.
+     *
+     * @return bool
+     */
+    public function getGenerateSheetNavigationBlock()
+    {
+        return $this->generateSheetNavigationBlock;
+    }
+
+    /**
+     * Set sheet index.
+     *
+     * @param bool $generateSheetNavigationBlock Flag indicating whether the sheet navigation block should be generated or not
+     *
+     * @return $this
+     */
+    public function setGenerateSheetNavigationBlock($generateSheetNavigationBlock)
+    {
+        $this->generateSheetNavigationBlock = (bool) $generateSheetNavigationBlock;
+
+        return $this;
+    }
+
+    /**
+     * Write all sheets (resets sheetIndex to NULL).
+     *
+     * @return $this
+     */
+    public function writeAllSheets()
+    {
+        $this->sheetIndex = null;
+
+        return $this;
+    }
+
+    private static function generateMeta(?string $val, string $desc): string
+    {
+        return ($val || $val === '0')
+            ? ('      <meta name="' . $desc . '" content="' . htmlspecialchars($val, Settings::htmlEntityFlags()) . '" />' . PHP_EOL)
+            : '';
+    }
+
+    public const BODY_LINE = '  <body>' . PHP_EOL;
+
+    private const CUSTOM_TO_META = [
+        Properties::PROPERTY_TYPE_BOOLEAN => 'bool',
+        Properties::PROPERTY_TYPE_DATE => 'date',
+        Properties::PROPERTY_TYPE_FLOAT => 'float',
+        Properties::PROPERTY_TYPE_INTEGER => 'int',
+        Properties::PROPERTY_TYPE_STRING => 'string',
+    ];
+
+    /**
+     * Generate HTML header.
+     *
+     * @param bool $includeStyles Include styles?
+     *
+     * @return string
+     */
+    public function generateHTMLHeader($includeStyles = false)
+    {
+        // Construct HTML
+        $properties = $this->spreadsheet->getProperties();
+        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . PHP_EOL;
+        $html .= '<html xmlns="http://www.w3.org/1999/xhtml">' . PHP_EOL;
+        $html .= '  <head>' . PHP_EOL;
+        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . PHP_EOL;
+        $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" />' . PHP_EOL;
+        $html .= '      <title>' . htmlspecialchars($properties->getTitle(), Settings::htmlEntityFlags()) . '</title>' . PHP_EOL;
+        $html .= self::generateMeta($properties->getCreator(), 'author');
+        $html .= self::generateMeta($properties->getTitle(), 'title');
+        $html .= self::generateMeta($properties->getDescription(), 'description');
+        $html .= self::generateMeta($properties->getSubject(), 'subject');
+        $html .= self::generateMeta($properties->getKeywords(), 'keywords');
+        $html .= self::generateMeta($properties->getCategory(), 'category');
+        $html .= self::generateMeta($properties->getCompany(), 'company');
+        $html .= self::generateMeta($properties->getManager(), 'manager');
+        $html .= self::generateMeta($properties->getLastModifiedBy(), 'lastModifiedBy');
+        $date = Date::dateTimeFromTimestamp((string) $properties->getCreated());
+        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
+        $html .= self::generateMeta($date->format(DATE_W3C), 'created');
+        $date = Date::dateTimeFromTimestamp((string) $properties->getModified());
+        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
+        $html .= self::generateMeta($date->format(DATE_W3C), 'modified');
+
+        $customProperties = $properties->getCustomProperties();
+        foreach ($customProperties as $customProperty) {
+            $propertyValue = $properties->getCustomPropertyValue($customProperty);
+            $propertyType = $properties->getCustomPropertyType($customProperty);
+            $propertyQualifier = self::CUSTOM_TO_META[$propertyType] ?? null;
+            if ($propertyQualifier !== null) {
+                if ($propertyType === Properties::PROPERTY_TYPE_BOOLEAN) {
+                    $propertyValue = $propertyValue ? '1' : '0';
+                } elseif ($propertyType === Properties::PROPERTY_TYPE_DATE) {
+                    $date = Date::dateTimeFromTimestamp((string) $propertyValue);
+                    $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
+                    $propertyValue = $date->format(DATE_W3C);
+                } else {
+                    $propertyValue = (string) $propertyValue;
+                }
+                $html .= self::generateMeta($propertyValue, htmlspecialchars("custom.$propertyQualifier.$customProperty"));
+            }
+        }
+
+        if (!empty($properties->getHyperlinkBase())) {
+            $html .= '      <base href="' . htmlspecialchars($properties->getHyperlinkBase()) . '" />' . PHP_EOL;
+        }
+
+        $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true);
+
+        $html .= '  </head>' . PHP_EOL;
+        $html .= '' . PHP_EOL;
+        $html .= self::BODY_LINE;
+
+        return $html;
+    }
+
+    private function generateSheetPrep(): array
+    {
+        // Ensure that Spans have been calculated?
+        $this->calculateSpans();
+
+        // Fetch sheets
+        if ($this->sheetIndex === null) {
+            $sheets = $this->spreadsheet->getAllSheets();
+        } else {
+            $sheets = [$this->spreadsheet->getSheet($this->sheetIndex)];
+        }
+
+        return $sheets;
+    }
+
+    private function generateSheetStarts(Worksheet $sheet, int $rowMin): array
+    {
+        // calculate start of <tbody>, <thead>
+        $tbodyStart = $rowMin;
+        $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
+        if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
+            $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop();
+
+            // we can only support repeating rows that start at top row
+            if ($rowsToRepeatAtTop[0] == 1) {
+                $theadStart = $rowsToRepeatAtTop[0];
+                $theadEnd = $rowsToRepeatAtTop[1];
+                $tbodyStart = $rowsToRepeatAtTop[1] + 1;
+            }
+        }
+
+        return [$theadStart, $theadEnd, $tbodyStart];
+    }
+
+    private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array
+    {
+        // <thead> ?
+        $startTag = ($row == $theadStart) ? ('        <thead>' . PHP_EOL) : '';
+        if (!$startTag) {
+            $startTag = ($row == $tbodyStart) ? ('        <tbody>' . PHP_EOL) : '';
+        }
+        $endTag = ($row == $theadEnd) ? ('        </thead>' . PHP_EOL) : '';
+        $cellType = ($row >= $tbodyStart) ? 'td' : 'th';
+
+        return [$cellType, $startTag, $endTag];
+    }
+
+    /**
+     * Generate sheet data.
+     *
+     * @return string
+     */
+    public function generateSheetData()
+    {
+        $sheets = $this->generateSheetPrep();
+
+        // Construct HTML
+        $html = '';
+
+        // Loop all sheets
+        $sheetId = 0;
+        foreach ($sheets as $sheet) {
+            // Write table header
+            $html .= $this->generateTableHeader($sheet);
+
+            // Get worksheet dimension
+            [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension());
+            [$minCol, $minRow] = Coordinate::indexesFromString($min);
+            [$maxCol, $maxRow] = Coordinate::indexesFromString($max);
+
+            [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow);
+
+            // Loop through cells
+            $row = $minRow - 1;
+            while ($row++ < $maxRow) {
+                [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart);
+                $html .= $startTag;
+
+                // Write row if there are HTML table cells in it
+                if (!isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) {
+                    // Start a new rowData
+                    $rowData = [];
+                    // Loop through columns
+                    $column = $minCol;
+                    while ($column <= $maxCol) {
+                        // Cell exists?
+                        $cellAddress = Coordinate::stringFromColumnIndex($column) . $row;
+                        $rowData[$column++] = ($sheet->getCellCollection()->has($cellAddress)) ? $cellAddress : '';
+                    }
+                    $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
+                }
+
+                $html .= $endTag;
+            }
+            --$row;
+            $html .= $this->extendRowsForChartsAndImages($sheet, $row);
+
+            // Write table footer
+            $html .= $this->generateTableFooter();
+            // Writing PDF?
+            if ($this->isPdf && $this->useInlineCss) {
+                if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) {
+                    $html .= '<div style="page-break-before:always" ></div>';
+                }
+            }
+
+            // Next sheet
+            ++$sheetId;
+        }
+
+        return $html;
+    }
+
+    /**
+     * Generate sheet tabs.
+     *
+     * @return string
+     */
+    public function generateNavigation()
+    {
+        // Fetch sheets
+        $sheets = [];
+        if ($this->sheetIndex === null) {
+            $sheets = $this->spreadsheet->getAllSheets();
+        } else {
+            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
+        }
+
+        // Construct HTML
+        $html = '';
+
+        // Only if there are more than 1 sheets
+        if (count($sheets) > 1) {
+            // Loop all sheets
+            $sheetId = 0;
+
+            $html .= '<ul class="navigation">' . PHP_EOL;
+
+            foreach ($sheets as $sheet) {
+                $html .= '  <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . htmlspecialchars($sheet->getTitle()) . '</a></li>' . PHP_EOL;
+                ++$sheetId;
+            }
+
+            $html .= '</ul>' . PHP_EOL;
+        }
+
+        return $html;
+    }
+
+    /**
+     * Extend Row if chart is placed after nominal end of row.
+     * This code should be exercised by sample:
+     * Chart/32_Chart_read_write_PDF.php.
+     *
+     * @param int $row Row to check for charts
+     *
+     * @return array
+     */
+    private function extendRowsForCharts(Worksheet $worksheet, int $row)
+    {
+        $rowMax = $row;
+        $colMax = 'A';
+        $anyfound = false;
+        if ($this->includeCharts) {
+            foreach ($worksheet->getChartCollection() as $chart) {
+                if ($chart instanceof Chart) {
+                    $anyfound = true;
+                    $chartCoordinates = $chart->getTopLeftPosition();
+                    $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']);
+                    $chartCol = Coordinate::columnIndexFromString($chartTL[0]);
+                    if ($chartTL[1] > $rowMax) {
+                        $rowMax = $chartTL[1];
+                        if ($chartCol > Coordinate::columnIndexFromString($colMax)) {
+                            $colMax = $chartTL[0];
+                        }
+                    }
+                }
+            }
+        }
+
+        return [$rowMax, $colMax, $anyfound];
+    }
+
+    private function extendRowsForChartsAndImages(Worksheet $worksheet, int $row): string
+    {
+        [$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($worksheet, $row);
+
+        foreach ($worksheet->getDrawingCollection() as $drawing) {
+            if ($drawing instanceof Drawing && $drawing->getPath() === '') {
+                continue;
+            }
+            $anyfound = true;
+            $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates());
+            $imageCol = Coordinate::columnIndexFromString($imageTL[0]);
+            if ($imageTL[1] > $rowMax) {
+                $rowMax = $imageTL[1];
+                if ($imageCol > Coordinate::columnIndexFromString($colMax)) {
+                    $colMax = $imageTL[0];
+                }
+            }
+        }
+
+        // Don't extend rows if not needed
+        if ($row === $rowMax || !$anyfound) {
+            return '';
+        }
+
+        $html = '';
+        ++$colMax;
+        ++$row;
+        while ($row <= $rowMax) {
+            $html .= '<tr>';
+            for ($col = 'A'; $col != $colMax; ++$col) {
+                $htmlx = $this->writeImageInCell($worksheet, $col . $row);
+                $htmlx .= $this->includeCharts ? $this->writeChartInCell($worksheet, $col . $row) : '';
+                if ($htmlx) {
+                    $html .= "<td class='style0' style='position: relative;'>$htmlx</td>";
+                } else {
+                    $html .= "<td class='style0'></td>";
+                }
+            }
+            ++$row;
+            $html .= '</tr>' . PHP_EOL;
+        }
+
+        return $html;
+    }
+
+    /**
+     * Convert Windows file name to file protocol URL.
+     *
+     * @param string $filename file name on local system
+     *
+     * @return string
+     */
+    public static function winFileToUrl($filename, bool $mpdf = false)
+    {
+        // Windows filename
+        if (substr($filename, 1, 2) === ':\\') {
+            $protocol = $mpdf ? '' : 'file:///';
+            $filename = $protocol . str_replace('\\', '/', $filename);
+        }
+
+        return $filename;
+    }
+
+    /**
+     * Generate image tag in cell.
+     *
+     * @param Worksheet $worksheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+     * @param string $coordinates Cell coordinates
+     *
+     * @return string
+     */
+    private function writeImageInCell(Worksheet $worksheet, $coordinates)
+    {
+        // Construct HTML
+        $html = '';
+
+        // Write images
+        foreach ($worksheet->getDrawingCollection() as $drawing) {
+            if ($drawing->getCoordinates() != $coordinates) {
+                continue;
+            }
+            $filedesc = $drawing->getDescription();
+            $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image';
+            if ($drawing instanceof Drawing && $drawing->getPath() !== '') {
+                $filename = $drawing->getPath();
+
+                // Strip off eventual '.'
+                $filename = (string) preg_replace('/^[.]/', '', $filename);
+
+                // Prepend images root
+                $filename = $this->getImagesRoot() . $filename;
+
+                // Strip off eventual '.' if followed by non-/
+                $filename = (string) preg_replace('@^[.]([^/])@', '$1', $filename);
+
+                // Convert UTF8 data to PCDATA
+                $filename = htmlspecialchars($filename, Settings::htmlEntityFlags());
+
+                $html .= PHP_EOL;
+                $imageData = self::winFileToUrl($filename, $this->isMPdf);
+
+                if ($this->embedImages || substr($imageData, 0, 6) === 'zip://') {
+                    $imageData = 'data:,';
+                    $picture = @file_get_contents($filename);
+                    if ($picture !== false) {
+                        $mimeContentType = (string) @mime_content_type($filename);
+                        if (substr($mimeContentType, 0, 6) === 'image/') {
+                            // base64 encode the binary data
+                            $base64 = base64_encode($picture);
+                            $imageData = 'data:' . $mimeContentType . ';base64,' . $base64;
+                        }
+                    }
+                }
+
+                $html .= '<img style="position: absolute; z-index: 1; left: ' .
+                    $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: ' .
+                    $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="' .
+                    $imageData . '" alt="' . $filedesc . '" />';
+            } elseif ($drawing instanceof MemoryDrawing) {
+                $imageResource = $drawing->getImageResource();
+                if ($imageResource) {
+                    ob_start(); //  Let's start output buffering.
+                    imagepng($imageResource); //  This will normally output the image, but because of ob_start(), it won't.
+                    $contents = (string) ob_get_contents(); //  Instead, output above is saved to $contents
+                    ob_end_clean(); //  End the output buffer.
+
+                    $dataUri = 'data:image/png;base64,' . base64_encode($contents);
+
+                    //  Because of the nature of tables, width is more important than height.
+                    //  max-width: 100% ensures that image doesnt overflow containing cell
+                    //  width: X sets width of supplied image.
+                    //  As a result, images bigger than cell will be contained and images smaller will not get stretched
+                    $html .= '<img alt="' . $filedesc . '" src="' . $dataUri . '" style="max-width:100%;width:' . $drawing->getWidth() . 'px;left: ' .
+                    $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px;position: absolute; z-index: 1;" />';
+                }
+            }
+        }
+
+        return $html;
+    }
+
+    /**
+     * Generate chart tag in cell.
+     * This code should be exercised by sample:
+     * Chart/32_Chart_read_write_PDF.php.
+     */
+    private function writeChartInCell(Worksheet $worksheet, string $coordinates): string
+    {
+        // Construct HTML
+        $html = '';
+
+        // Write charts
+        foreach ($worksheet->getChartCollection() as $chart) {
+            if ($chart instanceof Chart) {
+                $chartCoordinates = $chart->getTopLeftPosition();
+                if ($chartCoordinates['cell'] == $coordinates) {
+                    $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
+                    if (!$chart->render($chartFileName)) {
+                        return '';
+                    }
+
+                    $html .= PHP_EOL;
+                    $imageDetails = getimagesize($chartFileName) ?: [];
+                    $filedesc = $chart->getTitle();
+                    $filedesc = $filedesc ? $filedesc->getCaptionText() : '';
+                    $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart';
+                    $picture = file_get_contents($chartFileName);
+                    if ($picture !== false) {
+                        $base64 = base64_encode($picture);
+                        $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
+
+                        $html .= '<img style="position: absolute; z-index: 1; left: ' . $chartCoordinates['xOffset'] . 'px; top: ' . $chartCoordinates['yOffset'] . 'px; width: ' . $imageDetails[0] . 'px; height: ' . $imageDetails[1] . 'px;" src="' . $imageData . '" alt="' . $filedesc . '" />' . PHP_EOL;
+                    }
+                    unlink($chartFileName);
+                }
+            }
+        }
+
+        // Return
+        return $html;
+    }
+
+    /**
+     * Generate CSS styles.
+     *
+     * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (&lt;style&gt; and &lt;/style&gt;)
+     *
+     * @return string
+     */
+    public function generateStyles($generateSurroundingHTML = true)
+    {
+        // Build CSS
+        $css = $this->buildCSS($generateSurroundingHTML);
+
+        // Construct HTML
+        $html = '';
+
+        // Start styles
+        if ($generateSurroundingHTML) {
+            $html .= '    <style type="text/css">' . PHP_EOL;
+            $html .= (array_key_exists('html', $css)) ? ('      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL) : '';
+        }
+
+        // Write all other styles
+        foreach ($css as $styleName => $styleDefinition) {
+            if ($styleName != 'html') {
+                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
+            }
+        }
+        $html .= $this->generatePageDeclarations(false);
+
+        // End styles
+        if ($generateSurroundingHTML) {
+            $html .= '    </style>' . PHP_EOL;
+        }
+
+        // Return
+        return $html;
+    }
+
+    private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void
+    {
+        // Calculate row heights
+        foreach ($sheet->getRowDimensions() as $rowDimension) {
+            $row = $rowDimension->getRowIndex() - 1;
+
+            // table.sheetN tr.rowYYYYYY { }
+            $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = [];
+
+            if ($rowDimension->getRowHeight() != -1) {
+                $pt_height = $rowDimension->getRowHeight();
+                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt';
+            }
+            if ($rowDimension->getVisible() === false) {
+                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none';
+                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden';
+            }
+        }
+    }
+
+    private function buildCssPerSheet(Worksheet $sheet, array &$css): void
+    {
+        // Calculate hash code
+        $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
+        $setup = $sheet->getPageSetup();
+        if ($setup->getFitToPage() && $setup->getFitToHeight() === 1) {
+            $css["table.sheet$sheetIndex"]['page-break-inside'] = 'avoid';
+            $css["table.sheet$sheetIndex"]['break-inside'] = 'avoid';
+        }
+
+        // Build styles
+        // Calculate column widths
+        $sheet->calculateColumnWidths();
+
+        // col elements, initialize
+        $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
+        $column = -1;
+        while ($column++ < $highestColumnIndex) {
+            $this->columnWidths[$sheetIndex][$column] = 42; // approximation
+            $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt';
+        }
+
+        // col elements, loop through columnDimensions and set width
+        foreach ($sheet->getColumnDimensions() as $columnDimension) {
+            $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
+            $width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont);
+            $width = SharedDrawing::pixelsToPoints($width);
+            if ($columnDimension->getVisible() === false) {
+                $css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none';
+            }
+            if ($width >= 0) {
+                $this->columnWidths[$sheetIndex][$column] = $width;
+                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
+            }
+        }
+
+        // Default row height
+        $rowDimension = $sheet->getDefaultRowDimension();
+
+        // table.sheetN tr { }
+        $css['table.sheet' . $sheetIndex . ' tr'] = [];
+
+        if ($rowDimension->getRowHeight() == -1) {
+            $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
+        } else {
+            $pt_height = $rowDimension->getRowHeight();
+        }
+        $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
+        if ($rowDimension->getVisible() === false) {
+            $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none';
+            $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden';
+        }
+
+        $this->buildCssRowHeights($sheet, $css, $sheetIndex);
+    }
+
+    /**
+     * Build CSS styles.
+     *
+     * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
+     *
+     * @return array
+     */
+    public function buildCSS($generateSurroundingHTML = true)
+    {
+        // Cached?
+        if ($this->cssStyles !== null) {
+            return $this->cssStyles;
+        }
+
+        // Ensure that spans have been calculated
+        $this->calculateSpans();
+
+        // Construct CSS
+        $css = [];
+
+        // Start styles
+        if ($generateSurroundingHTML) {
+            // html { }
+            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
+            $css['html']['font-size'] = '11pt';
+            $css['html']['background-color'] = 'white';
+        }
+
+        // CSS for comments as found in LibreOffice
+        $css['a.comment-indicator:hover + div.comment'] = [
+            'background' => '#ffd',
+            'position' => 'absolute',
+            'display' => 'block',
+            'border' => '1px solid black',
+            'padding' => '0.5em',
+        ];
+
+        $css['a.comment-indicator'] = [
+            'background' => 'red',
+            'display' => 'inline-block',
+            'border' => '1px solid black',
+            'width' => '0.5em',
+            'height' => '0.5em',
+        ];
+
+        $css['div.comment']['display'] = 'none';
+
+        // table { }
+        $css['table']['border-collapse'] = 'collapse';
+
+        // .b {}
+        $css['.b']['text-align'] = 'center'; // BOOL
+
+        // .e {}
+        $css['.e']['text-align'] = 'center'; // ERROR
+
+        // .f {}
+        $css['.f']['text-align'] = 'right'; // FORMULA
+
+        // .inlineStr {}
+        $css['.inlineStr']['text-align'] = 'left'; // INLINE
+
+        // .n {}
+        $css['.n']['text-align'] = 'right'; // NUMERIC
+
+        // .s {}
+        $css['.s']['text-align'] = 'left'; // STRING
+
+        // Calculate cell style hashes
+        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
+            $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style);
+            //$css['th.style' . $index] = $this->createCSSStyle($style);
+        }
+
+        // Fetch sheets
+        $sheets = [];
+        if ($this->sheetIndex === null) {
+            $sheets = $this->spreadsheet->getAllSheets();
+        } else {
+            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
+        }
+
+        // Build styles per sheet
+        foreach ($sheets as $sheet) {
+            $this->buildCssPerSheet($sheet, $css);
+        }
+
+        // Cache
+        if ($this->cssStyles === null) {
+            $this->cssStyles = $css;
+        }
+
+        // Return
+        return $css;
+    }
+
+    /**
+     * Create CSS style.
+     *
+     * @return array
+     */
+    private function createCSSStyle(Style $style)
+    {
+        // Create CSS
+        return array_merge(
+            $this->createCSSStyleAlignment($style->getAlignment()),
+            $this->createCSSStyleBorders($style->getBorders()),
+            $this->createCSSStyleFont($style->getFont()),
+            $this->createCSSStyleFill($style->getFill())
+        );
+    }
+
+    /**
+     * Create CSS style.
+     *
+     * @return array
+     */
+    private function createCSSStyleAlignment(Alignment $alignment)
+    {
+        // Construct CSS
+        $css = [];
+
+        // Create CSS
+        $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? '');
+        if ($verticalAlign) {
+            $css['vertical-align'] = $verticalAlign;
+        }
+        $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? '');
+        if ($textAlign) {
+            $css['text-align'] = $textAlign;
+            if (in_array($textAlign, ['left', 'right'])) {
+                $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px';
+            }
+        }
+        $rotation = $alignment->getTextRotation();
+        if ($rotation !== 0 && $rotation !== Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) {
+            if ($this->isMPdf) {
+                $css['text-rotate'] = "$rotation";
+            } else {
+                $css['transform'] = "rotate({$rotation}deg)";
+            }
+        }
+
+        return $css;
+    }
+
+    /**
+     * Create CSS style.
+     *
+     * @return array
+     */
+    private function createCSSStyleFont(Font $font)
+    {
+        // Construct CSS
+        $css = [];
+
+        // Create CSS
+        if ($font->getBold()) {
+            $css['font-weight'] = 'bold';
+        }
+        if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) {
+            $css['text-decoration'] = 'underline line-through';
+        } elseif ($font->getUnderline() != Font::UNDERLINE_NONE) {
+            $css['text-decoration'] = 'underline';
+        } elseif ($font->getStrikethrough()) {
+            $css['text-decoration'] = 'line-through';
+        }
+        if ($font->getItalic()) {
+            $css['font-style'] = 'italic';
+        }
+
+        $css['color'] = '#' . $font->getColor()->getRGB();
+        $css['font-family'] = '\'' . htmlspecialchars((string) $font->getName(), ENT_QUOTES) . '\'';
+        $css['font-size'] = $font->getSize() . 'pt';
+
+        return $css;
+    }
+
+    /**
+     * Create CSS style.
+     *
+     * @param Borders $borders Borders
+     *
+     * @return array
+     */
+    private function createCSSStyleBorders(Borders $borders)
+    {
+        // Construct CSS
+        $css = [];
+
+        // Create CSS
+        $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
+        $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
+        $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
+        $css['border-right'] = $this->createCSSStyleBorder($borders->getRight());
+
+        return $css;
+    }
+
+    /**
+     * Create CSS style.
+     *
+     * @param Border $border Border
+     */
+    private function createCSSStyleBorder(Border $border): string
+    {
+        //    Create CSS - add !important to non-none border styles for merged cells
+        $borderStyle = $this->mapBorderStyle($border->getBorderStyle());
+
+        return $borderStyle . ' #' . $border->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important');
+    }
+
+    /**
+     * Create CSS style (Fill).
+     *
+     * @param Fill $fill Fill
+     *
+     * @return array
+     */
+    private function createCSSStyleFill(Fill $fill)
+    {
+        // Construct HTML
+        $css = [];
+
+        // Create CSS
+        if ($fill->getFillType() !== Fill::FILL_NONE) {
+            $value = $fill->getFillType() == Fill::FILL_NONE ?
+                'white' : '#' . $fill->getStartColor()->getRGB();
+            $css['background-color'] = $value;
+        }
+
+        return $css;
+    }
+
+    /**
+     * Generate HTML footer.
+     */
+    public function generateHTMLFooter(): string
+    {
+        // Construct HTML
+        $html = '';
+        $html .= '  </body>' . PHP_EOL;
+        $html .= '</html>' . PHP_EOL;
+
+        return $html;
+    }
+
+    private function generateTableTagInline(Worksheet $worksheet, string $id): string
+    {
+        $style = isset($this->cssStyles['table']) ?
+            $this->assembleCSS($this->cssStyles['table']) : '';
+
+        $prntgrid = $worksheet->getPrintGridlines();
+        $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines();
+        if ($viewgrid && $prntgrid) {
+            $html = "    <table border='1' cellpadding='1' $id cellspacing='1' style='$style' class='gridlines gridlinesp'>" . PHP_EOL;
+        } elseif ($viewgrid) {
+            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlines'>" . PHP_EOL;
+        } elseif ($prntgrid) {
+            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlinesp'>" . PHP_EOL;
+        } else {
+            $html = "    <table border='0' cellpadding='1' $id cellspacing='0' style='$style'>" . PHP_EOL;
+        }
+
+        return $html;
+    }
+
+    private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void
+    {
+        if (!$this->useInlineCss) {
+            $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : '';
+            $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : '';
+            $html .= "    <table border='0' cellpadding='0' cellspacing='0' $id class='sheet$sheetIndex$gridlines$gridlinesp'>" . PHP_EOL;
+        } else {
+            $html .= $this->generateTableTagInline($worksheet, $id);
+        }
+    }
+
+    /**
+     * Generate table header.
+     *
+     * @param Worksheet $worksheet The worksheet for the table we are writing
+     * @param bool $showid whether or not to add id to table tag
+     *
+     * @return string
+     */
+    private function generateTableHeader(Worksheet $worksheet, $showid = true)
+    {
+        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
+
+        // Construct HTML
+        $html = '';
+        $id = $showid ? "id='sheet$sheetIndex'" : '';
+        if ($showid) {
+            $html .= "<div style='page: page$sheetIndex'>" . PHP_EOL;
+        } else {
+            $html .= "<div style='page: page$sheetIndex' class='scrpgbrk'>" . PHP_EOL;
+        }
+
+        $this->generateTableTag($worksheet, $id, $html, $sheetIndex);
+
+        // Write <col> elements
+        $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1;
+        $i = -1;
+        while ($i++ < $highestColumnIndex) {
+            if (!$this->useInlineCss) {
+                $html .= '        <col class="col' . $i . '" />' . PHP_EOL;
+            } else {
+                $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) ?
+                    $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
+                $html .= '        <col style="' . $style . '" />' . PHP_EOL;
+            }
+        }
+
+        return $html;
+    }
+
+    /**
+     * Generate table footer.
+     */
+    private function generateTableFooter(): string
+    {
+        return '    </tbody></table>' . PHP_EOL . '</div>' . PHP_EOL;
+    }
+
+    /**
+     * Generate row start.
+     *
+     * @param int $sheetIndex Sheet index (0-based)
+     * @param int $row row number
+     *
+     * @return string
+     */
+    private function generateRowStart(Worksheet $worksheet, $sheetIndex, $row)
+    {
+        $html = '';
+        if (count($worksheet->getBreaks()) > 0) {
+            $breaks = $worksheet->getRowBreaks();
+
+            // check if a break is needed before this row
+            if (isset($breaks['A' . $row])) {
+                // close table: </table>
+                $html .= $this->generateTableFooter();
+                if ($this->isPdf && $this->useInlineCss) {
+                    $html .= '<div style="page-break-before:always" />';
+                }
+
+                // open table again: <table> + <col> etc.
+                $html .= $this->generateTableHeader($worksheet, false);
+                $html .= '<tbody>' . PHP_EOL;
+            }
+        }
+
+        // Write row start
+        if (!$this->useInlineCss) {
+            $html .= '          <tr class="row' . $row . '">' . PHP_EOL;
+        } else {
+            $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row])
+                ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]) : '';
+
+            $html .= '          <tr style="' . $style . '">' . PHP_EOL;
+        }
+
+        return $html;
+    }
+
+    private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array
+    {
+        $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : '';
+        $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1);
+        if (!$this->useInlineCss) {
+            $cssClass = 'column' . $columnNumber;
+        } else {
+            $cssClass = [];
+            // The statements below do nothing.
+            // Commenting out the code rather than deleting it
+            // in case someone can figure out what their intent was.
+            //if ($cellType == 'th') {
+            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
+            //        $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
+            //    }
+            //} else {
+            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
+            //        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
+            //    }
+            //}
+            // End of mystery statements.
+        }
+
+        return [$cell, $cssClass, $coordinate];
+    }
+
+    private function generateRowCellDataValueRich(Cell $cell, string &$cellData): void
+    {
+        // Loop through rich text elements
+        $elements = $cell->getValue()->getRichTextElements();
+        foreach ($elements as $element) {
+            // Rich text start?
+            if ($element instanceof Run) {
+                $cellEnd = '';
+                if ($element->getFont() !== null) {
+                    $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
+
+                    if ($element->getFont()->getSuperscript()) {
+                        $cellData .= '<sup>';
+                        $cellEnd = '</sup>';
+                    } elseif ($element->getFont()->getSubscript()) {
+                        $cellData .= '<sub>';
+                        $cellEnd = '</sub>';
+                    }
+                }
+
+                // Convert UTF8 data to PCDATA
+                $cellText = $element->getText();
+                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
+
+                $cellData .= $cellEnd;
+
+                $cellData .= '</span>';
+            } else {
+                // Convert UTF8 data to PCDATA
+                $cellText = $element->getText();
+                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
+            }
+        }
+    }
+
+    private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
+    {
+        if ($cell->getValue() instanceof RichText) {
+            $this->generateRowCellDataValueRich($cell, $cellData);
+        } else {
+            $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue();
+            $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
+
+            $cellData = NumberFormat::toFormattedString(
+                $origData ?? '',
+                $formatCode ?? NumberFormat::FORMAT_GENERAL,
+                [$this, 'formatColor']
+            );
+
+            if ($cellData === $origData) {
+                $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags());
+            }
+            if ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
+                $cellData = '<sup>' . $cellData . '</sup>';
+            } elseif ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
+                $cellData = '<sub>' . $cellData . '</sub>';
+            }
+        }
+    }
+
+    /**
+     * @param null|Cell|string $cell
+     * @param array|string $cssClass
+     */
+    private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, string $cellType): string
+    {
+        $cellData = '&nbsp;';
+        if ($cell instanceof Cell) {
+            $cellData = '';
+            // Don't know what this does, and no test cases.
+            //if ($cell->getParent() === null) {
+            //    $cell->attach($worksheet);
+            //}
+            // Value
+            $this->generateRowCellDataValue($worksheet, $cell, $cellData);
+
+            // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
+            // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
+            $cellData = (string) preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
+
+            // convert newline "\n" to '<br>'
+            $cellData = nl2br($cellData);
+
+            // Extend CSS class?
+            if (!$this->useInlineCss && is_string($cssClass)) {
+                $cssClass .= ' style' . $cell->getXfIndex();
+                $cssClass .= ' ' . $cell->getDataType();
+            } elseif (is_array($cssClass)) {
+                if ($cellType == 'th') {
+                    if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) {
+                        $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]);
+                    }
+                } else {
+                    if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) {
+                        $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]);
+                    }
+                }
+
+                // General horizontal alignment: Actual horizontal alignment depends on dataType
+                $sharedStyle = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
+                if (
+                    $sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
+                    && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
+                ) {
+                    $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
+                }
+            }
+        } else {
+            // Use default borders for empty cell
+            if (is_string($cssClass)) {
+                $cssClass .= ' style0';
+            }
+        }
+
+        return $cellData;
+    }
+
+    private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string
+    {
+        return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : '';
+    }
+
+    private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string
+    {
+        $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
+        $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
+
+        return $html;
+    }
+
+    /**
+     * @param array|string $cssClass
+     */
+    private function generateRowWriteCell(string &$html, Worksheet $worksheet, string $coordinate, string $cellType, string $cellData, int $colSpan, int $rowSpan, $cssClass, int $colNum, int $sheetIndex, int $row): void
+    {
+        // Image?
+        $htmlx = $this->writeImageInCell($worksheet, $coordinate);
+        // Chart?
+        $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
+        // Column start
+        $html .= '            <' . $cellType;
+        if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
+            $html .= ' class="' . $cssClass . '"';
+            if ($htmlx) {
+                $html .= " style='position: relative;'";
+            }
+        } else {
+            //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
+            // We must explicitly write the width of the <td> element because TCPDF
+            // does not recognize e.g. <col style="width:42pt">
+            if ($this->useInlineCss) {
+                $xcssClass = is_array($cssClass) ? $cssClass : [];
+            } else {
+                if (is_string($cssClass)) {
+                    $html .= ' class="' . $cssClass . '"';
+                }
+                $xcssClass = [];
+            }
+            $width = 0;
+            $i = $colNum - 1;
+            $e = $colNum + $colSpan - 1;
+            while ($i++ < $e) {
+                if (isset($this->columnWidths[$sheetIndex][$i])) {
+                    $width += $this->columnWidths[$sheetIndex][$i];
+                }
+            }
+            $xcssClass['width'] = (string) $width . 'pt';
+            // We must also explicitly write the height of the <td> element because TCPDF
+            // does not recognize e.g. <tr style="height:50pt">
+            if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) {
+                $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'];
+                $xcssClass['height'] = $height;
+            }
+            //** end of redundant code **
+
+            if ($htmlx) {
+                $xcssClass['position'] = 'relative';
+            }
+            $html .= ' style="' . $this->assembleCSS($xcssClass) . '"';
+        }
+        $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
+
+        $html .= '>';
+        $html .= $htmlx;
+
+        $html .= $this->writeComment($worksheet, $coordinate);
+
+        // Cell data
+        $html .= $cellData;
+
+        // Column end
+        $html .= '</' . $cellType . '>' . PHP_EOL;
+    }
+
+    /**
+     * Generate row.
+     *
+     * @param array $values Array containing cells in a row
+     * @param int $row Row number (0-based)
+     * @param string $cellType eg: 'td'
+     *
+     * @return string
+     */
+    private function generateRow(Worksheet $worksheet, array $values, $row, $cellType)
+    {
+        // Sheet index
+        $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
+        $html = $this->generateRowStart($worksheet, $sheetIndex, $row);
+        $generateDiv = $this->isMPdf && $worksheet->getRowDimension($row + 1)->getVisible() === false;
+        if ($generateDiv) {
+            $html .= '<div style="visibility:hidden; display:none;">' . PHP_EOL;
+        }
+
+        // Write cells
+        $colNum = 0;
+        foreach ($values as $cellAddress) {
+            [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum);
+
+            // Cell Data
+            $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass, $cellType);
+
+            // Hyperlink?
+            if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) {
+                $url = $worksheet->getHyperlink($coordinate)->getUrl();
+                $urlDecode1 = html_entity_decode($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+                $urlTrim = preg_replace('/^\\s+/u', '', $urlDecode1) ?? $urlDecode1;
+                $parseScheme = preg_match('/^([\\w\\s\\x00-\\x1f]+):/u', strtolower($urlTrim), $matches);
+                if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 'mailto', 's3'], true)) {
+                    $cellData = htmlspecialchars($url, Settings::htmlEntityFlags());
+                    $cellData = self::replaceControlChars($cellData);
+                } else {
+                    $cellData = '<a href="' . htmlspecialchars($url, Settings::htmlEntityFlags()) . '" title="' . htmlspecialchars($worksheet->getHyperlink($coordinate)->getTooltip(), Settings::htmlEntityFlags()) . '">' . $cellData . '</a>';
+                }
+            }
+
+            // Should the cell be written or is it swallowed by a rowspan or colspan?
+            $writeCell = !(isset($this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])
+                && $this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum]);
+
+            // Colspan and Rowspan
+            $colSpan = 1;
+            $rowSpan = 1;
+            if (isset($this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])) {
+                $spans = $this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum];
+                $rowSpan = $spans['rowspan'];
+                $colSpan = $spans['colspan'];
+
+                //    Also apply style from last cell in merge to fix borders -
+                //        relies on !important for non-none border declarations in createCSSStyleBorder
+                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($row + $rowSpan);
+                if (!$this->useInlineCss) {
+                    $cssClass .= ' style' . $worksheet->getCell($endCellCoord)->getXfIndex();
+                }
+            }
+
+            // Write
+            if ($writeCell) {
+                $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row);
+            }
+
+            // Next column
+            ++$colNum;
+        }
+
+        // Write row end
+        if ($generateDiv) {
+            $html .= '</div>' . PHP_EOL;
+        }
+        $html .= '          </tr>' . PHP_EOL;
+
+        // Return
+        return $html;
+    }
+
+    private static function replaceNonAscii(array $matches): string
+    {
+        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
+    }
+
+    private static function replaceControlChars(string $convert): string
+    {
+        return (string) preg_replace_callback(
+            '/[\\x00-\\x1f]/',
+            [self::class, 'replaceNonAscii'],
+            $convert
+        );
+    }
+
+    /**
+     * Takes array where of CSS properties / values and converts to CSS string.
+     *
+     * @return string
+     */
+    private function assembleCSS(array $values = [])
+    {
+        $pairs = [];
+        foreach ($values as $property => $value) {
+            $pairs[] = $property . ':' . $value;
+        }
+        $string = implode('; ', $pairs);
+
+        return $string;
+    }
+
+    /**
+     * Get images root.
+     *
+     * @return string
+     */
+    public function getImagesRoot()
+    {
+        return $this->imagesRoot;
+    }
+
+    /**
+     * Set images root.
+     *
+     * @param string $imagesRoot
+     *
+     * @return $this
+     */
+    public function setImagesRoot($imagesRoot)
+    {
+        $this->imagesRoot = $imagesRoot;
+
+        return $this;
+    }
+
+    /**
+     * Get embed images.
+     *
+     * @return bool
+     */
+    public function getEmbedImages()
+    {
+        return $this->embedImages;
+    }
+
+    /**
+     * Set embed images.
+     *
+     * @param bool $embedImages
+     *
+     * @return $this
+     */
+    public function setEmbedImages($embedImages)
+    {
+        $this->embedImages = $embedImages;
+
+        return $this;
+    }
+
+    /**
+     * Get use inline CSS?
+     *
+     * @return bool
+     */
+    public function getUseInlineCss()
+    {
+        return $this->useInlineCss;
+    }
+
+    /**
+     * Set use inline CSS?
+     *
+     * @param bool $useInlineCss
+     *
+     * @return $this
+     */
+    public function setUseInlineCss($useInlineCss)
+    {
+        $this->useInlineCss = $useInlineCss;
+
+        return $this;
+    }
+
+    /**
+     * Get use embedded CSS?
+     *
+     * @return bool
+     *
+     * @codeCoverageIgnore
+     *
+     * @deprecated no longer used
+     */
+    public function getUseEmbeddedCSS()
+    {
+        return $this->useEmbeddedCSS;
+    }
+
+    /**
+     * Set use embedded CSS?
+     *
+     * @param bool $useEmbeddedCSS
+     *
+     * @return $this
+     *
+     * @codeCoverageIgnore
+     *
+     * @deprecated no longer used
+     */
+    public function setUseEmbeddedCSS($useEmbeddedCSS)
+    {
+        $this->useEmbeddedCSS = $useEmbeddedCSS;
+
+        return $this;
+    }
+
+    /**
+     * Add color to formatted string as inline style.
+     *
+     * @param string $value Plain formatted value without color
+     * @param string $format Format code
+     *
+     * @return string
+     */
+    public function formatColor($value, $format)
+    {
+        // Color information, e.g. [Red] is always at the beginning
+        $color = null; // initialize
+        $matches = [];
+
+        $color_regex = '/^\\[[a-zA-Z]+\\]/';
+        if (preg_match($color_regex, $format, $matches)) {
+            $color = str_replace(['[', ']'], '', $matches[0]);
+            $color = strtolower($color);
+        }
+
+        // convert to PCDATA
+        $result = htmlspecialchars($value, Settings::htmlEntityFlags());
+
+        // color span tag
+        if ($color !== null) {
+            $result = '<span style="color:' . $color . '">' . $result . '</span>';
+        }
+
+        return $result;
+    }
+
+    /**
+     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
+     */
+    private function calculateSpans(): void
+    {
+        if ($this->spansAreCalculated) {
+            return;
+        }
+        // Identify all cells that should be omitted in HTML due to cell merge.
+        // In HTML only the upper-left cell should be written and it should have
+        //   appropriate rowspan / colspan attribute
+        $sheetIndexes = $this->sheetIndex !== null ?
+            [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
+
+        foreach ($sheetIndexes as $sheetIndex) {
+            $sheet = $this->spreadsheet->getSheet($sheetIndex);
+
+            $candidateSpannedRow = [];
+
+            // loop through all Excel merged cells
+            foreach ($sheet->getMergeCells() as $cells) {
+                [$cells] = Coordinate::splitRange($cells);
+                $first = $cells[0];
+                $last = $cells[1];
+
+                [$fc, $fr] = Coordinate::indexesFromString($first);
+                $fc = $fc - 1;
+
+                [$lc, $lr] = Coordinate::indexesFromString($last);
+                $lc = $lc - 1;
+
+                // loop through the individual cells in the individual merge
+                $r = $fr - 1;
+                while ($r++ < $lr) {
+                    // also, flag this row as a HTML row that is candidate to be omitted
+                    $candidateSpannedRow[$r] = $r;
+
+                    $c = $fc - 1;
+                    while ($c++ < $lc) {
+                        if (!($c == $fc && $r == $fr)) {
+                            // not the upper-left cell (should not be written in HTML)
+                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
+                                'baseCell' => [$fr, $fc],
+                            ];
+                        } else {
+                            // upper-left is the base cell that should hold the colspan/rowspan attribute
+                            $this->isBaseCell[$sheetIndex][$r][$c] = [
+                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
+                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
+                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
+                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
+                            ];
+                        }
+                    }
+                }
+            }
+
+            $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
+
+            // TODO: Same for columns
+        }
+
+        // We have calculated the spans
+        $this->spansAreCalculated = true;
+    }
+
+    private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void
+    {
+        // Identify which rows should be omitted in HTML. These are the rows where all the cells
+        //   participate in a merge and the where base cells are somewhere above.
+        $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
+        foreach ($candidateSpannedRow as $rowIndex) {
+            if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
+                if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
+                    $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
+                }
+            }
+        }
+
+        // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
+        if (isset($this->isSpannedRow[$sheetIndex])) {
+            foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
+                $adjustedBaseCells = [];
+                $c = -1;
+                $e = $countColumns - 1;
+                while ($c++ < $e) {
+                    $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
+
+                    if (!in_array($baseCell, $adjustedBaseCells, true)) {
+                        // subtract rowspan by 1
+                        --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
+                        $adjustedBaseCells[] = $baseCell;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Write a comment in the same format as LibreOffice.
+     *
+     * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
+     *
+     * @param string $coordinate
+     *
+     * @return string
+     */
+    private function writeComment(Worksheet $worksheet, $coordinate)
+    {
+        $result = '';
+        if (!$this->isPdf && isset($worksheet->getComments()[$coordinate])) {
+            $sanitizer = new HTMLPurifier();
+            $cachePath = File::sysGetTempDir() . '/phpsppur';
+            if (is_dir($cachePath) || mkdir($cachePath)) {
+                $sanitizer->config->set('Cache.SerializerPath', $cachePath);
+            }
+            $sanitizedString = $sanitizer->purify($worksheet->getComment($coordinate)->getText()->getPlainText());
+            if ($sanitizedString !== '') {
+                $result .= '<a class="comment-indicator"></a>';
+                $result .= '<div class="comment">' . nl2br($sanitizedString) . '</div>';
+                $result .= PHP_EOL;
+            }
+        }
+
+        return $result;
+    }
+
+    public function getOrientation(): ?string
+    {
+        // Expect Pdf classes to override this method.
+        return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null;
+    }
+
+    /**
+     * Generate @page declarations.
+     *
+     * @param bool $generateSurroundingHTML
+     *
+     * @return    string
+     */
+    private function generatePageDeclarations($generateSurroundingHTML)
+    {
+        // Ensure that Spans have been calculated?
+        $this->calculateSpans();
+
+        // Fetch sheets
+        $sheets = [];
+        if ($this->sheetIndex === null) {
+            $sheets = $this->spreadsheet->getAllSheets();
+        } else {
+            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
+        }
+
+        // Construct HTML
+        $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
+
+        // Loop all sheets
+        $sheetId = 0;
+        foreach ($sheets as $worksheet) {
+            $htmlPage .= "@page page$sheetId { ";
+            $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; ';
+            $htmlPage .= 'margin-left: ' . $left;
+            $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; ';
+            $htmlPage .= 'margin-right: ' . $right;
+            $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; ';
+            $htmlPage .= 'margin-top: ' . $top;
+            $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; ';
+            $htmlPage .= 'margin-bottom: ' . $bottom;
+            $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation();
+            if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) {
+                $htmlPage .= 'size: landscape; ';
+            } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) {
+                $htmlPage .= 'size: portrait; ';
+            }
+            $htmlPage .= '}' . PHP_EOL;
+            ++$sheetId;
+        }
+        $htmlPage .= implode(PHP_EOL, [
+            '.navigation {page-break-after: always;}',
+            '.scrpgbrk, div + div {page-break-before: always;}',
+            '@media screen {',
+            '  .gridlines td {border: 1px solid black;}',
+            '  .gridlines th {border: 1px solid black;}',
+            '  body>div {margin-top: 5px;}',
+            '  body>div:first-child {margin-top: 0;}',
+            '  .scrpgbrk {margin-top: 1px;}',
+            '}',
+            '@media print {',
+            '  .gridlinesp td {border: 1px solid black;}',
+            '  .gridlinesp th {border: 1px solid black;}',
+            '  .navigation {display: none;}',
+            '}',
+            '',
+        ]);
+        $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
+
+        return $htmlPage;
+    }
+}

+ 98 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer;
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+
+interface IWriter
+{
+    public const SAVE_WITH_CHARTS = 1;
+
+    public const DISABLE_PRECALCULATE_FORMULAE = 2;
+
+    /**
+     * IWriter constructor.
+     *
+     * @param Spreadsheet $spreadsheet The spreadsheet that we want to save using this Writer
+     */
+    public function __construct(Spreadsheet $spreadsheet);
+
+    /**
+     * Write charts in workbook?
+     *        If this is true, then the Writer will write definitions for any charts that exist in the PhpSpreadsheet object.
+     *        If false (the default) it will ignore any charts defined in the PhpSpreadsheet object.
+     *
+     * @return bool
+     */
+    public function getIncludeCharts();
+
+    /**
+     * Set write charts in workbook
+     *        Set to true, to advise the Writer to include any charts that exist in the PhpSpreadsheet object.
+     *        Set to false (the default) to ignore charts.
+     *
+     * @param bool $includeCharts
+     *
+     * @return IWriter
+     */
+    public function setIncludeCharts($includeCharts);
+
+    /**
+     * Get Pre-Calculate Formulas flag
+     *     If this is true (the default), then the writer will recalculate all formulae in a workbook when saving,
+     *        so that the pre-calculated values are immediately available to MS Excel or other office spreadsheet
+     *        viewer when opening the file
+     *     If false, then formulae are not calculated on save. This is faster for saving in PhpSpreadsheet, but slower
+     *        when opening the resulting file in MS Excel, because Excel has to recalculate the formulae itself.
+     *
+     * @return bool
+     */
+    public function getPreCalculateFormulas();
+
+    /**
+     * Set Pre-Calculate Formulas
+     *        Set to true (the default) to advise the Writer to calculate all formulae on save
+     *        Set to false to prevent precalculation of formulae on save.
+     *
+     * @param bool $precalculateFormulas Pre-Calculate Formulas?
+     *
+     * @return IWriter
+     */
+    public function setPreCalculateFormulas($precalculateFormulas);
+
+    /**
+     * Save PhpSpreadsheet to file.
+     *
+     * @param resource|string $filename Name of the file to save
+     * @param int $flags Flags that can change the behaviour of the Writer:
+     *            self::SAVE_WITH_CHARTS                Save any charts that are defined (if the Writer supports Charts)
+     *            self::DISABLE_PRECALCULATE_FORMULAE   Don't Precalculate formulae before saving the file
+     *
+     * @throws Exception
+     */
+    public function save($filename, int $flags = 0): void;
+
+    /**
+     * Get use disk caching where possible?
+     *
+     * @return bool
+     */
+    public function getUseDiskCaching();
+
+    /**
+     * Set use disk caching where possible?
+     *
+     * @param bool $useDiskCache
+     * @param string $cacheDirectory Disk caching directory
+     *
+     * @return IWriter
+     */
+    public function setUseDiskCaching($useDiskCache, $cacheDirectory = null);
+
+    /**
+     * Get disk caching directory.
+     *
+     * @return string
+     */
+    public function getDiskCachingDirectory();
+}

+ 186 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods.php

@@ -0,0 +1,186 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer;
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+use PhpOffice\PhpSpreadsheet\Writer\Ods\Content;
+use PhpOffice\PhpSpreadsheet\Writer\Ods\Meta;
+use PhpOffice\PhpSpreadsheet\Writer\Ods\MetaInf;
+use PhpOffice\PhpSpreadsheet\Writer\Ods\Mimetype;
+use PhpOffice\PhpSpreadsheet\Writer\Ods\Settings;
+use PhpOffice\PhpSpreadsheet\Writer\Ods\Styles;
+use PhpOffice\PhpSpreadsheet\Writer\Ods\Thumbnails;
+use ZipStream\Exception\OverflowException;
+use ZipStream\ZipStream;
+
+class Ods extends BaseWriter
+{
+    /**
+     * Private PhpSpreadsheet.
+     *
+     * @var Spreadsheet
+     */
+    private $spreadSheet;
+
+    /**
+     * @var Content
+     */
+    private $writerPartContent;
+
+    /**
+     * @var Meta
+     */
+    private $writerPartMeta;
+
+    /**
+     * @var MetaInf
+     */
+    private $writerPartMetaInf;
+
+    /**
+     * @var Mimetype
+     */
+    private $writerPartMimetype;
+
+    /**
+     * @var Settings
+     */
+    private $writerPartSettings;
+
+    /**
+     * @var Styles
+     */
+    private $writerPartStyles;
+
+    /**
+     * @var Thumbnails
+     */
+    private $writerPartThumbnails;
+
+    /**
+     * Create a new Ods.
+     */
+    public function __construct(Spreadsheet $spreadsheet)
+    {
+        $this->setSpreadsheet($spreadsheet);
+
+        $this->writerPartContent = new Content($this);
+        $this->writerPartMeta = new Meta($this);
+        $this->writerPartMetaInf = new MetaInf($this);
+        $this->writerPartMimetype = new Mimetype($this);
+        $this->writerPartSettings = new Settings($this);
+        $this->writerPartStyles = new Styles($this);
+        $this->writerPartThumbnails = new Thumbnails($this);
+    }
+
+    public function getWriterPartContent(): Content
+    {
+        return $this->writerPartContent;
+    }
+
+    public function getWriterPartMeta(): Meta
+    {
+        return $this->writerPartMeta;
+    }
+
+    public function getWriterPartMetaInf(): MetaInf
+    {
+        return $this->writerPartMetaInf;
+    }
+
+    public function getWriterPartMimetype(): Mimetype
+    {
+        return $this->writerPartMimetype;
+    }
+
+    public function getWriterPartSettings(): Settings
+    {
+        return $this->writerPartSettings;
+    }
+
+    public function getWriterPartStyles(): Styles
+    {
+        return $this->writerPartStyles;
+    }
+
+    public function getWriterPartThumbnails(): Thumbnails
+    {
+        return $this->writerPartThumbnails;
+    }
+
+    /**
+     * Save PhpSpreadsheet to file.
+     *
+     * @param resource|string $filename
+     */
+    public function save($filename, int $flags = 0): void
+    {
+        $this->processFlags($flags);
+
+        // garbage collect
+        $this->spreadSheet->garbageCollect();
+
+        $this->openFileHandle($filename);
+
+        $zip = $this->createZip();
+
+        $zip->addFile('META-INF/manifest.xml', $this->getWriterPartMetaInf()->write());
+        $zip->addFile('Thumbnails/thumbnail.png', $this->getWriterPartthumbnails()->write());
+        // Settings always need to be written before Content; Styles after Content
+        $zip->addFile('settings.xml', $this->getWriterPartsettings()->write());
+        $zip->addFile('content.xml', $this->getWriterPartcontent()->write());
+        $zip->addFile('meta.xml', $this->getWriterPartmeta()->write());
+        $zip->addFile('mimetype', $this->getWriterPartmimetype()->write());
+        $zip->addFile('styles.xml', $this->getWriterPartstyles()->write());
+
+        // Close file
+        try {
+            $zip->finish();
+        } catch (OverflowException $e) {
+            throw new WriterException('Could not close resource.');
+        }
+
+        $this->maybeCloseFileHandle();
+    }
+
+    /**
+     * Create zip object.
+     *
+     * @return ZipStream
+     */
+    private function createZip()
+    {
+        // Try opening the ZIP file
+        if (!is_resource($this->fileHandle)) {
+            throw new WriterException('Could not open resource for writing.');
+        }
+
+        // Create new ZIP stream
+        return ZipStream0::newZipStream($this->fileHandle);
+    }
+
+    /**
+     * Get Spreadsheet object.
+     *
+     * @return Spreadsheet
+     */
+    public function getSpreadsheet()
+    {
+        return $this->spreadSheet;
+    }
+
+    /**
+     * Set Spreadsheet object.
+     *
+     * @param Spreadsheet $spreadsheet PhpSpreadsheet object
+     *
+     * @return $this
+     */
+    public function setSpreadsheet(Spreadsheet $spreadsheet)
+    {
+        $this->spreadSheet = $spreadsheet;
+
+        return $this;
+    }
+}

+ 66 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/AutoFilters.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+
+class AutoFilters
+{
+    /**
+     * @var XMLWriter
+     */
+    private $objWriter;
+
+    /**
+     * @var Spreadsheet
+     */
+    private $spreadsheet;
+
+    public function __construct(XMLWriter $objWriter, Spreadsheet $spreadsheet)
+    {
+        $this->objWriter = $objWriter;
+        $this->spreadsheet = $spreadsheet;
+    }
+
+    /** @var mixed */
+    private static $scrutinizerFalse = false;
+
+    public function write(): void
+    {
+        $wrapperWritten = self::$scrutinizerFalse;
+        $sheetCount = $this->spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            $worksheet = $this->spreadsheet->getSheet($i);
+            $autofilter = $worksheet->getAutoFilter();
+            if ($autofilter !== null && !empty($autofilter->getRange())) {
+                if ($wrapperWritten === false) {
+                    $this->objWriter->startElement('table:database-ranges');
+                    $wrapperWritten = true;
+                }
+                $this->objWriter->startElement('table:database-range');
+                $this->objWriter->writeAttribute('table:orientation', 'column');
+                $this->objWriter->writeAttribute('table:display-filter-buttons', 'true');
+                $this->objWriter->writeAttribute(
+                    'table:target-range-address',
+                    $this->formatRange($worksheet, $autofilter)
+                );
+                $this->objWriter->endElement();
+            }
+        }
+
+        if ($wrapperWritten === true) {
+            $this->objWriter->endElement();
+        }
+    }
+
+    protected function formatRange(Worksheet $worksheet, Autofilter $autofilter): string
+    {
+        $title = $worksheet->getTitle();
+        $range = $autofilter->getRange();
+
+        return "'{$title}'.{$range}";
+    }
+}

+ 30 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods\Cell;
+
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+
+/**
+ * @author     Alexander Pervakov <frost-nzcr4@jagmort.com>
+ */
+class Comment
+{
+    public static function write(XMLWriter $objWriter, Cell $cell): void
+    {
+        $comments = $cell->getWorksheet()->getComments();
+        if (!isset($comments[$cell->getCoordinate()])) {
+            return;
+        }
+        $comment = $comments[$cell->getCoordinate()];
+
+        $objWriter->startElement('office:annotation');
+        $objWriter->writeAttribute('svg:width', $comment->getWidth());
+        $objWriter->writeAttribute('svg:height', $comment->getHeight());
+        $objWriter->writeAttribute('svg:x', $comment->getMarginLeft());
+        $objWriter->writeAttribute('svg:y', $comment->getMarginTop());
+        $objWriter->writeElement('dc:creator', $comment->getAuthor());
+        $objWriter->writeElement('text:p', $comment->getText()->getPlainText());
+        $objWriter->endElement();
+    }
+}

+ 259 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php

@@ -0,0 +1,259 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods\Cell;
+
+use PhpOffice\PhpSpreadsheet\Helper\Dimension;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+use PhpOffice\PhpSpreadsheet\Style\Font;
+use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle;
+use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension;
+use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+
+class Style
+{
+    public const CELL_STYLE_PREFIX = 'ce';
+    public const COLUMN_STYLE_PREFIX = 'co';
+    public const ROW_STYLE_PREFIX = 'ro';
+    public const TABLE_STYLE_PREFIX = 'ta';
+
+    /** @var XMLWriter */
+    private $writer;
+
+    public function __construct(XMLWriter $writer)
+    {
+        $this->writer = $writer;
+    }
+
+    private function mapHorizontalAlignment(string $horizontalAlignment): string
+    {
+        switch ($horizontalAlignment) {
+            case Alignment::HORIZONTAL_CENTER:
+            case Alignment::HORIZONTAL_CENTER_CONTINUOUS:
+            case Alignment::HORIZONTAL_DISTRIBUTED:
+                return 'center';
+            case Alignment::HORIZONTAL_RIGHT:
+                return 'end';
+            case Alignment::HORIZONTAL_FILL:
+            case Alignment::HORIZONTAL_JUSTIFY:
+                return 'justify';
+        }
+
+        return 'start';
+    }
+
+    private function mapVerticalAlignment(string $verticalAlignment): string
+    {
+        switch ($verticalAlignment) {
+            case Alignment::VERTICAL_TOP:
+                return 'top';
+            case Alignment::VERTICAL_CENTER:
+                return 'middle';
+            case Alignment::VERTICAL_DISTRIBUTED:
+            case Alignment::VERTICAL_JUSTIFY:
+                return 'automatic';
+        }
+
+        return 'bottom';
+    }
+
+    private function writeFillStyle(Fill $fill): void
+    {
+        switch ($fill->getFillType()) {
+            case Fill::FILL_SOLID:
+                $this->writer->writeAttribute('fo:background-color', sprintf(
+                    '#%s',
+                    strtolower($fill->getStartColor()->getRGB())
+                ));
+
+                break;
+            case Fill::FILL_GRADIENT_LINEAR:
+            case Fill::FILL_GRADIENT_PATH:
+                /// TODO :: To be implemented
+                break;
+            case Fill::FILL_NONE:
+            default:
+        }
+    }
+
+    private function writeCellProperties(CellStyle $style): void
+    {
+        // Align
+        $hAlign = $style->getAlignment()->getHorizontal();
+        $vAlign = $style->getAlignment()->getVertical();
+        $wrap = $style->getAlignment()->getWrapText();
+
+        $this->writer->startElement('style:table-cell-properties');
+        if (!empty($vAlign) || $wrap) {
+            if (!empty($vAlign)) {
+                $vAlign = $this->mapVerticalAlignment($vAlign);
+                $this->writer->writeAttribute('style:vertical-align', $vAlign);
+            }
+            if ($wrap) {
+                $this->writer->writeAttribute('fo:wrap-option', 'wrap');
+            }
+        }
+        $this->writer->writeAttribute('style:rotation-align', 'none');
+
+        // Fill
+        $this->writeFillStyle($style->getFill());
+
+        $this->writer->endElement();
+
+        if (!empty($hAlign)) {
+            $hAlign = $this->mapHorizontalAlignment($hAlign);
+            $this->writer->startElement('style:paragraph-properties');
+            $this->writer->writeAttribute('fo:text-align', $hAlign);
+            $this->writer->endElement();
+        }
+    }
+
+    protected function mapUnderlineStyle(Font $font): string
+    {
+        switch ($font->getUnderline()) {
+            case Font::UNDERLINE_DOUBLE:
+            case Font::UNDERLINE_DOUBLEACCOUNTING:
+                return'double';
+            case Font::UNDERLINE_SINGLE:
+            case Font::UNDERLINE_SINGLEACCOUNTING:
+                return'single';
+        }
+
+        return 'none';
+    }
+
+    protected function writeTextProperties(CellStyle $style): void
+    {
+        // Font
+        $this->writer->startElement('style:text-properties');
+
+        $font = $style->getFont();
+
+        if ($font->getBold()) {
+            $this->writer->writeAttribute('fo:font-weight', 'bold');
+            $this->writer->writeAttribute('style:font-weight-complex', 'bold');
+            $this->writer->writeAttribute('style:font-weight-asian', 'bold');
+        }
+
+        if ($font->getItalic()) {
+            $this->writer->writeAttribute('fo:font-style', 'italic');
+        }
+
+        $this->writer->writeAttribute('fo:color', sprintf('#%s', $font->getColor()->getRGB()));
+
+        if ($family = $font->getName()) {
+            $this->writer->writeAttribute('fo:font-family', $family);
+        }
+
+        if ($size = $font->getSize()) {
+            $this->writer->writeAttribute('fo:font-size', sprintf('%.1Fpt', $size));
+        }
+
+        if ($font->getUnderline() && $font->getUnderline() !== Font::UNDERLINE_NONE) {
+            $this->writer->writeAttribute('style:text-underline-style', 'solid');
+            $this->writer->writeAttribute('style:text-underline-width', 'auto');
+            $this->writer->writeAttribute('style:text-underline-color', 'font-color');
+
+            $underline = $this->mapUnderlineStyle($font);
+            $this->writer->writeAttribute('style:text-underline-type', $underline);
+        }
+
+        $this->writer->endElement(); // Close style:text-properties
+    }
+
+    protected function writeColumnProperties(ColumnDimension $columnDimension): void
+    {
+        $this->writer->startElement('style:table-column-properties');
+        $this->writer->writeAttribute(
+            'style:column-width',
+            round($columnDimension->getWidth(Dimension::UOM_CENTIMETERS), 3) . 'cm'
+        );
+        $this->writer->writeAttribute('fo:break-before', 'auto');
+
+        // End
+        $this->writer->endElement(); // Close style:table-column-properties
+    }
+
+    public function writeColumnStyles(ColumnDimension $columnDimension, int $sheetId): void
+    {
+        $this->writer->startElement('style:style');
+        $this->writer->writeAttribute('style:family', 'table-column');
+        $this->writer->writeAttribute(
+            'style:name',
+            sprintf('%s_%d_%d', self::COLUMN_STYLE_PREFIX, $sheetId, $columnDimension->getColumnNumeric())
+        );
+
+        $this->writeColumnProperties($columnDimension);
+
+        // End
+        $this->writer->endElement(); // Close style:style
+    }
+
+    protected function writeRowProperties(RowDimension $rowDimension): void
+    {
+        $this->writer->startElement('style:table-row-properties');
+        $this->writer->writeAttribute(
+            'style:row-height',
+            round($rowDimension->getRowHeight(Dimension::UOM_CENTIMETERS), 3) . 'cm'
+        );
+        $this->writer->writeAttribute('style:use-optimal-row-height', 'false');
+        $this->writer->writeAttribute('fo:break-before', 'auto');
+
+        // End
+        $this->writer->endElement(); // Close style:table-row-properties
+    }
+
+    public function writeRowStyles(RowDimension $rowDimension, int $sheetId): void
+    {
+        $this->writer->startElement('style:style');
+        $this->writer->writeAttribute('style:family', 'table-row');
+        $this->writer->writeAttribute(
+            'style:name',
+            sprintf('%s_%d_%d', self::ROW_STYLE_PREFIX, $sheetId, $rowDimension->getRowIndex())
+        );
+
+        $this->writeRowProperties($rowDimension);
+
+        // End
+        $this->writer->endElement(); // Close style:style
+    }
+
+    public function writeTableStyle(Worksheet $worksheet, int $sheetId): void
+    {
+        $this->writer->startElement('style:style');
+        $this->writer->writeAttribute('style:family', 'table');
+        $this->writer->writeAttribute(
+            'style:name',
+            sprintf('%s%d', self::TABLE_STYLE_PREFIX, $sheetId)
+        );
+
+        $this->writer->startElement('style:table-properties');
+
+        $this->writer->writeAttribute(
+            'table:display',
+            $worksheet->getSheetState() === Worksheet::SHEETSTATE_VISIBLE ? 'true' : 'false'
+        );
+
+        $this->writer->endElement(); // Close style:table-properties
+        $this->writer->endElement(); // Close style:style
+    }
+
+    public function write(CellStyle $style): void
+    {
+        $this->writer->startElement('style:style');
+        $this->writer->writeAttribute('style:name', self::CELL_STYLE_PREFIX . $style->getIndex());
+        $this->writer->writeAttribute('style:family', 'table-cell');
+        $this->writer->writeAttribute('style:parent-style-name', 'Default');
+
+        // Alignment, fill colour, etc
+        $this->writeCellProperties($style);
+
+        // style:text-properties
+        $this->writeTextProperties($style);
+
+        // End
+        $this->writer->endElement(); // Close style:style
+    }
+}

+ 345 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php

@@ -0,0 +1,345 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Row;
+use PhpOffice\PhpSpreadsheet\Worksheet\RowCellIterator;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+use PhpOffice\PhpSpreadsheet\Writer\Exception;
+use PhpOffice\PhpSpreadsheet\Writer\Ods;
+use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Comment;
+use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Style;
+
+/**
+ * @author     Alexander Pervakov <frost-nzcr4@jagmort.com>
+ */
+class Content extends WriterPart
+{
+    const NUMBER_COLS_REPEATED_MAX = 1024;
+    const NUMBER_ROWS_REPEATED_MAX = 1048576;
+
+    /** @var Formula */
+    private $formulaConvertor;
+
+    /**
+     * Set parent Ods writer.
+     */
+    public function __construct(Ods $writer)
+    {
+        parent::__construct($writer);
+
+        $this->formulaConvertor = new Formula($this->getParentWriter()->getSpreadsheet()->getDefinedNames());
+    }
+
+    /**
+     * Write content.xml to XML format.
+     *
+     * @return string XML Output
+     */
+    public function write(): string
+    {
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8');
+
+        // Content
+        $objWriter->startElement('office:document-content');
+        $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
+        $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0');
+        $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
+        $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0');
+        $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0');
+        $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
+        $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
+        $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
+        $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0');
+        $objWriter->writeAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0');
+        $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0');
+        $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0');
+        $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0');
+        $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0');
+        $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML');
+        $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0');
+        $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0');
+        $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
+        $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer');
+        $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc');
+        $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events');
+        $objWriter->writeAttribute('xmlns:xforms', 'http://www.w3.org/2002/xforms');
+        $objWriter->writeAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema');
+        $objWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
+        $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report');
+        $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2');
+        $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
+        $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#');
+        $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table');
+        $objWriter->writeAttribute('xmlns:field', 'urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0');
+        $objWriter->writeAttribute('xmlns:formx', 'urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0');
+        $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/');
+        $objWriter->writeAttribute('office:version', '1.2');
+
+        $objWriter->writeElement('office:scripts');
+        $objWriter->writeElement('office:font-face-decls');
+
+        // Styles XF
+        $objWriter->startElement('office:automatic-styles');
+        $this->writeXfStyles($objWriter, $this->getParentWriter()->getSpreadsheet());
+        $objWriter->endElement();
+
+        $objWriter->startElement('office:body');
+        $objWriter->startElement('office:spreadsheet');
+        $objWriter->writeElement('table:calculation-settings');
+
+        $this->writeSheets($objWriter);
+
+        (new AutoFilters($objWriter, $this->getParentWriter()->getSpreadsheet()))->write();
+        // Defined names (ranges and formulae)
+        (new NamedExpressions($objWriter, $this->getParentWriter()->getSpreadsheet(), $this->formulaConvertor))->write();
+
+        $objWriter->endElement();
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write sheets.
+     */
+    private function writeSheets(XMLWriter $objWriter): void
+    {
+        $spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */
+        $sheetCount = $spreadsheet->getSheetCount();
+        for ($sheetIndex = 0; $sheetIndex < $sheetCount; ++$sheetIndex) {
+            $objWriter->startElement('table:table');
+            $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle());
+            $objWriter->writeAttribute('table:style-name', Style::TABLE_STYLE_PREFIX . (string) ($sheetIndex + 1));
+            $objWriter->writeElement('office:forms');
+            $lastColumn = 0;
+            foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) {
+                $thisColumn = $columnDimension->getColumnNumeric();
+                $emptyColumns = $thisColumn - $lastColumn - 1;
+                if ($emptyColumns > 0) {
+                    $objWriter->startElement('table:table-column');
+                    $objWriter->writeAttribute('table:number-columns-repeated', (string) $emptyColumns);
+                    $objWriter->endElement();
+                }
+                $lastColumn = $thisColumn;
+                $objWriter->startElement('table:table-column');
+                $objWriter->writeAttribute(
+                    'table:style-name',
+                    sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric())
+                );
+                $objWriter->writeAttribute('table:default-cell-style-name', 'ce0');
+//                $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
+                $objWriter->endElement();
+            }
+            $this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex);
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write rows of the specified sheet.
+     */
+    private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void
+    {
+        $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX;
+        $span_row = 0;
+        $rows = $sheet->getRowIterator();
+        foreach ($rows as $row) {
+            $cellIterator = $row->getCellIterator();
+            --$numberRowsRepeated;
+            if ($cellIterator->valid()) {
+                $objWriter->startElement('table:table-row');
+                if ($span_row) {
+                    if ($span_row > 1) {
+                        $objWriter->writeAttribute('table:number-rows-repeated', (string) $span_row);
+                    }
+                    $objWriter->startElement('table:table-cell');
+                    $objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX);
+                    $objWriter->endElement();
+                    $span_row = 0;
+                } else {
+                    if ($sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) {
+                        $objWriter->writeAttribute(
+                            'table:style-name',
+                            sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex())
+                        );
+                    }
+                    $this->writeCells($objWriter, $cellIterator);
+                }
+                $objWriter->endElement();
+            } else {
+                ++$span_row;
+            }
+        }
+    }
+
+    /**
+     * Write cells of the specified row.
+     */
+    private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void
+    {
+        $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX;
+        $prevColumn = -1;
+        foreach ($cells as $cell) {
+            /** @var \PhpOffice\PhpSpreadsheet\Cell\Cell $cell */
+            $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
+
+            $this->writeCellSpan($objWriter, $column, $prevColumn);
+            $objWriter->startElement('table:table-cell');
+            $this->writeCellMerge($objWriter, $cell);
+
+            // Style XF
+            $style = $cell->getXfIndex();
+            if ($style !== null) {
+                $objWriter->writeAttribute('table:style-name', Style::CELL_STYLE_PREFIX . $style);
+            }
+
+            switch ($cell->getDataType()) {
+                case DataType::TYPE_BOOL:
+                    $objWriter->writeAttribute('office:value-type', 'boolean');
+                    $objWriter->writeAttribute('office:value', $cell->getValue());
+                    $objWriter->writeElement('text:p', $cell->getValue());
+
+                    break;
+                case DataType::TYPE_ERROR:
+                    $objWriter->writeAttribute('table:formula', 'of:=#NULL!');
+                    $objWriter->writeAttribute('office:value-type', 'string');
+                    $objWriter->writeAttribute('office:string-value', '');
+                    $objWriter->writeElement('text:p', '#NULL!');
+
+                    break;
+                case DataType::TYPE_FORMULA:
+                    $formulaValue = $cell->getValue();
+                    if ($this->getParentWriter()->getPreCalculateFormulas()) {
+                        try {
+                            $formulaValue = $cell->getCalculatedValue();
+                        } catch (Exception $e) {
+                            // don't do anything
+                        }
+                    }
+                    $objWriter->writeAttribute('table:formula', $this->formulaConvertor->convertFormula($cell->getValue()));
+                    if (is_numeric($formulaValue)) {
+                        $objWriter->writeAttribute('office:value-type', 'float');
+                    } else {
+                        $objWriter->writeAttribute('office:value-type', 'string');
+                    }
+                    $objWriter->writeAttribute('office:value', $formulaValue);
+                    $objWriter->writeElement('text:p', $formulaValue);
+
+                    break;
+                case DataType::TYPE_NUMERIC:
+                    $objWriter->writeAttribute('office:value-type', 'float');
+                    $objWriter->writeAttribute('office:value', $cell->getValue());
+                    $objWriter->writeElement('text:p', $cell->getValue());
+
+                    break;
+                case DataType::TYPE_INLINE:
+                    // break intentionally omitted
+                case DataType::TYPE_STRING:
+                    $objWriter->writeAttribute('office:value-type', 'string');
+                    $objWriter->writeElement('text:p', $cell->getValue());
+
+                    break;
+            }
+            Comment::write($objWriter, $cell);
+            $objWriter->endElement();
+            $prevColumn = $column;
+        }
+
+        $numberColsRepeated = $numberColsRepeated - $prevColumn - 1;
+        if ($numberColsRepeated > 0) {
+            if ($numberColsRepeated > 1) {
+                $objWriter->startElement('table:table-cell');
+                $objWriter->writeAttribute('table:number-columns-repeated', (string) $numberColsRepeated);
+                $objWriter->endElement();
+            } else {
+                $objWriter->writeElement('table:table-cell');
+            }
+        }
+    }
+
+    /**
+     * Write span.
+     *
+     * @param int $curColumn
+     * @param int $prevColumn
+     */
+    private function writeCellSpan(XMLWriter $objWriter, $curColumn, $prevColumn): void
+    {
+        $diff = $curColumn - $prevColumn - 1;
+        if (1 === $diff) {
+            $objWriter->writeElement('table:table-cell');
+        } elseif ($diff > 1) {
+            $objWriter->startElement('table:table-cell');
+            $objWriter->writeAttribute('table:number-columns-repeated', (string) $diff);
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write XF cell styles.
+     */
+    private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void
+    {
+        $styleWriter = new Style($writer);
+
+        $sheetCount = $spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            $worksheet = $spreadsheet->getSheet($i);
+            $styleWriter->writeTableStyle($worksheet, $i + 1);
+
+            $worksheet->calculateColumnWidths();
+            foreach ($worksheet->getColumnDimensions() as $columnDimension) {
+                if ($columnDimension->getWidth() !== -1.0) {
+                    $styleWriter->writeColumnStyles($columnDimension, $i);
+                }
+            }
+        }
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            $worksheet = $spreadsheet->getSheet($i);
+            foreach ($worksheet->getRowDimensions() as $rowDimension) {
+                if ($rowDimension->getRowHeight() > 0.0) {
+                    $styleWriter->writeRowStyles($rowDimension, $i);
+                }
+            }
+        }
+
+        foreach ($spreadsheet->getCellXfCollection() as $style) {
+            $styleWriter->write($style);
+        }
+    }
+
+    /**
+     * Write attributes for merged cell.
+     */
+    private function writeCellMerge(XMLWriter $objWriter, Cell $cell): void
+    {
+        if (!$cell->isMergeRangeValueCell()) {
+            return;
+        }
+
+        $mergeRange = Coordinate::splitRange((string) $cell->getMergeRange());
+        [$startCell, $endCell] = $mergeRange[0];
+        $start = Coordinate::coordinateFromString($startCell);
+        $end = Coordinate::coordinateFromString($endCell);
+        $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1;
+        $rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1;
+
+        $objWriter->writeAttribute('table:number-columns-spanned', (string) $columnSpan);
+        $objWriter->writeAttribute('table:number-rows-spanned', (string) $rowSpan);
+    }
+}

+ 120 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Formula.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\DefinedName;
+
+class Formula
+{
+    /** @var array */
+    private $definedNames = [];
+
+    /**
+     * @param DefinedName[] $definedNames
+     */
+    public function __construct(array $definedNames)
+    {
+        foreach ($definedNames as $definedName) {
+            $this->definedNames[] = $definedName->getName();
+        }
+    }
+
+    public function convertFormula(string $formula, string $worksheetName = ''): string
+    {
+        $formula = $this->convertCellReferences($formula, $worksheetName);
+        $formula = $this->convertDefinedNames($formula);
+
+        if (substr($formula, 0, 1) !== '=') {
+            $formula = '=' . $formula;
+        }
+
+        return 'of:' . $formula;
+    }
+
+    private function convertDefinedNames(string $formula): string
+    {
+        $splitCount = preg_match_all(
+            '/' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '/mui',
+            $formula,
+            $splitRanges,
+            PREG_OFFSET_CAPTURE
+        );
+
+        $lengths = array_map('strlen', array_column($splitRanges[0], 0));
+        $offsets = array_column($splitRanges[0], 1);
+        $values = array_column($splitRanges[0], 0);
+
+        while ($splitCount > 0) {
+            --$splitCount;
+            $length = $lengths[$splitCount];
+            $offset = $offsets[$splitCount];
+            $value = $values[$splitCount];
+
+            if (in_array($value, $this->definedNames, true)) {
+                $formula = substr($formula, 0, $offset) . '$$' . $value . substr($formula, $offset + $length);
+            }
+        }
+
+        return $formula;
+    }
+
+    private function convertCellReferences(string $formula, string $worksheetName): string
+    {
+        $splitCount = preg_match_all(
+            '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui',
+            $formula,
+            $splitRanges,
+            PREG_OFFSET_CAPTURE
+        );
+
+        $lengths = array_map('strlen', array_column($splitRanges[0], 0));
+        $offsets = array_column($splitRanges[0], 1);
+
+        $worksheets = $splitRanges[2];
+        $columns = $splitRanges[6];
+        $rows = $splitRanges[7];
+
+        // Replace any commas in the formula with semi-colons for Ods
+        // If by chance there are commas in worksheet names, then they will be "fixed" again in the loop
+        //    because we've already extracted worksheet names with our preg_match_all()
+        $formula = str_replace(',', ';', $formula);
+        while ($splitCount > 0) {
+            --$splitCount;
+            $length = $lengths[$splitCount];
+            $offset = $offsets[$splitCount];
+            $worksheet = $worksheets[$splitCount][0];
+            $column = $columns[$splitCount][0];
+            $row = $rows[$splitCount][0];
+
+            $newRange = '';
+            if (empty($worksheet)) {
+                if (($offset === 0) || ($formula[$offset - 1] !== ':')) {
+                    // We need a worksheet
+                    $worksheet = $worksheetName;
+                }
+            } else {
+                $worksheet = str_replace("''", "'", trim($worksheet, "'"));
+            }
+            if (!empty($worksheet)) {
+                $newRange = "['" . str_replace("'", "''", $worksheet) . "'";
+            } elseif (substr($formula, $offset - 1, 1) !== ':') {
+                $newRange = '[';
+            }
+            $newRange .= '.';
+
+            if (!empty($column)) {
+                $newRange .= $column;
+            }
+            if (!empty($row)) {
+                $newRange .= $row;
+            }
+            // close the wrapping [] unless this is the first part of a range
+            $newRange .= substr($formula, $offset + $length, 1) !== ':' ? ']' : '';
+
+            $formula = substr($formula, 0, $offset) . $newRange . substr($formula, $offset + $length);
+        }
+
+        return $formula;
+    }
+}

+ 122 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Meta.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+use PhpOffice\PhpSpreadsheet\Document\Properties;
+use PhpOffice\PhpSpreadsheet\Shared\Date;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+
+class Meta extends WriterPart
+{
+    /**
+     * Write meta.xml to XML format.
+     *
+     * @return string XML Output
+     */
+    public function write(): string
+    {
+        $spreadsheet = $this->getParentWriter()->getSpreadsheet();
+
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8');
+
+        // Meta
+        $objWriter->startElement('office:document-meta');
+
+        $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
+        $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
+        $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
+        $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0');
+        $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
+        $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#');
+        $objWriter->writeAttribute('office:version', '1.2');
+
+        $objWriter->startElement('office:meta');
+
+        $objWriter->writeElement('meta:initial-creator', $spreadsheet->getProperties()->getCreator());
+        $objWriter->writeElement('dc:creator', $spreadsheet->getProperties()->getCreator());
+        $created = $spreadsheet->getProperties()->getCreated();
+        $date = Date::dateTimeFromTimestamp("$created");
+        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
+        $objWriter->writeElement('meta:creation-date', $date->format(DATE_W3C));
+        $created = $spreadsheet->getProperties()->getModified();
+        $date = Date::dateTimeFromTimestamp("$created");
+        $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
+        $objWriter->writeElement('dc:date', $date->format(DATE_W3C));
+        $objWriter->writeElement('dc:title', $spreadsheet->getProperties()->getTitle());
+        $objWriter->writeElement('dc:description', $spreadsheet->getProperties()->getDescription());
+        $objWriter->writeElement('dc:subject', $spreadsheet->getProperties()->getSubject());
+        $objWriter->writeElement('meta:keyword', $spreadsheet->getProperties()->getKeywords());
+        // Don't know if this changed over time, but the keywords are all
+        //  in a single declaration now.
+        //$keywords = explode(' ', $spreadsheet->getProperties()->getKeywords());
+        //foreach ($keywords as $keyword) {
+        //    $objWriter->writeElement('meta:keyword', $keyword);
+        //}
+
+        //<meta:document-statistic meta:table-count="XXX" meta:cell-count="XXX" meta:object-count="XXX"/>
+        $objWriter->startElement('meta:user-defined');
+        $objWriter->writeAttribute('meta:name', 'Company');
+        $objWriter->writeRaw($spreadsheet->getProperties()->getCompany());
+        $objWriter->endElement();
+
+        $objWriter->startElement('meta:user-defined');
+        $objWriter->writeAttribute('meta:name', 'category');
+        $objWriter->writeRaw($spreadsheet->getProperties()->getCategory());
+        $objWriter->endElement();
+
+        self::writeDocPropsCustom($objWriter, $spreadsheet);
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+
+    private static function writeDocPropsCustom(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
+    {
+        $customPropertyList = $spreadsheet->getProperties()->getCustomProperties();
+        foreach ($customPropertyList as $key => $customProperty) {
+            $propertyValue = $spreadsheet->getProperties()->getCustomPropertyValue($customProperty);
+            $propertyType = $spreadsheet->getProperties()->getCustomPropertyType($customProperty);
+
+            $objWriter->startElement('meta:user-defined');
+            $objWriter->writeAttribute('meta:name', $customProperty);
+
+            switch ($propertyType) {
+                case Properties::PROPERTY_TYPE_INTEGER:
+                case Properties::PROPERTY_TYPE_FLOAT:
+                    $objWriter->writeAttribute('meta:value-type', 'float');
+                    $objWriter->writeRawData($propertyValue);
+
+                    break;
+                case Properties::PROPERTY_TYPE_BOOLEAN:
+                    $objWriter->writeAttribute('meta:value-type', 'boolean');
+                    $objWriter->writeRawData($propertyValue ? 'true' : 'false');
+
+                    break;
+                case Properties::PROPERTY_TYPE_DATE:
+                    $objWriter->writeAttribute('meta:value-type', 'date');
+                    $dtobj = Date::dateTimeFromTimestamp($propertyValue ?? 0);
+                    $objWriter->writeRawData($dtobj->format(DATE_W3C));
+
+                    break;
+                default:
+                    $objWriter->writeRawData($propertyValue);
+
+                    break;
+            }
+
+            $objWriter->endElement();
+        }
+    }
+}

+ 60 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/MetaInf.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+
+class MetaInf extends WriterPart
+{
+    /**
+     * Write META-INF/manifest.xml to XML format.
+     *
+     * @return string XML Output
+     */
+    public function write(): string
+    {
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8');
+
+        // Manifest
+        $objWriter->startElement('manifest:manifest');
+        $objWriter->writeAttribute('xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0');
+        $objWriter->writeAttribute('manifest:version', '1.2');
+
+        $objWriter->startElement('manifest:file-entry');
+        $objWriter->writeAttribute('manifest:full-path', '/');
+        $objWriter->writeAttribute('manifest:version', '1.2');
+        $objWriter->writeAttribute('manifest:media-type', 'application/vnd.oasis.opendocument.spreadsheet');
+        $objWriter->endElement();
+        $objWriter->startElement('manifest:file-entry');
+        $objWriter->writeAttribute('manifest:full-path', 'meta.xml');
+        $objWriter->writeAttribute('manifest:media-type', 'text/xml');
+        $objWriter->endElement();
+        $objWriter->startElement('manifest:file-entry');
+        $objWriter->writeAttribute('manifest:full-path', 'settings.xml');
+        $objWriter->writeAttribute('manifest:media-type', 'text/xml');
+        $objWriter->endElement();
+        $objWriter->startElement('manifest:file-entry');
+        $objWriter->writeAttribute('manifest:full-path', 'content.xml');
+        $objWriter->writeAttribute('manifest:media-type', 'text/xml');
+        $objWriter->endElement();
+        $objWriter->startElement('manifest:file-entry');
+        $objWriter->writeAttribute('manifest:full-path', 'Thumbnails/thumbnail.png');
+        $objWriter->writeAttribute('manifest:media-type', 'image/png');
+        $objWriter->endElement();
+        $objWriter->startElement('manifest:file-entry');
+        $objWriter->writeAttribute('manifest:full-path', 'styles.xml');
+        $objWriter->writeAttribute('manifest:media-type', 'text/xml');
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+}

+ 16 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Mimetype.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+class Mimetype extends WriterPart
+{
+    /**
+     * Write mimetype to plain text format.
+     *
+     * @return string XML Output
+     */
+    public function write(): string
+    {
+        return 'application/vnd.oasis.opendocument.spreadsheet';
+    }
+}

+ 140 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php

@@ -0,0 +1,140 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\DefinedName;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+
+class NamedExpressions
+{
+    /** @var XMLWriter */
+    private $objWriter;
+
+    /** @var Spreadsheet */
+    private $spreadsheet;
+
+    /** @var Formula */
+    private $formulaConvertor;
+
+    public function __construct(XMLWriter $objWriter, Spreadsheet $spreadsheet, Formula $formulaConvertor)
+    {
+        $this->objWriter = $objWriter;
+        $this->spreadsheet = $spreadsheet;
+        $this->formulaConvertor = $formulaConvertor;
+    }
+
+    public function write(): string
+    {
+        $this->objWriter->startElement('table:named-expressions');
+        $this->writeExpressions();
+        $this->objWriter->endElement();
+
+        return '';
+    }
+
+    private function writeExpressions(): void
+    {
+        $definedNames = $this->spreadsheet->getDefinedNames();
+
+        foreach ($definedNames as $definedName) {
+            if ($definedName->isFormula()) {
+                $this->objWriter->startElement('table:named-expression');
+                $this->writeNamedFormula($definedName, $this->spreadsheet->getActiveSheet());
+            } else {
+                $this->objWriter->startElement('table:named-range');
+                $this->writeNamedRange($definedName);
+            }
+
+            $this->objWriter->endElement();
+        }
+    }
+
+    private function writeNamedFormula(DefinedName $definedName, Worksheet $defaultWorksheet): void
+    {
+        $title = ($definedName->getWorksheet() !== null) ? $definedName->getWorksheet()->getTitle() : $defaultWorksheet->getTitle();
+        $this->objWriter->writeAttribute('table:name', $definedName->getName());
+        $this->objWriter->writeAttribute(
+            'table:expression',
+            $this->formulaConvertor->convertFormula($definedName->getValue(), $title)
+        );
+        $this->objWriter->writeAttribute('table:base-cell-address', $this->convertAddress(
+            $definedName,
+            "'" . $title . "'!\$A\$1"
+        ));
+    }
+
+    private function writeNamedRange(DefinedName $definedName): void
+    {
+        $baseCell = '$A$1';
+        $ws = $definedName->getWorksheet();
+        if ($ws !== null) {
+            $baseCell = "'" . $ws->getTitle() . "'!$baseCell";
+        }
+        $this->objWriter->writeAttribute('table:name', $definedName->getName());
+        $this->objWriter->writeAttribute('table:base-cell-address', $this->convertAddress(
+            $definedName,
+            $baseCell
+        ));
+        $this->objWriter->writeAttribute('table:cell-range-address', $this->convertAddress($definedName, $definedName->getValue()));
+    }
+
+    private function convertAddress(DefinedName $definedName, string $address): string
+    {
+        $splitCount = preg_match_all(
+            '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui',
+            $address,
+            $splitRanges,
+            PREG_OFFSET_CAPTURE
+        );
+
+        $lengths = array_map('strlen', array_column($splitRanges[0], 0));
+        $offsets = array_column($splitRanges[0], 1);
+
+        $worksheets = $splitRanges[2];
+        $columns = $splitRanges[6];
+        $rows = $splitRanges[7];
+
+        while ($splitCount > 0) {
+            --$splitCount;
+            $length = $lengths[$splitCount];
+            $offset = $offsets[$splitCount];
+            $worksheet = $worksheets[$splitCount][0];
+            $column = $columns[$splitCount][0];
+            $row = $rows[$splitCount][0];
+
+            $newRange = '';
+            if (empty($worksheet)) {
+                if (($offset === 0) || ($address[$offset - 1] !== ':')) {
+                    // We need a worksheet
+                    $ws = $definedName->getWorksheet();
+                    if ($ws !== null) {
+                        $worksheet = $ws->getTitle();
+                    }
+                }
+            } else {
+                $worksheet = str_replace("''", "'", trim($worksheet, "'"));
+            }
+            if (!empty($worksheet)) {
+                $newRange = "'" . str_replace("'", "''", $worksheet) . "'.";
+            }
+
+            if (!empty($column)) {
+                $newRange .= $column;
+            }
+            if (!empty($row)) {
+                $newRange .= $row;
+            }
+
+            $address = substr($address, 0, $offset) . $newRange . substr($address, $offset + $length);
+        }
+
+        if (substr($address, 0, 1) === '=') {
+            $address = substr($address, 1);
+        }
+
+        return $address;
+    }
+}

+ 152 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Settings.php

@@ -0,0 +1,152 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+
+class Settings extends WriterPart
+{
+    /**
+     * Write settings.xml to XML format.
+     *
+     * @return string XML Output
+     */
+    public function write(): string
+    {
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8');
+
+        // Settings
+        $objWriter->startElement('office:document-settings');
+        $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
+        $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
+        $objWriter->writeAttribute('xmlns:config', 'urn:oasis:names:tc:opendocument:xmlns:config:1.0');
+        $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
+        $objWriter->writeAttribute('office:version', '1.2');
+
+        $objWriter->startElement('office:settings');
+        $objWriter->startElement('config:config-item-set');
+        $objWriter->writeAttribute('config:name', 'ooo:view-settings');
+        $objWriter->startElement('config:config-item-map-indexed');
+        $objWriter->writeAttribute('config:name', 'Views');
+        $objWriter->startElement('config:config-item-map-entry');
+        $spreadsheet = $this->getParentWriter()->getSpreadsheet();
+
+        $objWriter->startElement('config:config-item');
+        $objWriter->writeAttribute('config:name', 'ViewId');
+        $objWriter->writeAttribute('config:type', 'string');
+        $objWriter->text('view1');
+        $objWriter->endElement(); // ViewId
+        $objWriter->startElement('config:config-item-map-named');
+
+        $this->writeAllWorksheetSettings($objWriter, $spreadsheet);
+
+        $wstitle = $spreadsheet->getActiveSheet()->getTitle();
+        $objWriter->startElement('config:config-item');
+        $objWriter->writeAttribute('config:name', 'ActiveTable');
+        $objWriter->writeAttribute('config:type', 'string');
+        $objWriter->text($wstitle);
+        $objWriter->endElement(); // config:config-item ActiveTable
+
+        $objWriter->endElement(); // config:config-item-map-entry
+        $objWriter->endElement(); // config:config-item-map-indexed Views
+        $objWriter->endElement(); // config:config-item-set ooo:view-settings
+        $objWriter->startElement('config:config-item-set');
+        $objWriter->writeAttribute('config:name', 'ooo:configuration-settings');
+        $objWriter->endElement(); // config:config-item-set ooo:configuration-settings
+        $objWriter->endElement(); // office:settings
+        $objWriter->endElement(); // office:document-settings
+
+        return $objWriter->getData();
+    }
+
+    private function writeAllWorksheetSettings(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
+    {
+        $objWriter->writeAttribute('config:name', 'Tables');
+
+        foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
+            $this->writeWorksheetSettings($objWriter, $worksheet);
+        }
+
+        $objWriter->endElement(); // config:config-item-map-entry Tables
+    }
+
+    private function writeWorksheetSettings(XMLWriter $objWriter, Worksheet $worksheet): void
+    {
+        $objWriter->startElement('config:config-item-map-entry');
+        $objWriter->writeAttribute('config:name', $worksheet->getTitle());
+
+        $this->writeSelectedCells($objWriter, $worksheet);
+        if ($worksheet->getFreezePane() !== null) {
+            $this->writeFreezePane($objWriter, $worksheet);
+        }
+
+        $objWriter->endElement(); // config:config-item-map-entry Worksheet
+    }
+
+    private function writeSelectedCells(XMLWriter $objWriter, Worksheet $worksheet): void
+    {
+        $selected = $worksheet->getSelectedCells();
+        if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) {
+            $colSel = Coordinate::columnIndexFromString($matches[1]) - 1;
+            $rowSel = (int) $matches[2] - 1;
+            $objWriter->startElement('config:config-item');
+            $objWriter->writeAttribute('config:name', 'CursorPositionX');
+            $objWriter->writeAttribute('config:type', 'int');
+            $objWriter->text((string) $colSel);
+            $objWriter->endElement();
+            $objWriter->startElement('config:config-item');
+            $objWriter->writeAttribute('config:name', 'CursorPositionY');
+            $objWriter->writeAttribute('config:type', 'int');
+            $objWriter->text((string) $rowSel);
+            $objWriter->endElement();
+        }
+    }
+
+    private function writeSplitValue(XMLWriter $objWriter, string $splitMode, string $type, string $value): void
+    {
+        $objWriter->startElement('config:config-item');
+        $objWriter->writeAttribute('config:name', $splitMode);
+        $objWriter->writeAttribute('config:type', $type);
+        $objWriter->text($value);
+        $objWriter->endElement();
+    }
+
+    private function writeFreezePane(XMLWriter $objWriter, Worksheet $worksheet): void
+    {
+        $freezePane = CellAddress::fromCellAddress($worksheet->getFreezePane());
+        if ($freezePane->cellAddress() === 'A1') {
+            return;
+        }
+
+        $columnId = $freezePane->columnId();
+        $columnName = $freezePane->columnName();
+        $row = $freezePane->rowId();
+
+        $this->writeSplitValue($objWriter, 'HorizontalSplitMode', 'short', '2');
+        $this->writeSplitValue($objWriter, 'HorizontalSplitPosition', 'int', (string) ($columnId - 1));
+        $this->writeSplitValue($objWriter, 'PositionLeft', 'short', '0');
+        $this->writeSplitValue($objWriter, 'PositionRight', 'short', (string) ($columnId - 1));
+
+        for ($column = 'A'; $column !== $columnName; ++$column) {
+            $worksheet->getColumnDimension($column)->setAutoSize(true);
+        }
+
+        $this->writeSplitValue($objWriter, 'VerticalSplitMode', 'short', '2');
+        $this->writeSplitValue($objWriter, 'VerticalSplitPosition', 'int', (string) ($row - 1));
+        $this->writeSplitValue($objWriter, 'PositionTop', 'short', '0');
+        $this->writeSplitValue($objWriter, 'PositionBottom', 'short', (string) ($row - 1));
+
+        $this->writeSplitValue($objWriter, 'ActiveSplitRange', 'short', '3');
+    }
+}

+ 65 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Styles.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+
+class Styles extends WriterPart
+{
+    /**
+     * Write styles.xml to XML format.
+     *
+     * @return string XML Output
+     */
+    public function write(): string
+    {
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8');
+
+        // Content
+        $objWriter->startElement('office:document-styles');
+        $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
+        $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0');
+        $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
+        $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0');
+        $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0');
+        $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
+        $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
+        $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
+        $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0');
+        $objWriter->writeAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0');
+        $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0');
+        $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0');
+        $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0');
+        $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0');
+        $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML');
+        $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0');
+        $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0');
+        $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
+        $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer');
+        $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc');
+        $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events');
+        $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report');
+        $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2');
+        $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
+        $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#');
+        $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table');
+        $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/');
+        $objWriter->writeAttribute('office:version', '1.2');
+
+        $objWriter->writeElement('office:font-face-decls');
+        $objWriter->writeElement('office:styles');
+        $objWriter->writeElement('office:automatic-styles');
+        $objWriter->writeElement('office:master-styles');
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+}

+ 16 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Thumbnails.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+class Thumbnails extends WriterPart
+{
+    /**
+     * Write Thumbnails/thumbnail.png to PNG format.
+     *
+     * @return string XML Output
+     */
+    public function write(): string
+    {
+        return '';
+    }
+}

+ 35 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/WriterPart.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+use PhpOffice\PhpSpreadsheet\Writer\Ods;
+
+abstract class WriterPart
+{
+    /**
+     * Parent Ods object.
+     *
+     * @var Ods
+     */
+    private $parentWriter;
+
+    /**
+     * Get Ods writer.
+     *
+     * @return Ods
+     */
+    public function getParentWriter()
+    {
+        return $this->parentWriter;
+    }
+
+    /**
+     * Set parent Ods writer.
+     */
+    public function __construct(Ods $writer)
+    {
+        $this->parentWriter = $writer;
+    }
+
+    abstract public function write(): string;
+}

+ 251 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf.php

@@ -0,0 +1,251 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer;
+
+use PhpOffice\PhpSpreadsheet\Shared\File;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+
+abstract class Pdf extends Html
+{
+    /**
+     * Temporary storage directory.
+     *
+     * @var string
+     */
+    protected $tempDir = '';
+
+    /**
+     * Font.
+     *
+     * @var string
+     */
+    protected $font = 'freesans';
+
+    /**
+     * Orientation (Over-ride).
+     *
+     * @var ?string
+     */
+    protected $orientation;
+
+    /**
+     * Paper size (Over-ride).
+     *
+     * @var ?int
+     */
+    protected $paperSize;
+
+    /**
+     * Paper Sizes xRef List.
+     *
+     * @var array
+     */
+    protected static $paperSizes = [
+        PageSetup::PAPERSIZE_LETTER => 'LETTER', //    (8.5 in. by 11 in.)
+        PageSetup::PAPERSIZE_LETTER_SMALL => 'LETTER', //    (8.5 in. by 11 in.)
+        PageSetup::PAPERSIZE_TABLOID => [792.00, 1224.00], //    (11 in. by 17 in.)
+        PageSetup::PAPERSIZE_LEDGER => [1224.00, 792.00], //    (17 in. by 11 in.)
+        PageSetup::PAPERSIZE_LEGAL => 'LEGAL', //    (8.5 in. by 14 in.)
+        PageSetup::PAPERSIZE_STATEMENT => [396.00, 612.00], //    (5.5 in. by 8.5 in.)
+        PageSetup::PAPERSIZE_EXECUTIVE => 'EXECUTIVE', //    (7.25 in. by 10.5 in.)
+        PageSetup::PAPERSIZE_A3 => 'A3', //    (297 mm by 420 mm)
+        PageSetup::PAPERSIZE_A4 => 'A4', //    (210 mm by 297 mm)
+        PageSetup::PAPERSIZE_A4_SMALL => 'A4', //    (210 mm by 297 mm)
+        PageSetup::PAPERSIZE_A5 => 'A5', //    (148 mm by 210 mm)
+        PageSetup::PAPERSIZE_B4 => 'B4', //    (250 mm by 353 mm)
+        PageSetup::PAPERSIZE_B5 => 'B5', //    (176 mm by 250 mm)
+        PageSetup::PAPERSIZE_FOLIO => 'FOLIO', //    (8.5 in. by 13 in.)
+        PageSetup::PAPERSIZE_QUARTO => [609.45, 779.53], //    (215 mm by 275 mm)
+        PageSetup::PAPERSIZE_STANDARD_1 => [720.00, 1008.00], //    (10 in. by 14 in.)
+        PageSetup::PAPERSIZE_STANDARD_2 => [792.00, 1224.00], //    (11 in. by 17 in.)
+        PageSetup::PAPERSIZE_NOTE => 'LETTER', //    (8.5 in. by 11 in.)
+        PageSetup::PAPERSIZE_NO9_ENVELOPE => [279.00, 639.00], //    (3.875 in. by 8.875 in.)
+        PageSetup::PAPERSIZE_NO10_ENVELOPE => [297.00, 684.00], //    (4.125 in. by 9.5 in.)
+        PageSetup::PAPERSIZE_NO11_ENVELOPE => [324.00, 747.00], //    (4.5 in. by 10.375 in.)
+        PageSetup::PAPERSIZE_NO12_ENVELOPE => [342.00, 792.00], //    (4.75 in. by 11 in.)
+        PageSetup::PAPERSIZE_NO14_ENVELOPE => [360.00, 828.00], //    (5 in. by 11.5 in.)
+        PageSetup::PAPERSIZE_C => [1224.00, 1584.00], //    (17 in. by 22 in.)
+        PageSetup::PAPERSIZE_D => [1584.00, 2448.00], //    (22 in. by 34 in.)
+        PageSetup::PAPERSIZE_E => [2448.00, 3168.00], //    (34 in. by 44 in.)
+        PageSetup::PAPERSIZE_DL_ENVELOPE => [311.81, 623.62], //    (110 mm by 220 mm)
+        PageSetup::PAPERSIZE_C5_ENVELOPE => 'C5', //    (162 mm by 229 mm)
+        PageSetup::PAPERSIZE_C3_ENVELOPE => 'C3', //    (324 mm by 458 mm)
+        PageSetup::PAPERSIZE_C4_ENVELOPE => 'C4', //    (229 mm by 324 mm)
+        PageSetup::PAPERSIZE_C6_ENVELOPE => 'C6', //    (114 mm by 162 mm)
+        PageSetup::PAPERSIZE_C65_ENVELOPE => [323.15, 649.13], //    (114 mm by 229 mm)
+        PageSetup::PAPERSIZE_B4_ENVELOPE => 'B4', //    (250 mm by 353 mm)
+        PageSetup::PAPERSIZE_B5_ENVELOPE => 'B5', //    (176 mm by 250 mm)
+        PageSetup::PAPERSIZE_B6_ENVELOPE => [498.90, 354.33], //    (176 mm by 125 mm)
+        PageSetup::PAPERSIZE_ITALY_ENVELOPE => [311.81, 651.97], //    (110 mm by 230 mm)
+        PageSetup::PAPERSIZE_MONARCH_ENVELOPE => [279.00, 540.00], //    (3.875 in. by 7.5 in.)
+        PageSetup::PAPERSIZE_6_3_4_ENVELOPE => [261.00, 468.00], //    (3.625 in. by 6.5 in.)
+        PageSetup::PAPERSIZE_US_STANDARD_FANFOLD => [1071.00, 792.00], //    (14.875 in. by 11 in.)
+        PageSetup::PAPERSIZE_GERMAN_STANDARD_FANFOLD => [612.00, 864.00], //    (8.5 in. by 12 in.)
+        PageSetup::PAPERSIZE_GERMAN_LEGAL_FANFOLD => 'FOLIO', //    (8.5 in. by 13 in.)
+        PageSetup::PAPERSIZE_ISO_B4 => 'B4', //    (250 mm by 353 mm)
+        PageSetup::PAPERSIZE_JAPANESE_DOUBLE_POSTCARD => [566.93, 419.53], //    (200 mm by 148 mm)
+        PageSetup::PAPERSIZE_STANDARD_PAPER_1 => [648.00, 792.00], //    (9 in. by 11 in.)
+        PageSetup::PAPERSIZE_STANDARD_PAPER_2 => [720.00, 792.00], //    (10 in. by 11 in.)
+        PageSetup::PAPERSIZE_STANDARD_PAPER_3 => [1080.00, 792.00], //    (15 in. by 11 in.)
+        PageSetup::PAPERSIZE_INVITE_ENVELOPE => [623.62, 623.62], //    (220 mm by 220 mm)
+        PageSetup::PAPERSIZE_LETTER_EXTRA_PAPER => [667.80, 864.00], //    (9.275 in. by 12 in.)
+        PageSetup::PAPERSIZE_LEGAL_EXTRA_PAPER => [667.80, 1080.00], //    (9.275 in. by 15 in.)
+        PageSetup::PAPERSIZE_TABLOID_EXTRA_PAPER => [841.68, 1296.00], //    (11.69 in. by 18 in.)
+        PageSetup::PAPERSIZE_A4_EXTRA_PAPER => [668.98, 912.76], //    (236 mm by 322 mm)
+        PageSetup::PAPERSIZE_LETTER_TRANSVERSE_PAPER => [595.80, 792.00], //    (8.275 in. by 11 in.)
+        PageSetup::PAPERSIZE_A4_TRANSVERSE_PAPER => 'A4', //    (210 mm by 297 mm)
+        PageSetup::PAPERSIZE_LETTER_EXTRA_TRANSVERSE_PAPER => [667.80, 864.00], //    (9.275 in. by 12 in.)
+        PageSetup::PAPERSIZE_SUPERA_SUPERA_A4_PAPER => [643.46, 1009.13], //    (227 mm by 356 mm)
+        PageSetup::PAPERSIZE_SUPERB_SUPERB_A3_PAPER => [864.57, 1380.47], //    (305 mm by 487 mm)
+        PageSetup::PAPERSIZE_LETTER_PLUS_PAPER => [612.00, 913.68], //    (8.5 in. by 12.69 in.)
+        PageSetup::PAPERSIZE_A4_PLUS_PAPER => [595.28, 935.43], //    (210 mm by 330 mm)
+        PageSetup::PAPERSIZE_A5_TRANSVERSE_PAPER => 'A5', //    (148 mm by 210 mm)
+        PageSetup::PAPERSIZE_JIS_B5_TRANSVERSE_PAPER => [515.91, 728.50], //    (182 mm by 257 mm)
+        PageSetup::PAPERSIZE_A3_EXTRA_PAPER => [912.76, 1261.42], //    (322 mm by 445 mm)
+        PageSetup::PAPERSIZE_A5_EXTRA_PAPER => [493.23, 666.14], //    (174 mm by 235 mm)
+        PageSetup::PAPERSIZE_ISO_B5_EXTRA_PAPER => [569.76, 782.36], //    (201 mm by 276 mm)
+        PageSetup::PAPERSIZE_A2_PAPER => 'A2', //    (420 mm by 594 mm)
+        PageSetup::PAPERSIZE_A3_TRANSVERSE_PAPER => 'A3', //    (297 mm by 420 mm)
+        PageSetup::PAPERSIZE_A3_EXTRA_TRANSVERSE_PAPER => [912.76, 1261.42], //    (322 mm by 445 mm)
+    ];
+
+    /**
+     * Create a new PDF Writer instance.
+     *
+     * @param Spreadsheet $spreadsheet Spreadsheet object
+     */
+    public function __construct(Spreadsheet $spreadsheet)
+    {
+        parent::__construct($spreadsheet);
+        //$this->setUseInlineCss(true);
+        $this->tempDir = File::sysGetTempDir() . '/phpsppdf';
+        $this->isPdf = true;
+    }
+
+    /**
+     * Get Font.
+     *
+     * @return string
+     */
+    public function getFont()
+    {
+        return $this->font;
+    }
+
+    /**
+     * Set font. Examples:
+     *      'arialunicid0-chinese-simplified'
+     *      'arialunicid0-chinese-traditional'
+     *      'arialunicid0-korean'
+     *      'arialunicid0-japanese'.
+     *
+     * @param string $fontName
+     *
+     * @return $this
+     */
+    public function setFont($fontName)
+    {
+        $this->font = $fontName;
+
+        return $this;
+    }
+
+    /**
+     * Get Paper Size.
+     *
+     * @return ?int
+     */
+    public function getPaperSize()
+    {
+        return $this->paperSize;
+    }
+
+    /**
+     * Set Paper Size.
+     *
+     * @param int $paperSize Paper size see PageSetup::PAPERSIZE_*
+     *
+     * @return self
+     */
+    public function setPaperSize($paperSize)
+    {
+        $this->paperSize = $paperSize;
+
+        return $this;
+    }
+
+    /**
+     * Get Orientation.
+     */
+    public function getOrientation(): ?string
+    {
+        return $this->orientation;
+    }
+
+    /**
+     * Set Orientation.
+     *
+     * @param string $orientation Page orientation see PageSetup::ORIENTATION_*
+     *
+     * @return self
+     */
+    public function setOrientation($orientation)
+    {
+        $this->orientation = $orientation;
+
+        return $this;
+    }
+
+    /**
+     * Get temporary storage directory.
+     *
+     * @return string
+     */
+    public function getTempDir()
+    {
+        return $this->tempDir;
+    }
+
+    /**
+     * Set temporary storage directory.
+     *
+     * @param string $temporaryDirectory Temporary storage directory
+     *
+     * @return self
+     */
+    public function setTempDir($temporaryDirectory)
+    {
+        if (is_dir($temporaryDirectory)) {
+            $this->tempDir = $temporaryDirectory;
+        } else {
+            throw new WriterException("Directory does not exist: $temporaryDirectory");
+        }
+
+        return $this;
+    }
+
+    /**
+     * Save Spreadsheet to PDF file, pre-save.
+     *
+     * @param string $filename Name of the file to save as
+     *
+     * @return resource
+     */
+    protected function prepareForSave($filename)
+    {
+        //  Open file
+        $this->openFileHandle($filename);
+
+        return $this->fileHandle;
+    }
+
+    /**
+     * Save PhpSpreadsheet to PDF file, post-save.
+     */
+    protected function restoreStateAfterSave(): void
+    {
+        $this->maybeCloseFileHandle();
+    }
+}

+ 60 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Pdf;
+
+use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
+use PhpOffice\PhpSpreadsheet\Writer\Pdf;
+
+class Dompdf extends Pdf
+{
+    /**
+     * embed images, or link to images.
+     *
+     * @var bool
+     */
+    protected $embedImages = true;
+
+    /**
+     * Gets the implementation of external PDF library that should be used.
+     *
+     * @return \Dompdf\Dompdf implementation
+     */
+    protected function createExternalWriterInstance()
+    {
+        return new \Dompdf\Dompdf();
+    }
+
+    /**
+     * Save Spreadsheet to file.
+     *
+     * @param string $filename Name of the file to save as
+     */
+    public function save($filename, int $flags = 0): void
+    {
+        $fileHandle = parent::prepareForSave($filename);
+
+        //  Check for paper size and page orientation
+        $setup = $this->spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageSetup();
+        $orientation = $this->getOrientation() ?? $setup->getOrientation();
+        $orientation = ($orientation === PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
+        $printPaperSize = $this->getPaperSize() ?? $setup->getPaperSize();
+        $paperSize = self::$paperSizes[$printPaperSize] ?? PageSetup::getPaperSizeDefault();
+        if (is_array($paperSize) && count($paperSize) === 2) {
+            $paperSize = [0.0, 0.0, $paperSize[0], $paperSize[1]];
+        }
+
+        $orientation = ($orientation == 'L') ? 'landscape' : 'portrait';
+
+        //  Create PDF
+        $pdf = $this->createExternalWriterInstance();
+        $pdf->setPaper($paperSize, $orientation);
+
+        $pdf->loadHtml($this->generateHTMLAll());
+        $pdf->render();
+
+        //  Write to file
+        fwrite($fileHandle, $pdf->output() ?? '');
+
+        parent::restoreStateAfterSave();
+    }
+}

+ 93 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Pdf;
+
+use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
+use PhpOffice\PhpSpreadsheet\Writer\Html;
+use PhpOffice\PhpSpreadsheet\Writer\Pdf;
+
+class Mpdf extends Pdf
+{
+    /** @var bool */
+    protected $isMPdf = true;
+
+    /**
+     * Gets the implementation of external PDF library that should be used.
+     *
+     * @param array $config Configuration array
+     *
+     * @return \Mpdf\Mpdf implementation
+     */
+    protected function createExternalWriterInstance($config)
+    {
+        return new \Mpdf\Mpdf($config);
+    }
+
+    /**
+     * Save Spreadsheet to file.
+     *
+     * @param string $filename Name of the file to save as
+     */
+    public function save($filename, int $flags = 0): void
+    {
+        $fileHandle = parent::prepareForSave($filename);
+
+        //  Check for paper size and page orientation
+        $setup = $this->spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageSetup();
+        $orientation = $this->getOrientation() ?? $setup->getOrientation();
+        $orientation = ($orientation === PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
+        $printPaperSize = $this->getPaperSize() ?? $setup->getPaperSize();
+        $paperSize = self::$paperSizes[$printPaperSize] ?? PageSetup::getPaperSizeDefault();
+
+        //  Create PDF
+        $config = ['tempDir' => $this->tempDir . '/mpdf'];
+        $pdf = $this->createExternalWriterInstance($config);
+        $ortmp = $orientation;
+        $pdf->_setPageSize($paperSize, $ortmp);
+        $pdf->DefOrientation = $orientation;
+        $pdf->AddPageByArray([
+            'orientation' => $orientation,
+            'margin-left' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getLeft()),
+            'margin-right' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getRight()),
+            'margin-top' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getTop()),
+            'margin-bottom' => $this->inchesToMm($this->spreadsheet->getActiveSheet()->getPageMargins()->getBottom()),
+        ]);
+
+        //  Document info
+        $pdf->SetTitle($this->spreadsheet->getProperties()->getTitle());
+        $pdf->SetAuthor($this->spreadsheet->getProperties()->getCreator());
+        $pdf->SetSubject($this->spreadsheet->getProperties()->getSubject());
+        $pdf->SetKeywords($this->spreadsheet->getProperties()->getKeywords());
+        $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator());
+
+        $html = $this->generateHTMLAll();
+        $bodyLocation = strpos($html, Html::BODY_LINE);
+        // Make sure first data presented to Mpdf includes body tag
+        //   so that Mpdf doesn't parse it as content. Issue 2432.
+        if ($bodyLocation !== false) {
+            $bodyLocation += strlen(Html::BODY_LINE);
+            $pdf->WriteHTML(substr($html, 0, $bodyLocation));
+            $html = substr($html, $bodyLocation);
+        }
+        foreach (\array_chunk(\explode(PHP_EOL, $html), 1000) as $lines) {
+            $pdf->WriteHTML(\implode(PHP_EOL, $lines));
+        }
+
+        //  Write to file
+        fwrite($fileHandle, $pdf->Output('', 'S'));
+
+        parent::restoreStateAfterSave();
+    }
+
+    /**
+     * Convert inches to mm.
+     *
+     * @param float $inches
+     *
+     * @return float
+     */
+    private function inchesToMm($inches)
+    {
+        return $inches * 25.4;
+    }
+}

+ 84 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Pdf;
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
+use PhpOffice\PhpSpreadsheet\Writer\Pdf;
+
+class Tcpdf extends Pdf
+{
+    /**
+     * Create a new PDF Writer instance.
+     *
+     * @param Spreadsheet $spreadsheet Spreadsheet object
+     */
+    public function __construct(Spreadsheet $spreadsheet)
+    {
+        parent::__construct($spreadsheet);
+        $this->setUseInlineCss(true);
+    }
+
+    /**
+     * Gets the implementation of external PDF library that should be used.
+     *
+     * @param string $orientation Page orientation
+     * @param string $unit Unit measure
+     * @param array|string $paperSize Paper size
+     *
+     * @return \TCPDF implementation
+     */
+    protected function createExternalWriterInstance($orientation, $unit, $paperSize)
+    {
+        return new \TCPDF($orientation, $unit, $paperSize);
+    }
+
+    /**
+     * Save Spreadsheet to file.
+     *
+     * @param string $filename Name of the file to save as
+     */
+    public function save($filename, int $flags = 0): void
+    {
+        $fileHandle = parent::prepareForSave($filename);
+
+        //  Default PDF paper size
+        $paperSize = 'LETTER'; //    Letter    (8.5 in. by 11 in.)
+
+        //  Check for paper size and page orientation
+        $setup = $this->spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageSetup();
+        $orientation = $this->getOrientation() ?? $setup->getOrientation();
+        $orientation = ($orientation === PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
+        $printPaperSize = $this->getPaperSize() ?? $setup->getPaperSize();
+        $paperSize = self::$paperSizes[$printPaperSize] ?? PageSetup::getPaperSizeDefault();
+        $printMargins = $this->spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageMargins();
+
+        //  Create PDF
+        $pdf = $this->createExternalWriterInstance($orientation, 'pt', $paperSize);
+        $pdf->setFontSubsetting(false);
+        //    Set margins, converting inches to points (using 72 dpi)
+        $pdf->SetMargins($printMargins->getLeft() * 72, $printMargins->getTop() * 72, $printMargins->getRight() * 72);
+        $pdf->SetAutoPageBreak(true, $printMargins->getBottom() * 72);
+
+        $pdf->setPrintHeader(false);
+        $pdf->setPrintFooter(false);
+
+        $pdf->AddPage();
+
+        //  Set the appropriate font
+        $pdf->SetFont($this->getFont());
+        $pdf->writeHTML($this->generateHTMLAll());
+
+        //  Document info
+        $pdf->SetTitle($this->spreadsheet->getProperties()->getTitle());
+        $pdf->SetAuthor($this->spreadsheet->getProperties()->getCreator());
+        $pdf->SetSubject($this->spreadsheet->getProperties()->getSubject());
+        $pdf->SetKeywords($this->spreadsheet->getProperties()->getKeywords());
+        $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator());
+
+        //  Write to file
+        fwrite($fileHandle, $pdf->output('', 'S'));
+
+        parent::restoreStateAfterSave();
+    }
+}

+ 924 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls.php

@@ -0,0 +1,924 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer;
+
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Calculation\Functions;
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\RichText\RichText;
+use PhpOffice\PhpSpreadsheet\RichText\Run;
+use PhpOffice\PhpSpreadsheet\Shared\Escher;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE\Blip;
+use PhpOffice\PhpSpreadsheet\Shared\OLE;
+use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File;
+use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
+use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
+use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
+use PhpOffice\PhpSpreadsheet\Writer\Xls\Parser;
+use PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook;
+use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet;
+
+class Xls extends BaseWriter
+{
+    /**
+     * PhpSpreadsheet object.
+     *
+     * @var Spreadsheet
+     */
+    private $spreadsheet;
+
+    /**
+     * Total number of shared strings in workbook.
+     *
+     * @var int
+     */
+    private $strTotal = 0;
+
+    /**
+     * Number of unique shared strings in workbook.
+     *
+     * @var int
+     */
+    private $strUnique = 0;
+
+    /**
+     * Array of unique shared strings in workbook.
+     *
+     * @var array
+     */
+    private $strTable = [];
+
+    /**
+     * Color cache. Mapping between RGB value and color index.
+     *
+     * @var array
+     */
+    private $colors;
+
+    /**
+     * Formula parser.
+     *
+     * @var Parser
+     */
+    private $parser;
+
+    /**
+     * Identifier clusters for drawings. Used in MSODRAWINGGROUP record.
+     *
+     * @var array
+     */
+    private $IDCLs;
+
+    /**
+     * Basic OLE object summary information.
+     *
+     * @var string
+     */
+    private $summaryInformation;
+
+    /**
+     * Extended OLE object document summary information.
+     *
+     * @var string
+     */
+    private $documentSummaryInformation;
+
+    /**
+     * @var Workbook
+     */
+    private $writerWorkbook;
+
+    /**
+     * @var Worksheet[]
+     */
+    private $writerWorksheets;
+
+    /**
+     * Create a new Xls Writer.
+     *
+     * @param Spreadsheet $spreadsheet PhpSpreadsheet object
+     */
+    public function __construct(Spreadsheet $spreadsheet)
+    {
+        $this->spreadsheet = $spreadsheet;
+
+        $this->parser = new Xls\Parser($spreadsheet);
+    }
+
+    /**
+     * Save Spreadsheet to file.
+     *
+     * @param resource|string $filename
+     */
+    public function save($filename, int $flags = 0): void
+    {
+        $this->processFlags($flags);
+
+        // garbage collect
+        $this->spreadsheet->garbageCollect();
+
+        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
+        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
+        $saveDateReturnType = Functions::getReturnDateType();
+        Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
+
+        // initialize colors array
+        $this->colors = [];
+
+        // Initialise workbook writer
+        $this->writerWorkbook = new Xls\Workbook($this->spreadsheet, $this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser);
+
+        // Initialise worksheet writers
+        $countSheets = $this->spreadsheet->getSheetCount();
+        for ($i = 0; $i < $countSheets; ++$i) {
+            $this->writerWorksheets[$i] = new Xls\Worksheet($this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser, $this->preCalculateFormulas, $this->spreadsheet->getSheet($i));
+        }
+
+        // build Escher objects. Escher objects for workbooks needs to be build before Escher object for workbook.
+        $this->buildWorksheetEschers();
+        $this->buildWorkbookEscher();
+
+        // add 15 identical cell style Xfs
+        // for now, we use the first cellXf instead of cellStyleXf
+        $cellXfCollection = $this->spreadsheet->getCellXfCollection();
+        for ($i = 0; $i < 15; ++$i) {
+            $this->writerWorkbook->addXfWriter($cellXfCollection[0], true);
+        }
+
+        // add all the cell Xfs
+        foreach ($this->spreadsheet->getCellXfCollection() as $style) {
+            $this->writerWorkbook->addXfWriter($style, false);
+        }
+
+        // add fonts from rich text eleemnts
+        for ($i = 0; $i < $countSheets; ++$i) {
+            foreach ($this->writerWorksheets[$i]->phpSheet->getCellCollection()->getCoordinates() as $coordinate) {
+                /** @var Cell $cell */
+                $cell = $this->writerWorksheets[$i]->phpSheet->getCellCollection()->get($coordinate);
+                $cVal = $cell->getValue();
+                if ($cVal instanceof RichText) {
+                    $elements = $cVal->getRichTextElements();
+                    foreach ($elements as $element) {
+                        if ($element instanceof Run) {
+                            $font = $element->getFont();
+                            if ($font !== null) {
+                                $this->writerWorksheets[$i]->fontHashIndex[$font->getHashCode()] = $this->writerWorkbook->addFont($font);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // initialize OLE file
+        $workbookStreamName = 'Workbook';
+        $OLE = new File(OLE::ascToUcs($workbookStreamName));
+
+        // Write the worksheet streams before the global workbook stream,
+        // because the byte sizes of these are needed in the global workbook stream
+        $worksheetSizes = [];
+        for ($i = 0; $i < $countSheets; ++$i) {
+            $this->writerWorksheets[$i]->close();
+            $worksheetSizes[] = $this->writerWorksheets[$i]->_datasize;
+        }
+
+        // add binary data for global workbook stream
+        $OLE->append($this->writerWorkbook->writeWorkbook($worksheetSizes));
+
+        // add binary data for sheet streams
+        for ($i = 0; $i < $countSheets; ++$i) {
+            $OLE->append($this->writerWorksheets[$i]->getData());
+        }
+
+        $this->documentSummaryInformation = $this->writeDocumentSummaryInformation();
+        // initialize OLE Document Summary Information
+        if (!empty($this->documentSummaryInformation)) {
+            $OLE_DocumentSummaryInformation = new File(OLE::ascToUcs(chr(5) . 'DocumentSummaryInformation'));
+            $OLE_DocumentSummaryInformation->append($this->documentSummaryInformation);
+        }
+
+        $this->summaryInformation = $this->writeSummaryInformation();
+        // initialize OLE Summary Information
+        if (!empty($this->summaryInformation)) {
+            $OLE_SummaryInformation = new File(OLE::ascToUcs(chr(5) . 'SummaryInformation'));
+            $OLE_SummaryInformation->append($this->summaryInformation);
+        }
+
+        // define OLE Parts
+        $arrRootData = [$OLE];
+        // initialize OLE Properties file
+        if (isset($OLE_SummaryInformation)) {
+            $arrRootData[] = $OLE_SummaryInformation;
+        }
+        // initialize OLE Extended Properties file
+        if (isset($OLE_DocumentSummaryInformation)) {
+            $arrRootData[] = $OLE_DocumentSummaryInformation;
+        }
+
+        $time = $this->spreadsheet->getProperties()->getModified();
+        $root = new Root($time, $time, $arrRootData);
+        // save the OLE file
+        $this->openFileHandle($filename);
+        $root->save($this->fileHandle);
+        $this->maybeCloseFileHandle();
+
+        Functions::setReturnDateType($saveDateReturnType);
+        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
+    }
+
+    /**
+     * Build the Worksheet Escher objects.
+     */
+    private function buildWorksheetEschers(): void
+    {
+        // 1-based index to BstoreContainer
+        $blipIndex = 0;
+        $lastReducedSpId = 0;
+        $lastSpId = 0;
+
+        foreach ($this->spreadsheet->getAllsheets() as $sheet) {
+            // sheet index
+            $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
+
+            // check if there are any shapes for this sheet
+            $filterRange = $sheet->getAutoFilter()->getRange();
+            if (count($sheet->getDrawingCollection()) == 0 && empty($filterRange)) {
+                continue;
+            }
+
+            // create intermediate Escher object
+            $escher = new Escher();
+
+            // dgContainer
+            $dgContainer = new DgContainer();
+
+            // set the drawing index (we use sheet index + 1)
+            $dgId = $sheet->getParentOrThrow()->getIndex($sheet) + 1;
+            $dgContainer->setDgId($dgId);
+            $escher->setDgContainer($dgContainer);
+
+            // spgrContainer
+            $spgrContainer = new SpgrContainer();
+            $dgContainer->setSpgrContainer($spgrContainer);
+
+            // add one shape which is the group shape
+            $spContainer = new SpContainer();
+            $spContainer->setSpgr(true);
+            $spContainer->setSpType(0);
+            $spContainer->setSpId(($sheet->getParentOrThrow()->getIndex($sheet) + 1) << 10);
+            $spgrContainer->addChild($spContainer);
+
+            // add the shapes
+
+            $countShapes[$sheetIndex] = 0; // count number of shapes (minus group shape), in sheet
+
+            foreach ($sheet->getDrawingCollection() as $drawing) {
+                ++$blipIndex;
+
+                ++$countShapes[$sheetIndex];
+
+                // add the shape
+                $spContainer = new SpContainer();
+
+                // set the shape type
+                $spContainer->setSpType(0x004B);
+                // set the shape flag
+                $spContainer->setSpFlag(0x02);
+
+                // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index)
+                $reducedSpId = $countShapes[$sheetIndex];
+                $spId = $reducedSpId | ($sheet->getParentOrThrow()->getIndex($sheet) + 1) << 10;
+                $spContainer->setSpId($spId);
+
+                // keep track of last reducedSpId
+                $lastReducedSpId = $reducedSpId;
+
+                // keep track of last spId
+                $lastSpId = $spId;
+
+                // set the BLIP index
+                $spContainer->setOPT(0x4104, $blipIndex);
+
+                // set coordinates and offsets, client anchor
+                $coordinates = $drawing->getCoordinates();
+                $offsetX = $drawing->getOffsetX();
+                $offsetY = $drawing->getOffsetY();
+                $width = $drawing->getWidth();
+                $height = $drawing->getHeight();
+
+                $twoAnchor = \PhpOffice\PhpSpreadsheet\Shared\Xls::oneAnchor2twoAnchor($sheet, $coordinates, $offsetX, $offsetY, $width, $height);
+
+                if (is_array($twoAnchor)) {
+                    $spContainer->setStartCoordinates($twoAnchor['startCoordinates']);
+                    $spContainer->setStartOffsetX($twoAnchor['startOffsetX']);
+                    $spContainer->setStartOffsetY($twoAnchor['startOffsetY']);
+                    $spContainer->setEndCoordinates($twoAnchor['endCoordinates']);
+                    $spContainer->setEndOffsetX($twoAnchor['endOffsetX']);
+                    $spContainer->setEndOffsetY($twoAnchor['endOffsetY']);
+
+                    $spgrContainer->addChild($spContainer);
+                }
+            }
+
+            // AutoFilters
+            if (!empty($filterRange)) {
+                $rangeBounds = Coordinate::rangeBoundaries($filterRange);
+                $iNumColStart = $rangeBounds[0][0];
+                $iNumColEnd = $rangeBounds[1][0];
+
+                $iInc = $iNumColStart;
+                while ($iInc <= $iNumColEnd) {
+                    ++$countShapes[$sheetIndex];
+
+                    // create an Drawing Object for the dropdown
+                    $oDrawing = new BaseDrawing();
+                    // get the coordinates of drawing
+                    $cDrawing = Coordinate::stringFromColumnIndex($iInc) . $rangeBounds[0][1];
+                    $oDrawing->setCoordinates($cDrawing);
+                    $oDrawing->setWorksheet($sheet);
+
+                    // add the shape
+                    $spContainer = new SpContainer();
+                    // set the shape type
+                    $spContainer->setSpType(0x00C9);
+                    // set the shape flag
+                    $spContainer->setSpFlag(0x01);
+
+                    // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index)
+                    $reducedSpId = $countShapes[$sheetIndex];
+                    $spId = $reducedSpId | ($sheet->getParentOrThrow()->getIndex($sheet) + 1) << 10;
+                    $spContainer->setSpId($spId);
+
+                    // keep track of last reducedSpId
+                    $lastReducedSpId = $reducedSpId;
+
+                    // keep track of last spId
+                    $lastSpId = $spId;
+
+                    $spContainer->setOPT(0x007F, 0x01040104); // Protection -> fLockAgainstGrouping
+                    $spContainer->setOPT(0x00BF, 0x00080008); // Text -> fFitTextToShape
+                    $spContainer->setOPT(0x01BF, 0x00010000); // Fill Style -> fNoFillHitTest
+                    $spContainer->setOPT(0x01FF, 0x00080000); // Line Style -> fNoLineDrawDash
+                    $spContainer->setOPT(0x03BF, 0x000A0000); // Group Shape -> fPrint
+
+                    // set coordinates and offsets, client anchor
+                    $endCoordinates = Coordinate::stringFromColumnIndex($iInc);
+                    $endCoordinates .= $rangeBounds[0][1] + 1;
+
+                    $spContainer->setStartCoordinates($cDrawing);
+                    $spContainer->setStartOffsetX(0);
+                    $spContainer->setStartOffsetY(0);
+                    $spContainer->setEndCoordinates($endCoordinates);
+                    $spContainer->setEndOffsetX(0);
+                    $spContainer->setEndOffsetY(0);
+
+                    $spgrContainer->addChild($spContainer);
+                    ++$iInc;
+                }
+            }
+
+            // identifier clusters, used for workbook Escher object
+            $this->IDCLs[$dgId] = $lastReducedSpId;
+
+            // set last shape index
+            $dgContainer->setLastSpId($lastSpId);
+
+            // set the Escher object
+            $this->writerWorksheets[$sheetIndex]->setEscher($escher);
+        }
+    }
+
+    private function processMemoryDrawing(BstoreContainer &$bstoreContainer, MemoryDrawing $drawing, string $renderingFunctionx): void
+    {
+        switch ($renderingFunctionx) {
+            case MemoryDrawing::RENDERING_JPEG:
+                $blipType = BSE::BLIPTYPE_JPEG;
+                $renderingFunction = 'imagejpeg';
+
+                break;
+            default:
+                $blipType = BSE::BLIPTYPE_PNG;
+                $renderingFunction = 'imagepng';
+
+                break;
+        }
+
+        ob_start();
+        call_user_func($renderingFunction, $drawing->getImageResource());
+        $blipData = ob_get_contents();
+        ob_end_clean();
+
+        $blip = new Blip();
+        $blip->setData("$blipData");
+
+        $BSE = new BSE();
+        $BSE->setBlipType($blipType);
+        $BSE->setBlip($blip);
+
+        $bstoreContainer->addBSE($BSE);
+    }
+
+    private function processDrawing(BstoreContainer &$bstoreContainer, Drawing $drawing): void
+    {
+        $blipType = 0;
+        $blipData = '';
+        $filename = $drawing->getPath();
+
+        $imageSize = getimagesize($filename);
+        $imageFormat = empty($imageSize) ? 0 : ($imageSize[2] ?? 0);
+
+        switch ($imageFormat) {
+            case 1: // GIF, not supported by BIFF8, we convert to PNG
+                $blipType = BSE::BLIPTYPE_PNG;
+                $newImage = @imagecreatefromgif($filename);
+                if ($newImage === false) {
+                    throw new Exception("Unable to create image from $filename");
+                }
+                ob_start();
+                imagepng($newImage);
+                $blipData = ob_get_contents();
+                ob_end_clean();
+
+                break;
+            case 2: // JPEG
+                $blipType = BSE::BLIPTYPE_JPEG;
+                $blipData = file_get_contents($filename);
+
+                break;
+            case 3: // PNG
+                $blipType = BSE::BLIPTYPE_PNG;
+                $blipData = file_get_contents($filename);
+
+                break;
+            case 6: // Windows DIB (BMP), we convert to PNG
+                $blipType = BSE::BLIPTYPE_PNG;
+                $newImage = @imagecreatefrombmp($filename);
+                if ($newImage === false) {
+                    throw new Exception("Unable to create image from $filename");
+                }
+                ob_start();
+                imagepng($newImage);
+                $blipData = ob_get_contents();
+                ob_end_clean();
+
+                break;
+        }
+        if ($blipData) {
+            $blip = new Blip();
+            $blip->setData($blipData);
+
+            $BSE = new BSE();
+            $BSE->setBlipType($blipType);
+            $BSE->setBlip($blip);
+
+            $bstoreContainer->addBSE($BSE);
+        }
+    }
+
+    private function processBaseDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing): void
+    {
+        if ($drawing instanceof Drawing && $drawing->getPath() !== '') {
+            $this->processDrawing($bstoreContainer, $drawing);
+        } elseif ($drawing instanceof MemoryDrawing) {
+            $this->processMemoryDrawing($bstoreContainer, $drawing, $drawing->getRenderingFunction());
+        }
+    }
+
+    private function checkForDrawings(): bool
+    {
+        // any drawings in this workbook?
+        $found = false;
+        foreach ($this->spreadsheet->getAllSheets() as $sheet) {
+            if (count($sheet->getDrawingCollection()) > 0) {
+                $found = true;
+
+                break;
+            }
+        }
+
+        return $found;
+    }
+
+    /**
+     * Build the Escher object corresponding to the MSODRAWINGGROUP record.
+     */
+    private function buildWorkbookEscher(): void
+    {
+        // nothing to do if there are no drawings
+        if (!$this->checkForDrawings()) {
+            return;
+        }
+
+        // if we reach here, then there are drawings in the workbook
+        $escher = new Escher();
+
+        // dggContainer
+        $dggContainer = new DggContainer();
+        $escher->setDggContainer($dggContainer);
+
+        // set IDCLs (identifier clusters)
+        $dggContainer->setIDCLs($this->IDCLs);
+
+        // this loop is for determining maximum shape identifier of all drawing
+        $spIdMax = 0;
+        $totalCountShapes = 0;
+        $countDrawings = 0;
+
+        foreach ($this->spreadsheet->getAllsheets() as $sheet) {
+            $sheetCountShapes = 0; // count number of shapes (minus group shape), in sheet
+
+            $addCount = 0;
+            foreach ($sheet->getDrawingCollection() as $drawing) {
+                $addCount = 1;
+                ++$sheetCountShapes;
+                ++$totalCountShapes;
+
+                $spId = $sheetCountShapes | ($this->spreadsheet->getIndex($sheet) + 1) << 10;
+                $spIdMax = max($spId, $spIdMax);
+            }
+            $countDrawings += $addCount;
+        }
+
+        $dggContainer->setSpIdMax($spIdMax + 1);
+        $dggContainer->setCDgSaved($countDrawings);
+        $dggContainer->setCSpSaved($totalCountShapes + $countDrawings); // total number of shapes incl. one group shapes per drawing
+
+        // bstoreContainer
+        $bstoreContainer = new BstoreContainer();
+        $dggContainer->setBstoreContainer($bstoreContainer);
+
+        // the BSE's (all the images)
+        foreach ($this->spreadsheet->getAllsheets() as $sheet) {
+            foreach ($sheet->getDrawingCollection() as $drawing) {
+                $this->processBaseDrawing($bstoreContainer, $drawing);
+            }
+        }
+
+        // Set the Escher object
+        $this->writerWorkbook->setEscher($escher);
+    }
+
+    /**
+     * Build the OLE Part for DocumentSummary Information.
+     *
+     * @return string
+     */
+    private function writeDocumentSummaryInformation()
+    {
+        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
+        $data = pack('v', 0xFFFE);
+        // offset: 2; size: 2;
+        $data .= pack('v', 0x0000);
+        // offset: 4; size: 2; OS version
+        $data .= pack('v', 0x0106);
+        // offset: 6; size: 2; OS indicator
+        $data .= pack('v', 0x0002);
+        // offset: 8; size: 16
+        $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00);
+        // offset: 24; size: 4; section count
+        $data .= pack('V', 0x0001);
+
+        // offset: 28; size: 16; first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae
+        $data .= pack('vvvvvvvv', 0xD502, 0xD5CD, 0x2E9C, 0x101B, 0x9793, 0x0008, 0x2C2B, 0xAEF9);
+        // offset: 44; size: 4; offset of the start
+        $data .= pack('V', 0x30);
+
+        // SECTION
+        $dataSection = [];
+        $dataSection_NumProps = 0;
+        $dataSection_Summary = '';
+        $dataSection_Content = '';
+
+        // GKPIDDSI_CODEPAGE: CodePage
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x01],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer
+            'data' => ['data' => 1252],
+        ];
+        ++$dataSection_NumProps;
+
+        // GKPIDDSI_CATEGORY : Category
+        $dataProp = $this->spreadsheet->getProperties()->getCategory();
+        if ($dataProp) {
+            $dataSection[] = [
+                'summary' => ['pack' => 'V', 'data' => 0x02],
+                'offset' => ['pack' => 'V'],
+                'type' => ['pack' => 'V', 'data' => 0x1E],
+                'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
+            ];
+            ++$dataSection_NumProps;
+        }
+        // GKPIDDSI_VERSION :Version of the application that wrote the property storage
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x17],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x03],
+            'data' => ['pack' => 'V', 'data' => 0x000C0000],
+        ];
+        ++$dataSection_NumProps;
+        // GKPIDDSI_SCALE : FALSE
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x0B],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x0B],
+            'data' => ['data' => false],
+        ];
+        ++$dataSection_NumProps;
+        // GKPIDDSI_LINKSDIRTY : True if any of the values for the linked properties have changed outside of the application
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x10],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x0B],
+            'data' => ['data' => false],
+        ];
+        ++$dataSection_NumProps;
+        // GKPIDDSI_SHAREDOC : FALSE
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x13],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x0B],
+            'data' => ['data' => false],
+        ];
+        ++$dataSection_NumProps;
+        // GKPIDDSI_HYPERLINKSCHANGED : True if any of the values for the _PID_LINKS (hyperlink text) have changed outside of the application
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x16],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x0B],
+            'data' => ['data' => false],
+        ];
+        ++$dataSection_NumProps;
+
+        // GKPIDDSI_DOCSPARTS
+        // MS-OSHARED p75 (2.3.3.2.2.1)
+        // Structure is VtVecUnalignedLpstrValue (2.3.3.1.9)
+        // cElements
+        $dataProp = pack('v', 0x0001);
+        $dataProp .= pack('v', 0x0000);
+        // array of UnalignedLpstr
+        // cch
+        $dataProp .= pack('v', 0x000A);
+        $dataProp .= pack('v', 0x0000);
+        // value
+        $dataProp .= 'Worksheet' . chr(0);
+
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x0D],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x101E],
+            'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
+        ];
+        ++$dataSection_NumProps;
+
+        // GKPIDDSI_HEADINGPAIR
+        // VtVecHeadingPairValue
+        // cElements
+        $dataProp = pack('v', 0x0002);
+        $dataProp .= pack('v', 0x0000);
+        // Array of vtHeadingPair
+        // vtUnalignedString - headingString
+        // stringType
+        $dataProp .= pack('v', 0x001E);
+        // padding
+        $dataProp .= pack('v', 0x0000);
+        // UnalignedLpstr
+        // cch
+        $dataProp .= pack('v', 0x0013);
+        $dataProp .= pack('v', 0x0000);
+        // value
+        $dataProp .= 'Feuilles de calcul';
+        // vtUnalignedString - headingParts
+        // wType : 0x0003 = 32 bit signed integer
+        $dataProp .= pack('v', 0x0300);
+        // padding
+        $dataProp .= pack('v', 0x0000);
+        // value
+        $dataProp .= pack('v', 0x0100);
+        $dataProp .= pack('v', 0x0000);
+        $dataProp .= pack('v', 0x0000);
+        $dataProp .= pack('v', 0x0000);
+
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x0C],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x100C],
+            'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
+        ];
+        ++$dataSection_NumProps;
+
+        //         4     Section Length
+        //        4     Property count
+        //        8 * $dataSection_NumProps (8 =  ID (4) + OffSet(4))
+        $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8;
+        foreach ($dataSection as $dataProp) {
+            // Summary
+            $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']);
+            // Offset
+            $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset);
+            // DataType
+            $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']);
+            // Data
+            if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer
+                $dataSection_Content .= pack('V', $dataProp['data']['data']);
+
+                $dataSection_Content_Offset += 4 + 4;
+            } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer
+                $dataSection_Content .= pack('V', $dataProp['data']['data']);
+
+                $dataSection_Content_Offset += 4 + 4;
+            } elseif ($dataProp['type']['data'] == 0x0B) { // Boolean
+                $dataSection_Content .= pack('V', (int) $dataProp['data']['data']);
+                $dataSection_Content_Offset += 4 + 4;
+            } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length
+                // Null-terminated string
+                $dataProp['data']['data'] .= chr(0);
+                ++$dataProp['data']['length'];
+                // Complete the string with null string for being a %4
+                $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4));
+                $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT);
+
+                $dataSection_Content .= pack('V', $dataProp['data']['length']);
+                $dataSection_Content .= $dataProp['data']['data'];
+
+                $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']);
+                /* Condition below can never be true
+                } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
+                    $dataSection_Content .= $dataProp['data']['data'];
+
+                    $dataSection_Content_Offset += 4 + 8;
+                */
+            } else {
+                $dataSection_Content .= $dataProp['data']['data'];
+
+                $dataSection_Content_Offset += 4 + $dataProp['data']['length'];
+            }
+        }
+        // Now $dataSection_Content_Offset contains the size of the content
+
+        // section header
+        // offset: $secOffset; size: 4; section length
+        //         + x  Size of the content (summary + content)
+        $data .= pack('V', $dataSection_Content_Offset);
+        // offset: $secOffset+4; size: 4; property count
+        $data .= pack('V', $dataSection_NumProps);
+        // Section Summary
+        $data .= $dataSection_Summary;
+        // Section Content
+        $data .= $dataSection_Content;
+
+        return $data;
+    }
+
+    /**
+     * @param float|int $dataProp
+     */
+    private function writeSummaryPropOle($dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void
+    {
+        if ($dataProp) {
+            $dataSection[] = [
+                'summary' => ['pack' => 'V', 'data' => $sumdata],
+                'offset' => ['pack' => 'V'],
+                'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length
+                'data' => ['data' => OLE::localDateToOLE($dataProp)],
+            ];
+            ++$dataSection_NumProps;
+        }
+    }
+
+    private function writeSummaryProp(string $dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void
+    {
+        if ($dataProp) {
+            $dataSection[] = [
+                'summary' => ['pack' => 'V', 'data' => $sumdata],
+                'offset' => ['pack' => 'V'],
+                'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length
+                'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
+            ];
+            ++$dataSection_NumProps;
+        }
+    }
+
+    /**
+     * Build the OLE Part for Summary Information.
+     *
+     * @return string
+     */
+    private function writeSummaryInformation()
+    {
+        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
+        $data = pack('v', 0xFFFE);
+        // offset: 2; size: 2;
+        $data .= pack('v', 0x0000);
+        // offset: 4; size: 2; OS version
+        $data .= pack('v', 0x0106);
+        // offset: 6; size: 2; OS indicator
+        $data .= pack('v', 0x0002);
+        // offset: 8; size: 16
+        $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00);
+        // offset: 24; size: 4; section count
+        $data .= pack('V', 0x0001);
+
+        // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9
+        $data .= pack('vvvvvvvv', 0x85E0, 0xF29F, 0x4FF9, 0x1068, 0x91AB, 0x0008, 0x272B, 0xD9B3);
+        // offset: 44; size: 4; offset of the start
+        $data .= pack('V', 0x30);
+
+        // SECTION
+        $dataSection = [];
+        $dataSection_NumProps = 0;
+        $dataSection_Summary = '';
+        $dataSection_Content = '';
+
+        // CodePage : CP-1252
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x01],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer
+            'data' => ['data' => 1252],
+        ];
+        ++$dataSection_NumProps;
+
+        $props = $this->spreadsheet->getProperties();
+        $this->writeSummaryProp($props->getTitle(), $dataSection_NumProps, $dataSection, 0x02, 0x1e);
+        $this->writeSummaryProp($props->getSubject(), $dataSection_NumProps, $dataSection, 0x03, 0x1e);
+        $this->writeSummaryProp($props->getCreator(), $dataSection_NumProps, $dataSection, 0x04, 0x1e);
+        $this->writeSummaryProp($props->getKeywords(), $dataSection_NumProps, $dataSection, 0x05, 0x1e);
+        $this->writeSummaryProp($props->getDescription(), $dataSection_NumProps, $dataSection, 0x06, 0x1e);
+        $this->writeSummaryProp($props->getLastModifiedBy(), $dataSection_NumProps, $dataSection, 0x08, 0x1e);
+        $this->writeSummaryPropOle($props->getCreated(), $dataSection_NumProps, $dataSection, 0x0c, 0x40);
+        $this->writeSummaryPropOle($props->getModified(), $dataSection_NumProps, $dataSection, 0x0d, 0x40);
+
+        //    Security
+        $dataSection[] = [
+            'summary' => ['pack' => 'V', 'data' => 0x13],
+            'offset' => ['pack' => 'V'],
+            'type' => ['pack' => 'V', 'data' => 0x03], // 4 byte signed integer
+            'data' => ['data' => 0x00],
+        ];
+        ++$dataSection_NumProps;
+
+        //         4     Section Length
+        //        4     Property count
+        //        8 * $dataSection_NumProps (8 =  ID (4) + OffSet(4))
+        $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8;
+        foreach ($dataSection as $dataProp) {
+            // Summary
+            $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']);
+            // Offset
+            $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset);
+            // DataType
+            $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']);
+            // Data
+            if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer
+                $dataSection_Content .= pack('V', $dataProp['data']['data']);
+
+                $dataSection_Content_Offset += 4 + 4;
+            } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer
+                $dataSection_Content .= pack('V', $dataProp['data']['data']);
+
+                $dataSection_Content_Offset += 4 + 4;
+            } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length
+                // Null-terminated string
+                $dataProp['data']['data'] .= chr(0);
+                ++$dataProp['data']['length'];
+                // Complete the string with null string for being a %4
+                $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4));
+                $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT);
+
+                $dataSection_Content .= pack('V', $dataProp['data']['length']);
+                $dataSection_Content .= $dataProp['data']['data'];
+
+                $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']);
+            } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
+                $dataSection_Content .= $dataProp['data']['data'];
+
+                $dataSection_Content_Offset += 4 + 8;
+            }
+            // Data Type Not Used at the moment
+        }
+        // Now $dataSection_Content_Offset contains the size of the content
+
+        // section header
+        // offset: $secOffset; size: 4; section length
+        //         + x  Size of the content (summary + content)
+        $data .= pack('V', $dataSection_Content_Offset);
+        // offset: $secOffset+4; size: 4; property count
+        $data .= pack('V', $dataSection_NumProps);
+        // Section Summary
+        $data .= $dataSection_Summary;
+        // Section Content
+        $data .= $dataSection_Content;
+
+        return $data;
+    }
+}

+ 224 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/BIFFwriter.php

@@ -0,0 +1,224 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+
+// Original file header of PEAR::Spreadsheet_Excel_Writer_BIFFwriter (used as the base for this class):
+// -----------------------------------------------------------------------------------------
+// *  Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
+// *
+// *  The majority of this is _NOT_ my code.  I simply ported it from the
+// *  PERL Spreadsheet::WriteExcel module.
+// *
+// *  The author of the Spreadsheet::WriteExcel module is John McNamara
+// *  <jmcnamara@cpan.org>
+// *
+// *  I _DO_ maintain this code, and John McNamara has nothing to do with the
+// *  porting of this code to PHP.  Any questions directly related to this
+// *  class library should be directed to me.
+// *
+// *  License Information:
+// *
+// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
+// *    Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+// *
+// *    This library is free software; you can redistribute it and/or
+// *    modify it under the terms of the GNU Lesser General Public
+// *    License as published by the Free Software Foundation; either
+// *    version 2.1 of the License, or (at your option) any later version.
+// *
+// *    This library is distributed in the hope that it will be useful,
+// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// *    Lesser General Public License for more details.
+// *
+// *    You should have received a copy of the GNU Lesser General Public
+// *    License along with this library; if not, write to the Free Software
+// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+// */
+class BIFFwriter
+{
+    /**
+     * The byte order of this architecture. 0 => little endian, 1 => big endian.
+     *
+     * @var ?int
+     */
+    private static $byteOrder;
+
+    /**
+     * The string containing the data of the BIFF stream.
+     *
+     * @var null|string
+     */
+    public $_data;
+
+    /**
+     * The size of the data in bytes. Should be the same as strlen($this->_data).
+     *
+     * @var int
+     */
+    public $_datasize;
+
+    /**
+     * The maximum length for a BIFF record (excluding record header and length field). See addContinue().
+     *
+     * @var int
+     *
+     * @see addContinue()
+     */
+    private $limit = 8224;
+
+    /**
+     * Constructor.
+     */
+    public function __construct()
+    {
+        $this->_data = '';
+        $this->_datasize = 0;
+    }
+
+    /**
+     * Determine the byte order and store it as class data to avoid
+     * recalculating it for each call to new().
+     *
+     * @return int
+     */
+    public static function getByteOrder()
+    {
+        if (!isset(self::$byteOrder)) {
+            // Check if "pack" gives the required IEEE 64bit float
+            $teststr = pack('d', 1.2345);
+            $number = pack('C8', 0x8D, 0x97, 0x6E, 0x12, 0x83, 0xC0, 0xF3, 0x3F);
+            if ($number == $teststr) {
+                $byte_order = 0; // Little Endian
+            } elseif ($number == strrev($teststr)) {
+                $byte_order = 1; // Big Endian
+            } else {
+                // Give up. I'll fix this in a later version.
+                throw new WriterException('Required floating point format not supported on this platform.');
+            }
+            self::$byteOrder = $byte_order;
+        }
+
+        return self::$byteOrder;
+    }
+
+    /**
+     * General storage function.
+     *
+     * @param string $data binary data to append
+     */
+    protected function append($data): void
+    {
+        if (strlen($data) - 4 > $this->limit) {
+            $data = $this->addContinue($data);
+        }
+        $this->_data .= $data;
+        $this->_datasize += strlen($data);
+    }
+
+    /**
+     * General storage function like append, but returns string instead of modifying $this->_data.
+     *
+     * @param string $data binary data to write
+     *
+     * @return string
+     */
+    public function writeData($data)
+    {
+        if (strlen($data) - 4 > $this->limit) {
+            $data = $this->addContinue($data);
+        }
+        $this->_datasize += strlen($data);
+
+        return $data;
+    }
+
+    /**
+     * Writes Excel BOF record to indicate the beginning of a stream or
+     * sub-stream in the BIFF file.
+     *
+     * @param int $type type of BIFF file to write: 0x0005 Workbook,
+     *                       0x0010 Worksheet
+     */
+    protected function storeBof($type): void
+    {
+        $record = 0x0809; // Record identifier    (BIFF5-BIFF8)
+        $length = 0x0010;
+
+        // by inspection of real files, MS Office Excel 2007 writes the following
+        $unknown = pack('VV', 0x000100D1, 0x00000406);
+
+        $build = 0x0DBB; //    Excel 97
+        $year = 0x07CC; //    Excel 97
+
+        $version = 0x0600; //    BIFF8
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvv', $version, $type, $build, $year);
+        $this->append($header . $data . $unknown);
+    }
+
+    /**
+     * Writes Excel EOF record to indicate the end of a BIFF stream.
+     */
+    protected function storeEof(): void
+    {
+        $record = 0x000A; // Record identifier
+        $length = 0x0000; // Number of bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $this->append($header);
+    }
+
+    /**
+     * Writes Excel EOF record to indicate the end of a BIFF stream.
+     */
+    public function writeEof(): string
+    {
+        $record = 0x000A; // Record identifier
+        $length = 0x0000; // Number of bytes to follow
+        $header = pack('vv', $record, $length);
+
+        return $this->writeData($header);
+    }
+
+    /**
+     * Excel limits the size of BIFF records. In Excel 5 the limit is 2084 bytes. In
+     * Excel 97 the limit is 8228 bytes. Records that are longer than these limits
+     * must be split up into CONTINUE blocks.
+     *
+     * This function takes a long BIFF record and inserts CONTINUE records as
+     * necessary.
+     *
+     * @param string $data The original binary data to be written
+     *
+     * @return string A very convenient string of continue blocks
+     */
+    private function addContinue($data)
+    {
+        $limit = $this->limit;
+        $record = 0x003C; // Record identifier
+
+        // The first 2080/8224 bytes remain intact. However, we have to change
+        // the length field of the record.
+        $tmp = substr($data, 0, 2) . pack('v', $limit) . substr($data, 4, $limit);
+
+        $header = pack('vv', $record, $limit); // Headers for continue records
+
+        // Retrieve chunks of 2080/8224 bytes +4 for the header.
+        $data_length = strlen($data);
+        for ($i = $limit + 4; $i < ($data_length - $limit); $i += $limit) {
+            $tmp .= $header;
+            $tmp .= substr($data, $i, $limit);
+        }
+
+        // Retrieve the last chunk of data
+        $header = pack('vv', $record, strlen($data) - $i);
+        $tmp .= $header;
+        $tmp .= substr($data, $i);
+
+        return $tmp;
+    }
+}

+ 78 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/CellDataValidation.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
+
+class CellDataValidation
+{
+    /**
+     * @var array<string, int>
+     */
+    protected static $validationTypeMap = [
+        DataValidation::TYPE_NONE => 0x00,
+        DataValidation::TYPE_WHOLE => 0x01,
+        DataValidation::TYPE_DECIMAL => 0x02,
+        DataValidation::TYPE_LIST => 0x03,
+        DataValidation::TYPE_DATE => 0x04,
+        DataValidation::TYPE_TIME => 0x05,
+        DataValidation::TYPE_TEXTLENGTH => 0x06,
+        DataValidation::TYPE_CUSTOM => 0x07,
+    ];
+
+    /**
+     * @var array<string, int>
+     */
+    protected static $errorStyleMap = [
+        DataValidation::STYLE_STOP => 0x00,
+        DataValidation::STYLE_WARNING => 0x01,
+        DataValidation::STYLE_INFORMATION => 0x02,
+    ];
+
+    /**
+     * @var array<string, int>
+     */
+    protected static $operatorMap = [
+        DataValidation::OPERATOR_BETWEEN => 0x00,
+        DataValidation::OPERATOR_NOTBETWEEN => 0x01,
+        DataValidation::OPERATOR_EQUAL => 0x02,
+        DataValidation::OPERATOR_NOTEQUAL => 0x03,
+        DataValidation::OPERATOR_GREATERTHAN => 0x04,
+        DataValidation::OPERATOR_LESSTHAN => 0x05,
+        DataValidation::OPERATOR_GREATERTHANOREQUAL => 0x06,
+        DataValidation::OPERATOR_LESSTHANOREQUAL => 0x07,
+    ];
+
+    public static function type(DataValidation $dataValidation): int
+    {
+        $validationType = $dataValidation->getType();
+
+        if (is_string($validationType) && array_key_exists($validationType, self::$validationTypeMap)) {
+            return self::$validationTypeMap[$validationType];
+        }
+
+        return self::$validationTypeMap[DataValidation::TYPE_NONE];
+    }
+
+    public static function errorStyle(DataValidation $dataValidation): int
+    {
+        $errorStyle = $dataValidation->getErrorStyle();
+
+        if (is_string($errorStyle) && array_key_exists($errorStyle, self::$errorStyleMap)) {
+            return self::$errorStyleMap[$errorStyle];
+        }
+
+        return self::$errorStyleMap[DataValidation::STYLE_STOP];
+    }
+
+    public static function operator(DataValidation $dataValidation): int
+    {
+        $operator = $dataValidation->getOperator();
+
+        if (is_string($operator) && array_key_exists($operator, self::$operatorMap)) {
+            return self::$operatorMap[$operator];
+        }
+
+        return self::$operatorMap[DataValidation::OPERATOR_BETWEEN];
+    }
+}

+ 76 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
+
+class ConditionalHelper
+{
+    /**
+     * Formula parser.
+     *
+     * @var Parser
+     */
+    protected $parser;
+
+    /**
+     * @var mixed
+     */
+    protected $condition;
+
+    /**
+     * @var string
+     */
+    protected $cellRange;
+
+    /**
+     * @var null|string
+     */
+    protected $tokens;
+
+    /**
+     * @var int
+     */
+    protected $size;
+
+    public function __construct(Parser $parser)
+    {
+        $this->parser = $parser;
+    }
+
+    /**
+     * @param mixed $condition
+     */
+    public function processCondition($condition, string $cellRange): void
+    {
+        $this->condition = $condition;
+        $this->cellRange = $cellRange;
+
+        if (is_int($condition) || is_float($condition)) {
+            $this->size = ($condition <= 65535 ? 3 : 0x0000);
+            $this->tokens = pack('Cv', 0x1E, $condition);
+        } else {
+            try {
+                $formula = Wizard\WizardAbstract::reverseAdjustCellRef((string) $condition, $cellRange);
+                $this->parser->parse($formula);
+                $this->tokens = $this->parser->toReversePolish();
+                $this->size = strlen($this->tokens ?? '');
+            } catch (PhpSpreadsheetException $e) {
+                // In the event of a parser error with a formula value, we set the expression to ptgInt + 0
+                $this->tokens = pack('Cv', 0x1E, 0);
+                $this->size = 3;
+            }
+        }
+    }
+
+    public function tokens(): ?string
+    {
+        return $this->tokens;
+    }
+
+    public function size(): int
+    {
+        return $this->size;
+    }
+}

+ 28 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/ErrorCode.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+class ErrorCode
+{
+    /**
+     * @var array<string, int>
+     */
+    protected static $errorCodeMap = [
+        '#NULL!' => 0x00,
+        '#DIV/0!' => 0x07,
+        '#VALUE!' => 0x0F,
+        '#REF!' => 0x17,
+        '#NAME?' => 0x1D,
+        '#NUM!' => 0x24,
+        '#N/A' => 0x2A,
+    ];
+
+    public static function error(string $errorCode): int
+    {
+        if (array_key_exists($errorCode, self::$errorCodeMap)) {
+            return self::$errorCodeMap[$errorCode];
+        }
+
+        return 0;
+    }
+}

+ 524 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Escher.php

@@ -0,0 +1,524 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Shared\Escher as SharedEscher;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
+use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE\Blip;
+
+class Escher
+{
+    /**
+     * The object we are writing.
+     *
+     * @var Blip|BSE|BstoreContainer|DgContainer|DggContainer|Escher|SpContainer|SpgrContainer
+     */
+    private $object;
+
+    /**
+     * The written binary data.
+     *
+     * @var string
+     */
+    private $data;
+
+    /**
+     * Shape offsets. Positions in binary stream where a new shape record begins.
+     *
+     * @var array
+     */
+    private $spOffsets;
+
+    /**
+     * Shape types.
+     *
+     * @var array
+     */
+    private $spTypes;
+
+    /**
+     * Constructor.
+     *
+     * @param mixed $object
+     */
+    public function __construct($object)
+    {
+        $this->object = $object;
+    }
+
+    /**
+     * Process the object to be written.
+     *
+     * @return string
+     */
+    public function close()
+    {
+        // initialize
+        $this->data = '';
+
+        switch (get_class($this->object)) {
+            case SharedEscher::class:
+                if ($dggContainer = $this->object->/** @scrutinizer ignore-call */ getDggContainer()) {
+                    $writer = new self($dggContainer);
+                    $this->data = $writer->close();
+                } elseif ($dgContainer = $this->object->/** @scrutinizer ignore-call */ getDgContainer()) {
+                    $writer = new self($dgContainer);
+                    $this->data = $writer->close();
+                    $this->spOffsets = $writer->getSpOffsets();
+                    $this->spTypes = $writer->getSpTypes();
+                }
+
+                break;
+            case DggContainer::class:
+                // this is a container record
+
+                // initialize
+                $innerData = '';
+
+                // write the dgg
+                $recVer = 0x0;
+                $recInstance = 0x0000;
+                $recType = 0xF006;
+
+                $recVerInstance = $recVer;
+                $recVerInstance |= $recInstance << 4;
+
+                // dgg data
+                $dggData =
+                    pack(
+                        'VVVV',
+                        $this->object->/** @scrutinizer ignore-call */ getSpIdMax(), // maximum shape identifier increased by one
+                        $this->object->/** @scrutinizer ignore-call */ getCDgSaved() + 1, // number of file identifier clusters increased by one
+                        $this->object->/** @scrutinizer ignore-call */ getCSpSaved(),
+                        $this->object->/** @scrutinizer ignore-call */ getCDgSaved() // count total number of drawings saved
+                    );
+
+                // add file identifier clusters (one per drawing)
+                /** @scrutinizer ignore-call */
+                $IDCLs = $this->object->getIDCLs();
+
+                foreach ($IDCLs as $dgId => $maxReducedSpId) {
+                    $dggData .= pack('VV', $dgId, $maxReducedSpId + 1);
+                }
+
+                $header = pack('vvV', $recVerInstance, $recType, strlen($dggData));
+                $innerData .= $header . $dggData;
+
+                // write the bstoreContainer
+                if ($bstoreContainer = $this->object->/** @scrutinizer ignore-call */ getBstoreContainer()) {
+                    $writer = new self($bstoreContainer);
+                    $innerData .= $writer->close();
+                }
+
+                // write the record
+                $recVer = 0xF;
+                $recInstance = 0x0000;
+                $recType = 0xF000;
+                $length = strlen($innerData);
+
+                $recVerInstance = $recVer;
+                $recVerInstance |= $recInstance << 4;
+
+                $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                $this->data = $header . $innerData;
+
+                break;
+            case BstoreContainer::class:
+                // this is a container record
+
+                // initialize
+                $innerData = '';
+
+                // treat the inner data
+                if ($BSECollection = $this->object->/** @scrutinizer ignore-call */ getBSECollection()) {
+                    foreach ($BSECollection as $BSE) {
+                        $writer = new self($BSE);
+                        $innerData .= $writer->close();
+                    }
+                }
+
+                // write the record
+                $recVer = 0xF;
+                $recInstance = count($this->object->/** @scrutinizer ignore-call */ getBSECollection());
+                $recType = 0xF001;
+                $length = strlen($innerData);
+
+                $recVerInstance = $recVer;
+                $recVerInstance |= $recInstance << 4;
+
+                $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                $this->data = $header . $innerData;
+
+                break;
+            case BSE::class:
+                // this is a semi-container record
+
+                // initialize
+                $innerData = '';
+
+                // here we treat the inner data
+                if ($blip = $this->object->/** @scrutinizer ignore-call */ getBlip()) {
+                    $writer = new self($blip);
+                    $innerData .= $writer->close();
+                }
+
+                // initialize
+                $data = '';
+
+                /** @scrutinizer ignore-call */
+                $btWin32 = $this->object->getBlipType();
+                /** @scrutinizer ignore-call */
+                $btMacOS = $this->object->getBlipType();
+                $data .= pack('CC', $btWin32, $btMacOS);
+
+                $rgbUid = pack('VVVV', 0, 0, 0, 0); // todo
+                $data .= $rgbUid;
+
+                $tag = 0;
+                $size = strlen($innerData);
+                $cRef = 1;
+                $foDelay = 0; //todo
+                $unused1 = 0x0;
+                $cbName = 0x0;
+                $unused2 = 0x0;
+                $unused3 = 0x0;
+                $data .= pack('vVVVCCCC', $tag, $size, $cRef, $foDelay, $unused1, $cbName, $unused2, $unused3);
+
+                $data .= $innerData;
+
+                // write the record
+                $recVer = 0x2;
+                /** @scrutinizer ignore-call */
+                $recInstance = $this->object->getBlipType();
+                $recType = 0xF007;
+                $length = strlen($data);
+
+                $recVerInstance = $recVer;
+                $recVerInstance |= $recInstance << 4;
+
+                $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                $this->data = $header;
+
+                $this->data .= $data;
+
+                break;
+            case Blip::class:
+                // this is an atom record
+
+                // write the record
+                switch ($this->object->/** @scrutinizer ignore-call */ getParent()->/** @scrutinizer ignore-call */ getBlipType()) {
+                    case BSE::BLIPTYPE_JPEG:
+                        // initialize
+                        $innerData = '';
+
+                        $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo
+                        $innerData .= $rgbUid1;
+
+                        $tag = 0xFF; // todo
+                        $innerData .= pack('C', $tag);
+
+                        $innerData .= $this->object->/** @scrutinizer ignore-call */ getData();
+
+                        $recVer = 0x0;
+                        $recInstance = 0x46A;
+                        $recType = 0xF01D;
+                        $length = strlen($innerData);
+
+                        $recVerInstance = $recVer;
+                        $recVerInstance |= $recInstance << 4;
+
+                        $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                        $this->data = $header;
+
+                        $this->data .= $innerData;
+
+                        break;
+                    case BSE::BLIPTYPE_PNG:
+                        // initialize
+                        $innerData = '';
+
+                        $rgbUid1 = pack('VVVV', 0, 0, 0, 0); // todo
+                        $innerData .= $rgbUid1;
+
+                        $tag = 0xFF; // todo
+                        $innerData .= pack('C', $tag);
+
+                        $innerData .= $this->object->/** @scrutinizer ignore-call */ getData();
+
+                        $recVer = 0x0;
+                        $recInstance = 0x6E0;
+                        $recType = 0xF01E;
+                        $length = strlen($innerData);
+
+                        $recVerInstance = $recVer;
+                        $recVerInstance |= $recInstance << 4;
+
+                        $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                        $this->data = $header;
+
+                        $this->data .= $innerData;
+
+                        break;
+                }
+
+                break;
+            case DgContainer::class:
+                // this is a container record
+
+                // initialize
+                $innerData = '';
+
+                // write the dg
+                $recVer = 0x0;
+                /** @scrutinizer ignore-call */
+                $recInstance = $this->object->getDgId();
+                $recType = 0xF008;
+                $length = 8;
+
+                $recVerInstance = $recVer;
+                $recVerInstance |= $recInstance << 4;
+
+                $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                // number of shapes in this drawing (including group shape)
+                $countShapes = count($this->object->/** @scrutinizer ignore-call */ getSpgrContainerOrThrow()->getChildren());
+                $innerData .= $header . pack('VV', $countShapes, $this->object->/** @scrutinizer ignore-call */ getLastSpId());
+
+                // write the spgrContainer
+                if ($spgrContainer = $this->object->/** @scrutinizer ignore-call */ getSpgrContainer()) {
+                    $writer = new self($spgrContainer);
+                    $innerData .= $writer->close();
+
+                    // get the shape offsets relative to the spgrContainer record
+                    $spOffsets = $writer->getSpOffsets();
+                    $spTypes = $writer->getSpTypes();
+
+                    // save the shape offsets relative to dgContainer
+                    foreach ($spOffsets as &$spOffset) {
+                        $spOffset += 24; // add length of dgContainer header data (8 bytes) plus dg data (16 bytes)
+                    }
+
+                    $this->spOffsets = $spOffsets;
+                    $this->spTypes = $spTypes;
+                }
+
+                // write the record
+                $recVer = 0xF;
+                $recInstance = 0x0000;
+                $recType = 0xF002;
+                $length = strlen($innerData);
+
+                $recVerInstance = $recVer;
+                $recVerInstance |= $recInstance << 4;
+
+                $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                $this->data = $header . $innerData;
+
+                break;
+            case SpgrContainer::class:
+                // this is a container record
+
+                // initialize
+                $innerData = '';
+
+                // initialize spape offsets
+                $totalSize = 8;
+                $spOffsets = [];
+                $spTypes = [];
+
+                // treat the inner data
+                foreach ($this->object->/** @scrutinizer ignore-call */ getChildren() as $spContainer) {
+                    $writer = new self($spContainer);
+                    $spData = $writer->close();
+                    $innerData .= $spData;
+
+                    // save the shape offsets (where new shape records begin)
+                    $totalSize += strlen($spData);
+                    $spOffsets[] = $totalSize;
+
+                    $spTypes = array_merge($spTypes, $writer->getSpTypes());
+                }
+
+                // write the record
+                $recVer = 0xF;
+                $recInstance = 0x0000;
+                $recType = 0xF003;
+                $length = strlen($innerData);
+
+                $recVerInstance = $recVer;
+                $recVerInstance |= $recInstance << 4;
+
+                $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                $this->data = $header . $innerData;
+                $this->spOffsets = $spOffsets;
+                $this->spTypes = $spTypes;
+
+                break;
+            case SpContainer::class:
+                // initialize
+                $data = '';
+
+                // build the data
+
+                // write group shape record, if necessary?
+                if ($this->object->/** @scrutinizer ignore-call */ getSpgr()) {
+                    $recVer = 0x1;
+                    $recInstance = 0x0000;
+                    $recType = 0xF009;
+                    $length = 0x00000010;
+
+                    $recVerInstance = $recVer;
+                    $recVerInstance |= $recInstance << 4;
+
+                    $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                    $data .= $header . pack('VVVV', 0, 0, 0, 0);
+                }
+                /** @scrutinizer ignore-call */
+                $this->spTypes[] = ($this->object->getSpType());
+
+                // write the shape record
+                $recVer = 0x2;
+                /** @scrutinizer ignore-call */
+                $recInstance = $this->object->getSpType(); // shape type
+                $recType = 0xF00A;
+                $length = 0x00000008;
+
+                $recVerInstance = $recVer;
+                $recVerInstance |= $recInstance << 4;
+
+                $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                $data .= $header . pack('VV', $this->object->/** @scrutinizer ignore-call */ getSpId(), $this->object->/** @scrutinizer ignore-call */ getSpgr() ? 0x0005 : 0x0A00);
+
+                // the options
+                if ($this->object->/** @scrutinizer ignore-call */ getOPTCollection()) {
+                    $optData = '';
+
+                    $recVer = 0x3;
+                    $recInstance = count($this->object->/** @scrutinizer ignore-call */ getOPTCollection());
+                    $recType = 0xF00B;
+                    foreach ($this->object->/** @scrutinizer ignore-call */ getOPTCollection() as $property => $value) {
+                        $optData .= pack('vV', $property, $value);
+                    }
+                    $length = strlen($optData);
+
+                    $recVerInstance = $recVer;
+                    $recVerInstance |= $recInstance << 4;
+
+                    $header = pack('vvV', $recVerInstance, $recType, $length);
+                    $data .= $header . $optData;
+                }
+
+                // the client anchor
+                if ($this->object->/** @scrutinizer ignore-call */ getStartCoordinates()) {
+                    $recVer = 0x0;
+                    $recInstance = 0x0;
+                    $recType = 0xF010;
+
+                    // start coordinates
+                    [$column, $row] = Coordinate::indexesFromString($this->object->/** @scrutinizer ignore-call */ getStartCoordinates());
+                    $c1 = $column - 1;
+                    $r1 = $row - 1;
+
+                    // start offsetX
+                    /** @scrutinizer ignore-call */
+                    $startOffsetX = $this->object->getStartOffsetX();
+
+                    // start offsetY
+                    /** @scrutinizer ignore-call */
+                    $startOffsetY = $this->object->getStartOffsetY();
+
+                    // end coordinates
+                    [$column, $row] = Coordinate::indexesFromString($this->object->/** @scrutinizer ignore-call */ getEndCoordinates());
+                    $c2 = $column - 1;
+                    $r2 = $row - 1;
+
+                    // end offsetX
+                    /** @scrutinizer ignore-call */
+                    $endOffsetX = $this->object->getEndOffsetX();
+
+                    // end offsetY
+                    /** @scrutinizer ignore-call */
+                    $endOffsetY = $this->object->getEndOffsetY();
+
+                    $clientAnchorData = pack('vvvvvvvvv', $this->object->/** @scrutinizer ignore-call */ getSpFlag(), $c1, $startOffsetX, $r1, $startOffsetY, $c2, $endOffsetX, $r2, $endOffsetY);
+
+                    $length = strlen($clientAnchorData);
+
+                    $recVerInstance = $recVer;
+                    $recVerInstance |= $recInstance << 4;
+
+                    $header = pack('vvV', $recVerInstance, $recType, $length);
+                    $data .= $header . $clientAnchorData;
+                }
+
+                // the client data, just empty for now
+                if (!$this->object->/** @scrutinizer ignore-call */ getSpgr()) {
+                    $clientDataData = '';
+
+                    $recVer = 0x0;
+                    $recInstance = 0x0;
+                    $recType = 0xF011;
+
+                    $length = strlen($clientDataData);
+
+                    $recVerInstance = $recVer;
+                    $recVerInstance |= $recInstance << 4;
+
+                    $header = pack('vvV', $recVerInstance, $recType, $length);
+                    $data .= $header . $clientDataData;
+                }
+
+                // write the record
+                $recVer = 0xF;
+                $recInstance = 0x0000;
+                $recType = 0xF004;
+                $length = strlen($data);
+
+                $recVerInstance = $recVer;
+                $recVerInstance |= $recInstance << 4;
+
+                $header = pack('vvV', $recVerInstance, $recType, $length);
+
+                $this->data = $header . $data;
+
+                break;
+        }
+
+        return $this->data;
+    }
+
+    /**
+     * Gets the shape offsets.
+     *
+     * @return array
+     */
+    public function getSpOffsets()
+    {
+        return $this->spOffsets;
+    }
+
+    /**
+     * Gets the shape types.
+     *
+     * @return array
+     */
+    public function getSpTypes()
+    {
+        return $this->spTypes;
+    }
+}

+ 146 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Font.php

@@ -0,0 +1,146 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+
+class Font
+{
+    /**
+     * Color index.
+     *
+     * @var int
+     */
+    private $colorIndex;
+
+    /**
+     * Font.
+     *
+     * @var \PhpOffice\PhpSpreadsheet\Style\Font
+     */
+    private $font;
+
+    /**
+     * Constructor.
+     */
+    public function __construct(\PhpOffice\PhpSpreadsheet\Style\Font $font)
+    {
+        $this->colorIndex = 0x7FFF;
+        $this->font = $font;
+    }
+
+    /**
+     * Set the color index.
+     *
+     * @param int $colorIndex
+     */
+    public function setColorIndex($colorIndex): void
+    {
+        $this->colorIndex = $colorIndex;
+    }
+
+    /** @var int */
+    private static $notImplemented = 0;
+
+    /**
+     * Get font record data.
+     *
+     * @return string
+     */
+    public function writeFont()
+    {
+        $font_outline = self::$notImplemented;
+        $font_shadow = self::$notImplemented;
+
+        $icv = $this->colorIndex; // Index to color palette
+        if ($this->font->getSuperscript()) {
+            $sss = 1;
+        } elseif ($this->font->getSubscript()) {
+            $sss = 2;
+        } else {
+            $sss = 0;
+        }
+        $bFamily = 0; // Font family
+        $bCharSet = \PhpOffice\PhpSpreadsheet\Shared\Font::getCharsetFromFontName((string) $this->font->getName()); // Character set
+
+        $record = 0x31; // Record identifier
+        $reserved = 0x00; // Reserved
+        $grbit = 0x00; // Font attributes
+        if ($this->font->getItalic()) {
+            $grbit |= 0x02;
+        }
+        if ($this->font->getStrikethrough()) {
+            $grbit |= 0x08;
+        }
+        if ($font_outline) {
+            $grbit |= 0x10;
+        }
+        if ($font_shadow) {
+            $grbit |= 0x20;
+        }
+
+        $data = pack(
+            'vvvvvCCCC',
+            // Fontsize (in twips)
+            $this->font->getSize() * 20,
+            $grbit,
+            // Colour
+            $icv,
+            // Font weight
+            self::mapBold($this->font->getBold()),
+            // Superscript/Subscript
+            $sss,
+            self::mapUnderline((string) $this->font->getUnderline()),
+            $bFamily,
+            $bCharSet,
+            $reserved
+        );
+        $data .= StringHelper::UTF8toBIFF8UnicodeShort((string) $this->font->getName());
+
+        $length = strlen($data);
+        $header = pack('vv', $record, $length);
+
+        return $header . $data;
+    }
+
+    /**
+     * Map to BIFF5-BIFF8 codes for bold.
+     */
+    private static function mapBold(?bool $bold): int
+    {
+        if ($bold === true) {
+            return 0x2BC; //  700 = Bold font weight
+        }
+
+        return 0x190; //  400 = Normal font weight
+    }
+
+    /**
+     * Map of BIFF2-BIFF8 codes for underline styles.
+     *
+     * @var int[]
+     */
+    private static $mapUnderline = [
+        \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE => 0x00,
+        \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE => 0x01,
+        \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE => 0x02,
+        \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING => 0x21,
+        \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING => 0x22,
+    ];
+
+    /**
+     * Map underline.
+     *
+     * @param string $underline
+     *
+     * @return int
+     */
+    private static function mapUnderline($underline)
+    {
+        if (isset(self::$mapUnderline[$underline])) {
+            return self::$mapUnderline[$underline];
+        }
+
+        return 0x00;
+    }
+}

+ 1664 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php

@@ -0,0 +1,1664 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use Composer\Pcre\Preg;
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as PhpspreadsheetWorksheet;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+
+// Original file header of PEAR::Spreadsheet_Excel_Writer_Parser (used as the base for this class):
+// -----------------------------------------------------------------------------------------
+// *  Class for parsing Excel formulas
+// *
+// *  License Information:
+// *
+// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
+// *    Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+// *
+// *    This library is free software; you can redistribute it and/or
+// *    modify it under the terms of the GNU Lesser General Public
+// *    License as published by the Free Software Foundation; either
+// *    version 2.1 of the License, or (at your option) any later version.
+// *
+// *    This library is distributed in the hope that it will be useful,
+// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// *    Lesser General Public License for more details.
+// *
+// *    You should have received a copy of the GNU Lesser General Public
+// *    License along with this library; if not, write to the Free Software
+// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+// */
+class Parser
+{
+    /**    Constants                */
+    // Sheet title in unquoted form
+    // Invalid sheet title characters cannot occur in the sheet title:
+    //         *:/\?[]
+    // Moreover, there are valid sheet title characters that cannot occur in unquoted form (there may be more?)
+    // +-% '^&<>=,;#()"{}
+    const REGEX_SHEET_TITLE_UNQUOTED = '[^\*\:\/\\\\\?\[\]\+\-\% \\\'\^\&\<\>\=\,\;\#\(\)\"\{\}]+';
+
+    // Sheet title in quoted form (without surrounding quotes)
+    // Invalid sheet title characters cannot occur in the sheet title:
+    // *:/\?[]                    (usual invalid sheet title characters)
+    // Single quote is represented as a pair ''
+    // Former value for this constant led to "catastrophic backtracking",
+    //     unable to handle double apostrophes.
+    //     (*COMMIT) should prevent this.
+    const REGEX_SHEET_TITLE_QUOTED = "([^*:/\\\\?\[\]']|'')+";
+
+    const REGEX_CELL_TITLE_QUOTED = "~^'"
+        . self::REGEX_SHEET_TITLE_QUOTED
+        . '(:' . self::REGEX_SHEET_TITLE_QUOTED . ')?'
+        . "'!(*COMMIT)"
+        . '[$]?[A-Ia-i]?[A-Za-z][$]?(\\d+)'
+        . '$~u';
+
+    const REGEX_RANGE_TITLE_QUOTED = "~^'"
+        . self::REGEX_SHEET_TITLE_QUOTED
+        . '(:' . self::REGEX_SHEET_TITLE_QUOTED . ')?'
+        . "'!(*COMMIT)"
+        . '[$]?[A-Ia-i]?[A-Za-z][$]?(\\d+)'
+        . ':'
+        . '[$]?[A-Ia-i]?[A-Za-z][$]?(\\d+)'
+        . '$~u';
+
+    private const UTF8 = 'UTF-8';
+
+    /**
+     * The index of the character we are currently looking at.
+     */
+    public int $currentCharacter;
+
+    /**
+     * The token we are working on.
+     */
+    public string $currentToken;
+
+    /**
+     * The formula to parse.
+     */
+    private string $formula;
+
+    /**
+     * The character ahead of the current char.
+     */
+    public string $lookAhead;
+
+    /**
+     * The parse tree to be generated.
+     *
+     * @var array|string
+     */
+    public $parseTree;
+
+    /**
+     * Array of external sheets.
+     */
+    private array $externalSheets;
+
+    /**
+     * Array of sheet references in the form of REF structures.
+     */
+    public array $references;
+
+    /**
+     * The Excel ptg indices.
+     */
+    private array $ptg = [
+        'ptgExp' => 0x01,
+        'ptgTbl' => 0x02,
+        'ptgAdd' => 0x03,
+        'ptgSub' => 0x04,
+        'ptgMul' => 0x05,
+        'ptgDiv' => 0x06,
+        'ptgPower' => 0x07,
+        'ptgConcat' => 0x08,
+        'ptgLT' => 0x09,
+        'ptgLE' => 0x0A,
+        'ptgEQ' => 0x0B,
+        'ptgGE' => 0x0C,
+        'ptgGT' => 0x0D,
+        'ptgNE' => 0x0E,
+        'ptgIsect' => 0x0F,
+        'ptgUnion' => 0x10,
+        'ptgRange' => 0x11,
+        'ptgUplus' => 0x12,
+        'ptgUminus' => 0x13,
+        'ptgPercent' => 0x14,
+        'ptgParen' => 0x15,
+        'ptgMissArg' => 0x16,
+        'ptgStr' => 0x17,
+        'ptgAttr' => 0x19,
+        'ptgSheet' => 0x1A,
+        'ptgEndSheet' => 0x1B,
+        'ptgErr' => 0x1C,
+        'ptgBool' => 0x1D,
+        'ptgInt' => 0x1E,
+        'ptgNum' => 0x1F,
+        'ptgArray' => 0x20,
+        'ptgFunc' => 0x21,
+        'ptgFuncVar' => 0x22,
+        'ptgName' => 0x23,
+        'ptgRef' => 0x24,
+        'ptgArea' => 0x25,
+        'ptgMemArea' => 0x26,
+        'ptgMemErr' => 0x27,
+        'ptgMemNoMem' => 0x28,
+        'ptgMemFunc' => 0x29,
+        'ptgRefErr' => 0x2A,
+        'ptgAreaErr' => 0x2B,
+        'ptgRefN' => 0x2C,
+        'ptgAreaN' => 0x2D,
+        'ptgMemAreaN' => 0x2E,
+        'ptgMemNoMemN' => 0x2F,
+        'ptgNameX' => 0x39,
+        'ptgRef3d' => 0x3A,
+        'ptgArea3d' => 0x3B,
+        'ptgRefErr3d' => 0x3C,
+        'ptgAreaErr3d' => 0x3D,
+        'ptgArrayV' => 0x40,
+        'ptgFuncV' => 0x41,
+        'ptgFuncVarV' => 0x42,
+        'ptgNameV' => 0x43,
+        'ptgRefV' => 0x44,
+        'ptgAreaV' => 0x45,
+        'ptgMemAreaV' => 0x46,
+        'ptgMemErrV' => 0x47,
+        'ptgMemNoMemV' => 0x48,
+        'ptgMemFuncV' => 0x49,
+        'ptgRefErrV' => 0x4A,
+        'ptgAreaErrV' => 0x4B,
+        'ptgRefNV' => 0x4C,
+        'ptgAreaNV' => 0x4D,
+        'ptgMemAreaNV' => 0x4E,
+        'ptgMemNoMemNV' => 0x4F,
+        'ptgFuncCEV' => 0x58,
+        'ptgNameXV' => 0x59,
+        'ptgRef3dV' => 0x5A,
+        'ptgArea3dV' => 0x5B,
+        'ptgRefErr3dV' => 0x5C,
+        'ptgAreaErr3dV' => 0x5D,
+        'ptgArrayA' => 0x60,
+        'ptgFuncA' => 0x61,
+        'ptgFuncVarA' => 0x62,
+        'ptgNameA' => 0x63,
+        'ptgRefA' => 0x64,
+        'ptgAreaA' => 0x65,
+        'ptgMemAreaA' => 0x66,
+        'ptgMemErrA' => 0x67,
+        'ptgMemNoMemA' => 0x68,
+        'ptgMemFuncA' => 0x69,
+        'ptgRefErrA' => 0x6A,
+        'ptgAreaErrA' => 0x6B,
+        'ptgRefNA' => 0x6C,
+        'ptgAreaNA' => 0x6D,
+        'ptgMemAreaNA' => 0x6E,
+        'ptgMemNoMemNA' => 0x6F,
+        'ptgFuncCEA' => 0x78,
+        'ptgNameXA' => 0x79,
+        'ptgRef3dA' => 0x7A,
+        'ptgArea3dA' => 0x7B,
+        'ptgRefErr3dA' => 0x7C,
+        'ptgAreaErr3dA' => 0x7D,
+    ];
+
+    /**
+     * Thanks to Michael Meeks and Gnumeric for the initial arg values.
+     *
+     * The following hash was generated by "function_locale.pl" in the distro.
+     * Refer to function_locale.pl for non-English function names.
+     *
+     * The array elements are as follow:
+     * ptg:   The Excel function ptg code.
+     * args:  The number of arguments that the function takes:
+     *           >=0 is a fixed number of arguments.
+     *           -1  is a variable  number of arguments.
+     * class: The reference, value or array class of the function args.
+     * vol:   The function is volatile.
+     */
+    private array $functions = [
+        // function                  ptg  args  class  vol
+        'COUNT' => [0, -1, 0, 0],
+        'IF' => [1, -1, 1, 0],
+        'ISNA' => [2, 1, 1, 0],
+        'ISERROR' => [3, 1, 1, 0],
+        'SUM' => [4, -1, 0, 0],
+        'AVERAGE' => [5, -1, 0, 0],
+        'MIN' => [6, -1, 0, 0],
+        'MAX' => [7, -1, 0, 0],
+        'ROW' => [8, -1, 0, 0],
+        'COLUMN' => [9, -1, 0, 0],
+        'NA' => [10, 0, 0, 0],
+        'NPV' => [11, -1, 1, 0],
+        'STDEV' => [12, -1, 0, 0],
+        'DOLLAR' => [13, -1, 1, 0],
+        'FIXED' => [14, -1, 1, 0],
+        'SIN' => [15, 1, 1, 0],
+        'COS' => [16, 1, 1, 0],
+        'TAN' => [17, 1, 1, 0],
+        'ATAN' => [18, 1, 1, 0],
+        'PI' => [19, 0, 1, 0],
+        'SQRT' => [20, 1, 1, 0],
+        'EXP' => [21, 1, 1, 0],
+        'LN' => [22, 1, 1, 0],
+        'LOG10' => [23, 1, 1, 0],
+        'ABS' => [24, 1, 1, 0],
+        'INT' => [25, 1, 1, 0],
+        'SIGN' => [26, 1, 1, 0],
+        'ROUND' => [27, 2, 1, 0],
+        'LOOKUP' => [28, -1, 0, 0],
+        'INDEX' => [29, -1, 0, 1],
+        'REPT' => [30, 2, 1, 0],
+        'MID' => [31, 3, 1, 0],
+        'LEN' => [32, 1, 1, 0],
+        'VALUE' => [33, 1, 1, 0],
+        'TRUE' => [34, 0, 1, 0],
+        'FALSE' => [35, 0, 1, 0],
+        'AND' => [36, -1, 0, 0],
+        'OR' => [37, -1, 0, 0],
+        'NOT' => [38, 1, 1, 0],
+        'MOD' => [39, 2, 1, 0],
+        'DCOUNT' => [40, 3, 0, 0],
+        'DSUM' => [41, 3, 0, 0],
+        'DAVERAGE' => [42, 3, 0, 0],
+        'DMIN' => [43, 3, 0, 0],
+        'DMAX' => [44, 3, 0, 0],
+        'DSTDEV' => [45, 3, 0, 0],
+        'VAR' => [46, -1, 0, 0],
+        'DVAR' => [47, 3, 0, 0],
+        'TEXT' => [48, 2, 1, 0],
+        'LINEST' => [49, -1, 0, 0],
+        'TREND' => [50, -1, 0, 0],
+        'LOGEST' => [51, -1, 0, 0],
+        'GROWTH' => [52, -1, 0, 0],
+        'PV' => [56, -1, 1, 0],
+        'FV' => [57, -1, 1, 0],
+        'NPER' => [58, -1, 1, 0],
+        'PMT' => [59, -1, 1, 0],
+        'RATE' => [60, -1, 1, 0],
+        'MIRR' => [61, 3, 0, 0],
+        'IRR' => [62, -1, 0, 0],
+        'RAND' => [63, 0, 1, 1],
+        'MATCH' => [64, -1, 0, 0],
+        'DATE' => [65, 3, 1, 0],
+        'TIME' => [66, 3, 1, 0],
+        'DAY' => [67, 1, 1, 0],
+        'MONTH' => [68, 1, 1, 0],
+        'YEAR' => [69, 1, 1, 0],
+        'WEEKDAY' => [70, -1, 1, 0],
+        'HOUR' => [71, 1, 1, 0],
+        'MINUTE' => [72, 1, 1, 0],
+        'SECOND' => [73, 1, 1, 0],
+        'NOW' => [74, 0, 1, 1],
+        'AREAS' => [75, 1, 0, 1],
+        'ROWS' => [76, 1, 0, 1],
+        'COLUMNS' => [77, 1, 0, 1],
+        'OFFSET' => [78, -1, 0, 1],
+        'SEARCH' => [82, -1, 1, 0],
+        'TRANSPOSE' => [83, 1, 1, 0],
+        'TYPE' => [86, 1, 1, 0],
+        'ATAN2' => [97, 2, 1, 0],
+        'ASIN' => [98, 1, 1, 0],
+        'ACOS' => [99, 1, 1, 0],
+        'CHOOSE' => [100, -1, 1, 0],
+        'HLOOKUP' => [101, -1, 0, 0],
+        'VLOOKUP' => [102, -1, 0, 0],
+        'ISREF' => [105, 1, 0, 0],
+        'LOG' => [109, -1, 1, 0],
+        'CHAR' => [111, 1, 1, 0],
+        'LOWER' => [112, 1, 1, 0],
+        'UPPER' => [113, 1, 1, 0],
+        'PROPER' => [114, 1, 1, 0],
+        'LEFT' => [115, -1, 1, 0],
+        'RIGHT' => [116, -1, 1, 0],
+        'EXACT' => [117, 2, 1, 0],
+        'TRIM' => [118, 1, 1, 0],
+        'REPLACE' => [119, 4, 1, 0],
+        'SUBSTITUTE' => [120, -1, 1, 0],
+        'CODE' => [121, 1, 1, 0],
+        'FIND' => [124, -1, 1, 0],
+        'CELL' => [125, -1, 0, 1],
+        'ISERR' => [126, 1, 1, 0],
+        'ISTEXT' => [127, 1, 1, 0],
+        'ISNUMBER' => [128, 1, 1, 0],
+        'ISBLANK' => [129, 1, 1, 0],
+        'T' => [130, 1, 0, 0],
+        'N' => [131, 1, 0, 0],
+        'DATEVALUE' => [140, 1, 1, 0],
+        'TIMEVALUE' => [141, 1, 1, 0],
+        'SLN' => [142, 3, 1, 0],
+        'SYD' => [143, 4, 1, 0],
+        'DDB' => [144, -1, 1, 0],
+        'INDIRECT' => [148, -1, 1, 1],
+        'CALL' => [150, -1, 1, 0],
+        'CLEAN' => [162, 1, 1, 0],
+        'MDETERM' => [163, 1, 2, 0],
+        'MINVERSE' => [164, 1, 2, 0],
+        'MMULT' => [165, 2, 2, 0],
+        'IPMT' => [167, -1, 1, 0],
+        'PPMT' => [168, -1, 1, 0],
+        'COUNTA' => [169, -1, 0, 0],
+        'PRODUCT' => [183, -1, 0, 0],
+        'FACT' => [184, 1, 1, 0],
+        'DPRODUCT' => [189, 3, 0, 0],
+        'ISNONTEXT' => [190, 1, 1, 0],
+        'STDEVP' => [193, -1, 0, 0],
+        'VARP' => [194, -1, 0, 0],
+        'DSTDEVP' => [195, 3, 0, 0],
+        'DVARP' => [196, 3, 0, 0],
+        'TRUNC' => [197, -1, 1, 0],
+        'ISLOGICAL' => [198, 1, 1, 0],
+        'DCOUNTA' => [199, 3, 0, 0],
+        'USDOLLAR' => [204, -1, 1, 0],
+        'FINDB' => [205, -1, 1, 0],
+        'SEARCHB' => [206, -1, 1, 0],
+        'REPLACEB' => [207, 4, 1, 0],
+        'LEFTB' => [208, -1, 1, 0],
+        'RIGHTB' => [209, -1, 1, 0],
+        'MIDB' => [210, 3, 1, 0],
+        'LENB' => [211, 1, 1, 0],
+        'ROUNDUP' => [212, 2, 1, 0],
+        'ROUNDDOWN' => [213, 2, 1, 0],
+        'ASC' => [214, 1, 1, 0],
+        'DBCS' => [215, 1, 1, 0],
+        'RANK' => [216, -1, 0, 0],
+        'ADDRESS' => [219, -1, 1, 0],
+        'DAYS360' => [220, -1, 1, 0],
+        'TODAY' => [221, 0, 1, 1],
+        'VDB' => [222, -1, 1, 0],
+        'MEDIAN' => [227, -1, 0, 0],
+        'SUMPRODUCT' => [228, -1, 2, 0],
+        'SINH' => [229, 1, 1, 0],
+        'COSH' => [230, 1, 1, 0],
+        'TANH' => [231, 1, 1, 0],
+        'ASINH' => [232, 1, 1, 0],
+        'ACOSH' => [233, 1, 1, 0],
+        'ATANH' => [234, 1, 1, 0],
+        'DGET' => [235, 3, 0, 0],
+        'INFO' => [244, 1, 1, 1],
+        'DB' => [247, -1, 1, 0],
+        'FREQUENCY' => [252, 2, 0, 0],
+        'ERROR.TYPE' => [261, 1, 1, 0],
+        'REGISTER.ID' => [267, -1, 1, 0],
+        'AVEDEV' => [269, -1, 0, 0],
+        'BETADIST' => [270, -1, 1, 0],
+        'GAMMALN' => [271, 1, 1, 0],
+        'BETAINV' => [272, -1, 1, 0],
+        'BINOMDIST' => [273, 4, 1, 0],
+        'CHIDIST' => [274, 2, 1, 0],
+        'CHIINV' => [275, 2, 1, 0],
+        'COMBIN' => [276, 2, 1, 0],
+        'CONFIDENCE' => [277, 3, 1, 0],
+        'CRITBINOM' => [278, 3, 1, 0],
+        'EVEN' => [279, 1, 1, 0],
+        'EXPONDIST' => [280, 3, 1, 0],
+        'FDIST' => [281, 3, 1, 0],
+        'FINV' => [282, 3, 1, 0],
+        'FISHER' => [283, 1, 1, 0],
+        'FISHERINV' => [284, 1, 1, 0],
+        'FLOOR' => [285, 2, 1, 0],
+        'GAMMADIST' => [286, 4, 1, 0],
+        'GAMMAINV' => [287, 3, 1, 0],
+        'CEILING' => [288, 2, 1, 0],
+        'HYPGEOMDIST' => [289, 4, 1, 0],
+        'LOGNORMDIST' => [290, 3, 1, 0],
+        'LOGINV' => [291, 3, 1, 0],
+        'NEGBINOMDIST' => [292, 3, 1, 0],
+        'NORMDIST' => [293, 4, 1, 0],
+        'NORMSDIST' => [294, 1, 1, 0],
+        'NORMINV' => [295, 3, 1, 0],
+        'NORMSINV' => [296, 1, 1, 0],
+        'STANDARDIZE' => [297, 3, 1, 0],
+        'ODD' => [298, 1, 1, 0],
+        'PERMUT' => [299, 2, 1, 0],
+        'POISSON' => [300, 3, 1, 0],
+        'TDIST' => [301, 3, 1, 0],
+        'WEIBULL' => [302, 4, 1, 0],
+        'SUMXMY2' => [303, 2, 2, 0],
+        'SUMX2MY2' => [304, 2, 2, 0],
+        'SUMX2PY2' => [305, 2, 2, 0],
+        'CHITEST' => [306, 2, 2, 0],
+        'CORREL' => [307, 2, 2, 0],
+        'COVAR' => [308, 2, 2, 0],
+        'FORECAST' => [309, 3, 2, 0],
+        'FTEST' => [310, 2, 2, 0],
+        'INTERCEPT' => [311, 2, 2, 0],
+        'PEARSON' => [312, 2, 2, 0],
+        'RSQ' => [313, 2, 2, 0],
+        'STEYX' => [314, 2, 2, 0],
+        'SLOPE' => [315, 2, 2, 0],
+        'TTEST' => [316, 4, 2, 0],
+        'PROB' => [317, -1, 2, 0],
+        'DEVSQ' => [318, -1, 0, 0],
+        'GEOMEAN' => [319, -1, 0, 0],
+        'HARMEAN' => [320, -1, 0, 0],
+        'SUMSQ' => [321, -1, 0, 0],
+        'KURT' => [322, -1, 0, 0],
+        'SKEW' => [323, -1, 0, 0],
+        'ZTEST' => [324, -1, 0, 0],
+        'LARGE' => [325, 2, 0, 0],
+        'SMALL' => [326, 2, 0, 0],
+        'QUARTILE' => [327, 2, 0, 0],
+        'PERCENTILE' => [328, 2, 0, 0],
+        'PERCENTRANK' => [329, -1, 0, 0],
+        'MODE' => [330, -1, 2, 0],
+        'TRIMMEAN' => [331, 2, 0, 0],
+        'TINV' => [332, 2, 1, 0],
+        'CONCATENATE' => [336, -1, 1, 0],
+        'POWER' => [337, 2, 1, 0],
+        'RADIANS' => [342, 1, 1, 0],
+        'DEGREES' => [343, 1, 1, 0],
+        'SUBTOTAL' => [344, -1, 0, 0],
+        'SUMIF' => [345, -1, 0, 0],
+        'COUNTIF' => [346, 2, 0, 0],
+        'COUNTBLANK' => [347, 1, 0, 0],
+        'ISPMT' => [350, 4, 1, 0],
+        'DATEDIF' => [351, 3, 1, 0],
+        'DATESTRING' => [352, 1, 1, 0],
+        'NUMBERSTRING' => [353, 2, 1, 0],
+        'ROMAN' => [354, -1, 1, 0],
+        'GETPIVOTDATA' => [358, -1, 0, 0],
+        'HYPERLINK' => [359, -1, 1, 0],
+        'PHONETIC' => [360, 1, 0, 0],
+        'AVERAGEA' => [361, -1, 0, 0],
+        'MAXA' => [362, -1, 0, 0],
+        'MINA' => [363, -1, 0, 0],
+        'STDEVPA' => [364, -1, 0, 0],
+        'VARPA' => [365, -1, 0, 0],
+        'STDEVA' => [366, -1, 0, 0],
+        'VARA' => [367, -1, 0, 0],
+        'BAHTTEXT' => [368, 1, 0, 0],
+    ];
+
+    private Spreadsheet $spreadsheet;
+
+    /**
+     * The class constructor.
+     */
+    public function __construct(Spreadsheet $spreadsheet)
+    {
+        $this->spreadsheet = $spreadsheet;
+
+        $this->currentCharacter = 0;
+        $this->currentToken = ''; // The token we are working on.
+        $this->formula = ''; // The formula to parse.
+        $this->lookAhead = ''; // The character ahead of the current char.
+        $this->parseTree = ''; // The parse tree to be generated.
+        $this->externalSheets = [];
+        $this->references = [];
+    }
+
+    /**
+     * Convert a token to the proper ptg value.
+     *
+     * @param string $token the token to convert
+     *
+     * @return string the converted token on success
+     */
+    private function convert(string $token): string
+    {
+        if (Preg::isMatch('/"([^"]|""){0,255}"/', $token)) {
+            return $this->convertString($token);
+        }
+        if (is_numeric($token)) {
+            return $this->convertNumber($token);
+        }
+        // match references like A1 or $A$1
+        if (Preg::isMatch('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) {
+            return $this->convertRef2d($token);
+        }
+        // match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1
+        if (Preg::isMatch('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) {
+            return $this->convertRef3d($token);
+        }
+        // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1
+        if (self::matchCellSheetnameQuoted($token)) {
+            return $this->convertRef3d($token);
+        }
+        // match ranges like A1:B2 or $A$1:$B$2
+        if (Preg::isMatch('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) {
+            return $this->convertRange2d($token);
+        }
+        // match external ranges like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2
+        if (Preg::isMatch('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) {
+            return $this->convertRange3d($token);
+        }
+        // match external ranges like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2
+        if (self::matchRangeSheetnameQuoted($token)) {
+            return $this->convertRange3d($token);
+        }
+        // operators (including parentheses)
+        if (isset($this->ptg[$token])) {
+            return pack('C', $this->ptg[$token]);
+        }
+        // match error codes
+        if (Preg::isMatch('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') {
+            return $this->convertError($token);
+        }
+        if (Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) {
+            return $this->convertDefinedName($token);
+        }
+        // commented so argument number can be processed correctly. See toReversePolish().
+        /*if (Preg::isMatch("/[A-Z0-9\xc0-\xdc\.]+/", $token))
+        {
+            return($this->convertFunction($token, $this->_func_args));
+        }*/
+        // if it's an argument, ignore the token (the argument remains)
+        if ($token == 'arg') {
+            return '';
+        }
+        if (Preg::isMatch('/^true$/i', $token)) {
+            return $this->convertBool(1);
+        }
+        if (Preg::isMatch('/^false$/i', $token)) {
+            return $this->convertBool(0);
+        }
+
+        // TODO: use real error codes
+        throw new WriterException("Unknown token $token");
+    }
+
+    /**
+     * Convert a number token to ptgInt or ptgNum.
+     *
+     * @param float|int|string $num an integer or double for conversion to its ptg value
+     */
+    private function convertNumber($num): string
+    {
+        // Integer in the range 0..2**16-1
+        if ((Preg::isMatch('/^\\d+$/', (string) $num)) && ($num <= 65535)) {
+            return pack('Cv', $this->ptg['ptgInt'], $num);
+        }
+
+        // A float
+        if (BIFFwriter::getByteOrder()) { // if it's Big Endian
+            $num = strrev((string) $num);
+        }
+
+        return pack('Cd', $this->ptg['ptgNum'], $num);
+    }
+
+    private function convertBool(int $num): string
+    {
+        return pack('CC', $this->ptg['ptgBool'], $num);
+    }
+
+    /**
+     * Convert a string token to ptgStr.
+     *
+     * @param string $string a string for conversion to its ptg value
+     *
+     * @return string the converted token
+     */
+    private function convertString(string $string): string
+    {
+        // chop away beggining and ending quotes
+        $string = substr($string, 1, -1);
+        if (strlen($string) > 255) {
+            throw new WriterException('String is too long');
+        }
+
+        return pack('C', $this->ptg['ptgStr']) . StringHelper::UTF8toBIFF8UnicodeShort($string);
+    }
+
+    /**
+     * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of
+     * args that it takes.
+     *
+     * @param string $token the name of the function for convertion to ptg value
+     * @param int $num_args the number of arguments the function receives
+     *
+     * @return string The packed ptg for the function
+     */
+    private function convertFunction(string $token, int $num_args): string
+    {
+        $args = $this->functions[$token][1];
+
+        // Fixed number of args eg. TIME($i, $j, $k).
+        if ($args >= 0) {
+            return pack('Cv', $this->ptg['ptgFuncV'], $this->functions[$token][0]);
+        }
+
+        // Variable number of args eg. SUM($i, $j, $k, ..).
+        return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]);
+    }
+
+    /**
+     * Convert an Excel range such as A1:D4 to a ptgRefV.
+     *
+     * @param string $range An Excel range in the A1:A2
+     */
+    private function convertRange2d(string $range, int $class = 0): string
+    {
+        // TODO: possible class value 0,1,2 check Formula.pm
+        // Split the range into 2 cell refs
+        if (Preg::isMatch('/^(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)\:(\$)?([A-Ia-i]?[A-Za-z])(\$)?(\d+)$/', $range)) {
+            [$cell1, $cell2] = explode(':', $range);
+        } else {
+            // TODO: use real error codes
+            throw new WriterException('Unknown range separator');
+        }
+        // Convert the cell references
+        [$row1, $col1] = $this->cellToPackedRowcol($cell1);
+        [$row2, $col2] = $this->cellToPackedRowcol($cell2);
+
+        // The ptg value depends on the class of the ptg.
+        if ($class == 0) {
+            $ptgArea = pack('C', $this->ptg['ptgArea']);
+        } elseif ($class == 1) {
+            $ptgArea = pack('C', $this->ptg['ptgAreaV']);
+        } elseif ($class == 2) {
+            $ptgArea = pack('C', $this->ptg['ptgAreaA']);
+        } else {
+            // TODO: use real error codes
+            throw new WriterException("Unknown class $class");
+        }
+
+        return $ptgArea . $row1 . $row2 . $col1 . $col2;
+    }
+
+    /**
+     * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to
+     * a ptgArea3d.
+     *
+     * @param string $token an Excel range in the Sheet1!A1:A2 format
+     *
+     * @return string the packed ptgArea3d token on success
+     */
+    private function convertRange3d(string $token): string
+    {
+        // Split the ref at the ! symbol
+        [$ext_ref, $range] = PhpspreadsheetWorksheet::extractSheetTitle($token, true);
+
+        // Convert the external reference part (different for BIFF8)
+        $ext_ref = $this->getRefIndex($ext_ref ?? '');
+
+        // Split the range into 2 cell refs
+        [$cell1, $cell2] = explode(':', $range ?? '');
+
+        // Convert the cell references
+        if (Preg::isMatch('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\\d+)$/', $cell1)) {
+            [$row1, $col1] = $this->cellToPackedRowcol($cell1);
+            [$row2, $col2] = $this->cellToPackedRowcol($cell2);
+        } else { // It's a rows range (like 26:27)
+            [$row1, $col1, $row2, $col2] = $this->rangeToPackedRange($cell1 . ':' . $cell2);
+        }
+
+        // The ptg value depends on the class of the ptg.
+        $ptgArea = pack('C', $this->ptg['ptgArea3d']);
+
+        return $ptgArea . $ext_ref . $row1 . $row2 . $col1 . $col2;
+    }
+
+    /**
+     * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV.
+     *
+     * @param string $cell An Excel cell reference
+     *
+     * @return string The cell in packed() format with the corresponding ptg
+     */
+    private function convertRef2d(string $cell): string
+    {
+        // Convert the cell reference
+        $cell_array = $this->cellToPackedRowcol($cell);
+        [$row, $col] = $cell_array;
+
+        // The ptg value depends on the class of the ptg.
+        $ptgRef = pack('C', $this->ptg['ptgRefA']);
+
+        return $ptgRef . $row . $col;
+    }
+
+    /**
+     * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a
+     * ptgRef3d.
+     *
+     * @param string $cell An Excel cell reference
+     *
+     * @return string the packed ptgRef3d token on success
+     */
+    private function convertRef3d(string $cell): string
+    {
+        // Split the ref at the ! symbol
+        [$ext_ref, $cell] = PhpspreadsheetWorksheet::extractSheetTitle($cell, true);
+
+        // Convert the external reference part (different for BIFF8)
+        $ext_ref = $this->getRefIndex($ext_ref ?? '');
+
+        // Convert the cell reference part
+        [$row, $col] = $this->cellToPackedRowcol($cell ?? '');
+
+        // The ptg value depends on the class of the ptg.
+        $ptgRef = pack('C', $this->ptg['ptgRef3dA']);
+
+        return $ptgRef . $ext_ref . $row . $col;
+    }
+
+    /**
+     * Convert an error code to a ptgErr.
+     *
+     * @param string $errorCode The error code for conversion to its ptg value
+     *
+     * @return string The error code ptgErr
+     */
+    private function convertError($errorCode)
+    {
+        switch ($errorCode) {
+            case '#NULL!':
+                return pack('C', 0x00);
+            case '#DIV/0!':
+                return pack('C', 0x07);
+            case '#VALUE!':
+                return pack('C', 0x0F);
+            case '#REF!':
+                return pack('C', 0x17);
+            case '#NAME?':
+                return pack('C', 0x1D);
+            case '#NUM!':
+                return pack('C', 0x24);
+            case '#N/A':
+                return pack('C', 0x2A);
+        }
+
+        return pack('C', 0xFF);
+    }
+
+    private bool $tryDefinedName = false;
+
+    private function convertDefinedName(string $name): string
+    {
+        if (strlen($name) > 255) {
+            throw new WriterException('Defined Name is too long');
+        }
+
+        if ($this->tryDefinedName) {
+            // @codeCoverageIgnoreStart
+            $nameReference = 1;
+            foreach ($this->spreadsheet->getDefinedNames() as $definedName) {
+                if ($name === $definedName->getName()) {
+                    break;
+                }
+                ++$nameReference;
+            }
+
+            $ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference);
+
+            return $ptgRef;
+            // @codeCoverageIgnoreEnd
+        }
+
+        throw new WriterException('Cannot yet write formulae with defined names to Xls');
+    }
+
+    /**
+     * Look up the REF index that corresponds to an external sheet name
+     * (or range). If it doesn't exist yet add it to the workbook's references
+     * array. It assumes all sheet names given must exist.
+     *
+     * @param string $ext_ref The name of the external reference
+     *
+     * @return string The reference index in packed() format on success
+     */
+    private function getRefIndex(string $ext_ref): string
+    {
+        $ext_ref = Preg::replace(["/^'/", "/'$/"], ['', ''], $ext_ref); // Remove leading and trailing ' if any.
+        $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with '
+
+        // Check if there is a sheet range eg., Sheet1:Sheet2.
+        if (Preg::isMatch('/:/', $ext_ref)) {
+            [$sheet_name1, $sheet_name2] = explode(':', $ext_ref);
+
+            $sheet1 = $this->getSheetIndex($sheet_name1);
+            if ($sheet1 == -1) {
+                throw new WriterException("Unknown sheet name $sheet_name1 in formula");
+            }
+            $sheet2 = $this->getSheetIndex($sheet_name2);
+            if ($sheet2 == -1) {
+                throw new WriterException("Unknown sheet name $sheet_name2 in formula");
+            }
+
+            // Reverse max and min sheet numbers if necessary
+            if ($sheet1 > $sheet2) {
+                [$sheet1, $sheet2] = [$sheet2, $sheet1];
+            }
+        } else { // Single sheet name only.
+            $sheet1 = $this->getSheetIndex($ext_ref);
+            if ($sheet1 == -1) {
+                throw new WriterException("Unknown sheet name $ext_ref in formula");
+            }
+            $sheet2 = $sheet1;
+        }
+
+        // assume all references belong to this document
+        $supbook_index = 0x00;
+        $ref = pack('vvv', $supbook_index, $sheet1, $sheet2);
+        $totalreferences = count($this->references);
+        $index = -1;
+        for ($i = 0; $i < $totalreferences; ++$i) {
+            if ($ref == $this->references[$i]) {
+                $index = $i;
+
+                break;
+            }
+        }
+        // if REF was not found add it to references array
+        if ($index == -1) {
+            $this->references[$totalreferences] = $ref;
+            $index = $totalreferences;
+        }
+
+        return pack('v', $index);
+    }
+
+    /**
+     * Look up the index that corresponds to an external sheet name. The hash of
+     * sheet names is updated by the addworksheet() method of the
+     * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class.
+     *
+     * @param string $sheet_name Sheet name
+     *
+     * @return int The sheet index, -1 if the sheet was not found
+     */
+    private function getSheetIndex(string $sheet_name): int
+    {
+        if (!isset($this->externalSheets[$sheet_name])) {
+            return -1;
+        }
+
+        return $this->externalSheets[$sheet_name];
+    }
+
+    /**
+     * This method is used to update the array of sheet names. It is
+     * called by the addWorksheet() method of the
+     * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class.
+     *
+     * @param string $name The name of the worksheet being added
+     * @param int $index The index of the worksheet being added
+     *
+     * @see Workbook::addWorksheet
+     */
+    public function setExtSheet(string $name, int $index): void
+    {
+        $this->externalSheets[$name] = $index;
+    }
+
+    /**
+     * pack() row and column into the required 3 or 4 byte format.
+     *
+     * @param string $cell The Excel cell reference to be packed
+     *
+     * @return array Array containing the row and column in packed() format
+     */
+    private function cellToPackedRowcol(string $cell): array
+    {
+        $cell = strtoupper($cell);
+        [$row, $col, $row_rel, $col_rel] = $this->cellToRowcol($cell);
+        if ($col >= 256) {
+            throw new WriterException("Column in: $cell greater than 255");
+        }
+        if ($row >= 65536) {
+            throw new WriterException("Row in: $cell greater than 65536 ");
+        }
+
+        // Set the high bits to indicate if row or col are relative.
+        $col |= $col_rel << 14;
+        $col |= $row_rel << 15;
+        $col = pack('v', $col);
+
+        $row = pack('v', $row);
+
+        return [$row, $col];
+    }
+
+    /**
+     * pack() row range into the required 3 or 4 byte format.
+     * Just using maximum col/rows, which is probably not the correct solution.
+     *
+     * @param string $range The Excel range to be packed
+     *
+     * @return array Array containing (row1,col1,row2,col2) in packed() format
+     */
+    private function rangeToPackedRange(string $range): array
+    {
+        if (!Preg::isMatch('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match)) {
+            // @codeCoverageIgnoreStart
+            throw new WriterException('Regexp failure in rangeToPackedRange');
+            // @codeCoverageIgnoreEnd
+        }
+        // return absolute rows if there is a $ in the ref
+        $row1_rel = empty($match[1]) ? 1 : 0;
+        $row1 = $match[2];
+        $row2_rel = empty($match[3]) ? 1 : 0;
+        $row2 = $match[4];
+        // Convert 1-index to zero-index
+        --$row1;
+        --$row2;
+        // Trick poor inocent Excel
+        $col1 = 0;
+        $col2 = 65535; // FIXME: maximum possible value for Excel 5 (change this!!!)
+
+        // FIXME: this changes for BIFF8
+        if (($row1 >= 65536) || ($row2 >= 65536)) {
+            throw new WriterException("Row in: $range greater than 65536 ");
+        }
+
+        // Set the high bits to indicate if rows are relative.
+        $col1 |= $row1_rel << 15;
+        $col2 |= $row2_rel << 15;
+        $col1 = pack('v', $col1);
+        $col2 = pack('v', $col2);
+
+        $row1 = pack('v', $row1);
+        $row2 = pack('v', $row2);
+
+        return [$row1, $col1, $row2, $col2];
+    }
+
+    /**
+     * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero
+     * indexed row and column number. Also returns two (0,1) values to indicate
+     * whether the row or column are relative references.
+     *
+     * @param string $cell the Excel cell reference in A1 format
+     */
+    private function cellToRowcol(string $cell): array
+    {
+        if (!Preg::isMatch('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match)) {
+            // @codeCoverageIgnoreStart
+            throw new WriterException('Regexp failure in cellToRowcol');
+            // @codeCoverageIgnoreEnd
+        }
+        // return absolute column if there is a $ in the ref
+        $col_rel = empty($match[1]) ? 1 : 0;
+        $col_ref = $match[2];
+        $row_rel = empty($match[3]) ? 1 : 0;
+        $row = $match[4];
+
+        // Convert base26 column string to a number.
+        $expn = strlen($col_ref) - 1;
+        $col = 0;
+        $col_ref_length = strlen($col_ref);
+        for ($i = 0; $i < $col_ref_length; ++$i) {
+            $col += (ord($col_ref[$i]) - 64) * 26 ** $expn;
+            --$expn;
+        }
+
+        // Convert 1-index to zero-index
+        --$row;
+        --$col;
+
+        return [$row, $col, $row_rel, $col_rel];
+    }
+
+    /**
+     * Advance to the next valid token.
+     */
+    private function advance(): void
+    {
+        $token = '';
+        $i = $this->currentCharacter;
+        $formula = mb_str_split($this->formula, 1, self::UTF8);
+        $formula_length = count($formula);
+        // eat up white spaces
+        if ($i < $formula_length) {
+            while ($formula[$i] === ' ') {
+                ++$i;
+            }
+
+            if ($i < ($formula_length - 1)) {
+                $this->lookAhead = $formula[$i + 1];
+            }
+            $token = '';
+        }
+
+        while ($i < $formula_length) {
+            $token .= $formula[$i];
+
+            if ($i < ($formula_length - 1)) {
+                $this->lookAhead = $formula[$i + 1];
+            } else {
+                $this->lookAhead = '';
+            }
+
+            if ($this->match($token) !== '') {
+                $this->currentCharacter = $i + 1;
+                $this->currentToken = $token;
+
+                return;
+            }
+
+            if ($i < ($formula_length - 2)) {
+                $this->lookAhead = $formula[$i + 2];
+            } else { // if we run out of characters lookAhead becomes empty
+                $this->lookAhead = '';
+            }
+            ++$i;
+        }
+    }
+
+    /**
+     * Checks if it's a valid token.
+     *
+     * @param string $token the token to check
+     *
+     * @return string The checked token or empty string on failure
+     */
+    private function match(string $token): string
+    {
+        switch ($token) {
+            case '+':
+            case '-':
+            case '*':
+            case '/':
+            case '(':
+            case ')':
+            case ',':
+            case ';':
+            case '>=':
+            case '<=':
+            case '=':
+            case '<>':
+            case '^':
+            case '&':
+            case '%':
+                return $token;
+
+            case '>':
+                if ($this->lookAhead === '=') { // it's a GE token
+                    break;
+                }
+
+                return $token;
+
+            case '<':
+                // it's a LE or a NE token
+                if (($this->lookAhead === '=') || ($this->lookAhead === '>')) {
+                    break;
+                }
+
+                return $token;
+        }
+
+        // if it's a reference A1 or $A$1 or $A1 or A$1
+        if (
+            Preg::isMatch('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token)
+            && !Preg::isMatch('/\d/', $this->lookAhead)
+            && ($this->lookAhead !== ':')
+            && ($this->lookAhead !== '.')
+            && ($this->lookAhead !== '!')
+        ) {
+            return $token;
+        }
+        // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
+        if (
+            Preg::isMatch('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token)
+            && !Preg::isMatch('/\d/', $this->lookAhead)
+            && ($this->lookAhead !== ':')
+            && ($this->lookAhead !== '.')
+        ) {
+            return $token;
+        }
+        // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
+        if (
+            self::matchCellSheetnameQuoted($token)
+            && !Preg::isMatch('/\\d/', $this->lookAhead)
+            && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')
+        ) {
+            return $token;
+        }
+        // if it's a range A1:A2 or $A$1:$A$2
+        if (
+            Preg::isMatch(
+                '/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/',
+                $token
+            )
+            && !Preg::isMatch('/\d/', $this->lookAhead)
+        ) {
+            return $token;
+        }
+        // If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2
+        if (
+            Preg::isMatch(
+                '/^'
+                . self::REGEX_SHEET_TITLE_UNQUOTED
+                . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED
+                . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u',
+                $token
+            )
+            && !Preg::isMatch('/\d/', $this->lookAhead)
+        ) {
+            return $token;
+        }
+        // If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2
+        if (
+            self::matchRangeSheetnameQuoted($token)
+            && !Preg::isMatch('/\\d/', $this->lookAhead)
+        ) {
+            return $token;
+        }
+        // If it's a number (check that it's not a sheet name or range)
+        if (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) {
+            return $token;
+        }
+        if (
+            Preg::isMatch('/"([^"]|""){0,255}"/', $token)
+            && $this->lookAhead !== '"'
+            && (substr_count($token, '"') % 2 == 0)
+        ) {
+            // If it's a string (of maximum 255 characters)
+            return $token;
+        }
+        // If it's an error code
+        if (
+            Preg::isMatch('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token)
+            || $token === '#N/A'
+        ) {
+            return $token;
+        }
+        // if it's a function call
+        if (
+            Preg::isMatch("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token)
+            && ($this->lookAhead === '(')
+        ) {
+            return $token;
+        }
+        if (
+            Preg::isMatch(
+                '/^'
+                . Calculation::CALCULATION_REGEXP_DEFINEDNAME
+                . '$/miu',
+                $token
+            )
+            && $this->spreadsheet->getDefinedName($token) !== null
+        ) {
+            return $token;
+        }
+        if (
+            Preg::isMatch('/^true$/i', $token)
+            && ($this->lookAhead === ')' || $this->lookAhead === ',')
+        ) {
+            return $token;
+        }
+        if (
+            Preg::isMatch('/^false$/i', $token)
+            && ($this->lookAhead === ')' || $this->lookAhead === ',')
+        ) {
+            return $token;
+        }
+        if (substr($token, -1) === ')') {
+            //    It's an argument of some description (e.g. a named range),
+            //        precise nature yet to be determined
+            return $token;
+        }
+
+        return '';
+    }
+
+    /**
+     * The parsing method. It parses a formula.
+     *
+     * @param string $formula the formula to parse, without the initial equal
+     *                        sign (=)
+     *
+     * @return bool true on success
+     */
+    public function parse(string $formula): bool
+    {
+        $this->currentCharacter = 0;
+        $this->formula = $formula;
+        $this->lookAhead = mb_substr($formula, 1, 1, self::UTF8);
+        $this->advance();
+        $this->parseTree = $this->condition();
+
+        return true;
+    }
+
+    /**
+     * It parses a condition. It assumes the following rule:
+     * Cond -> Expr [(">" | "<") Expr].
+     *
+     * @return array The parsed ptg'd tree on success
+     */
+    private function condition(): array
+    {
+        $result = $this->expression();
+        if ($this->currentToken == '<') {
+            $this->advance();
+            $result2 = $this->expression();
+            $result = $this->createTree('ptgLT', $result, $result2);
+        } elseif ($this->currentToken == '>') {
+            $this->advance();
+            $result2 = $this->expression();
+            $result = $this->createTree('ptgGT', $result, $result2);
+        } elseif ($this->currentToken == '<=') {
+            $this->advance();
+            $result2 = $this->expression();
+            $result = $this->createTree('ptgLE', $result, $result2);
+        } elseif ($this->currentToken == '>=') {
+            $this->advance();
+            $result2 = $this->expression();
+            $result = $this->createTree('ptgGE', $result, $result2);
+        } elseif ($this->currentToken == '=') {
+            $this->advance();
+            $result2 = $this->expression();
+            $result = $this->createTree('ptgEQ', $result, $result2);
+        } elseif ($this->currentToken == '<>') {
+            $this->advance();
+            $result2 = $this->expression();
+            $result = $this->createTree('ptgNE', $result, $result2);
+        }
+
+        return $result;
+    }
+
+    /**
+     * It parses a expression. It assumes the following rule:
+     * Expr -> Term [("+" | "-") Term]
+     *      -> "string"
+     *      -> "-" Term : Negative value
+     *      -> "+" Term : Positive value
+     *      -> Error code.
+     *
+     * @return array The parsed ptg'd tree on success
+     */
+    private function expression(): array
+    {
+        // If it's a string return a string node
+        if (Preg::isMatch('/"([^"]|""){0,255}"/', $this->currentToken)) {
+            $tmp = str_replace('""', '"', $this->currentToken);
+            if (($tmp == '"') || ($tmp == '')) {
+                //    Trap for "" that has been used for an empty string
+                $tmp = '""';
+            }
+            $result = $this->createTree($tmp, '', '');
+            $this->advance();
+
+            return $result;
+        }
+        if (
+            Preg::isMatch('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken)
+            || $this->currentToken == '#N/A'
+        ) { // error code
+            $result = $this->createTree($this->currentToken, 'ptgErr', '');
+            $this->advance();
+
+            return $result;
+        }
+        if ($this->currentToken == '-') { // negative value
+            // catch "-" Term
+            $this->advance();
+            $result2 = $this->expression();
+
+            return $this->createTree('ptgUminus', $result2, '');
+        } elseif ($this->currentToken == '+') { // positive value
+            // catch "+" Term
+            $this->advance();
+            $result2 = $this->expression();
+
+            return $this->createTree('ptgUplus', $result2, '');
+        }
+        $result = $this->term();
+        while ($this->currentToken === '&') {
+            $this->advance();
+            $result2 = $this->expression();
+            $result = $this->createTree('ptgConcat', $result, $result2);
+        }
+        while (
+            ($this->currentToken == '+')
+            || ($this->currentToken == '-')
+            || ($this->currentToken == '^')
+        ) {
+            if ($this->currentToken == '+') {
+                $this->advance();
+                $result2 = $this->term();
+                $result = $this->createTree('ptgAdd', $result, $result2);
+            } elseif ($this->currentToken == '-') {
+                $this->advance();
+                $result2 = $this->term();
+                $result = $this->createTree('ptgSub', $result, $result2);
+            } else {
+                $this->advance();
+                $result2 = $this->term();
+                $result = $this->createTree('ptgPower', $result, $result2);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * This function just introduces a ptgParen element in the tree, so that Excel
+     * doesn't get confused when working with a parenthesized formula afterwards.
+     *
+     * @return array The parsed ptg'd tree
+     *
+     * @see fact()
+     */
+    private function parenthesizedExpression(): array
+    {
+        return $this->createTree('ptgParen', $this->expression(), '');
+    }
+
+    /**
+     * It parses a term. It assumes the following rule:
+     * Term -> Fact [("*" | "/") Fact].
+     *
+     * @return array The parsed ptg'd tree on success
+     */
+    private function term(): array
+    {
+        $result = $this->fact();
+        while (
+            ($this->currentToken == '*')
+            || ($this->currentToken == '/')
+        ) {
+            if ($this->currentToken == '*') {
+                $this->advance();
+                $result2 = $this->fact();
+                $result = $this->createTree('ptgMul', $result, $result2);
+            } else {
+                $this->advance();
+                $result2 = $this->fact();
+                $result = $this->createTree('ptgDiv', $result, $result2);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * It parses a factor. It assumes the following rule:
+     * Fact -> ( Expr )
+     *       | CellRef
+     *       | CellRange
+     *       | Number
+     *       | Function.
+     *
+     * @return array The parsed ptg'd tree on success
+     */
+    private function fact(): array
+    {
+        $currentToken = $this->currentToken;
+        if ($currentToken === '(') {
+            $this->advance(); // eat the "("
+            $result = $this->parenthesizedExpression();
+            if ($this->currentToken !== ')') {
+                throw new WriterException("')' token expected.");
+            }
+            $this->advance(); // eat the ")"
+
+            return $result;
+        }
+        // if it's a reference
+        if (Preg::isMatch('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) {
+            $result = $this->createTree($this->currentToken, '', '');
+            $this->advance();
+
+            return $result;
+        }
+        if (
+            Preg::isMatch(
+                '/^'
+                . self::REGEX_SHEET_TITLE_UNQUOTED
+                . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED
+                . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u',
+                $this->currentToken
+            )
+        ) {
+            // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
+            $result = $this->createTree($this->currentToken, '', '');
+            $this->advance();
+
+            return $result;
+        }
+        if (self::matchCellSheetnameQuoted($this->currentToken)) {
+            // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
+            $result = $this->createTree($this->currentToken, '', '');
+            $this->advance();
+
+            return $result;
+        }
+        if (
+            Preg::isMatch(
+                '/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/',
+                $this->currentToken
+            )
+            || Preg::isMatch(
+                '/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/',
+                $this->currentToken
+            )
+        ) {
+            // if it's a range A1:B2 or $A$1:$B$2
+            // must be an error?
+            $result = $this->createTree($this->currentToken, '', '');
+            $this->advance();
+
+            return $result;
+        }
+        if (
+            Preg::isMatch(
+                '/^'
+                . self::REGEX_SHEET_TITLE_UNQUOTED
+                . '(\\:'
+                . self::REGEX_SHEET_TITLE_UNQUOTED
+                . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u',
+                $this->currentToken
+            )
+        ) {
+            // If it's an external range (Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2)
+            // must be an error?
+            $result = $this->createTree($this->currentToken, '', '');
+            $this->advance();
+
+            return $result;
+        }
+        if (self::matchRangeSheetnameQuoted($this->currentToken)) {
+            // If it's an external range ('Sheet1'!A1:B2 or 'Sheet1'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1'!$A$1:$B$2)
+            // must be an error?
+            $result = $this->createTree($this->currentToken, '', '');
+            $this->advance();
+
+            return $result;
+        }
+        if (is_numeric($this->currentToken)) {
+            // If it's a number or a percent
+            if ($this->lookAhead === '%') {
+                $result = $this->createTree('ptgPercent', $this->currentToken, '');
+                $this->advance(); // Skip the percentage operator once we've pre-built that tree
+            } else {
+                $result = $this->createTree($this->currentToken, '', '');
+            }
+            $this->advance();
+
+            return $result;
+        }
+        if (
+            Preg::isMatch("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken)
+            && ($this->lookAhead === '(')
+        ) {
+            // if it's a function call
+            return $this->func();
+        }
+        if (
+            Preg::isMatch(
+                '/^'
+                . Calculation::CALCULATION_REGEXP_DEFINEDNAME
+                . '$/miu',
+                $this->currentToken
+            )
+            && $this->spreadsheet->getDefinedName($this->currentToken) !== null
+        ) {
+            $result = $this->createTree('ptgName', $this->currentToken, '');
+            $this->advance();
+
+            return $result;
+        }
+        if (Preg::isMatch('/^true|false$/i', $this->currentToken)) {
+            $result = $this->createTree($this->currentToken, '', '');
+            $this->advance();
+
+            return $result;
+        }
+
+        throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter);
+    }
+
+    /**
+     * It parses a function call. It assumes the following rule:
+     * Func -> ( Expr [,Expr]* ).
+     *
+     * @return array The parsed ptg'd tree on success
+     */
+    private function func(): array
+    {
+        $num_args = 0; // number of arguments received
+        $function = strtoupper($this->currentToken);
+        $result = ''; // initialize result
+        $this->advance();
+        $this->advance(); // eat the "("
+        while ($this->currentToken !== ')') {
+            if ($num_args > 0) {
+                if ($this->currentToken === ',' || $this->currentToken === ';') {
+                    $this->advance(); // eat the "," or ";"
+                } else {
+                    throw new WriterException("Syntax error: comma expected in function $function, arg #{$num_args}");
+                }
+                $result2 = $this->condition();
+                $result = $this->createTree('arg', $result, $result2);
+            } else { // first argument
+                $result2 = $this->condition();
+                $result = $this->createTree('arg', '', $result2);
+            }
+            ++$num_args;
+        }
+        if (!isset($this->functions[$function])) {
+            throw new WriterException("Function $function() doesn't exist");
+        }
+        $args = $this->functions[$function][1];
+        // If fixed number of args eg. TIME($i, $j, $k). Check that the number of args is valid.
+        if (($args >= 0) && ($args != $num_args)) {
+            throw new WriterException("Incorrect number of arguments in function $function() ");
+        }
+
+        $result = $this->createTree($function, $result, $num_args);
+        $this->advance(); // eat the ")"
+
+        return $result;
+    }
+
+    /**
+     * Creates a tree. In fact an array which may have one or two arrays (sub-trees)
+     * as elements.
+     *
+     * @param mixed $value the value of this node
+     * @param mixed $left the left array (sub-tree) or a final node
+     * @param mixed $right the right array (sub-tree) or a final node
+     *
+     * @return array A tree
+     */
+    private function createTree($value, $left, $right): array
+    {
+        return ['value' => $value, 'left' => $left, 'right' => $right];
+    }
+
+    /**
+     * Builds a string containing the tree in reverse polish notation (What you
+     * would use in a HP calculator stack).
+     * The following tree:.
+     *
+     *    +
+     *   / \
+     *  2   3
+     *
+     * produces: "23+"
+     *
+     * The following tree:
+     *
+     *    +
+     *   / \
+     *  3   *
+     *     / \
+     *    6   A1
+     *
+     * produces: "36A1*+"
+     *
+     * In fact all operands, functions, references, etc... are written as ptg's
+     *
+     * @param array $tree the optional tree to convert
+     *
+     * @return string The tree in reverse polish notation
+     */
+    public function toReversePolish(array $tree = []): string
+    {
+        $polish = ''; // the string we are going to return
+        if (empty($tree)) { // If it's the first call use parseTree
+            $tree = $this->parseTree;
+        }
+        if (!is_array($tree) || !isset($tree['left'], $tree['right'], $tree['value'])) {
+            throw new WriterException('Unexpected non-array');
+        }
+
+        if (is_array($tree['left'])) {
+            $converted_tree = $this->toReversePolish($tree['left']);
+            $polish .= $converted_tree;
+        } elseif ($tree['left'] != '') { // It's a final node
+            $converted_tree = $this->convert($tree['left']);
+            $polish .= $converted_tree;
+        }
+        if (is_array($tree['right'])) {
+            $converted_tree = $this->toReversePolish($tree['right']);
+            $polish .= $converted_tree;
+        } elseif ($tree['right'] != '') { // It's a final node
+            $converted_tree = $this->convert($tree['right']);
+            $polish .= $converted_tree;
+        }
+        // if it's a function convert it here (so we can set it's arguments)
+        if (
+            Preg::isMatch("/^[A-Z0-9\xc0-\xdc\\.]+$/", $tree['value'])
+            && !Preg::isMatch('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value'])
+            && !Preg::isMatch(
+                '/^[A-Ia-i]?[A-Za-z](\\d+)\\.\\.[A-Ia-i]?[A-Za-z](\\d+)$/',
+                $tree['value']
+            )
+            && !is_numeric($tree['value'])
+            && !isset($this->ptg[$tree['value']])
+        ) {
+            // left subtree for a function is always an array.
+            if ($tree['left'] != '') {
+                $left_tree = $this->toReversePolish($tree['left']);
+            } else {
+                $left_tree = '';
+            }
+
+            // add its left subtree and return.
+            if ($left_tree !== '' || $tree['right'] !== '') {
+                return $left_tree . $this->convertFunction($tree['value'], $tree['right'] ?: 0);
+            }
+        }
+        $converted_tree = $this->convert($tree['value']);
+
+        return $polish . $converted_tree;
+    }
+
+    public static function matchCellSheetnameQuoted(string $token): bool
+    {
+        return Preg::isMatch(
+            self::REGEX_CELL_TITLE_QUOTED,
+            $token
+        );
+    }
+
+    public static function matchRangeSheetnameQuoted(string $token): bool
+    {
+        return Preg::isMatch(
+            self::REGEX_RANGE_TITLE_QUOTED,
+            $token
+        );
+    }
+}

+ 59 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellAlignment.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls\Style;
+
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+
+class CellAlignment
+{
+    /**
+     * @var array<string, int>
+     */
+    private static $horizontalMap = [
+        Alignment::HORIZONTAL_GENERAL => 0,
+        Alignment::HORIZONTAL_LEFT => 1,
+        Alignment::HORIZONTAL_RIGHT => 3,
+        Alignment::HORIZONTAL_CENTER => 2,
+        Alignment::HORIZONTAL_CENTER_CONTINUOUS => 6,
+        Alignment::HORIZONTAL_JUSTIFY => 5,
+    ];
+
+    /**
+     * @var array<string, int>
+     */
+    private static $verticalMap = [
+        Alignment::VERTICAL_BOTTOM => 2,
+        Alignment::VERTICAL_TOP => 0,
+        Alignment::VERTICAL_CENTER => 1,
+        Alignment::VERTICAL_JUSTIFY => 3,
+    ];
+
+    public static function horizontal(Alignment $alignment): int
+    {
+        $horizontalAlignment = $alignment->getHorizontal();
+
+        if (is_string($horizontalAlignment) && array_key_exists($horizontalAlignment, self::$horizontalMap)) {
+            return self::$horizontalMap[$horizontalAlignment];
+        }
+
+        return self::$horizontalMap[Alignment::HORIZONTAL_GENERAL];
+    }
+
+    public static function wrap(Alignment $alignment): int
+    {
+        $wrap = $alignment->getWrapText();
+
+        return ($wrap === true) ? 1 : 0;
+    }
+
+    public static function vertical(Alignment $alignment): int
+    {
+        $verticalAlignment = $alignment->getVertical();
+
+        if (is_string($verticalAlignment) && array_key_exists($verticalAlignment, self::$verticalMap)) {
+            return self::$verticalMap[$verticalAlignment];
+        }
+
+        return self::$verticalMap[Alignment::VERTICAL_BOTTOM];
+    }
+}

+ 40 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellBorder.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls\Style;
+
+use PhpOffice\PhpSpreadsheet\Style\Border;
+
+class CellBorder
+{
+    /**
+     * @var array<string, int>
+     */
+    protected static $styleMap = [
+        Border::BORDER_NONE => 0x00,
+        Border::BORDER_THIN => 0x01,
+        Border::BORDER_MEDIUM => 0x02,
+        Border::BORDER_DASHED => 0x03,
+        Border::BORDER_DOTTED => 0x04,
+        Border::BORDER_THICK => 0x05,
+        Border::BORDER_DOUBLE => 0x06,
+        Border::BORDER_HAIR => 0x07,
+        Border::BORDER_MEDIUMDASHED => 0x08,
+        Border::BORDER_DASHDOT => 0x09,
+        Border::BORDER_MEDIUMDASHDOT => 0x0A,
+        Border::BORDER_DASHDOTDOT => 0x0B,
+        Border::BORDER_MEDIUMDASHDOTDOT => 0x0C,
+        Border::BORDER_SLANTDASHDOT => 0x0D,
+        Border::BORDER_OMIT => 0x00,
+    ];
+
+    public static function style(Border $border): int
+    {
+        $borderStyle = $border->getBorderStyle();
+
+        if (is_string($borderStyle) && array_key_exists($borderStyle, self::$styleMap)) {
+            return self::$styleMap[$borderStyle];
+        }
+
+        return self::$styleMap[Border::BORDER_NONE];
+    }
+}

+ 46 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/CellFill.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls\Style;
+
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+
+class CellFill
+{
+    /**
+     * @var array<string, int>
+     */
+    protected static $fillStyleMap = [
+        Fill::FILL_NONE => 0x00,
+        Fill::FILL_SOLID => 0x01,
+        Fill::FILL_PATTERN_MEDIUMGRAY => 0x02,
+        Fill::FILL_PATTERN_DARKGRAY => 0x03,
+        Fill::FILL_PATTERN_LIGHTGRAY => 0x04,
+        Fill::FILL_PATTERN_DARKHORIZONTAL => 0x05,
+        Fill::FILL_PATTERN_DARKVERTICAL => 0x06,
+        Fill::FILL_PATTERN_DARKDOWN => 0x07,
+        Fill::FILL_PATTERN_DARKUP => 0x08,
+        Fill::FILL_PATTERN_DARKGRID => 0x09,
+        Fill::FILL_PATTERN_DARKTRELLIS => 0x0A,
+        Fill::FILL_PATTERN_LIGHTHORIZONTAL => 0x0B,
+        Fill::FILL_PATTERN_LIGHTVERTICAL => 0x0C,
+        Fill::FILL_PATTERN_LIGHTDOWN => 0x0D,
+        Fill::FILL_PATTERN_LIGHTUP => 0x0E,
+        Fill::FILL_PATTERN_LIGHTGRID => 0x0F,
+        Fill::FILL_PATTERN_LIGHTTRELLIS => 0x10,
+        Fill::FILL_PATTERN_GRAY125 => 0x11,
+        Fill::FILL_PATTERN_GRAY0625 => 0x12,
+        Fill::FILL_GRADIENT_LINEAR => 0x00, // does not exist in BIFF8
+        Fill::FILL_GRADIENT_PATH => 0x00,   // does not exist in BIFF8
+    ];
+
+    public static function style(Fill $fill): int
+    {
+        $fillStyle = $fill->getFillType();
+
+        if (is_string($fillStyle) && array_key_exists($fillStyle, self::$fillStyleMap)) {
+            return self::$fillStyleMap[$fillStyle];
+        }
+
+        return self::$fillStyleMap[Fill::FILL_NONE];
+    }
+}

+ 90 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Style/ColorMap.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls\Style;
+
+use PhpOffice\PhpSpreadsheet\Style\Color;
+
+class ColorMap
+{
+    /**
+     * @var array<string, int>
+     */
+    private static $colorMap = [
+        '#000000' => 0x08,
+        '#FFFFFF' => 0x09,
+        '#FF0000' => 0x0A,
+        '#00FF00' => 0x0B,
+        '#0000FF' => 0x0C,
+        '#FFFF00' => 0x0D,
+        '#FF00FF' => 0x0E,
+        '#00FFFF' => 0x0F,
+        '#800000' => 0x10,
+        '#008000' => 0x11,
+        '#000080' => 0x12,
+        '#808000' => 0x13,
+        '#800080' => 0x14,
+        '#008080' => 0x15,
+        '#C0C0C0' => 0x16,
+        '#808080' => 0x17,
+        '#9999FF' => 0x18,
+        '#993366' => 0x19,
+        '#FFFFCC' => 0x1A,
+        '#CCFFFF' => 0x1B,
+        '#660066' => 0x1C,
+        '#FF8080' => 0x1D,
+        '#0066CC' => 0x1E,
+        '#CCCCFF' => 0x1F,
+        //        '#000080' => 0x20,
+        //        '#FF00FF' => 0x21,
+        //        '#FFFF00' => 0x22,
+        //        '#00FFFF' => 0x23,
+        //        '#800080' => 0x24,
+        //        '#800000' => 0x25,
+        //        '#008080' => 0x26,
+        //        '#0000FF' => 0x27,
+        '#00CCFF' => 0x28,
+        //        '#CCFFFF' => 0x29,
+        '#CCFFCC' => 0x2A,
+        '#FFFF99' => 0x2B,
+        '#99CCFF' => 0x2C,
+        '#FF99CC' => 0x2D,
+        '#CC99FF' => 0x2E,
+        '#FFCC99' => 0x2F,
+        '#3366FF' => 0x30,
+        '#33CCCC' => 0x31,
+        '#99CC00' => 0x32,
+        '#FFCC00' => 0x33,
+        '#FF9900' => 0x34,
+        '#FF6600' => 0x35,
+        '#666699' => 0x36,
+        '#969696' => 0x37,
+        '#003366' => 0x38,
+        '#339966' => 0x39,
+        '#003300' => 0x3A,
+        '#333300' => 0x3B,
+        '#993300' => 0x3C,
+        //        '#993366' => 0x3D,
+        '#333399' => 0x3E,
+        '#333333' => 0x3F,
+    ];
+
+    public static function lookup(Color $color, int $defaultIndex = 0x00): int
+    {
+        $colorRgb = $color->getRGB();
+        if (is_string($colorRgb) && array_key_exists("#{$colorRgb}", self::$colorMap)) {
+            return self::$colorMap["#{$colorRgb}"];
+        }
+
+//      TODO Try and map RGB value to nearest colour within the define pallette
+//        $red =  Color::getRed($colorRgb, false);
+//        $green = Color::getGreen($colorRgb, false);
+//        $blue = Color::getBlue($colorRgb, false);
+
+//        $paletteSpace = 3;
+//        $newColor = ($red * $paletteSpace / 256) * ($paletteSpace * $paletteSpace) +
+//            ($green * $paletteSpace / 256) * $paletteSpace +
+//            ($blue * $paletteSpace / 256);
+
+        return $defaultIndex;
+    }
+}

+ 1190 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php

@@ -0,0 +1,1190 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\DefinedName;
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+use PhpOffice\PhpSpreadsheet\Shared\Date;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Style;
+
+// Original file header of PEAR::Spreadsheet_Excel_Writer_Workbook (used as the base for this class):
+// -----------------------------------------------------------------------------------------
+// /*
+// *  Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
+// *
+// *  The majority of this is _NOT_ my code.  I simply ported it from the
+// *  PERL Spreadsheet::WriteExcel module.
+// *
+// *  The author of the Spreadsheet::WriteExcel module is John McNamara
+// *  <jmcnamara@cpan.org>
+// *
+// *  I _DO_ maintain this code, and John McNamara has nothing to do with the
+// *  porting of this code to PHP.  Any questions directly related to this
+// *  class library should be directed to me.
+// *
+// *  License Information:
+// *
+// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
+// *    Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+// *
+// *    This library is free software; you can redistribute it and/or
+// *    modify it under the terms of the GNU Lesser General Public
+// *    License as published by the Free Software Foundation; either
+// *    version 2.1 of the License, or (at your option) any later version.
+// *
+// *    This library is distributed in the hope that it will be useful,
+// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// *    Lesser General Public License for more details.
+// *
+// *    You should have received a copy of the GNU Lesser General Public
+// *    License along with this library; if not, write to the Free Software
+// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+// */
+class Workbook extends BIFFwriter
+{
+    /**
+     * Formula parser.
+     *
+     * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
+     */
+    private $parser;
+
+    /**
+     * The BIFF file size for the workbook. Not currently used.
+     *
+     * @var int
+     *
+     * @see calcSheetOffsets()
+     */
+    private $biffSize; // @phpstan-ignore-line
+
+    /**
+     * XF Writers.
+     *
+     * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[]
+     */
+    private $xfWriters = [];
+
+    /**
+     * Array containing the colour palette.
+     *
+     * @var array
+     */
+    private $palette;
+
+    /**
+     * The codepage indicates the text encoding used for strings.
+     *
+     * @var int
+     */
+    private $codepage;
+
+    /**
+     * The country code used for localization.
+     *
+     * @var int
+     */
+    private $countryCode;
+
+    /**
+     * Workbook.
+     *
+     * @var Spreadsheet
+     */
+    private $spreadsheet;
+
+    /**
+     * Fonts writers.
+     *
+     * @var Font[]
+     */
+    private $fontWriters = [];
+
+    /**
+     * Added fonts. Maps from font's hash => index in workbook.
+     *
+     * @var array
+     */
+    private $addedFonts = [];
+
+    /**
+     * Shared number formats.
+     *
+     * @var array
+     */
+    private $numberFormats = [];
+
+    /**
+     * Added number formats. Maps from numberFormat's hash => index in workbook.
+     *
+     * @var array
+     */
+    private $addedNumberFormats = [];
+
+    /**
+     * Sizes of the binary worksheet streams.
+     *
+     * @var array
+     */
+    private $worksheetSizes = [];
+
+    /**
+     * Offsets of the binary worksheet streams relative to the start of the global workbook stream.
+     *
+     * @var array
+     */
+    private $worksheetOffsets = [];
+
+    /**
+     * Total number of shared strings in workbook.
+     *
+     * @var int
+     */
+    private $stringTotal;
+
+    /**
+     * Number of unique shared strings in workbook.
+     *
+     * @var int
+     */
+    private $stringUnique;
+
+    /**
+     * Array of unique shared strings in workbook.
+     *
+     * @var array
+     */
+    private $stringTable;
+
+    /**
+     * Color cache.
+     *
+     * @var array
+     */
+    private $colors;
+
+    /**
+     * Escher object corresponding to MSODRAWINGGROUP.
+     *
+     * @var null|\PhpOffice\PhpSpreadsheet\Shared\Escher
+     */
+    private $escher;
+
+    /** @var mixed */
+    private static $scrutinizerFalse = false;
+
+    /**
+     * Class constructor.
+     *
+     * @param Spreadsheet $spreadsheet The Workbook
+     * @param int $str_total Total number of strings
+     * @param int $str_unique Total number of unique strings
+     * @param array $str_table String Table
+     * @param array $colors Colour Table
+     * @param Parser $parser The formula parser created for the Workbook
+     */
+    public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser)
+    {
+        // It needs to call its parent's constructor explicitly
+        parent::__construct();
+
+        $this->parser = $parser;
+        $this->biffSize = 0;
+        $this->palette = [];
+        $this->countryCode = -1;
+
+        $this->stringTotal = &$str_total;
+        $this->stringUnique = &$str_unique;
+        $this->stringTable = &$str_table;
+        $this->colors = &$colors;
+        $this->setPaletteXl97();
+
+        $this->spreadsheet = $spreadsheet;
+
+        $this->codepage = 0x04B0;
+
+        // Add empty sheets and Build color cache
+        $countSheets = $spreadsheet->getSheetCount();
+        for ($i = 0; $i < $countSheets; ++$i) {
+            $phpSheet = $spreadsheet->getSheet($i);
+
+            $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser
+
+            $supbook_index = 0x00;
+            $ref = pack('vvv', $supbook_index, $i, $i);
+            $this->parser->references[] = $ref; // Register reference with parser
+
+            // Sheet tab colors?
+            if ($phpSheet->isTabColorSet()) {
+                $this->addColor($phpSheet->getTabColor()->getRGB());
+            }
+        }
+    }
+
+    /**
+     * Add a new XF writer.
+     *
+     * @param bool $isStyleXf Is it a style XF?
+     *
+     * @return int Index to XF record
+     */
+    public function addXfWriter(Style $style, $isStyleXf = false)
+    {
+        $xfWriter = new Xf($style);
+        $xfWriter->setIsStyleXf($isStyleXf);
+
+        // Add the font if not already added
+        $fontIndex = $this->addFont($style->getFont());
+
+        // Assign the font index to the xf record
+        $xfWriter->setFontIndex($fontIndex);
+
+        // Background colors, best to treat these after the font so black will come after white in custom palette
+        $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB()));
+        $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB()));
+        $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB()));
+        $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB()));
+        $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB()));
+        $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB()));
+        $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB()));
+
+        // Add the number format if it is not a built-in one and not already added
+        if ($style->getNumberFormat()->getBuiltInFormatCode() === self::$scrutinizerFalse) {
+            $numberFormatHashCode = $style->getNumberFormat()->getHashCode();
+
+            if (isset($this->addedNumberFormats[$numberFormatHashCode])) {
+                $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode];
+            } else {
+                $numberFormatIndex = 164 + count($this->numberFormats);
+                $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat();
+                $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex;
+            }
+        } else {
+            $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode();
+        }
+
+        // Assign the number format index to xf record
+        $xfWriter->setNumberFormatIndex($numberFormatIndex);
+
+        $this->xfWriters[] = $xfWriter;
+
+        return count($this->xfWriters) - 1;
+    }
+
+    /**
+     * Add a font to added fonts.
+     *
+     * @return int Index to FONT record
+     */
+    public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font)
+    {
+        $fontHashCode = $font->getHashCode();
+        if (isset($this->addedFonts[$fontHashCode])) {
+            $fontIndex = $this->addedFonts[$fontHashCode];
+        } else {
+            $countFonts = count($this->fontWriters);
+            $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1;
+
+            $fontWriter = new Font($font);
+            $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB()));
+            $this->fontWriters[] = $fontWriter;
+
+            $this->addedFonts[$fontHashCode] = $fontIndex;
+        }
+
+        return $fontIndex;
+    }
+
+    /**
+     * Alter color palette adding a custom color.
+     *
+     * @param string $rgb E.g. 'FF00AA'
+     *
+     * @return int Color index
+     */
+    private function addColor($rgb)
+    {
+        if (!isset($this->colors[$rgb])) {
+            $color =
+                [
+                    hexdec(substr($rgb, 0, 2)),
+                    hexdec(substr($rgb, 2, 2)),
+                    hexdec(substr($rgb, 4)),
+                    0,
+                ];
+            $colorIndex = array_search($color, $this->palette);
+            if ($colorIndex) {
+                $this->colors[$rgb] = $colorIndex;
+            } else {
+                if (count($this->colors) === 0) {
+                    $lastColor = 7;
+                } else {
+                    $lastColor = end($this->colors);
+                }
+                if ($lastColor < 57) {
+                    // then we add a custom color altering the palette
+                    $colorIndex = $lastColor + 1;
+                    $this->palette[$colorIndex] = $color;
+                    $this->colors[$rgb] = $colorIndex;
+                } else {
+                    // no room for more custom colors, just map to black
+                    $colorIndex = 0;
+                }
+            }
+        } else {
+            // fetch already added custom color
+            $colorIndex = $this->colors[$rgb];
+        }
+
+        return $colorIndex;
+    }
+
+    /**
+     * Sets the colour palette to the Excel 97+ default.
+     */
+    private function setPaletteXl97(): void
+    {
+        $this->palette = [
+            0x08 => [0x00, 0x00, 0x00, 0x00],
+            0x09 => [0xff, 0xff, 0xff, 0x00],
+            0x0A => [0xff, 0x00, 0x00, 0x00],
+            0x0B => [0x00, 0xff, 0x00, 0x00],
+            0x0C => [0x00, 0x00, 0xff, 0x00],
+            0x0D => [0xff, 0xff, 0x00, 0x00],
+            0x0E => [0xff, 0x00, 0xff, 0x00],
+            0x0F => [0x00, 0xff, 0xff, 0x00],
+            0x10 => [0x80, 0x00, 0x00, 0x00],
+            0x11 => [0x00, 0x80, 0x00, 0x00],
+            0x12 => [0x00, 0x00, 0x80, 0x00],
+            0x13 => [0x80, 0x80, 0x00, 0x00],
+            0x14 => [0x80, 0x00, 0x80, 0x00],
+            0x15 => [0x00, 0x80, 0x80, 0x00],
+            0x16 => [0xc0, 0xc0, 0xc0, 0x00],
+            0x17 => [0x80, 0x80, 0x80, 0x00],
+            0x18 => [0x99, 0x99, 0xff, 0x00],
+            0x19 => [0x99, 0x33, 0x66, 0x00],
+            0x1A => [0xff, 0xff, 0xcc, 0x00],
+            0x1B => [0xcc, 0xff, 0xff, 0x00],
+            0x1C => [0x66, 0x00, 0x66, 0x00],
+            0x1D => [0xff, 0x80, 0x80, 0x00],
+            0x1E => [0x00, 0x66, 0xcc, 0x00],
+            0x1F => [0xcc, 0xcc, 0xff, 0x00],
+            0x20 => [0x00, 0x00, 0x80, 0x00],
+            0x21 => [0xff, 0x00, 0xff, 0x00],
+            0x22 => [0xff, 0xff, 0x00, 0x00],
+            0x23 => [0x00, 0xff, 0xff, 0x00],
+            0x24 => [0x80, 0x00, 0x80, 0x00],
+            0x25 => [0x80, 0x00, 0x00, 0x00],
+            0x26 => [0x00, 0x80, 0x80, 0x00],
+            0x27 => [0x00, 0x00, 0xff, 0x00],
+            0x28 => [0x00, 0xcc, 0xff, 0x00],
+            0x29 => [0xcc, 0xff, 0xff, 0x00],
+            0x2A => [0xcc, 0xff, 0xcc, 0x00],
+            0x2B => [0xff, 0xff, 0x99, 0x00],
+            0x2C => [0x99, 0xcc, 0xff, 0x00],
+            0x2D => [0xff, 0x99, 0xcc, 0x00],
+            0x2E => [0xcc, 0x99, 0xff, 0x00],
+            0x2F => [0xff, 0xcc, 0x99, 0x00],
+            0x30 => [0x33, 0x66, 0xff, 0x00],
+            0x31 => [0x33, 0xcc, 0xcc, 0x00],
+            0x32 => [0x99, 0xcc, 0x00, 0x00],
+            0x33 => [0xff, 0xcc, 0x00, 0x00],
+            0x34 => [0xff, 0x99, 0x00, 0x00],
+            0x35 => [0xff, 0x66, 0x00, 0x00],
+            0x36 => [0x66, 0x66, 0x99, 0x00],
+            0x37 => [0x96, 0x96, 0x96, 0x00],
+            0x38 => [0x00, 0x33, 0x66, 0x00],
+            0x39 => [0x33, 0x99, 0x66, 0x00],
+            0x3A => [0x00, 0x33, 0x00, 0x00],
+            0x3B => [0x33, 0x33, 0x00, 0x00],
+            0x3C => [0x99, 0x33, 0x00, 0x00],
+            0x3D => [0x99, 0x33, 0x66, 0x00],
+            0x3E => [0x33, 0x33, 0x99, 0x00],
+            0x3F => [0x33, 0x33, 0x33, 0x00],
+        ];
+    }
+
+    /**
+     * Assemble worksheets into a workbook and send the BIFF data to an OLE
+     * storage.
+     *
+     * @param array $worksheetSizes The sizes in bytes of the binary worksheet streams
+     *
+     * @return string Binary data for workbook stream
+     */
+    public function writeWorkbook(array $worksheetSizes)
+    {
+        $this->worksheetSizes = $worksheetSizes;
+
+        // Calculate the number of selected worksheet tabs and call the finalization
+        // methods for each worksheet
+        $total_worksheets = $this->spreadsheet->getSheetCount();
+
+        // Add part 1 of the Workbook globals, what goes before the SHEET records
+        $this->storeBof(0x0005);
+        $this->writeCodepage();
+        $this->writeWindow1();
+
+        $this->writeDateMode();
+        $this->writeAllFonts();
+        $this->writeAllNumberFormats();
+        $this->writeAllXfs();
+        $this->writeAllStyles();
+        $this->writePalette();
+
+        // Prepare part 3 of the workbook global stream, what goes after the SHEET records
+        $part3 = '';
+        if ($this->countryCode !== -1) {
+            $part3 .= $this->writeCountry();
+        }
+        $part3 .= $this->writeRecalcId();
+
+        $part3 .= $this->writeSupbookInternal();
+        /* TODO: store external SUPBOOK records and XCT and CRN records
+        in case of external references for BIFF8 */
+        $part3 .= $this->writeExternalsheetBiff8();
+        $part3 .= $this->writeAllDefinedNamesBiff8();
+        $part3 .= $this->writeMsoDrawingGroup();
+        $part3 .= $this->writeSharedStringsTable();
+
+        $part3 .= $this->writeEof();
+
+        // Add part 2 of the Workbook globals, the SHEET records
+        $this->calcSheetOffsets();
+        for ($i = 0; $i < $total_worksheets; ++$i) {
+            $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]);
+        }
+
+        // Add part 3 of the Workbook globals
+        $this->_data .= $part3;
+
+        return $this->_data;
+    }
+
+    /**
+     * Calculate offsets for Worksheet BOF records.
+     */
+    private function calcSheetOffsets(): void
+    {
+        $boundsheet_length = 10; // fixed length for a BOUNDSHEET record
+
+        // size of Workbook globals part 1 + 3
+        $offset = $this->_datasize;
+
+        // add size of Workbook globals part 2, the length of the SHEET records
+        $total_worksheets = count($this->spreadsheet->getAllSheets());
+        foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) {
+            $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle()));
+        }
+
+        // add the sizes of each of the Sheet substreams, respectively
+        for ($i = 0; $i < $total_worksheets; ++$i) {
+            $this->worksheetOffsets[$i] = $offset;
+            $offset += $this->worksheetSizes[$i];
+        }
+        $this->biffSize = $offset;
+    }
+
+    /**
+     * Store the Excel FONT records.
+     */
+    private function writeAllFonts(): void
+    {
+        foreach ($this->fontWriters as $fontWriter) {
+            $this->append($fontWriter->writeFont());
+        }
+    }
+
+    /**
+     * Store user defined numerical formats i.e. FORMAT records.
+     */
+    private function writeAllNumberFormats(): void
+    {
+        foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) {
+            $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex);
+        }
+    }
+
+    /**
+     * Write all XF records.
+     */
+    private function writeAllXfs(): void
+    {
+        foreach ($this->xfWriters as $xfWriter) {
+            $this->append($xfWriter->writeXf());
+        }
+    }
+
+    /**
+     * Write all STYLE records.
+     */
+    private function writeAllStyles(): void
+    {
+        $this->writeStyle();
+    }
+
+    private function parseDefinedNameValue(DefinedName $definedName): string
+    {
+        $definedRange = $definedName->getValue();
+        $splitCount = preg_match_all(
+            '/' . Calculation::CALCULATION_REGEXP_CELLREF . '/mui',
+            $definedRange,
+            $splitRanges,
+            PREG_OFFSET_CAPTURE
+        );
+
+        $lengths = array_map('strlen', array_column($splitRanges[0], 0));
+        $offsets = array_column($splitRanges[0], 1);
+
+        $worksheets = $splitRanges[2];
+        $columns = $splitRanges[6];
+        $rows = $splitRanges[7];
+
+        while ($splitCount > 0) {
+            --$splitCount;
+            $length = $lengths[$splitCount];
+            $offset = $offsets[$splitCount];
+            $worksheet = $worksheets[$splitCount][0];
+            $column = $columns[$splitCount][0];
+            $row = $rows[$splitCount][0];
+
+            $newRange = '';
+            if (empty($worksheet)) {
+                if (($offset === 0) || ($definedRange[$offset - 1] !== ':')) {
+                    // We should have a worksheet
+                    $worksheet = $definedName->getWorksheet() ? $definedName->getWorksheet()->getTitle() : null;
+                }
+            } else {
+                $worksheet = str_replace("''", "'", trim($worksheet, "'"));
+            }
+            if (!empty($worksheet)) {
+                $newRange = "'" . str_replace("'", "''", $worksheet) . "'!";
+            }
+
+            if (!empty($column)) {
+                $newRange .= "\${$column}";
+            }
+            if (!empty($row)) {
+                $newRange .= "\${$row}";
+            }
+
+            $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length);
+        }
+
+        return $definedRange;
+    }
+
+    /**
+     * Writes all the DEFINEDNAME records (BIFF8).
+     * So far this is only used for repeating rows/columns (print titles) and print areas.
+     */
+    private function writeAllDefinedNamesBiff8(): string
+    {
+        $chunk = '';
+
+        // Named ranges
+        $definedNames = $this->spreadsheet->getDefinedNames();
+        if (count($definedNames) > 0) {
+            // Loop named ranges
+            foreach ($definedNames as $definedName) {
+                $range = $this->parseDefinedNameValue($definedName);
+
+                // parse formula
+                try {
+                    $this->parser->parse($range);
+                    $formulaData = $this->parser->toReversePolish();
+
+                    // make sure tRef3d is of type tRef3dR (0x3A)
+                    if (isset($formulaData[0]) && ($formulaData[0] == "\x7A" || $formulaData[0] == "\x5A")) {
+                        $formulaData = "\x3A" . substr($formulaData, 1);
+                    }
+
+                    if ($definedName->getLocalOnly()) {
+                        // local scope
+                        $scopeWs = $definedName->getScope();
+                        $scope = ($scopeWs === null) ? 0 : ($this->spreadsheet->getIndex($scopeWs) + 1);
+                    } else {
+                        // global scope
+                        $scope = 0;
+                    }
+                    $chunk .= $this->writeData($this->writeDefinedNameBiff8($definedName->getName(), $formulaData, $scope, false));
+                } catch (PhpSpreadsheetException $e) {
+                    // do nothing
+                }
+            }
+        }
+
+        // total number of sheets
+        $total_worksheets = $this->spreadsheet->getSheetCount();
+
+        // write the print titles (repeating rows, columns), if any
+        for ($i = 0; $i < $total_worksheets; ++$i) {
+            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
+            // simultaneous repeatColumns repeatRows
+            if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) {
+                $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
+                $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
+                $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
+
+                $repeat = $sheetSetup->getRowsToRepeatAtTop();
+                $rowmin = $repeat[0] - 1;
+                $rowmax = $repeat[1] - 1;
+
+                // construct formula data manually
+                $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc
+                $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d
+                $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d
+                $formulaData .= pack('C', 0x10); // tList
+
+                // store the DEFINEDNAME record
+                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
+            } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) {
+                // (exclusive) either repeatColumns or repeatRows.
+                // Columns to repeat
+                if ($sheetSetup->isColumnsToRepeatAtLeftSet()) {
+                    $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
+                    $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
+                    $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
+                } else {
+                    $colmin = 0;
+                    $colmax = 255;
+                }
+                // Rows to repeat
+                if ($sheetSetup->isRowsToRepeatAtTopSet()) {
+                    $repeat = $sheetSetup->getRowsToRepeatAtTop();
+                    $rowmin = $repeat[0] - 1;
+                    $rowmax = $repeat[1] - 1;
+                } else {
+                    $rowmin = 0;
+                    $rowmax = 65535;
+                }
+
+                // construct formula data manually because parser does not recognize absolute 3d cell references
+                $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax);
+
+                // store the DEFINEDNAME record
+                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
+            }
+        }
+
+        // write the print areas, if any
+        for ($i = 0; $i < $total_worksheets; ++$i) {
+            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
+            if ($sheetSetup->isPrintAreaSet()) {
+                // Print area, e.g. A3:J6,H1:X20
+                $printArea = Coordinate::splitRange($sheetSetup->getPrintArea());
+                $countPrintArea = count($printArea);
+
+                $formulaData = '';
+                for ($j = 0; $j < $countPrintArea; ++$j) {
+                    $printAreaRect = $printArea[$j]; // e.g. A3:J6
+                    $printAreaRect[0] = Coordinate::indexesFromString($printAreaRect[0]);
+                    $printAreaRect[1] = Coordinate::indexesFromString($printAreaRect[1]);
+
+                    $print_rowmin = $printAreaRect[0][1] - 1;
+                    $print_rowmax = $printAreaRect[1][1] - 1;
+                    $print_colmin = $printAreaRect[0][0] - 1;
+                    $print_colmax = $printAreaRect[1][0] - 1;
+
+                    // construct formula data manually because parser does not recognize absolute 3d cell references
+                    $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax);
+
+                    if ($j > 0) {
+                        $formulaData .= pack('C', 0x10); // list operator token ','
+                    }
+                }
+
+                // store the DEFINEDNAME record
+                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true));
+            }
+        }
+
+        // write autofilters, if any
+        for ($i = 0; $i < $total_worksheets; ++$i) {
+            $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter();
+            $autoFilterRange = $sheetAutoFilter->getRange();
+            if (!empty($autoFilterRange)) {
+                $rangeBounds = Coordinate::rangeBoundaries($autoFilterRange);
+
+                //Autofilter built in name
+                $name = pack('C', 0x0D);
+
+                $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true));
+            }
+        }
+
+        return $chunk;
+    }
+
+    /**
+     * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data.
+     *
+     * @param string $name The name in UTF-8
+     * @param string $formulaData The binary formula data
+     * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
+     * @param bool $isBuiltIn Built-in name?
+     *
+     * @return string Complete binary record data
+     */
+    private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false)
+    {
+        $record = 0x0018;
+
+        // option flags
+        $options = $isBuiltIn ? 0x20 : 0x00;
+
+        // length of the name, character count
+        $nlen = StringHelper::countCharacters($name);
+
+        // name with stripped length field
+        $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2);
+
+        // size of the formula (in bytes)
+        $sz = strlen($formulaData);
+
+        // combine the parts
+        $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0)
+            . $name . $formulaData;
+        $length = strlen($data);
+
+        $header = pack('vv', $record, $length);
+
+        return $header . $data;
+    }
+
+    /**
+     * Write a short NAME record.
+     *
+     * @param string $name
+     * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
+     * @param int[][] $rangeBounds range boundaries
+     * @param bool $isHidden
+     *
+     * @return string Complete binary record data
+     * */
+    private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false)
+    {
+        $record = 0x0018;
+
+        // option flags
+        $options = ($isHidden ? 0x21 : 0x00);
+
+        $extra = pack(
+            'Cvvvvv',
+            0x3B,
+            $sheetIndex - 1,
+            $rangeBounds[0][1] - 1,
+            $rangeBounds[1][1] - 1,
+            $rangeBounds[0][0] - 1,
+            $rangeBounds[1][0] - 1
+        );
+
+        // size of the formula (in bytes)
+        $sz = strlen($extra);
+
+        // combine the parts
+        $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0)
+            . $name . $extra;
+        $length = strlen($data);
+
+        $header = pack('vv', $record, $length);
+
+        return $header . $data;
+    }
+
+    /**
+     * Stores the CODEPAGE biff record.
+     */
+    private function writeCodepage(): void
+    {
+        $record = 0x0042; // Record identifier
+        $length = 0x0002; // Number of bytes to follow
+        $cv = $this->codepage; // The code page
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $cv);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write Excel BIFF WINDOW1 record.
+     */
+    private function writeWindow1(): void
+    {
+        $record = 0x003D; // Record identifier
+        $length = 0x0012; // Number of bytes to follow
+
+        $xWn = 0x0000; // Horizontal position of window
+        $yWn = 0x0000; // Vertical position of window
+        $dxWn = 0x25BC; // Width of window
+        $dyWn = 0x1572; // Height of window
+
+        $grbit = 0x0038; // Option flags
+
+        // not supported by PhpSpreadsheet, so there is only one selected sheet, the active
+        $ctabsel = 1; // Number of workbook tabs selected
+
+        $wTabRatio = 0x0258; // Tab to scrollbar ratio
+
+        // not supported by PhpSpreadsheet, set to 0
+        $itabFirst = 0; // 1st displayed worksheet
+        $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Writes Excel BIFF BOUNDSHEET record.
+     *
+     * @param int $offset Location of worksheet BOF
+     */
+    private function writeBoundSheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, $offset): void
+    {
+        $sheetname = $sheet->getTitle();
+        $record = 0x0085; // Record identifier
+
+        // sheet state
+        switch ($sheet->getSheetState()) {
+            case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VISIBLE:
+                $ss = 0x00;
+
+                break;
+            case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_HIDDEN:
+                $ss = 0x01;
+
+                break;
+            case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VERYHIDDEN:
+                $ss = 0x02;
+
+                break;
+            default:
+                $ss = 0x00;
+
+                break;
+        }
+
+        // sheet type
+        $st = 0x00;
+
+        //$grbit = 0x0000; // Visibility and sheet type
+
+        $data = pack('VCC', $offset, $ss, $st);
+        $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname);
+
+        $length = strlen($data);
+        $header = pack('vv', $record, $length);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write Internal SUPBOOK record.
+     */
+    private function writeSupbookInternal(): string
+    {
+        $record = 0x01AE; // Record identifier
+        $length = 0x0004; // Bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401);
+
+        return $this->writeData($header . $data);
+    }
+
+    /**
+     * Writes the Excel BIFF EXTERNSHEET record. These references are used by
+     * formulas.
+     */
+    private function writeExternalsheetBiff8(): string
+    {
+        $totalReferences = count($this->parser->references);
+        $record = 0x0017; // Record identifier
+        $length = 2 + 6 * $totalReferences; // Number of bytes to follow
+
+        //$supbook_index = 0; // FIXME: only using internal SUPBOOK record
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $totalReferences);
+        for ($i = 0; $i < $totalReferences; ++$i) {
+            $data .= $this->parser->references[$i];
+        }
+
+        return $this->writeData($header . $data);
+    }
+
+    /**
+     * Write Excel BIFF STYLE records.
+     */
+    private function writeStyle(): void
+    {
+        $record = 0x0293; // Record identifier
+        $length = 0x0004; // Bytes to follow
+
+        $ixfe = 0x8000; // Index to cell style XF
+        $BuiltIn = 0x00; // Built-in style
+        $iLevel = 0xff; // Outline style level
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vCC', $ixfe, $BuiltIn, $iLevel);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Writes Excel FORMAT record for non "built-in" numerical formats.
+     *
+     * @param string $format Custom format string
+     * @param int $ifmt Format index code
+     */
+    private function writeNumberFormat($format, $ifmt): void
+    {
+        $record = 0x041E; // Record identifier
+
+        $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format);
+        $length = 2 + strlen($numberFormatString); // Number of bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $ifmt) . $numberFormatString;
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write DATEMODE record to indicate the date system in use (1904 or 1900).
+     */
+    private function writeDateMode(): void
+    {
+        $record = 0x0022; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $f1904 = (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904)
+            ? 1
+            : 0; // Flag for 1904 date system
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $f1904);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Stores the COUNTRY record for localization.
+     *
+     * @return string
+     */
+    private function writeCountry()
+    {
+        $record = 0x008C; // Record identifier
+        $length = 4; // Number of bytes to follow
+
+        $header = pack('vv', $record, $length);
+        // using the same country code always for simplicity
+        $data = pack('vv', $this->countryCode, $this->countryCode);
+
+        return $this->writeData($header . $data);
+    }
+
+    /**
+     * Write the RECALCID record.
+     *
+     * @return string
+     */
+    private function writeRecalcId()
+    {
+        $record = 0x01C1; // Record identifier
+        $length = 8; // Number of bytes to follow
+
+        $header = pack('vv', $record, $length);
+
+        // by inspection of real Excel files, MS Office Excel 2007 writes this
+        $data = pack('VV', 0x000001C1, 0x00001E667);
+
+        return $this->writeData($header . $data);
+    }
+
+    /**
+     * Stores the PALETTE biff record.
+     */
+    private function writePalette(): void
+    {
+        $aref = $this->palette;
+
+        $record = 0x0092; // Record identifier
+        $length = 2 + 4 * count($aref); // Number of bytes to follow
+        $ccv = count($aref); // Number of RGB values to follow
+        $data = ''; // The RGB data
+
+        // Pack the RGB data
+        foreach ($aref as $color) {
+            foreach ($color as $byte) {
+                $data .= pack('C', $byte);
+            }
+        }
+
+        $header = pack('vvv', $record, $length, $ccv);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Handling of the SST continue blocks is complicated by the need to include an
+     * additional continuation byte depending on whether the string is split between
+     * blocks or whether it starts at the beginning of the block. (There are also
+     * additional complications that will arise later when/if Rich Strings are
+     * supported).
+     *
+     * The Excel documentation says that the SST record should be followed by an
+     * EXTSST record. The EXTSST record is a hash table that is used to optimise
+     * access to SST. However, despite the documentation it doesn't seem to be
+     * required so we will ignore it.
+     *
+     * @return string Binary data
+     */
+    private function writeSharedStringsTable()
+    {
+        // maximum size of record data (excluding record header)
+        $continue_limit = 8224;
+
+        // initialize array of record data blocks
+        $recordDatas = [];
+
+        // start SST record data block with total number of strings, total number of unique strings
+        $recordData = pack('VV', $this->stringTotal, $this->stringUnique);
+
+        // loop through all (unique) strings in shared strings table
+        foreach (array_keys($this->stringTable) as $string) {
+            // here $string is a BIFF8 encoded string
+
+            // length = character count
+            $headerinfo = unpack('vlength/Cencoding', $string);
+
+            // currently, this is always 1 = uncompressed
+            $encoding = $headerinfo['encoding'] ?? 1;
+
+            // initialize finished writing current $string
+            $finished = false;
+
+            while ($finished === false) {
+                // normally, there will be only one cycle, but if string cannot immediately be written as is
+                // there will be need for more than one cylcle, if string longer than one record data block, there
+                // may be need for even more cycles
+
+                if (strlen($recordData) + strlen($string) <= $continue_limit) {
+                    // then we can write the string (or remainder of string) without any problems
+                    $recordData .= $string;
+
+                    if (strlen($recordData) + strlen($string) == $continue_limit) {
+                        // we close the record data block, and initialize a new one
+                        $recordDatas[] = $recordData;
+                        $recordData = '';
+                    }
+
+                    // we are finished writing this string
+                    $finished = true;
+                } else {
+                    // special treatment writing the string (or remainder of the string)
+                    // If the string is very long it may need to be written in more than one CONTINUE record.
+
+                    // check how many bytes more there is room for in the current record
+                    $space_remaining = $continue_limit - strlen($recordData);
+
+                    // minimum space needed
+                    // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character
+                    // compressed:   2 byte string length length field + 1 byte option flags + 1 byte character
+                    $min_space_needed = ($encoding == 1) ? 5 : 4;
+
+                    // We have two cases
+                    // 1. space remaining is less than minimum space needed
+                    //        here we must waste the space remaining and move to next record data block
+                    // 2. space remaining is greater than or equal to minimum space needed
+                    //        here we write as much as we can in the current block, then move to next record data block
+
+                    if ($space_remaining < $min_space_needed) {
+                        // 1. space remaining is less than minimum space needed.
+                        // we close the block, store the block data
+                        $recordDatas[] = $recordData;
+
+                        // and start new record data block where we start writing the string
+                        $recordData = '';
+                    } else {
+                        // 2. space remaining is greater than or equal to minimum space needed.
+                        // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below
+                        $effective_space_remaining = $space_remaining;
+
+                        // for uncompressed strings, sometimes effective space remaining is reduced by 1
+                        if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) {
+                            --$effective_space_remaining;
+                        }
+
+                        // one block fininshed, store the block data
+                        $recordData .= substr($string, 0, $effective_space_remaining);
+
+                        $string = substr($string, $effective_space_remaining); // for next cycle in while loop
+                        $recordDatas[] = $recordData;
+
+                        // start new record data block with the repeated option flags
+                        $recordData = pack('C', $encoding);
+                    }
+                }
+            }
+        }
+
+        // Store the last record data block unless it is empty
+        // if there was no need for any continue records, this will be the for SST record data block itself
+        if (strlen($recordData) > 0) {
+            $recordDatas[] = $recordData;
+        }
+
+        // combine into one chunk with all the blocks SST, CONTINUE,...
+        $chunk = '';
+        foreach ($recordDatas as $i => $recordData) {
+            // first block should have the SST record header, remaing should have CONTINUE header
+            $record = ($i == 0) ? 0x00FC : 0x003C;
+
+            $header = pack('vv', $record, strlen($recordData));
+            $data = $header . $recordData;
+
+            $chunk .= $this->writeData($data);
+        }
+
+        return $chunk;
+    }
+
+    /**
+     * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records.
+     */
+    private function writeMsoDrawingGroup(): string
+    {
+        // write the Escher stream if necessary
+        if (isset($this->escher)) {
+            $writer = new Escher($this->escher);
+            $data = $writer->close();
+
+            $record = 0x00EB;
+            $length = strlen($data);
+            $header = pack('vv', $record, $length);
+
+            return $this->writeData($header . $data);
+        }
+
+        return '';
+    }
+
+    /**
+     * Get Escher object.
+     */
+    public function getEscher(): ?\PhpOffice\PhpSpreadsheet\Shared\Escher
+    {
+        return $this->escher;
+    }
+
+    /**
+     * Set Escher object.
+     */
+    public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher): void
+    {
+        $this->escher = $escher;
+    }
+}

+ 3218 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Worksheet.php

@@ -0,0 +1,3218 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use GdImage;
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
+use PhpOffice\PhpSpreadsheet\RichText\RichText;
+use PhpOffice\PhpSpreadsheet\RichText\Run;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Shared\Xls;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Color;
+use PhpOffice\PhpSpreadsheet\Style\Conditional;
+use PhpOffice\PhpSpreadsheet\Style\Protection;
+use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
+use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+
+// Original file header of PEAR::Spreadsheet_Excel_Writer_Worksheet (used as the base for this class):
+// -----------------------------------------------------------------------------------------
+// /*
+// *  Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
+// *
+// *  The majority of this is _NOT_ my code.  I simply ported it from the
+// *  PERL Spreadsheet::WriteExcel module.
+// *
+// *  The author of the Spreadsheet::WriteExcel module is John McNamara
+// *  <jmcnamara@cpan.org>
+// *
+// *  I _DO_ maintain this code, and John McNamara has nothing to do with the
+// *  porting of this code to PHP.  Any questions directly related to this
+// *  class library should be directed to me.
+// *
+// *  License Information:
+// *
+// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
+// *    Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+// *
+// *    This library is free software; you can redistribute it and/or
+// *    modify it under the terms of the GNU Lesser General Public
+// *    License as published by the Free Software Foundation; either
+// *    version 2.1 of the License, or (at your option) any later version.
+// *
+// *    This library is distributed in the hope that it will be useful,
+// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// *    Lesser General Public License for more details.
+// *
+// *    You should have received a copy of the GNU Lesser General Public
+// *    License along with this library; if not, write to the Free Software
+// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+// */
+class Worksheet extends BIFFwriter
+{
+    /** @var int */
+    private static $always0 = 0;
+
+    /** @var int */
+    private static $always1 = 1;
+
+    /**
+     * Formula parser.
+     *
+     * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
+     */
+    private $parser;
+
+    /**
+     * Array containing format information for columns.
+     *
+     * @var array
+     */
+    private $columnInfo;
+
+    /**
+     * The active pane for the worksheet.
+     *
+     * @var int
+     */
+    private $activePane;
+
+    /**
+     * Whether to use outline.
+     *
+     * @var bool
+     */
+    private $outlineOn;
+
+    /**
+     * Auto outline styles.
+     *
+     * @var bool
+     */
+    private $outlineStyle;
+
+    /**
+     * Whether to have outline summary below.
+     * Not currently used.
+     *
+     * @var bool
+     */
+    private $outlineBelow; //* @phpstan-ignore-line
+
+    /**
+     * Whether to have outline summary at the right.
+     * Not currently used.
+     *
+     * @var bool
+     */
+    private $outlineRight; //* @phpstan-ignore-line
+
+    /**
+     * Reference to the total number of strings in the workbook.
+     *
+     * @var int
+     */
+    private $stringTotal;
+
+    /**
+     * Reference to the number of unique strings in the workbook.
+     *
+     * @var int
+     */
+    private $stringUnique;
+
+    /**
+     * Reference to the array containing all the unique strings in the workbook.
+     *
+     * @var array
+     */
+    private $stringTable;
+
+    /**
+     * Color cache.
+     *
+     * @var array
+     */
+    private $colors;
+
+    /**
+     * Index of first used row (at least 0).
+     *
+     * @var int
+     */
+    private $firstRowIndex;
+
+    /**
+     * Index of last used row. (no used rows means -1).
+     *
+     * @var int
+     */
+    private $lastRowIndex;
+
+    /**
+     * Index of first used column (at least 0).
+     *
+     * @var int
+     */
+    private $firstColumnIndex;
+
+    /**
+     * Index of last used column (no used columns means -1).
+     *
+     * @var int
+     */
+    private $lastColumnIndex;
+
+    /**
+     * Sheet object.
+     *
+     * @var \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
+     */
+    public $phpSheet;
+
+    /**
+     * Escher object corresponding to MSODRAWING.
+     *
+     * @var null|\PhpOffice\PhpSpreadsheet\Shared\Escher
+     */
+    private $escher;
+
+    /**
+     * Array of font hashes associated to FONT records index.
+     *
+     * @var array
+     */
+    public $fontHashIndex;
+
+    /**
+     * @var bool
+     */
+    private $preCalculateFormulas;
+
+    /**
+     * @var int
+     */
+    private $printHeaders;
+
+    /**
+     * Constructor.
+     *
+     * @param int $str_total Total number of strings
+     * @param int $str_unique Total number of unique strings
+     * @param array $str_table String Table
+     * @param array $colors Colour Table
+     * @param Parser $parser The formula parser created for the Workbook
+     * @param bool $preCalculateFormulas Flag indicating whether formulas should be calculated or just written
+     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet The worksheet to write
+     */
+    public function __construct(&$str_total, &$str_unique, &$str_table, &$colors, Parser $parser, $preCalculateFormulas, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet)
+    {
+        // It needs to call its parent's constructor explicitly
+        parent::__construct();
+
+        $this->preCalculateFormulas = $preCalculateFormulas;
+        $this->stringTotal = &$str_total;
+        $this->stringUnique = &$str_unique;
+        $this->stringTable = &$str_table;
+        $this->colors = &$colors;
+        $this->parser = $parser;
+
+        $this->phpSheet = $phpSheet;
+
+        $this->columnInfo = [];
+        $this->activePane = 3;
+
+        $this->printHeaders = 0;
+
+        $this->outlineStyle = false;
+        $this->outlineBelow = true;
+        $this->outlineRight = true;
+        $this->outlineOn = true;
+
+        $this->fontHashIndex = [];
+
+        // calculate values for DIMENSIONS record
+        $minR = 1;
+        $minC = 'A';
+
+        $maxR = $this->phpSheet->getHighestRow();
+        $maxC = $this->phpSheet->getHighestColumn();
+
+        // Determine lowest and highest column and row
+        $this->firstRowIndex = $minR;
+        $this->lastRowIndex = ($maxR > 65535) ? 65535 : $maxR;
+
+        $this->firstColumnIndex = Coordinate::columnIndexFromString($minC);
+        $this->lastColumnIndex = Coordinate::columnIndexFromString($maxC);
+
+        if ($this->lastColumnIndex > 255) {
+            $this->lastColumnIndex = 255;
+        }
+    }
+
+    /**
+     * Add data to the beginning of the workbook (note the reverse order)
+     * and to the end of the workbook.
+     *
+     * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::storeWorkbook()
+     */
+    public function close(): void
+    {
+        $phpSheet = $this->phpSheet;
+
+        // Storing selected cells and active sheet because it changes while parsing cells with formulas.
+        $selectedCells = $this->phpSheet->getSelectedCells();
+        $activeSheetIndex = $this->phpSheet->getParentOrThrow()->getActiveSheetIndex();
+
+        // Write BOF record
+        $this->storeBof(0x0010);
+
+        // Write PRINTHEADERS
+        $this->writePrintHeaders();
+
+        // Write PRINTGRIDLINES
+        $this->writePrintGridlines();
+
+        // Write GRIDSET
+        $this->writeGridset();
+
+        // Calculate column widths
+        $phpSheet->calculateColumnWidths();
+
+        // Column dimensions
+        if (($defaultWidth = $phpSheet->getDefaultColumnDimension()->getWidth()) < 0) {
+            $defaultWidth = \PhpOffice\PhpSpreadsheet\Shared\Font::getDefaultColumnWidthByFont($phpSheet->getParentOrThrow()->getDefaultStyle()->getFont());
+        }
+
+        $columnDimensions = $phpSheet->getColumnDimensions();
+        $maxCol = $this->lastColumnIndex - 1;
+        for ($i = 0; $i <= $maxCol; ++$i) {
+            $hidden = 0;
+            $level = 0;
+            $xfIndex = 15; // there are 15 cell style Xfs
+
+            $width = $defaultWidth;
+
+            $columnLetter = Coordinate::stringFromColumnIndex($i + 1);
+            if (isset($columnDimensions[$columnLetter])) {
+                $columnDimension = $columnDimensions[$columnLetter];
+                if ($columnDimension->getWidth() >= 0) {
+                    $width = $columnDimension->getWidth();
+                }
+                $hidden = $columnDimension->getVisible() ? 0 : 1;
+                $level = $columnDimension->getOutlineLevel();
+                $xfIndex = $columnDimension->getXfIndex() + 15; // there are 15 cell style Xfs
+            }
+
+            // Components of columnInfo:
+            // $firstcol first column on the range
+            // $lastcol  last column on the range
+            // $width    width to set
+            // $xfIndex  The optional cell style Xf index to apply to the columns
+            // $hidden   The optional hidden atribute
+            // $level    The optional outline level
+            $this->columnInfo[] = [$i, $i, $width, $xfIndex, $hidden, $level];
+        }
+
+        // Write GUTS
+        $this->writeGuts();
+
+        // Write DEFAULTROWHEIGHT
+        $this->writeDefaultRowHeight();
+        // Write WSBOOL
+        $this->writeWsbool();
+        // Write horizontal and vertical page breaks
+        $this->writeBreaks();
+        // Write page header
+        $this->writeHeader();
+        // Write page footer
+        $this->writeFooter();
+        // Write page horizontal centering
+        $this->writeHcenter();
+        // Write page vertical centering
+        $this->writeVcenter();
+        // Write left margin
+        $this->writeMarginLeft();
+        // Write right margin
+        $this->writeMarginRight();
+        // Write top margin
+        $this->writeMarginTop();
+        // Write bottom margin
+        $this->writeMarginBottom();
+        // Write page setup
+        $this->writeSetup();
+        // Write sheet protection
+        $this->writeProtect();
+        // Write SCENPROTECT
+        $this->writeScenProtect();
+        // Write OBJECTPROTECT
+        $this->writeObjectProtect();
+        // Write sheet password
+        $this->writePassword();
+        // Write DEFCOLWIDTH record
+        $this->writeDefcol();
+
+        // Write the COLINFO records if they exist
+        if (!empty($this->columnInfo)) {
+            $colcount = count($this->columnInfo);
+            for ($i = 0; $i < $colcount; ++$i) {
+                $this->writeColinfo($this->columnInfo[$i]);
+            }
+        }
+        $autoFilterRange = $phpSheet->getAutoFilter()->getRange();
+        if (!empty($autoFilterRange)) {
+            // Write AUTOFILTERINFO
+            $this->writeAutoFilterInfo();
+        }
+
+        // Write sheet dimensions
+        $this->writeDimensions();
+
+        // Row dimensions
+        foreach ($phpSheet->getRowDimensions() as $rowDimension) {
+            $xfIndex = $rowDimension->getXfIndex() + 15; // there are 15 cellXfs
+            $this->writeRow(
+                $rowDimension->getRowIndex() - 1,
+                (int) $rowDimension->getRowHeight(),
+                $xfIndex,
+                !$rowDimension->getVisible(),
+                $rowDimension->getOutlineLevel()
+            );
+        }
+
+        // Write Cells
+        foreach ($phpSheet->getCellCollection()->getSortedCoordinates() as $coordinate) {
+            /** @var Cell $cell */
+            $cell = $phpSheet->getCellCollection()->get($coordinate);
+            $row = $cell->getRow() - 1;
+            $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
+
+            // Don't break Excel break the code!
+            if ($row > 65535 || $column > 255) {
+                throw new WriterException('Rows or columns overflow! Excel5 has limit to 65535 rows and 255 columns. Use XLSX instead.');
+            }
+
+            // Write cell value
+            $xfIndex = $cell->getXfIndex() + 15; // there are 15 cell style Xfs
+
+            $cVal = $cell->getValue();
+            if ($cVal instanceof RichText) {
+                $arrcRun = [];
+                $str_pos = 0;
+                $elements = $cVal->getRichTextElements();
+                foreach ($elements as $element) {
+                    // FONT Index
+                    $str_fontidx = 0;
+                    if ($element instanceof Run) {
+                        $getFont = $element->getFont();
+                        if ($getFont !== null) {
+                            $str_fontidx = $this->fontHashIndex[$getFont->getHashCode()];
+                        }
+                    }
+                    $arrcRun[] = ['strlen' => $str_pos, 'fontidx' => $str_fontidx];
+                    // Position FROM
+                    $str_pos += StringHelper::countCharacters($element->getText(), 'UTF-8');
+                }
+                $this->writeRichTextString($row, $column, $cVal->getPlainText(), $xfIndex, $arrcRun);
+            } else {
+                switch ($cell->getDatatype()) {
+                    case DataType::TYPE_STRING:
+                    case DataType::TYPE_INLINE:
+                    case DataType::TYPE_NULL:
+                        if ($cVal === '' || $cVal === null) {
+                            $this->writeBlank($row, $column, $xfIndex);
+                        } else {
+                            $this->writeString($row, $column, $cVal, $xfIndex);
+                        }
+
+                        break;
+                    case DataType::TYPE_NUMERIC:
+                        $this->writeNumber($row, $column, $cVal, $xfIndex);
+
+                        break;
+                    case DataType::TYPE_FORMULA:
+                        $calculatedValue = $this->preCalculateFormulas ?
+                            $cell->getCalculatedValue() : null;
+                        if (self::WRITE_FORMULA_EXCEPTION == $this->writeFormula($row, $column, $cVal, $xfIndex, $calculatedValue)) {
+                            if ($calculatedValue === null) {
+                                $calculatedValue = $cell->getCalculatedValue();
+                            }
+                            $calctype = gettype($calculatedValue);
+                            switch ($calctype) {
+                                case 'integer':
+                                case 'double':
+                                    $this->writeNumber($row, $column, (float) $calculatedValue, $xfIndex);
+
+                                    break;
+                                case 'string':
+                                    $this->writeString($row, $column, $calculatedValue, $xfIndex);
+
+                                    break;
+                                case 'boolean':
+                                    $this->writeBoolErr($row, $column, (int) $calculatedValue, 0, $xfIndex);
+
+                                    break;
+                                default:
+                                    $this->writeString($row, $column, $cVal, $xfIndex);
+                            }
+                        }
+
+                        break;
+                    case DataType::TYPE_BOOL:
+                        $this->writeBoolErr($row, $column, $cVal, 0, $xfIndex);
+
+                        break;
+                    case DataType::TYPE_ERROR:
+                        $this->writeBoolErr($row, $column, ErrorCode::error($cVal), 1, $xfIndex);
+
+                        break;
+                }
+            }
+        }
+
+        // Append
+        $this->writeMsoDrawing();
+
+        // Restoring active sheet.
+        $this->phpSheet->getParentOrThrow()->setActiveSheetIndex($activeSheetIndex);
+
+        // Write WINDOW2 record
+        $this->writeWindow2();
+
+        // Write PLV record
+        $this->writePageLayoutView();
+
+        // Write ZOOM record
+        $this->writeZoom();
+        if ($phpSheet->getFreezePane()) {
+            $this->writePanes();
+        }
+
+        // Restoring selected cells.
+        $this->phpSheet->setSelectedCells($selectedCells);
+
+        // Write SELECTION record
+        $this->writeSelection();
+
+        // Write MergedCellsTable Record
+        $this->writeMergedCells();
+
+        // Hyperlinks
+        $phpParent = $phpSheet->getParent();
+        $hyperlinkbase = ($phpParent === null) ? '' : $phpParent->getProperties()->getHyperlinkBase();
+        foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) {
+            [$column, $row] = Coordinate::indexesFromString($coordinate);
+
+            $url = $hyperlink->getUrl();
+
+            if (strpos($url, 'sheet://') !== false) {
+                // internal to current workbook
+                $url = str_replace('sheet://', 'internal:', $url);
+            } elseif (preg_match('/^(http:|https:|ftp:|mailto:)/', $url)) {
+                // URL
+            } elseif (!empty($hyperlinkbase) && preg_match('~^([A-Za-z]:)?[/\\\\]~', $url) !== 1) {
+                $url = "$hyperlinkbase$url";
+                if (preg_match('/^(http:|https:|ftp:|mailto:)/', $url) !== 1) {
+                    $url = 'external:' . $url;
+                }
+            } else {
+                // external (local file)
+                $url = 'external:' . $url;
+            }
+
+            $this->writeUrl($row - 1, $column - 1, $url);
+        }
+
+        $this->writeDataValidity();
+        $this->writeSheetLayout();
+
+        // Write SHEETPROTECTION record
+        $this->writeSheetProtection();
+        $this->writeRangeProtection();
+
+        // Write Conditional Formatting Rules and Styles
+        $this->writeConditionalFormatting();
+
+        $this->storeEof();
+    }
+
+    private function writeConditionalFormatting(): void
+    {
+        $conditionalFormulaHelper = new ConditionalHelper($this->parser);
+
+        $arrConditionalStyles = $this->phpSheet->getConditionalStylesCollection();
+        if (!empty($arrConditionalStyles)) {
+            $arrConditional = [];
+
+            // Write ConditionalFormattingTable records
+            foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) {
+                $cfHeaderWritten = false;
+                foreach ($conditionalStyles as $conditional) {
+                    /** @var Conditional $conditional */
+                    if (
+                        $conditional->getConditionType() === Conditional::CONDITION_EXPRESSION ||
+                        $conditional->getConditionType() === Conditional::CONDITION_CELLIS
+                    ) {
+                        // Write CFHEADER record (only if there are Conditional Styles that we are able to write)
+                        if ($cfHeaderWritten === false) {
+                            $cfHeaderWritten = $this->writeCFHeader($cellCoordinate, $conditionalStyles);
+                        }
+                        if ($cfHeaderWritten === true && !isset($arrConditional[$conditional->getHashCode()])) {
+                            // This hash code has been handled
+                            $arrConditional[$conditional->getHashCode()] = true;
+
+                            // Write CFRULE record
+                            $this->writeCFRule($conditionalFormulaHelper, $conditional, $cellCoordinate);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Write a cell range address in BIFF8
+     * always fixed range
+     * See section 2.5.14 in OpenOffice.org's Documentation of the Microsoft Excel File Format.
+     *
+     * @param string $range E.g. 'A1' or 'A1:B6'
+     *
+     * @return string Binary data
+     */
+    private function writeBIFF8CellRangeAddressFixed($range)
+    {
+        $explodes = explode(':', $range);
+
+        // extract first cell, e.g. 'A1'
+        $firstCell = $explodes[0];
+
+        // extract last cell, e.g. 'B6'
+        if (count($explodes) == 1) {
+            $lastCell = $firstCell;
+        } else {
+            $lastCell = $explodes[1];
+        }
+
+        $firstCellCoordinates = Coordinate::indexesFromString($firstCell); // e.g. [0, 1]
+        $lastCellCoordinates = Coordinate::indexesFromString($lastCell); // e.g. [1, 6]
+
+        return pack('vvvv', $firstCellCoordinates[1] - 1, $lastCellCoordinates[1] - 1, $firstCellCoordinates[0] - 1, $lastCellCoordinates[0] - 1);
+    }
+
+    /**
+     * Retrieves data from memory in one chunk, or from disk
+     * sized chunks.
+     *
+     * @return string The data
+     */
+    public function getData()
+    {
+        // Return data stored in memory
+        if (isset($this->_data)) {
+            $tmp = $this->_data;
+            $this->_data = null;
+
+            return $tmp;
+        }
+
+        // No data to return
+        return '';
+    }
+
+    /**
+     * Set the option to print the row and column headers on the printed page.
+     *
+     * @param int $print Whether to print the headers or not. Defaults to 1 (print).
+     */
+    public function printRowColHeaders($print = 1): void
+    {
+        $this->printHeaders = $print;
+    }
+
+    /**
+     * This method sets the properties for outlining and grouping. The defaults
+     * correspond to Excel's defaults.
+     *
+     * @param bool $visible
+     * @param bool $symbols_below
+     * @param bool $symbols_right
+     * @param bool $auto_style
+     */
+    public function setOutline($visible = true, $symbols_below = true, $symbols_right = true, $auto_style = false): void
+    {
+        $this->outlineOn = $visible;
+        $this->outlineBelow = $symbols_below;
+        $this->outlineRight = $symbols_right;
+        $this->outlineStyle = $auto_style;
+    }
+
+    /**
+     * Write a double to the specified row and column (zero indexed).
+     * An integer can be written as a double. Excel will display an
+     * integer. $format is optional.
+     *
+     * Returns  0 : normal termination
+     *         -2 : row or column out of range
+     *
+     * @param int $row Zero indexed row
+     * @param int $col Zero indexed column
+     * @param float $num The number to write
+     * @param mixed $xfIndex The optional XF format
+     *
+     * @return int
+     */
+    private function writeNumber($row, $col, $num, $xfIndex)
+    {
+        $record = 0x0203; // Record identifier
+        $length = 0x000E; // Number of bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvv', $row, $col, $xfIndex);
+        $xl_double = pack('d', $num);
+        if (self::getByteOrder()) { // if it's Big Endian
+            $xl_double = strrev($xl_double);
+        }
+
+        $this->append($header . $data . $xl_double);
+
+        return 0;
+    }
+
+    /**
+     * Write a LABELSST record or a LABEL record. Which one depends on BIFF version.
+     *
+     * @param int $row Row index (0-based)
+     * @param int $col Column index (0-based)
+     * @param string $str The string
+     * @param int $xfIndex Index to XF record
+     */
+    private function writeString($row, $col, $str, $xfIndex): void
+    {
+        $this->writeLabelSst($row, $col, $str, $xfIndex);
+    }
+
+    /**
+     * Write a LABELSST record or a LABEL record. Which one depends on BIFF version
+     * It differs from writeString by the writing of rich text strings.
+     *
+     * @param int $row Row index (0-based)
+     * @param int $col Column index (0-based)
+     * @param string $str The string
+     * @param int $xfIndex The XF format index for the cell
+     * @param array $arrcRun Index to Font record and characters beginning
+     */
+    private function writeRichTextString($row, $col, $str, $xfIndex, $arrcRun): void
+    {
+        $record = 0x00FD; // Record identifier
+        $length = 0x000A; // Bytes to follow
+        $str = StringHelper::UTF8toBIFF8UnicodeShort($str, $arrcRun);
+
+        // check if string is already present
+        if (!isset($this->stringTable[$str])) {
+            $this->stringTable[$str] = $this->stringUnique++;
+        }
+        ++$this->stringTotal;
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write a string to the specified row and column (zero indexed).
+     * This is the BIFF8 version (no 255 chars limit).
+     * $format is optional.
+     *
+     * @param int $row Zero indexed row
+     * @param int $col Zero indexed column
+     * @param string $str The string to write
+     * @param mixed $xfIndex The XF format index for the cell
+     */
+    private function writeLabelSst($row, $col, $str, $xfIndex): void
+    {
+        $record = 0x00FD; // Record identifier
+        $length = 0x000A; // Bytes to follow
+
+        $str = StringHelper::UTF8toBIFF8UnicodeLong($str);
+
+        // check if string is already present
+        if (!isset($this->stringTable[$str])) {
+            $this->stringTable[$str] = $this->stringUnique++;
+        }
+        ++$this->stringTotal;
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write a blank cell to the specified row and column (zero indexed).
+     * A blank cell is used to specify formatting without adding a string
+     * or a number.
+     *
+     * A blank cell without a format serves no purpose. Therefore, we don't write
+     * a BLANK record unless a format is specified.
+     *
+     * Returns  0 : normal termination (including no format)
+     *         -1 : insufficient number of arguments
+     *         -2 : row or column out of range
+     *
+     * @param int $row Zero indexed row
+     * @param int $col Zero indexed column
+     * @param mixed $xfIndex The XF format index
+     *
+     * @return int
+     */
+    public function writeBlank($row, $col, $xfIndex)
+    {
+        $record = 0x0201; // Record identifier
+        $length = 0x0006; // Number of bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvv', $row, $col, $xfIndex);
+        $this->append($header . $data);
+
+        return 0;
+    }
+
+    /**
+     * Write a boolean or an error type to the specified row and column (zero indexed).
+     *
+     * @param int $row Row index (0-based)
+     * @param int $col Column index (0-based)
+     * @param int $value
+     * @param int $isError Error or Boolean?
+     * @param int $xfIndex
+     *
+     * @return int
+     */
+    private function writeBoolErr($row, $col, $value, $isError, $xfIndex)
+    {
+        $record = 0x0205;
+        $length = 8;
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvCC', $row, $col, $xfIndex, $value, $isError);
+        $this->append($header . $data);
+
+        return 0;
+    }
+
+    const WRITE_FORMULA_NORMAL = 0;
+    const WRITE_FORMULA_ERRORS = -1;
+    const WRITE_FORMULA_RANGE = -2;
+    const WRITE_FORMULA_EXCEPTION = -3;
+
+    /** @var bool */
+    private static $allowThrow = false;
+
+    public static function setAllowThrow(bool $allowThrow): void
+    {
+        self::$allowThrow = $allowThrow;
+    }
+
+    public static function getAllowThrow(): bool
+    {
+        return self::$allowThrow;
+    }
+
+    /**
+     * Write a formula to the specified row and column (zero indexed).
+     * The textual representation of the formula is passed to the parser in
+     * Parser.php which returns a packed binary string.
+     *
+     * Returns  0 : WRITE_FORMULA_NORMAL  normal termination
+     *         -1 : WRITE_FORMULA_ERRORS formula errors (bad formula)
+     *         -2 : WRITE_FORMULA_RANGE  row or column out of range
+     *         -3 : WRITE_FORMULA_EXCEPTION parse raised exception, probably due to definedname
+     *
+     * @param int $row Zero indexed row
+     * @param int $col Zero indexed column
+     * @param string $formula The formula text string
+     * @param mixed $xfIndex The XF format index
+     * @param mixed $calculatedValue Calculated value
+     *
+     * @return int
+     */
+    private function writeFormula($row, $col, $formula, $xfIndex, $calculatedValue)
+    {
+        $record = 0x0006; // Record identifier
+        // Initialize possible additional value for STRING record that should be written after the FORMULA record?
+        $stringValue = null;
+
+        // calculated value
+        if (isset($calculatedValue)) {
+            // Since we can't yet get the data type of the calculated value,
+            // we use best effort to determine data type
+            if (is_bool($calculatedValue)) {
+                // Boolean value
+                $num = pack('CCCvCv', 0x01, 0x00, (int) $calculatedValue, 0x00, 0x00, 0xFFFF);
+            } elseif (is_int($calculatedValue) || is_float($calculatedValue)) {
+                // Numeric value
+                $num = pack('d', $calculatedValue);
+            } elseif (is_string($calculatedValue)) {
+                $errorCodes = DataType::getErrorCodes();
+                if (isset($errorCodes[$calculatedValue])) {
+                    // Error value
+                    $num = pack('CCCvCv', 0x02, 0x00, ErrorCode::error($calculatedValue), 0x00, 0x00, 0xFFFF);
+                } elseif ($calculatedValue === '') {
+                    // Empty string (and BIFF8)
+                    $num = pack('CCCvCv', 0x03, 0x00, 0x00, 0x00, 0x00, 0xFFFF);
+                } else {
+                    // Non-empty string value (or empty string BIFF5)
+                    $stringValue = $calculatedValue;
+                    $num = pack('CCCvCv', 0x00, 0x00, 0x00, 0x00, 0x00, 0xFFFF);
+                }
+            } else {
+                // We are really not supposed to reach here
+                $num = pack('d', 0x00);
+            }
+        } else {
+            $num = pack('d', 0x00);
+        }
+
+        $grbit = 0x03; // Option flags
+        $unknown = 0x0000; // Must be zero
+
+        // Strip the '=' or '@' sign at the beginning of the formula string
+        if ($formula[0] == '=') {
+            $formula = substr($formula, 1);
+        } else {
+            // Error handling
+            $this->writeString($row, $col, 'Unrecognised character for formula', 0);
+
+            return self::WRITE_FORMULA_ERRORS;
+        }
+
+        // Parse the formula using the parser in Parser.php
+        try {
+            $this->parser->parse($formula);
+            $formula = $this->parser->toReversePolish();
+
+            $formlen = strlen($formula); // Length of the binary string
+            $length = 0x16 + $formlen; // Length of the record data
+
+            $header = pack('vv', $record, $length);
+
+            $data = pack('vvv', $row, $col, $xfIndex)
+                . $num
+                . pack('vVv', $grbit, $unknown, $formlen);
+            $this->append($header . $data . $formula);
+
+            // Append also a STRING record if necessary
+            if ($stringValue !== null) {
+                $this->writeStringRecord($stringValue);
+            }
+
+            return self::WRITE_FORMULA_NORMAL;
+        } catch (PhpSpreadsheetException $e) {
+            if (self::$allowThrow) {
+                throw $e;
+            }
+
+            return self::WRITE_FORMULA_EXCEPTION;
+        }
+    }
+
+    /**
+     * Write a STRING record. This.
+     *
+     * @param string $stringValue
+     */
+    private function writeStringRecord($stringValue): void
+    {
+        $record = 0x0207; // Record identifier
+        $data = StringHelper::UTF8toBIFF8UnicodeLong($stringValue);
+
+        $length = strlen($data);
+        $header = pack('vv', $record, $length);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write a hyperlink.
+     * This is comprised of two elements: the visible label and
+     * the invisible link. The visible label is the same as the link unless an
+     * alternative string is specified. The label is written using the
+     * writeString() method. Therefore the 255 characters string limit applies.
+     * $string and $format are optional.
+     *
+     * The hyperlink can be to a http, ftp, mail, internal sheet (not yet), or external
+     * directory url.
+     *
+     * @param int $row Row
+     * @param int $col Column
+     * @param string $url URL string
+     */
+    private function writeUrl($row, $col, $url): void
+    {
+        // Add start row and col to arg list
+        $this->writeUrlRange($row, $col, $row, $col, $url);
+    }
+
+    /**
+     * This is the more general form of writeUrl(). It allows a hyperlink to be
+     * written to a range of cells. This function also decides the type of hyperlink
+     * to be written. These are either, Web (http, ftp, mailto), Internal
+     * (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1').
+     *
+     * @param int $row1 Start row
+     * @param int $col1 Start column
+     * @param int $row2 End row
+     * @param int $col2 End column
+     * @param string $url URL string
+     *
+     * @see writeUrl()
+     */
+    private function writeUrlRange($row1, $col1, $row2, $col2, $url): void
+    {
+        // Check for internal/external sheet links or default to web link
+        if (preg_match('[^internal:]', $url)) {
+            $this->writeUrlInternal($row1, $col1, $row2, $col2, $url);
+        }
+        if (preg_match('[^external:]', $url)) {
+            $this->writeUrlExternal($row1, $col1, $row2, $col2, $url);
+        }
+
+        $this->writeUrlWeb($row1, $col1, $row2, $col2, $url);
+    }
+
+    /**
+     * Used to write http, ftp and mailto hyperlinks.
+     * The link type ($options) is 0x03 is the same as absolute dir ref without
+     * sheet. However it is differentiated by the $unknown2 data stream.
+     *
+     * @param int $row1 Start row
+     * @param int $col1 Start column
+     * @param int $row2 End row
+     * @param int $col2 End column
+     * @param string $url URL string
+     *
+     * @see writeUrl()
+     */
+    public function writeUrlWeb($row1, $col1, $row2, $col2, $url): void
+    {
+        $record = 0x01B8; // Record identifier
+
+        // Pack the undocumented parts of the hyperlink stream
+        $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000');
+        $unknown2 = pack('H*', 'E0C9EA79F9BACE118C8200AA004BA90B');
+
+        // Pack the option flags
+        $options = pack('V', 0x03);
+
+        // Convert URL to a null terminated wchar string
+
+        /** @phpstan-ignore-next-line */
+        $url = implode("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY));
+        $url = $url . "\0\0\0";
+
+        // Pack the length of the URL
+        $url_len = pack('V', strlen($url));
+
+        // Calculate the data length
+        $length = 0x34 + strlen($url);
+
+        // Pack the header data
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvv', $row1, $row2, $col1, $col2);
+
+        // Write the packed data
+        $this->append($header . $data . $unknown1 . $options . $unknown2 . $url_len . $url);
+    }
+
+    /**
+     * Used to write internal reference hyperlinks such as "Sheet1!A1".
+     *
+     * @param int $row1 Start row
+     * @param int $col1 Start column
+     * @param int $row2 End row
+     * @param int $col2 End column
+     * @param string $url URL string
+     *
+     * @see writeUrl()
+     */
+    private function writeUrlInternal($row1, $col1, $row2, $col2, $url): void
+    {
+        $record = 0x01B8; // Record identifier
+
+        // Strip URL type
+        $url = (string) preg_replace('/^internal:/', '', $url);
+
+        // Pack the undocumented parts of the hyperlink stream
+        $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000');
+
+        // Pack the option flags
+        $options = pack('V', 0x08);
+
+        // Convert the URL type and to a null terminated wchar string
+        $url .= "\0";
+
+        // character count
+        $url_len = StringHelper::countCharacters($url);
+        $url_len = pack('V', $url_len);
+
+        $url = StringHelper::convertEncoding($url, 'UTF-16LE', 'UTF-8');
+
+        // Calculate the data length
+        $length = 0x24 + strlen($url);
+
+        // Pack the header data
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvv', $row1, $row2, $col1, $col2);
+
+        // Write the packed data
+        $this->append($header . $data . $unknown1 . $options . $url_len . $url);
+    }
+
+    /**
+     * Write links to external directory names such as 'c:\foo.xls',
+     * c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'.
+     *
+     * Note: Excel writes some relative links with the $dir_long string. We ignore
+     * these cases for the sake of simpler code.
+     *
+     * @param int $row1 Start row
+     * @param int $col1 Start column
+     * @param int $row2 End row
+     * @param int $col2 End column
+     * @param string $url URL string
+     *
+     * @see writeUrl()
+     */
+    private function writeUrlExternal($row1, $col1, $row2, $col2, $url): void
+    {
+        // Network drives are different. We will handle them separately
+        // MS/Novell network drives and shares start with \\
+        if (preg_match('[^external:\\\\]', $url)) {
+            return;
+        }
+
+        $record = 0x01B8; // Record identifier
+
+        // Strip URL type and change Unix dir separator to Dos style (if needed)
+        //
+        $url = (string) preg_replace(['/^external:/', '/\//'], ['', '\\'], $url);
+
+        // Determine if the link is relative or absolute:
+        //   relative if link contains no dir separator, "somefile.xls"
+        //   relative if link starts with up-dir, "..\..\somefile.xls"
+        //   otherwise, absolute
+
+        $absolute = 0x00; // relative path
+        if (preg_match('/^[A-Z]:/', $url)) {
+            $absolute = 0x02; // absolute path on Windows, e.g. C:\...
+        }
+        $link_type = 0x01 | $absolute;
+
+        // Determine if the link contains a sheet reference and change some of the
+        // parameters accordingly.
+        // Split the dir name and sheet name (if it exists)
+        $dir_long = $url;
+        if (preg_match('/\\#/', $url)) {
+            $link_type |= 0x08;
+        }
+
+        // Pack the link type
+        $link_type = pack('V', $link_type);
+
+        // Calculate the up-level dir count e.g.. (..\..\..\ == 3)
+        $up_count = preg_match_all('/\\.\\.\\\\/', $dir_long, $useless);
+        $up_count = pack('v', $up_count);
+
+        // Store the short dos dir name (null terminated)
+        $dir_short = (string) preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0";
+
+        // Store the long dir name as a wchar string (non-null terminated)
+        //$dir_long = $dir_long . "\0";
+
+        // Pack the lengths of the dir strings
+        $dir_short_len = pack('V', strlen($dir_short));
+        //$dir_long_len = pack('V', strlen($dir_long));
+        $stream_len = pack('V', 0); //strlen($dir_long) + 0x06);
+
+        // Pack the undocumented parts of the hyperlink stream
+        $unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000');
+        $unknown2 = pack('H*', '0303000000000000C000000000000046');
+        $unknown3 = pack('H*', 'FFFFADDE000000000000000000000000000000000000000');
+        //$unknown4 = pack('v', 0x03);
+
+        // Pack the main data stream
+        $data = pack('vvvv', $row1, $row2, $col1, $col2) .
+            $unknown1 .
+            $link_type .
+            $unknown2 .
+            $up_count .
+            $dir_short_len .
+            $dir_short .
+            $unknown3 .
+            $stream_len; /*.
+                          $dir_long_len .
+                          $unknown4     .
+                          $dir_long     .
+                          $sheet_len    .
+                          $sheet        ;*/
+
+        // Pack the header data
+        $length = strlen($data);
+        $header = pack('vv', $record, $length);
+
+        // Write the packed data
+        $this->append($header . $data);
+    }
+
+    /**
+     * This method is used to set the height and format for a row.
+     *
+     * @param int $row The row to set
+     * @param int $height Height we are giving to the row.
+     *                        Use null to set XF without setting height
+     * @param int $xfIndex The optional cell style Xf index to apply to the columns
+     * @param bool $hidden The optional hidden attribute
+     * @param int $level The optional outline level for row, in range [0,7]
+     */
+    private function writeRow($row, $height, $xfIndex, $hidden = false, $level = 0): void
+    {
+        $record = 0x0208; // Record identifier
+        $length = 0x0010; // Number of bytes to follow
+
+        $colMic = 0x0000; // First defined column
+        $colMac = 0x0000; // Last defined column
+        $irwMac = 0x0000; // Used by Excel to optimise loading
+        $reserved = 0x0000; // Reserved
+        $grbit = 0x0000; // Option flags
+        $ixfe = $xfIndex;
+
+        if ($height < 0) {
+            $height = null;
+        }
+
+        // Use writeRow($row, null, $XF) to set XF format without setting height
+        if ($height !== null) {
+            $miyRw = $height * 20; // row height
+        } else {
+            $miyRw = 0xff; // default row height is 256
+        }
+
+        // Set the options flags. fUnsynced is used to show that the font and row
+        // heights are not compatible. This is usually the case for WriteExcel.
+        // The collapsed flag 0x10 doesn't seem to be used to indicate that a row
+        // is collapsed. Instead it is used to indicate that the previous row is
+        // collapsed. The zero height flag, 0x20, is used to collapse a row.
+
+        $grbit |= $level;
+        if ($hidden === true) {
+            $grbit |= 0x0030;
+        }
+        if ($height !== null) {
+            $grbit |= 0x0040; // fUnsynced
+        }
+        if ($xfIndex !== 0xF) {
+            $grbit |= 0x0080;
+        }
+        $grbit |= 0x0100;
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvvvvvv', $row, $colMic, $colMac, $miyRw, $irwMac, $reserved, $grbit, $ixfe);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Writes Excel DIMENSIONS to define the area in which there is data.
+     */
+    private function writeDimensions(): void
+    {
+        $record = 0x0200; // Record identifier
+
+        $length = 0x000E;
+        $data = pack('VVvvv', $this->firstRowIndex, $this->lastRowIndex + 1, $this->firstColumnIndex, $this->lastColumnIndex + 1, 0x0000); // reserved
+
+        $header = pack('vv', $record, $length);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write BIFF record Window2.
+     */
+    private function writeWindow2(): void
+    {
+        $record = 0x023E; // Record identifier
+        $length = 0x0012;
+
+        $rwTop = 0x0000; // Top row visible in window
+        $colLeft = 0x0000; // Leftmost column visible in window
+
+        // The options flags that comprise $grbit
+        $fDspFmla = 0; // 0 - bit
+        $fDspGrid = $this->phpSheet->getShowGridlines() ? 1 : 0; // 1
+        $fDspRwCol = $this->phpSheet->getShowRowColHeaders() ? 1 : 0; // 2
+        $fFrozen = $this->phpSheet->getFreezePane() ? 1 : 0; // 3
+        $fDspZeros = 1; // 4
+        $fDefaultHdr = 1; // 5
+        $fArabic = $this->phpSheet->getRightToLeft() ? 1 : 0; // 6
+        $fDspGuts = $this->outlineOn; // 7
+        $fFrozenNoSplit = 0; // 0 - bit
+        // no support in PhpSpreadsheet for selected sheet, therefore sheet is only selected if it is the active sheet
+        $fSelected = ($this->phpSheet === $this->phpSheet->getParentOrThrow()->getActiveSheet()) ? 1 : 0;
+        $fPageBreakPreview = $this->phpSheet->getSheetView()->getView() === SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW;
+
+        $grbit = $fDspFmla;
+        $grbit |= $fDspGrid << 1;
+        $grbit |= $fDspRwCol << 2;
+        $grbit |= $fFrozen << 3;
+        $grbit |= $fDspZeros << 4;
+        $grbit |= $fDefaultHdr << 5;
+        $grbit |= $fArabic << 6;
+        $grbit |= $fDspGuts << 7;
+        $grbit |= $fFrozenNoSplit << 8;
+        $grbit |= $fSelected << 9; // Selected sheets.
+        $grbit |= $fSelected << 10; // Active sheet.
+        $grbit |= $fPageBreakPreview << 11;
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvv', $grbit, $rwTop, $colLeft);
+
+        // FIXME !!!
+        $rgbHdr = 0x0040; // Row/column heading and gridline color index
+        $zoom_factor_page_break = ($fPageBreakPreview ? $this->phpSheet->getSheetView()->getZoomScale() : 0x0000);
+        $zoom_factor_normal = $this->phpSheet->getSheetView()->getZoomScaleNormal();
+
+        $data .= pack('vvvvV', $rgbHdr, 0x0000, $zoom_factor_page_break, $zoom_factor_normal, 0x00000000);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write BIFF record DEFAULTROWHEIGHT.
+     */
+    private function writeDefaultRowHeight(): void
+    {
+        $defaultRowHeight = $this->phpSheet->getDefaultRowDimension()->getRowHeight();
+
+        if ($defaultRowHeight < 0) {
+            return;
+        }
+
+        // convert to twips
+        $defaultRowHeight = (int) 20 * $defaultRowHeight;
+
+        $record = 0x0225; // Record identifier
+        $length = 0x0004; // Number of bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vv', 1, $defaultRowHeight);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write BIFF record DEFCOLWIDTH if COLINFO records are in use.
+     */
+    private function writeDefcol(): void
+    {
+        $defaultColWidth = 8;
+
+        $record = 0x0055; // Record identifier
+        $length = 0x0002; // Number of bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $defaultColWidth);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write BIFF record COLINFO to define column widths.
+     *
+     * Note: The SDK says the record length is 0x0B but Excel writes a 0x0C
+     * length record.
+     *
+     * @param array $col_array This is the only parameter received and is composed of the following:
+     *                0 => First formatted column,
+     *                1 => Last formatted column,
+     *                2 => Col width (8.43 is Excel default),
+     *                3 => The optional XF format of the column,
+     *                4 => Option flags.
+     *                5 => Optional outline level
+     */
+    private function writeColinfo($col_array): void
+    {
+        $colFirst = $col_array[0] ?? null;
+        $colLast = $col_array[1] ?? null;
+        $coldx = $col_array[2] ?? 8.43;
+        $xfIndex = $col_array[3] ?? 15;
+        $grbit = $col_array[4] ?? 0;
+        $level = $col_array[5] ?? 0;
+
+        $record = 0x007D; // Record identifier
+        $length = 0x000C; // Number of bytes to follow
+
+        $coldx *= 256; // Convert to units of 1/256 of a char
+
+        $ixfe = $xfIndex;
+        $reserved = 0x0000; // Reserved
+
+        $level = max(0, min($level, 7));
+        $grbit |= $level << 8;
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvvvv', $colFirst, $colLast, $coldx, $ixfe, $grbit, $reserved);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write BIFF record SELECTION.
+     */
+    private function writeSelection(): void
+    {
+        // look up the selected cell range
+        $selectedCells = Coordinate::splitRange($this->phpSheet->getSelectedCells());
+        $selectedCells = $selectedCells[0];
+        if (count($selectedCells) == 2) {
+            [$first, $last] = $selectedCells;
+        } else {
+            $first = $selectedCells[0];
+            $last = $selectedCells[0];
+        }
+
+        [$colFirst, $rwFirst] = Coordinate::coordinateFromString($first);
+        $colFirst = Coordinate::columnIndexFromString($colFirst) - 1; // base 0 column index
+        --$rwFirst; // base 0 row index
+
+        [$colLast, $rwLast] = Coordinate::coordinateFromString($last);
+        $colLast = Coordinate::columnIndexFromString($colLast) - 1; // base 0 column index
+        --$rwLast; // base 0 row index
+
+        // make sure we are not out of bounds
+        $colFirst = min($colFirst, 255);
+        $colLast = min($colLast, 255);
+
+        $rwFirst = min($rwFirst, 65535);
+        $rwLast = min($rwLast, 65535);
+
+        $record = 0x001D; // Record identifier
+        $length = 0x000F; // Number of bytes to follow
+
+        $pnn = $this->activePane; // Pane position
+        $rwAct = $rwFirst; // Active row
+        $colAct = $colFirst; // Active column
+        $irefAct = 0; // Active cell ref
+        $cref = 1; // Number of refs
+
+        // Swap last row/col for first row/col as necessary
+        if ($rwFirst > $rwLast) {
+            [$rwFirst, $rwLast] = [$rwLast, $rwFirst];
+        }
+
+        if ($colFirst > $colLast) {
+            [$colFirst, $colLast] = [$colLast, $colFirst];
+        }
+
+        $header = pack('vv', $record, $length);
+        $data = pack('CvvvvvvCC', $pnn, $rwAct, $colAct, $irefAct, $cref, $rwFirst, $rwLast, $colFirst, $colLast);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Store the MERGEDCELLS records for all ranges of merged cells.
+     */
+    private function writeMergedCells(): void
+    {
+        $mergeCells = $this->phpSheet->getMergeCells();
+        $countMergeCells = count($mergeCells);
+
+        if ($countMergeCells == 0) {
+            return;
+        }
+
+        // maximum allowed number of merged cells per record
+        $maxCountMergeCellsPerRecord = 1027;
+
+        // record identifier
+        $record = 0x00E5;
+
+        // counter for total number of merged cells treated so far by the writer
+        $i = 0;
+
+        // counter for number of merged cells written in record currently being written
+        $j = 0;
+
+        // initialize record data
+        $recordData = '';
+
+        // loop through the merged cells
+        foreach ($mergeCells as $mergeCell) {
+            ++$i;
+            ++$j;
+
+            // extract the row and column indexes
+            $range = Coordinate::splitRange($mergeCell);
+            [$first, $last] = $range[0];
+            [$firstColumn, $firstRow] = Coordinate::indexesFromString($first);
+            [$lastColumn, $lastRow] = Coordinate::indexesFromString($last);
+
+            $recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, $firstColumn - 1, $lastColumn - 1);
+
+            // flush record if we have reached limit for number of merged cells, or reached final merged cell
+            if ($j == $maxCountMergeCellsPerRecord || $i == $countMergeCells) {
+                $recordData = pack('v', $j) . $recordData;
+                $length = strlen($recordData);
+                $header = pack('vv', $record, $length);
+                $this->append($header . $recordData);
+
+                // initialize for next record, if any
+                $recordData = '';
+                $j = 0;
+            }
+        }
+    }
+
+    /**
+     * Write SHEETLAYOUT record.
+     */
+    private function writeSheetLayout(): void
+    {
+        if (!$this->phpSheet->isTabColorSet()) {
+            return;
+        }
+
+        $recordData = pack(
+            'vvVVVvv',
+            0x0862,
+            0x0000, // unused
+            0x00000000, // unused
+            0x00000000, // unused
+            0x00000014, // size of record data
+            $this->colors[$this->phpSheet->getTabColor()->getRGB()], // color index
+            0x0000        // unused
+        );
+
+        $length = strlen($recordData);
+
+        $record = 0x0862; // Record identifier
+        $header = pack('vv', $record, $length);
+        $this->append($header . $recordData);
+    }
+
+    private static function protectionBitsDefaultFalse(?bool $value, int $shift): int
+    {
+        if ($value === false) {
+            return 1 << $shift;
+        }
+
+        return 0;
+    }
+
+    private static function protectionBitsDefaultTrue(?bool $value, int $shift): int
+    {
+        if ($value !== false) {
+            return 1 << $shift;
+        }
+
+        return 0;
+    }
+
+    /**
+     * Write SHEETPROTECTION.
+     */
+    private function writeSheetProtection(): void
+    {
+        // record identifier
+        $record = 0x0867;
+
+        // prepare options
+        $protection = $this->phpSheet->getProtection();
+        $options = self::protectionBitsDefaultTrue($protection->getObjects(), 0)
+            | self::protectionBitsDefaultTrue($protection->getScenarios(), 1)
+            | self::protectionBitsDefaultFalse($protection->getFormatCells(), 2)
+            | self::protectionBitsDefaultFalse($protection->getFormatColumns(), 3)
+            | self::protectionBitsDefaultFalse($protection->getFormatRows(), 4)
+            | self::protectionBitsDefaultFalse($protection->getInsertColumns(), 5)
+            | self::protectionBitsDefaultFalse($protection->getInsertRows(), 6)
+            | self::protectionBitsDefaultFalse($protection->getInsertHyperlinks(), 7)
+            | self::protectionBitsDefaultFalse($protection->getDeleteColumns(), 8)
+            | self::protectionBitsDefaultFalse($protection->getDeleteRows(), 9)
+            | self::protectionBitsDefaultTrue($protection->getSelectLockedCells(), 10)
+            | self::protectionBitsDefaultFalse($protection->getSort(), 11)
+            | self::protectionBitsDefaultFalse($protection->getAutoFilter(), 12)
+            | self::protectionBitsDefaultFalse($protection->getPivotTables(), 13)
+            | self::protectionBitsDefaultTrue($protection->getSelectUnlockedCells(), 14);
+
+        // record data
+        $recordData = pack(
+            'vVVCVVvv',
+            0x0867, // repeated record identifier
+            0x0000, // not used
+            0x0000, // not used
+            0x00, // not used
+            0x01000200, // unknown data
+            0xFFFFFFFF, // unknown data
+            $options, // options
+            0x0000 // not used
+        );
+
+        $length = strlen($recordData);
+        $header = pack('vv', $record, $length);
+
+        $this->append($header . $recordData);
+    }
+
+    /**
+     * Write BIFF record RANGEPROTECTION.
+     *
+     * Openoffice.org's Documentation of the Microsoft Excel File Format uses term RANGEPROTECTION for these records
+     * Microsoft Office Excel 97-2007 Binary File Format Specification uses term FEAT for these records
+     */
+    private function writeRangeProtection(): void
+    {
+        foreach ($this->phpSheet->getProtectedCells() as $range => $password) {
+            // number of ranges, e.g. 'A1:B3 C20:D25'
+            $cellRanges = explode(' ', $range);
+            $cref = count($cellRanges);
+
+            $recordData = pack(
+                'vvVVvCVvVv',
+                0x0868,
+                0x00,
+                0x0000,
+                0x0000,
+                0x02,
+                0x0,
+                0x0000,
+                $cref,
+                0x0000,
+                0x00
+            );
+
+            foreach ($cellRanges as $cellRange) {
+                $recordData .= $this->writeBIFF8CellRangeAddressFixed($cellRange);
+            }
+
+            // the rgbFeat structure
+            $recordData .= pack(
+                'VV',
+                0x0000,
+                hexdec($password)
+            );
+
+            $recordData .= StringHelper::UTF8toBIFF8UnicodeLong('p' . md5($recordData));
+
+            $length = strlen($recordData);
+
+            $record = 0x0868; // Record identifier
+            $header = pack('vv', $record, $length);
+            $this->append($header . $recordData);
+        }
+    }
+
+    /**
+     * Writes the Excel BIFF PANE record.
+     * The panes can either be frozen or thawed (unfrozen).
+     * Frozen panes are specified in terms of an integer number of rows and columns.
+     * Thawed panes are specified in terms of Excel's units for rows and columns.
+     */
+    private function writePanes(): void
+    {
+        if (!$this->phpSheet->getFreezePane()) {
+            // thaw panes
+            return;
+        }
+
+        [$column, $row] = Coordinate::indexesFromString($this->phpSheet->getFreezePane());
+        $x = $column - 1;
+        $y = $row - 1;
+
+        [$leftMostColumn, $topRow] = Coordinate::indexesFromString($this->phpSheet->getTopLeftCell() ?? '');
+        //Coordinates are zero-based in xls files
+        $rwTop = $topRow - 1;
+        $colLeft = $leftMostColumn - 1;
+
+        $record = 0x0041; // Record identifier
+        $length = 0x000A; // Number of bytes to follow
+
+        // Determine which pane should be active. There is also the undocumented
+        // option to override this should it be necessary: may be removed later.
+        $pnnAct = 0;
+        if ($x != 0 && $y != 0) {
+            $pnnAct = 0; // Bottom right
+        }
+        if ($x != 0 && $y == 0) {
+            $pnnAct = 1; // Top right
+        }
+        if ($x == 0 && $y != 0) {
+            $pnnAct = 2; // Bottom left
+        }
+        if ($x == 0 && $y == 0) {
+            $pnnAct = 3; // Top left
+        }
+
+        $this->activePane = $pnnAct; // Used in writeSelection
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvvv', $x, $y, $rwTop, $colLeft, $pnnAct);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Store the page setup SETUP BIFF record.
+     */
+    private function writeSetup(): void
+    {
+        $record = 0x00A1; // Record identifier
+        $length = 0x0022; // Number of bytes to follow
+
+        $iPaperSize = $this->phpSheet->getPageSetup()->getPaperSize(); // Paper size
+        $iScale = $this->phpSheet->getPageSetup()->getScale() ?: 100; // Print scaling factor
+
+        $iPageStart = 0x01; // Starting page number
+        $iFitWidth = (int) $this->phpSheet->getPageSetup()->getFitToWidth(); // Fit to number of pages wide
+        $iFitHeight = (int) $this->phpSheet->getPageSetup()->getFitToHeight(); // Fit to number of pages high
+        $iRes = 0x0258; // Print resolution
+        $iVRes = 0x0258; // Vertical print resolution
+
+        $numHdr = $this->phpSheet->getPageMargins()->getHeader(); // Header Margin
+
+        $numFtr = $this->phpSheet->getPageMargins()->getFooter(); // Footer Margin
+        $iCopies = 0x01; // Number of copies
+
+        // Order of printing pages
+        $fLeftToRight = $this->phpSheet->getPageSetup()->getPageOrder() === PageSetup::PAGEORDER_DOWN_THEN_OVER
+            ? 0x0 : 0x1;
+        // Page orientation
+        $fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE)
+            ? 0x0 : 0x1;
+
+        $fNoPls = 0x0; // Setup not read from printer
+        $fNoColor = 0x0; // Print black and white
+        $fDraft = 0x0; // Print draft quality
+        $fNotes = 0x0; // Print notes
+        $fNoOrient = 0x0; // Orientation not set
+        $fUsePage = 0x0; // Use custom starting page
+
+        $grbit = $fLeftToRight;
+        $grbit |= $fLandscape << 1;
+        $grbit |= $fNoPls << 2;
+        $grbit |= $fNoColor << 3;
+        $grbit |= $fDraft << 4;
+        $grbit |= $fNotes << 5;
+        $grbit |= $fNoOrient << 6;
+        $grbit |= $fUsePage << 7;
+
+        $numHdr = pack('d', $numHdr);
+        $numFtr = pack('d', $numFtr);
+        if (self::getByteOrder()) { // if it's Big Endian
+            $numHdr = strrev($numHdr);
+            $numFtr = strrev($numFtr);
+        }
+
+        $header = pack('vv', $record, $length);
+        $data1 = pack('vvvvvvvv', $iPaperSize, $iScale, $iPageStart, $iFitWidth, $iFitHeight, $grbit, $iRes, $iVRes);
+        $data2 = $numHdr . $numFtr;
+        $data3 = pack('v', $iCopies);
+        $this->append($header . $data1 . $data2 . $data3);
+    }
+
+    /**
+     * Store the header caption BIFF record.
+     */
+    private function writeHeader(): void
+    {
+        $record = 0x0014; // Record identifier
+
+        /* removing for now
+        // need to fix character count (multibyte!)
+        if (strlen($this->phpSheet->getHeaderFooter()->getOddHeader()) <= 255) {
+            $str      = $this->phpSheet->getHeaderFooter()->getOddHeader();       // header string
+        } else {
+            $str = '';
+        }
+        */
+
+        $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddHeader());
+        $length = strlen($recordData);
+
+        $header = pack('vv', $record, $length);
+
+        $this->append($header . $recordData);
+    }
+
+    /**
+     * Store the footer caption BIFF record.
+     */
+    private function writeFooter(): void
+    {
+        $record = 0x0015; // Record identifier
+
+        /* removing for now
+        // need to fix character count (multibyte!)
+        if (strlen($this->phpSheet->getHeaderFooter()->getOddFooter()) <= 255) {
+            $str = $this->phpSheet->getHeaderFooter()->getOddFooter();
+        } else {
+            $str = '';
+        }
+        */
+
+        $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddFooter());
+        $length = strlen($recordData);
+
+        $header = pack('vv', $record, $length);
+
+        $this->append($header . $recordData);
+    }
+
+    /**
+     * Store the horizontal centering HCENTER BIFF record.
+     */
+    private function writeHcenter(): void
+    {
+        $record = 0x0083; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $fHCenter = $this->phpSheet->getPageSetup()->getHorizontalCentered() ? 1 : 0; // Horizontal centering
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $fHCenter);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Store the vertical centering VCENTER BIFF record.
+     */
+    private function writeVcenter(): void
+    {
+        $record = 0x0084; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $fVCenter = $this->phpSheet->getPageSetup()->getVerticalCentered() ? 1 : 0; // Horizontal centering
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $fVCenter);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Store the LEFTMARGIN BIFF record.
+     */
+    private function writeMarginLeft(): void
+    {
+        $record = 0x0026; // Record identifier
+        $length = 0x0008; // Bytes to follow
+
+        $margin = $this->phpSheet->getPageMargins()->getLeft(); // Margin in inches
+
+        $header = pack('vv', $record, $length);
+        $data = pack('d', $margin);
+        if (self::getByteOrder()) { // if it's Big Endian
+            $data = strrev($data);
+        }
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Store the RIGHTMARGIN BIFF record.
+     */
+    private function writeMarginRight(): void
+    {
+        $record = 0x0027; // Record identifier
+        $length = 0x0008; // Bytes to follow
+
+        $margin = $this->phpSheet->getPageMargins()->getRight(); // Margin in inches
+
+        $header = pack('vv', $record, $length);
+        $data = pack('d', $margin);
+        if (self::getByteOrder()) { // if it's Big Endian
+            $data = strrev($data);
+        }
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Store the TOPMARGIN BIFF record.
+     */
+    private function writeMarginTop(): void
+    {
+        $record = 0x0028; // Record identifier
+        $length = 0x0008; // Bytes to follow
+
+        $margin = $this->phpSheet->getPageMargins()->getTop(); // Margin in inches
+
+        $header = pack('vv', $record, $length);
+        $data = pack('d', $margin);
+        if (self::getByteOrder()) { // if it's Big Endian
+            $data = strrev($data);
+        }
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Store the BOTTOMMARGIN BIFF record.
+     */
+    private function writeMarginBottom(): void
+    {
+        $record = 0x0029; // Record identifier
+        $length = 0x0008; // Bytes to follow
+
+        $margin = $this->phpSheet->getPageMargins()->getBottom(); // Margin in inches
+
+        $header = pack('vv', $record, $length);
+        $data = pack('d', $margin);
+        if (self::getByteOrder()) { // if it's Big Endian
+            $data = strrev($data);
+        }
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write the PRINTHEADERS BIFF record.
+     */
+    private function writePrintHeaders(): void
+    {
+        $record = 0x002a; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $fPrintRwCol = $this->printHeaders; // Boolean flag
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $fPrintRwCol);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the
+     * GRIDSET record.
+     */
+    private function writePrintGridlines(): void
+    {
+        $record = 0x002b; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $fPrintGrid = $this->phpSheet->getPrintGridlines() ? 1 : 0; // Boolean flag
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $fPrintGrid);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write the GRIDSET BIFF record. Must be used in conjunction with the
+     * PRINTGRIDLINES record.
+     */
+    private function writeGridset(): void
+    {
+        $record = 0x0082; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $fGridSet = !$this->phpSheet->getPrintGridlines(); // Boolean flag
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $fGridSet);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write the AUTOFILTERINFO BIFF record. This is used to configure the number of autofilter select used in the sheet.
+     */
+    private function writeAutoFilterInfo(): void
+    {
+        $record = 0x009D; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $rangeBounds = Coordinate::rangeBoundaries($this->phpSheet->getAutoFilter()->getRange());
+        $iNumFilters = 1 + $rangeBounds[1][0] - $rangeBounds[0][0];
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $iNumFilters);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write the GUTS BIFF record. This is used to configure the gutter margins
+     * where Excel outline symbols are displayed. The visibility of the gutters is
+     * controlled by a flag in WSBOOL.
+     *
+     * @see writeWsbool()
+     */
+    private function writeGuts(): void
+    {
+        $record = 0x0080; // Record identifier
+        $length = 0x0008; // Bytes to follow
+
+        $dxRwGut = 0x0000; // Size of row gutter
+        $dxColGut = 0x0000; // Size of col gutter
+
+        // determine maximum row outline level
+        $maxRowOutlineLevel = 0;
+        foreach ($this->phpSheet->getRowDimensions() as $rowDimension) {
+            $maxRowOutlineLevel = max($maxRowOutlineLevel, $rowDimension->getOutlineLevel());
+        }
+
+        $col_level = 0;
+
+        // Calculate the maximum column outline level. The equivalent calculation
+        // for the row outline level is carried out in writeRow().
+        $colcount = count($this->columnInfo);
+        for ($i = 0; $i < $colcount; ++$i) {
+            $col_level = max($this->columnInfo[$i][5], $col_level);
+        }
+
+        // Set the limits for the outline levels (0 <= x <= 7).
+        $col_level = max(0, min($col_level, 7));
+
+        // The displayed level is one greater than the max outline levels
+        if ($maxRowOutlineLevel) {
+            ++$maxRowOutlineLevel;
+        }
+        if ($col_level) {
+            ++$col_level;
+        }
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvvv', $dxRwGut, $dxColGut, $maxRowOutlineLevel, $col_level);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction
+     * with the SETUP record.
+     */
+    private function writeWsbool(): void
+    {
+        $record = 0x0081; // Record identifier
+        $length = 0x0002; // Bytes to follow
+        $grbit = 0x0000;
+
+        // The only option that is of interest is the flag for fit to page. So we
+        // set all the options in one go.
+        //
+        // Set the option flags
+        $grbit |= 0x0001; // Auto page breaks visible
+        if ($this->outlineStyle) {
+            $grbit |= 0x0020; // Auto outline styles
+        }
+        if ($this->phpSheet->getShowSummaryBelow()) {
+            $grbit |= 0x0040; // Outline summary below
+        }
+        if ($this->phpSheet->getShowSummaryRight()) {
+            $grbit |= 0x0080; // Outline summary right
+        }
+        if ($this->phpSheet->getPageSetup()->getFitToPage()) {
+            $grbit |= 0x0100; // Page setup fit to page
+        }
+        if ($this->outlineOn) {
+            $grbit |= 0x0400; // Outline symbols displayed
+        }
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $grbit);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write the HORIZONTALPAGEBREAKS and VERTICALPAGEBREAKS BIFF records.
+     */
+    private function writeBreaks(): void
+    {
+        // initialize
+        $vbreaks = [];
+        $hbreaks = [];
+
+        foreach ($this->phpSheet->getRowBreaks() as $cell => $break) {
+            // Fetch coordinates
+            $coordinates = Coordinate::coordinateFromString($cell);
+            $hbreaks[] = $coordinates[1];
+        }
+        foreach ($this->phpSheet->getColumnBreaks() as $cell => $break) {
+            // Fetch coordinates
+            $coordinates = Coordinate::indexesFromString($cell);
+            $vbreaks[] = $coordinates[0] - 1;
+        }
+
+        //horizontal page breaks
+        if (!empty($hbreaks)) {
+            // Sort and filter array of page breaks
+            sort($hbreaks, SORT_NUMERIC);
+            if ($hbreaks[0] == 0) { // don't use first break if it's 0
+                array_shift($hbreaks);
+            }
+
+            $record = 0x001b; // Record identifier
+            $cbrk = count($hbreaks); // Number of page breaks
+            $length = 2 + 6 * $cbrk; // Bytes to follow
+
+            $header = pack('vv', $record, $length);
+            $data = pack('v', $cbrk);
+
+            // Append each page break
+            foreach ($hbreaks as $hbreak) {
+                $data .= pack('vvv', $hbreak, 0x0000, 0x00ff);
+            }
+
+            $this->append($header . $data);
+        }
+
+        // vertical page breaks
+        if (!empty($vbreaks)) {
+            // 1000 vertical pagebreaks appears to be an internal Excel 5 limit.
+            // It is slightly higher in Excel 97/200, approx. 1026
+            $vbreaks = array_slice($vbreaks, 0, 1000);
+
+            // Sort and filter array of page breaks
+            sort($vbreaks, SORT_NUMERIC);
+            if ($vbreaks[0] == 0) { // don't use first break if it's 0
+                array_shift($vbreaks);
+            }
+
+            $record = 0x001a; // Record identifier
+            $cbrk = count($vbreaks); // Number of page breaks
+            $length = 2 + 6 * $cbrk; // Bytes to follow
+
+            $header = pack('vv', $record, $length);
+            $data = pack('v', $cbrk);
+
+            // Append each page break
+            foreach ($vbreaks as $vbreak) {
+                $data .= pack('vvv', $vbreak, 0x0000, 0xffff);
+            }
+
+            $this->append($header . $data);
+        }
+    }
+
+    /**
+     * Set the Biff PROTECT record to indicate that the worksheet is protected.
+     */
+    private function writeProtect(): void
+    {
+        // Exit unless sheet protection has been specified
+        if ($this->phpSheet->getProtection()->getSheet() !== true) {
+            return;
+        }
+
+        $record = 0x0012; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $fLock = 1; // Worksheet is protected
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $fLock);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write SCENPROTECT.
+     */
+    private function writeScenProtect(): void
+    {
+        // Exit if sheet protection is not active
+        if ($this->phpSheet->getProtection()->getSheet() !== true) {
+            return;
+        }
+
+        // Exit if scenarios are not protected
+        if ($this->phpSheet->getProtection()->getScenarios() !== true) {
+            return;
+        }
+
+        $record = 0x00DD; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', 1);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write OBJECTPROTECT.
+     */
+    private function writeObjectProtect(): void
+    {
+        // Exit if sheet protection is not active
+        if ($this->phpSheet->getProtection()->getSheet() !== true) {
+            return;
+        }
+
+        // Exit if objects are not protected
+        if ($this->phpSheet->getProtection()->getObjects() !== true) {
+            return;
+        }
+
+        $record = 0x0063; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', 1);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write the worksheet PASSWORD record.
+     */
+    private function writePassword(): void
+    {
+        // Exit unless sheet protection and password have been specified
+        if ($this->phpSheet->getProtection()->getSheet() !== true || !$this->phpSheet->getProtection()->getPassword() || $this->phpSheet->getProtection()->getAlgorithm() !== '') {
+            return;
+        }
+
+        $record = 0x0013; // Record identifier
+        $length = 0x0002; // Bytes to follow
+
+        $wPassword = hexdec($this->phpSheet->getProtection()->getPassword()); // Encoded password
+
+        $header = pack('vv', $record, $length);
+        $data = pack('v', $wPassword);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Insert a 24bit bitmap image in a worksheet.
+     *
+     * @param int $row The row we are going to insert the bitmap into
+     * @param int $col The column we are going to insert the bitmap into
+     * @param mixed $bitmap The bitmap filename or GD-image resource
+     * @param int $x the horizontal position (offset) of the image inside the cell
+     * @param int $y the vertical position (offset) of the image inside the cell
+     * @param float $scale_x The horizontal scale
+     * @param float $scale_y The vertical scale
+     */
+    public function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1): void
+    {
+        $bitmap_array = (is_resource($bitmap) || $bitmap instanceof GdImage
+            ? $this->processBitmapGd($bitmap)
+            : $this->processBitmap($bitmap));
+        [$width, $height, $size, $data] = $bitmap_array;
+
+        // Scale the frame of the image.
+        $width *= $scale_x;
+        $height *= $scale_y;
+
+        // Calculate the vertices of the image and write the OBJ record
+        $this->positionImage($col, $row, $x, $y, (int) $width, (int) $height);
+
+        // Write the IMDATA record to store the bitmap data
+        $record = 0x007f;
+        $length = 8 + $size;
+        $cf = 0x09;
+        $env = 0x01;
+        $lcb = $size;
+
+        $header = pack('vvvvV', $record, $length, $cf, $env, $lcb);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Calculate the vertices that define the position of the image as required by
+     * the OBJ record.
+     *
+     *         +------------+------------+
+     *         |     A      |      B     |
+     *   +-----+------------+------------+
+     *   |     |(x1,y1)     |            |
+     *   |  1  |(A1)._______|______      |
+     *   |     |    |              |     |
+     *   |     |    |              |     |
+     *   +-----+----|    BITMAP    |-----+
+     *   |     |    |              |     |
+     *   |  2  |    |______________.     |
+     *   |     |            |        (B2)|
+     *   |     |            |     (x2,y2)|
+     *   +---- +------------+------------+
+     *
+     * Example of a bitmap that covers some of the area from cell A1 to cell B2.
+     *
+     * Based on the width and height of the bitmap we need to calculate 8 vars:
+     *     $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2.
+     * The width and height of the cells are also variable and have to be taken into
+     * account.
+     * The values of $col_start and $row_start are passed in from the calling
+     * function. The values of $col_end and $row_end are calculated by subtracting
+     * the width and height of the bitmap from the width and height of the
+     * underlying cells.
+     * The vertices are expressed as a percentage of the underlying cell width as
+     * follows (rhs values are in pixels):
+     *
+     *       x1 = X / W *1024
+     *       y1 = Y / H *256
+     *       x2 = (X-1) / W *1024
+     *       y2 = (Y-1) / H *256
+     *
+     *       Where:  X is distance from the left side of the underlying cell
+     *               Y is distance from the top of the underlying cell
+     *               W is the width of the cell
+     *               H is the height of the cell
+     * The SDK incorrectly states that the height should be expressed as a
+     *        percentage of 1024.
+     *
+     * @param int $col_start Col containing upper left corner of object
+     * @param int $row_start Row containing top left corner of object
+     * @param int $x1 Distance to left side of object
+     * @param int $y1 Distance to top of object
+     * @param int $width Width of image frame
+     * @param int $height Height of image frame
+     */
+    public function positionImage($col_start, $row_start, $x1, $y1, $width, $height): void
+    {
+        // Initialise end cell to the same as the start cell
+        $col_end = $col_start; // Col containing lower right corner of object
+        $row_end = $row_start; // Row containing bottom right corner of object
+
+        // Zero the specified offset if greater than the cell dimensions
+        if ($x1 >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1))) {
+            $x1 = 0;
+        }
+        if ($y1 >= Xls::sizeRow($this->phpSheet, $row_start + 1)) {
+            $y1 = 0;
+        }
+
+        $width = $width + $x1 - 1;
+        $height = $height + $y1 - 1;
+
+        // Subtract the underlying cell widths to find the end cell of the image
+        while ($width >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1))) {
+            $width -= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1));
+            ++$col_end;
+        }
+
+        // Subtract the underlying cell heights to find the end cell of the image
+        while ($height >= Xls::sizeRow($this->phpSheet, $row_end + 1)) {
+            $height -= Xls::sizeRow($this->phpSheet, $row_end + 1);
+            ++$row_end;
+        }
+
+        // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
+        // with zero eight or width.
+        //
+        if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) == 0) {
+            return;
+        }
+        if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) == 0) {
+            return;
+        }
+        if (Xls::sizeRow($this->phpSheet, $row_start + 1) == 0) {
+            return;
+        }
+        if (Xls::sizeRow($this->phpSheet, $row_end + 1) == 0) {
+            return;
+        }
+
+        // Convert the pixel values to the percentage value expected by Excel
+        $x1 = $x1 / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) * 1024;
+        $y1 = $y1 / Xls::sizeRow($this->phpSheet, $row_start + 1) * 256;
+        $x2 = $width / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) * 1024; // Distance to right side of object
+        $y2 = $height / Xls::sizeRow($this->phpSheet, $row_end + 1) * 256; // Distance to bottom of object
+
+        $this->writeObjPicture($col_start, $x1, $row_start, $y1, $col_end, $x2, $row_end, $y2);
+    }
+
+    /**
+     * Store the OBJ record that precedes an IMDATA record. This could be generalise
+     * to support other Excel objects.
+     *
+     * @param int $colL Column containing upper left corner of object
+     * @param int $dxL Distance from left side of cell
+     * @param int $rwT Row containing top left corner of object
+     * @param int $dyT Distance from top of cell
+     * @param int $colR Column containing lower right corner of object
+     * @param int $dxR Distance from right of cell
+     * @param int $rwB Row containing bottom right corner of object
+     * @param int $dyB Distance from bottom of cell
+     */
+    private function writeObjPicture($colL, $dxL, $rwT, $dyT, $colR, $dxR, $rwB, $dyB): void
+    {
+        $record = 0x005d; // Record identifier
+        $length = 0x003c; // Bytes to follow
+
+        $cObj = 0x0001; // Count of objects in file (set to 1)
+        $OT = 0x0008; // Object type. 8 = Picture
+        $id = 0x0001; // Object ID
+        $grbit = 0x0614; // Option flags
+
+        $cbMacro = 0x0000; // Length of FMLA structure
+        $Reserved1 = 0x0000; // Reserved
+        $Reserved2 = 0x0000; // Reserved
+
+        $icvBack = 0x09; // Background colour
+        $icvFore = 0x09; // Foreground colour
+        $fls = 0x00; // Fill pattern
+        $fAuto = 0x00; // Automatic fill
+        $icv = 0x08; // Line colour
+        $lns = 0xff; // Line style
+        $lnw = 0x01; // Line weight
+        $fAutoB = 0x00; // Automatic border
+        $frs = 0x0000; // Frame style
+        $cf = 0x0009; // Image format, 9 = bitmap
+        $Reserved3 = 0x0000; // Reserved
+        $cbPictFmla = 0x0000; // Length of FMLA structure
+        $Reserved4 = 0x0000; // Reserved
+        $grbit2 = 0x0001; // Option flags
+        $Reserved5 = 0x0000; // Reserved
+
+        $header = pack('vv', $record, $length);
+        $data = pack('V', $cObj);
+        $data .= pack('v', $OT);
+        $data .= pack('v', $id);
+        $data .= pack('v', $grbit);
+        $data .= pack('v', $colL);
+        $data .= pack('v', $dxL);
+        $data .= pack('v', $rwT);
+        $data .= pack('v', $dyT);
+        $data .= pack('v', $colR);
+        $data .= pack('v', $dxR);
+        $data .= pack('v', $rwB);
+        $data .= pack('v', $dyB);
+        $data .= pack('v', $cbMacro);
+        $data .= pack('V', $Reserved1);
+        $data .= pack('v', $Reserved2);
+        $data .= pack('C', $icvBack);
+        $data .= pack('C', $icvFore);
+        $data .= pack('C', $fls);
+        $data .= pack('C', $fAuto);
+        $data .= pack('C', $icv);
+        $data .= pack('C', $lns);
+        $data .= pack('C', $lnw);
+        $data .= pack('C', $fAutoB);
+        $data .= pack('v', $frs);
+        $data .= pack('V', $cf);
+        $data .= pack('v', $Reserved3);
+        $data .= pack('v', $cbPictFmla);
+        $data .= pack('v', $Reserved4);
+        $data .= pack('v', $grbit2);
+        $data .= pack('V', $Reserved5);
+
+        $this->append($header . $data);
+    }
+
+    /**
+     * Convert a GD-image into the internal format.
+     *
+     * @param GdImage|resource $image The image to process
+     *
+     * @return array Array with data and properties of the bitmap
+     */
+    public function processBitmapGd($image)
+    {
+        $width = imagesx($image);
+        $height = imagesy($image);
+
+        $data = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18);
+        for ($j = $height; --$j;) {
+            for ($i = 0; $i < $width; ++$i) {
+                /** @phpstan-ignore-next-line */
+                $color = imagecolorsforindex($image, imagecolorat($image, $i, $j));
+                if ($color !== false) {
+                    foreach (['red', 'green', 'blue'] as $key) {
+                        $color[$key] = $color[$key] + (int) round((255 - $color[$key]) * $color['alpha'] / 127);
+                    }
+                    $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']);
+                }
+            }
+            if (3 * $width % 4) {
+                $data .= str_repeat("\x00", 4 - 3 * $width % 4);
+            }
+        }
+
+        return [$width, $height, strlen($data), $data];
+    }
+
+    /**
+     * Convert a 24 bit bitmap into the modified internal format used by Windows.
+     * This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the
+     * MSDN library.
+     *
+     * @param string $bitmap The bitmap to process
+     *
+     * @return array Array with data and properties of the bitmap
+     */
+    public function processBitmap($bitmap)
+    {
+        // Open file.
+        $bmp_fd = @fopen($bitmap, 'rb');
+        if ($bmp_fd === false) {
+            throw new WriterException("Couldn't import $bitmap");
+        }
+
+        // Slurp the file into a string.
+        $data = (string) fread($bmp_fd, (int) filesize($bitmap));
+
+        // Check that the file is big enough to be a bitmap.
+        if (strlen($data) <= 0x36) {
+            throw new WriterException("$bitmap doesn't contain enough data.\n");
+        }
+
+        // The first 2 bytes are used to identify the bitmap.
+
+        $identity = unpack('A2ident', $data);
+        if ($identity === false || $identity['ident'] != 'BM') {
+            throw new WriterException("$bitmap doesn't appear to be a valid bitmap image.\n");
+        }
+
+        // Remove bitmap data: ID.
+        $data = substr($data, 2);
+
+        // Read and remove the bitmap size. This is more reliable than reading
+        // the data size at offset 0x22.
+        //
+        $size_array = unpack('Vsa', substr($data, 0, 4)) ?: [];
+        $size = $size_array['sa'];
+        $data = substr($data, 4);
+        $size -= 0x36; // Subtract size of bitmap header.
+        $size += 0x0C; // Add size of BIFF header.
+
+        // Remove bitmap data: reserved, offset, header length.
+        $data = substr($data, 12);
+
+        // Read and remove the bitmap width and height. Verify the sizes.
+        $width_and_height = unpack('V2', substr($data, 0, 8)) ?: [];
+        $width = $width_and_height[1];
+        $height = $width_and_height[2];
+        $data = substr($data, 8);
+        if ($width > 0xFFFF) {
+            throw new WriterException("$bitmap: largest image width supported is 65k.\n");
+        }
+        if ($height > 0xFFFF) {
+            throw new WriterException("$bitmap: largest image height supported is 65k.\n");
+        }
+
+        // Read and remove the bitmap planes and bpp data. Verify them.
+        $planes_and_bitcount = unpack('v2', substr($data, 0, 4));
+        $data = substr($data, 4);
+        if ($planes_and_bitcount === false || $planes_and_bitcount[2] != 24) { // Bitcount
+            throw new WriterException("$bitmap isn't a 24bit true color bitmap.\n");
+        }
+        if ($planes_and_bitcount[1] != 1) {
+            throw new WriterException("$bitmap: only 1 plane supported in bitmap image.\n");
+        }
+
+        // Read and remove the bitmap compression. Verify compression.
+        $compression = unpack('Vcomp', substr($data, 0, 4));
+        $data = substr($data, 4);
+
+        if ($compression === false || $compression['comp'] != 0) {
+            throw new WriterException("$bitmap: compression not supported in bitmap image.\n");
+        }
+
+        // Remove bitmap data: data size, hres, vres, colours, imp. colours.
+        $data = substr($data, 20);
+
+        // Add the BITMAPCOREHEADER data
+        $header = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18);
+        $data = $header . $data;
+
+        return [$width, $height, $size, $data];
+    }
+
+    /**
+     * Store the window zoom factor. This should be a reduced fraction but for
+     * simplicity we will store all fractions with a numerator of 100.
+     */
+    private function writeZoom(): void
+    {
+        // If scale is 100 we don't need to write a record
+        if ($this->phpSheet->getSheetView()->getZoomScale() == 100) {
+            return;
+        }
+
+        $record = 0x00A0; // Record identifier
+        $length = 0x0004; // Bytes to follow
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vv', $this->phpSheet->getSheetView()->getZoomScale(), 100);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Get Escher object.
+     */
+    public function getEscher(): ?\PhpOffice\PhpSpreadsheet\Shared\Escher
+    {
+        return $this->escher;
+    }
+
+    /**
+     * Set Escher object.
+     */
+    public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher): void
+    {
+        $this->escher = $escher;
+    }
+
+    /**
+     * Write MSODRAWING record.
+     */
+    private function writeMsoDrawing(): void
+    {
+        // write the Escher stream if necessary
+        if (isset($this->escher)) {
+            $writer = new Escher($this->escher);
+            $data = $writer->close();
+            $spOffsets = $writer->getSpOffsets();
+            $spTypes = $writer->getSpTypes();
+            // write the neccesary MSODRAWING, OBJ records
+
+            // split the Escher stream
+            $spOffsets[0] = 0;
+            $nm = count($spOffsets) - 1; // number of shapes excluding first shape
+            for ($i = 1; $i <= $nm; ++$i) {
+                // MSODRAWING record
+                $record = 0x00EC; // Record identifier
+
+                // chunk of Escher stream for one shape
+                $dataChunk = substr($data, $spOffsets[$i - 1], $spOffsets[$i] - $spOffsets[$i - 1]);
+
+                $length = strlen($dataChunk);
+                $header = pack('vv', $record, $length);
+
+                $this->append($header . $dataChunk);
+
+                // OBJ record
+                $record = 0x005D; // record identifier
+                $objData = '';
+
+                // ftCmo
+                if ($spTypes[$i] == 0x00C9) {
+                    // Add ftCmo (common object data) subobject
+                    $objData .=
+                        pack(
+                            'vvvvvVVV',
+                            0x0015, // 0x0015 = ftCmo
+                            0x0012, // length of ftCmo data
+                            0x0014, // object type, 0x0014 = filter
+                            $i, // object id number, Excel seems to use 1-based index, local for the sheet
+                            0x2101, // option flags, 0x2001 is what OpenOffice.org uses
+                            0, // reserved
+                            0, // reserved
+                            0  // reserved
+                        );
+
+                    // Add ftSbs Scroll bar subobject
+                    $objData .= pack('vv', 0x00C, 0x0014);
+                    $objData .= pack('H*', '0000000000000000640001000A00000010000100');
+                    // Add ftLbsData (List box data) subobject
+                    $objData .= pack('vv', 0x0013, 0x1FEE);
+                    $objData .= pack('H*', '00000000010001030000020008005700');
+                } else {
+                    // Add ftCmo (common object data) subobject
+                    $objData .=
+                        pack(
+                            'vvvvvVVV',
+                            0x0015, // 0x0015 = ftCmo
+                            0x0012, // length of ftCmo data
+                            0x0008, // object type, 0x0008 = picture
+                            $i, // object id number, Excel seems to use 1-based index, local for the sheet
+                            0x6011, // option flags, 0x6011 is what OpenOffice.org uses
+                            0, // reserved
+                            0, // reserved
+                            0  // reserved
+                        );
+                }
+
+                // ftEnd
+                $objData .=
+                    pack(
+                        'vv',
+                        0x0000, // 0x0000 = ftEnd
+                        0x0000  // length of ftEnd data
+                    );
+
+                $length = strlen($objData);
+                $header = pack('vv', $record, $length);
+                $this->append($header . $objData);
+            }
+        }
+    }
+
+    /**
+     * Store the DATAVALIDATIONS and DATAVALIDATION records.
+     */
+    private function writeDataValidity(): void
+    {
+        // Datavalidation collection
+        $dataValidationCollection = $this->phpSheet->getDataValidationCollection();
+
+        // Write data validations?
+        if (!empty($dataValidationCollection)) {
+            // DATAVALIDATIONS record
+            $record = 0x01B2; // Record identifier
+            $length = 0x0012; // Bytes to follow
+
+            $grbit = 0x0000; // Prompt box at cell, no cached validity data at DV records
+            $horPos = 0x00000000; // Horizontal position of prompt box, if fixed position
+            $verPos = 0x00000000; // Vertical position of prompt box, if fixed position
+            $objId = 0xFFFFFFFF; // Object identifier of drop down arrow object, or -1 if not visible
+
+            $header = pack('vv', $record, $length);
+            $data = pack('vVVVV', $grbit, $horPos, $verPos, $objId, count($dataValidationCollection));
+            $this->append($header . $data);
+
+            // DATAVALIDATION records
+            $record = 0x01BE; // Record identifier
+
+            foreach ($dataValidationCollection as $cellCoordinate => $dataValidation) {
+                // options
+                $options = 0x00000000;
+
+                // data type
+                $type = CellDataValidation::type($dataValidation);
+
+                $options |= $type << 0;
+
+                // error style
+                $errorStyle = CellDataValidation::errorStyle($dataValidation);
+
+                $options |= $errorStyle << 4;
+
+                // explicit formula?
+                if ($type == 0x03 && preg_match('/^\".*\"$/', $dataValidation->getFormula1())) {
+                    $options |= 0x01 << 7;
+                }
+
+                // empty cells allowed
+                $options |= $dataValidation->getAllowBlank() << 8;
+
+                // show drop down
+                $options |= (!$dataValidation->getShowDropDown()) << 9;
+
+                // show input message
+                $options |= $dataValidation->getShowInputMessage() << 18;
+
+                // show error message
+                $options |= $dataValidation->getShowErrorMessage() << 19;
+
+                // condition operator
+                $operator = CellDataValidation::operator($dataValidation);
+
+                $options |= $operator << 20;
+
+                $data = pack('V', $options);
+
+                // prompt title
+                $promptTitle = $dataValidation->getPromptTitle() !== '' ?
+                    $dataValidation->getPromptTitle() : chr(0);
+                $data .= StringHelper::UTF8toBIFF8UnicodeLong($promptTitle);
+
+                // error title
+                $errorTitle = $dataValidation->getErrorTitle() !== '' ?
+                    $dataValidation->getErrorTitle() : chr(0);
+                $data .= StringHelper::UTF8toBIFF8UnicodeLong($errorTitle);
+
+                // prompt text
+                $prompt = $dataValidation->getPrompt() !== '' ?
+                    $dataValidation->getPrompt() : chr(0);
+                $data .= StringHelper::UTF8toBIFF8UnicodeLong($prompt);
+
+                // error text
+                $error = $dataValidation->getError() !== '' ?
+                    $dataValidation->getError() : chr(0);
+                $data .= StringHelper::UTF8toBIFF8UnicodeLong($error);
+
+                // formula 1
+                try {
+                    $formula1 = $dataValidation->getFormula1();
+                    if ($type == 0x03) { // list type
+                        $formula1 = str_replace(',', chr(0), $formula1);
+                    }
+                    $this->parser->parse($formula1);
+                    $formula1 = $this->parser->toReversePolish();
+                    $sz1 = strlen($formula1);
+                } catch (PhpSpreadsheetException $e) {
+                    $sz1 = 0;
+                    $formula1 = '';
+                }
+                $data .= pack('vv', $sz1, 0x0000);
+                $data .= $formula1;
+
+                // formula 2
+                try {
+                    $formula2 = $dataValidation->getFormula2();
+                    if ($formula2 === '') {
+                        throw new WriterException('No formula2');
+                    }
+                    $this->parser->parse($formula2);
+                    $formula2 = $this->parser->toReversePolish();
+                    $sz2 = strlen($formula2);
+                } catch (PhpSpreadsheetException $e) {
+                    $sz2 = 0;
+                    $formula2 = '';
+                }
+                $data .= pack('vv', $sz2, 0x0000);
+                $data .= $formula2;
+
+                // cell range address list
+                $data .= pack('v', 0x0001);
+                $data .= $this->writeBIFF8CellRangeAddressFixed($cellCoordinate);
+
+                $length = strlen($data);
+                $header = pack('vv', $record, $length);
+
+                $this->append($header . $data);
+            }
+        }
+    }
+
+    /**
+     * Write PLV Record.
+     */
+    private function writePageLayoutView(): void
+    {
+        $record = 0x088B; // Record identifier
+        $length = 0x0010; // Bytes to follow
+
+        $rt = 0x088B; // 2
+        $grbitFrt = 0x0000; // 2
+        //$reserved = 0x0000000000000000; // 8
+        $wScalvePLV = $this->phpSheet->getSheetView()->getZoomScale(); // 2
+
+        // The options flags that comprise $grbit
+        if ($this->phpSheet->getSheetView()->getView() == SheetView::SHEETVIEW_PAGE_LAYOUT) {
+            $fPageLayoutView = 1;
+        } else {
+            $fPageLayoutView = 0;
+        }
+        $fRulerVisible = 0;
+        $fWhitespaceHidden = 0;
+
+        $grbit = $fPageLayoutView; // 2
+        $grbit |= $fRulerVisible << 1;
+        $grbit |= $fWhitespaceHidden << 3;
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vvVVvv', $rt, $grbitFrt, 0x00000000, 0x00000000, $wScalvePLV, $grbit);
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write CFRule Record.
+     */
+    private function writeCFRule(
+        ConditionalHelper $conditionalFormulaHelper,
+        Conditional $conditional,
+        string $cellRange
+    ): void {
+        $record = 0x01B1; // Record identifier
+        $type = null; // Type of the CF
+        $operatorType = null; // Comparison operator
+
+        if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) {
+            $type = 0x02;
+            $operatorType = 0x00;
+        } elseif ($conditional->getConditionType() == Conditional::CONDITION_CELLIS) {
+            $type = 0x01;
+
+            switch ($conditional->getOperatorType()) {
+                case Conditional::OPERATOR_NONE:
+                    $operatorType = 0x00;
+
+                    break;
+                case Conditional::OPERATOR_EQUAL:
+                    $operatorType = 0x03;
+
+                    break;
+                case Conditional::OPERATOR_GREATERTHAN:
+                    $operatorType = 0x05;
+
+                    break;
+                case Conditional::OPERATOR_GREATERTHANOREQUAL:
+                    $operatorType = 0x07;
+
+                    break;
+                case Conditional::OPERATOR_LESSTHAN:
+                    $operatorType = 0x06;
+
+                    break;
+                case Conditional::OPERATOR_LESSTHANOREQUAL:
+                    $operatorType = 0x08;
+
+                    break;
+                case Conditional::OPERATOR_NOTEQUAL:
+                    $operatorType = 0x04;
+
+                    break;
+                case Conditional::OPERATOR_BETWEEN:
+                    $operatorType = 0x01;
+
+                    break;
+                    // not OPERATOR_NOTBETWEEN 0x02
+            }
+        }
+
+        // $szValue1 : size of the formula data for first value or formula
+        // $szValue2 : size of the formula data for second value or formula
+        $arrConditions = $conditional->getConditions();
+        $numConditions = count($arrConditions);
+
+        $szValue1 = 0x0000;
+        $szValue2 = 0x0000;
+        $operand1 = null;
+        $operand2 = null;
+
+        if ($numConditions === 1) {
+            $conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange);
+            $szValue1 = $conditionalFormulaHelper->size();
+            $operand1 = $conditionalFormulaHelper->tokens();
+        } elseif ($numConditions === 2 && ($conditional->getOperatorType() === Conditional::OPERATOR_BETWEEN)) {
+            $conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange);
+            $szValue1 = $conditionalFormulaHelper->size();
+            $operand1 = $conditionalFormulaHelper->tokens();
+            $conditionalFormulaHelper->processCondition($arrConditions[1], $cellRange);
+            $szValue2 = $conditionalFormulaHelper->size();
+            $operand2 = $conditionalFormulaHelper->tokens();
+        }
+
+        // $flags : Option flags
+        // Alignment
+        $bAlignHz = ($conditional->getStyle()->getAlignment()->getHorizontal() === null ? 1 : 0);
+        $bAlignVt = ($conditional->getStyle()->getAlignment()->getVertical() === null ? 1 : 0);
+        $bAlignWrapTx = ($conditional->getStyle()->getAlignment()->getWrapText() === false ? 1 : 0);
+        $bTxRotation = ($conditional->getStyle()->getAlignment()->getTextRotation() === null ? 1 : 0);
+        $bIndent = ($conditional->getStyle()->getAlignment()->getIndent() === 0 ? 1 : 0);
+        $bShrinkToFit = ($conditional->getStyle()->getAlignment()->getShrinkToFit() === false ? 1 : 0);
+        if ($bAlignHz == 0 || $bAlignVt == 0 || $bAlignWrapTx == 0 || $bTxRotation == 0 || $bIndent == 0 || $bShrinkToFit == 0) {
+            $bFormatAlign = 1;
+        } else {
+            $bFormatAlign = 0;
+        }
+        // Protection
+        $bProtLocked = ($conditional->getStyle()->getProtection()->getLocked() == null ? 1 : 0);
+        $bProtHidden = ($conditional->getStyle()->getProtection()->getHidden() == null ? 1 : 0);
+        if ($bProtLocked == 0 || $bProtHidden == 0) {
+            $bFormatProt = 1;
+        } else {
+            $bFormatProt = 0;
+        }
+        // Border
+        $bBorderLeft = ($conditional->getStyle()->getBorders()->getLeft()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
+        $bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
+        $bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
+        $bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
+        if ($bBorderLeft === 1 || $bBorderRight === 1 || $bBorderTop === 1 || $bBorderBottom === 1) {
+            $bFormatBorder = 1;
+        } else {
+            $bFormatBorder = 0;
+        }
+        // Pattern
+        $bFillStyle = ($conditional->getStyle()->getFill()->getFillType() === null ? 0 : 1);
+        $bFillColor = ($conditional->getStyle()->getFill()->getStartColor()->getARGB() === null ? 0 : 1);
+        $bFillColorBg = ($conditional->getStyle()->getFill()->getEndColor()->getARGB() === null ? 0 : 1);
+        if ($bFillStyle == 1 || $bFillColor == 1 || $bFillColorBg == 1) {
+            $bFormatFill = 1;
+        } else {
+            $bFormatFill = 0;
+        }
+        // Font
+        if (
+            $conditional->getStyle()->getFont()->getName() !== null
+            || $conditional->getStyle()->getFont()->getSize() !== null
+            || $conditional->getStyle()->getFont()->getBold() !== null
+            || $conditional->getStyle()->getFont()->getItalic() !== null
+            || $conditional->getStyle()->getFont()->getSuperscript() !== null
+            || $conditional->getStyle()->getFont()->getSubscript() !== null
+            || $conditional->getStyle()->getFont()->getUnderline() !== null
+            || $conditional->getStyle()->getFont()->getStrikethrough() !== null
+            || $conditional->getStyle()->getFont()->getColor()->getARGB() !== null
+        ) {
+            $bFormatFont = 1;
+        } else {
+            $bFormatFont = 0;
+        }
+        // Alignment
+        $flags = 0;
+        $flags |= (1 == $bAlignHz ? 0x00000001 : 0);
+        $flags |= (1 == $bAlignVt ? 0x00000002 : 0);
+        $flags |= (1 == $bAlignWrapTx ? 0x00000004 : 0);
+        $flags |= (1 == $bTxRotation ? 0x00000008 : 0);
+        // Justify last line flag
+        $flags |= (1 == self::$always1 ? 0x00000010 : 0);
+        $flags |= (1 == $bIndent ? 0x00000020 : 0);
+        $flags |= (1 == $bShrinkToFit ? 0x00000040 : 0);
+        // Default
+        $flags |= (1 == self::$always1 ? 0x00000080 : 0);
+        // Protection
+        $flags |= (1 == $bProtLocked ? 0x00000100 : 0);
+        $flags |= (1 == $bProtHidden ? 0x00000200 : 0);
+        // Border
+        $flags |= (1 == $bBorderLeft ? 0x00000400 : 0);
+        $flags |= (1 == $bBorderRight ? 0x00000800 : 0);
+        $flags |= (1 == $bBorderTop ? 0x00001000 : 0);
+        $flags |= (1 == $bBorderBottom ? 0x00002000 : 0);
+        $flags |= (1 == self::$always1 ? 0x00004000 : 0); // Top left to Bottom right border
+        $flags |= (1 == self::$always1 ? 0x00008000 : 0); // Bottom left to Top right border
+        // Pattern
+        $flags |= (1 == $bFillStyle ? 0x00010000 : 0);
+        $flags |= (1 == $bFillColor ? 0x00020000 : 0);
+        $flags |= (1 == $bFillColorBg ? 0x00040000 : 0);
+        $flags |= (1 == self::$always1 ? 0x00380000 : 0);
+        // Font
+        $flags |= (1 == $bFormatFont ? 0x04000000 : 0);
+        // Alignment:
+        $flags |= (1 == $bFormatAlign ? 0x08000000 : 0);
+        // Border
+        $flags |= (1 == $bFormatBorder ? 0x10000000 : 0);
+        // Pattern
+        $flags |= (1 == $bFormatFill ? 0x20000000 : 0);
+        // Protection
+        $flags |= (1 == $bFormatProt ? 0x40000000 : 0);
+        // Text direction
+        $flags |= (1 == self::$always0 ? 0x80000000 : 0);
+
+        $dataBlockFont = null;
+        $dataBlockAlign = null;
+        $dataBlockBorder = null;
+        $dataBlockFill = null;
+
+        // Data Blocks
+        if ($bFormatFont == 1) {
+            // Font Name
+            if ($conditional->getStyle()->getFont()->getName() === null) {
+                $dataBlockFont = pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000);
+                $dataBlockFont .= pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000);
+            } else {
+                $dataBlockFont = StringHelper::UTF8toBIFF8UnicodeLong($conditional->getStyle()->getFont()->getName());
+            }
+            // Font Size
+            if ($conditional->getStyle()->getFont()->getSize() === null) {
+                $dataBlockFont .= pack('V', 20 * 11);
+            } else {
+                $dataBlockFont .= pack('V', 20 * $conditional->getStyle()->getFont()->getSize());
+            }
+            // Font Options
+            $dataBlockFont .= pack('V', 0);
+            // Font weight
+            if ($conditional->getStyle()->getFont()->getBold() === true) {
+                $dataBlockFont .= pack('v', 0x02BC);
+            } else {
+                $dataBlockFont .= pack('v', 0x0190);
+            }
+            // Escapement type
+            if ($conditional->getStyle()->getFont()->getSubscript() === true) {
+                $dataBlockFont .= pack('v', 0x02);
+                $fontEscapement = 0;
+            } elseif ($conditional->getStyle()->getFont()->getSuperscript() === true) {
+                $dataBlockFont .= pack('v', 0x01);
+                $fontEscapement = 0;
+            } else {
+                $dataBlockFont .= pack('v', 0x00);
+                $fontEscapement = 1;
+            }
+            // Underline type
+            switch ($conditional->getStyle()->getFont()->getUnderline()) {
+                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE:
+                    $dataBlockFont .= pack('C', 0x00);
+                    $fontUnderline = 0;
+
+                    break;
+                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE:
+                    $dataBlockFont .= pack('C', 0x02);
+                    $fontUnderline = 0;
+
+                    break;
+                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING:
+                    $dataBlockFont .= pack('C', 0x22);
+                    $fontUnderline = 0;
+
+                    break;
+                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE:
+                    $dataBlockFont .= pack('C', 0x01);
+                    $fontUnderline = 0;
+
+                    break;
+                case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING:
+                    $dataBlockFont .= pack('C', 0x21);
+                    $fontUnderline = 0;
+
+                    break;
+                default:
+                    $dataBlockFont .= pack('C', 0x00);
+                    $fontUnderline = 1;
+
+                    break;
+            }
+            // Not used (3)
+            $dataBlockFont .= pack('vC', 0x0000, 0x00);
+            // Font color index
+            $colorIdx = Style\ColorMap::lookup($conditional->getStyle()->getFont()->getColor(), 0x00);
+
+            $dataBlockFont .= pack('V', $colorIdx);
+            // Not used (4)
+            $dataBlockFont .= pack('V', 0x00000000);
+            // Options flags for modified font attributes
+            $optionsFlags = 0;
+            $optionsFlagsBold = ($conditional->getStyle()->getFont()->getBold() === null ? 1 : 0);
+            $optionsFlags |= (1 == $optionsFlagsBold ? 0x00000002 : 0);
+            $optionsFlags |= (1 == self::$always1 ? 0x00000008 : 0);
+            $optionsFlags |= (1 == self::$always1 ? 0x00000010 : 0);
+            $optionsFlags |= (1 == self::$always0 ? 0x00000020 : 0);
+            $optionsFlags |= (1 == self::$always1 ? 0x00000080 : 0);
+            $dataBlockFont .= pack('V', $optionsFlags);
+            // Escapement type
+            $dataBlockFont .= pack('V', $fontEscapement);
+            // Underline type
+            $dataBlockFont .= pack('V', $fontUnderline);
+            // Always
+            $dataBlockFont .= pack('V', 0x00000000);
+            // Always
+            $dataBlockFont .= pack('V', 0x00000000);
+            // Not used (8)
+            $dataBlockFont .= pack('VV', 0x00000000, 0x00000000);
+            // Always
+            $dataBlockFont .= pack('v', 0x0001);
+        }
+        if ($bFormatAlign === 1) {
+            // Alignment and text break
+            $blockAlign = Style\CellAlignment::horizontal($conditional->getStyle()->getAlignment());
+            $blockAlign |= Style\CellAlignment::wrap($conditional->getStyle()->getAlignment()) << 3;
+            $blockAlign |= Style\CellAlignment::vertical($conditional->getStyle()->getAlignment()) << 4;
+            $blockAlign |= 0 << 7;
+
+            // Text rotation angle
+            $blockRotation = $conditional->getStyle()->getAlignment()->getTextRotation();
+
+            // Indentation
+            $blockIndent = $conditional->getStyle()->getAlignment()->getIndent();
+            if ($conditional->getStyle()->getAlignment()->getShrinkToFit() === true) {
+                $blockIndent |= 1 << 4;
+            } else {
+                $blockIndent |= 0 << 4;
+            }
+            $blockIndent |= 0 << 6;
+
+            // Relative indentation
+            $blockIndentRelative = 255;
+
+            $dataBlockAlign = pack('CCvvv', $blockAlign, $blockRotation, $blockIndent, $blockIndentRelative, 0x0000);
+        }
+        if ($bFormatBorder === 1) {
+            $blockLineStyle = Style\CellBorder::style($conditional->getStyle()->getBorders()->getLeft());
+            $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getRight()) << 4;
+            $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getTop()) << 8;
+            $blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getBottom()) << 12;
+
+            // TODO writeCFRule() => $blockLineStyle => Index Color for left line
+            // TODO writeCFRule() => $blockLineStyle => Index Color for right line
+            // TODO writeCFRule() => $blockLineStyle => Top-left to bottom-right on/off
+            // TODO writeCFRule() => $blockLineStyle => Bottom-left to top-right on/off
+            $blockColor = 0;
+            // TODO writeCFRule() => $blockColor => Index Color for top line
+            // TODO writeCFRule() => $blockColor => Index Color for bottom line
+            // TODO writeCFRule() => $blockColor => Index Color for diagonal line
+            $blockColor |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getDiagonal()) << 21;
+            $dataBlockBorder = pack('vv', $blockLineStyle, $blockColor);
+        }
+        if ($bFormatFill === 1) {
+            // Fill Pattern Style
+            $blockFillPatternStyle = Style\CellFill::style($conditional->getStyle()->getFill());
+            // Background Color
+            $colorIdxBg = Style\ColorMap::lookup($conditional->getStyle()->getFill()->getStartColor(), 0x41);
+            // Foreground Color
+            $colorIdxFg = Style\ColorMap::lookup($conditional->getStyle()->getFill()->getEndColor(), 0x40);
+
+            $dataBlockFill = pack('v', $blockFillPatternStyle);
+            $dataBlockFill .= pack('v', $colorIdxFg | ($colorIdxBg << 7));
+        }
+
+        $data = pack('CCvvVv', $type, $operatorType, $szValue1, $szValue2, $flags, 0x0000);
+        if ($bFormatFont === 1) { // Block Formatting : OK
+            $data .= $dataBlockFont;
+        }
+        if ($bFormatAlign === 1) {
+            $data .= $dataBlockAlign;
+        }
+        if ($bFormatBorder === 1) {
+            $data .= $dataBlockBorder;
+        }
+        if ($bFormatFill === 1) { // Block Formatting : OK
+            $data .= $dataBlockFill;
+        }
+        if ($bFormatProt == 1) {
+            $data .= $this->getDataBlockProtection($conditional);
+        }
+        if ($operand1 !== null) {
+            $data .= $operand1;
+        }
+        if ($operand2 !== null) {
+            $data .= $operand2;
+        }
+        $header = pack('vv', $record, strlen($data));
+        $this->append($header . $data);
+    }
+
+    /**
+     * Write CFHeader record.
+     *
+     * @param Conditional[] $conditionalStyles
+     */
+    private function writeCFHeader(string $cellCoordinate, array $conditionalStyles): bool
+    {
+        $record = 0x01B0; // Record identifier
+        $length = 0x0016; // Bytes to follow
+
+        $numColumnMin = null;
+        $numColumnMax = null;
+        $numRowMin = null;
+        $numRowMax = null;
+
+        $arrConditional = [];
+        foreach ($conditionalStyles as $conditional) {
+            if (!in_array($conditional->getHashCode(), $arrConditional)) {
+                $arrConditional[] = $conditional->getHashCode();
+            }
+            // Cells
+            $rangeCoordinates = Coordinate::rangeBoundaries($cellCoordinate);
+            if ($numColumnMin === null || ($numColumnMin > $rangeCoordinates[0][0])) {
+                $numColumnMin = $rangeCoordinates[0][0];
+            }
+            if ($numColumnMax === null || ($numColumnMax < $rangeCoordinates[1][0])) {
+                $numColumnMax = $rangeCoordinates[1][0];
+            }
+            if ($numRowMin === null || ($numRowMin > $rangeCoordinates[0][1])) {
+                $numRowMin = (int) $rangeCoordinates[0][1];
+            }
+            if ($numRowMax === null || ($numRowMax < $rangeCoordinates[1][1])) {
+                $numRowMax = (int) $rangeCoordinates[1][1];
+            }
+        }
+
+        if (count($arrConditional) === 0) {
+            return false;
+        }
+
+        $needRedraw = 1;
+        $cellRange = pack('vvvv', $numRowMin - 1, $numRowMax - 1, $numColumnMin - 1, $numColumnMax - 1);
+
+        $header = pack('vv', $record, $length);
+        $data = pack('vv', count($arrConditional), $needRedraw);
+        $data .= $cellRange;
+        $data .= pack('v', 0x0001);
+        $data .= $cellRange;
+        $this->append($header . $data);
+
+        return true;
+    }
+
+    private function getDataBlockProtection(Conditional $conditional): int
+    {
+        $dataBlockProtection = 0;
+        if ($conditional->getStyle()->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED) {
+            $dataBlockProtection = 1;
+        }
+        if ($conditional->getStyle()->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED) {
+            $dataBlockProtection = 1 << 1;
+        }
+
+        return $dataBlockProtection;
+    }
+}

+ 415 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Xf.php

@@ -0,0 +1,415 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Borders;
+use PhpOffice\PhpSpreadsheet\Style\Protection;
+use PhpOffice\PhpSpreadsheet\Style\Style;
+use PhpOffice\PhpSpreadsheet\Writer\Xls\Style\CellAlignment;
+use PhpOffice\PhpSpreadsheet\Writer\Xls\Style\CellBorder;
+use PhpOffice\PhpSpreadsheet\Writer\Xls\Style\CellFill;
+
+// Original file header of PEAR::Spreadsheet_Excel_Writer_Format (used as the base for this class):
+// -----------------------------------------------------------------------------------------
+// /*
+// *  Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
+// *
+// *  The majority of this is _NOT_ my code.  I simply ported it from the
+// *  PERL Spreadsheet::WriteExcel module.
+// *
+// *  The author of the Spreadsheet::WriteExcel module is John McNamara
+// *  <jmcnamara@cpan.org>
+// *
+// *  I _DO_ maintain this code, and John McNamara has nothing to do with the
+// *  porting of this code to PHP.  Any questions directly related to this
+// *  class library should be directed to me.
+// *
+// *  License Information:
+// *
+// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
+// *    Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+// *
+// *    This library is free software; you can redistribute it and/or
+// *    modify it under the terms of the GNU Lesser General Public
+// *    License as published by the Free Software Foundation; either
+// *    version 2.1 of the License, or (at your option) any later version.
+// *
+// *    This library is distributed in the hope that it will be useful,
+// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
+// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// *    Lesser General Public License for more details.
+// *
+// *    You should have received a copy of the GNU Lesser General Public
+// *    License along with this library; if not, write to the Free Software
+// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+// */
+class Xf
+{
+    /**
+     * Style XF or a cell XF ?
+     *
+     * @var bool
+     */
+    private $isStyleXf;
+
+    /**
+     * Index to the FONT record. Index 4 does not exist.
+     *
+     * @var int
+     */
+    private $fontIndex;
+
+    /**
+     * An index (2 bytes) to a FORMAT record (number format).
+     *
+     * @var int
+     */
+    private $numberFormatIndex;
+
+    /**
+     * 1 bit, apparently not used.
+     *
+     * @var int
+     */
+    private $textJustLast;
+
+    /**
+     * The cell's foreground color.
+     *
+     * @var int
+     */
+    private $foregroundColor;
+
+    /**
+     * The cell's background color.
+     *
+     * @var int
+     */
+    private $backgroundColor;
+
+    /**
+     * Color of the bottom border of the cell.
+     *
+     * @var int
+     */
+    private $bottomBorderColor;
+
+    /**
+     * Color of the top border of the cell.
+     *
+     * @var int
+     */
+    private $topBorderColor;
+
+    /**
+     * Color of the left border of the cell.
+     *
+     * @var int
+     */
+    private $leftBorderColor;
+
+    /**
+     * Color of the right border of the cell.
+     *
+     * @var int
+     */
+    private $rightBorderColor;
+
+    //private $diag; // theoretically int, not yet implemented
+
+    /**
+     * @var int
+     */
+    private $diagColor;
+
+    /**
+     * @var Style
+     */
+    private $style;
+
+    /**
+     * Constructor.
+     *
+     * @param Style $style The XF format
+     */
+    public function __construct(Style $style)
+    {
+        $this->isStyleXf = false;
+        $this->fontIndex = 0;
+
+        $this->numberFormatIndex = 0;
+
+        $this->textJustLast = 0;
+
+        $this->foregroundColor = 0x40;
+        $this->backgroundColor = 0x41;
+
+        //$this->diag = 0;
+
+        $this->bottomBorderColor = 0x40;
+        $this->topBorderColor = 0x40;
+        $this->leftBorderColor = 0x40;
+        $this->rightBorderColor = 0x40;
+        $this->diagColor = 0x40;
+        $this->style = $style;
+    }
+
+    /**
+     * Generate an Excel BIFF XF record (style or cell).
+     *
+     * @return string The XF record
+     */
+    public function writeXf()
+    {
+        // Set the type of the XF record and some of the attributes.
+        if ($this->isStyleXf) {
+            $style = 0xFFF5;
+        } else {
+            $style = self::mapLocked($this->style->getProtection()->getLocked());
+            $style |= self::mapHidden($this->style->getProtection()->getHidden()) << 1;
+        }
+
+        // Flags to indicate if attributes have been set.
+        $atr_num = ($this->numberFormatIndex != 0) ? 1 : 0;
+        $atr_fnt = ($this->fontIndex != 0) ? 1 : 0;
+        $atr_alc = ((int) $this->style->getAlignment()->getWrapText()) ? 1 : 0;
+        $atr_bdr = (CellBorder::style($this->style->getBorders()->getBottom()) ||
+            CellBorder::style($this->style->getBorders()->getTop()) ||
+            CellBorder::style($this->style->getBorders()->getLeft()) ||
+            CellBorder::style($this->style->getBorders()->getRight())) ? 1 : 0;
+        $atr_pat = ($this->foregroundColor != 0x40) ? 1 : 0;
+        $atr_pat = ($this->backgroundColor != 0x41) ? 1 : $atr_pat;
+        $atr_pat = CellFill::style($this->style->getFill()) ? 1 : $atr_pat;
+        $atr_prot = self::mapLocked($this->style->getProtection()->getLocked())
+            | self::mapHidden($this->style->getProtection()->getHidden());
+
+        // Zero the default border colour if the border has not been set.
+        if (CellBorder::style($this->style->getBorders()->getBottom()) == 0) {
+            $this->bottomBorderColor = 0;
+        }
+        if (CellBorder::style($this->style->getBorders()->getTop()) == 0) {
+            $this->topBorderColor = 0;
+        }
+        if (CellBorder::style($this->style->getBorders()->getRight()) == 0) {
+            $this->rightBorderColor = 0;
+        }
+        if (CellBorder::style($this->style->getBorders()->getLeft()) == 0) {
+            $this->leftBorderColor = 0;
+        }
+        if (CellBorder::style($this->style->getBorders()->getDiagonal()) == 0) {
+            $this->diagColor = 0;
+        }
+
+        $record = 0x00E0; // Record identifier
+        $length = 0x0014; // Number of bytes to follow
+
+        $ifnt = $this->fontIndex; // Index to FONT record
+        $ifmt = $this->numberFormatIndex; // Index to FORMAT record
+
+        // Alignment
+        $align = CellAlignment::horizontal($this->style->getAlignment());
+        $align |= CellAlignment::wrap($this->style->getAlignment()) << 3;
+        $align |= CellAlignment::vertical($this->style->getAlignment()) << 4;
+        $align |= $this->textJustLast << 7;
+
+        $used_attrib = $atr_num << 2;
+        $used_attrib |= $atr_fnt << 3;
+        $used_attrib |= $atr_alc << 4;
+        $used_attrib |= $atr_bdr << 5;
+        $used_attrib |= $atr_pat << 6;
+        $used_attrib |= $atr_prot << 7;
+
+        $icv = $this->foregroundColor; // fg and bg pattern colors
+        $icv |= $this->backgroundColor << 7;
+
+        $border1 = CellBorder::style($this->style->getBorders()->getLeft()); // Border line style and color
+        $border1 |= CellBorder::style($this->style->getBorders()->getRight()) << 4;
+        $border1 |= CellBorder::style($this->style->getBorders()->getTop()) << 8;
+        $border1 |= CellBorder::style($this->style->getBorders()->getBottom()) << 12;
+        $border1 |= $this->leftBorderColor << 16;
+        $border1 |= $this->rightBorderColor << 23;
+
+        $diagonalDirection = $this->style->getBorders()->getDiagonalDirection();
+        $diag_tl_to_rb = $diagonalDirection == Borders::DIAGONAL_BOTH
+            || $diagonalDirection == Borders::DIAGONAL_DOWN;
+        $diag_tr_to_lb = $diagonalDirection == Borders::DIAGONAL_BOTH
+            || $diagonalDirection == Borders::DIAGONAL_UP;
+        $border1 |= $diag_tl_to_rb << 30;
+        $border1 |= $diag_tr_to_lb << 31;
+
+        $border2 = $this->topBorderColor; // Border color
+        $border2 |= $this->bottomBorderColor << 7;
+        $border2 |= $this->diagColor << 14;
+        $border2 |= CellBorder::style($this->style->getBorders()->getDiagonal()) << 21;
+        $border2 |= CellFill::style($this->style->getFill()) << 26;
+
+        $header = pack('vv', $record, $length);
+
+        //BIFF8 options: identation, shrinkToFit and  text direction
+        $biff8_options = $this->style->getAlignment()->getIndent();
+        $biff8_options |= (int) $this->style->getAlignment()->getShrinkToFit() << 4;
+
+        $data = pack('vvvC', $ifnt, $ifmt, $style, $align);
+        $data .= pack('CCC', self::mapTextRotation((int) $this->style->getAlignment()->getTextRotation()), $biff8_options, $used_attrib);
+        $data .= pack('VVv', $border1, $border2, $icv);
+
+        return $header . $data;
+    }
+
+    /**
+     * Is this a style XF ?
+     *
+     * @param bool $value
+     */
+    public function setIsStyleXf($value): void
+    {
+        $this->isStyleXf = $value;
+    }
+
+    /**
+     * Sets the cell's bottom border color.
+     *
+     * @param int $colorIndex Color index
+     */
+    public function setBottomColor($colorIndex): void
+    {
+        $this->bottomBorderColor = $colorIndex;
+    }
+
+    /**
+     * Sets the cell's top border color.
+     *
+     * @param int $colorIndex Color index
+     */
+    public function setTopColor($colorIndex): void
+    {
+        $this->topBorderColor = $colorIndex;
+    }
+
+    /**
+     * Sets the cell's left border color.
+     *
+     * @param int $colorIndex Color index
+     */
+    public function setLeftColor($colorIndex): void
+    {
+        $this->leftBorderColor = $colorIndex;
+    }
+
+    /**
+     * Sets the cell's right border color.
+     *
+     * @param int $colorIndex Color index
+     */
+    public function setRightColor($colorIndex): void
+    {
+        $this->rightBorderColor = $colorIndex;
+    }
+
+    /**
+     * Sets the cell's diagonal border color.
+     *
+     * @param int $colorIndex Color index
+     */
+    public function setDiagColor($colorIndex): void
+    {
+        $this->diagColor = $colorIndex;
+    }
+
+    /**
+     * Sets the cell's foreground color.
+     *
+     * @param int $colorIndex Color index
+     */
+    public function setFgColor($colorIndex): void
+    {
+        $this->foregroundColor = $colorIndex;
+    }
+
+    /**
+     * Sets the cell's background color.
+     *
+     * @param int $colorIndex Color index
+     */
+    public function setBgColor($colorIndex): void
+    {
+        $this->backgroundColor = $colorIndex;
+    }
+
+    /**
+     * Sets the index to the number format record
+     * It can be date, time, currency, etc...
+     *
+     * @param int $numberFormatIndex Index to format record
+     */
+    public function setNumberFormatIndex($numberFormatIndex): void
+    {
+        $this->numberFormatIndex = $numberFormatIndex;
+    }
+
+    /**
+     * Set the font index.
+     *
+     * @param int $value Font index, note that value 4 does not exist
+     */
+    public function setFontIndex($value): void
+    {
+        $this->fontIndex = $value;
+    }
+
+    /**
+     * Map to BIFF8 codes for text rotation angle.
+     *
+     * @param int $textRotation
+     *
+     * @return int
+     */
+    private static function mapTextRotation($textRotation)
+    {
+        if ($textRotation >= 0) {
+            return $textRotation;
+        }
+        if ($textRotation == Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) {
+            return Alignment::TEXTROTATION_STACK_EXCEL;
+        }
+
+        return 90 - $textRotation;
+    }
+
+    private const LOCK_ARRAY = [
+        Protection::PROTECTION_INHERIT => 1,
+        Protection::PROTECTION_PROTECTED => 1,
+        Protection::PROTECTION_UNPROTECTED => 0,
+    ];
+
+    /**
+     * Map locked values.
+     *
+     * @param string $locked
+     *
+     * @return int
+     */
+    private static function mapLocked($locked)
+    {
+        return array_key_exists($locked, self::LOCK_ARRAY) ? self::LOCK_ARRAY[$locked] : 1;
+    }
+
+    private const HIDDEN_ARRAY = [
+        Protection::PROTECTION_INHERIT => 0,
+        Protection::PROTECTION_PROTECTED => 1,
+        Protection::PROTECTION_UNPROTECTED => 0,
+    ];
+
+    /**
+     * Map hidden.
+     *
+     * @param string $hidden
+     *
+     * @return int
+     */
+    private static function mapHidden($hidden)
+    {
+        return array_key_exists($hidden, self::HIDDEN_ARRAY) ? self::HIDDEN_ARRAY[$hidden] : 0;
+    }
+}

+ 763 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php

@@ -0,0 +1,763 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer;
+
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Calculation\Functions;
+use PhpOffice\PhpSpreadsheet\HashTable;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Borders;
+use PhpOffice\PhpSpreadsheet\Style\Conditional;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+use PhpOffice\PhpSpreadsheet\Style\Font;
+use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
+use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
+use PhpOffice\PhpSpreadsheet\Worksheet\Drawing as WorksheetDrawing;
+use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Chart;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Comments;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\ContentTypes;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\DocProps;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Drawing;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Rels;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsRibbon;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsVBA;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\StringTable;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Style;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Table;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Theme;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Workbook;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet;
+use ZipArchive;
+use ZipStream\Exception\OverflowException;
+use ZipStream\ZipStream;
+
+class Xlsx extends BaseWriter
+{
+    /**
+     * Office2003 compatibility.
+     *
+     * @var bool
+     */
+    private $office2003compatibility = false;
+
+    /**
+     * Private Spreadsheet.
+     *
+     * @var Spreadsheet
+     */
+    private $spreadSheet;
+
+    /**
+     * Private string table.
+     *
+     * @var string[]
+     */
+    private $stringTable = [];
+
+    /**
+     * Private unique Conditional HashTable.
+     *
+     * @var HashTable<Conditional>
+     */
+    private $stylesConditionalHashTable;
+
+    /**
+     * Private unique Style HashTable.
+     *
+     * @var HashTable<\PhpOffice\PhpSpreadsheet\Style\Style>
+     */
+    private $styleHashTable;
+
+    /**
+     * Private unique Fill HashTable.
+     *
+     * @var HashTable<Fill>
+     */
+    private $fillHashTable;
+
+    /**
+     * Private unique \PhpOffice\PhpSpreadsheet\Style\Font HashTable.
+     *
+     * @var HashTable<Font>
+     */
+    private $fontHashTable;
+
+    /**
+     * Private unique Borders HashTable.
+     *
+     * @var HashTable<Borders>
+     */
+    private $bordersHashTable;
+
+    /**
+     * Private unique NumberFormat HashTable.
+     *
+     * @var HashTable<NumberFormat>
+     */
+    private $numFmtHashTable;
+
+    /**
+     * Private unique \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\BaseDrawing HashTable.
+     *
+     * @var HashTable<BaseDrawing>
+     */
+    private $drawingHashTable;
+
+    /**
+     * Private handle for zip stream.
+     *
+     * @var ZipStream
+     */
+    private $zip;
+
+    /**
+     * @var Chart
+     */
+    private $writerPartChart;
+
+    /**
+     * @var Comments
+     */
+    private $writerPartComments;
+
+    /**
+     * @var ContentTypes
+     */
+    private $writerPartContentTypes;
+
+    /**
+     * @var DocProps
+     */
+    private $writerPartDocProps;
+
+    /**
+     * @var Drawing
+     */
+    private $writerPartDrawing;
+
+    /**
+     * @var Rels
+     */
+    private $writerPartRels;
+
+    /**
+     * @var RelsRibbon
+     */
+    private $writerPartRelsRibbon;
+
+    /**
+     * @var RelsVBA
+     */
+    private $writerPartRelsVBA;
+
+    /**
+     * @var StringTable
+     */
+    private $writerPartStringTable;
+
+    /**
+     * @var Style
+     */
+    private $writerPartStyle;
+
+    /**
+     * @var Theme
+     */
+    private $writerPartTheme;
+
+    /**
+     * @var Table
+     */
+    private $writerPartTable;
+
+    /**
+     * @var Workbook
+     */
+    private $writerPartWorkbook;
+
+    /**
+     * @var Worksheet
+     */
+    private $writerPartWorksheet;
+
+    /**
+     * Create a new Xlsx Writer.
+     */
+    public function __construct(Spreadsheet $spreadsheet)
+    {
+        // Assign PhpSpreadsheet
+        $this->setSpreadsheet($spreadsheet);
+
+        $this->writerPartChart = new Chart($this);
+        $this->writerPartComments = new Comments($this);
+        $this->writerPartContentTypes = new ContentTypes($this);
+        $this->writerPartDocProps = new DocProps($this);
+        $this->writerPartDrawing = new Drawing($this);
+        $this->writerPartRels = new Rels($this);
+        $this->writerPartRelsRibbon = new RelsRibbon($this);
+        $this->writerPartRelsVBA = new RelsVBA($this);
+        $this->writerPartStringTable = new StringTable($this);
+        $this->writerPartStyle = new Style($this);
+        $this->writerPartTheme = new Theme($this);
+        $this->writerPartTable = new Table($this);
+        $this->writerPartWorkbook = new Workbook($this);
+        $this->writerPartWorksheet = new Worksheet($this);
+
+        // Set HashTable variables
+        // @phpstan-ignore-next-line
+        $this->bordersHashTable = new HashTable();
+        // @phpstan-ignore-next-line
+        $this->drawingHashTable = new HashTable();
+        // @phpstan-ignore-next-line
+        $this->fillHashTable = new HashTable();
+        // @phpstan-ignore-next-line
+        $this->fontHashTable = new HashTable();
+        // @phpstan-ignore-next-line
+        $this->numFmtHashTable = new HashTable();
+        // @phpstan-ignore-next-line
+        $this->styleHashTable = new HashTable();
+        // @phpstan-ignore-next-line
+        $this->stylesConditionalHashTable = new HashTable();
+    }
+
+    public function getWriterPartChart(): Chart
+    {
+        return $this->writerPartChart;
+    }
+
+    public function getWriterPartComments(): Comments
+    {
+        return $this->writerPartComments;
+    }
+
+    public function getWriterPartContentTypes(): ContentTypes
+    {
+        return $this->writerPartContentTypes;
+    }
+
+    public function getWriterPartDocProps(): DocProps
+    {
+        return $this->writerPartDocProps;
+    }
+
+    public function getWriterPartDrawing(): Drawing
+    {
+        return $this->writerPartDrawing;
+    }
+
+    public function getWriterPartRels(): Rels
+    {
+        return $this->writerPartRels;
+    }
+
+    public function getWriterPartRelsRibbon(): RelsRibbon
+    {
+        return $this->writerPartRelsRibbon;
+    }
+
+    public function getWriterPartRelsVBA(): RelsVBA
+    {
+        return $this->writerPartRelsVBA;
+    }
+
+    public function getWriterPartStringTable(): StringTable
+    {
+        return $this->writerPartStringTable;
+    }
+
+    public function getWriterPartStyle(): Style
+    {
+        return $this->writerPartStyle;
+    }
+
+    public function getWriterPartTheme(): Theme
+    {
+        return $this->writerPartTheme;
+    }
+
+    public function getWriterPartTable(): Table
+    {
+        return $this->writerPartTable;
+    }
+
+    public function getWriterPartWorkbook(): Workbook
+    {
+        return $this->writerPartWorkbook;
+    }
+
+    public function getWriterPartWorksheet(): Worksheet
+    {
+        return $this->writerPartWorksheet;
+    }
+
+    /**
+     * Save PhpSpreadsheet to file.
+     *
+     * @param resource|string $filename
+     */
+    public function save($filename, int $flags = 0): void
+    {
+        $this->processFlags($flags);
+
+        // garbage collect
+        $this->pathNames = [];
+        $this->spreadSheet->garbageCollect();
+
+        $saveDebugLog = Calculation::getInstance($this->spreadSheet)->getDebugLog()->getWriteDebugLog();
+        Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog(false);
+        $saveDateReturnType = Functions::getReturnDateType();
+        Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
+
+        // Create string lookup table
+        $this->stringTable = [];
+        for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
+            $this->stringTable = $this->getWriterPartStringTable()->createStringTable($this->spreadSheet->getSheet($i), $this->stringTable);
+        }
+
+        // Create styles dictionaries
+        $this->styleHashTable->addFromSource($this->getWriterPartStyle()->allStyles($this->spreadSheet));
+        $this->stylesConditionalHashTable->addFromSource($this->getWriterPartStyle()->allConditionalStyles($this->spreadSheet));
+        $this->fillHashTable->addFromSource($this->getWriterPartStyle()->allFills($this->spreadSheet));
+        $this->fontHashTable->addFromSource($this->getWriterPartStyle()->allFonts($this->spreadSheet));
+        $this->bordersHashTable->addFromSource($this->getWriterPartStyle()->allBorders($this->spreadSheet));
+        $this->numFmtHashTable->addFromSource($this->getWriterPartStyle()->allNumberFormats($this->spreadSheet));
+
+        // Create drawing dictionary
+        $this->drawingHashTable->addFromSource($this->getWriterPartDrawing()->allDrawings($this->spreadSheet));
+
+        $zipContent = [];
+        // Add [Content_Types].xml to ZIP file
+        $zipContent['[Content_Types].xml'] = $this->getWriterPartContentTypes()->writeContentTypes($this->spreadSheet, $this->includeCharts);
+
+        //if hasMacros, add the vbaProject.bin file, Certificate file(if exists)
+        if ($this->spreadSheet->hasMacros()) {
+            $macrosCode = $this->spreadSheet->getMacrosCode();
+            if ($macrosCode !== null) {
+                // we have the code ?
+                $zipContent['xl/vbaProject.bin'] = $macrosCode; //allways in 'xl', allways named vbaProject.bin
+                if ($this->spreadSheet->hasMacrosCertificate()) {
+                    //signed macros ?
+                    // Yes : add the certificate file and the related rels file
+                    $zipContent['xl/vbaProjectSignature.bin'] = $this->spreadSheet->getMacrosCertificate();
+                    $zipContent['xl/_rels/vbaProject.bin.rels'] = $this->getWriterPartRelsVBA()->writeVBARelationships();
+                }
+            }
+        }
+        //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels)
+        if ($this->spreadSheet->hasRibbon()) {
+            $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target');
+            $tmpRibbonTarget = is_string($tmpRibbonTarget) ? $tmpRibbonTarget : '';
+            $zipContent[$tmpRibbonTarget] = $this->spreadSheet->getRibbonXMLData('data');
+            if ($this->spreadSheet->hasRibbonBinObjects()) {
+                $tmpRootPath = dirname($tmpRibbonTarget) . '/';
+                $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write
+                if (is_array($ribbonBinObjects)) {
+                    foreach ($ribbonBinObjects as $aPath => $aContent) {
+                        $zipContent[$tmpRootPath . $aPath] = $aContent;
+                    }
+                }
+                //the rels for files
+                $zipContent[$tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels'] = $this->getWriterPartRelsRibbon()->writeRibbonRelationships($this->spreadSheet);
+            }
+        }
+
+        // Add relationships to ZIP file
+        $zipContent['_rels/.rels'] = $this->getWriterPartRels()->writeRelationships($this->spreadSheet);
+        $zipContent['xl/_rels/workbook.xml.rels'] = $this->getWriterPartRels()->writeWorkbookRelationships($this->spreadSheet);
+
+        // Add document properties to ZIP file
+        $zipContent['docProps/app.xml'] = $this->getWriterPartDocProps()->writeDocPropsApp($this->spreadSheet);
+        $zipContent['docProps/core.xml'] = $this->getWriterPartDocProps()->writeDocPropsCore($this->spreadSheet);
+        $customPropertiesPart = $this->getWriterPartDocProps()->writeDocPropsCustom($this->spreadSheet);
+        if ($customPropertiesPart !== null) {
+            $zipContent['docProps/custom.xml'] = $customPropertiesPart;
+        }
+
+        // Add theme to ZIP file
+        $zipContent['xl/theme/theme1.xml'] = $this->getWriterPartTheme()->writeTheme($this->spreadSheet);
+
+        // Add string table to ZIP file
+        $zipContent['xl/sharedStrings.xml'] = $this->getWriterPartStringTable()->writeStringTable($this->stringTable);
+
+        // Add styles to ZIP file
+        $zipContent['xl/styles.xml'] = $this->getWriterPartStyle()->writeStyles($this->spreadSheet);
+
+        // Add workbook to ZIP file
+        $zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas);
+
+        $chartCount = 0;
+        // Add worksheets
+        for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
+            $zipContent['xl/worksheets/sheet' . ($i + 1) . '.xml'] = $this->getWriterPartWorksheet()->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts);
+            if ($this->includeCharts) {
+                $charts = $this->spreadSheet->getSheet($i)->getChartCollection();
+                if (count($charts) > 0) {
+                    foreach ($charts as $chart) {
+                        $zipContent['xl/charts/chart' . ($chartCount + 1) . '.xml'] = $this->getWriterPartChart()->writeChart($chart, $this->preCalculateFormulas);
+                        ++$chartCount;
+                    }
+                }
+            }
+        }
+
+        $chartRef1 = 0;
+        $tableRef1 = 1;
+        // Add worksheet relationships (drawings, ...)
+        for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
+            // Add relationships
+            $zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts, $tableRef1);
+
+            // Add unparsedLoadedData
+            $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName();
+            $unparsedLoadedData = $this->spreadSheet->getUnparsedLoadedData();
+            if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'])) {
+                foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'] as $ctrlProp) {
+                    $zipContent[$ctrlProp['filePath']] = $ctrlProp['content'];
+                }
+            }
+            if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'])) {
+                foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'] as $ctrlProp) {
+                    $zipContent[$ctrlProp['filePath']] = $ctrlProp['content'];
+                }
+            }
+
+            $drawings = $this->spreadSheet->getSheet($i)->getDrawingCollection();
+            $drawingCount = count($drawings);
+            if ($this->includeCharts) {
+                $chartCount = $this->spreadSheet->getSheet($i)->getChartCount();
+            }
+
+            // Add drawing and image relationship parts
+            if (($drawingCount > 0) || ($chartCount > 0)) {
+                // Drawing relationships
+                $zipContent['xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts);
+
+                // Drawings
+                $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = $this->getWriterPartDrawing()->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts);
+            } elseif (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingAlternateContents'])) {
+                // Drawings
+                $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = $this->getWriterPartDrawing()->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts);
+            }
+
+            // Add unparsed drawings
+            if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['Drawings'])) {
+                foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['Drawings'] as $relId => $drawingXml) {
+                    $drawingFile = array_search($relId, $unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']);
+                    if ($drawingFile !== false) {
+                        //$drawingFile = ltrim($drawingFile, '.');
+                        //$zipContent['xl' . $drawingFile] = $drawingXml;
+                        $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = $drawingXml;
+                    }
+                }
+            }
+
+            // Add comment relationship parts
+            $legacy = $unparsedLoadedData['sheets'][$this->spreadSheet->getSheet($i)->getCodeName()]['legacyDrawing'] ?? null;
+            if (count($this->spreadSheet->getSheet($i)->getComments()) > 0 || $legacy !== null) {
+                // VML Comments relationships
+                $zipContent['xl/drawings/_rels/vmlDrawing' . ($i + 1) . '.vml.rels'] = $this->getWriterPartRels()->writeVMLDrawingRelationships($this->spreadSheet->getSheet($i));
+
+                // VML Comments
+                $zipContent['xl/drawings/vmlDrawing' . ($i + 1) . '.vml'] = $legacy ?? $this->getWriterPartComments()->writeVMLComments($this->spreadSheet->getSheet($i));
+            }
+
+            // Comments
+            if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) {
+                $zipContent['xl/comments' . ($i + 1) . '.xml'] = $this->getWriterPartComments()->writeComments($this->spreadSheet->getSheet($i));
+
+                // Media
+                foreach ($this->spreadSheet->getSheet($i)->getComments() as $comment) {
+                    if ($comment->hasBackgroundImage()) {
+                        $image = $comment->getBackgroundImage();
+                        $zipContent['xl/media/' . $image->getMediaFilename()] = $this->processDrawing($image);
+                    }
+                }
+            }
+
+            // Add unparsed relationship parts
+            if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'])) {
+                foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'] as $vmlDrawing) {
+                    if (!isset($zipContent[$vmlDrawing['filePath']])) {
+                        $zipContent[$vmlDrawing['filePath']] = $vmlDrawing['content'];
+                    }
+                }
+            }
+
+            // Add header/footer relationship parts
+            if (count($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) {
+                // VML Drawings
+                $zipContent['xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml'] = $this->getWriterPartDrawing()->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i));
+
+                // VML Drawing relationships
+                $zipContent['xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels'] = $this->getWriterPartRels()->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i));
+
+                // Media
+                foreach ($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages() as $image) {
+                    if ($image->getPath() !== '') {
+                        $zipContent['xl/media/' . $image->getIndexedFilename()] = file_get_contents($image->getPath());
+                    }
+                }
+            }
+
+            // Add Table parts
+            $tables = $this->spreadSheet->getSheet($i)->getTableCollection();
+            foreach ($tables as $table) {
+                $zipContent['xl/tables/table' . $tableRef1 . '.xml'] = $this->getWriterPartTable()->writeTable($table, $tableRef1++);
+            }
+        }
+
+        // Add media
+        for ($i = 0; $i < $this->getDrawingHashTable()->count(); ++$i) {
+            if ($this->getDrawingHashTable()->getByIndex($i) instanceof WorksheetDrawing) {
+                $imageContents = null;
+                $imagePath = $this->getDrawingHashTable()->getByIndex($i)->getPath();
+                if ($imagePath === '') {
+                    continue;
+                }
+                if (strpos($imagePath, 'zip://') !== false) {
+                    $imagePath = substr($imagePath, 6);
+                    $imagePathSplitted = explode('#', $imagePath);
+
+                    $imageZip = new ZipArchive();
+                    $imageZip->open($imagePathSplitted[0]);
+                    $imageContents = $imageZip->getFromName($imagePathSplitted[1]);
+                    $imageZip->close();
+                    unset($imageZip);
+                } else {
+                    $imageContents = file_get_contents($imagePath);
+                }
+
+                $zipContent['xl/media/' . $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()] = $imageContents;
+            } elseif ($this->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) {
+                ob_start();
+                /** @var callable */
+                $callable = $this->getDrawingHashTable()->getByIndex($i)->getRenderingFunction();
+                call_user_func(
+                    $callable,
+                    $this->getDrawingHashTable()->getByIndex($i)->getImageResource()
+                );
+                $imageContents = ob_get_contents();
+                ob_end_clean();
+
+                $zipContent['xl/media/' . $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()] = $imageContents;
+            }
+        }
+
+        Functions::setReturnDateType($saveDateReturnType);
+        Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
+
+        $this->openFileHandle($filename);
+
+        $this->zip = ZipStream0::newZipStream($this->fileHandle);
+
+        $this->addZipFiles($zipContent);
+
+        // Close file
+        try {
+            $this->zip->finish();
+        } catch (OverflowException $e) {
+            throw new WriterException('Could not close resource.');
+        }
+
+        $this->maybeCloseFileHandle();
+    }
+
+    /**
+     * Get Spreadsheet object.
+     *
+     * @return Spreadsheet
+     */
+    public function getSpreadsheet()
+    {
+        return $this->spreadSheet;
+    }
+
+    /**
+     * Set Spreadsheet object.
+     *
+     * @param Spreadsheet $spreadsheet PhpSpreadsheet object
+     *
+     * @return $this
+     */
+    public function setSpreadsheet(Spreadsheet $spreadsheet)
+    {
+        $this->spreadSheet = $spreadsheet;
+
+        return $this;
+    }
+
+    /**
+     * Get string table.
+     *
+     * @return string[]
+     */
+    public function getStringTable()
+    {
+        return $this->stringTable;
+    }
+
+    /**
+     * Get Style HashTable.
+     *
+     * @return HashTable<\PhpOffice\PhpSpreadsheet\Style\Style>
+     */
+    public function getStyleHashTable()
+    {
+        return $this->styleHashTable;
+    }
+
+    /**
+     * Get Conditional HashTable.
+     *
+     * @return HashTable<Conditional>
+     */
+    public function getStylesConditionalHashTable()
+    {
+        return $this->stylesConditionalHashTable;
+    }
+
+    /**
+     * Get Fill HashTable.
+     *
+     * @return HashTable<Fill>
+     */
+    public function getFillHashTable()
+    {
+        return $this->fillHashTable;
+    }
+
+    /**
+     * Get \PhpOffice\PhpSpreadsheet\Style\Font HashTable.
+     *
+     * @return HashTable<Font>
+     */
+    public function getFontHashTable()
+    {
+        return $this->fontHashTable;
+    }
+
+    /**
+     * Get Borders HashTable.
+     *
+     * @return HashTable<Borders>
+     */
+    public function getBordersHashTable()
+    {
+        return $this->bordersHashTable;
+    }
+
+    /**
+     * Get NumberFormat HashTable.
+     *
+     * @return HashTable<NumberFormat>
+     */
+    public function getNumFmtHashTable()
+    {
+        return $this->numFmtHashTable;
+    }
+
+    /**
+     * Get \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\BaseDrawing HashTable.
+     *
+     * @return HashTable<BaseDrawing>
+     */
+    public function getDrawingHashTable()
+    {
+        return $this->drawingHashTable;
+    }
+
+    /**
+     * Get Office2003 compatibility.
+     *
+     * @return bool
+     */
+    public function getOffice2003Compatibility()
+    {
+        return $this->office2003compatibility;
+    }
+
+    /**
+     * Set Office2003 compatibility.
+     *
+     * @param bool $office2003compatibility Office2003 compatibility?
+     *
+     * @return $this
+     */
+    public function setOffice2003Compatibility($office2003compatibility)
+    {
+        $this->office2003compatibility = $office2003compatibility;
+
+        return $this;
+    }
+
+    /** @var array */
+    private $pathNames = [];
+
+    private function addZipFile(string $path, string $content): void
+    {
+        if (!in_array($path, $this->pathNames)) {
+            $this->pathNames[] = $path;
+            $this->zip->addFile($path, $content);
+        }
+    }
+
+    private function addZipFiles(array $zipContent): void
+    {
+        foreach ($zipContent as $path => $content) {
+            $this->addZipFile($path, $content);
+        }
+    }
+
+    /**
+     * @return mixed
+     */
+    private function processDrawing(WorksheetDrawing $drawing)
+    {
+        $data = null;
+        $filename = $drawing->getPath();
+        if ($filename === '') {
+            return null;
+        }
+        $imageData = getimagesize($filename);
+
+        if (!empty($imageData)) {
+            switch ($imageData[2]) {
+                case 1: // GIF, not supported by BIFF8, we convert to PNG
+                    $image = imagecreatefromgif($filename);
+                    if ($image !== false) {
+                        ob_start();
+                        imagepng($image);
+                        $data = ob_get_contents();
+                        ob_end_clean();
+                    }
+
+                    break;
+
+                case 2: // JPEG
+                    $data = file_get_contents($filename);
+
+                    break;
+
+                case 3: // PNG
+                    $data = file_get_contents($filename);
+
+                    break;
+
+                case 6: // Windows DIB (BMP), we convert to PNG
+                    $image = imagecreatefrombmp($filename);
+                    if ($image !== false) {
+                        ob_start();
+                        imagepng($image);
+                        $data = ob_get_contents();
+                        ob_end_clean();
+                    }
+
+                    break;
+            }
+        }
+
+        return $data;
+    }
+}

+ 125 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/AutoFilter.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column;
+use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as ActualWorksheet;
+
+class AutoFilter extends WriterPart
+{
+    /**
+     * Write AutoFilter.
+     */
+    public static function writeAutoFilter(XMLWriter $objWriter, ActualWorksheet $worksheet): void
+    {
+        $autoFilterRange = $worksheet->getAutoFilter()->getRange();
+        if (!empty($autoFilterRange)) {
+            // autoFilter
+            $objWriter->startElement('autoFilter');
+
+            // Strip any worksheet reference from the filter coordinates
+            $range = Coordinate::splitRange($autoFilterRange);
+            $range = $range[0];
+            //    Strip any worksheet ref
+            [$ws, $range[0]] = ActualWorksheet::extractSheetTitle($range[0], true);
+            $range = implode(':', $range);
+
+            $objWriter->writeAttribute('ref', str_replace('$', '', $range));
+
+            $columns = $worksheet->getAutoFilter()->getColumns();
+            if (count($columns) > 0) {
+                foreach ($columns as $columnID => $column) {
+                    $colId = $worksheet->getAutoFilter()->getColumnOffset($columnID);
+                    self::writeAutoFilterColumn($objWriter, $column, $colId);
+                }
+            }
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write AutoFilter's filterColumn.
+     */
+    public static function writeAutoFilterColumn(XMLWriter $objWriter, Column $column, int $colId): void
+    {
+        $rules = $column->getRules();
+        if (count($rules) > 0) {
+            $objWriter->startElement('filterColumn');
+            $objWriter->writeAttribute('colId', "$colId");
+
+            $objWriter->startElement($column->getFilterType());
+            if ($column->getJoin() == Column::AUTOFILTER_COLUMN_JOIN_AND) {
+                $objWriter->writeAttribute('and', '1');
+            }
+
+            foreach ($rules as $rule) {
+                self::writeAutoFilterColumnRule($column, $rule, $objWriter);
+            }
+
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write AutoFilter's filterColumn Rule.
+     */
+    private static function writeAutoFilterColumnRule(Column $column, Rule $rule, XMLWriter $objWriter): void
+    {
+        if (
+            ($column->getFilterType() === Column::AUTOFILTER_FILTERTYPE_FILTER) &&
+            ($rule->getOperator() === Rule::AUTOFILTER_COLUMN_RULE_EQUAL) &&
+            ($rule->getValue() === '')
+        ) {
+            //    Filter rule for Blanks
+            $objWriter->writeAttribute('blank', '1');
+        } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER) {
+            //    Dynamic Filter Rule
+            $objWriter->writeAttribute('type', $rule->getGrouping());
+            $val = $column->getAttribute('val');
+            if ($val !== null) {
+                $objWriter->writeAttribute('val', "$val");
+            }
+            $maxVal = $column->getAttribute('maxVal');
+            if ($maxVal !== null) {
+                $objWriter->writeAttribute('maxVal', "$maxVal");
+            }
+        } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_TOPTENFILTER) {
+            //    Top 10 Filter Rule
+            $ruleValue = $rule->getValue();
+            if (!is_array($ruleValue)) {
+                $objWriter->writeAttribute('val', "$ruleValue");
+            }
+            $objWriter->writeAttribute('percent', (($rule->getOperator() === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) ? '1' : '0'));
+            $objWriter->writeAttribute('top', (($rule->getGrouping() === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) ? '1' : '0'));
+        } else {
+            //    Filter, DateGroupItem or CustomFilter
+            $objWriter->startElement($rule->getRuleType());
+
+            if ($rule->getOperator() !== Rule::AUTOFILTER_COLUMN_RULE_EQUAL) {
+                $objWriter->writeAttribute('operator', $rule->getOperator());
+            }
+            if ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DATEGROUP) {
+                // Date Group filters
+                $ruleValue = $rule->getValue();
+                if (is_array($ruleValue)) {
+                    foreach ($ruleValue as $key => $value) {
+                        $objWriter->writeAttribute($key, "$value");
+                    }
+                }
+                $objWriter->writeAttribute('dateTimeGrouping', $rule->getGrouping());
+            } else {
+                $ruleValue = $rule->getValue();
+                if (!is_array($ruleValue)) {
+                    $objWriter->writeAttribute('val', "$ruleValue");
+                }
+            }
+
+            $objWriter->endElement();
+        }
+    }
+}

+ 1842 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php

@@ -0,0 +1,1842 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Chart\Axis;
+use PhpOffice\PhpSpreadsheet\Chart\ChartColor;
+use PhpOffice\PhpSpreadsheet\Chart\DataSeries;
+use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues;
+use PhpOffice\PhpSpreadsheet\Chart\Layout;
+use PhpOffice\PhpSpreadsheet\Chart\Legend;
+use PhpOffice\PhpSpreadsheet\Chart\PlotArea;
+use PhpOffice\PhpSpreadsheet\Chart\Properties;
+use PhpOffice\PhpSpreadsheet\Chart\Title;
+use PhpOffice\PhpSpreadsheet\Chart\TrendLine;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Style\Font;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+
+class Chart extends WriterPart
+{
+    /**
+     * @var int
+     */
+    private $seriesIndex;
+
+    /**
+     * Write charts to XML format.
+     *
+     * @param mixed $calculateCellValues
+     *
+     * @return string XML Output
+     */
+    public function writeChart(\PhpOffice\PhpSpreadsheet\Chart\Chart $chart, $calculateCellValues = true)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+        //    Ensure that data series values are up-to-date before we save
+        if ($calculateCellValues) {
+            $chart->refresh();
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // c:chartSpace
+        $objWriter->startElement('c:chartSpace');
+        $objWriter->writeAttribute('xmlns:c', Namespaces::CHART);
+        $objWriter->writeAttribute('xmlns:a', Namespaces::DRAWINGML);
+        $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
+
+        $objWriter->startElement('c:date1904');
+        $objWriter->writeAttribute('val', '0');
+        $objWriter->endElement();
+        $objWriter->startElement('c:lang');
+        $objWriter->writeAttribute('val', 'en-GB');
+        $objWriter->endElement();
+        $objWriter->startElement('c:roundedCorners');
+        $objWriter->writeAttribute('val', $chart->getRoundedCorners() ? '1' : '0');
+        $objWriter->endElement();
+
+        $this->writeAlternateContent($objWriter);
+
+        $objWriter->startElement('c:chart');
+
+        $this->writeTitle($objWriter, $chart->getTitle());
+
+        $objWriter->startElement('c:autoTitleDeleted');
+        $objWriter->writeAttribute('val', (string) (int) $chart->getAutoTitleDeleted());
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:view3D');
+        $surface2D = false;
+        $plotArea = $chart->getPlotArea();
+        if ($plotArea !== null) {
+            $seriesArray = $plotArea->getPlotGroup();
+            foreach ($seriesArray as $series) {
+                if ($series->getPlotType() === DataSeries::TYPE_SURFACECHART) {
+                    $surface2D = true;
+
+                    break;
+                }
+            }
+        }
+        $this->writeView3D($objWriter, $chart->getRotX(), 'c:rotX', $surface2D, 90);
+        $this->writeView3D($objWriter, $chart->getRotY(), 'c:rotY', $surface2D);
+        $this->writeView3D($objWriter, $chart->getRAngAx(), 'c:rAngAx', $surface2D);
+        $this->writeView3D($objWriter, $chart->getPerspective(), 'c:perspective', $surface2D);
+        $objWriter->endElement(); // view3D
+
+        $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY());
+
+        $this->writeLegend($objWriter, $chart->getLegend());
+
+        $objWriter->startElement('c:plotVisOnly');
+        $objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly());
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:dispBlanksAs');
+        $objWriter->writeAttribute('val', $chart->getDisplayBlanksAs());
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:showDLblsOverMax');
+        $objWriter->writeAttribute('val', '0');
+        $objWriter->endElement();
+
+        $objWriter->endElement(); // c:chart
+
+        $objWriter->startElement('c:spPr');
+        if ($chart->getNoFill()) {
+            $objWriter->startElement('a:noFill');
+            $objWriter->endElement(); // a:noFill
+        }
+        $fillColor = $chart->getFillColor();
+        if ($fillColor->isUsable()) {
+            $this->writeColor($objWriter, $fillColor);
+        }
+        $borderLines = $chart->getBorderLines();
+        $this->writeLineStyles($objWriter, $borderLines);
+        $this->writeEffects($objWriter, $borderLines);
+        $objWriter->endElement(); // c:spPr
+
+        $this->writePrintSettings($objWriter);
+
+        $objWriter->endElement(); // c:chartSpace
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void
+    {
+        if ($value === null && $surface2D) {
+            $value = $default;
+        }
+        if ($value !== null) {
+            $objWriter->startElement($tag);
+            $objWriter->writeAttribute('val', "$value");
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write Chart Title.
+     */
+    private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void
+    {
+        if ($title === null) {
+            return;
+        }
+
+        $objWriter->startElement('c:title');
+        $objWriter->startElement('c:tx');
+        $objWriter->startElement('c:rich');
+
+        $objWriter->startElement('a:bodyPr');
+        $objWriter->endElement();
+
+        $objWriter->startElement('a:lstStyle');
+        $objWriter->endElement();
+
+        $objWriter->startElement('a:p');
+        $objWriter->startElement('a:pPr');
+        $objWriter->startElement('a:defRPr');
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        $caption = $title->getCaption();
+        if ((is_array($caption)) && (count($caption) > 0)) {
+            $caption = $caption[0];
+        }
+        $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a');
+
+        $objWriter->endElement();
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        $this->writeLayout($objWriter, $title->getLayout());
+
+        $objWriter->startElement('c:overlay');
+        $objWriter->writeAttribute('val', ($title->getOverlay()) ? '1' : '0');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Chart Legend.
+     */
+    private function writeLegend(XMLWriter $objWriter, ?Legend $legend = null): void
+    {
+        if ($legend === null) {
+            return;
+        }
+
+        $objWriter->startElement('c:legend');
+
+        $objWriter->startElement('c:legendPos');
+        $objWriter->writeAttribute('val', $legend->getPosition());
+        $objWriter->endElement();
+
+        $this->writeLayout($objWriter, $legend->getLayout());
+
+        $objWriter->startElement('c:overlay');
+        $objWriter->writeAttribute('val', ($legend->getOverlay()) ? '1' : '0');
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:spPr');
+        $fillColor = $legend->getFillColor();
+        if ($fillColor->isUsable()) {
+            $this->writeColor($objWriter, $fillColor);
+        }
+        $borderLines = $legend->getBorderLines();
+        $this->writeLineStyles($objWriter, $borderLines);
+        $this->writeEffects($objWriter, $borderLines);
+        $objWriter->endElement(); // c:spPr
+
+        $legendText = $legend->getLegendText();
+        $objWriter->startElement('c:txPr');
+        $objWriter->startElement('a:bodyPr');
+        $objWriter->endElement();
+
+        $objWriter->startElement('a:lstStyle');
+        $objWriter->endElement();
+
+        $objWriter->startElement('a:p');
+        $objWriter->startElement('a:pPr');
+        $objWriter->writeAttribute('rtl', '0');
+
+        $objWriter->startElement('a:defRPr');
+        if ($legendText !== null) {
+            $this->writeColor($objWriter, $legendText->getFillColorObject());
+            $this->writeEffects($objWriter, $legendText);
+        }
+        $objWriter->endElement(); // a:defRpr
+        $objWriter->endElement(); // a:pPr
+
+        $objWriter->startElement('a:endParaRPr');
+        $objWriter->writeAttribute('lang', 'en-US');
+        $objWriter->endElement(); // a:endParaRPr
+
+        $objWriter->endElement(); // a:p
+        $objWriter->endElement(); // c:txPr
+
+        $objWriter->endElement(); // c:legend
+    }
+
+    /**
+     * Write Chart Plot Area.
+     */
+    private function writePlotArea(XMLWriter $objWriter, ?PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null): void
+    {
+        if ($plotArea === null) {
+            return;
+        }
+
+        $id1 = $id2 = $id3 = '0';
+        $this->seriesIndex = 0;
+        $objWriter->startElement('c:plotArea');
+
+        $layout = $plotArea->getLayout();
+
+        $this->writeLayout($objWriter, $layout);
+
+        $chartTypes = self::getChartType($plotArea);
+        $catIsMultiLevelSeries = $valIsMultiLevelSeries = false;
+        $plotGroupingType = '';
+        $chartType = null;
+        foreach ($chartTypes as $chartType) {
+            $objWriter->startElement('c:' . $chartType);
+
+            $groupCount = $plotArea->getPlotGroupCount();
+            $plotGroup = null;
+            for ($i = 0; $i < $groupCount; ++$i) {
+                $plotGroup = $plotArea->getPlotGroupByIndex($i);
+                $groupType = $plotGroup->getPlotType();
+                if ($groupType == $chartType) {
+                    $plotStyle = $plotGroup->getPlotStyle();
+                    if (!empty($plotStyle) && $groupType === DataSeries::TYPE_RADARCHART) {
+                        $objWriter->startElement('c:radarStyle');
+                        $objWriter->writeAttribute('val', $plotStyle);
+                        $objWriter->endElement();
+                    } elseif (!empty($plotStyle) && $groupType === DataSeries::TYPE_SCATTERCHART) {
+                        $objWriter->startElement('c:scatterStyle');
+                        $objWriter->writeAttribute('val', $plotStyle);
+                        $objWriter->endElement();
+                    } elseif ($groupType === DataSeries::TYPE_SURFACECHART_3D || $groupType === DataSeries::TYPE_SURFACECHART) {
+                        $objWriter->startElement('c:wireframe');
+                        $objWriter->writeAttribute('val', $plotStyle ? '1' : '0');
+                        $objWriter->endElement();
+                    }
+
+                    $this->writePlotGroup($plotGroup, $chartType, $objWriter, $catIsMultiLevelSeries, $valIsMultiLevelSeries, $plotGroupingType);
+                }
+            }
+
+            $this->writeDataLabels($objWriter, $layout);
+
+            if ($chartType === DataSeries::TYPE_LINECHART && $plotGroup) {
+                //    Line only, Line3D can't be smoothed
+                $objWriter->startElement('c:smooth');
+                $objWriter->writeAttribute('val', (string) (int) $plotGroup->getSmoothLine());
+                $objWriter->endElement();
+            } elseif (($chartType === DataSeries::TYPE_BARCHART) || ($chartType === DataSeries::TYPE_BARCHART_3D)) {
+                $objWriter->startElement('c:gapWidth');
+                $objWriter->writeAttribute('val', '150');
+                $objWriter->endElement();
+
+                if ($plotGroupingType == 'percentStacked' || $plotGroupingType == 'stacked') {
+                    $objWriter->startElement('c:overlap');
+                    $objWriter->writeAttribute('val', '100');
+                    $objWriter->endElement();
+                }
+            } elseif ($chartType === DataSeries::TYPE_BUBBLECHART) {
+                $scale = ($plotGroup === null) ? '' : (string) $plotGroup->getPlotStyle();
+                if ($scale !== '') {
+                    $objWriter->startElement('c:bubbleScale');
+                    $objWriter->writeAttribute('val', $scale);
+                    $objWriter->endElement();
+                }
+
+                $objWriter->startElement('c:showNegBubbles');
+                $objWriter->writeAttribute('val', '0');
+                $objWriter->endElement();
+            } elseif ($chartType === DataSeries::TYPE_STOCKCHART) {
+                $objWriter->startElement('c:hiLowLines');
+                $objWriter->endElement();
+
+                $gapWidth = $plotArea->getGapWidth();
+                $upBars = $plotArea->getUseUpBars();
+                $downBars = $plotArea->getUseDownBars();
+                if ($gapWidth !== null || $upBars || $downBars) {
+                    $objWriter->startElement('c:upDownBars');
+                    if ($gapWidth !== null) {
+                        $objWriter->startElement('c:gapWidth');
+                        $objWriter->writeAttribute('val', "$gapWidth");
+                        $objWriter->endElement();
+                    }
+                    if ($upBars) {
+                        $objWriter->startElement('c:upBars');
+                        $objWriter->endElement();
+                    }
+                    if ($downBars) {
+                        $objWriter->startElement('c:downBars');
+                        $objWriter->endElement();
+                    }
+                    $objWriter->endElement(); // c:upDownBars
+                }
+            }
+
+            //    Generate 3 unique numbers to use for axId values
+            $id1 = '110438656';
+            $id2 = '110444544';
+            $id3 = '110365312'; // used in Surface Chart
+
+            if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) {
+                $objWriter->startElement('c:axId');
+                $objWriter->writeAttribute('val', $id1);
+                $objWriter->endElement();
+                $objWriter->startElement('c:axId');
+                $objWriter->writeAttribute('val', $id2);
+                $objWriter->endElement();
+                if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) {
+                    $objWriter->startElement('c:axId');
+                    $objWriter->writeAttribute('val', $id3);
+                    $objWriter->endElement();
+                }
+            } else {
+                $objWriter->startElement('c:firstSliceAng');
+                $objWriter->writeAttribute('val', '0');
+                $objWriter->endElement();
+
+                if ($chartType === DataSeries::TYPE_DONUTCHART) {
+                    $objWriter->startElement('c:holeSize');
+                    $objWriter->writeAttribute('val', '50');
+                    $objWriter->endElement();
+                }
+            }
+
+            $objWriter->endElement();
+        }
+
+        if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) {
+            if ($chartType === DataSeries::TYPE_BUBBLECHART) {
+                $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis ?? new Axis());
+            } else {
+                $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis ?? new Axis());
+            }
+
+            $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis ?? new Axis());
+            if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) {
+                $this->writeSerAxis($objWriter, $id2, $id3);
+            }
+        }
+        $stops = $plotArea->getGradientFillStops();
+        if ($plotArea->getNoFill() || !empty($stops)) {
+            $objWriter->startElement('c:spPr');
+            if ($plotArea->getNoFill()) {
+                $objWriter->startElement('a:noFill');
+                $objWriter->endElement(); // a:noFill
+            }
+            if (!empty($stops)) {
+                $objWriter->startElement('a:gradFill');
+                $objWriter->startElement('a:gsLst');
+                foreach ($stops as $stop) {
+                    $objWriter->startElement('a:gs');
+                    $objWriter->writeAttribute('pos', (string) (Properties::PERCENTAGE_MULTIPLIER * (float) $stop[0]));
+                    $this->writeColor($objWriter, $stop[1], false);
+                    $objWriter->endElement(); // a:gs
+                }
+                $objWriter->endElement(); // a:gsLst
+                $angle = $plotArea->getGradientFillAngle();
+                if ($angle !== null) {
+                    $objWriter->startElement('a:lin');
+                    $objWriter->writeAttribute('ang', Properties::angleToXml($angle));
+                    $objWriter->endElement(); // a:lin
+                }
+                $objWriter->endElement(); // a:gradFill
+            }
+            $objWriter->endElement(); // c:spPr
+        }
+
+        $objWriter->endElement(); // c:plotArea
+    }
+
+    private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void
+    {
+        if ($value !== null) {
+            $objWriter->startElement("c:$name");
+            $objWriter->writeAttribute('val', $value ? '1' : '0');
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write Data Labels.
+     */
+    private function writeDataLabels(XMLWriter $objWriter, ?Layout $chartLayout = null): void
+    {
+        if (!isset($chartLayout)) {
+            return;
+        }
+        $objWriter->startElement('c:dLbls');
+
+        $fillColor = $chartLayout->getLabelFillColor();
+        $borderColor = $chartLayout->getLabelBorderColor();
+        if ($fillColor && $fillColor->isUsable()) {
+            $objWriter->startElement('c:spPr');
+            $this->writeColor($objWriter, $fillColor);
+            if ($borderColor && $borderColor->isUsable()) {
+                $objWriter->startElement('a:ln');
+                $this->writeColor($objWriter, $borderColor);
+                $objWriter->endElement(); // a:ln
+            }
+            $objWriter->endElement(); // c:spPr
+        }
+        $labelFont = $chartLayout->getLabelFont();
+        if ($labelFont !== null) {
+            $objWriter->startElement('c:txPr');
+
+            $objWriter->startElement('a:bodyPr');
+            $objWriter->writeAttribute('wrap', 'square');
+            $objWriter->writeAttribute('lIns', '38100');
+            $objWriter->writeAttribute('tIns', '19050');
+            $objWriter->writeAttribute('rIns', '38100');
+            $objWriter->writeAttribute('bIns', '19050');
+            $objWriter->writeAttribute('anchor', 'ctr');
+            $objWriter->startElement('a:spAutoFit');
+            $objWriter->endElement(); // a:spAutoFit
+            $objWriter->endElement(); // a:bodyPr
+
+            $objWriter->startElement('a:lstStyle');
+            $objWriter->endElement(); // a:lstStyle
+            $this->writeLabelFont($objWriter, $labelFont, $chartLayout->getLabelEffects());
+
+            $objWriter->endElement(); // c:txPr
+        }
+
+        if ($chartLayout->getNumFmtCode() !== '') {
+            $objWriter->startElement('c:numFmt');
+            $objWriter->writeAttribute('formatCode', $chartLayout->getnumFmtCode());
+            $objWriter->writeAttribute('sourceLinked', (string) (int) $chartLayout->getnumFmtLinked());
+            $objWriter->endElement(); // c:numFmt
+        }
+        if ($chartLayout->getDLblPos() !== '') {
+            $objWriter->startElement('c:dLblPos');
+            $objWriter->writeAttribute('val', $chartLayout->getDLblPos());
+            $objWriter->endElement(); // c:dLblPos
+        }
+        $this->writeDataLabelsBool($objWriter, 'showLegendKey', $chartLayout->getShowLegendKey());
+        $this->writeDataLabelsBool($objWriter, 'showVal', $chartLayout->getShowVal());
+        $this->writeDataLabelsBool($objWriter, 'showCatName', $chartLayout->getShowCatName());
+        $this->writeDataLabelsBool($objWriter, 'showSerName', $chartLayout->getShowSerName());
+        $this->writeDataLabelsBool($objWriter, 'showPercent', $chartLayout->getShowPercent());
+        $this->writeDataLabelsBool($objWriter, 'showBubbleSize', $chartLayout->getShowBubbleSize());
+        $this->writeDataLabelsBool($objWriter, 'showLeaderLines', $chartLayout->getShowLeaderLines());
+
+        $objWriter->endElement(); // c:dLbls
+    }
+
+    /**
+     * Write Category Axis.
+     *
+     * @param string $id1
+     * @param string $id2
+     * @param bool $isMultiLevelSeries
+     */
+    private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void
+    {
+        // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc
+        // In that case, xAxis may contain values like the yAxis, or it may be a date axis (LINECHART).
+        $axisType = $yAxis->getAxisType();
+        if ($axisType !== '') {
+            $objWriter->startElement("c:$axisType");
+        } elseif ($yAxis->getAxisIsNumericFormat()) {
+            $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE);
+        } else {
+            $objWriter->startElement('c:' . Axis::AXIS_TYPE_CATEGORY);
+        }
+        $majorGridlines = $yAxis->getMajorGridlines();
+        $minorGridlines = $yAxis->getMinorGridlines();
+
+        if ($id1 !== '0') {
+            $objWriter->startElement('c:axId');
+            $objWriter->writeAttribute('val', $id1);
+            $objWriter->endElement();
+        }
+
+        $objWriter->startElement('c:scaling');
+        if ($yAxis->getAxisOptionsProperty('maximum') !== null) {
+            $objWriter->startElement('c:max');
+            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('maximum'));
+            $objWriter->endElement();
+        }
+        if ($yAxis->getAxisOptionsProperty('minimum') !== null) {
+            $objWriter->startElement('c:min');
+            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minimum'));
+            $objWriter->endElement();
+        }
+        if (!empty($yAxis->getAxisOptionsProperty('orientation'))) {
+            $objWriter->startElement('c:orientation');
+            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation'));
+            $objWriter->endElement();
+        }
+        $objWriter->endElement(); // c:scaling
+
+        $objWriter->startElement('c:delete');
+        $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('hidden') ?? '0');
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:axPos');
+        $objWriter->writeAttribute('val', 'b');
+        $objWriter->endElement();
+
+        if ($majorGridlines !== null) {
+            $objWriter->startElement('c:majorGridlines');
+            $objWriter->startElement('c:spPr');
+            $this->writeLineStyles($objWriter, $majorGridlines);
+            $this->writeEffects($objWriter, $majorGridlines);
+            $objWriter->endElement(); //end spPr
+            $objWriter->endElement(); //end majorGridLines
+        }
+
+        if ($minorGridlines !== null && $minorGridlines->getObjectState()) {
+            $objWriter->startElement('c:minorGridlines');
+            $objWriter->startElement('c:spPr');
+            $this->writeLineStyles($objWriter, $minorGridlines);
+            $this->writeEffects($objWriter, $minorGridlines);
+            $objWriter->endElement(); //end spPr
+            $objWriter->endElement(); //end minorGridLines
+        }
+
+        if ($xAxisLabel !== null) {
+            $objWriter->startElement('c:title');
+            $objWriter->startElement('c:tx');
+            $objWriter->startElement('c:rich');
+
+            $objWriter->startElement('a:bodyPr');
+            $objWriter->endElement();
+
+            $objWriter->startElement('a:lstStyle');
+            $objWriter->endElement();
+
+            $objWriter->startElement('a:p');
+
+            $caption = $xAxisLabel->getCaption();
+            if (is_array($caption)) {
+                $caption = $caption[0];
+            }
+            $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a');
+
+            $objWriter->endElement();
+            $objWriter->endElement();
+            $objWriter->endElement();
+
+            $layout = $xAxisLabel->getLayout();
+            $this->writeLayout($objWriter, $layout);
+
+            $objWriter->startElement('c:overlay');
+            $objWriter->writeAttribute('val', '0');
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+        }
+
+        $objWriter->startElement('c:numFmt');
+        $objWriter->writeAttribute('formatCode', $yAxis->getAxisNumberFormat());
+        $objWriter->writeAttribute('sourceLinked', $yAxis->getAxisNumberSourceLinked());
+        $objWriter->endElement();
+
+        if (!empty($yAxis->getAxisOptionsProperty('major_tick_mark'))) {
+            $objWriter->startElement('c:majorTickMark');
+            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark'));
+            $objWriter->endElement();
+        }
+
+        if (!empty($yAxis->getAxisOptionsProperty('minor_tick_mark'))) {
+            $objWriter->startElement('c:minorTickMark');
+            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark'));
+            $objWriter->endElement();
+        }
+
+        if (!empty($yAxis->getAxisOptionsProperty('axis_labels'))) {
+            $objWriter->startElement('c:tickLblPos');
+            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels'));
+            $objWriter->endElement();
+        }
+
+        $textRotation = $yAxis->getAxisOptionsProperty('textRotation');
+        $axisText = $yAxis->getAxisText();
+
+        if ($axisText !== null || is_numeric($textRotation)) {
+            $objWriter->startElement('c:txPr');
+            $objWriter->startElement('a:bodyPr');
+            if (is_numeric($textRotation)) {
+                $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation));
+            }
+            $objWriter->endElement(); // a:bodyPr
+            $objWriter->startElement('a:lstStyle');
+            $objWriter->endElement(); // a:lstStyle
+            $this->writeLabelFont($objWriter, ($axisText === null) ? null : $axisText->getFont(), $axisText);
+            $objWriter->endElement(); // c:txPr
+        }
+
+        $objWriter->startElement('c:spPr');
+        $this->writeColor($objWriter, $yAxis->getFillColorObject());
+        $this->writeLineStyles($objWriter, $yAxis, $yAxis->getNoFill());
+        $this->writeEffects($objWriter, $yAxis);
+        $objWriter->endElement(); // spPr
+
+        if ($yAxis->getAxisOptionsProperty('major_unit') !== null) {
+            $objWriter->startElement('c:majorUnit');
+            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_unit'));
+            $objWriter->endElement();
+        }
+
+        if ($yAxis->getAxisOptionsProperty('minor_unit') !== null) {
+            $objWriter->startElement('c:minorUnit');
+            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_unit'));
+            $objWriter->endElement();
+        }
+
+        if ($id2 !== '0') {
+            $objWriter->startElement('c:crossAx');
+            $objWriter->writeAttribute('val', $id2);
+            $objWriter->endElement();
+
+            if (!empty($yAxis->getAxisOptionsProperty('horizontal_crosses'))) {
+                $objWriter->startElement('c:crosses');
+                $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses'));
+                $objWriter->endElement();
+            }
+        }
+
+        $objWriter->startElement('c:auto');
+        // LineChart with dateAx wants '0'
+        $objWriter->writeAttribute('val', ($axisType === Axis::AXIS_TYPE_DATE) ? '0' : '1');
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:lblAlgn');
+        $objWriter->writeAttribute('val', 'ctr');
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:lblOffset');
+        $objWriter->writeAttribute('val', '100');
+        $objWriter->endElement();
+
+        if ($axisType === Axis::AXIS_TYPE_DATE) {
+            $property = 'baseTimeUnit';
+            $propertyVal = $yAxis->getAxisOptionsProperty($property);
+            if (!empty($propertyVal)) {
+                $objWriter->startElement("c:$property");
+                $objWriter->writeAttribute('val', $propertyVal);
+                $objWriter->endElement();
+            }
+            $property = 'majorTimeUnit';
+            $propertyVal = $yAxis->getAxisOptionsProperty($property);
+            if (!empty($propertyVal)) {
+                $objWriter->startElement("c:$property");
+                $objWriter->writeAttribute('val', $propertyVal);
+                $objWriter->endElement();
+            }
+            $property = 'minorTimeUnit';
+            $propertyVal = $yAxis->getAxisOptionsProperty($property);
+            if (!empty($propertyVal)) {
+                $objWriter->startElement("c:$property");
+                $objWriter->writeAttribute('val', $propertyVal);
+                $objWriter->endElement();
+            }
+        }
+
+        if ($isMultiLevelSeries) {
+            $objWriter->startElement('c:noMultiLvlLbl');
+            $objWriter->writeAttribute('val', '0');
+            $objWriter->endElement();
+        }
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Value Axis.
+     *
+     * @param null|string $groupType Chart type
+     * @param string $id1
+     * @param string $id2
+     * @param bool $isMultiLevelSeries
+     */
+    private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis): void
+    {
+        $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE);
+        $majorGridlines = $xAxis->getMajorGridlines();
+        $minorGridlines = $xAxis->getMinorGridlines();
+
+        if ($id2 !== '0') {
+            $objWriter->startElement('c:axId');
+            $objWriter->writeAttribute('val', $id2);
+            $objWriter->endElement();
+        }
+
+        $objWriter->startElement('c:scaling');
+
+        if ($xAxis->getAxisOptionsProperty('maximum') !== null) {
+            $objWriter->startElement('c:max');
+            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('maximum'));
+            $objWriter->endElement();
+        }
+
+        if ($xAxis->getAxisOptionsProperty('minimum') !== null) {
+            $objWriter->startElement('c:min');
+            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minimum'));
+            $objWriter->endElement();
+        }
+
+        if (!empty($xAxis->getAxisOptionsProperty('orientation'))) {
+            $objWriter->startElement('c:orientation');
+            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation'));
+            $objWriter->endElement();
+        }
+
+        $objWriter->endElement(); // c:scaling
+
+        $objWriter->startElement('c:delete');
+        $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('hidden') ?? '0');
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:axPos');
+        $objWriter->writeAttribute('val', 'l');
+        $objWriter->endElement();
+
+        if ($majorGridlines !== null) {
+            $objWriter->startElement('c:majorGridlines');
+            $objWriter->startElement('c:spPr');
+            $this->writeLineStyles($objWriter, $majorGridlines);
+            $this->writeEffects($objWriter, $majorGridlines);
+            $objWriter->endElement(); //end spPr
+            $objWriter->endElement(); //end majorGridLines
+        }
+
+        if ($minorGridlines !== null && $minorGridlines->getObjectState()) {
+            $objWriter->startElement('c:minorGridlines');
+            $objWriter->startElement('c:spPr');
+            $this->writeLineStyles($objWriter, $minorGridlines);
+            $this->writeEffects($objWriter, $minorGridlines);
+            $objWriter->endElement(); //end spPr
+            $objWriter->endElement(); //end minorGridLines
+        }
+
+        if ($yAxisLabel !== null) {
+            $objWriter->startElement('c:title');
+            $objWriter->startElement('c:tx');
+            $objWriter->startElement('c:rich');
+
+            $objWriter->startElement('a:bodyPr');
+            $objWriter->endElement();
+
+            $objWriter->startElement('a:lstStyle');
+            $objWriter->endElement();
+
+            $objWriter->startElement('a:p');
+
+            $caption = $yAxisLabel->getCaption();
+            if (is_array($caption)) {
+                $caption = $caption[0];
+            }
+            $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a');
+
+            $objWriter->endElement();
+            $objWriter->endElement();
+            $objWriter->endElement();
+
+            if ($groupType !== DataSeries::TYPE_BUBBLECHART) {
+                $layout = $yAxisLabel->getLayout();
+                $this->writeLayout($objWriter, $layout);
+            }
+
+            $objWriter->startElement('c:overlay');
+            $objWriter->writeAttribute('val', '0');
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+        }
+
+        $objWriter->startElement('c:numFmt');
+        $objWriter->writeAttribute('formatCode', $xAxis->getAxisNumberFormat());
+        $objWriter->writeAttribute('sourceLinked', $xAxis->getAxisNumberSourceLinked());
+        $objWriter->endElement();
+
+        if (!empty($xAxis->getAxisOptionsProperty('major_tick_mark'))) {
+            $objWriter->startElement('c:majorTickMark');
+            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark'));
+            $objWriter->endElement();
+        }
+
+        if (!empty($xAxis->getAxisOptionsProperty('minor_tick_mark'))) {
+            $objWriter->startElement('c:minorTickMark');
+            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark'));
+            $objWriter->endElement();
+        }
+
+        if (!empty($xAxis->getAxisOptionsProperty('axis_labels'))) {
+            $objWriter->startElement('c:tickLblPos');
+            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels'));
+            $objWriter->endElement();
+        }
+
+        $textRotation = $xAxis->getAxisOptionsProperty('textRotation');
+        $axisText = $xAxis->getAxisText();
+
+        if ($axisText !== null || is_numeric($textRotation)) {
+            $objWriter->startElement('c:txPr');
+            $objWriter->startElement('a:bodyPr');
+            if (is_numeric($textRotation)) {
+                $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation));
+            }
+            $objWriter->endElement(); // a:bodyPr
+            $objWriter->startElement('a:lstStyle');
+            $objWriter->endElement(); // a:lstStyle
+
+            $this->writeLabelFont($objWriter, ($axisText === null) ? null : $axisText->getFont(), $axisText);
+
+            $objWriter->endElement(); // c:txPr
+        }
+
+        $objWriter->startElement('c:spPr');
+        $this->writeColor($objWriter, $xAxis->getFillColorObject());
+        $this->writeLineStyles($objWriter, $xAxis, $xAxis->getNoFill());
+        $this->writeEffects($objWriter, $xAxis);
+        $objWriter->endElement(); //end spPr
+
+        if ($id1 !== '0') {
+            $objWriter->startElement('c:crossAx');
+            $objWriter->writeAttribute('val', $id1);
+            $objWriter->endElement();
+
+            if ($xAxis->getAxisOptionsProperty('horizontal_crosses_value') !== null) {
+                $objWriter->startElement('c:crossesAt');
+                $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses_value'));
+                $objWriter->endElement();
+            } else {
+                $crosses = $xAxis->getAxisOptionsProperty('horizontal_crosses');
+                if ($crosses) {
+                    $objWriter->startElement('c:crosses');
+                    $objWriter->writeAttribute('val', $crosses);
+                    $objWriter->endElement();
+                }
+            }
+
+            $crossBetween = $xAxis->getCrossBetween();
+            if ($crossBetween !== '') {
+                $objWriter->startElement('c:crossBetween');
+                $objWriter->writeAttribute('val', $crossBetween);
+                $objWriter->endElement();
+            }
+
+            if ($xAxis->getAxisOptionsProperty('major_unit') !== null) {
+                $objWriter->startElement('c:majorUnit');
+                $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_unit'));
+                $objWriter->endElement();
+            }
+
+            if ($xAxis->getAxisOptionsProperty('minor_unit') !== null) {
+                $objWriter->startElement('c:minorUnit');
+                $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_unit'));
+                $objWriter->endElement();
+            }
+        }
+
+        if ($isMultiLevelSeries) {
+            if ($groupType !== DataSeries::TYPE_BUBBLECHART) {
+                $objWriter->startElement('c:noMultiLvlLbl');
+                $objWriter->writeAttribute('val', '0');
+                $objWriter->endElement();
+            }
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Ser Axis, for Surface chart.
+     */
+    private function writeSerAxis(XMLWriter $objWriter, string $id2, string $id3): void
+    {
+        $objWriter->startElement('c:serAx');
+
+        $objWriter->startElement('c:axId');
+        $objWriter->writeAttribute('val', $id3);
+        $objWriter->endElement(); // axId
+
+        $objWriter->startElement('c:scaling');
+        $objWriter->startElement('c:orientation');
+        $objWriter->writeAttribute('val', 'minMax');
+        $objWriter->endElement(); // orientation
+        $objWriter->endElement(); // scaling
+
+        $objWriter->startElement('c:delete');
+        $objWriter->writeAttribute('val', '0');
+        $objWriter->endElement(); // delete
+
+        $objWriter->startElement('c:axPos');
+        $objWriter->writeAttribute('val', 'b');
+        $objWriter->endElement(); // axPos
+
+        $objWriter->startElement('c:majorTickMark');
+        $objWriter->writeAttribute('val', 'out');
+        $objWriter->endElement(); // majorTickMark
+
+        $objWriter->startElement('c:minorTickMark');
+        $objWriter->writeAttribute('val', 'none');
+        $objWriter->endElement(); // minorTickMark
+
+        $objWriter->startElement('c:tickLblPos');
+        $objWriter->writeAttribute('val', 'nextTo');
+        $objWriter->endElement(); // tickLblPos
+
+        $objWriter->startElement('c:crossAx');
+        $objWriter->writeAttribute('val', $id2);
+        $objWriter->endElement(); // crossAx
+
+        $objWriter->startElement('c:crosses');
+        $objWriter->writeAttribute('val', 'autoZero');
+        $objWriter->endElement(); // crosses
+
+        $objWriter->endElement(); //serAx
+    }
+
+    /**
+     * Get the data series type(s) for a chart plot series.
+     *
+     * @return string[]
+     */
+    private static function getChartType(PlotArea $plotArea): array
+    {
+        $groupCount = $plotArea->getPlotGroupCount();
+
+        if ($groupCount == 1) {
+            $chartType = [$plotArea->getPlotGroupByIndex(0)->getPlotType()];
+        } else {
+            $chartTypes = [];
+            for ($i = 0; $i < $groupCount; ++$i) {
+                $chartTypes[] = $plotArea->getPlotGroupByIndex($i)->getPlotType();
+            }
+            $chartType = array_unique($chartTypes);
+            if (count($chartTypes) == 0) {
+                throw new WriterException('Chart is not yet implemented');
+            }
+        }
+
+        return $chartType;
+    }
+
+    /**
+     * Method writing plot series values.
+     */
+    private function writePlotSeriesValuesElement(XMLWriter $objWriter, int $val, ?ChartColor $fillColor): void
+    {
+        if ($fillColor === null || !$fillColor->isUsable()) {
+            return;
+        }
+        $objWriter->startElement('c:dPt');
+
+        $objWriter->startElement('c:idx');
+        $objWriter->writeAttribute('val', "$val");
+        $objWriter->endElement(); // c:idx
+
+        $objWriter->startElement('c:spPr');
+        $this->writeColor($objWriter, $fillColor);
+        $objWriter->endElement(); // c:spPr
+
+        $objWriter->endElement(); // c:dPt
+    }
+
+    /**
+     * Write Plot Group (series of related plots).
+     *
+     * @param string $groupType Type of plot for dataseries
+     * @param bool $catIsMultiLevelSeries Is category a multi-series category
+     * @param bool $valIsMultiLevelSeries Is value set a multi-series set
+     * @param string $plotGroupingType Type of grouping for multi-series values
+     */
+    private function writePlotGroup(?DataSeries $plotGroup, string $groupType, XMLWriter $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void
+    {
+        if ($plotGroup === null) {
+            return;
+        }
+
+        if (($groupType == DataSeries::TYPE_BARCHART) || ($groupType == DataSeries::TYPE_BARCHART_3D)) {
+            $objWriter->startElement('c:barDir');
+            $objWriter->writeAttribute('val', $plotGroup->getPlotDirection());
+            $objWriter->endElement();
+        }
+
+        $plotGroupingType = $plotGroup->getPlotGrouping();
+        if ($plotGroupingType !== null && $groupType !== DataSeries::TYPE_SURFACECHART && $groupType !== DataSeries::TYPE_SURFACECHART_3D) {
+            $objWriter->startElement('c:grouping');
+            $objWriter->writeAttribute('val', $plotGroupingType);
+            $objWriter->endElement();
+        }
+
+        //    Get these details before the loop, because we can use the count to check for varyColors
+        $plotSeriesOrder = $plotGroup->getPlotOrder();
+        $plotSeriesCount = count($plotSeriesOrder);
+
+        if (($groupType !== DataSeries::TYPE_RADARCHART) && ($groupType !== DataSeries::TYPE_STOCKCHART)) {
+            if ($groupType !== DataSeries::TYPE_LINECHART) {
+                if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART) || ($plotSeriesCount > 1)) {
+                    $objWriter->startElement('c:varyColors');
+                    $objWriter->writeAttribute('val', '1');
+                    $objWriter->endElement();
+                } else {
+                    $objWriter->startElement('c:varyColors');
+                    $objWriter->writeAttribute('val', '0');
+                    $objWriter->endElement();
+                }
+            }
+        }
+
+        $plotSeriesIdx = 0;
+        foreach ($plotSeriesOrder as $plotSeriesIdx => $plotSeriesRef) {
+            $objWriter->startElement('c:ser');
+
+            $objWriter->startElement('c:idx');
+            $adder = array_key_exists(0, $plotSeriesOrder) ? $this->seriesIndex : 0;
+            $objWriter->writeAttribute('val', (string) ($adder + $plotSeriesIdx));
+            $objWriter->endElement();
+
+            $objWriter->startElement('c:order');
+            $objWriter->writeAttribute('val', (string) ($adder + $plotSeriesRef));
+            $objWriter->endElement();
+
+            $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx);
+            $labelFill = null;
+            if ($plotLabel && $groupType === DataSeries::TYPE_LINECHART) {
+                $labelFill = $plotLabel->getFillColorObject();
+                $labelFill = ($labelFill instanceof ChartColor) ? $labelFill : null;
+            }
+
+            //    Values
+            $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesIdx);
+
+            if ($plotSeriesValues !== false && in_array($groupType, self::CUSTOM_COLOR_TYPES, true)) {
+                $fillColorValues = $plotSeriesValues->getFillColorObject();
+                if ($fillColorValues !== null && is_array($fillColorValues)) {
+                    foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) {
+                        $this->writePlotSeriesValuesElement($objWriter, $dataKey, $fillColorValues[$dataKey] ?? null);
+                    }
+                }
+            }
+            if ($plotSeriesValues !== false && $plotSeriesValues->getLabelLayout()) {
+                $this->writeDataLabels($objWriter, $plotSeriesValues->getLabelLayout());
+            }
+
+            //    Labels
+            $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx);
+            if ($plotSeriesLabel && ($plotSeriesLabel->getPointCount() > 0)) {
+                $objWriter->startElement('c:tx');
+                $objWriter->startElement('c:strRef');
+                $this->writePlotSeriesLabel($plotSeriesLabel, $objWriter);
+                $objWriter->endElement();
+                $objWriter->endElement();
+            }
+
+            //    Formatting for the points
+            if (
+                $plotSeriesValues !== false
+            ) {
+                $objWriter->startElement('c:spPr');
+                if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) {
+                    $fillColor = $plotLabel->getFillColorObject();
+                    if ($fillColor !== null && !is_array($fillColor) && $fillColor->isUsable()) {
+                        $this->writeColor($objWriter, $fillColor);
+                    }
+                }
+                $fillObject = $labelFill ?? $plotSeriesValues->getFillColorObject();
+                $callLineStyles = true;
+                if ($fillObject instanceof ChartColor && $fillObject->isUsable()) {
+                    if ($groupType === DataSeries::TYPE_LINECHART) {
+                        $objWriter->startElement('a:ln');
+                        $callLineStyles = false;
+                    }
+                    $this->writeColor($objWriter, $fillObject);
+                    if (!$callLineStyles) {
+                        $objWriter->endElement(); // a:ln
+                    }
+                }
+                $nofill = $groupType === DataSeries::TYPE_STOCKCHART || (($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) && !$plotSeriesValues->getScatterLines());
+                if ($callLineStyles) {
+                    $this->writeLineStyles($objWriter, $plotSeriesValues, $nofill);
+                    $this->writeEffects($objWriter, $plotSeriesValues);
+                }
+                $objWriter->endElement(); // c:spPr
+            }
+
+            if ($plotSeriesValues) {
+                $plotSeriesMarker = $plotSeriesValues->getPointMarker();
+                $markerFillColor = $plotSeriesValues->getMarkerFillColor();
+                $fillUsed = $markerFillColor->IsUsable();
+                $markerBorderColor = $plotSeriesValues->getMarkerBorderColor();
+                $borderUsed = $markerBorderColor->isUsable();
+                if ($plotSeriesMarker || $fillUsed || $borderUsed) {
+                    $objWriter->startElement('c:marker');
+                    $objWriter->startElement('c:symbol');
+                    if ($plotSeriesMarker) {
+                        $objWriter->writeAttribute('val', $plotSeriesMarker);
+                    }
+                    $objWriter->endElement();
+
+                    if ($plotSeriesMarker !== 'none') {
+                        $objWriter->startElement('c:size');
+                        $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointSize());
+                        $objWriter->endElement(); // c:size
+                        $objWriter->startElement('c:spPr');
+                        $this->writeColor($objWriter, $markerFillColor);
+                        if ($borderUsed) {
+                            $objWriter->startElement('a:ln');
+                            $this->writeColor($objWriter, $markerBorderColor);
+                            $objWriter->endElement(); // a:ln
+                        }
+                        $objWriter->endElement(); // spPr
+                    }
+
+                    $objWriter->endElement();
+                }
+            }
+
+            if (($groupType === DataSeries::TYPE_BARCHART) || ($groupType === DataSeries::TYPE_BARCHART_3D) || ($groupType === DataSeries::TYPE_BUBBLECHART)) {
+                $objWriter->startElement('c:invertIfNegative');
+                $objWriter->writeAttribute('val', '0');
+                $objWriter->endElement();
+            }
+            // Trendlines
+            if ($plotSeriesValues !== false) {
+                foreach ($plotSeriesValues->getTrendLines() as $trendLine) {
+                    $trendLineType = $trendLine->getTrendLineType();
+                    $order = $trendLine->getOrder();
+                    $period = $trendLine->getPeriod();
+                    $dispRSqr = $trendLine->getDispRSqr();
+                    $dispEq = $trendLine->getDispEq();
+                    $forward = $trendLine->getForward();
+                    $backward = $trendLine->getBackward();
+                    $intercept = $trendLine->getIntercept();
+                    $name = $trendLine->getName();
+                    $trendLineColor = $trendLine->getLineColor(); // ChartColor
+
+                    $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell'
+                    if ($name !== '') {
+                        $objWriter->startElement('c:name');
+                        $objWriter->writeRawData($name);
+                        $objWriter->endElement(); // c:name
+                    }
+                    $objWriter->startElement('c:spPr');
+
+                    if (!$trendLineColor->isUsable()) {
+                        // use dataSeriesValues line color as a backup if $trendLineColor is null
+                        $dsvLineColor = $plotSeriesValues->getLineColor();
+                        if ($dsvLineColor->isUsable()) {
+                            $trendLine
+                                ->getLineColor()
+                                ->setColorProperties($dsvLineColor->getValue(), $dsvLineColor->getAlpha(), $dsvLineColor->getType());
+                        }
+                    } // otherwise, hope Excel does the right thing
+
+                    $this->writeLineStyles($objWriter, $trendLine, false); // suppress noFill
+
+                    $objWriter->endElement(); // spPr
+
+                    $objWriter->startElement('c:trendlineType'); // N.B lowercase 'ell'
+                    $objWriter->writeAttribute('val', $trendLineType);
+                    $objWriter->endElement(); // trendlineType
+                    if ($backward !== 0.0) {
+                        $objWriter->startElement('c:backward');
+                        $objWriter->writeAttribute('val', "$backward");
+                        $objWriter->endElement(); // c:backward
+                    }
+                    if ($forward !== 0.0) {
+                        $objWriter->startElement('c:forward');
+                        $objWriter->writeAttribute('val', "$forward");
+                        $objWriter->endElement(); // c:forward
+                    }
+                    if ($intercept !== 0.0) {
+                        $objWriter->startElement('c:intercept');
+                        $objWriter->writeAttribute('val', "$intercept");
+                        $objWriter->endElement(); // c:intercept
+                    }
+                    if ($trendLineType == TrendLine::TRENDLINE_POLYNOMIAL) {
+                        $objWriter->startElement('c:order');
+                        $objWriter->writeAttribute('val', $order);
+                        $objWriter->endElement(); // order
+                    }
+                    if ($trendLineType == TrendLine::TRENDLINE_MOVING_AVG) {
+                        $objWriter->startElement('c:period');
+                        $objWriter->writeAttribute('val', $period);
+                        $objWriter->endElement(); // period
+                    }
+                    $objWriter->startElement('c:dispRSqr');
+                    $objWriter->writeAttribute('val', $dispRSqr ? '1' : '0');
+                    $objWriter->endElement();
+                    $objWriter->startElement('c:dispEq');
+                    $objWriter->writeAttribute('val', $dispEq ? '1' : '0');
+                    $objWriter->endElement();
+                    if ($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) {
+                        $objWriter->startElement('c:trendlineLbl');
+                        $objWriter->startElement('c:numFmt');
+                        $objWriter->writeAttribute('formatCode', 'General');
+                        $objWriter->writeAttribute('sourceLinked', '0');
+                        $objWriter->endElement();  // numFmt
+                        $objWriter->endElement();  // trendlineLbl
+                    }
+
+                    $objWriter->endElement(); // trendline
+                }
+            }
+
+            //    Category Labels
+            $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesIdx);
+            if ($plotSeriesCategory && ($plotSeriesCategory->getPointCount() > 0)) {
+                $catIsMultiLevelSeries = $catIsMultiLevelSeries || $plotSeriesCategory->isMultiLevelSeries();
+
+                if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) {
+                    $plotStyle = $plotGroup->getPlotStyle();
+                    if (is_numeric($plotStyle)) {
+                        $objWriter->startElement('c:explosion');
+                        $objWriter->writeAttribute('val', $plotStyle);
+                        $objWriter->endElement();
+                    }
+                }
+
+                if (($groupType === DataSeries::TYPE_BUBBLECHART) || ($groupType === DataSeries::TYPE_SCATTERCHART)) {
+                    $objWriter->startElement('c:xVal');
+                } else {
+                    $objWriter->startElement('c:cat');
+                }
+
+                // xVals (Categories) are not always 'str'
+                // Test X-axis Label's Datatype to decide 'str' vs 'num'
+                $CategoryDatatype = $plotSeriesCategory->getDataType();
+                if ($CategoryDatatype == DataSeriesValues::DATASERIES_TYPE_NUMBER) {
+                    $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'num');
+                } else {
+                    $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str');
+                }
+                $objWriter->endElement();
+            }
+
+            //    Values
+            if ($plotSeriesValues) {
+                $valIsMultiLevelSeries = $valIsMultiLevelSeries || $plotSeriesValues->isMultiLevelSeries();
+
+                if (($groupType === DataSeries::TYPE_BUBBLECHART) || ($groupType === DataSeries::TYPE_SCATTERCHART)) {
+                    $objWriter->startElement('c:yVal');
+                } else {
+                    $objWriter->startElement('c:val');
+                }
+
+                $this->writePlotSeriesValues($plotSeriesValues, $objWriter, $groupType, 'num');
+                $objWriter->endElement();
+                if ($groupType === DataSeries::TYPE_SCATTERCHART && $plotGroup->getPlotStyle() === 'smoothMarker') {
+                    $objWriter->startElement('c:smooth');
+                    $objWriter->writeAttribute('val', $plotSeriesValues->getSmoothLine() ? '1' : '0');
+                    $objWriter->endElement();
+                }
+            }
+
+            if ($groupType === DataSeries::TYPE_BUBBLECHART) {
+                if (!empty($plotGroup->getPlotBubbleSizes()[$plotSeriesIdx])) {
+                    $objWriter->startElement('c:bubbleSize');
+                    $this->writePlotSeriesValues(
+                        $plotGroup->getPlotBubbleSizes()[$plotSeriesIdx],
+                        $objWriter,
+                        $groupType,
+                        'num'
+                    );
+                    $objWriter->endElement();
+                    if ($plotSeriesValues !== false) {
+                        $objWriter->startElement('c:bubble3D');
+                        $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0');
+                        $objWriter->endElement();
+                    }
+                } elseif ($plotSeriesValues !== false) {
+                    $this->writeBubbles($plotSeriesValues, $objWriter);
+                }
+            }
+
+            $objWriter->endElement();
+        }
+
+        $this->seriesIndex += $plotSeriesIdx + 1;
+    }
+
+    /**
+     * Write Plot Series Label.
+     */
+    private function writePlotSeriesLabel(?DataSeriesValues $plotSeriesLabel, XMLWriter $objWriter): void
+    {
+        if ($plotSeriesLabel === null) {
+            return;
+        }
+
+        $objWriter->startElement('c:f');
+        $objWriter->writeRawData($plotSeriesLabel->getDataSource());
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:strCache');
+        $objWriter->startElement('c:ptCount');
+        $objWriter->writeAttribute('val', (string) $plotSeriesLabel->getPointCount());
+        $objWriter->endElement();
+
+        foreach ($plotSeriesLabel->getDataValues() as $plotLabelKey => $plotLabelValue) {
+            $objWriter->startElement('c:pt');
+            $objWriter->writeAttribute('idx', $plotLabelKey);
+
+            $objWriter->startElement('c:v');
+            $objWriter->writeRawData($plotLabelValue);
+            $objWriter->endElement();
+            $objWriter->endElement();
+        }
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Plot Series Values.
+     *
+     * @param string $groupType Type of plot for dataseries
+     * @param string $dataType Datatype of series values
+     */
+    private function writePlotSeriesValues(?DataSeriesValues $plotSeriesValues, XMLWriter $objWriter, $groupType, $dataType = 'str'): void
+    {
+        if ($plotSeriesValues === null) {
+            return;
+        }
+
+        if ($plotSeriesValues->isMultiLevelSeries()) {
+            $levelCount = $plotSeriesValues->multiLevelCount();
+
+            $objWriter->startElement('c:multiLvlStrRef');
+
+            $objWriter->startElement('c:f');
+            $objWriter->writeRawData($plotSeriesValues->getDataSource());
+            $objWriter->endElement();
+
+            $objWriter->startElement('c:multiLvlStrCache');
+
+            $objWriter->startElement('c:ptCount');
+            $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount());
+            $objWriter->endElement();
+
+            for ($level = 0; $level < $levelCount; ++$level) {
+                $objWriter->startElement('c:lvl');
+
+                foreach ($plotSeriesValues->getDataValues() as $plotSeriesKey => $plotSeriesValue) {
+                    if (isset($plotSeriesValue[$level])) {
+                        $objWriter->startElement('c:pt');
+                        $objWriter->writeAttribute('idx', $plotSeriesKey);
+
+                        $objWriter->startElement('c:v');
+                        $objWriter->writeRawData($plotSeriesValue[$level]);
+                        $objWriter->endElement();
+                        $objWriter->endElement();
+                    }
+                }
+
+                $objWriter->endElement();
+            }
+
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+        } else {
+            $objWriter->startElement('c:' . $dataType . 'Ref');
+
+            $objWriter->startElement('c:f');
+            $objWriter->writeRawData($plotSeriesValues->getDataSource());
+            $objWriter->endElement();
+
+            $count = $plotSeriesValues->getPointCount();
+            $source = $plotSeriesValues->getDataSource();
+            $values = $plotSeriesValues->getDataValues();
+            if ($count > 1 || ($count === 1 && array_key_exists(0, $values) && "=$source" !== (string) $values[0])) {
+                $objWriter->startElement('c:' . $dataType . 'Cache');
+
+                if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) {
+                    if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) {
+                        $objWriter->startElement('c:formatCode');
+                        $objWriter->writeRawData($plotSeriesValues->getFormatCode());
+                        $objWriter->endElement();
+                    }
+                }
+
+                $objWriter->startElement('c:ptCount');
+                $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount());
+                $objWriter->endElement();
+
+                $dataValues = $plotSeriesValues->getDataValues();
+                if (!empty($dataValues)) {
+                    foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) {
+                        $objWriter->startElement('c:pt');
+                        $objWriter->writeAttribute('idx', $plotSeriesKey);
+
+                        $objWriter->startElement('c:v');
+                        $objWriter->writeRawData($plotSeriesValue);
+                        $objWriter->endElement();
+                        $objWriter->endElement();
+                    }
+                }
+
+                $objWriter->endElement(); // *Cache
+            }
+
+            $objWriter->endElement(); // *Ref
+        }
+    }
+
+    private const CUSTOM_COLOR_TYPES = [
+        DataSeries::TYPE_BARCHART,
+        DataSeries::TYPE_BARCHART_3D,
+        DataSeries::TYPE_PIECHART,
+        DataSeries::TYPE_PIECHART_3D,
+        DataSeries::TYPE_DONUTCHART,
+    ];
+
+    /**
+     * Write Bubble Chart Details.
+     */
+    private function writeBubbles(?DataSeriesValues $plotSeriesValues, XMLWriter $objWriter): void
+    {
+        if ($plotSeriesValues === null) {
+            return;
+        }
+
+        $objWriter->startElement('c:bubbleSize');
+        $objWriter->startElement('c:numLit');
+
+        $objWriter->startElement('c:formatCode');
+        $objWriter->writeRawData('General');
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:ptCount');
+        $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount());
+        $objWriter->endElement();
+
+        $dataValues = $plotSeriesValues->getDataValues();
+        if (!empty($dataValues)) {
+            foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) {
+                $objWriter->startElement('c:pt');
+                $objWriter->writeAttribute('idx', $plotSeriesKey);
+                $objWriter->startElement('c:v');
+                $objWriter->writeRawData('1');
+                $objWriter->endElement();
+                $objWriter->endElement();
+            }
+        }
+
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:bubble3D');
+        $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0');
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Layout.
+     */
+    private function writeLayout(XMLWriter $objWriter, ?Layout $layout = null): void
+    {
+        $objWriter->startElement('c:layout');
+
+        if ($layout !== null) {
+            $objWriter->startElement('c:manualLayout');
+
+            $layoutTarget = $layout->getLayoutTarget();
+            if ($layoutTarget !== null) {
+                $objWriter->startElement('c:layoutTarget');
+                $objWriter->writeAttribute('val', $layoutTarget);
+                $objWriter->endElement();
+            }
+
+            $xMode = $layout->getXMode();
+            if ($xMode !== null) {
+                $objWriter->startElement('c:xMode');
+                $objWriter->writeAttribute('val', $xMode);
+                $objWriter->endElement();
+            }
+
+            $yMode = $layout->getYMode();
+            if ($yMode !== null) {
+                $objWriter->startElement('c:yMode');
+                $objWriter->writeAttribute('val', $yMode);
+                $objWriter->endElement();
+            }
+
+            $x = $layout->getXPosition();
+            if ($x !== null) {
+                $objWriter->startElement('c:x');
+                $objWriter->writeAttribute('val', "$x");
+                $objWriter->endElement();
+            }
+
+            $y = $layout->getYPosition();
+            if ($y !== null) {
+                $objWriter->startElement('c:y');
+                $objWriter->writeAttribute('val', "$y");
+                $objWriter->endElement();
+            }
+
+            $w = $layout->getWidth();
+            if ($w !== null) {
+                $objWriter->startElement('c:w');
+                $objWriter->writeAttribute('val', "$w");
+                $objWriter->endElement();
+            }
+
+            $h = $layout->getHeight();
+            if ($h !== null) {
+                $objWriter->startElement('c:h');
+                $objWriter->writeAttribute('val', "$h");
+                $objWriter->endElement();
+            }
+
+            $objWriter->endElement();
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Alternate Content block.
+     */
+    private function writeAlternateContent(XMLWriter $objWriter): void
+    {
+        $objWriter->startElement('mc:AlternateContent');
+        $objWriter->writeAttribute('xmlns:mc', Namespaces::COMPATIBILITY);
+
+        $objWriter->startElement('mc:Choice');
+        $objWriter->writeAttribute('Requires', 'c14');
+        $objWriter->writeAttribute('xmlns:c14', Namespaces::CHART_ALTERNATE);
+
+        $objWriter->startElement('c14:style');
+        $objWriter->writeAttribute('val', '102');
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        $objWriter->startElement('mc:Fallback');
+        $objWriter->startElement('c:style');
+        $objWriter->writeAttribute('val', '2');
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Printer Settings.
+     */
+    private function writePrintSettings(XMLWriter $objWriter): void
+    {
+        $objWriter->startElement('c:printSettings');
+
+        $objWriter->startElement('c:headerFooter');
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:pageMargins');
+        $objWriter->writeAttribute('footer', '0.3');
+        $objWriter->writeAttribute('header', '0.3');
+        $objWriter->writeAttribute('r', '0.7');
+        $objWriter->writeAttribute('l', '0.7');
+        $objWriter->writeAttribute('t', '0.75');
+        $objWriter->writeAttribute('b', '0.75');
+        $objWriter->endElement();
+
+        $objWriter->startElement('c:pageSetup');
+        $objWriter->writeAttribute('orientation', 'portrait');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    private function writeEffects(XMLWriter $objWriter, Properties $yAxis): void
+    {
+        if (
+            !empty($yAxis->getSoftEdgesSize())
+            || !empty($yAxis->getShadowProperty('effect'))
+            || !empty($yAxis->getGlowProperty('size'))
+        ) {
+            $objWriter->startElement('a:effectLst');
+            $this->writeGlow($objWriter, $yAxis);
+            $this->writeShadow($objWriter, $yAxis);
+            $this->writeSoftEdge($objWriter, $yAxis);
+            $objWriter->endElement(); // effectLst
+        }
+    }
+
+    private function writeShadow(XMLWriter $objWriter, Properties $xAxis): void
+    {
+        if (empty($xAxis->getShadowProperty('effect'))) {
+            return;
+        }
+        /** @var string */
+        $effect = $xAxis->getShadowProperty('effect');
+        $objWriter->startElement("a:$effect");
+
+        if (is_numeric($xAxis->getShadowProperty('blur'))) {
+            $objWriter->writeAttribute('blurRad', Properties::pointsToXml((float) $xAxis->getShadowProperty('blur')));
+        }
+        if (is_numeric($xAxis->getShadowProperty('distance'))) {
+            $objWriter->writeAttribute('dist', Properties::pointsToXml((float) $xAxis->getShadowProperty('distance')));
+        }
+        if (is_numeric($xAxis->getShadowProperty('direction'))) {
+            $objWriter->writeAttribute('dir', Properties::angleToXml((float) $xAxis->getShadowProperty('direction')));
+        }
+        $algn = $xAxis->getShadowProperty('algn');
+        if (is_string($algn) && $algn !== '') {
+            $objWriter->writeAttribute('algn', $algn);
+        }
+        foreach (['sx', 'sy'] as $sizeType) {
+            $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]);
+            if (is_numeric($sizeValue)) {
+                $objWriter->writeAttribute($sizeType, Properties::tenthOfPercentToXml((float) $sizeValue));
+            }
+        }
+        foreach (['kx', 'ky'] as $sizeType) {
+            $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]);
+            if (is_numeric($sizeValue)) {
+                $objWriter->writeAttribute($sizeType, Properties::angleToXml((float) $sizeValue));
+            }
+        }
+        $rotWithShape = $xAxis->getShadowProperty('rotWithShape');
+        if (is_numeric($rotWithShape)) {
+            $objWriter->writeAttribute('rotWithShape', (string) (int) $rotWithShape);
+        }
+
+        $this->writeColor($objWriter, $xAxis->getShadowColorObject(), false);
+
+        $objWriter->endElement();
+    }
+
+    private function writeGlow(XMLWriter $objWriter, Properties $yAxis): void
+    {
+        $size = $yAxis->getGlowProperty('size');
+        if (empty($size)) {
+            return;
+        }
+        $objWriter->startElement('a:glow');
+        $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $size));
+        $this->writeColor($objWriter, $yAxis->getGlowColorObject(), false);
+        $objWriter->endElement(); // glow
+    }
+
+    private function writeSoftEdge(XMLWriter $objWriter, Properties $yAxis): void
+    {
+        $softEdgeSize = $yAxis->getSoftEdgesSize();
+        if (empty($softEdgeSize)) {
+            return;
+        }
+        $objWriter->startElement('a:softEdge');
+        $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $softEdgeSize));
+        $objWriter->endElement(); //end softEdge
+    }
+
+    private function writeLineStyles(XMLWriter $objWriter, Properties $gridlines, bool $noFill = false): void
+    {
+        $objWriter->startElement('a:ln');
+        $widthTemp = $gridlines->getLineStyleProperty('width');
+        if (is_numeric($widthTemp)) {
+            $objWriter->writeAttribute('w', Properties::pointsToXml((float) $widthTemp));
+        }
+        $this->writeNotEmpty($objWriter, 'cap', $gridlines->getLineStyleProperty('cap'));
+        $this->writeNotEmpty($objWriter, 'cmpd', $gridlines->getLineStyleProperty('compound'));
+        if ($noFill) {
+            $objWriter->startElement('a:noFill');
+            $objWriter->endElement();
+        } else {
+            $this->writeColor($objWriter, $gridlines->getLineColor());
+        }
+
+        $dash = $gridlines->getLineStyleProperty('dash');
+        if (!empty($dash)) {
+            $objWriter->startElement('a:prstDash');
+            $this->writeNotEmpty($objWriter, 'val', $dash);
+            $objWriter->endElement();
+        }
+
+        if ($gridlines->getLineStyleProperty('join') === 'miter') {
+            $objWriter->startElement('a:miter');
+            $objWriter->writeAttribute('lim', '800000');
+            $objWriter->endElement();
+        } elseif ($gridlines->getLineStyleProperty('join') === 'bevel') {
+            $objWriter->startElement('a:bevel');
+            $objWriter->endElement();
+        }
+
+        if ($gridlines->getLineStyleProperty(['arrow', 'head', 'type'])) {
+            $objWriter->startElement('a:headEnd');
+            $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'head', 'type']));
+            $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('head'));
+            $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('head'));
+            $objWriter->endElement();
+        }
+
+        if ($gridlines->getLineStyleProperty(['arrow', 'end', 'type'])) {
+            $objWriter->startElement('a:tailEnd');
+            $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'end', 'type']));
+            $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('end'));
+            $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('end'));
+            $objWriter->endElement();
+        }
+        $objWriter->endElement(); //end ln
+    }
+
+    private function writeNotEmpty(XMLWriter $objWriter, string $name, ?string $value): void
+    {
+        if ($value !== null && $value !== '') {
+            $objWriter->writeAttribute($name, $value);
+        }
+    }
+
+    private function writeColor(XMLWriter $objWriter, ChartColor $chartColor, bool $solidFill = true): void
+    {
+        $type = $chartColor->getType();
+        $value = $chartColor->getValue();
+        if (!empty($type) && !empty($value)) {
+            if ($solidFill) {
+                $objWriter->startElement('a:solidFill');
+            }
+            $objWriter->startElement("a:$type");
+            $objWriter->writeAttribute('val', $value);
+            $alpha = $chartColor->getAlpha();
+            if (is_numeric($alpha)) {
+                $objWriter->startElement('a:alpha');
+                $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha));
+                $objWriter->endElement(); // a:alpha
+            }
+            $brightness = $chartColor->getBrightness();
+            if (is_numeric($brightness)) {
+                $brightness = (int) $brightness;
+                $lumOff = 100 - $brightness;
+                $objWriter->startElement('a:lumMod');
+                $objWriter->writeAttribute('val', ChartColor::alphaToXml($brightness));
+                $objWriter->endElement(); // a:lumMod
+                $objWriter->startElement('a:lumOff');
+                $objWriter->writeAttribute('val', ChartColor::alphaToXml($lumOff));
+                $objWriter->endElement(); // a:lumOff
+            }
+            $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr
+            if ($solidFill) {
+                $objWriter->endElement(); //a:solidFill
+            }
+        }
+    }
+
+    private function writeLabelFont(XMLWriter $objWriter, ?Font $labelFont, ?Properties $axisText): void
+    {
+        $objWriter->startElement('a:p');
+        $objWriter->startElement('a:pPr');
+        $objWriter->startElement('a:defRPr');
+        if ($labelFont !== null) {
+            $fontSize = $labelFont->getSize();
+            if (is_numeric($fontSize)) {
+                $fontSize *= (($fontSize < 100) ? 100 : 1);
+                $objWriter->writeAttribute('sz', (string) $fontSize);
+            }
+            if ($labelFont->getBold() === true) {
+                $objWriter->writeAttribute('b', '1');
+            }
+            if ($labelFont->getItalic() === true) {
+                $objWriter->writeAttribute('i', '1');
+            }
+            $fontColor = $labelFont->getChartColor();
+            if ($fontColor !== null) {
+                $this->writeColor($objWriter, $fontColor);
+            }
+        }
+        if ($axisText !== null) {
+            $this->writeEffects($objWriter, $axisText);
+        }
+        if ($labelFont !== null) {
+            if (!empty($labelFont->getLatin())) {
+                $objWriter->startElement('a:latin');
+                $objWriter->writeAttribute('typeface', $labelFont->getLatin());
+                $objWriter->endElement();
+            }
+            if (!empty($labelFont->getEastAsian())) {
+                $objWriter->startElement('a:eastAsian');
+                $objWriter->writeAttribute('typeface', $labelFont->getEastAsian());
+                $objWriter->endElement();
+            }
+            if (!empty($labelFont->getComplexScript())) {
+                $objWriter->startElement('a:complexScript');
+                $objWriter->writeAttribute('typeface', $labelFont->getComplexScript());
+                $objWriter->endElement();
+            }
+        }
+        $objWriter->endElement(); // a:defRPr
+        $objWriter->endElement(); // a:pPr
+        $objWriter->endElement(); // a:p
+    }
+}

+ 236 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php

@@ -0,0 +1,236 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Comment;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+
+class Comments extends WriterPart
+{
+    /**
+     * Write comments to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeComments(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Comments cache
+        $comments = $worksheet->getComments();
+
+        // Authors cache
+        $authors = [];
+        $authorId = 0;
+        foreach ($comments as $comment) {
+            if (!isset($authors[$comment->getAuthor()])) {
+                $authors[$comment->getAuthor()] = $authorId++;
+            }
+        }
+
+        // comments
+        $objWriter->startElement('comments');
+        $objWriter->writeAttribute('xmlns', Namespaces::MAIN);
+
+        // Loop through authors
+        $objWriter->startElement('authors');
+        foreach ($authors as $author => $index) {
+            $objWriter->writeElement('author', $author);
+        }
+        $objWriter->endElement();
+
+        // Loop through comments
+        $objWriter->startElement('commentList');
+        foreach ($comments as $key => $value) {
+            $this->writeComment($objWriter, $key, $value, $authors);
+        }
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write comment to XML format.
+     *
+     * @param string $cellReference Cell reference
+     * @param Comment $comment Comment
+     * @param array $authors Array of authors
+     */
+    private function writeComment(XMLWriter $objWriter, $cellReference, Comment $comment, array $authors): void
+    {
+        // comment
+        $objWriter->startElement('comment');
+        $objWriter->writeAttribute('ref', $cellReference);
+        $objWriter->writeAttribute('authorId', $authors[$comment->getAuthor()]);
+
+        // text
+        $objWriter->startElement('text');
+        $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $comment->getText());
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write VML comments to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeVMLComments(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Comments cache
+        $comments = $worksheet->getComments();
+
+        // xml
+        $objWriter->startElement('xml');
+        $objWriter->writeAttribute('xmlns:v', Namespaces::URN_VML);
+        $objWriter->writeAttribute('xmlns:o', Namespaces::URN_MSOFFICE);
+        $objWriter->writeAttribute('xmlns:x', Namespaces::URN_EXCEL);
+
+        // o:shapelayout
+        $objWriter->startElement('o:shapelayout');
+        $objWriter->writeAttribute('v:ext', 'edit');
+
+        // o:idmap
+        $objWriter->startElement('o:idmap');
+        $objWriter->writeAttribute('v:ext', 'edit');
+        $objWriter->writeAttribute('data', '1');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // v:shapetype
+        $objWriter->startElement('v:shapetype');
+        $objWriter->writeAttribute('id', '_x0000_t202');
+        $objWriter->writeAttribute('coordsize', '21600,21600');
+        $objWriter->writeAttribute('o:spt', '202');
+        $objWriter->writeAttribute('path', 'm,l,21600r21600,l21600,xe');
+
+        // v:stroke
+        $objWriter->startElement('v:stroke');
+        $objWriter->writeAttribute('joinstyle', 'miter');
+        $objWriter->endElement();
+
+        // v:path
+        $objWriter->startElement('v:path');
+        $objWriter->writeAttribute('gradientshapeok', 't');
+        $objWriter->writeAttribute('o:connecttype', 'rect');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // Loop through comments
+        foreach ($comments as $key => $value) {
+            $this->writeVMLComment($objWriter, $key, $value);
+        }
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write VML comment to XML format.
+     *
+     * @param string $cellReference Cell reference, eg: 'A1'
+     * @param Comment $comment Comment
+     */
+    private function writeVMLComment(XMLWriter $objWriter, $cellReference, Comment $comment): void
+    {
+        // Metadata
+        [$column, $row] = Coordinate::indexesFromString($cellReference);
+        $id = 1024 + $column + $row;
+        $id = substr("$id", 0, 4);
+
+        // v:shape
+        $objWriter->startElement('v:shape');
+        $objWriter->writeAttribute('id', '_x0000_s' . $id);
+        $objWriter->writeAttribute('type', '#_x0000_t202');
+        $objWriter->writeAttribute('style', 'position:absolute;margin-left:' . $comment->getMarginLeft() . ';margin-top:' . $comment->getMarginTop() . ';width:' . $comment->getWidth() . ';height:' . $comment->getHeight() . ';z-index:1;visibility:' . ($comment->getVisible() ? 'visible' : 'hidden'));
+        $objWriter->writeAttribute('fillcolor', '#' . $comment->getFillColor()->getRGB());
+        $objWriter->writeAttribute('o:insetmode', 'auto');
+
+        // v:fill
+        $objWriter->startElement('v:fill');
+        $objWriter->writeAttribute('color2', '#' . $comment->getFillColor()->getRGB());
+        if ($comment->hasBackgroundImage()) {
+            $bgImage = $comment->getBackgroundImage();
+            $objWriter->writeAttribute('o:relid', 'rId' . $bgImage->getImageIndex());
+            $objWriter->writeAttribute('o:title', $bgImage->getName());
+            $objWriter->writeAttribute('type', 'frame');
+        }
+        $objWriter->endElement();
+
+        // v:shadow
+        $objWriter->startElement('v:shadow');
+        $objWriter->writeAttribute('on', 't');
+        $objWriter->writeAttribute('color', 'black');
+        $objWriter->writeAttribute('obscured', 't');
+        $objWriter->endElement();
+
+        // v:path
+        $objWriter->startElement('v:path');
+        $objWriter->writeAttribute('o:connecttype', 'none');
+        $objWriter->endElement();
+
+        // v:textbox
+        $objWriter->startElement('v:textbox');
+        $objWriter->writeAttribute('style', 'mso-direction-alt:auto');
+
+        // div
+        $objWriter->startElement('div');
+        $objWriter->writeAttribute('style', 'text-align:left');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // x:ClientData
+        $objWriter->startElement('x:ClientData');
+        $objWriter->writeAttribute('ObjectType', 'Note');
+
+        // x:MoveWithCells
+        $objWriter->writeElement('x:MoveWithCells', '');
+
+        // x:SizeWithCells
+        $objWriter->writeElement('x:SizeWithCells', '');
+
+        // x:AutoFill
+        $objWriter->writeElement('x:AutoFill', 'False');
+
+        // x:Row
+        $objWriter->writeElement('x:Row', (string) ($row - 1));
+
+        // x:Column
+        $objWriter->writeElement('x:Column', (string) ($column - 1));
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+}

+ 272 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php

@@ -0,0 +1,272 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\File;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Drawing as WorksheetDrawing;
+use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+
+class ContentTypes extends WriterPart
+{
+    /**
+     * Write content types to XML format.
+     *
+     * @param bool $includeCharts Flag indicating if we should include drawing details for charts
+     *
+     * @return string XML Output
+     */
+    public function writeContentTypes(Spreadsheet $spreadsheet, $includeCharts = false)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Types
+        $objWriter->startElement('Types');
+        $objWriter->writeAttribute('xmlns', Namespaces::CONTENT_TYPES);
+
+        // Theme
+        $this->writeOverrideContentType($objWriter, '/xl/theme/theme1.xml', 'application/vnd.openxmlformats-officedocument.theme+xml');
+
+        // Styles
+        $this->writeOverrideContentType($objWriter, '/xl/styles.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml');
+
+        // Rels
+        $this->writeDefaultContentType($objWriter, 'rels', 'application/vnd.openxmlformats-package.relationships+xml');
+
+        // XML
+        $this->writeDefaultContentType($objWriter, 'xml', 'application/xml');
+
+        // VML
+        $this->writeDefaultContentType($objWriter, 'vml', 'application/vnd.openxmlformats-officedocument.vmlDrawing');
+
+        // Workbook
+        if ($spreadsheet->hasMacros()) { //Macros in workbook ?
+            // Yes : not standard content but "macroEnabled"
+            $this->writeOverrideContentType($objWriter, '/xl/workbook.xml', 'application/vnd.ms-excel.sheet.macroEnabled.main+xml');
+            //... and define a new type for the VBA project
+            // Better use Override, because we can use 'bin' also for xl\printerSettings\printerSettings1.bin
+            $this->writeOverrideContentType($objWriter, '/xl/vbaProject.bin', 'application/vnd.ms-office.vbaProject');
+            if ($spreadsheet->hasMacrosCertificate()) {
+                // signed macros ?
+                // Yes : add needed information
+                $this->writeOverrideContentType($objWriter, '/xl/vbaProjectSignature.bin', 'application/vnd.ms-office.vbaProjectSignature');
+            }
+        } else {
+            // no macros in workbook, so standard type
+            $this->writeOverrideContentType($objWriter, '/xl/workbook.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml');
+        }
+
+        // DocProps
+        $this->writeOverrideContentType($objWriter, '/docProps/app.xml', 'application/vnd.openxmlformats-officedocument.extended-properties+xml');
+
+        $this->writeOverrideContentType($objWriter, '/docProps/core.xml', 'application/vnd.openxmlformats-package.core-properties+xml');
+
+        $customPropertyList = $spreadsheet->getProperties()->getCustomProperties();
+        if (!empty($customPropertyList)) {
+            $this->writeOverrideContentType($objWriter, '/docProps/custom.xml', 'application/vnd.openxmlformats-officedocument.custom-properties+xml');
+        }
+
+        // Worksheets
+        $sheetCount = $spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            $this->writeOverrideContentType($objWriter, '/xl/worksheets/sheet' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml');
+        }
+
+        // Shared strings
+        $this->writeOverrideContentType($objWriter, '/xl/sharedStrings.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml');
+
+        // Table
+        $table = 1;
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            $tableCount = $spreadsheet->getSheet($i)->getTableCollection()->count();
+
+            for ($t = 1; $t <= $tableCount; ++$t) {
+                $this->writeOverrideContentType($objWriter, '/xl/tables/table' . $table++ . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml');
+            }
+        }
+
+        // Add worksheet relationship content types
+        $unparsedLoadedData = $spreadsheet->getUnparsedLoadedData();
+        $chart = 1;
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            $drawings = $spreadsheet->getSheet($i)->getDrawingCollection();
+            $drawingCount = count($drawings);
+            $chartCount = ($includeCharts) ? $spreadsheet->getSheet($i)->getChartCount() : 0;
+            $hasUnparsedDrawing = isset($unparsedLoadedData['sheets'][$spreadsheet->getSheet($i)->getCodeName()]['drawingOriginalIds']);
+
+            //    We need a drawing relationship for the worksheet if we have either drawings or charts
+            if (($drawingCount > 0) || ($chartCount > 0) || $hasUnparsedDrawing) {
+                $this->writeOverrideContentType($objWriter, '/xl/drawings/drawing' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.drawing+xml');
+            }
+
+            //    If we have charts, then we need a chart relationship for every individual chart
+            if ($chartCount > 0) {
+                for ($c = 0; $c < $chartCount; ++$c) {
+                    $this->writeOverrideContentType($objWriter, '/xl/charts/chart' . $chart++ . '.xml', 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml');
+                }
+            }
+        }
+
+        // Comments
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            if (count($spreadsheet->getSheet($i)->getComments()) > 0) {
+                $this->writeOverrideContentType($objWriter, '/xl/comments' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml');
+            }
+        }
+
+        // Add media content-types
+        $aMediaContentTypes = [];
+        $mediaCount = $this->getParentWriter()->getDrawingHashTable()->count();
+        for ($i = 0; $i < $mediaCount; ++$i) {
+            $extension = '';
+            $mimeType = '';
+
+            $drawing = $this->getParentWriter()->getDrawingHashTable()->getByIndex($i);
+            if ($drawing instanceof WorksheetDrawing && $drawing->getPath() !== '') {
+                $extension = strtolower($drawing->getExtension());
+                if ($drawing->getIsUrl()) {
+                    $mimeType = image_type_to_mime_type($drawing->getType());
+                } else {
+                    $mimeType = $this->getImageMimeType($drawing->getPath());
+                }
+            } elseif ($drawing instanceof MemoryDrawing) {
+                $extension = strtolower($drawing->getMimeType());
+                $extension = explode('/', $extension);
+                $extension = $extension[1];
+
+                $mimeType = $drawing->getMimeType();
+            }
+
+            if ($mimeType !== '' && !isset($aMediaContentTypes[$extension])) {
+                $aMediaContentTypes[$extension] = $mimeType;
+
+                $this->writeDefaultContentType($objWriter, $extension, $mimeType);
+            }
+        }
+        if ($spreadsheet->hasRibbonBinObjects()) {
+            // Some additional objects in the ribbon ?
+            // we need to write "Extension" but not already write for media content
+            $tabRibbonTypes = array_diff($spreadsheet->getRibbonBinObjects('types') ?? [], array_keys($aMediaContentTypes));
+            foreach ($tabRibbonTypes as $aRibbonType) {
+                $mimeType = 'image/.' . $aRibbonType; //we wrote $mimeType like customUI Editor
+                $this->writeDefaultContentType($objWriter, $aRibbonType, $mimeType);
+            }
+        }
+        $sheetCount = $spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            if (count($spreadsheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) {
+                foreach ($spreadsheet->getSheet($i)->getHeaderFooter()->getImages() as $image) {
+                    if ($image->getPath() !== '' && !isset($aMediaContentTypes[strtolower($image->getExtension())])) {
+                        $aMediaContentTypes[strtolower($image->getExtension())] = $this->getImageMimeType($image->getPath());
+
+                        $this->writeDefaultContentType($objWriter, strtolower($image->getExtension()), $aMediaContentTypes[strtolower($image->getExtension())]);
+                    }
+                }
+            }
+
+            if (count($spreadsheet->getSheet($i)->getComments()) > 0) {
+                foreach ($spreadsheet->getSheet($i)->getComments() as $comment) {
+                    if (!$comment->hasBackgroundImage()) {
+                        continue;
+                    }
+
+                    $bgImage = $comment->getBackgroundImage();
+                    $bgImageExtentionKey = strtolower($bgImage->getImageFileExtensionForSave(false));
+
+                    if (!isset($aMediaContentTypes[$bgImageExtentionKey])) {
+                        $aMediaContentTypes[$bgImageExtentionKey] = $bgImage->getImageMimeType();
+
+                        $this->writeDefaultContentType($objWriter, $bgImageExtentionKey, $aMediaContentTypes[$bgImageExtentionKey]);
+                    }
+                }
+            }
+        }
+
+        // unparsed defaults
+        if (isset($unparsedLoadedData['default_content_types'])) {
+            foreach ($unparsedLoadedData['default_content_types'] as $extName => $contentType) {
+                $this->writeDefaultContentType($objWriter, $extName, $contentType);
+            }
+        }
+
+        // unparsed overrides
+        if (isset($unparsedLoadedData['override_content_types'])) {
+            foreach ($unparsedLoadedData['override_content_types'] as $partName => $overrideType) {
+                $this->writeOverrideContentType($objWriter, $partName, $overrideType);
+            }
+        }
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Get image mime type.
+     *
+     * @param string $filename Filename
+     *
+     * @return string Mime Type
+     */
+    private function getImageMimeType($filename)
+    {
+        if (File::fileExists($filename)) {
+            $image = getimagesize($filename);
+
+            return image_type_to_mime_type((is_array($image) && count($image) >= 3) ? $image[2] : 0);
+        }
+
+        throw new WriterException("File $filename does not exist");
+    }
+
+    /**
+     * Write Default content type.
+     *
+     * @param string $partName Part name
+     * @param string $contentType Content type
+     */
+    private function writeDefaultContentType(XMLWriter $objWriter, $partName, $contentType): void
+    {
+        if ($partName != '' && $contentType != '') {
+            // Write content type
+            $objWriter->startElement('Default');
+            $objWriter->writeAttribute('Extension', $partName);
+            $objWriter->writeAttribute('ContentType', $contentType);
+            $objWriter->endElement();
+        } else {
+            throw new WriterException('Invalid parameters passed.');
+        }
+    }
+
+    /**
+     * Write Override content type.
+     *
+     * @param string $partName Part name
+     * @param string $contentType Content type
+     */
+    private function writeOverrideContentType(XMLWriter $objWriter, $partName, $contentType): void
+    {
+        if ($partName != '' && $contentType != '') {
+            // Write content type
+            $objWriter->startElement('Override');
+            $objWriter->writeAttribute('PartName', $partName);
+            $objWriter->writeAttribute('ContentType', $contentType);
+            $objWriter->endElement();
+        } else {
+            throw new WriterException('Invalid parameters passed.');
+        }
+    }
+}

+ 244 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php

@@ -0,0 +1,244 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use Exception;
+use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\DefinedName;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as ActualWorksheet;
+
+class DefinedNames
+{
+    /** @var XMLWriter */
+    private $objWriter;
+
+    /** @var Spreadsheet */
+    private $spreadsheet;
+
+    public function __construct(XMLWriter $objWriter, Spreadsheet $spreadsheet)
+    {
+        $this->objWriter = $objWriter;
+        $this->spreadsheet = $spreadsheet;
+    }
+
+    public function write(): void
+    {
+        // Write defined names
+        $this->objWriter->startElement('definedNames');
+
+        // Named ranges
+        if (count($this->spreadsheet->getDefinedNames()) > 0) {
+            // Named ranges
+            $this->writeNamedRangesAndFormulae();
+        }
+
+        // Other defined names
+        $sheetCount = $this->spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            // NamedRange for autoFilter
+            $this->writeNamedRangeForAutofilter($this->spreadsheet->getSheet($i), $i);
+
+            // NamedRange for Print_Titles
+            $this->writeNamedRangeForPrintTitles($this->spreadsheet->getSheet($i), $i);
+
+            // NamedRange for Print_Area
+            $this->writeNamedRangeForPrintArea($this->spreadsheet->getSheet($i), $i);
+        }
+
+        $this->objWriter->endElement();
+    }
+
+    /**
+     * Write defined names.
+     */
+    private function writeNamedRangesAndFormulae(): void
+    {
+        // Loop named ranges
+        $definedNames = $this->spreadsheet->getDefinedNames();
+        foreach ($definedNames as $definedName) {
+            $this->writeDefinedName($definedName);
+        }
+    }
+
+    /**
+     * Write Defined Name for named range.
+     */
+    private function writeDefinedName(DefinedName $definedName): void
+    {
+        // definedName for named range
+        $local = -1;
+        if ($definedName->getLocalOnly() && $definedName->getScope() !== null) {
+            try {
+                $local = $definedName->getScope()->getParentOrThrow()->getIndex($definedName->getScope());
+            } catch (Exception $e) {
+                // See issue 2266 - deleting sheet which contains
+                //     defined names will cause Exception above.
+                return;
+            }
+        }
+        $this->objWriter->startElement('definedName');
+        $this->objWriter->writeAttribute('name', $definedName->getName());
+        if ($local >= 0) {
+            $this->objWriter->writeAttribute(
+                'localSheetId',
+                "$local"
+            );
+        }
+
+        $definedRange = $this->getDefinedRange($definedName);
+
+        $this->objWriter->writeRawData($definedRange);
+
+        $this->objWriter->endElement();
+    }
+
+    /**
+     * Write Defined Name for autoFilter.
+     */
+    private function writeNamedRangeForAutofilter(ActualWorksheet $worksheet, int $worksheetId = 0): void
+    {
+        // NamedRange for autoFilter
+        $autoFilterRange = $worksheet->getAutoFilter()->getRange();
+        if (!empty($autoFilterRange)) {
+            $this->objWriter->startElement('definedName');
+            $this->objWriter->writeAttribute('name', '_xlnm._FilterDatabase');
+            $this->objWriter->writeAttribute('localSheetId', "$worksheetId");
+            $this->objWriter->writeAttribute('hidden', '1');
+
+            // Create absolute coordinate and write as raw text
+            $range = Coordinate::splitRange($autoFilterRange);
+            $range = $range[0];
+            //    Strip any worksheet ref so we can make the cell ref absolute
+            [, $range[0]] = ActualWorksheet::extractSheetTitle($range[0], true);
+
+            $range[0] = Coordinate::absoluteCoordinate($range[0]);
+            if (count($range) > 1) {
+                $range[1] = Coordinate::absoluteCoordinate($range[1]);
+            }
+            $range = implode(':', $range);
+
+            $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!' . $range);
+
+            $this->objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write Defined Name for PrintTitles.
+     */
+    private function writeNamedRangeForPrintTitles(ActualWorksheet $worksheet, int $worksheetId = 0): void
+    {
+        // NamedRange for PrintTitles
+        if ($worksheet->getPageSetup()->isColumnsToRepeatAtLeftSet() || $worksheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
+            $this->objWriter->startElement('definedName');
+            $this->objWriter->writeAttribute('name', '_xlnm.Print_Titles');
+            $this->objWriter->writeAttribute('localSheetId', "$worksheetId");
+
+            // Setting string
+            $settingString = '';
+
+            // Columns to repeat
+            if ($worksheet->getPageSetup()->isColumnsToRepeatAtLeftSet()) {
+                $repeat = $worksheet->getPageSetup()->getColumnsToRepeatAtLeft();
+
+                $settingString .= '\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!$' . $repeat[0] . ':$' . $repeat[1];
+            }
+
+            // Rows to repeat
+            if ($worksheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
+                if ($worksheet->getPageSetup()->isColumnsToRepeatAtLeftSet()) {
+                    $settingString .= ',';
+                }
+
+                $repeat = $worksheet->getPageSetup()->getRowsToRepeatAtTop();
+
+                $settingString .= '\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!$' . $repeat[0] . ':$' . $repeat[1];
+            }
+
+            $this->objWriter->writeRawData($settingString);
+
+            $this->objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write Defined Name for PrintTitles.
+     */
+    private function writeNamedRangeForPrintArea(ActualWorksheet $worksheet, int $worksheetId = 0): void
+    {
+        // NamedRange for PrintArea
+        if ($worksheet->getPageSetup()->isPrintAreaSet()) {
+            $this->objWriter->startElement('definedName');
+            $this->objWriter->writeAttribute('name', '_xlnm.Print_Area');
+            $this->objWriter->writeAttribute('localSheetId', "$worksheetId");
+
+            // Print area
+            $printArea = Coordinate::splitRange($worksheet->getPageSetup()->getPrintArea());
+
+            $chunks = [];
+            foreach ($printArea as $printAreaRect) {
+                $printAreaRect[0] = Coordinate::absoluteReference($printAreaRect[0]);
+                $printAreaRect[1] = Coordinate::absoluteReference($printAreaRect[1]);
+                $chunks[] = '\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!' . implode(':', $printAreaRect);
+            }
+
+            $this->objWriter->writeRawData(implode(',', $chunks));
+
+            $this->objWriter->endElement();
+        }
+    }
+
+    private function getDefinedRange(DefinedName $definedName): string
+    {
+        $definedRange = $definedName->getValue();
+        $splitCount = preg_match_all(
+            '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui',
+            $definedRange,
+            $splitRanges,
+            PREG_OFFSET_CAPTURE
+        );
+
+        $lengths = array_map('strlen', array_column($splitRanges[0], 0));
+        $offsets = array_column($splitRanges[0], 1);
+
+        $worksheets = $splitRanges[2];
+        $columns = $splitRanges[6];
+        $rows = $splitRanges[7];
+
+        while ($splitCount > 0) {
+            --$splitCount;
+            $length = $lengths[$splitCount];
+            $offset = $offsets[$splitCount];
+            $worksheet = $worksheets[$splitCount][0];
+            $column = $columns[$splitCount][0];
+            $row = $rows[$splitCount][0];
+
+            $newRange = '';
+            if (empty($worksheet)) {
+                if (($offset === 0) || ($definedRange[$offset - 1] !== ':')) {
+                    // We should have a worksheet
+                    $ws = $definedName->getWorksheet();
+                    $worksheet = ($ws === null) ? null : $ws->getTitle();
+                }
+            } else {
+                $worksheet = str_replace("''", "'", trim($worksheet, "'"));
+            }
+
+            if (!empty($worksheet)) {
+                $newRange = "'" . str_replace("'", "''", $worksheet) . "'!";
+            }
+            $newRange = "{$newRange}{$column}{$row}";
+
+            $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length);
+        }
+
+        if (substr($definedRange, 0, 1) === '=') {
+            $definedRange = substr($definedRange, 1);
+        }
+
+        return $definedRange;
+    }
+}

+ 250 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php

@@ -0,0 +1,250 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Document\Properties;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\Date;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+
+class DocProps extends WriterPart
+{
+    /**
+     * Write docProps/app.xml to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeDocPropsApp(Spreadsheet $spreadsheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Properties
+        $objWriter->startElement('Properties');
+        $objWriter->writeAttribute('xmlns', Namespaces::EXTENDED_PROPERTIES);
+        $objWriter->writeAttribute('xmlns:vt', Namespaces::PROPERTIES_VTYPES);
+
+        // Application
+        $objWriter->writeElement('Application', 'Microsoft Excel');
+
+        // DocSecurity
+        $objWriter->writeElement('DocSecurity', '0');
+
+        // ScaleCrop
+        $objWriter->writeElement('ScaleCrop', 'false');
+
+        // HeadingPairs
+        $objWriter->startElement('HeadingPairs');
+
+        // Vector
+        $objWriter->startElement('vt:vector');
+        $objWriter->writeAttribute('size', '2');
+        $objWriter->writeAttribute('baseType', 'variant');
+
+        // Variant
+        $objWriter->startElement('vt:variant');
+        $objWriter->writeElement('vt:lpstr', 'Worksheets');
+        $objWriter->endElement();
+
+        // Variant
+        $objWriter->startElement('vt:variant');
+        $objWriter->writeElement('vt:i4', (string) $spreadsheet->getSheetCount());
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // TitlesOfParts
+        $objWriter->startElement('TitlesOfParts');
+
+        // Vector
+        $objWriter->startElement('vt:vector');
+        $objWriter->writeAttribute('size', (string) $spreadsheet->getSheetCount());
+        $objWriter->writeAttribute('baseType', 'lpstr');
+
+        $sheetCount = $spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            $objWriter->writeElement('vt:lpstr', $spreadsheet->getSheet($i)->getTitle());
+        }
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // Company
+        $objWriter->writeElement('Company', $spreadsheet->getProperties()->getCompany());
+
+        // Company
+        $objWriter->writeElement('Manager', $spreadsheet->getProperties()->getManager());
+
+        // LinksUpToDate
+        $objWriter->writeElement('LinksUpToDate', 'false');
+
+        // SharedDoc
+        $objWriter->writeElement('SharedDoc', 'false');
+
+        // HyperlinkBase
+        $objWriter->writeElement('HyperlinkBase', $spreadsheet->getProperties()->getHyperlinkBase());
+
+        // HyperlinksChanged
+        $objWriter->writeElement('HyperlinksChanged', 'false');
+
+        // AppVersion
+        $objWriter->writeElement('AppVersion', '12.0000');
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write docProps/core.xml to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeDocPropsCore(Spreadsheet $spreadsheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // cp:coreProperties
+        $objWriter->startElement('cp:coreProperties');
+        $objWriter->writeAttribute('xmlns:cp', Namespaces::CORE_PROPERTIES2);
+        $objWriter->writeAttribute('xmlns:dc', Namespaces::DC_ELEMENTS);
+        $objWriter->writeAttribute('xmlns:dcterms', Namespaces::DC_TERMS);
+        $objWriter->writeAttribute('xmlns:dcmitype', Namespaces::DC_DCMITYPE);
+        $objWriter->writeAttribute('xmlns:xsi', Namespaces::SCHEMA_INSTANCE);
+
+        // dc:creator
+        $objWriter->writeElement('dc:creator', $spreadsheet->getProperties()->getCreator());
+
+        // cp:lastModifiedBy
+        $objWriter->writeElement('cp:lastModifiedBy', $spreadsheet->getProperties()->getLastModifiedBy());
+
+        // dcterms:created
+        $objWriter->startElement('dcterms:created');
+        $objWriter->writeAttribute('xsi:type', 'dcterms:W3CDTF');
+        $created = $spreadsheet->getProperties()->getCreated();
+        $date = Date::dateTimeFromTimestamp("$created");
+        $objWriter->writeRawData($date->format(DATE_W3C));
+        $objWriter->endElement();
+
+        // dcterms:modified
+        $objWriter->startElement('dcterms:modified');
+        $objWriter->writeAttribute('xsi:type', 'dcterms:W3CDTF');
+        $created = $spreadsheet->getProperties()->getModified();
+        $date = Date::dateTimeFromTimestamp("$created");
+        $objWriter->writeRawData($date->format(DATE_W3C));
+        $objWriter->endElement();
+
+        // dc:title
+        $objWriter->writeElement('dc:title', $spreadsheet->getProperties()->getTitle());
+
+        // dc:description
+        $objWriter->writeElement('dc:description', $spreadsheet->getProperties()->getDescription());
+
+        // dc:subject
+        $objWriter->writeElement('dc:subject', $spreadsheet->getProperties()->getSubject());
+
+        // cp:keywords
+        $objWriter->writeElement('cp:keywords', $spreadsheet->getProperties()->getKeywords());
+
+        // cp:category
+        $objWriter->writeElement('cp:category', $spreadsheet->getProperties()->getCategory());
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write docProps/custom.xml to XML format.
+     *
+     * @return null|string XML Output
+     */
+    public function writeDocPropsCustom(Spreadsheet $spreadsheet)
+    {
+        $customPropertyList = $spreadsheet->getProperties()->getCustomProperties();
+        if (empty($customPropertyList)) {
+            return null;
+        }
+
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // cp:coreProperties
+        $objWriter->startElement('Properties');
+        $objWriter->writeAttribute('xmlns', Namespaces::CUSTOM_PROPERTIES);
+        $objWriter->writeAttribute('xmlns:vt', Namespaces::PROPERTIES_VTYPES);
+
+        foreach ($customPropertyList as $key => $customProperty) {
+            $propertyValue = $spreadsheet->getProperties()->getCustomPropertyValue($customProperty);
+            $propertyType = $spreadsheet->getProperties()->getCustomPropertyType($customProperty);
+
+            $objWriter->startElement('property');
+            $objWriter->writeAttribute('fmtid', '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}');
+            $objWriter->writeAttribute('pid', (string) ($key + 2));
+            $objWriter->writeAttribute('name', $customProperty);
+
+            switch ($propertyType) {
+                case Properties::PROPERTY_TYPE_INTEGER:
+                    $objWriter->writeElement('vt:i4', $propertyValue);
+
+                    break;
+                case Properties::PROPERTY_TYPE_FLOAT:
+                    $objWriter->writeElement('vt:r8', sprintf('%F', $propertyValue));
+
+                    break;
+                case Properties::PROPERTY_TYPE_BOOLEAN:
+                    $objWriter->writeElement('vt:bool', ($propertyValue) ? 'true' : 'false');
+
+                    break;
+                case Properties::PROPERTY_TYPE_DATE:
+                    $objWriter->startElement('vt:filetime');
+                    $date = Date::dateTimeFromTimestamp("$propertyValue");
+                    $objWriter->writeRawData($date->format(DATE_W3C));
+                    $objWriter->endElement();
+
+                    break;
+                default:
+                    $objWriter->writeElement('vt:lpwstr', $propertyValue);
+
+                    break;
+            }
+
+            $objWriter->endElement();
+        }
+
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+}

+ 571 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php

@@ -0,0 +1,571 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
+use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+
+class Drawing extends WriterPart
+{
+    /**
+     * Write drawings to XML format.
+     *
+     * @param bool $includeCharts Flag indicating if we should include drawing details for charts
+     *
+     * @return string XML Output
+     */
+    public function writeDrawings(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $includeCharts = false)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // xdr:wsDr
+        $objWriter->startElement('xdr:wsDr');
+        $objWriter->writeAttribute('xmlns:xdr', Namespaces::SPREADSHEET_DRAWING);
+        $objWriter->writeAttribute('xmlns:a', Namespaces::DRAWINGML);
+
+        // Loop through images and write drawings
+        $i = 1;
+        $iterator = $worksheet->getDrawingCollection()->getIterator();
+        while ($iterator->valid()) {
+            /** @var BaseDrawing $pDrawing */
+            $pDrawing = $iterator->current();
+            $pRelationId = $i;
+            $hlinkClickId = $pDrawing->getHyperlink() === null ? null : ++$i;
+
+            $this->writeDrawing($objWriter, $pDrawing, $pRelationId, $hlinkClickId);
+
+            $iterator->next();
+            ++$i;
+        }
+
+        if ($includeCharts) {
+            $chartCount = $worksheet->getChartCount();
+            // Loop through charts and write the chart position
+            if ($chartCount > 0) {
+                for ($c = 0; $c < $chartCount; ++$c) {
+                    $chart = $worksheet->getChartByIndex((string) $c);
+                    if ($chart !== false) {
+                        $this->writeChart($objWriter, $chart, $c + $i);
+                    }
+                }
+            }
+        }
+
+        // unparsed AlternateContent
+        $unparsedLoadedData = $worksheet->getParentOrThrow()->getUnparsedLoadedData();
+        if (isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingAlternateContents'])) {
+            foreach ($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingAlternateContents'] as $drawingAlternateContent) {
+                $objWriter->writeRaw($drawingAlternateContent);
+            }
+        }
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write drawings to XML format.
+     *
+     * @param int $relationId
+     */
+    public function writeChart(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Chart\Chart $chart, $relationId = -1): void
+    {
+        $tl = $chart->getTopLeftPosition();
+        $tlColRow = Coordinate::indexesFromString($tl['cell']);
+        $br = $chart->getBottomRightPosition();
+
+        $isTwoCellAnchor = $br['cell'] !== '';
+        if ($isTwoCellAnchor) {
+            $brColRow = Coordinate::indexesFromString($br['cell']);
+
+            $objWriter->startElement('xdr:twoCellAnchor');
+
+            $objWriter->startElement('xdr:from');
+            $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1));
+            $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset']));
+            $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1));
+            $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset']));
+            $objWriter->endElement();
+            $objWriter->startElement('xdr:to');
+            $objWriter->writeElement('xdr:col', (string) ($brColRow[0] - 1));
+            $objWriter->writeElement('xdr:colOff', self::stringEmu($br['xOffset']));
+            $objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1));
+            $objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset']));
+            $objWriter->endElement();
+        } elseif ($chart->getOneCellAnchor()) {
+            $objWriter->startElement('xdr:oneCellAnchor');
+
+            $objWriter->startElement('xdr:from');
+            $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1));
+            $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset']));
+            $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1));
+            $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset']));
+            $objWriter->endElement();
+            $objWriter->startElement('xdr:ext');
+            $objWriter->writeAttribute('cx', self::stringEmu($br['xOffset']));
+            $objWriter->writeAttribute('cy', self::stringEmu($br['yOffset']));
+            $objWriter->endElement();
+        } else {
+            $objWriter->startElement('xdr:absoluteAnchor');
+            $objWriter->startElement('xdr:pos');
+            $objWriter->writeAttribute('x', '0');
+            $objWriter->writeAttribute('y', '0');
+            $objWriter->endElement();
+            $objWriter->startElement('xdr:ext');
+            $objWriter->writeAttribute('cx', self::stringEmu($br['xOffset']));
+            $objWriter->writeAttribute('cy', self::stringEmu($br['yOffset']));
+            $objWriter->endElement();
+        }
+
+        $objWriter->startElement('xdr:graphicFrame');
+        $objWriter->writeAttribute('macro', '');
+        $objWriter->startElement('xdr:nvGraphicFramePr');
+        $objWriter->startElement('xdr:cNvPr');
+        $objWriter->writeAttribute('name', 'Chart ' . $relationId);
+        $objWriter->writeAttribute('id', (string) (1025 * $relationId));
+        $objWriter->endElement();
+        $objWriter->startElement('xdr:cNvGraphicFramePr');
+        $objWriter->startElement('a:graphicFrameLocks');
+        $objWriter->endElement();
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        $objWriter->startElement('xdr:xfrm');
+        $objWriter->startElement('a:off');
+        $objWriter->writeAttribute('x', '0');
+        $objWriter->writeAttribute('y', '0');
+        $objWriter->endElement();
+        $objWriter->startElement('a:ext');
+        $objWriter->writeAttribute('cx', '0');
+        $objWriter->writeAttribute('cy', '0');
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        $objWriter->startElement('a:graphic');
+        $objWriter->startElement('a:graphicData');
+        $objWriter->writeAttribute('uri', Namespaces::CHART);
+        $objWriter->startElement('c:chart');
+        $objWriter->writeAttribute('xmlns:c', Namespaces::CHART);
+        $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
+        $objWriter->writeAttribute('r:id', 'rId' . $relationId);
+        $objWriter->endElement();
+        $objWriter->endElement();
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        $objWriter->startElement('xdr:clientData');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write drawings to XML format.
+     *
+     * @param int $relationId
+     * @param null|int $hlinkClickId
+     */
+    public function writeDrawing(XMLWriter $objWriter, BaseDrawing $drawing, $relationId = -1, $hlinkClickId = null): void
+    {
+        if ($relationId >= 0) {
+            $isTwoCellAnchor = $drawing->getCoordinates2() !== '';
+            if ($isTwoCellAnchor) {
+                // xdr:twoCellAnchor
+                $objWriter->startElement('xdr:twoCellAnchor');
+                if ($drawing->validEditAs()) {
+                    $objWriter->writeAttribute('editAs', $drawing->getEditAs());
+                }
+                // Image location
+                $aCoordinates = Coordinate::indexesFromString($drawing->getCoordinates());
+                $aCoordinates2 = Coordinate::indexesFromString($drawing->getCoordinates2());
+
+                // xdr:from
+                $objWriter->startElement('xdr:from');
+                $objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1));
+                $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX()));
+                $objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1));
+                $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY()));
+                $objWriter->endElement();
+
+                // xdr:to
+                $objWriter->startElement('xdr:to');
+                $objWriter->writeElement('xdr:col', (string) ($aCoordinates2[0] - 1));
+                $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX2()));
+                $objWriter->writeElement('xdr:row', (string) ($aCoordinates2[1] - 1));
+                $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY2()));
+                $objWriter->endElement();
+            } else {
+                // xdr:oneCellAnchor
+                $objWriter->startElement('xdr:oneCellAnchor');
+                // Image location
+                $aCoordinates = Coordinate::indexesFromString($drawing->getCoordinates());
+
+                // xdr:from
+                $objWriter->startElement('xdr:from');
+                $objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1));
+                $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX()));
+                $objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1));
+                $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY()));
+                $objWriter->endElement();
+
+                // xdr:ext
+                $objWriter->startElement('xdr:ext');
+                $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth()));
+                $objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight()));
+                $objWriter->endElement();
+            }
+
+            // xdr:pic
+            $objWriter->startElement('xdr:pic');
+
+            // xdr:nvPicPr
+            $objWriter->startElement('xdr:nvPicPr');
+
+            // xdr:cNvPr
+            $objWriter->startElement('xdr:cNvPr');
+            $objWriter->writeAttribute('id', (string) $relationId);
+            $objWriter->writeAttribute('name', $drawing->getName());
+            $objWriter->writeAttribute('descr', $drawing->getDescription());
+
+            //a:hlinkClick
+            $this->writeHyperLinkDrawing($objWriter, $hlinkClickId);
+
+            $objWriter->endElement();
+
+            // xdr:cNvPicPr
+            $objWriter->startElement('xdr:cNvPicPr');
+
+            // a:picLocks
+            $objWriter->startElement('a:picLocks');
+            $objWriter->writeAttribute('noChangeAspect', '1');
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+
+            // xdr:blipFill
+            $objWriter->startElement('xdr:blipFill');
+
+            // a:blip
+            $objWriter->startElement('a:blip');
+            $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
+            $objWriter->writeAttribute('r:embed', 'rId' . $relationId);
+            $objWriter->endElement();
+
+            // a:stretch
+            $objWriter->startElement('a:stretch');
+            $objWriter->writeElement('a:fillRect', null);
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+
+            // xdr:spPr
+            $objWriter->startElement('xdr:spPr');
+
+            // a:xfrm
+            $objWriter->startElement('a:xfrm');
+            $objWriter->writeAttribute('rot', (string) SharedDrawing::degreesToAngle($drawing->getRotation()));
+            if ($isTwoCellAnchor) {
+                $objWriter->startElement('a:ext');
+                $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth()));
+                $objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight()));
+                $objWriter->endElement();
+            }
+            $objWriter->endElement();
+
+            // a:prstGeom
+            $objWriter->startElement('a:prstGeom');
+            $objWriter->writeAttribute('prst', 'rect');
+
+            // a:avLst
+            $objWriter->writeElement('a:avLst', null);
+
+            $objWriter->endElement();
+
+            if ($drawing->getShadow()->getVisible()) {
+                // a:effectLst
+                $objWriter->startElement('a:effectLst');
+
+                // a:outerShdw
+                $objWriter->startElement('a:outerShdw');
+                $objWriter->writeAttribute('blurRad', self::stringEmu($drawing->getShadow()->getBlurRadius()));
+                $objWriter->writeAttribute('dist', self::stringEmu($drawing->getShadow()->getDistance()));
+                $objWriter->writeAttribute('dir', (string) SharedDrawing::degreesToAngle($drawing->getShadow()->getDirection()));
+                $objWriter->writeAttribute('algn', $drawing->getShadow()->getAlignment());
+                $objWriter->writeAttribute('rotWithShape', '0');
+
+                // a:srgbClr
+                $objWriter->startElement('a:srgbClr');
+                $objWriter->writeAttribute('val', $drawing->getShadow()->getColor()->getRGB());
+
+                // a:alpha
+                $objWriter->startElement('a:alpha');
+                $objWriter->writeAttribute('val', (string) ($drawing->getShadow()->getAlpha() * 1000));
+                $objWriter->endElement();
+
+                $objWriter->endElement();
+
+                $objWriter->endElement();
+
+                $objWriter->endElement();
+            }
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+
+            // xdr:clientData
+            $objWriter->writeElement('xdr:clientData', null);
+
+            $objWriter->endElement();
+        } else {
+            throw new WriterException('Invalid parameters passed.');
+        }
+    }
+
+    /**
+     * Write VML header/footer images to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeVMLHeaderFooterImages(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Header/footer images
+        $images = $worksheet->getHeaderFooter()->getImages();
+
+        // xml
+        $objWriter->startElement('xml');
+        $objWriter->writeAttribute('xmlns:v', Namespaces::URN_VML);
+        $objWriter->writeAttribute('xmlns:o', Namespaces::URN_MSOFFICE);
+        $objWriter->writeAttribute('xmlns:x', Namespaces::URN_EXCEL);
+
+        // o:shapelayout
+        $objWriter->startElement('o:shapelayout');
+        $objWriter->writeAttribute('v:ext', 'edit');
+
+        // o:idmap
+        $objWriter->startElement('o:idmap');
+        $objWriter->writeAttribute('v:ext', 'edit');
+        $objWriter->writeAttribute('data', '1');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // v:shapetype
+        $objWriter->startElement('v:shapetype');
+        $objWriter->writeAttribute('id', '_x0000_t75');
+        $objWriter->writeAttribute('coordsize', '21600,21600');
+        $objWriter->writeAttribute('o:spt', '75');
+        $objWriter->writeAttribute('o:preferrelative', 't');
+        $objWriter->writeAttribute('path', 'm@4@5l@4@11@9@11@9@5xe');
+        $objWriter->writeAttribute('filled', 'f');
+        $objWriter->writeAttribute('stroked', 'f');
+
+        // v:stroke
+        $objWriter->startElement('v:stroke');
+        $objWriter->writeAttribute('joinstyle', 'miter');
+        $objWriter->endElement();
+
+        // v:formulas
+        $objWriter->startElement('v:formulas');
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'if lineDrawn pixelLineWidth 0');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'sum @0 1 0');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'sum 0 0 @1');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'prod @2 1 2');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'prod @3 21600 pixelWidth');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'prod @3 21600 pixelHeight');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'sum @0 0 1');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'prod @6 1 2');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'prod @7 21600 pixelWidth');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'sum @8 21600 0');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'prod @7 21600 pixelHeight');
+        $objWriter->endElement();
+
+        // v:f
+        $objWriter->startElement('v:f');
+        $objWriter->writeAttribute('eqn', 'sum @10 21600 0');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // v:path
+        $objWriter->startElement('v:path');
+        $objWriter->writeAttribute('o:extrusionok', 'f');
+        $objWriter->writeAttribute('gradientshapeok', 't');
+        $objWriter->writeAttribute('o:connecttype', 'rect');
+        $objWriter->endElement();
+
+        // o:lock
+        $objWriter->startElement('o:lock');
+        $objWriter->writeAttribute('v:ext', 'edit');
+        $objWriter->writeAttribute('aspectratio', 't');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // Loop through images
+        foreach ($images as $key => $value) {
+            $this->writeVMLHeaderFooterImage($objWriter, $key, $value);
+        }
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write VML comment to XML format.
+     *
+     * @param string $reference Reference
+     */
+    private function writeVMLHeaderFooterImage(XMLWriter $objWriter, $reference, HeaderFooterDrawing $image): void
+    {
+        // Calculate object id
+        preg_match('{(\d+)}', md5($reference), $m);
+        $id = 1500 + ((int) substr($m[1], 0, 2) * 1);
+
+        // Calculate offset
+        $width = $image->getWidth();
+        $height = $image->getHeight();
+        $marginLeft = $image->getOffsetX();
+        $marginTop = $image->getOffsetY();
+
+        // v:shape
+        $objWriter->startElement('v:shape');
+        $objWriter->writeAttribute('id', $reference);
+        $objWriter->writeAttribute('o:spid', '_x0000_s' . $id);
+        $objWriter->writeAttribute('type', '#_x0000_t75');
+        $objWriter->writeAttribute('style', "position:absolute;margin-left:{$marginLeft}px;margin-top:{$marginTop}px;width:{$width}px;height:{$height}px;z-index:1");
+
+        // v:imagedata
+        $objWriter->startElement('v:imagedata');
+        $objWriter->writeAttribute('o:relid', 'rId' . $reference);
+        $objWriter->writeAttribute('o:title', $image->getName());
+        $objWriter->endElement();
+
+        // o:lock
+        $objWriter->startElement('o:lock');
+        $objWriter->writeAttribute('v:ext', 'edit');
+        $objWriter->writeAttribute('textRotation', 't');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Get an array of all drawings.
+     *
+     * @return BaseDrawing[] All drawings in PhpSpreadsheet
+     */
+    public function allDrawings(Spreadsheet $spreadsheet)
+    {
+        // Get an array of all drawings
+        $aDrawings = [];
+
+        // Loop through PhpSpreadsheet
+        $sheetCount = $spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            // Loop through images and add to array
+            $iterator = $spreadsheet->getSheet($i)->getDrawingCollection()->getIterator();
+            while ($iterator->valid()) {
+                $aDrawings[] = $iterator->current();
+
+                $iterator->next();
+            }
+        }
+
+        return $aDrawings;
+    }
+
+    /**
+     * @param null|int $hlinkClickId
+     */
+    private function writeHyperLinkDrawing(XMLWriter $objWriter, $hlinkClickId): void
+    {
+        if ($hlinkClickId === null) {
+            return;
+        }
+
+        $objWriter->startElement('a:hlinkClick');
+        $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
+        $objWriter->writeAttribute('r:id', 'rId' . $hlinkClickId);
+        $objWriter->endElement();
+    }
+
+    private static function stringEmu(int $pixelValue): string
+    {
+        return (string) SharedDrawing::pixelsToEMU($pixelValue);
+    }
+}

+ 194 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php

@@ -0,0 +1,194 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+class FunctionPrefix
+{
+    const XLFNREGEXP = '/(?:_xlfn\.)?((?:_xlws\.)?\b('
+            // functions added with Excel 2010
+        . 'beta[.]dist'
+        . '|beta[.]inv'
+        . '|binom[.]dist'
+        . '|binom[.]inv'
+        . '|ceiling[.]precise'
+        . '|chisq[.]dist'
+        . '|chisq[.]dist[.]rt'
+        . '|chisq[.]inv'
+        . '|chisq[.]inv[.]rt'
+        . '|chisq[.]test'
+        . '|confidence[.]norm'
+        . '|confidence[.]t'
+        . '|covariance[.]p'
+        . '|covariance[.]s'
+        . '|erf[.]precise'
+        . '|erfc[.]precise'
+        . '|expon[.]dist'
+        . '|f[.]dist'
+        . '|f[.]dist[.]rt'
+        . '|f[.]inv'
+        . '|f[.]inv[.]rt'
+        . '|f[.]test'
+        . '|floor[.]precise'
+        . '|gamma[.]dist'
+        . '|gamma[.]inv'
+        . '|gammaln[.]precise'
+        . '|lognorm[.]dist'
+        . '|lognorm[.]inv'
+        . '|mode[.]mult'
+        . '|mode[.]sngl'
+        . '|negbinom[.]dist'
+        . '|networkdays[.]intl'
+        . '|norm[.]dist'
+        . '|norm[.]inv'
+        . '|norm[.]s[.]dist'
+        . '|norm[.]s[.]inv'
+        . '|percentile[.]exc'
+        . '|percentile[.]inc'
+        . '|percentrank[.]exc'
+        . '|percentrank[.]inc'
+        . '|poisson[.]dist'
+        . '|quartile[.]exc'
+        . '|quartile[.]inc'
+        . '|rank[.]avg'
+        . '|rank[.]eq'
+        . '|stdev[.]p'
+        . '|stdev[.]s'
+        . '|t[.]dist'
+        . '|t[.]dist[.]2t'
+        . '|t[.]dist[.]rt'
+        . '|t[.]inv'
+        . '|t[.]inv[.]2t'
+        . '|t[.]test'
+        . '|var[.]p'
+        . '|var[.]s'
+        . '|weibull[.]dist'
+        . '|z[.]test'
+        // functions added with Excel 2013
+        . '|acot'
+        . '|acoth'
+        . '|arabic'
+        . '|averageifs'
+        . '|binom[.]dist[.]range'
+        . '|bitand'
+        . '|bitlshift'
+        . '|bitor'
+        . '|bitrshift'
+        . '|bitxor'
+        . '|ceiling[.]math'
+        . '|combina'
+        . '|cot'
+        . '|coth'
+        . '|csc'
+        . '|csch'
+        . '|days'
+        . '|dbcs'
+        . '|decimal'
+        . '|encodeurl'
+        . '|filterxml'
+        . '|floor[.]math'
+        . '|formulatext'
+        . '|gamma'
+        . '|gauss'
+        . '|ifna'
+        . '|imcosh'
+        . '|imcot'
+        . '|imcsc'
+        . '|imcsch'
+        . '|imsec'
+        . '|imsech'
+        . '|imsinh'
+        . '|imtan'
+        . '|isformula'
+        . '|iso[.]ceiling'
+        . '|isoweeknum'
+        . '|munit'
+        . '|numbervalue'
+        . '|pduration'
+        . '|permutationa'
+        . '|phi'
+        . '|rri'
+        . '|sec'
+        . '|sech'
+        . '|sheet'
+        . '|sheets'
+        . '|skew[.]p'
+        . '|unichar'
+        . '|unicode'
+        . '|webservice'
+        . '|xor'
+        // functions added with Excel 2016
+        . '|forecast[.]et2'
+        . '|forecast[.]ets[.]confint'
+        . '|forecast[.]ets[.]seasonality'
+        . '|forecast[.]ets[.]stat'
+        . '|forecast[.]linear'
+        . '|switch'
+        // functions added with Excel 2019
+        . '|concat'
+        . '|countifs'
+        . '|ifs'
+        . '|maxifs'
+        . '|minifs'
+        . '|sumifs'
+        . '|textjoin'
+        // functions added with Excel 365
+        . '|filter'
+        . '|randarray'
+        . '|anchorarray'
+        . '|sequence'
+        . '|sort'
+        . '|sortby'
+        . '|unique'
+        . '|xlookup'
+        . '|xmatch'
+        . '|arraytotext'
+        . '|call'
+        . '|let'
+        . '|lambda'
+        . '|single'
+        . '|register[.]id'
+        . '|textafter'
+        . '|textbefore'
+        . '|textsplit'
+        . '|valuetotext'
+        . '))\s*\(/Umui';
+
+    const XLWSREGEXP = '/(?<!_xlws\.)('
+        // functions added with Excel 365
+        . 'filter'
+        . '|sort'
+        . ')\s*\(/mui';
+
+    /**
+     * Prefix function name in string with _xlfn. where required.
+     */
+    protected static function addXlfnPrefix(string $functionString): string
+    {
+        return (string) preg_replace(self::XLFNREGEXP, '_xlfn.$1(', $functionString);
+    }
+
+    /**
+     * Prefix function name in string with _xlws. where required.
+     */
+    protected static function addXlwsPrefix(string $functionString): string
+    {
+        return (string) preg_replace(self::XLWSREGEXP, '_xlws.$1(', $functionString);
+    }
+
+    /**
+     * Prefix function name in string with _xlfn. where required.
+     */
+    public static function addFunctionPrefix(string $functionString): string
+    {
+        return self::addXlwsPrefix(self::addXlfnPrefix($functionString));
+    }
+
+    /**
+     * Prefix function name in string with _xlfn. where required.
+     * Leading character, expected to be equals sign, is stripped.
+     */
+    public static function addFunctionPrefixStripEquals(string $functionString): string
+    {
+        return self::addFunctionPrefix(substr($functionString, 1));
+    }
+}

+ 498 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Rels.php

@@ -0,0 +1,498 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
+use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+
+class Rels extends WriterPart
+{
+    /**
+     * Write relationships to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeRelationships(Spreadsheet $spreadsheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Relationships
+        $objWriter->startElement('Relationships');
+        $objWriter->writeAttribute('xmlns', Namespaces::RELATIONSHIPS);
+
+        $customPropertyList = $spreadsheet->getProperties()->getCustomProperties();
+        if (!empty($customPropertyList)) {
+            // Relationship docProps/app.xml
+            $this->writeRelationship(
+                $objWriter,
+                4,
+                Namespaces::RELATIONSHIPS_CUSTOM_PROPERTIES,
+                'docProps/custom.xml'
+            );
+        }
+
+        // Relationship docProps/app.xml
+        $this->writeRelationship(
+            $objWriter,
+            3,
+            Namespaces::RELATIONSHIPS_EXTENDED_PROPERTIES,
+            'docProps/app.xml'
+        );
+
+        // Relationship docProps/core.xml
+        $this->writeRelationship(
+            $objWriter,
+            2,
+            Namespaces::CORE_PROPERTIES,
+            'docProps/core.xml'
+        );
+
+        // Relationship xl/workbook.xml
+        $this->writeRelationship(
+            $objWriter,
+            1,
+            Namespaces::OFFICE_DOCUMENT,
+            'xl/workbook.xml'
+        );
+        // a custom UI in workbook ?
+        $target = $spreadsheet->getRibbonXMLData('target');
+        if ($spreadsheet->hasRibbon()) {
+            $this->writeRelationShip(
+                $objWriter,
+                5,
+                Namespaces::EXTENSIBILITY,
+                is_string($target) ? $target : ''
+            );
+        }
+
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write workbook relationships to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeWorkbookRelationships(Spreadsheet $spreadsheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Relationships
+        $objWriter->startElement('Relationships');
+        $objWriter->writeAttribute('xmlns', Namespaces::RELATIONSHIPS);
+
+        // Relationship styles.xml
+        $this->writeRelationship(
+            $objWriter,
+            1,
+            Namespaces::STYLES,
+            'styles.xml'
+        );
+
+        // Relationship theme/theme1.xml
+        $this->writeRelationship(
+            $objWriter,
+            2,
+            Namespaces::THEME2,
+            'theme/theme1.xml'
+        );
+
+        // Relationship sharedStrings.xml
+        $this->writeRelationship(
+            $objWriter,
+            3,
+            Namespaces::SHARED_STRINGS,
+            'sharedStrings.xml'
+        );
+
+        // Relationships with sheets
+        $sheetCount = $spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            $this->writeRelationship(
+                $objWriter,
+                ($i + 1 + 3),
+                Namespaces::WORKSHEET,
+                'worksheets/sheet' . ($i + 1) . '.xml'
+            );
+        }
+        // Relationships for vbaProject if needed
+        // id : just after the last sheet
+        if ($spreadsheet->hasMacros()) {
+            $this->writeRelationShip(
+                $objWriter,
+                ($i + 1 + 3),
+                Namespaces::VBA,
+                'vbaProject.bin'
+            );
+            ++$i; //increment i if needed for an another relation
+        }
+
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write worksheet relationships to XML format.
+     *
+     * Numbering is as follows:
+     *     rId1                 - Drawings
+     *  rId_hyperlink_x     - Hyperlinks
+     *
+     * @param int $worksheetId
+     * @param bool $includeCharts Flag indicating if we should write charts
+     * @param int $tableRef Table ID
+     *
+     * @return string XML Output
+     */
+    public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false, $tableRef = 1)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Relationships
+        $objWriter->startElement('Relationships');
+        $objWriter->writeAttribute('xmlns', Namespaces::RELATIONSHIPS);
+
+        // Write drawing relationships?
+        $drawingOriginalIds = [];
+        $unparsedLoadedData = $worksheet->getParentOrThrow()->getUnparsedLoadedData();
+        if (isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds'])) {
+            $drawingOriginalIds = $unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds'];
+        }
+
+        if ($includeCharts) {
+            $charts = $worksheet->getChartCollection();
+        } else {
+            $charts = [];
+        }
+
+        if (($worksheet->getDrawingCollection()->count() > 0) || (count($charts) > 0) || $drawingOriginalIds) {
+            $rId = 1;
+
+            // Use original $relPath to get original $rId.
+            // Take first. In future can be overwritten.
+            // (! synchronize with \PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet::writeDrawings)
+            reset($drawingOriginalIds);
+            $relPath = key($drawingOriginalIds);
+            if (isset($drawingOriginalIds[$relPath])) {
+                $rId = (int) (substr($drawingOriginalIds[$relPath], 3));
+            }
+
+            // Generate new $relPath to write drawing relationship
+            $relPath = '../drawings/drawing' . $worksheetId . '.xml';
+            $this->writeRelationship(
+                $objWriter,
+                $rId,
+                Namespaces::RELATIONSHIPS_DRAWING,
+                $relPath
+            );
+        }
+
+        // Write hyperlink relationships?
+        $i = 1;
+        foreach ($worksheet->getHyperlinkCollection() as $hyperlink) {
+            if (!$hyperlink->isInternal()) {
+                $this->writeRelationship(
+                    $objWriter,
+                    '_hyperlink_' . $i,
+                    Namespaces::HYPERLINK,
+                    $hyperlink->getUrl(),
+                    'External'
+                );
+
+                ++$i;
+            }
+        }
+
+        // Write comments relationship?
+        $i = 1;
+        if (count($worksheet->getComments()) > 0 || isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'])) {
+            $this->writeRelationship(
+                $objWriter,
+                '_comments_vml' . $i,
+                Namespaces::VML,
+                '../drawings/vmlDrawing' . $worksheetId . '.vml'
+            );
+        }
+
+        if (count($worksheet->getComments()) > 0) {
+            $this->writeRelationship(
+                $objWriter,
+                '_comments' . $i,
+                Namespaces::COMMENTS,
+                '../comments' . $worksheetId . '.xml'
+            );
+        }
+
+        // Write Table
+        $tableCount = $worksheet->getTableCollection()->count();
+        for ($i = 1; $i <= $tableCount; ++$i) {
+            $this->writeRelationship(
+                $objWriter,
+                '_table_' . $i,
+                Namespaces::RELATIONSHIPS_TABLE,
+                '../tables/table' . $tableRef++ . '.xml'
+            );
+        }
+
+        // Write header/footer relationship?
+        $i = 1;
+        if (count($worksheet->getHeaderFooter()->getImages()) > 0) {
+            $this->writeRelationship(
+                $objWriter,
+                '_headerfooter_vml' . $i,
+                Namespaces::VML,
+                '../drawings/vmlDrawingHF' . $worksheetId . '.vml'
+            );
+        }
+
+        $this->writeUnparsedRelationship($worksheet, $objWriter, 'ctrlProps', Namespaces::RELATIONSHIPS_CTRLPROP);
+        $this->writeUnparsedRelationship($worksheet, $objWriter, 'vmlDrawings', Namespaces::VML);
+        $this->writeUnparsedRelationship($worksheet, $objWriter, 'printerSettings', Namespaces::RELATIONSHIPS_PRINTER_SETTINGS);
+
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+
+    private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, XMLWriter $objWriter, string $relationship, string $type): void
+    {
+        $unparsedLoadedData = $worksheet->getParentOrThrow()->getUnparsedLoadedData();
+        if (!isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()][$relationship])) {
+            return;
+        }
+
+        foreach ($unparsedLoadedData['sheets'][$worksheet->getCodeName()][$relationship] as $rId => $value) {
+            if (substr($rId, 0, 17) !== '_headerfooter_vml') {
+                $this->writeRelationship(
+                    $objWriter,
+                    $rId,
+                    $type,
+                    $value['relFilePath']
+                );
+            }
+        }
+    }
+
+    /**
+     * Write drawing relationships to XML format.
+     *
+     * @param int $chartRef Chart ID
+     * @param bool $includeCharts Flag indicating if we should write charts
+     *
+     * @return string XML Output
+     */
+    public function writeDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, &$chartRef, $includeCharts = false)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Relationships
+        $objWriter->startElement('Relationships');
+        $objWriter->writeAttribute('xmlns', Namespaces::RELATIONSHIPS);
+
+        // Loop through images and write relationships
+        $i = 1;
+        $iterator = $worksheet->getDrawingCollection()->getIterator();
+        while ($iterator->valid()) {
+            $drawing = $iterator->current();
+            if (
+                $drawing instanceof \PhpOffice\PhpSpreadsheet\Worksheet\Drawing
+                || $drawing instanceof MemoryDrawing
+            ) {
+                // Write relationship for image drawing
+                $this->writeRelationship(
+                    $objWriter,
+                    $i,
+                    Namespaces::IMAGE,
+                    '../media/' . $drawing->getIndexedFilename()
+                );
+
+                $i = $this->writeDrawingHyperLink($objWriter, $drawing, $i);
+            }
+
+            $iterator->next();
+            ++$i;
+        }
+
+        if ($includeCharts) {
+            // Loop through charts and write relationships
+            $chartCount = $worksheet->getChartCount();
+            if ($chartCount > 0) {
+                for ($c = 0; $c < $chartCount; ++$c) {
+                    $this->writeRelationship(
+                        $objWriter,
+                        $i++,
+                        Namespaces::RELATIONSHIPS_CHART,
+                        '../charts/chart' . ++$chartRef . '.xml'
+                    );
+                }
+            }
+        }
+
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write header/footer drawing relationships to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeHeaderFooterDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Relationships
+        $objWriter->startElement('Relationships');
+        $objWriter->writeAttribute('xmlns', Namespaces::RELATIONSHIPS);
+
+        // Loop through images and write relationships
+        foreach ($worksheet->getHeaderFooter()->getImages() as $key => $value) {
+            // Write relationship for image drawing
+            $this->writeRelationship(
+                $objWriter,
+                $key,
+                Namespaces::IMAGE,
+                '../media/' . $value->getIndexedFilename()
+            );
+        }
+
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+
+    public function writeVMLDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet): string
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Relationships
+        $objWriter->startElement('Relationships');
+        $objWriter->writeAttribute('xmlns', Namespaces::RELATIONSHIPS);
+
+        // Loop through images and write relationships
+        foreach ($worksheet->getComments() as $comment) {
+            if (!$comment->hasBackgroundImage()) {
+                continue;
+            }
+
+            $bgImage = $comment->getBackgroundImage();
+            $this->writeRelationship(
+                $objWriter,
+                $bgImage->getImageIndex(),
+                Namespaces::IMAGE,
+                '../media/' . $bgImage->getMediaFilename()
+            );
+        }
+
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write Override content type.
+     *
+     * @param int|string $id Relationship ID. rId will be prepended!
+     * @param string $type Relationship type
+     * @param string $target Relationship target
+     * @param string $targetMode Relationship target mode
+     */
+    private function writeRelationship(XMLWriter $objWriter, $id, $type, $target, $targetMode = ''): void
+    {
+        if ($type != '' && $target != '') {
+            // Write relationship
+            $objWriter->startElement('Relationship');
+            $objWriter->writeAttribute('Id', 'rId' . $id);
+            $objWriter->writeAttribute('Type', $type);
+            $objWriter->writeAttribute('Target', $target);
+
+            if ($targetMode != '') {
+                $objWriter->writeAttribute('TargetMode', $targetMode);
+            }
+
+            $objWriter->endElement();
+        } else {
+            throw new WriterException('Invalid parameters passed.');
+        }
+    }
+
+    private function writeDrawingHyperLink(XMLWriter $objWriter, BaseDrawing $drawing, int $i): int
+    {
+        if ($drawing->getHyperlink() === null) {
+            return $i;
+        }
+
+        ++$i;
+        $this->writeRelationship(
+            $objWriter,
+            $i,
+            Namespaces::HYPERLINK,
+            $drawing->getHyperlink()->getUrl(),
+            $drawing->getHyperlink()->getTypeHyperlink()
+        );
+
+        return $i;
+    }
+}

+ 46 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsRibbon.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+
+class RelsRibbon extends WriterPart
+{
+    /**
+     * Write relationships for additional objects of custom UI (ribbon).
+     *
+     * @return string XML Output
+     */
+    public function writeRibbonRelationships(Spreadsheet $spreadsheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Relationships
+        $objWriter->startElement('Relationships');
+        $objWriter->writeAttribute('xmlns', Namespaces::RELATIONSHIPS);
+        $localRels = $spreadsheet->getRibbonBinObjects('names');
+        if (is_array($localRels)) {
+            foreach ($localRels as $aId => $aTarget) {
+                $objWriter->startElement('Relationship');
+                $objWriter->writeAttribute('Id', $aId);
+                $objWriter->writeAttribute('Type', Namespaces::IMAGE);
+                $objWriter->writeAttribute('Target', $aTarget);
+                $objWriter->endElement();
+            }
+        }
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+}

+ 40 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/RelsVBA.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+
+class RelsVBA extends WriterPart
+{
+    /**
+     * Write relationships for a signed VBA Project.
+     *
+     * @return string XML Output
+     */
+    public function writeVBARelationships()
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Relationships
+        $objWriter->startElement('Relationships');
+        $objWriter->writeAttribute('xmlns', Namespaces::RELATIONSHIPS);
+        $objWriter->startElement('Relationship');
+        $objWriter->writeAttribute('Id', 'rId1');
+        $objWriter->writeAttribute('Type', Namespaces::VBA_SIGNATURE);
+        $objWriter->writeAttribute('Target', 'vbaProjectSignature.bin');
+        $objWriter->endElement();
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+}

+ 346 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php

@@ -0,0 +1,346 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Chart\ChartColor;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\RichText\RichText;
+use PhpOffice\PhpSpreadsheet\RichText\Run;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as ActualWorksheet;
+
+class StringTable extends WriterPart
+{
+    /**
+     * Create worksheet stringtable.
+     *
+     * @param string[] $existingTable Existing table to eventually merge with
+     *
+     * @return string[] String table for worksheet
+     */
+    public function createStringTable(ActualWorksheet $worksheet, $existingTable = null)
+    {
+        // Create string lookup table
+        $aStringTable = [];
+
+        // Is an existing table given?
+        if (($existingTable !== null) && is_array($existingTable)) {
+            $aStringTable = $existingTable;
+        }
+
+        // Fill index array
+        $aFlippedStringTable = $this->flipStringTable($aStringTable);
+
+        // Loop through cells
+        foreach ($worksheet->getCellCollection()->getCoordinates() as $coordinate) {
+            /** @var Cell $cell */
+            $cell = $worksheet->getCellCollection()->get($coordinate);
+            $cellValue = $cell->getValue();
+            if (
+                !is_object($cellValue) &&
+                ($cellValue !== null) &&
+                $cellValue !== '' &&
+                ($cell->getDataType() == DataType::TYPE_STRING || $cell->getDataType() == DataType::TYPE_STRING2 || $cell->getDataType() == DataType::TYPE_NULL) &&
+                !isset($aFlippedStringTable[$cellValue])
+            ) {
+                $aStringTable[] = $cellValue;
+                $aFlippedStringTable[$cellValue] = true;
+            } elseif (
+                $cellValue instanceof RichText &&
+                ($cellValue !== null) &&
+                !isset($aFlippedStringTable[$cellValue->getHashCode()])
+            ) {
+                $aStringTable[] = $cellValue;
+                $aFlippedStringTable[$cellValue->getHashCode()] = true;
+            }
+        }
+
+        return $aStringTable;
+    }
+
+    /**
+     * Write string table to XML format.
+     *
+     * @param (RichText|string)[] $stringTable
+     *
+     * @return string XML Output
+     */
+    public function writeStringTable(array $stringTable)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // String table
+        $objWriter->startElement('sst');
+        $objWriter->writeAttribute('xmlns', Namespaces::MAIN);
+        $objWriter->writeAttribute('uniqueCount', (string) count($stringTable));
+
+        // Loop through string table
+        foreach ($stringTable as $textElement) {
+            $objWriter->startElement('si');
+
+            if (!($textElement instanceof RichText)) {
+                $textToWrite = StringHelper::controlCharacterPHP2OOXML($textElement);
+                $objWriter->startElement('t');
+                if ($textToWrite !== trim($textToWrite)) {
+                    $objWriter->writeAttribute('xml:space', 'preserve');
+                }
+                $objWriter->writeRawData($textToWrite);
+                $objWriter->endElement();
+            } else {
+                $this->writeRichText($objWriter, $textElement);
+            }
+
+            $objWriter->endElement();
+        }
+
+        $objWriter->endElement();
+
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write Rich Text.
+     *
+     * @param string $prefix Optional Namespace prefix
+     */
+    public function writeRichText(XMLWriter $objWriter, RichText $richText, $prefix = null): void
+    {
+        if ($prefix !== null) {
+            $prefix .= ':';
+        }
+
+        // Loop through rich text elements
+        $elements = $richText->getRichTextElements();
+        foreach ($elements as $element) {
+            // r
+            $objWriter->startElement($prefix . 'r');
+
+            // rPr
+            if ($element instanceof Run && $element->getFont() !== null) {
+                // rPr
+                $objWriter->startElement($prefix . 'rPr');
+
+                // rFont
+                if ($element->getFont()->getName() !== null) {
+                    $objWriter->startElement($prefix . 'rFont');
+                    $objWriter->writeAttribute('val', $element->getFont()->getName());
+                    $objWriter->endElement();
+                }
+
+                // Bold
+                $objWriter->startElement($prefix . 'b');
+                $objWriter->writeAttribute('val', ($element->getFont()->getBold() ? 'true' : 'false'));
+                $objWriter->endElement();
+
+                // Italic
+                $objWriter->startElement($prefix . 'i');
+                $objWriter->writeAttribute('val', ($element->getFont()->getItalic() ? 'true' : 'false'));
+                $objWriter->endElement();
+
+                // Superscript / subscript
+                if ($element->getFont()->getSuperscript() || $element->getFont()->getSubscript()) {
+                    $objWriter->startElement($prefix . 'vertAlign');
+                    if ($element->getFont()->getSuperscript()) {
+                        $objWriter->writeAttribute('val', 'superscript');
+                    } elseif ($element->getFont()->getSubscript()) {
+                        $objWriter->writeAttribute('val', 'subscript');
+                    }
+                    $objWriter->endElement();
+                }
+
+                // Strikethrough
+                $objWriter->startElement($prefix . 'strike');
+                $objWriter->writeAttribute('val', ($element->getFont()->getStrikethrough() ? 'true' : 'false'));
+                $objWriter->endElement();
+
+                // Color
+                if ($element->getFont()->getColor()->getARGB() !== null) {
+                    $objWriter->startElement($prefix . 'color');
+                    $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB());
+                    $objWriter->endElement();
+                }
+
+                // Size
+                if ($element->getFont()->getSize() !== null) {
+                    $objWriter->startElement($prefix . 'sz');
+                    $objWriter->writeAttribute('val', (string) $element->getFont()->getSize());
+                    $objWriter->endElement();
+                }
+
+                // Underline
+                if ($element->getFont()->getUnderline() !== null) {
+                    $objWriter->startElement($prefix . 'u');
+                    $objWriter->writeAttribute('val', $element->getFont()->getUnderline());
+                    $objWriter->endElement();
+                }
+
+                $objWriter->endElement();
+            }
+
+            // t
+            $objWriter->startElement($prefix . 't');
+            $objWriter->writeAttribute('xml:space', 'preserve');
+            $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($element->getText()));
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write Rich Text.
+     *
+     * @param RichText|string $richText text string or Rich text
+     * @param string $prefix Optional Namespace prefix
+     */
+    public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $prefix = ''): void
+    {
+        if (!($richText instanceof RichText)) {
+            $textRun = $richText;
+            $richText = new RichText();
+            $run = $richText->createTextRun($textRun ?? '');
+            $run->setFont(null);
+        }
+
+        if ($prefix !== '') {
+            $prefix .= ':';
+        }
+
+        // Loop through rich text elements
+        $elements = $richText->getRichTextElements();
+        foreach ($elements as $element) {
+            // r
+            $objWriter->startElement($prefix . 'r');
+            if ($element->getFont() !== null) {
+                // rPr
+                $objWriter->startElement($prefix . 'rPr');
+                $fontSize = $element->getFont()->getSize();
+                if (is_numeric($fontSize)) {
+                    $fontSize *= (($fontSize < 100) ? 100 : 1);
+                    $objWriter->writeAttribute('sz', (string) $fontSize);
+                }
+
+                // Bold
+                $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? '1' : '0'));
+                // Italic
+                $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? '1' : '0'));
+                // Underline
+                $underlineType = $element->getFont()->getUnderline();
+                switch ($underlineType) {
+                    case 'single':
+                        $underlineType = 'sng';
+
+                        break;
+                    case 'double':
+                        $underlineType = 'dbl';
+
+                        break;
+                }
+                if ($underlineType !== null) {
+                    $objWriter->writeAttribute('u', $underlineType);
+                }
+                // Strikethrough
+                $objWriter->writeAttribute('strike', ($element->getFont()->getStriketype() ?: 'noStrike'));
+                // Superscript/subscript
+                if ($element->getFont()->getBaseLine()) {
+                    $objWriter->writeAttribute('baseline', (string) $element->getFont()->getBaseLine());
+                }
+
+                // Color
+                $this->writeChartTextColor($objWriter, $element->getFont()->getChartColor(), $prefix);
+
+                // Underscore Color
+                $this->writeChartTextColor($objWriter, $element->getFont()->getUnderlineColor(), $prefix, 'uFill');
+
+                // fontName
+                if ($element->getFont()->getLatin()) {
+                    $objWriter->startElement($prefix . 'latin');
+                    $objWriter->writeAttribute('typeface', $element->getFont()->getLatin());
+                    $objWriter->endElement();
+                }
+                if ($element->getFont()->getEastAsian()) {
+                    $objWriter->startElement($prefix . 'ea');
+                    $objWriter->writeAttribute('typeface', $element->getFont()->getEastAsian());
+                    $objWriter->endElement();
+                }
+                if ($element->getFont()->getComplexScript()) {
+                    $objWriter->startElement($prefix . 'cs');
+                    $objWriter->writeAttribute('typeface', $element->getFont()->getComplexScript());
+                    $objWriter->endElement();
+                }
+
+                $objWriter->endElement();
+            }
+
+            // t
+            $objWriter->startElement($prefix . 't');
+            $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($element->getText()));
+            $objWriter->endElement();
+
+            $objWriter->endElement();
+        }
+    }
+
+    private function writeChartTextColor(XMLWriter $objWriter, ?ChartColor $underlineColor, string $prefix, ?string $openTag = ''): void
+    {
+        if ($underlineColor !== null) {
+            $type = $underlineColor->getType();
+            $value = $underlineColor->getValue();
+            if (!empty($type) && !empty($value)) {
+                if ($openTag !== '') {
+                    $objWriter->startElement($prefix . $openTag);
+                }
+                $objWriter->startElement($prefix . 'solidFill');
+                $objWriter->startElement($prefix . $type);
+                $objWriter->writeAttribute('val', $value);
+                $alpha = $underlineColor->getAlpha();
+                if (is_numeric($alpha)) {
+                    $objWriter->startElement('a:alpha');
+                    $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha));
+                    $objWriter->endElement();
+                }
+                $objWriter->endElement(); // srgbClr/schemeClr/prstClr
+                $objWriter->endElement(); // solidFill
+                if ($openTag !== '') {
+                    $objWriter->endElement(); // uFill
+                }
+            }
+        }
+    }
+
+    /**
+     * Flip string table (for index searching).
+     *
+     * @param array $stringTable Stringtable
+     *
+     * @return array
+     */
+    public function flipStringTable(array $stringTable)
+    {
+        // Return value
+        $returnValue = [];
+
+        // Loop through stringtable and add flipped items to $returnValue
+        foreach ($stringTable as $key => $value) {
+            if (!$value instanceof RichText) {
+                $returnValue[$value] = $key;
+            } elseif ($value instanceof RichText) {
+                $returnValue[$value->getHashCode()] = $key;
+            }
+        }
+
+        return $returnValue;
+    }
+}

+ 734 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Style.php

@@ -0,0 +1,734 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Borders;
+use PhpOffice\PhpSpreadsheet\Style\Conditional;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+use PhpOffice\PhpSpreadsheet\Style\Font;
+use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
+use PhpOffice\PhpSpreadsheet\Style\Protection;
+
+class Style extends WriterPart
+{
+    /**
+     * Write styles to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeStyles(Spreadsheet $spreadsheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // styleSheet
+        $objWriter->startElement('styleSheet');
+        $objWriter->writeAttribute('xml:space', 'preserve');
+        $objWriter->writeAttribute('xmlns', Namespaces::MAIN);
+
+        // numFmts
+        $objWriter->startElement('numFmts');
+        $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getNumFmtHashTable()->count());
+
+        // numFmt
+        for ($i = 0; $i < $this->getParentWriter()->getNumFmtHashTable()->count(); ++$i) {
+            $this->writeNumFmt($objWriter, $this->getParentWriter()->getNumFmtHashTable()->getByIndex($i), $i);
+        }
+
+        $objWriter->endElement();
+
+        // fonts
+        $objWriter->startElement('fonts');
+        $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFontHashTable()->count());
+
+        // font
+        for ($i = 0; $i < $this->getParentWriter()->getFontHashTable()->count(); ++$i) {
+            $thisfont = $this->getParentWriter()->getFontHashTable()->getByIndex($i);
+            if ($thisfont !== null) {
+                $this->writeFont($objWriter, $thisfont);
+            }
+        }
+
+        $objWriter->endElement();
+
+        // fills
+        $objWriter->startElement('fills');
+        $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFillHashTable()->count());
+
+        // fill
+        for ($i = 0; $i < $this->getParentWriter()->getFillHashTable()->count(); ++$i) {
+            $thisfill = $this->getParentWriter()->getFillHashTable()->getByIndex($i);
+            if ($thisfill !== null) {
+                $this->writeFill($objWriter, $thisfill);
+            }
+        }
+
+        $objWriter->endElement();
+
+        // borders
+        $objWriter->startElement('borders');
+        $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getBordersHashTable()->count());
+
+        // border
+        for ($i = 0; $i < $this->getParentWriter()->getBordersHashTable()->count(); ++$i) {
+            $thisborder = $this->getParentWriter()->getBordersHashTable()->getByIndex($i);
+            if ($thisborder !== null) {
+                $this->writeBorder($objWriter, $thisborder);
+            }
+        }
+
+        $objWriter->endElement();
+
+        // cellStyleXfs
+        $objWriter->startElement('cellStyleXfs');
+        $objWriter->writeAttribute('count', '1');
+
+        // xf
+        $objWriter->startElement('xf');
+        $objWriter->writeAttribute('numFmtId', '0');
+        $objWriter->writeAttribute('fontId', '0');
+        $objWriter->writeAttribute('fillId', '0');
+        $objWriter->writeAttribute('borderId', '0');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // cellXfs
+        $objWriter->startElement('cellXfs');
+        $objWriter->writeAttribute('count', (string) count($spreadsheet->getCellXfCollection()));
+
+        // xf
+        $alignment = new Alignment();
+        $defaultAlignHash = $alignment->getHashCode();
+        if ($defaultAlignHash !== $spreadsheet->getDefaultStyle()->getAlignment()->getHashCode()) {
+            $defaultAlignHash = '';
+        }
+        foreach ($spreadsheet->getCellXfCollection() as $cellXf) {
+            $this->writeCellStyleXf($objWriter, $cellXf, $spreadsheet, $defaultAlignHash);
+        }
+
+        $objWriter->endElement();
+
+        // cellStyles
+        $objWriter->startElement('cellStyles');
+        $objWriter->writeAttribute('count', '1');
+
+        // cellStyle
+        $objWriter->startElement('cellStyle');
+        $objWriter->writeAttribute('name', 'Normal');
+        $objWriter->writeAttribute('xfId', '0');
+        $objWriter->writeAttribute('builtinId', '0');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // dxfs
+        $objWriter->startElement('dxfs');
+        $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getStylesConditionalHashTable()->count());
+
+        // dxf
+        for ($i = 0; $i < $this->getParentWriter()->getStylesConditionalHashTable()->count(); ++$i) {
+            /** @var ?Conditional */
+            $thisstyle = $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i);
+            if ($thisstyle !== null) {
+                $this->writeCellStyleDxf($objWriter, $thisstyle->getStyle());
+            }
+        }
+
+        $objWriter->endElement();
+
+        // tableStyles
+        $objWriter->startElement('tableStyles');
+        $objWriter->writeAttribute('defaultTableStyle', 'TableStyleMedium9');
+        $objWriter->writeAttribute('defaultPivotStyle', 'PivotTableStyle1');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write Fill.
+     */
+    private function writeFill(XMLWriter $objWriter, Fill $fill): void
+    {
+        // Check if this is a pattern type or gradient type
+        if (
+            $fill->getFillType() === Fill::FILL_GRADIENT_LINEAR ||
+            $fill->getFillType() === Fill::FILL_GRADIENT_PATH
+        ) {
+            // Gradient fill
+            $this->writeGradientFill($objWriter, $fill);
+        } elseif ($fill->getFillType() !== null) {
+            // Pattern fill
+            $this->writePatternFill($objWriter, $fill);
+        }
+    }
+
+    /**
+     * Write Gradient Fill.
+     */
+    private function writeGradientFill(XMLWriter $objWriter, Fill $fill): void
+    {
+        // fill
+        $objWriter->startElement('fill');
+
+        // gradientFill
+        $objWriter->startElement('gradientFill');
+        $objWriter->writeAttribute('type', (string) $fill->getFillType());
+        $objWriter->writeAttribute('degree', (string) $fill->getRotation());
+
+        // stop
+        $objWriter->startElement('stop');
+        $objWriter->writeAttribute('position', '0');
+
+        // color
+        if ($fill->getStartColor()->getARGB() !== null) {
+            $objWriter->startElement('color');
+            $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB());
+            $objWriter->endElement();
+        }
+
+        $objWriter->endElement();
+
+        // stop
+        $objWriter->startElement('stop');
+        $objWriter->writeAttribute('position', '1');
+
+        // color
+        if ($fill->getEndColor()->getARGB() !== null) {
+            $objWriter->startElement('color');
+            $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB());
+            $objWriter->endElement();
+        }
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    private static function writePatternColors(Fill $fill): bool
+    {
+        if ($fill->getFillType() === Fill::FILL_NONE) {
+            return false;
+        }
+
+        return $fill->getFillType() === Fill::FILL_SOLID || $fill->getColorsChanged();
+    }
+
+    /**
+     * Write Pattern Fill.
+     */
+    private function writePatternFill(XMLWriter $objWriter, Fill $fill): void
+    {
+        // fill
+        $objWriter->startElement('fill');
+
+        // patternFill
+        $objWriter->startElement('patternFill');
+        $objWriter->writeAttribute('patternType', (string) $fill->getFillType());
+
+        if (self::writePatternColors($fill)) {
+            // fgColor
+            if ($fill->getStartColor()->getARGB()) {
+                if (!$fill->getEndColor()->getARGB() && $fill->getFillType() === Fill::FILL_SOLID) {
+                    $objWriter->startElement('bgColor');
+                    $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB());
+                } else {
+                    $objWriter->startElement('fgColor');
+                    $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB());
+                }
+                $objWriter->endElement();
+            }
+            // bgColor
+            if ($fill->getEndColor()->getARGB()) {
+                $objWriter->startElement('bgColor');
+                $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB());
+                $objWriter->endElement();
+            }
+        }
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    private function startFont(XMLWriter $objWriter, bool &$fontStarted): void
+    {
+        if (!$fontStarted) {
+            $fontStarted = true;
+            $objWriter->startElement('font');
+        }
+    }
+
+    /**
+     * Write Font.
+     */
+    private function writeFont(XMLWriter $objWriter, Font $font): void
+    {
+        $fontStarted = false;
+        // font
+        //    Weird! The order of these elements actually makes a difference when opening Xlsx
+        //        files in Excel2003 with the compatibility pack. It's not documented behaviour,
+        //        and makes for a real WTF!
+
+        // Bold. We explicitly write this element also when false (like MS Office Excel 2007 does
+        // for conditional formatting). Otherwise it will apparently not be picked up in conditional
+        // formatting style dialog
+        if ($font->getBold() !== null) {
+            $this->startFont($objWriter, $fontStarted);
+            $objWriter->startElement('b');
+            $objWriter->writeAttribute('val', $font->getBold() ? '1' : '0');
+            $objWriter->endElement();
+        }
+
+        // Italic
+        if ($font->getItalic() !== null) {
+            $this->startFont($objWriter, $fontStarted);
+            $objWriter->startElement('i');
+            $objWriter->writeAttribute('val', $font->getItalic() ? '1' : '0');
+            $objWriter->endElement();
+        }
+
+        // Strikethrough
+        if ($font->getStrikethrough() !== null) {
+            $this->startFont($objWriter, $fontStarted);
+            $objWriter->startElement('strike');
+            $objWriter->writeAttribute('val', $font->getStrikethrough() ? '1' : '0');
+            $objWriter->endElement();
+        }
+
+        // Underline
+        if ($font->getUnderline() !== null) {
+            $this->startFont($objWriter, $fontStarted);
+            $objWriter->startElement('u');
+            $objWriter->writeAttribute('val', $font->getUnderline());
+            $objWriter->endElement();
+        }
+
+        // Superscript / subscript
+        if ($font->getSuperscript() === true || $font->getSubscript() === true) {
+            $this->startFont($objWriter, $fontStarted);
+            $objWriter->startElement('vertAlign');
+            if ($font->getSuperscript() === true) {
+                $objWriter->writeAttribute('val', 'superscript');
+            } elseif ($font->getSubscript() === true) {
+                $objWriter->writeAttribute('val', 'subscript');
+            }
+            $objWriter->endElement();
+        }
+
+        // Size
+        if ($font->getSize() !== null) {
+            $this->startFont($objWriter, $fontStarted);
+            $objWriter->startElement('sz');
+            $objWriter->writeAttribute('val', StringHelper::formatNumber($font->getSize()));
+            $objWriter->endElement();
+        }
+
+        // Foreground color
+        if ($font->getColor()->getARGB() !== null) {
+            $this->startFont($objWriter, $fontStarted);
+            $objWriter->startElement('color');
+            $objWriter->writeAttribute('rgb', $font->getColor()->getARGB());
+            $objWriter->endElement();
+        }
+
+        // Name
+        if ($font->getName() !== null) {
+            $this->startFont($objWriter, $fontStarted);
+            $objWriter->startElement('name');
+            $objWriter->writeAttribute('val', $font->getName());
+            $objWriter->endElement();
+        }
+
+        if (!empty($font->getScheme())) {
+            $this->startFont($objWriter, $fontStarted);
+            $objWriter->startElement('scheme');
+            $objWriter->writeAttribute('val', $font->getScheme());
+            $objWriter->endElement();
+        }
+
+        if ($fontStarted) {
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write Border.
+     */
+    private function writeBorder(XMLWriter $objWriter, Borders $borders): void
+    {
+        // Write border
+        $objWriter->startElement('border');
+        // Diagonal?
+        switch ($borders->getDiagonalDirection()) {
+            case Borders::DIAGONAL_UP:
+                $objWriter->writeAttribute('diagonalUp', 'true');
+                $objWriter->writeAttribute('diagonalDown', 'false');
+
+                break;
+            case Borders::DIAGONAL_DOWN:
+                $objWriter->writeAttribute('diagonalUp', 'false');
+                $objWriter->writeAttribute('diagonalDown', 'true');
+
+                break;
+            case Borders::DIAGONAL_BOTH:
+                $objWriter->writeAttribute('diagonalUp', 'true');
+                $objWriter->writeAttribute('diagonalDown', 'true');
+
+                break;
+        }
+
+        // BorderPr
+        $this->writeBorderPr($objWriter, 'left', $borders->getLeft());
+        $this->writeBorderPr($objWriter, 'right', $borders->getRight());
+        $this->writeBorderPr($objWriter, 'top', $borders->getTop());
+        $this->writeBorderPr($objWriter, 'bottom', $borders->getBottom());
+        $this->writeBorderPr($objWriter, 'diagonal', $borders->getDiagonal());
+        $objWriter->endElement();
+    }
+
+    /** @var mixed */
+    private static $scrutinizerFalse = false;
+
+    /**
+     * Write Cell Style Xf.
+     */
+    private function writeCellStyleXf(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Style\Style $style, Spreadsheet $spreadsheet, string $defaultAlignHash): void
+    {
+        // xf
+        $objWriter->startElement('xf');
+        $objWriter->writeAttribute('xfId', '0');
+        $objWriter->writeAttribute('fontId', (string) (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($style->getFont()->getHashCode()));
+        if ($style->getQuotePrefix()) {
+            $objWriter->writeAttribute('quotePrefix', '1');
+        }
+
+        if ($style->getNumberFormat()->getBuiltInFormatCode() === self::$scrutinizerFalse) {
+            $objWriter->writeAttribute('numFmtId', (string) (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($style->getNumberFormat()->getHashCode()) + 164));
+        } else {
+            $objWriter->writeAttribute('numFmtId', (string) (int) $style->getNumberFormat()->getBuiltInFormatCode());
+        }
+
+        $objWriter->writeAttribute('fillId', (string) (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($style->getFill()->getHashCode()));
+        $objWriter->writeAttribute('borderId', (string) (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($style->getBorders()->getHashCode()));
+
+        // Apply styles?
+        $objWriter->writeAttribute('applyFont', ($spreadsheet->getDefaultStyle()->getFont()->getHashCode() != $style->getFont()->getHashCode()) ? '1' : '0');
+        $objWriter->writeAttribute('applyNumberFormat', ($spreadsheet->getDefaultStyle()->getNumberFormat()->getHashCode() != $style->getNumberFormat()->getHashCode()) ? '1' : '0');
+        $objWriter->writeAttribute('applyFill', ($spreadsheet->getDefaultStyle()->getFill()->getHashCode() != $style->getFill()->getHashCode()) ? '1' : '0');
+        $objWriter->writeAttribute('applyBorder', ($spreadsheet->getDefaultStyle()->getBorders()->getHashCode() != $style->getBorders()->getHashCode()) ? '1' : '0');
+        if ($defaultAlignHash !== '' && $defaultAlignHash === $style->getAlignment()->getHashCode()) {
+            $applyAlignment = '0';
+        } else {
+            $applyAlignment = '1';
+        }
+        $objWriter->writeAttribute('applyAlignment', $applyAlignment);
+        if ($style->getProtection()->getLocked() != Protection::PROTECTION_INHERIT || $style->getProtection()->getHidden() != Protection::PROTECTION_INHERIT) {
+            $objWriter->writeAttribute('applyProtection', 'true');
+        }
+
+        // alignment
+        if ($applyAlignment === '1') {
+            $objWriter->startElement('alignment');
+            $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? '';
+            $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? '';
+            if ($horizontal !== '') {
+                $objWriter->writeAttribute('horizontal', $horizontal);
+            }
+            if ($vertical !== '') {
+                $objWriter->writeAttribute('vertical', $vertical);
+            }
+
+            if ($style->getAlignment()->getTextRotation() >= 0) {
+                $textRotation = $style->getAlignment()->getTextRotation();
+            } else {
+                $textRotation = 90 - $style->getAlignment()->getTextRotation();
+            }
+            $objWriter->writeAttribute('textRotation', (string) $textRotation);
+
+            $objWriter->writeAttribute('wrapText', ($style->getAlignment()->getWrapText() ? 'true' : 'false'));
+            $objWriter->writeAttribute('shrinkToFit', ($style->getAlignment()->getShrinkToFit() ? 'true' : 'false'));
+
+            if ($style->getAlignment()->getIndent() > 0) {
+                $objWriter->writeAttribute('indent', (string) $style->getAlignment()->getIndent());
+            }
+            if ($style->getAlignment()->getReadOrder() > 0) {
+                $objWriter->writeAttribute('readingOrder', (string) $style->getAlignment()->getReadOrder());
+            }
+            $objWriter->endElement();
+        }
+
+        // protection
+        if ($style->getProtection()->getLocked() != Protection::PROTECTION_INHERIT || $style->getProtection()->getHidden() != Protection::PROTECTION_INHERIT) {
+            $objWriter->startElement('protection');
+            if ($style->getProtection()->getLocked() != Protection::PROTECTION_INHERIT) {
+                $objWriter->writeAttribute('locked', ($style->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED ? 'true' : 'false'));
+            }
+            if ($style->getProtection()->getHidden() != Protection::PROTECTION_INHERIT) {
+                $objWriter->writeAttribute('hidden', ($style->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED ? 'true' : 'false'));
+            }
+            $objWriter->endElement();
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Cell Style Dxf.
+     */
+    private function writeCellStyleDxf(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Style\Style $style): void
+    {
+        // dxf
+        $objWriter->startElement('dxf');
+
+        // font
+        $this->writeFont($objWriter, $style->getFont());
+
+        // numFmt
+        $this->writeNumFmt($objWriter, $style->getNumberFormat());
+
+        // fill
+        $this->writeFill($objWriter, $style->getFill());
+
+        // alignment
+        $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? '';
+        $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? '';
+        $rotation = $style->getAlignment()->getTextRotation();
+        if ($horizontal || $vertical || $rotation !== null) {
+            $objWriter->startElement('alignment');
+            if ($horizontal) {
+                $objWriter->writeAttribute('horizontal', $horizontal);
+            }
+            if ($vertical) {
+                $objWriter->writeAttribute('vertical', $vertical);
+            }
+
+            if ($rotation !== null) {
+                if ($rotation >= 0) {
+                    $textRotation = $rotation;
+                } else {
+                    $textRotation = 90 - $rotation;
+                }
+                $objWriter->writeAttribute('textRotation', (string) $textRotation);
+            }
+            $objWriter->endElement();
+        }
+
+        // border
+        $this->writeBorder($objWriter, $style->getBorders());
+
+        // protection
+        if ((!empty($style->getProtection()->getLocked())) || (!empty($style->getProtection()->getHidden()))) {
+            if (
+                $style->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT ||
+                $style->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT
+            ) {
+                $objWriter->startElement('protection');
+                if (
+                    ($style->getProtection()->getLocked() !== null) &&
+                    ($style->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT)
+                ) {
+                    $objWriter->writeAttribute('locked', ($style->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED ? 'true' : 'false'));
+                }
+                if (
+                    ($style->getProtection()->getHidden() !== null) &&
+                    ($style->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT)
+                ) {
+                    $objWriter->writeAttribute('hidden', ($style->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED ? 'true' : 'false'));
+                }
+                $objWriter->endElement();
+            }
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write BorderPr.
+     *
+     * @param string $name Element name
+     */
+    private function writeBorderPr(XMLWriter $objWriter, $name, Border $border): void
+    {
+        // Write BorderPr
+        if ($border->getBorderStyle() === Border::BORDER_OMIT) {
+            return;
+        }
+        $objWriter->startElement($name);
+        if ($border->getBorderStyle() !== Border::BORDER_NONE) {
+            $objWriter->writeAttribute('style', $border->getBorderStyle());
+
+            // color
+            if ($border->getColor()->getARGB() !== null) {
+                $objWriter->startElement('color');
+                $objWriter->writeAttribute('rgb', $border->getColor()->getARGB());
+                $objWriter->endElement();
+            }
+        }
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write NumberFormat.
+     *
+     * @param int $id Number Format identifier
+     */
+    private function writeNumFmt(XMLWriter $objWriter, ?NumberFormat $numberFormat, $id = 0): void
+    {
+        // Translate formatcode
+        $formatCode = ($numberFormat === null) ? null : $numberFormat->getFormatCode();
+
+        // numFmt
+        if ($formatCode !== null) {
+            $objWriter->startElement('numFmt');
+            $objWriter->writeAttribute('numFmtId', (string) ($id + 164));
+            $objWriter->writeAttribute('formatCode', $formatCode);
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Get an array of all styles.
+     *
+     * @return \PhpOffice\PhpSpreadsheet\Style\Style[] All styles in PhpSpreadsheet
+     */
+    public function allStyles(Spreadsheet $spreadsheet)
+    {
+        return $spreadsheet->getCellXfCollection();
+    }
+
+    /**
+     * Get an array of all conditional styles.
+     *
+     * @return Conditional[] All conditional styles in PhpSpreadsheet
+     */
+    public function allConditionalStyles(Spreadsheet $spreadsheet)
+    {
+        // Get an array of all styles
+        $aStyles = [];
+
+        $sheetCount = $spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            foreach ($spreadsheet->getSheet($i)->getConditionalStylesCollection() as $conditionalStyles) {
+                foreach ($conditionalStyles as $conditionalStyle) {
+                    $aStyles[] = $conditionalStyle;
+                }
+            }
+        }
+
+        return $aStyles;
+    }
+
+    /**
+     * Get an array of all fills.
+     *
+     * @return Fill[] All fills in PhpSpreadsheet
+     */
+    public function allFills(Spreadsheet $spreadsheet)
+    {
+        // Get an array of unique fills
+        $aFills = [];
+
+        // Two first fills are predefined
+        $fill0 = new Fill();
+        $fill0->setFillType(Fill::FILL_NONE);
+        $aFills[] = $fill0;
+
+        $fill1 = new Fill();
+        $fill1->setFillType(Fill::FILL_PATTERN_GRAY125);
+        $aFills[] = $fill1;
+        // The remaining fills
+        $aStyles = $this->allStyles($spreadsheet);
+        /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */
+        foreach ($aStyles as $style) {
+            if (!isset($aFills[$style->getFill()->getHashCode()])) {
+                $aFills[$style->getFill()->getHashCode()] = $style->getFill();
+            }
+        }
+
+        return $aFills;
+    }
+
+    /**
+     * Get an array of all fonts.
+     *
+     * @return Font[] All fonts in PhpSpreadsheet
+     */
+    public function allFonts(Spreadsheet $spreadsheet)
+    {
+        // Get an array of unique fonts
+        $aFonts = [];
+        $aStyles = $this->allStyles($spreadsheet);
+
+        /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */
+        foreach ($aStyles as $style) {
+            if (!isset($aFonts[$style->getFont()->getHashCode()])) {
+                $aFonts[$style->getFont()->getHashCode()] = $style->getFont();
+            }
+        }
+
+        return $aFonts;
+    }
+
+    /**
+     * Get an array of all borders.
+     *
+     * @return Borders[] All borders in PhpSpreadsheet
+     */
+    public function allBorders(Spreadsheet $spreadsheet)
+    {
+        // Get an array of unique borders
+        $aBorders = [];
+        $aStyles = $this->allStyles($spreadsheet);
+
+        /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */
+        foreach ($aStyles as $style) {
+            if (!isset($aBorders[$style->getBorders()->getHashCode()])) {
+                $aBorders[$style->getBorders()->getHashCode()] = $style->getBorders();
+            }
+        }
+
+        return $aBorders;
+    }
+
+    /**
+     * Get an array of all number formats.
+     *
+     * @return NumberFormat[] All number formats in PhpSpreadsheet
+     */
+    public function allNumberFormats(Spreadsheet $spreadsheet)
+    {
+        // Get an array of unique number formats
+        $aNumFmts = [];
+        $aStyles = $this->allStyles($spreadsheet);
+
+        /** @var \PhpOffice\PhpSpreadsheet\Style\Style $style */
+        foreach ($aStyles as $style) {
+            if ($style->getNumberFormat()->getBuiltInFormatCode() === false && !isset($aNumFmts[$style->getNumberFormat()->getHashCode()])) {
+                $aNumFmts[$style->getNumberFormat()->getHashCode()] = $style->getNumberFormat();
+            }
+        }
+
+        return $aNumFmts;
+    }
+}

+ 115 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Table.php

@@ -0,0 +1,115 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Worksheet\Table as WorksheetTable;
+
+class Table extends WriterPart
+{
+    /**
+     * Write Table to XML format.
+     *
+     * @param int $tableRef Table ID
+     *
+     * @return string XML Output
+     */
+    public function writeTable(WorksheetTable $table, $tableRef): string
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Table
+        $name = 'Table' . $tableRef;
+        $range = $table->getRange();
+
+        $objWriter->startElement('table');
+        $objWriter->writeAttribute('xml:space', 'preserve');
+        $objWriter->writeAttribute('xmlns', Namespaces::MAIN);
+        $objWriter->writeAttribute('id', (string) $tableRef);
+        $objWriter->writeAttribute('name', $name);
+        $objWriter->writeAttribute('displayName', $table->getName() ?: $name);
+        $objWriter->writeAttribute('ref', $range);
+        $objWriter->writeAttribute('headerRowCount', $table->getShowHeaderRow() ? '1' : '0');
+        $objWriter->writeAttribute('totalsRowCount', $table->getShowTotalsRow() ? '1' : '0');
+
+        // Table Boundaries
+        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($table->getRange());
+
+        // Table Auto Filter
+        if ($table->getShowHeaderRow() && $table->getAllowFilter() === true) {
+            $objWriter->startElement('autoFilter');
+            $objWriter->writeAttribute('ref', $range);
+            $objWriter->endElement();
+            foreach (range($rangeStart[0], $rangeEnd[0]) as $offset => $columnIndex) {
+                $column = $table->getColumnByOffset($offset);
+
+                if (!$column->getShowFilterButton()) {
+                    $objWriter->startElement('filterColumn');
+                    $objWriter->writeAttribute('colId', (string) $offset);
+                    $objWriter->writeAttribute('hiddenButton', '1');
+                    $objWriter->endElement();
+                } else {
+                    $column = $table->getAutoFilter()->getColumnByOffset($offset);
+                    AutoFilter::writeAutoFilterColumn($objWriter, $column, $offset);
+                }
+            }
+        }
+
+        // Table Columns
+        $objWriter->startElement('tableColumns');
+        $objWriter->writeAttribute('count', (string) ($rangeEnd[0] - $rangeStart[0] + 1));
+        foreach (range($rangeStart[0], $rangeEnd[0]) as $offset => $columnIndex) {
+            $worksheet = $table->getWorksheet();
+            if (!$worksheet) {
+                continue;
+            }
+
+            $column = $table->getColumnByOffset($offset);
+            $cell = $worksheet->getCell([$columnIndex, $rangeStart[1]]);
+
+            $objWriter->startElement('tableColumn');
+            $objWriter->writeAttribute('id', (string) ($offset + 1));
+            $objWriter->writeAttribute('name', $table->getShowHeaderRow() ? $cell->getValue() : 'Column' . ($offset + 1));
+
+            if ($table->getShowTotalsRow()) {
+                if ($column->getTotalsRowLabel()) {
+                    $objWriter->writeAttribute('totalsRowLabel', $column->getTotalsRowLabel());
+                }
+                if ($column->getTotalsRowFunction()) {
+                    $objWriter->writeAttribute('totalsRowFunction', $column->getTotalsRowFunction());
+                }
+            }
+            if ($column->getColumnFormula()) {
+                $objWriter->writeElement('calculatedColumnFormula', $column->getColumnFormula());
+            }
+
+            $objWriter->endElement();
+        }
+        $objWriter->endElement();
+
+        // Table Styles
+        $objWriter->startElement('tableStyleInfo');
+        $objWriter->writeAttribute('name', $table->getStyle()->getTheme());
+        $objWriter->writeAttribute('showFirstColumn', $table->getStyle()->getShowFirstColumn() ? '1' : '0');
+        $objWriter->writeAttribute('showLastColumn', $table->getStyle()->getShowLastColumn() ? '1' : '0');
+        $objWriter->writeAttribute('showRowStripes', $table->getStyle()->getShowRowStripes() ? '1' : '0');
+        $objWriter->writeAttribute('showColumnStripes', $table->getStyle()->getShowColumnStripes() ? '1' : '0');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+}

+ 744 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Theme.php

@@ -0,0 +1,744 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Theme as SpreadsheetTheme;
+
+class Theme extends WriterPart
+{
+    /**
+     * Write theme to XML format.
+     *
+     * @return string XML Output
+     */
+    public function writeTheme(Spreadsheet $spreadsheet)
+    {
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+        $theme = $spreadsheet->getTheme();
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // a:theme
+        $objWriter->startElement('a:theme');
+        $objWriter->writeAttribute('xmlns:a', Namespaces::DRAWINGML);
+        $objWriter->writeAttribute('name', 'Office Theme');
+
+        // a:themeElements
+        $objWriter->startElement('a:themeElements');
+
+        // a:clrScheme
+        $objWriter->startElement('a:clrScheme');
+        $objWriter->writeAttribute('name', $theme->getThemeColorName());
+
+        $this->writeColourScheme($objWriter, $theme);
+
+        $objWriter->endElement();
+
+        // a:fontScheme
+        $objWriter->startElement('a:fontScheme');
+        $objWriter->writeAttribute('name', $theme->getThemeFontName());
+
+        // a:majorFont
+        $objWriter->startElement('a:majorFont');
+        $this->writeFonts(
+            $objWriter,
+            $theme->getMajorFontLatin(),
+            $theme->getMajorFontEastAsian(),
+            $theme->getMajorFontComplexScript(),
+            $theme->getMajorFontSubstitutions()
+        );
+        $objWriter->endElement(); // a:majorFont
+
+        // a:minorFont
+        $objWriter->startElement('a:minorFont');
+        $this->writeFonts(
+            $objWriter,
+            $theme->getMinorFontLatin(),
+            $theme->getMinorFontEastAsian(),
+            $theme->getMinorFontComplexScript(),
+            $theme->getMinorFontSubstitutions()
+        );
+        $objWriter->endElement(); // a:minorFont
+
+        $objWriter->endElement(); // a:fontScheme
+
+        // a:fmtScheme
+        $objWriter->startElement('a:fmtScheme');
+        $objWriter->writeAttribute('name', 'Office');
+
+        // a:fillStyleLst
+        $objWriter->startElement('a:fillStyleLst');
+
+        // a:solidFill
+        $objWriter->startElement('a:solidFill');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gradFill
+        $objWriter->startElement('a:gradFill');
+        $objWriter->writeAttribute('rotWithShape', '1');
+
+        // a:gsLst
+        $objWriter->startElement('a:gsLst');
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '0');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:tint
+        $objWriter->startElement('a:tint');
+        $objWriter->writeAttribute('val', '50000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '300000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '35000');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:tint
+        $objWriter->startElement('a:tint');
+        $objWriter->writeAttribute('val', '37000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '300000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '100000');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:tint
+        $objWriter->startElement('a:tint');
+        $objWriter->writeAttribute('val', '15000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '350000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:lin
+        $objWriter->startElement('a:lin');
+        $objWriter->writeAttribute('ang', '16200000');
+        $objWriter->writeAttribute('scaled', '1');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gradFill
+        $objWriter->startElement('a:gradFill');
+        $objWriter->writeAttribute('rotWithShape', '1');
+
+        // a:gsLst
+        $objWriter->startElement('a:gsLst');
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '0');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:shade
+        $objWriter->startElement('a:shade');
+        $objWriter->writeAttribute('val', '51000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '130000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '80000');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:shade
+        $objWriter->startElement('a:shade');
+        $objWriter->writeAttribute('val', '93000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '130000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '100000');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:shade
+        $objWriter->startElement('a:shade');
+        $objWriter->writeAttribute('val', '94000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '135000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:lin
+        $objWriter->startElement('a:lin');
+        $objWriter->writeAttribute('ang', '16200000');
+        $objWriter->writeAttribute('scaled', '0');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:lnStyleLst
+        $objWriter->startElement('a:lnStyleLst');
+
+        // a:ln
+        $objWriter->startElement('a:ln');
+        $objWriter->writeAttribute('w', '9525');
+        $objWriter->writeAttribute('cap', 'flat');
+        $objWriter->writeAttribute('cmpd', 'sng');
+        $objWriter->writeAttribute('algn', 'ctr');
+
+        // a:solidFill
+        $objWriter->startElement('a:solidFill');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:shade
+        $objWriter->startElement('a:shade');
+        $objWriter->writeAttribute('val', '95000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '105000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:prstDash
+        $objWriter->startElement('a:prstDash');
+        $objWriter->writeAttribute('val', 'solid');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:ln
+        $objWriter->startElement('a:ln');
+        $objWriter->writeAttribute('w', '25400');
+        $objWriter->writeAttribute('cap', 'flat');
+        $objWriter->writeAttribute('cmpd', 'sng');
+        $objWriter->writeAttribute('algn', 'ctr');
+
+        // a:solidFill
+        $objWriter->startElement('a:solidFill');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:prstDash
+        $objWriter->startElement('a:prstDash');
+        $objWriter->writeAttribute('val', 'solid');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:ln
+        $objWriter->startElement('a:ln');
+        $objWriter->writeAttribute('w', '38100');
+        $objWriter->writeAttribute('cap', 'flat');
+        $objWriter->writeAttribute('cmpd', 'sng');
+        $objWriter->writeAttribute('algn', 'ctr');
+
+        // a:solidFill
+        $objWriter->startElement('a:solidFill');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:prstDash
+        $objWriter->startElement('a:prstDash');
+        $objWriter->writeAttribute('val', 'solid');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:effectStyleLst
+        $objWriter->startElement('a:effectStyleLst');
+
+        // a:effectStyle
+        $objWriter->startElement('a:effectStyle');
+
+        // a:effectLst
+        $objWriter->startElement('a:effectLst');
+
+        // a:outerShdw
+        $objWriter->startElement('a:outerShdw');
+        $objWriter->writeAttribute('blurRad', '40000');
+        $objWriter->writeAttribute('dist', '20000');
+        $objWriter->writeAttribute('dir', '5400000');
+        $objWriter->writeAttribute('rotWithShape', '0');
+
+        // a:srgbClr
+        $objWriter->startElement('a:srgbClr');
+        $objWriter->writeAttribute('val', '000000');
+
+        // a:alpha
+        $objWriter->startElement('a:alpha');
+        $objWriter->writeAttribute('val', '38000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:effectStyle
+        $objWriter->startElement('a:effectStyle');
+
+        // a:effectLst
+        $objWriter->startElement('a:effectLst');
+
+        // a:outerShdw
+        $objWriter->startElement('a:outerShdw');
+        $objWriter->writeAttribute('blurRad', '40000');
+        $objWriter->writeAttribute('dist', '23000');
+        $objWriter->writeAttribute('dir', '5400000');
+        $objWriter->writeAttribute('rotWithShape', '0');
+
+        // a:srgbClr
+        $objWriter->startElement('a:srgbClr');
+        $objWriter->writeAttribute('val', '000000');
+
+        // a:alpha
+        $objWriter->startElement('a:alpha');
+        $objWriter->writeAttribute('val', '35000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:effectStyle
+        $objWriter->startElement('a:effectStyle');
+
+        // a:effectLst
+        $objWriter->startElement('a:effectLst');
+
+        // a:outerShdw
+        $objWriter->startElement('a:outerShdw');
+        $objWriter->writeAttribute('blurRad', '40000');
+        $objWriter->writeAttribute('dist', '23000');
+        $objWriter->writeAttribute('dir', '5400000');
+        $objWriter->writeAttribute('rotWithShape', '0');
+
+        // a:srgbClr
+        $objWriter->startElement('a:srgbClr');
+        $objWriter->writeAttribute('val', '000000');
+
+        // a:alpha
+        $objWriter->startElement('a:alpha');
+        $objWriter->writeAttribute('val', '35000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:scene3d
+        $objWriter->startElement('a:scene3d');
+
+        // a:camera
+        $objWriter->startElement('a:camera');
+        $objWriter->writeAttribute('prst', 'orthographicFront');
+
+        // a:rot
+        $objWriter->startElement('a:rot');
+        $objWriter->writeAttribute('lat', '0');
+        $objWriter->writeAttribute('lon', '0');
+        $objWriter->writeAttribute('rev', '0');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:lightRig
+        $objWriter->startElement('a:lightRig');
+        $objWriter->writeAttribute('rig', 'threePt');
+        $objWriter->writeAttribute('dir', 't');
+
+        // a:rot
+        $objWriter->startElement('a:rot');
+        $objWriter->writeAttribute('lat', '0');
+        $objWriter->writeAttribute('lon', '0');
+        $objWriter->writeAttribute('rev', '1200000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:sp3d
+        $objWriter->startElement('a:sp3d');
+
+        // a:bevelT
+        $objWriter->startElement('a:bevelT');
+        $objWriter->writeAttribute('w', '63500');
+        $objWriter->writeAttribute('h', '25400');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:bgFillStyleLst
+        $objWriter->startElement('a:bgFillStyleLst');
+
+        // a:solidFill
+        $objWriter->startElement('a:solidFill');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gradFill
+        $objWriter->startElement('a:gradFill');
+        $objWriter->writeAttribute('rotWithShape', '1');
+
+        // a:gsLst
+        $objWriter->startElement('a:gsLst');
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '0');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:tint
+        $objWriter->startElement('a:tint');
+        $objWriter->writeAttribute('val', '40000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '350000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '40000');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:tint
+        $objWriter->startElement('a:tint');
+        $objWriter->writeAttribute('val', '45000');
+        $objWriter->endElement();
+
+        // a:shade
+        $objWriter->startElement('a:shade');
+        $objWriter->writeAttribute('val', '99000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '350000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '100000');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:shade
+        $objWriter->startElement('a:shade');
+        $objWriter->writeAttribute('val', '20000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '255000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:path
+        $objWriter->startElement('a:path');
+        $objWriter->writeAttribute('path', 'circle');
+
+        // a:fillToRect
+        $objWriter->startElement('a:fillToRect');
+        $objWriter->writeAttribute('l', '50000');
+        $objWriter->writeAttribute('t', '-80000');
+        $objWriter->writeAttribute('r', '50000');
+        $objWriter->writeAttribute('b', '180000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gradFill
+        $objWriter->startElement('a:gradFill');
+        $objWriter->writeAttribute('rotWithShape', '1');
+
+        // a:gsLst
+        $objWriter->startElement('a:gsLst');
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '0');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:tint
+        $objWriter->startElement('a:tint');
+        $objWriter->writeAttribute('val', '80000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '300000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:gs
+        $objWriter->startElement('a:gs');
+        $objWriter->writeAttribute('pos', '100000');
+
+        // a:schemeClr
+        $objWriter->startElement('a:schemeClr');
+        $objWriter->writeAttribute('val', 'phClr');
+
+        // a:shade
+        $objWriter->startElement('a:shade');
+        $objWriter->writeAttribute('val', '30000');
+        $objWriter->endElement();
+
+        // a:satMod
+        $objWriter->startElement('a:satMod');
+        $objWriter->writeAttribute('val', '200000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:path
+        $objWriter->startElement('a:path');
+        $objWriter->writeAttribute('path', 'circle');
+
+        // a:fillToRect
+        $objWriter->startElement('a:fillToRect');
+        $objWriter->writeAttribute('l', '50000');
+        $objWriter->writeAttribute('t', '50000');
+        $objWriter->writeAttribute('r', '50000');
+        $objWriter->writeAttribute('b', '50000');
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        // a:objectDefaults
+        $objWriter->writeElement('a:objectDefaults', null);
+
+        // a:extraClrSchemeLst
+        $objWriter->writeElement('a:extraClrSchemeLst', null);
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write fonts to XML format.
+     *
+     * @param string[] $fontSet
+     */
+    private function writeFonts(XMLWriter $objWriter, string $latinFont, string $eastAsianFont, string $complexScriptFont, array $fontSet): void
+    {
+        // a:latin
+        $objWriter->startElement('a:latin');
+        $objWriter->writeAttribute('typeface', $latinFont);
+        $objWriter->endElement();
+
+        // a:ea
+        $objWriter->startElement('a:ea');
+        $objWriter->writeAttribute('typeface', $eastAsianFont);
+        $objWriter->endElement();
+
+        // a:cs
+        $objWriter->startElement('a:cs');
+        $objWriter->writeAttribute('typeface', $complexScriptFont);
+        $objWriter->endElement();
+
+        foreach ($fontSet as $fontScript => $typeface) {
+            $objWriter->startElement('a:font');
+            $objWriter->writeAttribute('script', $fontScript);
+            $objWriter->writeAttribute('typeface', $typeface);
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write colour scheme to XML format.
+     */
+    private function writeColourScheme(XMLWriter $objWriter, SpreadsheetTheme $theme): void
+    {
+        $themeArray = $theme->getThemeColors();
+        // a:dk1
+        $objWriter->startElement('a:dk1');
+        $objWriter->startElement('a:sysClr');
+        $objWriter->writeAttribute('val', 'windowText');
+        $objWriter->writeAttribute('lastClr', $themeArray['dk1'] ?? '000000');
+        $objWriter->endElement(); // a:sysClr
+        $objWriter->endElement(); // a:dk1
+
+        // a:lt1
+        $objWriter->startElement('a:lt1');
+        $objWriter->startElement('a:sysClr');
+        $objWriter->writeAttribute('val', 'window');
+        $objWriter->writeAttribute('lastClr', $themeArray['lt1'] ?? 'FFFFFF');
+        $objWriter->endElement(); // a:sysClr
+        $objWriter->endElement(); // a:lt1
+
+        foreach ($themeArray as $colourName => $colourValue) {
+            if ($colourName !== 'dk1' && $colourName !== 'lt1') {
+                $objWriter->startElement('a:' . $colourName);
+                $objWriter->startElement('a:srgbClr');
+                $objWriter->writeAttribute('val', $colourValue);
+                $objWriter->endElement(); // a:srgbClr
+                $objWriter->endElement(); // a:$colourName
+            }
+        }
+    }
+}

+ 214 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php

@@ -0,0 +1,214 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\Shared\Date;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx\DefinedNames as DefinedNamesWriter;
+
+class Workbook extends WriterPart
+{
+    /**
+     * Write workbook to XML format.
+     *
+     * @param bool $recalcRequired Indicate whether formulas should be recalculated before writing
+     *
+     * @return string XML Output
+     */
+    public function writeWorkbook(Spreadsheet $spreadsheet, $recalcRequired = false)
+    {
+        // Create XML writer
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // workbook
+        $objWriter->startElement('workbook');
+        $objWriter->writeAttribute('xml:space', 'preserve');
+        $objWriter->writeAttribute('xmlns', Namespaces::MAIN);
+        $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
+
+        // fileVersion
+        $this->writeFileVersion($objWriter);
+
+        // workbookPr
+        $this->writeWorkbookPr($objWriter);
+
+        // workbookProtection
+        $this->writeWorkbookProtection($objWriter, $spreadsheet);
+
+        // bookViews
+        if ($this->getParentWriter()->getOffice2003Compatibility() === false) {
+            $this->writeBookViews($objWriter, $spreadsheet);
+        }
+
+        // sheets
+        $this->writeSheets($objWriter, $spreadsheet);
+
+        // definedNames
+        (new DefinedNamesWriter($objWriter, $spreadsheet))->write();
+
+        // calcPr
+        $this->writeCalcPr($objWriter, $recalcRequired);
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    /**
+     * Write file version.
+     */
+    private function writeFileVersion(XMLWriter $objWriter): void
+    {
+        $objWriter->startElement('fileVersion');
+        $objWriter->writeAttribute('appName', 'xl');
+        $objWriter->writeAttribute('lastEdited', '4');
+        $objWriter->writeAttribute('lowestEdited', '4');
+        $objWriter->writeAttribute('rupBuild', '4505');
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write WorkbookPr.
+     */
+    private function writeWorkbookPr(XMLWriter $objWriter): void
+    {
+        $objWriter->startElement('workbookPr');
+
+        if (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904) {
+            $objWriter->writeAttribute('date1904', '1');
+        }
+
+        $objWriter->writeAttribute('codeName', 'ThisWorkbook');
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write BookViews.
+     */
+    private function writeBookViews(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
+    {
+        // bookViews
+        $objWriter->startElement('bookViews');
+
+        // workbookView
+        $objWriter->startElement('workbookView');
+
+        $objWriter->writeAttribute('activeTab', (string) $spreadsheet->getActiveSheetIndex());
+        $objWriter->writeAttribute('autoFilterDateGrouping', ($spreadsheet->getAutoFilterDateGrouping() ? 'true' : 'false'));
+        $objWriter->writeAttribute('firstSheet', (string) $spreadsheet->getFirstSheetIndex());
+        $objWriter->writeAttribute('minimized', ($spreadsheet->getMinimized() ? 'true' : 'false'));
+        $objWriter->writeAttribute('showHorizontalScroll', ($spreadsheet->getShowHorizontalScroll() ? 'true' : 'false'));
+        $objWriter->writeAttribute('showSheetTabs', ($spreadsheet->getShowSheetTabs() ? 'true' : 'false'));
+        $objWriter->writeAttribute('showVerticalScroll', ($spreadsheet->getShowVerticalScroll() ? 'true' : 'false'));
+        $objWriter->writeAttribute('tabRatio', (string) $spreadsheet->getTabRatio());
+        $objWriter->writeAttribute('visibility', $spreadsheet->getVisibility());
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write WorkbookProtection.
+     */
+    private function writeWorkbookProtection(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
+    {
+        if ($spreadsheet->getSecurity()->isSecurityEnabled()) {
+            $objWriter->startElement('workbookProtection');
+            $objWriter->writeAttribute('lockRevision', ($spreadsheet->getSecurity()->getLockRevision() ? 'true' : 'false'));
+            $objWriter->writeAttribute('lockStructure', ($spreadsheet->getSecurity()->getLockStructure() ? 'true' : 'false'));
+            $objWriter->writeAttribute('lockWindows', ($spreadsheet->getSecurity()->getLockWindows() ? 'true' : 'false'));
+
+            if ($spreadsheet->getSecurity()->getRevisionsPassword() != '') {
+                $objWriter->writeAttribute('revisionsPassword', $spreadsheet->getSecurity()->getRevisionsPassword());
+            }
+
+            if ($spreadsheet->getSecurity()->getWorkbookPassword() != '') {
+                $objWriter->writeAttribute('workbookPassword', $spreadsheet->getSecurity()->getWorkbookPassword());
+            }
+
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write calcPr.
+     *
+     * @param bool $recalcRequired Indicate whether formulas should be recalculated before writing
+     */
+    private function writeCalcPr(XMLWriter $objWriter, $recalcRequired = true): void
+    {
+        $objWriter->startElement('calcPr');
+
+        //    Set the calcid to a higher value than Excel itself will use, otherwise Excel will always recalc
+        //  If MS Excel does do a recalc, then users opening a file in MS Excel will be prompted to save on exit
+        //     because the file has changed
+        $objWriter->writeAttribute('calcId', '999999');
+        $objWriter->writeAttribute('calcMode', 'auto');
+        //    fullCalcOnLoad isn't needed if we've recalculating for the save
+        $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? '1' : '0');
+        $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? '0' : '1');
+        $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? '0' : '1');
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write sheets.
+     */
+    private function writeSheets(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
+    {
+        // Write sheets
+        $objWriter->startElement('sheets');
+        $sheetCount = $spreadsheet->getSheetCount();
+        for ($i = 0; $i < $sheetCount; ++$i) {
+            // sheet
+            $this->writeSheet(
+                $objWriter,
+                $spreadsheet->getSheet($i)->getTitle(),
+                ($i + 1),
+                ($i + 1 + 3),
+                $spreadsheet->getSheet($i)->getSheetState()
+            );
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write sheet.
+     *
+     * @param string $worksheetName Sheet name
+     * @param int $worksheetId Sheet id
+     * @param int $relId Relationship ID
+     * @param string $sheetState Sheet state (visible, hidden, veryHidden)
+     */
+    private function writeSheet(XMLWriter $objWriter, $worksheetName, $worksheetId = 1, $relId = 1, $sheetState = 'visible'): void
+    {
+        if ($worksheetName != '') {
+            // Write sheet
+            $objWriter->startElement('sheet');
+            $objWriter->writeAttribute('name', $worksheetName);
+            $objWriter->writeAttribute('sheetId', (string) $worksheetId);
+            if ($sheetState !== 'visible' && $sheetState != '') {
+                $objWriter->writeAttribute('state', $sheetState);
+            }
+            $objWriter->writeAttribute('r:id', 'rId' . $relId);
+            $objWriter->endElement();
+        } else {
+            throw new WriterException('Invalid parameters passed.');
+        }
+    }
+}

+ 1462 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

@@ -0,0 +1,1462 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
+use PhpOffice\PhpSpreadsheet\Cell\Cell;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
+use PhpOffice\PhpSpreadsheet\RichText\RichText;
+use PhpOffice\PhpSpreadsheet\Settings;
+use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
+use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
+use PhpOffice\PhpSpreadsheet\Style\Conditional;
+use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
+use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension;
+use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as PhpspreadsheetWorksheet;
+
+class Worksheet extends WriterPart
+{
+    /** @var string */
+    private $numberStoredAsText = '';
+
+    /** @var string */
+    private $formula = '';
+
+    /** @var string */
+    private $twoDigitTextYear = '';
+
+    /** @var string */
+    private $evalError = '';
+
+    /**
+     * Write worksheet to XML format.
+     *
+     * @param string[] $stringTable
+     * @param bool $includeCharts Flag indicating if we should write charts
+     *
+     * @return string XML Output
+     */
+    public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = [], $includeCharts = false)
+    {
+        $this->numberStoredAsText = '';
+        $this->formula = '';
+        $this->twoDigitTextYear = '';
+        $this->evalError = '';
+        // Create XML writer
+        $objWriter = null;
+        if ($this->getParentWriter()->getUseDiskCaching()) {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
+        } else {
+            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
+        }
+
+        // XML header
+        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
+
+        // Worksheet
+        $objWriter->startElement('worksheet');
+        $objWriter->writeAttribute('xml:space', 'preserve');
+        $objWriter->writeAttribute('xmlns', Namespaces::MAIN);
+        $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
+
+        $objWriter->writeAttribute('xmlns:xdr', Namespaces::SPREADSHEET_DRAWING);
+        $objWriter->writeAttribute('xmlns:x14', Namespaces::DATA_VALIDATIONS1);
+        $objWriter->writeAttribute('xmlns:xm', Namespaces::DATA_VALIDATIONS2);
+        $objWriter->writeAttribute('xmlns:mc', Namespaces::COMPATIBILITY);
+        $objWriter->writeAttribute('mc:Ignorable', 'x14ac');
+        $objWriter->writeAttribute('xmlns:x14ac', Namespaces::SPREADSHEETML_AC);
+
+        // sheetPr
+        $this->writeSheetPr($objWriter, $worksheet);
+
+        // Dimension
+        $this->writeDimension($objWriter, $worksheet);
+
+        // sheetViews
+        $this->writeSheetViews($objWriter, $worksheet);
+
+        // sheetFormatPr
+        $this->writeSheetFormatPr($objWriter, $worksheet);
+
+        // cols
+        $this->writeCols($objWriter, $worksheet);
+
+        // sheetData
+        $this->writeSheetData($objWriter, $worksheet, $stringTable);
+
+        // sheetProtection
+        $this->writeSheetProtection($objWriter, $worksheet);
+
+        // protectedRanges
+        $this->writeProtectedRanges($objWriter, $worksheet);
+
+        // autoFilter
+        $this->writeAutoFilter($objWriter, $worksheet);
+
+        // mergeCells
+        $this->writeMergeCells($objWriter, $worksheet);
+
+        // conditionalFormatting
+        $this->writeConditionalFormatting($objWriter, $worksheet);
+
+        // dataValidations
+        $this->writeDataValidations($objWriter, $worksheet);
+
+        // hyperlinks
+        $this->writeHyperlinks($objWriter, $worksheet);
+
+        // Print options
+        $this->writePrintOptions($objWriter, $worksheet);
+
+        // Page margins
+        $this->writePageMargins($objWriter, $worksheet);
+
+        // Page setup
+        $this->writePageSetup($objWriter, $worksheet);
+
+        // Header / footer
+        $this->writeHeaderFooter($objWriter, $worksheet);
+
+        // Breaks
+        $this->writeBreaks($objWriter, $worksheet);
+
+        // IgnoredErrors
+        $this->writeIgnoredErrors($objWriter);
+
+        // Drawings and/or Charts
+        $this->writeDrawings($objWriter, $worksheet, $includeCharts);
+
+        // LegacyDrawing
+        $this->writeLegacyDrawing($objWriter, $worksheet);
+
+        // LegacyDrawingHF
+        $this->writeLegacyDrawingHF($objWriter, $worksheet);
+
+        // AlternateContent
+        $this->writeAlternateContent($objWriter, $worksheet);
+
+        // Table
+        $this->writeTable($objWriter, $worksheet);
+
+        // ConditionalFormattingRuleExtensionList
+        // (Must be inserted last. Not insert last, an Excel parse error will occur)
+        $this->writeExtLst($objWriter, $worksheet);
+
+        $objWriter->endElement();
+
+        // Return
+        return $objWriter->getData();
+    }
+
+    private function writeIgnoredError(XMLWriter $objWriter, bool &$started, string $attr, string $cells): void
+    {
+        if ($cells !== '') {
+            if (!$started) {
+                $objWriter->startElement('ignoredErrors');
+                $started = true;
+            }
+            $objWriter->startElement('ignoredError');
+            $objWriter->writeAttribute('sqref', substr($cells, 1));
+            $objWriter->writeAttribute($attr, '1');
+            $objWriter->endElement();
+        }
+    }
+
+    private function writeIgnoredErrors(XMLWriter $objWriter): void
+    {
+        $started = false;
+        $this->writeIgnoredError($objWriter, $started, 'numberStoredAsText', $this->numberStoredAsText);
+        $this->writeIgnoredError($objWriter, $started, 'formula', $this->formula);
+        $this->writeIgnoredError($objWriter, $started, 'twoDigitTextYear', $this->twoDigitTextYear);
+        $this->writeIgnoredError($objWriter, $started, 'evalError', $this->evalError);
+        if ($started) {
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write SheetPr.
+     */
+    private function writeSheetPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // sheetPr
+        $objWriter->startElement('sheetPr');
+        if ($worksheet->getParentOrThrow()->hasMacros()) {
+            //if the workbook have macros, we need to have codeName for the sheet
+            if (!$worksheet->hasCodeName()) {
+                $worksheet->setCodeName($worksheet->getTitle());
+            }
+            self::writeAttributeNotNull($objWriter, 'codeName', $worksheet->getCodeName());
+        }
+        $autoFilterRange = $worksheet->getAutoFilter()->getRange();
+        if (!empty($autoFilterRange)) {
+            $objWriter->writeAttribute('filterMode', '1');
+            if (!$worksheet->getAutoFilter()->getEvaluated()) {
+                $worksheet->getAutoFilter()->showHideRows();
+            }
+        }
+        $tables = $worksheet->getTableCollection();
+        if (count($tables)) {
+            foreach ($tables as $table) {
+                if (!$table->getAutoFilter()->getEvaluated()) {
+                    $table->getAutoFilter()->showHideRows();
+                }
+            }
+        }
+
+        // tabColor
+        if ($worksheet->isTabColorSet()) {
+            $objWriter->startElement('tabColor');
+            $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB() ?? '');
+            $objWriter->endElement();
+        }
+
+        // outlinePr
+        $objWriter->startElement('outlinePr');
+        $objWriter->writeAttribute('summaryBelow', ($worksheet->getShowSummaryBelow() ? '1' : '0'));
+        $objWriter->writeAttribute('summaryRight', ($worksheet->getShowSummaryRight() ? '1' : '0'));
+        $objWriter->endElement();
+
+        // pageSetUpPr
+        if ($worksheet->getPageSetup()->getFitToPage()) {
+            $objWriter->startElement('pageSetUpPr');
+            $objWriter->writeAttribute('fitToPage', '1');
+            $objWriter->endElement();
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Dimension.
+     */
+    private function writeDimension(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // dimension
+        $objWriter->startElement('dimension');
+        $objWriter->writeAttribute('ref', $worksheet->calculateWorksheetDimension());
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write SheetViews.
+     */
+    private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // sheetViews
+        $objWriter->startElement('sheetViews');
+
+        // Sheet selected?
+        $sheetSelected = false;
+        if ($this->getParentWriter()->getSpreadsheet()->getIndex($worksheet) == $this->getParentWriter()->getSpreadsheet()->getActiveSheetIndex()) {
+            $sheetSelected = true;
+        }
+
+        // sheetView
+        $objWriter->startElement('sheetView');
+        $objWriter->writeAttribute('tabSelected', $sheetSelected ? '1' : '0');
+        $objWriter->writeAttribute('workbookViewId', '0');
+
+        // Zoom scales
+        if ($worksheet->getSheetView()->getZoomScale() != 100) {
+            $objWriter->writeAttribute('zoomScale', (string) $worksheet->getSheetView()->getZoomScale());
+        }
+        if ($worksheet->getSheetView()->getZoomScaleNormal() != 100) {
+            $objWriter->writeAttribute('zoomScaleNormal', (string) $worksheet->getSheetView()->getZoomScaleNormal());
+        }
+
+        // Show zeros (Excel also writes this attribute only if set to false)
+        if ($worksheet->getSheetView()->getShowZeros() === false) {
+            $objWriter->writeAttribute('showZeros', '0');
+        }
+
+        // View Layout Type
+        if ($worksheet->getSheetView()->getView() !== SheetView::SHEETVIEW_NORMAL) {
+            $objWriter->writeAttribute('view', $worksheet->getSheetView()->getView());
+        }
+
+        // Gridlines
+        if ($worksheet->getShowGridlines()) {
+            $objWriter->writeAttribute('showGridLines', 'true');
+        } else {
+            $objWriter->writeAttribute('showGridLines', 'false');
+        }
+
+        // Row and column headers
+        if ($worksheet->getShowRowColHeaders()) {
+            $objWriter->writeAttribute('showRowColHeaders', '1');
+        } else {
+            $objWriter->writeAttribute('showRowColHeaders', '0');
+        }
+
+        // Right-to-left
+        if ($worksheet->getRightToLeft()) {
+            $objWriter->writeAttribute('rightToLeft', 'true');
+        }
+
+        $topLeftCell = $worksheet->getTopLeftCell();
+        $activeCell = $worksheet->getActiveCell();
+        $sqref = $worksheet->getSelectedCells();
+
+        // Pane
+        $pane = '';
+        if ($worksheet->getFreezePane()) {
+            [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane());
+            $xSplit = Coordinate::columnIndexFromString($xSplit);
+            --$xSplit;
+            --$ySplit;
+
+            // pane
+            $pane = 'topRight';
+            $objWriter->startElement('pane');
+            if ($xSplit > 0) {
+                $objWriter->writeAttribute('xSplit', "$xSplit");
+            }
+            if ($ySplit > 0) {
+                $objWriter->writeAttribute('ySplit', $ySplit);
+                $pane = ($xSplit > 0) ? 'bottomRight' : 'bottomLeft';
+            }
+            self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell);
+            $objWriter->writeAttribute('activePane', $pane);
+            $objWriter->writeAttribute('state', 'frozen');
+            $objWriter->endElement();
+
+            if (($xSplit > 0) && ($ySplit > 0)) {
+                //    Write additional selections if more than two panes (ie both an X and a Y split)
+                $objWriter->startElement('selection');
+                $objWriter->writeAttribute('pane', 'topRight');
+                $objWriter->endElement();
+                $objWriter->startElement('selection');
+                $objWriter->writeAttribute('pane', 'bottomLeft');
+                $objWriter->endElement();
+            }
+        } else {
+            self::writeAttributeNotNull($objWriter, 'topLeftCell', $topLeftCell);
+        }
+
+        // Selection
+        // Only need to write selection element if we have a split pane
+        // We cheat a little by over-riding the active cell selection, setting it to the split cell
+        $objWriter->startElement('selection');
+        if ($pane != '') {
+            $objWriter->writeAttribute('pane', $pane);
+        }
+        $objWriter->writeAttribute('activeCell', $activeCell);
+        $objWriter->writeAttribute('sqref', $sqref);
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write SheetFormatPr.
+     */
+    private function writeSheetFormatPr(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // sheetFormatPr
+        $objWriter->startElement('sheetFormatPr');
+
+        // Default row height
+        if ($worksheet->getDefaultRowDimension()->getRowHeight() >= 0) {
+            $objWriter->writeAttribute('customHeight', 'true');
+            $objWriter->writeAttribute('defaultRowHeight', StringHelper::formatNumber($worksheet->getDefaultRowDimension()->getRowHeight()));
+        } else {
+            $objWriter->writeAttribute('defaultRowHeight', '14.4');
+        }
+
+        // Set Zero Height row
+        if ($worksheet->getDefaultRowDimension()->getZeroHeight()) {
+            $objWriter->writeAttribute('zeroHeight', '1');
+        }
+
+        // Default column width
+        if ($worksheet->getDefaultColumnDimension()->getWidth() >= 0) {
+            $objWriter->writeAttribute('defaultColWidth', StringHelper::formatNumber($worksheet->getDefaultColumnDimension()->getWidth()));
+        }
+
+        // Outline level - row
+        $outlineLevelRow = 0;
+        foreach ($worksheet->getRowDimensions() as $dimension) {
+            if ($dimension->getOutlineLevel() > $outlineLevelRow) {
+                $outlineLevelRow = $dimension->getOutlineLevel();
+            }
+        }
+        $objWriter->writeAttribute('outlineLevelRow', (string) (int) $outlineLevelRow);
+
+        // Outline level - column
+        $outlineLevelCol = 0;
+        foreach ($worksheet->getColumnDimensions() as $dimension) {
+            if ($dimension->getOutlineLevel() > $outlineLevelCol) {
+                $outlineLevelCol = $dimension->getOutlineLevel();
+            }
+        }
+        $objWriter->writeAttribute('outlineLevelCol', (string) (int) $outlineLevelCol);
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Cols.
+     */
+    private function writeCols(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // cols
+        if (count($worksheet->getColumnDimensions()) > 0) {
+            $objWriter->startElement('cols');
+
+            $worksheet->calculateColumnWidths();
+
+            // Loop through column dimensions
+            foreach ($worksheet->getColumnDimensions() as $colDimension) {
+                // col
+                $objWriter->startElement('col');
+                $objWriter->writeAttribute('min', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex()));
+                $objWriter->writeAttribute('max', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex()));
+
+                if ($colDimension->getWidth() < 0) {
+                    // No width set, apply default of 10
+                    $objWriter->writeAttribute('width', '9.10');
+                } else {
+                    // Width set
+                    $objWriter->writeAttribute('width', StringHelper::formatNumber($colDimension->getWidth()));
+                }
+
+                // Column visibility
+                if ($colDimension->getVisible() === false) {
+                    $objWriter->writeAttribute('hidden', 'true');
+                }
+
+                // Auto size?
+                if ($colDimension->getAutoSize()) {
+                    $objWriter->writeAttribute('bestFit', 'true');
+                }
+
+                // Custom width?
+                if ($colDimension->getWidth() != $worksheet->getDefaultColumnDimension()->getWidth()) {
+                    $objWriter->writeAttribute('customWidth', 'true');
+                }
+
+                // Collapsed
+                if ($colDimension->getCollapsed() === true) {
+                    $objWriter->writeAttribute('collapsed', 'true');
+                }
+
+                // Outline level
+                if ($colDimension->getOutlineLevel() > 0) {
+                    $objWriter->writeAttribute('outlineLevel', (string) $colDimension->getOutlineLevel());
+                }
+
+                // Style
+                $objWriter->writeAttribute('style', (string) $colDimension->getXfIndex());
+
+                $objWriter->endElement();
+            }
+
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write SheetProtection.
+     */
+    private function writeSheetProtection(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        $protection = $worksheet->getProtection();
+        if (!$protection->isProtectionEnabled()) {
+            return;
+        }
+        // sheetProtection
+        $objWriter->startElement('sheetProtection');
+
+        if ($protection->getAlgorithm()) {
+            $objWriter->writeAttribute('algorithmName', $protection->getAlgorithm());
+            $objWriter->writeAttribute('hashValue', $protection->getPassword());
+            $objWriter->writeAttribute('saltValue', $protection->getSalt());
+            $objWriter->writeAttribute('spinCount', (string) $protection->getSpinCount());
+        } elseif ($protection->getPassword() !== '') {
+            $objWriter->writeAttribute('password', $protection->getPassword());
+        }
+
+        self::writeProtectionAttribute($objWriter, 'sheet', $protection->getSheet());
+        self::writeProtectionAttribute($objWriter, 'objects', $protection->getObjects());
+        self::writeProtectionAttribute($objWriter, 'scenarios', $protection->getScenarios());
+        self::writeProtectionAttribute($objWriter, 'formatCells', $protection->getFormatCells());
+        self::writeProtectionAttribute($objWriter, 'formatColumns', $protection->getFormatColumns());
+        self::writeProtectionAttribute($objWriter, 'formatRows', $protection->getFormatRows());
+        self::writeProtectionAttribute($objWriter, 'insertColumns', $protection->getInsertColumns());
+        self::writeProtectionAttribute($objWriter, 'insertRows', $protection->getInsertRows());
+        self::writeProtectionAttribute($objWriter, 'insertHyperlinks', $protection->getInsertHyperlinks());
+        self::writeProtectionAttribute($objWriter, 'deleteColumns', $protection->getDeleteColumns());
+        self::writeProtectionAttribute($objWriter, 'deleteRows', $protection->getDeleteRows());
+        self::writeProtectionAttribute($objWriter, 'sort', $protection->getSort());
+        self::writeProtectionAttribute($objWriter, 'autoFilter', $protection->getAutoFilter());
+        self::writeProtectionAttribute($objWriter, 'pivotTables', $protection->getPivotTables());
+        self::writeProtectionAttribute($objWriter, 'selectLockedCells', $protection->getSelectLockedCells());
+        self::writeProtectionAttribute($objWriter, 'selectUnlockedCells', $protection->getSelectUnlockedCells());
+        $objWriter->endElement();
+    }
+
+    private static function writeProtectionAttribute(XMLWriter $objWriter, string $name, ?bool $value): void
+    {
+        if ($value === true) {
+            $objWriter->writeAttribute($name, '1');
+        } elseif ($value === false) {
+            $objWriter->writeAttribute($name, '0');
+        }
+    }
+
+    private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition, string $attr, string $val): void
+    {
+        if ($condition) {
+            $objWriter->writeAttribute($attr, $val);
+        }
+    }
+
+    private static function writeAttributeNotNull(XMLWriter $objWriter, string $attr, ?string $val): void
+    {
+        if ($val !== null) {
+            $objWriter->writeAttribute($attr, $val);
+        }
+    }
+
+    private static function writeElementIf(XMLWriter $objWriter, bool $condition, string $attr, string $val): void
+    {
+        if ($condition) {
+            $objWriter->writeElement($attr, $val);
+        }
+    }
+
+    private static function writeOtherCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
+    {
+        $conditions = $conditional->getConditions();
+        if (
+            $conditional->getConditionType() == Conditional::CONDITION_CELLIS
+            || $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION
+            || !empty($conditions)
+        ) {
+            foreach ($conditions as $formula) {
+                // Formula
+                if (is_bool($formula)) {
+                    $formula = $formula ? 'TRUE' : 'FALSE';
+                }
+                $objWriter->writeElement('formula', FunctionPrefix::addFunctionPrefix("$formula"));
+            }
+        } else {
+            if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) {
+                // formula copied from ms xlsx xml source file
+                $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))=0');
+            } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSBLANKS) {
+                // formula copied from ms xlsx xml source file
+                $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))>0');
+            } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSERRORS) {
+                // formula copied from ms xlsx xml source file
+                $objWriter->writeElement('formula', 'ISERROR(' . $cellCoordinate . ')');
+            } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSERRORS) {
+                // formula copied from ms xlsx xml source file
+                $objWriter->writeElement('formula', 'NOT(ISERROR(' . $cellCoordinate . '))');
+            }
+        }
+    }
+
+    private static function writeTimePeriodCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
+    {
+        $txt = $conditional->getText();
+        if (!empty($txt)) {
+            $objWriter->writeAttribute('timePeriod', $txt);
+            if (empty($conditional->getConditions())) {
+                if ($conditional->getOperatorType() == Conditional::TIMEPERIOD_TODAY) {
+                    $objWriter->writeElement('formula', 'FLOOR(' . $cellCoordinate . ')=TODAY()');
+                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_TOMORROW) {
+                    $objWriter->writeElement('formula', 'FLOOR(' . $cellCoordinate . ')=TODAY()+1');
+                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_YESTERDAY) {
+                    $objWriter->writeElement('formula', 'FLOOR(' . $cellCoordinate . ')=TODAY()-1');
+                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_LAST_7_DAYS) {
+                    $objWriter->writeElement('formula', 'AND(TODAY()-FLOOR(' . $cellCoordinate . ',1)<=6,FLOOR(' . $cellCoordinate . ',1)<=TODAY())');
+                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_LAST_WEEK) {
+                    $objWriter->writeElement('formula', 'AND(TODAY()-ROUNDDOWN(' . $cellCoordinate . ',0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(' . $cellCoordinate . ',0)<(WEEKDAY(TODAY())+7))');
+                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_THIS_WEEK) {
+                    $objWriter->writeElement('formula', 'AND(TODAY()-ROUNDDOWN(' . $cellCoordinate . ',0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(' . $cellCoordinate . ',0)-TODAY()<=7-WEEKDAY(TODAY()))');
+                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_NEXT_WEEK) {
+                    $objWriter->writeElement('formula', 'AND(ROUNDDOWN(' . $cellCoordinate . ',0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(' . $cellCoordinate . ',0)-TODAY()<(15-WEEKDAY(TODAY())))');
+                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_LAST_MONTH) {
+                    $objWriter->writeElement('formula', 'AND(MONTH(' . $cellCoordinate . ')=MONTH(EDATE(TODAY(),0-1)),YEAR(' . $cellCoordinate . ')=YEAR(EDATE(TODAY(),0-1)))');
+                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_THIS_MONTH) {
+                    $objWriter->writeElement('formula', 'AND(MONTH(' . $cellCoordinate . ')=MONTH(TODAY()),YEAR(' . $cellCoordinate . ')=YEAR(TODAY()))');
+                } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_NEXT_MONTH) {
+                    $objWriter->writeElement('formula', 'AND(MONTH(' . $cellCoordinate . ')=MONTH(EDATE(TODAY(),0+1)),YEAR(' . $cellCoordinate . ')=YEAR(EDATE(TODAY(),0+1)))');
+                }
+            } else {
+                $objWriter->writeElement('formula', (string) ($conditional->getConditions()[0]));
+            }
+        }
+    }
+
+    private static function writeTextCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void
+    {
+        $txt = $conditional->getText();
+        if (!empty($txt)) {
+            $objWriter->writeAttribute('text', $txt);
+            if (empty($conditional->getConditions())) {
+                if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) {
+                    $objWriter->writeElement('formula', 'NOT(ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . ')))');
+                } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_BEGINSWITH) {
+                    $objWriter->writeElement('formula', 'LEFT(' . $cellCoordinate . ',LEN("' . $txt . '"))="' . $txt . '"');
+                } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_ENDSWITH) {
+                    $objWriter->writeElement('formula', 'RIGHT(' . $cellCoordinate . ',LEN("' . $txt . '"))="' . $txt . '"');
+                } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_NOTCONTAINS) {
+                    $objWriter->writeElement('formula', 'ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . '))');
+                }
+            } else {
+                $objWriter->writeElement('formula', (string) ($conditional->getConditions()[0]));
+            }
+        }
+    }
+
+    private static function writeExtConditionalFormattingElements(XMLWriter $objWriter, ConditionalFormattingRuleExtension $ruleExtension): void
+    {
+        $prefix = 'x14';
+        $objWriter->startElementNs($prefix, 'conditionalFormatting', null);
+
+        $objWriter->startElementNs($prefix, 'cfRule', null);
+        $objWriter->writeAttribute('type', $ruleExtension->getCfRule());
+        $objWriter->writeAttribute('id', $ruleExtension->getId());
+        $objWriter->startElementNs($prefix, 'dataBar', null);
+        $dataBar = $ruleExtension->getDataBarExt();
+        foreach ($dataBar->getXmlAttributes() as $attrKey => $val) {
+            $objWriter->writeAttribute($attrKey, $val);
+        }
+        $minCfvo = $dataBar->getMinimumConditionalFormatValueObject();
+        if ($minCfvo !== null) {
+            $objWriter->startElementNs($prefix, 'cfvo', null);
+            $objWriter->writeAttribute('type', $minCfvo->getType());
+            if ($minCfvo->getCellFormula()) {
+                $objWriter->writeElement('xm:f', $minCfvo->getCellFormula());
+            }
+            $objWriter->endElement(); //end cfvo
+        }
+
+        $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject();
+        if ($maxCfvo !== null) {
+            $objWriter->startElementNs($prefix, 'cfvo', null);
+            $objWriter->writeAttribute('type', $maxCfvo->getType());
+            if ($maxCfvo->getCellFormula()) {
+                $objWriter->writeElement('xm:f', $maxCfvo->getCellFormula());
+            }
+            $objWriter->endElement(); //end cfvo
+        }
+
+        foreach ($dataBar->getXmlElements() as $elmKey => $elmAttr) {
+            $objWriter->startElementNs($prefix, $elmKey, null);
+            foreach ($elmAttr as $attrKey => $attrVal) {
+                $objWriter->writeAttribute($attrKey, $attrVal);
+            }
+            $objWriter->endElement(); //end elmKey
+        }
+        $objWriter->endElement(); //end dataBar
+        $objWriter->endElement(); //end cfRule
+        $objWriter->writeElement('xm:sqref', $ruleExtension->getSqref());
+        $objWriter->endElement(); //end conditionalFormatting
+    }
+
+    private static function writeDataBarElements(XMLWriter $objWriter, ?ConditionalDataBar $dataBar): void
+    {
+        if ($dataBar) {
+            $objWriter->startElement('dataBar');
+            self::writeAttributeIf($objWriter, null !== $dataBar->getShowValue(), 'showValue', $dataBar->getShowValue() ? '1' : '0');
+
+            $minCfvo = $dataBar->getMinimumConditionalFormatValueObject();
+            if ($minCfvo) {
+                $objWriter->startElement('cfvo');
+                self::writeAttributeIf($objWriter, $minCfvo->getType(), 'type', (string) $minCfvo->getType());
+                self::writeAttributeIf($objWriter, $minCfvo->getValue(), 'val', (string) $minCfvo->getValue());
+                $objWriter->endElement();
+            }
+            $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject();
+            if ($maxCfvo) {
+                $objWriter->startElement('cfvo');
+                self::writeAttributeIf($objWriter, $maxCfvo->getType(), 'type', (string) $maxCfvo->getType());
+                self::writeAttributeIf($objWriter, $maxCfvo->getValue(), 'val', (string) $maxCfvo->getValue());
+                $objWriter->endElement();
+            }
+            if ($dataBar->getColor()) {
+                $objWriter->startElement('color');
+                $objWriter->writeAttribute('rgb', $dataBar->getColor());
+                $objWriter->endElement();
+            }
+            $objWriter->endElement(); // end dataBar
+
+            if ($dataBar->getConditionalFormattingRuleExt()) {
+                $objWriter->startElement('extLst');
+                $extension = $dataBar->getConditionalFormattingRuleExt();
+                $objWriter->startElement('ext');
+                $objWriter->writeAttribute('uri', '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}');
+                $objWriter->startElementNs('x14', 'id', null);
+                $objWriter->text($extension->getId());
+                $objWriter->endElement();
+                $objWriter->endElement();
+                $objWriter->endElement(); //end extLst
+            }
+        }
+    }
+
+    /**
+     * Write ConditionalFormatting.
+     */
+    private function writeConditionalFormatting(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // Conditional id
+        $id = 1;
+
+        // Loop through styles in the current worksheet
+        foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
+            $objWriter->startElement('conditionalFormatting');
+            $objWriter->writeAttribute('sqref', $cellCoordinate);
+
+            foreach ($conditionalStyles as $conditional) {
+                // WHY was this again?
+                // if ($this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) == '') {
+                //    continue;
+                // }
+                // cfRule
+                $objWriter->startElement('cfRule');
+                $objWriter->writeAttribute('type', $conditional->getConditionType());
+                self::writeAttributeIf(
+                    $objWriter,
+                    ($conditional->getConditionType() !== Conditional::CONDITION_DATABAR && $conditional->getNoFormatSet() === false),
+                    'dxfId',
+                    (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())
+                );
+                $objWriter->writeAttribute('priority', (string) $id++);
+
+                self::writeAttributeif(
+                    $objWriter,
+                    (
+                        $conditional->getConditionType() === Conditional::CONDITION_CELLIS
+                        || $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT
+                        || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT
+                        || $conditional->getConditionType() === Conditional::CONDITION_BEGINSWITH
+                        || $conditional->getConditionType() === Conditional::CONDITION_ENDSWITH
+                    ) && $conditional->getOperatorType() !== Conditional::OPERATOR_NONE,
+                    'operator',
+                    $conditional->getOperatorType()
+                );
+
+                self::writeAttributeIf($objWriter, $conditional->getStopIfTrue(), 'stopIfTrue', '1');
+
+                $cellRange = Coordinate::splitRange(str_replace('$', '', strtoupper($cellCoordinate)));
+                [$topLeftCell] = $cellRange[0];
+
+                if (
+                    $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT
+                    || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT
+                    || $conditional->getConditionType() === Conditional::CONDITION_BEGINSWITH
+                    || $conditional->getConditionType() === Conditional::CONDITION_ENDSWITH
+                ) {
+                    self::writeTextCondElements($objWriter, $conditional, $topLeftCell);
+                } elseif ($conditional->getConditionType() === Conditional::CONDITION_TIMEPERIOD) {
+                    self::writeTimePeriodCondElements($objWriter, $conditional, $topLeftCell);
+                } else {
+                    self::writeOtherCondElements($objWriter, $conditional, $topLeftCell);
+                }
+
+                //<dataBar>
+                self::writeDataBarElements($objWriter, $conditional->getDataBar());
+
+                $objWriter->endElement(); //end cfRule
+            }
+
+            $objWriter->endElement(); //end conditionalFormatting
+        }
+    }
+
+    /**
+     * Write DataValidations.
+     */
+    private function writeDataValidations(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // Datavalidation collection
+        $dataValidationCollection = $worksheet->getDataValidationCollection();
+
+        // Write data validations?
+        if (!empty($dataValidationCollection)) {
+            $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection);
+            $objWriter->startElement('dataValidations');
+            $objWriter->writeAttribute('count', (string) count($dataValidationCollection));
+
+            foreach ($dataValidationCollection as $coordinate => $dv) {
+                $objWriter->startElement('dataValidation');
+
+                if ($dv->getType() != '') {
+                    $objWriter->writeAttribute('type', $dv->getType());
+                }
+
+                if ($dv->getErrorStyle() != '') {
+                    $objWriter->writeAttribute('errorStyle', $dv->getErrorStyle());
+                }
+
+                if ($dv->getOperator() != '') {
+                    $objWriter->writeAttribute('operator', $dv->getOperator());
+                }
+
+                $objWriter->writeAttribute('allowBlank', ($dv->getAllowBlank() ? '1' : '0'));
+                $objWriter->writeAttribute('showDropDown', (!$dv->getShowDropDown() ? '1' : '0'));
+                $objWriter->writeAttribute('showInputMessage', ($dv->getShowInputMessage() ? '1' : '0'));
+                $objWriter->writeAttribute('showErrorMessage', ($dv->getShowErrorMessage() ? '1' : '0'));
+
+                if ($dv->getErrorTitle() !== '') {
+                    $objWriter->writeAttribute('errorTitle', $dv->getErrorTitle());
+                }
+                if ($dv->getError() !== '') {
+                    $objWriter->writeAttribute('error', $dv->getError());
+                }
+                if ($dv->getPromptTitle() !== '') {
+                    $objWriter->writeAttribute('promptTitle', $dv->getPromptTitle());
+                }
+                if ($dv->getPrompt() !== '') {
+                    $objWriter->writeAttribute('prompt', $dv->getPrompt());
+                }
+
+                $objWriter->writeAttribute('sqref', $dv->getSqref() ?? $coordinate);
+
+                if ($dv->getFormula1() !== '') {
+                    $objWriter->writeElement('formula1', $dv->getFormula1());
+                }
+                if ($dv->getFormula2() !== '') {
+                    $objWriter->writeElement('formula2', $dv->getFormula2());
+                }
+
+                $objWriter->endElement();
+            }
+
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write Hyperlinks.
+     */
+    private function writeHyperlinks(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // Hyperlink collection
+        $hyperlinkCollection = $worksheet->getHyperlinkCollection();
+
+        // Relation ID
+        $relationId = 1;
+
+        // Write hyperlinks?
+        if (!empty($hyperlinkCollection)) {
+            $objWriter->startElement('hyperlinks');
+
+            foreach ($hyperlinkCollection as $coordinate => $hyperlink) {
+                $objWriter->startElement('hyperlink');
+
+                $objWriter->writeAttribute('ref', $coordinate);
+                if (!$hyperlink->isInternal()) {
+                    $objWriter->writeAttribute('r:id', 'rId_hyperlink_' . $relationId);
+                    ++$relationId;
+                } else {
+                    $objWriter->writeAttribute('location', str_replace('sheet://', '', $hyperlink->getUrl()));
+                }
+
+                if ($hyperlink->getTooltip() !== '') {
+                    $objWriter->writeAttribute('tooltip', $hyperlink->getTooltip());
+                    $objWriter->writeAttribute('display', $hyperlink->getTooltip());
+                }
+
+                $objWriter->endElement();
+            }
+
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write ProtectedRanges.
+     */
+    private function writeProtectedRanges(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        if (count($worksheet->getProtectedCells()) > 0) {
+            // protectedRanges
+            $objWriter->startElement('protectedRanges');
+
+            // Loop protectedRanges
+            foreach ($worksheet->getProtectedCells() as $protectedCell => $passwordHash) {
+                // protectedRange
+                $objWriter->startElement('protectedRange');
+                $objWriter->writeAttribute('name', 'p' . md5($protectedCell));
+                $objWriter->writeAttribute('sqref', $protectedCell);
+                if (!empty($passwordHash)) {
+                    $objWriter->writeAttribute('password', $passwordHash);
+                }
+                $objWriter->endElement();
+            }
+
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write MergeCells.
+     */
+    private function writeMergeCells(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        if (count($worksheet->getMergeCells()) > 0) {
+            // mergeCells
+            $objWriter->startElement('mergeCells');
+
+            // Loop mergeCells
+            foreach ($worksheet->getMergeCells() as $mergeCell) {
+                // mergeCell
+                $objWriter->startElement('mergeCell');
+                $objWriter->writeAttribute('ref', $mergeCell);
+                $objWriter->endElement();
+            }
+
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write PrintOptions.
+     */
+    private function writePrintOptions(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // printOptions
+        $objWriter->startElement('printOptions');
+
+        $objWriter->writeAttribute('gridLines', ($worksheet->getPrintGridlines() ? 'true' : 'false'));
+        $objWriter->writeAttribute('gridLinesSet', 'true');
+
+        if ($worksheet->getPageSetup()->getHorizontalCentered()) {
+            $objWriter->writeAttribute('horizontalCentered', 'true');
+        }
+
+        if ($worksheet->getPageSetup()->getVerticalCentered()) {
+            $objWriter->writeAttribute('verticalCentered', 'true');
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write PageMargins.
+     */
+    private function writePageMargins(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // pageMargins
+        $objWriter->startElement('pageMargins');
+        $objWriter->writeAttribute('left', StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()));
+        $objWriter->writeAttribute('right', StringHelper::formatNumber($worksheet->getPageMargins()->getRight()));
+        $objWriter->writeAttribute('top', StringHelper::formatNumber($worksheet->getPageMargins()->getTop()));
+        $objWriter->writeAttribute('bottom', StringHelper::formatNumber($worksheet->getPageMargins()->getBottom()));
+        $objWriter->writeAttribute('header', StringHelper::formatNumber($worksheet->getPageMargins()->getHeader()));
+        $objWriter->writeAttribute('footer', StringHelper::formatNumber($worksheet->getPageMargins()->getFooter()));
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write AutoFilter.
+     */
+    private function writeAutoFilter(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        AutoFilter::writeAutoFilter($objWriter, $worksheet);
+    }
+
+    /**
+     * Write Table.
+     */
+    private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        $tableCount = $worksheet->getTableCollection()->count();
+
+        $objWriter->startElement('tableParts');
+        $objWriter->writeAttribute('count', (string) $tableCount);
+
+        for ($t = 1; $t <= $tableCount; ++$t) {
+            $objWriter->startElement('tablePart');
+            $objWriter->writeAttribute('r:id', 'rId_table_' . $t);
+            $objWriter->endElement();
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write PageSetup.
+     */
+    private function writePageSetup(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // pageSetup
+        $objWriter->startElement('pageSetup');
+        $objWriter->writeAttribute('paperSize', (string) $worksheet->getPageSetup()->getPaperSize());
+        $objWriter->writeAttribute('orientation', $worksheet->getPageSetup()->getOrientation());
+
+        if ($worksheet->getPageSetup()->getScale() !== null) {
+            $objWriter->writeAttribute('scale', (string) $worksheet->getPageSetup()->getScale());
+        }
+        if ($worksheet->getPageSetup()->getFitToHeight() !== null) {
+            $objWriter->writeAttribute('fitToHeight', (string) $worksheet->getPageSetup()->getFitToHeight());
+        } else {
+            $objWriter->writeAttribute('fitToHeight', '0');
+        }
+        if ($worksheet->getPageSetup()->getFitToWidth() !== null) {
+            $objWriter->writeAttribute('fitToWidth', (string) $worksheet->getPageSetup()->getFitToWidth());
+        } else {
+            $objWriter->writeAttribute('fitToWidth', '0');
+        }
+        if (!empty($worksheet->getPageSetup()->getFirstPageNumber())) {
+            $objWriter->writeAttribute('firstPageNumber', (string) $worksheet->getPageSetup()->getFirstPageNumber());
+            $objWriter->writeAttribute('useFirstPageNumber', '1');
+        }
+        $objWriter->writeAttribute('pageOrder', $worksheet->getPageSetup()->getPageOrder());
+
+        $getUnparsedLoadedData = $worksheet->getParentOrThrow()->getUnparsedLoadedData();
+        if (isset($getUnparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId'])) {
+            $objWriter->writeAttribute('r:id', $getUnparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId']);
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Header / Footer.
+     */
+    private function writeHeaderFooter(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // headerFooter
+        $objWriter->startElement('headerFooter');
+        $objWriter->writeAttribute('differentOddEven', ($worksheet->getHeaderFooter()->getDifferentOddEven() ? 'true' : 'false'));
+        $objWriter->writeAttribute('differentFirst', ($worksheet->getHeaderFooter()->getDifferentFirst() ? 'true' : 'false'));
+        $objWriter->writeAttribute('scaleWithDoc', ($worksheet->getHeaderFooter()->getScaleWithDocument() ? 'true' : 'false'));
+        $objWriter->writeAttribute('alignWithMargins', ($worksheet->getHeaderFooter()->getAlignWithMargins() ? 'true' : 'false'));
+
+        $objWriter->writeElement('oddHeader', $worksheet->getHeaderFooter()->getOddHeader());
+        $objWriter->writeElement('oddFooter', $worksheet->getHeaderFooter()->getOddFooter());
+        $objWriter->writeElement('evenHeader', $worksheet->getHeaderFooter()->getEvenHeader());
+        $objWriter->writeElement('evenFooter', $worksheet->getHeaderFooter()->getEvenFooter());
+        $objWriter->writeElement('firstHeader', $worksheet->getHeaderFooter()->getFirstHeader());
+        $objWriter->writeElement('firstFooter', $worksheet->getHeaderFooter()->getFirstFooter());
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Breaks.
+     */
+    private function writeBreaks(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // Get row and column breaks
+        $aRowBreaks = [];
+        $aColumnBreaks = [];
+        foreach ($worksheet->getRowBreaks() as $cell => $break) {
+            $aRowBreaks[$cell] = $break;
+        }
+        foreach ($worksheet->getColumnBreaks() as $cell => $break) {
+            $aColumnBreaks[$cell] = $break;
+        }
+
+        // rowBreaks
+        if (!empty($aRowBreaks)) {
+            $objWriter->startElement('rowBreaks');
+            $objWriter->writeAttribute('count', (string) count($aRowBreaks));
+            $objWriter->writeAttribute('manualBreakCount', (string) count($aRowBreaks));
+
+            foreach ($aRowBreaks as $cell => $break) {
+                $coords = Coordinate::coordinateFromString($cell);
+
+                $objWriter->startElement('brk');
+                $objWriter->writeAttribute('id', $coords[1]);
+                $objWriter->writeAttribute('man', '1');
+                $rowBreakMax = $break->getMaxColOrRow();
+                if ($rowBreakMax >= 0) {
+                    $objWriter->writeAttribute('max', "$rowBreakMax");
+                }
+                $objWriter->endElement();
+            }
+
+            $objWriter->endElement();
+        }
+
+        // Second, write column breaks
+        if (!empty($aColumnBreaks)) {
+            $objWriter->startElement('colBreaks');
+            $objWriter->writeAttribute('count', (string) count($aColumnBreaks));
+            $objWriter->writeAttribute('manualBreakCount', (string) count($aColumnBreaks));
+
+            foreach ($aColumnBreaks as $cell => $break) {
+                $coords = Coordinate::coordinateFromString($cell);
+
+                $objWriter->startElement('brk');
+                $objWriter->writeAttribute('id', (string) ((int) $coords[0] - 1));
+                $objWriter->writeAttribute('man', '1');
+                $objWriter->endElement();
+            }
+
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write SheetData.
+     *
+     * @param string[] $stringTable String table
+     */
+    private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet, array $stringTable): void
+    {
+        // Flipped stringtable, for faster index searching
+        $aFlippedStringTable = $this->getParentWriter()->getWriterPartstringtable()->flipStringTable($stringTable);
+
+        // sheetData
+        $objWriter->startElement('sheetData');
+
+        // Get column count
+        $colCount = Coordinate::columnIndexFromString($worksheet->getHighestColumn());
+
+        // Highest row number
+        $highestRow = $worksheet->getHighestRow();
+
+        // Loop through cells building a comma-separated list of the columns in each row
+        // This is a trade-off between the memory usage that is required for a full array of columns,
+        //      and execution speed
+        /** @var array<int, string> $cellsByRow */
+        $cellsByRow = [];
+        foreach ($worksheet->getCoordinates() as $coordinate) {
+            [$column, $row] = Coordinate::coordinateFromString($coordinate);
+            $cellsByRow[$row] = $cellsByRow[$row] ?? '';
+            $cellsByRow[$row] .= "{$column},";
+        }
+
+        $currentRow = 0;
+        while ($currentRow++ < $highestRow) {
+            $isRowSet = isset($cellsByRow[$currentRow]);
+            if ($isRowSet || $worksheet->rowDimensionExists($currentRow)) {
+                // Get row dimension
+                $rowDimension = $worksheet->getRowDimension($currentRow);
+
+                // Write current row?
+                $writeCurrentRow = $isRowSet || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() === false || $rowDimension->getCollapsed() === true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null;
+
+                if ($writeCurrentRow) {
+                    // Start a new row
+                    $objWriter->startElement('row');
+                    $objWriter->writeAttribute('r', "$currentRow");
+                    $objWriter->writeAttribute('spans', '1:' . $colCount);
+
+                    // Row dimensions
+                    if ($rowDimension->getRowHeight() >= 0) {
+                        $objWriter->writeAttribute('customHeight', '1');
+                        $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight()));
+                    }
+
+                    // Row visibility
+                    if (!$rowDimension->getVisible() === true) {
+                        $objWriter->writeAttribute('hidden', 'true');
+                    }
+
+                    // Collapsed
+                    if ($rowDimension->getCollapsed() === true) {
+                        $objWriter->writeAttribute('collapsed', 'true');
+                    }
+
+                    // Outline level
+                    if ($rowDimension->getOutlineLevel() > 0) {
+                        $objWriter->writeAttribute('outlineLevel', (string) $rowDimension->getOutlineLevel());
+                    }
+
+                    // Style
+                    if ($rowDimension->getXfIndex() !== null) {
+                        $objWriter->writeAttribute('s', (string) $rowDimension->getXfIndex());
+                        $objWriter->writeAttribute('customFormat', '1');
+                    }
+
+                    // Write cells
+                    if (isset($cellsByRow[$currentRow])) {
+                        // We have a comma-separated list of column names (with a trailing entry); split to an array
+                        $columnsInRow = explode(',', $cellsByRow[$currentRow]);
+                        array_pop($columnsInRow);
+                        foreach ($columnsInRow as $column) {
+                            // Write cell
+                            $coord = "$column$currentRow";
+                            if ($worksheet->getCell($coord)->getIgnoredErrors()->getNumberStoredAsText()) {
+                                $this->numberStoredAsText .= " $coord";
+                            }
+                            if ($worksheet->getCell($coord)->getIgnoredErrors()->getFormula()) {
+                                $this->formula .= " $coord";
+                            }
+                            if ($worksheet->getCell($coord)->getIgnoredErrors()->getTwoDigitTextYear()) {
+                                $this->twoDigitTextYear .= " $coord";
+                            }
+                            if ($worksheet->getCell($coord)->getIgnoredErrors()->getEvalError()) {
+                                $this->evalError .= " $coord";
+                            }
+                            $this->writeCell($objWriter, $worksheet, $coord, $aFlippedStringTable);
+                        }
+                    }
+
+                    // End row
+                    $objWriter->endElement();
+                }
+            }
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * @param RichText|string $cellValue
+     */
+    private function writeCellInlineStr(XMLWriter $objWriter, string $mappedType, $cellValue): void
+    {
+        $objWriter->writeAttribute('t', $mappedType);
+        if (!$cellValue instanceof RichText) {
+            $objWriter->startElement('is');
+            $objWriter->writeElement(
+                't',
+                StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue, Settings::htmlEntityFlags()))
+            );
+            $objWriter->endElement();
+        } else {
+            $objWriter->startElement('is');
+            $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $cellValue);
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * @param RichText|string $cellValue
+     * @param string[] $flippedStringTable
+     */
+    private function writeCellString(XMLWriter $objWriter, string $mappedType, $cellValue, array $flippedStringTable): void
+    {
+        $objWriter->writeAttribute('t', $mappedType);
+        if (!$cellValue instanceof RichText) {
+            self::writeElementIf($objWriter, isset($flippedStringTable[$cellValue]), 'v', $flippedStringTable[$cellValue] ?? '');
+        } else {
+            $objWriter->writeElement('v', $flippedStringTable[$cellValue->getHashCode()]);
+        }
+    }
+
+    /**
+     * @param float|int $cellValue
+     */
+    private function writeCellNumeric(XMLWriter $objWriter, $cellValue): void
+    {
+        //force a decimal to be written if the type is float
+        if (is_float($cellValue)) {
+            // force point as decimal separator in case current locale uses comma
+            $cellValue = str_replace(',', '.', (string) $cellValue);
+            if (strpos($cellValue, '.') === false) {
+                $cellValue = $cellValue . '.0';
+            }
+        }
+        $objWriter->writeElement('v', "$cellValue");
+    }
+
+    private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void
+    {
+        $objWriter->writeAttribute('t', $mappedType);
+        $objWriter->writeElement('v', $cellValue ? '1' : '0');
+    }
+
+    private function writeCellError(XMLWriter $objWriter, string $mappedType, string $cellValue, string $formulaerr = '#NULL!'): void
+    {
+        $objWriter->writeAttribute('t', $mappedType);
+        $cellIsFormula = substr($cellValue, 0, 1) === '=';
+        self::writeElementIf($objWriter, $cellIsFormula, 'f', FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
+        $objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue);
+    }
+
+    private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell $cell): void
+    {
+        $calculatedValue = $this->getParentWriter()->getPreCalculateFormulas() ? $cell->getCalculatedValue() : $cellValue;
+        if (is_string($calculatedValue)) {
+            if (ErrorValue::isError($calculatedValue)) {
+                $this->writeCellError($objWriter, 'e', $cellValue, $calculatedValue);
+
+                return;
+            }
+            $objWriter->writeAttribute('t', 'str');
+            $calculatedValue = StringHelper::controlCharacterPHP2OOXML($calculatedValue);
+        } elseif (is_bool($calculatedValue)) {
+            $objWriter->writeAttribute('t', 'b');
+            $calculatedValue = (int) $calculatedValue;
+        }
+
+        $attributes = $cell->getFormulaAttributes();
+        if (($attributes['t'] ?? null) === 'array') {
+            $objWriter->startElement('f');
+            $objWriter->writeAttribute('t', 'array');
+            $objWriter->writeAttribute('ref', $cell->getCoordinate());
+            $objWriter->writeAttribute('aca', '1');
+            $objWriter->writeAttribute('ca', '1');
+            $objWriter->text(FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
+            $objWriter->endElement();
+        } else {
+            $objWriter->writeElement('f', FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
+            self::writeElementIf(
+                $objWriter,
+                $this->getParentWriter()->getOffice2003Compatibility() === false,
+                'v',
+                ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue ?? '', 0, 1) !== '#')
+                    ? StringHelper::formatNumber($calculatedValue) : '0'
+            );
+        }
+    }
+
+    /**
+     * Write Cell.
+     *
+     * @param string $cellAddress Cell Address
+     * @param string[] $flippedStringTable String table (flipped), for faster index searching
+     */
+    private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet, string $cellAddress, array $flippedStringTable): void
+    {
+        // Cell
+        $pCell = $worksheet->getCell($cellAddress);
+        $objWriter->startElement('c');
+        $objWriter->writeAttribute('r', $cellAddress);
+
+        // Sheet styles
+        $xfi = $pCell->getXfIndex();
+        self::writeAttributeIf($objWriter, (bool) $xfi, 's', "$xfi");
+
+        // If cell value is supplied, write cell value
+        $cellValue = $pCell->getValue();
+        if (is_object($cellValue) || $cellValue !== '') {
+            // Map type
+            $mappedType = $pCell->getDataType();
+
+            // Write data depending on its type
+            switch (strtolower($mappedType)) {
+                case 'inlinestr':    // Inline string
+                    $this->writeCellInlineStr($objWriter, $mappedType, $cellValue);
+
+                    break;
+                case 's':            // String
+                    $this->writeCellString($objWriter, $mappedType, $cellValue, $flippedStringTable);
+
+                    break;
+                case 'f':            // Formula
+                    $this->writeCellFormula($objWriter, $cellValue, $pCell);
+
+                    break;
+                case 'n':            // Numeric
+                    $this->writeCellNumeric($objWriter, $cellValue);
+
+                    break;
+                case 'b':            // Boolean
+                    $this->writeCellBoolean($objWriter, $mappedType, $cellValue);
+
+                    break;
+                case 'e':            // Error
+                    $this->writeCellError($objWriter, $mappedType, $cellValue);
+            }
+        }
+
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write Drawings.
+     *
+     * @param bool $includeCharts Flag indicating if we should include drawing details for charts
+     */
+    private function writeDrawings(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet, $includeCharts = false): void
+    {
+        $unparsedLoadedData = $worksheet->getParentOrThrow()->getUnparsedLoadedData();
+        $hasUnparsedDrawing = isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds']);
+        $chartCount = ($includeCharts) ? $worksheet->getChartCollection()->count() : 0;
+        if ($chartCount == 0 && $worksheet->getDrawingCollection()->count() == 0 && !$hasUnparsedDrawing) {
+            return;
+        }
+
+        // If sheet contains drawings, add the relationships
+        $objWriter->startElement('drawing');
+
+        $rId = 'rId1';
+        if (isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds'])) {
+            $drawingOriginalIds = $unparsedLoadedData['sheets'][$worksheet->getCodeName()]['drawingOriginalIds'];
+            // take first. In future can be overriten
+            // (! synchronize with \PhpOffice\PhpSpreadsheet\Writer\Xlsx\Rels::writeWorksheetRelationships)
+            $rId = reset($drawingOriginalIds);
+        }
+
+        $objWriter->writeAttribute('r:id', $rId);
+        $objWriter->endElement();
+    }
+
+    /**
+     * Write LegacyDrawing.
+     */
+    private function writeLegacyDrawing(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // If sheet contains comments, add the relationships
+        $unparsedLoadedData = $worksheet->getParentOrThrow()->getUnparsedLoadedData();
+        if (count($worksheet->getComments()) > 0 || isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'])) {
+            $objWriter->startElement('legacyDrawing');
+            $objWriter->writeAttribute('r:id', 'rId_comments_vml1');
+            $objWriter->endElement();
+        }
+    }
+
+    /**
+     * Write LegacyDrawingHF.
+     */
+    private function writeLegacyDrawingHF(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        // If sheet contains images, add the relationships
+        if (count($worksheet->getHeaderFooter()->getImages()) > 0) {
+            $objWriter->startElement('legacyDrawingHF');
+            $objWriter->writeAttribute('r:id', 'rId_headerfooter_vml1');
+            $objWriter->endElement();
+        }
+    }
+
+    private function writeAlternateContent(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        if (empty($worksheet->getParentOrThrow()->getUnparsedLoadedData()['sheets'][$worksheet->getCodeName()]['AlternateContents'])) {
+            return;
+        }
+
+        foreach ($worksheet->getParentOrThrow()->getUnparsedLoadedData()['sheets'][$worksheet->getCodeName()]['AlternateContents'] as $alternateContent) {
+            $objWriter->writeRaw($alternateContent);
+        }
+    }
+
+    /**
+     * write <ExtLst>
+     * only implementation conditionalFormattings.
+     *
+     * @url https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/07d607af-5618-4ca2-b683-6a78dc0d9627
+     */
+    private function writeExtLst(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
+    {
+        $conditionalFormattingRuleExtList = [];
+        foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
+            /** @var Conditional $conditional */
+            foreach ($conditionalStyles as $conditional) {
+                $dataBar = $conditional->getDataBar();
+                if ($dataBar && $dataBar->getConditionalFormattingRuleExt()) {
+                    $conditionalFormattingRuleExtList[] = $dataBar->getConditionalFormattingRuleExt();
+                }
+            }
+        }
+
+        if (count($conditionalFormattingRuleExtList) > 0) {
+            $conditionalFormattingRuleExtNsPrefix = 'x14';
+            $objWriter->startElement('extLst');
+            $objWriter->startElement('ext');
+            $objWriter->writeAttribute('uri', '{78C0D931-6437-407d-A8EE-F0AAD7539E65}');
+            $objWriter->startElementNs($conditionalFormattingRuleExtNsPrefix, 'conditionalFormattings', null);
+            foreach ($conditionalFormattingRuleExtList as $extension) {
+                self::writeExtConditionalFormattingElements($objWriter, $extension);
+            }
+            $objWriter->endElement(); //end conditionalFormattings
+            $objWriter->endElement(); //end ext
+            $objWriter->endElement(); //end extLst
+        }
+    }
+}

+ 33 - 0
vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/WriterPart.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+abstract class WriterPart
+{
+    /**
+     * Parent Xlsx object.
+     *
+     * @var Xlsx
+     */
+    private $parentWriter;
+
+    /**
+     * Get parent Xlsx object.
+     *
+     * @return Xlsx
+     */
+    public function getParentWriter()
+    {
+        return $this->parentWriter;
+    }
+
+    /**
+     * Set parent Xlsx object.
+     */
+    public function __construct(Xlsx $writer)
+    {
+        $this->parentWriter = $writer;
+    }
+}

Some files were not shown because too many files changed in this diff