summaryrefslogtreecommitdiffstats
path: root/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls
diff options
context:
space:
mode:
authorAnton Luka Šijanec <anton@sijanec.eu>2024-05-27 13:08:29 +0200
committerAnton Luka Šijanec <anton@sijanec.eu>2024-05-27 13:08:29 +0200
commit75160b12821f7f4299cce7f0b69c83c1502ae071 (patch)
tree27e25e4ccaef45f0c58b22831164050d1af1d4db /vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls
parentprvi-commit (diff)
download1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.gz
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.bz2
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.lz
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.xz
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.zst
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.zip
Diffstat (limited to 'vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls')
-rw-r--r--vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/BIFFwriter.php224
-rw-r--r--vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Escher.php510
-rw-r--r--vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Font.php147
-rw-r--r--vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php1483
-rw-r--r--vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php1191
-rw-r--r--vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Worksheet.php4490
-rw-r--r--vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Xf.php548
7 files changed, 8593 insertions, 0 deletions
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/BIFFwriter.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/BIFFwriter.php
new file mode 100644
index 0000000..f989a0e
--- /dev/null
+++ b/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 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()
+ {
+ $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;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Escher.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Escher.php
new file mode 100644
index 0000000..f8af656
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Escher.php
@@ -0,0 +1,510 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+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.
+ */
+ private $object;
+
+ /**
+ * The written binary data.
+ */
+ 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 \PhpOffice\PhpSpreadsheet\Shared\Escher::class:
+ if ($dggContainer = $this->object->getDggContainer()) {
+ $writer = new self($dggContainer);
+ $this->data = $writer->close();
+ } elseif ($dgContainer = $this->object->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->getSpIdMax(), // maximum shape identifier increased by one
+ $this->object->getCDgSaved() + 1, // number of file identifier clusters increased by one
+ $this->object->getCSpSaved(),
+ $this->object->getCDgSaved() // count total number of drawings saved
+ );
+
+ // add file identifier clusters (one per drawing)
+ $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->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->getBSECollection()) {
+ foreach ($BSECollection as $BSE) {
+ $writer = new self($BSE);
+ $innerData .= $writer->close();
+ }
+ }
+
+ // write the record
+ $recVer = 0xF;
+ $recInstance = count($this->object->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->getBlip()) {
+ $writer = new self($blip);
+ $innerData .= $writer->close();
+ }
+
+ // initialize
+ $data = '';
+
+ $btWin32 = $this->object->getBlipType();
+ $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;
+ $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->getParent()->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->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->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;
+ $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->getSpgrContainer()->getChildren());
+ $innerData .= $header . pack('VV', $countShapes, $this->object->getLastSpId());
+
+ // write the spgrContainer
+ if ($spgrContainer = $this->object->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->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->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);
+ }
+ $this->spTypes[] = ($this->object->getSpType());
+
+ // write the shape record
+ $recVer = 0x2;
+ $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->getSpId(), $this->object->getSpgr() ? 0x0005 : 0x0A00);
+
+ // the options
+ if ($this->object->getOPTCollection()) {
+ $optData = '';
+
+ $recVer = 0x3;
+ $recInstance = count($this->object->getOPTCollection());
+ $recType = 0xF00B;
+ foreach ($this->object->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->getStartCoordinates()) {
+ $clientAnchorData = '';
+
+ $recVer = 0x0;
+ $recInstance = 0x0;
+ $recType = 0xF010;
+
+ // start coordinates
+ [$column, $row] = Coordinate::coordinateFromString($this->object->getStartCoordinates());
+ $c1 = Coordinate::columnIndexFromString($column) - 1;
+ $r1 = $row - 1;
+
+ // start offsetX
+ $startOffsetX = $this->object->getStartOffsetX();
+
+ // start offsetY
+ $startOffsetY = $this->object->getStartOffsetY();
+
+ // end coordinates
+ [$column, $row] = Coordinate::coordinateFromString($this->object->getEndCoordinates());
+ $c2 = Coordinate::columnIndexFromString($column) - 1;
+ $r2 = $row - 1;
+
+ // end offsetX
+ $endOffsetX = $this->object->getEndOffsetX();
+
+ // end offsetY
+ $endOffsetY = $this->object->getEndOffsetY();
+
+ $clientAnchorData = pack('vvvvvvvvv', $this->object->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->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;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Font.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Font.php
new file mode 100644
index 0000000..501f03b
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Font.php
@@ -0,0 +1,147 @@
+<?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;
+ }
+
+ /**
+ * Get font record data.
+ *
+ * @return string
+ */
+ public function writeFont()
+ {
+ $font_outline = 0;
+ $font_shadow = 0;
+
+ $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($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($this->font->getUnderline()),
+ $bFamily,
+ $bCharSet,
+ $reserved
+ );
+ $data .= StringHelper::UTF8toBIFF8UnicodeShort($this->font->getName());
+
+ $length = strlen($data);
+ $header = pack('vv', $record, $length);
+
+ return $header . $data;
+ }
+
+ /**
+ * Map to BIFF5-BIFF8 codes for bold.
+ *
+ * @param bool $bold
+ *
+ * @return int
+ */
+ private static function mapBold($bold)
+ {
+ if ($bold) {
+ return 0x2BC; // 700 = Bold font weight
+ }
+
+ return 0x190; // 400 = Normal font weight
+ }
+
+ /**
+ * Map of BIFF2-BIFF8 codes for underline styles.
+ *
+ * @var array of 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;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php
new file mode 100644
index 0000000..b5a3758
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Parser.php
@@ -0,0 +1,1483 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+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 ''
+ const REGEX_SHEET_TITLE_QUOTED = '(([^\*\:\/\\\\\?\[\]\\\'])+|(\\\'\\\')+)+';
+
+ /**
+ * The index of the character we are currently looking at.
+ *
+ * @var int
+ */
+ public $currentCharacter;
+
+ /**
+ * The token we are working on.
+ *
+ * @var string
+ */
+ public $currentToken;
+
+ /**
+ * The formula to parse.
+ *
+ * @var string
+ */
+ private $formula;
+
+ /**
+ * The character ahead of the current char.
+ *
+ * @var string
+ */
+ public $lookAhead;
+
+ /**
+ * The parse tree to be generated.
+ *
+ * @var string
+ */
+ public $parseTree;
+
+ /**
+ * Array of external sheets.
+ *
+ * @var array
+ */
+ private $externalSheets;
+
+ /**
+ * Array of sheet references in the form of REF structures.
+ *
+ * @var array
+ */
+ public $references;
+
+ /**
+ * The Excel ptg indices.
+ *
+ * @var array
+ */
+ private $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.
+ *
+ * @var array
+ */
+ private $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;
+
+ /**
+ * 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 mixed $token the token to convert
+ *
+ * @return mixed the converted token on success
+ */
+ private function convert($token)
+ {
+ if (preg_match('/"([^"]|""){0,255}"/', $token)) {
+ return $this->convertString($token);
+ } elseif (is_numeric($token)) {
+ return $this->convertNumber($token);
+ // match references like A1 or $A$1
+ } elseif (preg_match('/^\$?([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
+ } elseif (preg_match('/^' . 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
+ } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) {
+ return $this->convertRef3d($token);
+ // match ranges like A1:B2 or $A$1:$B$2
+ } elseif (preg_match('/^(\$)?[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
+ } elseif (preg_match('/^' . 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
+ } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)\\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)$/u", $token)) {
+ return $this->convertRange3d($token);
+ // operators (including parentheses)
+ } elseif (isset($this->ptg[$token])) {
+ return pack('C', $this->ptg[$token]);
+ // match error codes
+ } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') {
+ return $this->convertError($token);
+ } elseif (preg_match('/^' . 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().
+ /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token))
+ {
+ return($this->convertFunction($token, $this->_func_args));
+ }*/
+ // if it's an argument, ignore the token (the argument remains)
+ } elseif ($token == 'arg') {
+ return '';
+ }
+
+ // TODO: use real error codes
+ throw new WriterException("Unknown token $token");
+ }
+
+ /**
+ * Convert a number token to ptgInt or ptgNum.
+ *
+ * @param mixed $num an integer or double for conversion to its ptg value
+ *
+ * @return string
+ */
+ private function convertNumber($num)
+ {
+ // Integer in the range 0..2**16-1
+ if ((preg_match('/^\\d+$/', $num)) && ($num <= 65535)) {
+ return pack('Cv', $this->ptg['ptgInt'], $num);
+ }
+
+ // A float
+ if (BIFFwriter::getByteOrder()) { // if it's Big Endian
+ $num = strrev($num);
+ }
+
+ return pack('Cd', $this->ptg['ptgNum'], $num);
+ }
+
+ /**
+ * Convert a string token to ptgStr.
+ *
+ * @param string $string a string for conversion to its ptg value
+ *
+ * @return mixed the converted token on success
+ */
+ private function convertString($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($token, $num_args)
+ {
+ $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, ..).
+ if ($args == -1) {
+ 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
+ * @param int $class
+ *
+ * @return string
+ */
+ private function convertRange2d($range, $class = 0)
+ {
+ // TODO: possible class value 0,1,2 check Formula.pm
+ // Split the range into 2 cell refs
+ if (preg_match('/^(\$)?([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 mixed the packed ptgArea3d token on success
+ */
+ private function convertRange3d($token)
+ {
+ // 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_match('/^(\$)?[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($cell)
+ {
+ // 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 mixed the packed ptgRef3d token on success
+ */
+ private function convertRef3d($cell)
+ {
+ // 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 function convertDefinedName(string $name): void
+ {
+ if (strlen($name) > 255) {
+ throw new WriterException('Defined Name is too long');
+ }
+
+ $nameReference = 1;
+ foreach ($this->spreadsheet->getDefinedNames() as $definedName) {
+ if ($name === $definedName->getName()) {
+ break;
+ }
+ ++$nameReference;
+ }
+
+ $ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference);
+
+ throw new WriterException('Cannot yet write formulae with defined names to Xls');
+// return $ptgRef;
+ }
+
+ /**
+ * 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 mixed The reference index in packed() format on success
+ */
+ private function getRefIndex($ext_ref)
+ {
+ $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading ' if any.
+ $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any.
+ $ext_ref = str_replace('\'\'', '\'', $ext_ref); // Replace escaped '' with '
+
+ // Check if there is a sheet range eg., Sheet1:Sheet2.
+ if (preg_match('/:/', $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($sheet_name)
+ {
+ 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.
+ *
+ * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet()
+ *
+ * @param string $name The name of the worksheet being added
+ * @param int $index The index of the worksheet being added
+ */
+ public function setExtSheet($name, $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($cell)
+ {
+ $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($range)
+ {
+ preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match);
+ // 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
+ *
+ * @return array
+ */
+ private function cellToRowcol($cell)
+ {
+ preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/', $cell, $match);
+ // 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()
+ {
+ $i = $this->currentCharacter;
+ $formula_length = strlen($this->formula);
+ // eat up white spaces
+ if ($i < $formula_length) {
+ while ($this->formula[$i] == ' ') {
+ ++$i;
+ }
+
+ if ($i < ($formula_length - 1)) {
+ $this->lookAhead = $this->formula[$i + 1];
+ }
+ $token = '';
+ }
+
+ while ($i < $formula_length) {
+ $token .= $this->formula[$i];
+
+ if ($i < ($formula_length - 1)) {
+ $this->lookAhead = $this->formula[$i + 1];
+ } else {
+ $this->lookAhead = '';
+ }
+
+ if ($this->match($token) != '') {
+ $this->currentCharacter = $i + 1;
+ $this->currentToken = $token;
+
+ return 1;
+ }
+
+ if ($i < ($formula_length - 2)) {
+ $this->lookAhead = $this->formula[$i + 2];
+ } else { // if we run out of characters lookAhead becomes empty
+ $this->lookAhead = '';
+ }
+ ++$i;
+ }
+ //die("Lexical error ".$this->currentCharacter);
+ }
+
+ /**
+ * Checks if it's a valid token.
+ *
+ * @param mixed $token the token to check
+ *
+ * @return mixed The checked token or false on failure
+ */
+ private function match($token)
+ {
+ switch ($token) {
+ case '+':
+ case '-':
+ case '*':
+ case '/':
+ case '(':
+ case ')':
+ case ',':
+ case ';':
+ case '>=':
+ case '<=':
+ case '=':
+ case '<>':
+ case '^':
+ case '&':
+ case '%':
+ return $token;
+
+ break;
+ case '>':
+ if ($this->lookAhead === '=') { // it's a GE token
+ break;
+ }
+
+ return $token;
+
+ break;
+ case '<':
+ // it's a LE or a NE token
+ if (($this->lookAhead === '=') || ($this->lookAhead === '>')) {
+ break;
+ }
+
+ return $token;
+
+ break;
+ default:
+ // if it's a reference A1 or $A$1 or $A1 or A$1
+ if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) {
+ return $token;
+ } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) {
+ // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1)
+ return $token;
+ } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) {
+ // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1)
+ return $token;
+ } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) {
+ // if it's a range A1:A2 or $A$1:$A$2
+ return $token;
+ } elseif (preg_match('/^' . 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_match('/\d/', $this->lookAhead)) {
+ // 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
+ return $token;
+ } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead)) {
+ // 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
+ return $token;
+ } elseif (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) {
+ // If it's a number (check that it's not a sheet name or range)
+ return $token;
+ } elseif (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) {
+ // If it's a string (of maximum 255 characters)
+ return $token;
+ } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') {
+ // If it's an error code
+ return $token;
+ } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) {
+ // if it's a function call
+ return $token;
+ } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) {
+ return $token;
+ } elseif (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 mixed true on success
+ */
+ public function parse($formula)
+ {
+ $this->currentCharacter = 0;
+ $this->formula = (string) $formula;
+ $this->lookAhead = $formula[1] ?? '';
+ $this->advance();
+ $this->parseTree = $this->condition();
+
+ return true;
+ }
+
+ /**
+ * It parses a condition. It assumes the following rule:
+ * Cond -> Expr [(">" | "<") Expr].
+ *
+ * @return mixed The parsed ptg'd tree on success
+ */
+ private function condition()
+ {
+ $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);
+ } elseif ($this->currentToken == '&') {
+ $this->advance();
+ $result2 = $this->expression();
+ $result = $this->createTree('ptgConcat', $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 mixed The parsed ptg'd tree on success
+ */
+ private function expression()
+ {
+ // If it's a string return a string node
+ if (preg_match('/"([^"]|""){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 it's an error code
+ } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $this->currentToken) || $this->currentToken == '#N/A') {
+ $result = $this->createTree($this->currentToken, 'ptgErr', '');
+ $this->advance();
+
+ return $result;
+ // If it's a negative value
+ } elseif ($this->currentToken == '-') {
+ // catch "-" Term
+ $this->advance();
+ $result2 = $this->expression();
+
+ return $this->createTree('ptgUminus', $result2, '');
+ // If it's a positive value
+ } elseif ($this->currentToken == '+') {
+ // catch "+" Term
+ $this->advance();
+ $result2 = $this->expression();
+
+ return $this->createTree('ptgUplus', $result2, '');
+ }
+ $result = $this->term();
+ 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.
+ *
+ * @see fact()
+ *
+ * @return array The parsed ptg'd tree
+ */
+ private function parenthesizedExpression()
+ {
+ return $this->createTree('ptgParen', $this->expression(), '');
+ }
+
+ /**
+ * It parses a term. It assumes the following rule:
+ * Term -> Fact [("*" | "/") Fact].
+ *
+ * @return mixed The parsed ptg'd tree on success
+ */
+ private function term()
+ {
+ $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 mixed The parsed ptg'd tree on success
+ */
+ private function fact()
+ {
+ if ($this->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_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) {
+ $result = $this->createTree($this->currentToken, '', '');
+ $this->advance();
+
+ return $result;
+ } elseif (preg_match('/^' . 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;
+ } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[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;
+ } elseif (
+ preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) ||
+ preg_match('/^(\$)?[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;
+ } elseif (preg_match('/^' . 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;
+ } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([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'!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;
+ } elseif (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;
+ } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) {
+ // if it's a function call
+ return $this->func();
+ } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) {
+ $result = $this->createTree('ptgName', $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 mixed The parsed ptg'd tree on success
+ */
+ private function func()
+ {
+ $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)
+ {
+ 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($tree = [])
+ {
+ $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['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_match("/^[A-Z0-9\xc0-\xdc\\.]+$/", $tree['value']) &&
+ !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/', $tree['value']) &&
+ !preg_match('/^[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 it's left subtree and return.
+ return $left_tree . $this->convertFunction($tree['value'], $tree['right']);
+ }
+ $converted_tree = $this->convert($tree['value']);
+
+ return $polish . $converted_tree;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php
new file mode 100644
index 0000000..9b6f6de
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php
@@ -0,0 +1,1191 @@
+<?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.
+ *
+ * @var int
+ *
+ * @see calcSheetOffsets()
+ */
+ private $biffSize;
+
+ /**
+ * 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.
+ */
+ private $colors;
+
+ /**
+ * Escher object corresponding to MSODRAWINGGROUP.
+ *
+ * @var \PhpOffice\PhpSpreadsheet\Shared\Escher
+ */
+ private $escher;
+
+ /**
+ * 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() === false) {
+ $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 $pWorksheetSizes The sizes in bytes of the binary worksheet streams
+ *
+ * @return string Binary data for workbook stream
+ */
+ public function writeWorkbook(array $pWorksheetSizes)
+ {
+ $this->worksheetSizes = $pWorksheetSizes;
+
+ // 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 $pDefinedName): string
+ {
+ $definedRange = $pDefinedName->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 need a worksheet
+ $worksheet = $pDefinedName->getWorksheet()->getTitle();
+ }
+ } 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()
+ {
+ $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 {
+ $error = $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
+ $scope = $this->spreadsheet->getIndex($definedName->getScope()) + 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));
+
+ // (exclusive) either repeatColumns or repeatRows
+ } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) {
+ // 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::coordinateFromString($printAreaRect[0]);
+ $printAreaRect[1] = Coordinate::coordinateFromString($printAreaRect[1]);
+
+ $print_rowmin = $printAreaRect[0][1] - 1;
+ $print_rowmax = $printAreaRect[1][1] - 1;
+ $print_colmin = Coordinate::columnIndexFromString($printAreaRect[0][0]) - 1;
+ $print_colmax = Coordinate::columnIndexFromString($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 string $sheetIndex 1-based sheet index the defined name applies to. 0 = global
+ * @param integer[][] $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 Worksheet $sheet Worksheet name
+ * @param int $offset Location of worksheet BOF
+ */
+ private function writeBoundSheet($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()
+ {
+ $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()
+ {
+ $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'];
+
+ // 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
+
+ // 1. space remaining is less than minimum space needed
+ if ($space_remaining < $min_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 = '';
+
+ // 2. space remaining is greater than or equal to minimum space needed
+ } else {
+ // 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()
+ {
+ // 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.
+ *
+ * @return \PhpOffice\PhpSpreadsheet\Shared\Escher
+ */
+ public function getEscher()
+ {
+ return $this->escher;
+ }
+
+ /**
+ * Set Escher object.
+ *
+ * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue
+ */
+ public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null): void
+ {
+ $this->escher = $pValue;
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Worksheet.php
new file mode 100644
index 0000000..a5680e3
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Worksheet.php
@@ -0,0 +1,4490 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
+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\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Color;
+use PhpOffice\PhpSpreadsheet\Style\Conditional;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+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
+{
+ /**
+ * Formula parser.
+ *
+ * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
+ */
+ private $parser;
+
+ /**
+ * Maximum number of characters for a string (LABEL record in BIFF5).
+ *
+ * @var int
+ */
+ private $xlsStringMaxLength;
+
+ /**
+ * Array containing format information for columns.
+ *
+ * @var array
+ */
+ private $columnInfo;
+
+ /**
+ * Array containing the selected area for the worksheet.
+ *
+ * @var array
+ */
+ private $selection;
+
+ /**
+ * The active pane for the worksheet.
+ *
+ * @var int
+ */
+ private $activePane;
+
+ /**
+ * Whether to use outline.
+ *
+ * @var int
+ */
+ private $outlineOn;
+
+ /**
+ * Auto outline styles.
+ *
+ * @var bool
+ */
+ private $outlineStyle;
+
+ /**
+ * Whether to have outline summary below.
+ *
+ * @var bool
+ */
+ private $outlineBelow;
+
+ /**
+ * Whether to have outline summary at the right.
+ *
+ * @var bool
+ */
+ private $outlineRight;
+
+ /**
+ * 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.
+ */
+ 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;
+
+ /**
+ * Count cell style Xfs.
+ *
+ * @var int
+ */
+ private $countCellStyleXfs;
+
+ /**
+ * Escher object corresponding to MSODRAWING.
+ *
+ * @var \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->xlsStringMaxLength = 255;
+ $this->columnInfo = [];
+ $this->selection = [0, 0, 0, 0];
+ $this->activePane = 3;
+
+ $this->printHeaders = 0;
+
+ $this->outlineStyle = 0;
+ $this->outlineBelow = 1;
+ $this->outlineRight = 1;
+ $this->outlineOn = 1;
+
+ $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->lastRowIndex = ($maxR > 65535) ? 65535 : $maxR;
+
+ $this->firstColumnIndex = Coordinate::columnIndexFromString($minC);
+ $this->lastColumnIndex = Coordinate::columnIndexFromString($maxC);
+
+// if ($this->firstColumnIndex > 255) $this->firstColumnIndex = 255;
+ if ($this->lastColumnIndex > 255) {
+ $this->lastColumnIndex = 255;
+ }
+
+ $this->countCellStyleXfs = count($phpSheet->getParent()->getCellStyleXfCollection());
+ }
+
+ /**
+ * 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->getParent()->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->getParent()->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, $rowDimension->getRowHeight(), $xfIndex, ($rowDimension->getVisible() ? '0' : '1'), $rowDimension->getOutlineLevel());
+ }
+
+ // Write Cells
+ foreach ($phpSheet->getCoordinates() as $coordinate) {
+ $cell = $phpSheet->getCell($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_len = StringHelper::countCharacters($cVal->getPlainText(), 'UTF-8');
+ $str_pos = 0;
+ $elements = $cVal->getRichTextElements();
+ foreach ($elements as $element) {
+ // FONT Index
+ if ($element instanceof Run) {
+ $str_fontidx = $this->fontHashIndex[$element->getFont()->getHashCode()];
+ } else {
+ $str_fontidx = 0;
+ }
+ $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_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, $calculatedValue, $xfIndex);
+
+ break;
+ case 'string':
+ $this->writeString($row, $column, $calculatedValue, $xfIndex);
+
+ break;
+ case 'boolean':
+ $this->writeBoolErr($row, $column, $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, self::mapErrorCode($cVal), 1, $xfIndex);
+
+ break;
+ }
+ }
+ }
+
+ // Append
+ $this->writeMsoDrawing();
+
+ // Restoring active sheet.
+ $this->phpSheet->getParent()->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
+ foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) {
+ [$column, $row] = Coordinate::coordinateFromString($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
+ } else {
+ // external (local file)
+ $url = 'external:' . $url;
+ }
+
+ $this->writeUrl($row - 1, Coordinate::columnIndexFromString($column) - 1, $url);
+ }
+
+ $this->writeDataValidity();
+ $this->writeSheetLayout();
+
+ // Write SHEETPROTECTION record
+ $this->writeSheetProtection();
+ $this->writeRangeProtection();
+
+ $arrConditionalStyles = $phpSheet->getConditionalStylesCollection();
+ if (!empty($arrConditionalStyles)) {
+ $arrConditional = [];
+ // @TODO CFRule & CFHeader
+ // Write CFHEADER record
+ $this->writeCFHeader();
+ // Write ConditionalFormattingTable records
+ foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) {
+ foreach ($conditionalStyles as $conditional) {
+ if (
+ $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION
+ || $conditional->getConditionType() == Conditional::CONDITION_CELLIS
+ ) {
+ if (!isset($arrConditional[$conditional->getHashCode()])) {
+ // This hash code has been handled
+ $arrConditional[$conditional->getHashCode()] = true;
+
+ // Write CFRULE record
+ $this->writeCFRule($conditional);
+ }
+ }
+ }
+ }
+ }
+
+ $this->storeEof();
+ }
+
+ /**
+ * 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::coordinateFromString($firstCell); // e.g. [0, 1]
+ $lastCellCoordinates = Coordinate::coordinateFromString($lastCell); // e.g. [1, 6]
+
+ return pack('vvvv', $firstCellCoordinates[1] - 1, $lastCellCoordinates[1] - 1, Coordinate::columnIndexFromString($firstCellCoordinates[0]) - 1, Coordinate::columnIndexFromString($lastCellCoordinates[0]) - 1);
+ }
+
+ /**
+ * Retrieves data from memory in one chunk, or from disk in $buffer
+ * sized chunks.
+ *
+ * @return string The data
+ */
+ public function getData()
+ {
+ $buffer = 4096;
+
+ // Return data stored in memory
+ if (isset($this->_data)) {
+ $tmp = $this->_data;
+ $this->_data = null;
+
+ return $tmp;
+ }
+
+ // No data to return
+ return false;
+ }
+
+ /**
+ * 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;
+
+ // Ensure this is a boolean vale for Window2
+ if ($this->outlineOn) {
+ $this->outlineOn = 1;
+ }
+ }
+
+ /**
+ * 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 bool $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;
+
+ /**
+ * 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, self::mapErrorCode($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 {
+ $error = $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) {
+ 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.
+ *
+ * Returns 0 : normal termination
+ * -2 : row or column out of range
+ * -3 : long string truncated to 255 chars
+ *
+ * @param int $row Row
+ * @param int $col Column
+ * @param string $url URL string
+ *
+ * @return int
+ */
+ private function writeUrl($row, $col, $url)
+ {
+ // Add start row and col to arg list
+ return $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
+ *
+ * @return int
+ *
+ * @see writeUrl()
+ */
+ public function writeUrlRange($row1, $col1, $row2, $col2, $url)
+ {
+ // Check for internal/external sheet links or default to web link
+ if (preg_match('[^internal:]', $url)) {
+ return $this->writeUrlInternal($row1, $col1, $row2, $col2, $url);
+ }
+ if (preg_match('[^external:]', $url)) {
+ return $this->writeUrlExternal($row1, $col1, $row2, $col2, $url);
+ }
+
+ return $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
+ *
+ * @return int
+ *
+ * @see writeUrl()
+ */
+ public function writeUrlWeb($row1, $col1, $row2, $col2, $url)
+ {
+ $record = 0x01B8; // Record identifier
+ $length = 0x00000; // Bytes to follow
+
+ // 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
+ $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);
+
+ return 0;
+ }
+
+ /**
+ * 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
+ *
+ * @return int
+ *
+ * @see writeUrl()
+ */
+ public function writeUrlInternal($row1, $col1, $row2, $col2, $url)
+ {
+ $record = 0x01B8; // Record identifier
+ $length = 0x00000; // Bytes to follow
+
+ // Strip URL type
+ $url = 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);
+
+ return 0;
+ }
+
+ /**
+ * 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
+ *
+ * @return int
+ *
+ * @see writeUrl()
+ */
+ public function writeUrlExternal($row1, $col1, $row2, $col2, $url)
+ {
+ // Network drives are different. We will handle them separately
+ // MS/Novell network drives and shares start with \\
+ if (preg_match('[^external:\\\\]', $url)) {
+ return; //($this->writeUrlExternal_net($row1, $col1, $row2, $col2, $url, $str, $format));
+ }
+
+ $record = 0x01B8; // Record identifier
+ $length = 0x00000; // Bytes to follow
+
+ // Strip URL type and change Unix dir separator to Dos style (if needed)
+ //
+ $url = preg_replace('/^external:/', '', $url);
+ $url = preg_replace('/\//', '\\', $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 = 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);
+
+ return 0;
+ }
+
+ /**
+ * 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) {
+ $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;
+
+ $grbit = 0x00B6; // Option flags
+ $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->getParent()->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
+ {
+ if (isset($col_array[0])) {
+ $colFirst = $col_array[0];
+ }
+ if (isset($col_array[1])) {
+ $colLast = $col_array[1];
+ }
+ if (isset($col_array[2])) {
+ $coldx = $col_array[2];
+ } else {
+ $coldx = 8.43;
+ }
+ if (isset($col_array[3])) {
+ $xfIndex = $col_array[3];
+ } else {
+ $xfIndex = 15;
+ }
+ if (isset($col_array[4])) {
+ $grbit = $col_array[4];
+ } else {
+ $grbit = 0;
+ }
+ if (isset($col_array[5])) {
+ $level = $col_array[5];
+ } else {
+ $level = 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
+
+ if (!isset($rwLast)) {
+ $rwLast = $rwFirst; // Last row in reference
+ }
+ if (!isset($colLast)) {
+ $colLast = $colFirst; // Last col in reference
+ }
+
+ // 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::coordinateFromString($first);
+ [$lastColumn, $lastRow] = Coordinate::coordinateFromString($last);
+
+ $recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, Coordinate::columnIndexFromString($firstColumn) - 1, Coordinate::columnIndexFromString($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);
+ }
+
+ /**
+ * Write SHEETPROTECTION.
+ */
+ private function writeSheetProtection(): void
+ {
+ // record identifier
+ $record = 0x0867;
+
+ // prepare options
+ $options = (int) !$this->phpSheet->getProtection()->getObjects()
+ | (int) !$this->phpSheet->getProtection()->getScenarios() << 1
+ | (int) !$this->phpSheet->getProtection()->getFormatCells() << 2
+ | (int) !$this->phpSheet->getProtection()->getFormatColumns() << 3
+ | (int) !$this->phpSheet->getProtection()->getFormatRows() << 4
+ | (int) !$this->phpSheet->getProtection()->getInsertColumns() << 5
+ | (int) !$this->phpSheet->getProtection()->getInsertRows() << 6
+ | (int) !$this->phpSheet->getProtection()->getInsertHyperlinks() << 7
+ | (int) !$this->phpSheet->getProtection()->getDeleteColumns() << 8
+ | (int) !$this->phpSheet->getProtection()->getDeleteRows() << 9
+ | (int) !$this->phpSheet->getProtection()->getSelectLockedCells() << 10
+ | (int) !$this->phpSheet->getProtection()->getSort() << 11
+ | (int) !$this->phpSheet->getProtection()->getAutoFilter() << 12
+ | (int) !$this->phpSheet->getProtection()->getPivotTables() << 13
+ | (int) !$this->phpSheet->getProtection()->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 Documentaion 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
+ {
+ $panes = [];
+ if ($this->phpSheet->getFreezePane()) {
+ [$column, $row] = Coordinate::coordinateFromString($this->phpSheet->getFreezePane());
+ $panes[0] = Coordinate::columnIndexFromString($column) - 1;
+ $panes[1] = $row - 1;
+
+ [$leftMostColumn, $topRow] = Coordinate::coordinateFromString($this->phpSheet->getTopLeftCell());
+ //Coordinates are zero-based in xls files
+ $panes[2] = $topRow - 1;
+ $panes[3] = Coordinate::columnIndexFromString($leftMostColumn) - 1;
+ } else {
+ // thaw panes
+ return;
+ }
+
+ $x = $panes[0] ?? null;
+ $y = $panes[1] ?? null;
+ $rwTop = $panes[2] ?? null;
+ $colLeft = $panes[3] ?? null;
+ if (count($panes) > 4) { // if Active pane was received
+ $pnnAct = $panes[4];
+ } else {
+ $pnnAct = null;
+ }
+ $record = 0x0041; // Record identifier
+ $length = 0x000A; // Number of bytes to follow
+
+ // Code specific to frozen or thawed panes.
+ if ($this->phpSheet->getFreezePane()) {
+ // Set default values for $rwTop and $colLeft
+ if (!isset($rwTop)) {
+ $rwTop = $y;
+ }
+ if (!isset($colLeft)) {
+ $colLeft = $x;
+ }
+ } else {
+ // Set default values for $rwTop and $colLeft
+ if (!isset($rwTop)) {
+ $rwTop = 0;
+ }
+ if (!isset($colLeft)) {
+ $colLeft = 0;
+ }
+
+ // Convert Excel's row and column units to the internal units.
+ // The default row height is 12.75
+ // The default column width is 8.43
+ // The following slope and intersection values were interpolated.
+ //
+ $y = 20 * $y + 255;
+ $x = 113.879 * $x + 390;
+ }
+
+ // Determine which pane should be active. There is also the undocumented
+ // option to override this should it be necessary: may be removed later.
+ //
+ if (!isset($pnnAct)) {
+ 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() ?
+ $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
+ $grbit = 0x00; // Option flags
+ $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
+ ? 0x1 : 0x0;
+ // 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->getBreaks() as $cell => $breakType) {
+ // Fetch coordinates
+ $coordinates = Coordinate::coordinateFromString($cell);
+
+ // Decide what to do by the type of break
+ switch ($breakType) {
+ case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_COLUMN:
+ // Add to list of vertical breaks
+ $vbreaks[] = Coordinate::columnIndexFromString($coordinates[0]) - 1;
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_ROW:
+ // Add to list of horizontal breaks
+ $hbreaks[] = $coordinates[1];
+
+ break;
+ case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::BREAK_NONE:
+ default:
+ // Nothing to do
+ break;
+ }
+ }
+
+ //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()) {
+ 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()) {
+ return;
+ }
+
+ // Exit if scenarios are not protected
+ if (!$this->phpSheet->getProtection()->getScenarios()) {
+ 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()) {
+ return;
+ }
+
+ // Exit if objects are not protected
+ if (!$this->phpSheet->getProtection()->getObjects()) {
+ 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() || !$this->phpSheet->getProtection()->getPassword()) {
+ 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) ? $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, $width, $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 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) {
+ $color = imagecolorsforindex($image, imagecolorat($image, $i, $j));
+ foreach (['red', 'green', 'blue'] as $key) {
+ $color[$key] = $color[$key] + 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) {
+ throw new WriterException("Couldn't import $bitmap");
+ }
+
+ // Slurp the file into a string.
+ $data = fread($bmp_fd, 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['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[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['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.
+ *
+ * @return \PhpOffice\PhpSpreadsheet\Shared\Escher
+ */
+ public function getEscher()
+ {
+ return $this->escher;
+ }
+
+ /**
+ * Set Escher object.
+ *
+ * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue
+ */
+ public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null): void
+ {
+ $this->escher = $pValue;
+ }
+
+ /**
+ * 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) {
+ // initialize record data
+ $data = '';
+
+ // options
+ $options = 0x00000000;
+
+ // data type
+ $type = 0x00;
+ switch ($dataValidation->getType()) {
+ case DataValidation::TYPE_NONE:
+ $type = 0x00;
+
+ break;
+ case DataValidation::TYPE_WHOLE:
+ $type = 0x01;
+
+ break;
+ case DataValidation::TYPE_DECIMAL:
+ $type = 0x02;
+
+ break;
+ case DataValidation::TYPE_LIST:
+ $type = 0x03;
+
+ break;
+ case DataValidation::TYPE_DATE:
+ $type = 0x04;
+
+ break;
+ case DataValidation::TYPE_TIME:
+ $type = 0x05;
+
+ break;
+ case DataValidation::TYPE_TEXTLENGTH:
+ $type = 0x06;
+
+ break;
+ case DataValidation::TYPE_CUSTOM:
+ $type = 0x07;
+
+ break;
+ }
+
+ $options |= $type << 0;
+
+ // error style
+ $errorStyle = 0x00;
+ switch ($dataValidation->getErrorStyle()) {
+ case DataValidation::STYLE_STOP:
+ $errorStyle = 0x00;
+
+ break;
+ case DataValidation::STYLE_WARNING:
+ $errorStyle = 0x01;
+
+ break;
+ case DataValidation::STYLE_INFORMATION:
+ $errorStyle = 0x02;
+
+ break;
+ }
+
+ $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 = 0x00;
+ switch ($dataValidation->getOperator()) {
+ case DataValidation::OPERATOR_BETWEEN:
+ $operator = 0x00;
+
+ break;
+ case DataValidation::OPERATOR_NOTBETWEEN:
+ $operator = 0x01;
+
+ break;
+ case DataValidation::OPERATOR_EQUAL:
+ $operator = 0x02;
+
+ break;
+ case DataValidation::OPERATOR_NOTEQUAL:
+ $operator = 0x03;
+
+ break;
+ case DataValidation::OPERATOR_GREATERTHAN:
+ $operator = 0x04;
+
+ break;
+ case DataValidation::OPERATOR_LESSTHAN:
+ $operator = 0x05;
+
+ break;
+ case DataValidation::OPERATOR_GREATERTHANOREQUAL:
+ $operator = 0x06;
+
+ break;
+ case DataValidation::OPERATOR_LESSTHANOREQUAL:
+ $operator = 0x07;
+
+ break;
+ }
+
+ $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);
+ }
+ }
+ }
+
+ /**
+ * Map Error code.
+ *
+ * @param string $errorCode
+ *
+ * @return int
+ */
+ private static function mapErrorCode($errorCode)
+ {
+ switch ($errorCode) {
+ case '#NULL!':
+ return 0x00;
+ case '#DIV/0!':
+ return 0x07;
+ case '#VALUE!':
+ return 0x0F;
+ case '#REF!':
+ return 0x17;
+ case '#NAME?':
+ return 0x1D;
+ case '#NUM!':
+ return 0x24;
+ case '#N/A':
+ return 0x2A;
+ }
+
+ return 0;
+ }
+
+ /**
+ * 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(Conditional $conditional): void
+ {
+ $record = 0x01B1; // Record identifier
+
+ // $type : Type of the CF
+ // $operatorType : 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);
+ if ($numConditions == 1) {
+ $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000);
+ $szValue2 = 0x0000;
+ $operand1 = pack('Cv', 0x1E, $arrConditions[0]);
+ $operand2 = null;
+ } elseif ($numConditions == 2 && ($conditional->getOperatorType() == Conditional::OPERATOR_BETWEEN)) {
+ $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000);
+ $szValue2 = ($arrConditions[1] <= 65535 ? 3 : 0x0000);
+ $operand1 = pack('Cv', 0x1E, $arrConditions[0]);
+ $operand2 = pack('Cv', 0x1E, $arrConditions[1]);
+ } else {
+ $szValue1 = 0x0000;
+ $szValue2 = 0x0000;
+ $operand1 = null;
+ $operand2 = null;
+ }
+
+ // $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()->getColor()->getARGB() == Color::COLOR_BLACK
+ && $conditional->getStyle()->getBorders()->getLeft()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
+ $bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getColor()->getARGB() == Color::COLOR_BLACK
+ && $conditional->getStyle()->getBorders()->getRight()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
+ $bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getColor()->getARGB() == Color::COLOR_BLACK
+ && $conditional->getStyle()->getBorders()->getTop()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
+ $bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getColor()->getARGB() == Color::COLOR_BLACK
+ && $conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
+ if ($bBorderLeft == 0 || $bBorderRight == 0 || $bBorderTop == 0 || $bBorderBottom == 0) {
+ $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 == 0 || $bFillColor == 0 || $bFillColorBg == 0) {
+ $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 == 1 ? 0x00000010 : 0);
+ $flags |= (1 == $bIndent ? 0x00000020 : 0);
+ $flags |= (1 == $bShrinkToFit ? 0x00000040 : 0);
+ // Default
+ $flags |= (1 == 1 ? 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 == 1 ? 0x00004000 : 0); // Top left to Bottom right border
+ $flags |= (1 == 1 ? 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 == 1 ? 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 == 0 ? 0x80000000 : 0);
+
+ // 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
+ switch ($conditional->getStyle()->getFont()->getColor()->getRGB()) {
+ case '000000':
+ $colorIdx = 0x08;
+
+ break;
+ case 'FFFFFF':
+ $colorIdx = 0x09;
+
+ break;
+ case 'FF0000':
+ $colorIdx = 0x0A;
+
+ break;
+ case '00FF00':
+ $colorIdx = 0x0B;
+
+ break;
+ case '0000FF':
+ $colorIdx = 0x0C;
+
+ break;
+ case 'FFFF00':
+ $colorIdx = 0x0D;
+
+ break;
+ case 'FF00FF':
+ $colorIdx = 0x0E;
+
+ break;
+ case '00FFFF':
+ $colorIdx = 0x0F;
+
+ break;
+ case '800000':
+ $colorIdx = 0x10;
+
+ break;
+ case '008000':
+ $colorIdx = 0x11;
+
+ break;
+ case '000080':
+ $colorIdx = 0x12;
+
+ break;
+ case '808000':
+ $colorIdx = 0x13;
+
+ break;
+ case '800080':
+ $colorIdx = 0x14;
+
+ break;
+ case '008080':
+ $colorIdx = 0x15;
+
+ break;
+ case 'C0C0C0':
+ $colorIdx = 0x16;
+
+ break;
+ case '808080':
+ $colorIdx = 0x17;
+
+ break;
+ case '9999FF':
+ $colorIdx = 0x18;
+
+ break;
+ case '993366':
+ $colorIdx = 0x19;
+
+ break;
+ case 'FFFFCC':
+ $colorIdx = 0x1A;
+
+ break;
+ case 'CCFFFF':
+ $colorIdx = 0x1B;
+
+ break;
+ case '660066':
+ $colorIdx = 0x1C;
+
+ break;
+ case 'FF8080':
+ $colorIdx = 0x1D;
+
+ break;
+ case '0066CC':
+ $colorIdx = 0x1E;
+
+ break;
+ case 'CCCCFF':
+ $colorIdx = 0x1F;
+
+ break;
+ case '000080':
+ $colorIdx = 0x20;
+
+ break;
+ case 'FF00FF':
+ $colorIdx = 0x21;
+
+ break;
+ case 'FFFF00':
+ $colorIdx = 0x22;
+
+ break;
+ case '00FFFF':
+ $colorIdx = 0x23;
+
+ break;
+ case '800080':
+ $colorIdx = 0x24;
+
+ break;
+ case '800000':
+ $colorIdx = 0x25;
+
+ break;
+ case '008080':
+ $colorIdx = 0x26;
+
+ break;
+ case '0000FF':
+ $colorIdx = 0x27;
+
+ break;
+ case '00CCFF':
+ $colorIdx = 0x28;
+
+ break;
+ case 'CCFFFF':
+ $colorIdx = 0x29;
+
+ break;
+ case 'CCFFCC':
+ $colorIdx = 0x2A;
+
+ break;
+ case 'FFFF99':
+ $colorIdx = 0x2B;
+
+ break;
+ case '99CCFF':
+ $colorIdx = 0x2C;
+
+ break;
+ case 'FF99CC':
+ $colorIdx = 0x2D;
+
+ break;
+ case 'CC99FF':
+ $colorIdx = 0x2E;
+
+ break;
+ case 'FFCC99':
+ $colorIdx = 0x2F;
+
+ break;
+ case '3366FF':
+ $colorIdx = 0x30;
+
+ break;
+ case '33CCCC':
+ $colorIdx = 0x31;
+
+ break;
+ case '99CC00':
+ $colorIdx = 0x32;
+
+ break;
+ case 'FFCC00':
+ $colorIdx = 0x33;
+
+ break;
+ case 'FF9900':
+ $colorIdx = 0x34;
+
+ break;
+ case 'FF6600':
+ $colorIdx = 0x35;
+
+ break;
+ case '666699':
+ $colorIdx = 0x36;
+
+ break;
+ case '969696':
+ $colorIdx = 0x37;
+
+ break;
+ case '003366':
+ $colorIdx = 0x38;
+
+ break;
+ case '339966':
+ $colorIdx = 0x39;
+
+ break;
+ case '003300':
+ $colorIdx = 0x3A;
+
+ break;
+ case '333300':
+ $colorIdx = 0x3B;
+
+ break;
+ case '993300':
+ $colorIdx = 0x3C;
+
+ break;
+ case '993366':
+ $colorIdx = 0x3D;
+
+ break;
+ case '333399':
+ $colorIdx = 0x3E;
+
+ break;
+ case '333333':
+ $colorIdx = 0x3F;
+
+ break;
+ default:
+ $colorIdx = 0x00;
+
+ break;
+ }
+ $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 == 1 ? 0x00000008 : 0);
+ $optionsFlags |= (1 == 1 ? 0x00000010 : 0);
+ $optionsFlags |= (1 == 0 ? 0x00000020 : 0);
+ $optionsFlags |= (1 == 1 ? 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) {
+ $blockAlign = 0;
+ // Alignment and text break
+ switch ($conditional->getStyle()->getAlignment()->getHorizontal()) {
+ case Alignment::HORIZONTAL_GENERAL:
+ $blockAlign = 0;
+
+ break;
+ case Alignment::HORIZONTAL_LEFT:
+ $blockAlign = 1;
+
+ break;
+ case Alignment::HORIZONTAL_RIGHT:
+ $blockAlign = 3;
+
+ break;
+ case Alignment::HORIZONTAL_CENTER:
+ $blockAlign = 2;
+
+ break;
+ case Alignment::HORIZONTAL_CENTER_CONTINUOUS:
+ $blockAlign = 6;
+
+ break;
+ case Alignment::HORIZONTAL_JUSTIFY:
+ $blockAlign = 5;
+
+ break;
+ }
+ if ($conditional->getStyle()->getAlignment()->getWrapText() == true) {
+ $blockAlign |= 1 << 3;
+ } else {
+ $blockAlign |= 0 << 3;
+ }
+ switch ($conditional->getStyle()->getAlignment()->getVertical()) {
+ case Alignment::VERTICAL_BOTTOM:
+ $blockAlign = 2 << 4;
+
+ break;
+ case Alignment::VERTICAL_TOP:
+ $blockAlign = 0 << 4;
+
+ break;
+ case Alignment::VERTICAL_CENTER:
+ $blockAlign = 1 << 4;
+
+ break;
+ case Alignment::VERTICAL_JUSTIFY:
+ $blockAlign = 3 << 4;
+
+ break;
+ }
+ $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 = 0;
+ switch ($conditional->getStyle()->getBorders()->getLeft()->getBorderStyle()) {
+ case Border::BORDER_NONE:
+ $blockLineStyle |= 0x00;
+
+ break;
+ case Border::BORDER_THIN:
+ $blockLineStyle |= 0x01;
+
+ break;
+ case Border::BORDER_MEDIUM:
+ $blockLineStyle |= 0x02;
+
+ break;
+ case Border::BORDER_DASHED:
+ $blockLineStyle |= 0x03;
+
+ break;
+ case Border::BORDER_DOTTED:
+ $blockLineStyle |= 0x04;
+
+ break;
+ case Border::BORDER_THICK:
+ $blockLineStyle |= 0x05;
+
+ break;
+ case Border::BORDER_DOUBLE:
+ $blockLineStyle |= 0x06;
+
+ break;
+ case Border::BORDER_HAIR:
+ $blockLineStyle |= 0x07;
+
+ break;
+ case Border::BORDER_MEDIUMDASHED:
+ $blockLineStyle |= 0x08;
+
+ break;
+ case Border::BORDER_DASHDOT:
+ $blockLineStyle |= 0x09;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOT:
+ $blockLineStyle |= 0x0A;
+
+ break;
+ case Border::BORDER_DASHDOTDOT:
+ $blockLineStyle |= 0x0B;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOTDOT:
+ $blockLineStyle |= 0x0C;
+
+ break;
+ case Border::BORDER_SLANTDASHDOT:
+ $blockLineStyle |= 0x0D;
+
+ break;
+ }
+ switch ($conditional->getStyle()->getBorders()->getRight()->getBorderStyle()) {
+ case Border::BORDER_NONE:
+ $blockLineStyle |= 0x00 << 4;
+
+ break;
+ case Border::BORDER_THIN:
+ $blockLineStyle |= 0x01 << 4;
+
+ break;
+ case Border::BORDER_MEDIUM:
+ $blockLineStyle |= 0x02 << 4;
+
+ break;
+ case Border::BORDER_DASHED:
+ $blockLineStyle |= 0x03 << 4;
+
+ break;
+ case Border::BORDER_DOTTED:
+ $blockLineStyle |= 0x04 << 4;
+
+ break;
+ case Border::BORDER_THICK:
+ $blockLineStyle |= 0x05 << 4;
+
+ break;
+ case Border::BORDER_DOUBLE:
+ $blockLineStyle |= 0x06 << 4;
+
+ break;
+ case Border::BORDER_HAIR:
+ $blockLineStyle |= 0x07 << 4;
+
+ break;
+ case Border::BORDER_MEDIUMDASHED:
+ $blockLineStyle |= 0x08 << 4;
+
+ break;
+ case Border::BORDER_DASHDOT:
+ $blockLineStyle |= 0x09 << 4;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOT:
+ $blockLineStyle |= 0x0A << 4;
+
+ break;
+ case Border::BORDER_DASHDOTDOT:
+ $blockLineStyle |= 0x0B << 4;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOTDOT:
+ $blockLineStyle |= 0x0C << 4;
+
+ break;
+ case Border::BORDER_SLANTDASHDOT:
+ $blockLineStyle |= 0x0D << 4;
+
+ break;
+ }
+ switch ($conditional->getStyle()->getBorders()->getTop()->getBorderStyle()) {
+ case Border::BORDER_NONE:
+ $blockLineStyle |= 0x00 << 8;
+
+ break;
+ case Border::BORDER_THIN:
+ $blockLineStyle |= 0x01 << 8;
+
+ break;
+ case Border::BORDER_MEDIUM:
+ $blockLineStyle |= 0x02 << 8;
+
+ break;
+ case Border::BORDER_DASHED:
+ $blockLineStyle |= 0x03 << 8;
+
+ break;
+ case Border::BORDER_DOTTED:
+ $blockLineStyle |= 0x04 << 8;
+
+ break;
+ case Border::BORDER_THICK:
+ $blockLineStyle |= 0x05 << 8;
+
+ break;
+ case Border::BORDER_DOUBLE:
+ $blockLineStyle |= 0x06 << 8;
+
+ break;
+ case Border::BORDER_HAIR:
+ $blockLineStyle |= 0x07 << 8;
+
+ break;
+ case Border::BORDER_MEDIUMDASHED:
+ $blockLineStyle |= 0x08 << 8;
+
+ break;
+ case Border::BORDER_DASHDOT:
+ $blockLineStyle |= 0x09 << 8;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOT:
+ $blockLineStyle |= 0x0A << 8;
+
+ break;
+ case Border::BORDER_DASHDOTDOT:
+ $blockLineStyle |= 0x0B << 8;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOTDOT:
+ $blockLineStyle |= 0x0C << 8;
+
+ break;
+ case Border::BORDER_SLANTDASHDOT:
+ $blockLineStyle |= 0x0D << 8;
+
+ break;
+ }
+ switch ($conditional->getStyle()->getBorders()->getBottom()->getBorderStyle()) {
+ case Border::BORDER_NONE:
+ $blockLineStyle |= 0x00 << 12;
+
+ break;
+ case Border::BORDER_THIN:
+ $blockLineStyle |= 0x01 << 12;
+
+ break;
+ case Border::BORDER_MEDIUM:
+ $blockLineStyle |= 0x02 << 12;
+
+ break;
+ case Border::BORDER_DASHED:
+ $blockLineStyle |= 0x03 << 12;
+
+ break;
+ case Border::BORDER_DOTTED:
+ $blockLineStyle |= 0x04 << 12;
+
+ break;
+ case Border::BORDER_THICK:
+ $blockLineStyle |= 0x05 << 12;
+
+ break;
+ case Border::BORDER_DOUBLE:
+ $blockLineStyle |= 0x06 << 12;
+
+ break;
+ case Border::BORDER_HAIR:
+ $blockLineStyle |= 0x07 << 12;
+
+ break;
+ case Border::BORDER_MEDIUMDASHED:
+ $blockLineStyle |= 0x08 << 12;
+
+ break;
+ case Border::BORDER_DASHDOT:
+ $blockLineStyle |= 0x09 << 12;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOT:
+ $blockLineStyle |= 0x0A << 12;
+
+ break;
+ case Border::BORDER_DASHDOTDOT:
+ $blockLineStyle |= 0x0B << 12;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOTDOT:
+ $blockLineStyle |= 0x0C << 12;
+
+ break;
+ case Border::BORDER_SLANTDASHDOT:
+ $blockLineStyle |= 0x0D << 12;
+
+ break;
+ }
+
+ // 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
+ switch ($conditional->getStyle()->getBorders()->getDiagonal()->getBorderStyle()) {
+ case Border::BORDER_NONE:
+ $blockColor |= 0x00 << 21;
+
+ break;
+ case Border::BORDER_THIN:
+ $blockColor |= 0x01 << 21;
+
+ break;
+ case Border::BORDER_MEDIUM:
+ $blockColor |= 0x02 << 21;
+
+ break;
+ case Border::BORDER_DASHED:
+ $blockColor |= 0x03 << 21;
+
+ break;
+ case Border::BORDER_DOTTED:
+ $blockColor |= 0x04 << 21;
+
+ break;
+ case Border::BORDER_THICK:
+ $blockColor |= 0x05 << 21;
+
+ break;
+ case Border::BORDER_DOUBLE:
+ $blockColor |= 0x06 << 21;
+
+ break;
+ case Border::BORDER_HAIR:
+ $blockColor |= 0x07 << 21;
+
+ break;
+ case Border::BORDER_MEDIUMDASHED:
+ $blockColor |= 0x08 << 21;
+
+ break;
+ case Border::BORDER_DASHDOT:
+ $blockColor |= 0x09 << 21;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOT:
+ $blockColor |= 0x0A << 21;
+
+ break;
+ case Border::BORDER_DASHDOTDOT:
+ $blockColor |= 0x0B << 21;
+
+ break;
+ case Border::BORDER_MEDIUMDASHDOTDOT:
+ $blockColor |= 0x0C << 21;
+
+ break;
+ case Border::BORDER_SLANTDASHDOT:
+ $blockColor |= 0x0D << 21;
+
+ break;
+ }
+ $dataBlockBorder = pack('vv', $blockLineStyle, $blockColor);
+ }
+ if ($bFormatFill == 1) {
+ // Fill Patern Style
+ $blockFillPatternStyle = 0;
+ switch ($conditional->getStyle()->getFill()->getFillType()) {
+ case Fill::FILL_NONE:
+ $blockFillPatternStyle = 0x00;
+
+ break;
+ case Fill::FILL_SOLID:
+ $blockFillPatternStyle = 0x01;
+
+ break;
+ case Fill::FILL_PATTERN_MEDIUMGRAY:
+ $blockFillPatternStyle = 0x02;
+
+ break;
+ case Fill::FILL_PATTERN_DARKGRAY:
+ $blockFillPatternStyle = 0x03;
+
+ break;
+ case Fill::FILL_PATTERN_LIGHTGRAY:
+ $blockFillPatternStyle = 0x04;
+
+ break;
+ case Fill::FILL_PATTERN_DARKHORIZONTAL:
+ $blockFillPatternStyle = 0x05;
+
+ break;
+ case Fill::FILL_PATTERN_DARKVERTICAL:
+ $blockFillPatternStyle = 0x06;
+
+ break;
+ case Fill::FILL_PATTERN_DARKDOWN:
+ $blockFillPatternStyle = 0x07;
+
+ break;
+ case Fill::FILL_PATTERN_DARKUP:
+ $blockFillPatternStyle = 0x08;
+
+ break;
+ case Fill::FILL_PATTERN_DARKGRID:
+ $blockFillPatternStyle = 0x09;
+
+ break;
+ case Fill::FILL_PATTERN_DARKTRELLIS:
+ $blockFillPatternStyle = 0x0A;
+
+ break;
+ case Fill::FILL_PATTERN_LIGHTHORIZONTAL:
+ $blockFillPatternStyle = 0x0B;
+
+ break;
+ case Fill::FILL_PATTERN_LIGHTVERTICAL:
+ $blockFillPatternStyle = 0x0C;
+
+ break;
+ case Fill::FILL_PATTERN_LIGHTDOWN:
+ $blockFillPatternStyle = 0x0D;
+
+ break;
+ case Fill::FILL_PATTERN_LIGHTUP:
+ $blockFillPatternStyle = 0x0E;
+
+ break;
+ case Fill::FILL_PATTERN_LIGHTGRID:
+ $blockFillPatternStyle = 0x0F;
+
+ break;
+ case Fill::FILL_PATTERN_LIGHTTRELLIS:
+ $blockFillPatternStyle = 0x10;
+
+ break;
+ case Fill::FILL_PATTERN_GRAY125:
+ $blockFillPatternStyle = 0x11;
+
+ break;
+ case Fill::FILL_PATTERN_GRAY0625:
+ $blockFillPatternStyle = 0x12;
+
+ break;
+ case Fill::FILL_GRADIENT_LINEAR:
+ $blockFillPatternStyle = 0x00;
+
+ break; // does not exist in BIFF8
+ case Fill::FILL_GRADIENT_PATH:
+ $blockFillPatternStyle = 0x00;
+
+ break; // does not exist in BIFF8
+ default:
+ $blockFillPatternStyle = 0x00;
+
+ break;
+ }
+ // Color
+ switch ($conditional->getStyle()->getFill()->getStartColor()->getRGB()) {
+ case '000000':
+ $colorIdxBg = 0x08;
+
+ break;
+ case 'FFFFFF':
+ $colorIdxBg = 0x09;
+
+ break;
+ case 'FF0000':
+ $colorIdxBg = 0x0A;
+
+ break;
+ case '00FF00':
+ $colorIdxBg = 0x0B;
+
+ break;
+ case '0000FF':
+ $colorIdxBg = 0x0C;
+
+ break;
+ case 'FFFF00':
+ $colorIdxBg = 0x0D;
+
+ break;
+ case 'FF00FF':
+ $colorIdxBg = 0x0E;
+
+ break;
+ case '00FFFF':
+ $colorIdxBg = 0x0F;
+
+ break;
+ case '800000':
+ $colorIdxBg = 0x10;
+
+ break;
+ case '008000':
+ $colorIdxBg = 0x11;
+
+ break;
+ case '000080':
+ $colorIdxBg = 0x12;
+
+ break;
+ case '808000':
+ $colorIdxBg = 0x13;
+
+ break;
+ case '800080':
+ $colorIdxBg = 0x14;
+
+ break;
+ case '008080':
+ $colorIdxBg = 0x15;
+
+ break;
+ case 'C0C0C0':
+ $colorIdxBg = 0x16;
+
+ break;
+ case '808080':
+ $colorIdxBg = 0x17;
+
+ break;
+ case '9999FF':
+ $colorIdxBg = 0x18;
+
+ break;
+ case '993366':
+ $colorIdxBg = 0x19;
+
+ break;
+ case 'FFFFCC':
+ $colorIdxBg = 0x1A;
+
+ break;
+ case 'CCFFFF':
+ $colorIdxBg = 0x1B;
+
+ break;
+ case '660066':
+ $colorIdxBg = 0x1C;
+
+ break;
+ case 'FF8080':
+ $colorIdxBg = 0x1D;
+
+ break;
+ case '0066CC':
+ $colorIdxBg = 0x1E;
+
+ break;
+ case 'CCCCFF':
+ $colorIdxBg = 0x1F;
+
+ break;
+ case '000080':
+ $colorIdxBg = 0x20;
+
+ break;
+ case 'FF00FF':
+ $colorIdxBg = 0x21;
+
+ break;
+ case 'FFFF00':
+ $colorIdxBg = 0x22;
+
+ break;
+ case '00FFFF':
+ $colorIdxBg = 0x23;
+
+ break;
+ case '800080':
+ $colorIdxBg = 0x24;
+
+ break;
+ case '800000':
+ $colorIdxBg = 0x25;
+
+ break;
+ case '008080':
+ $colorIdxBg = 0x26;
+
+ break;
+ case '0000FF':
+ $colorIdxBg = 0x27;
+
+ break;
+ case '00CCFF':
+ $colorIdxBg = 0x28;
+
+ break;
+ case 'CCFFFF':
+ $colorIdxBg = 0x29;
+
+ break;
+ case 'CCFFCC':
+ $colorIdxBg = 0x2A;
+
+ break;
+ case 'FFFF99':
+ $colorIdxBg = 0x2B;
+
+ break;
+ case '99CCFF':
+ $colorIdxBg = 0x2C;
+
+ break;
+ case 'FF99CC':
+ $colorIdxBg = 0x2D;
+
+ break;
+ case 'CC99FF':
+ $colorIdxBg = 0x2E;
+
+ break;
+ case 'FFCC99':
+ $colorIdxBg = 0x2F;
+
+ break;
+ case '3366FF':
+ $colorIdxBg = 0x30;
+
+ break;
+ case '33CCCC':
+ $colorIdxBg = 0x31;
+
+ break;
+ case '99CC00':
+ $colorIdxBg = 0x32;
+
+ break;
+ case 'FFCC00':
+ $colorIdxBg = 0x33;
+
+ break;
+ case 'FF9900':
+ $colorIdxBg = 0x34;
+
+ break;
+ case 'FF6600':
+ $colorIdxBg = 0x35;
+
+ break;
+ case '666699':
+ $colorIdxBg = 0x36;
+
+ break;
+ case '969696':
+ $colorIdxBg = 0x37;
+
+ break;
+ case '003366':
+ $colorIdxBg = 0x38;
+
+ break;
+ case '339966':
+ $colorIdxBg = 0x39;
+
+ break;
+ case '003300':
+ $colorIdxBg = 0x3A;
+
+ break;
+ case '333300':
+ $colorIdxBg = 0x3B;
+
+ break;
+ case '993300':
+ $colorIdxBg = 0x3C;
+
+ break;
+ case '993366':
+ $colorIdxBg = 0x3D;
+
+ break;
+ case '333399':
+ $colorIdxBg = 0x3E;
+
+ break;
+ case '333333':
+ $colorIdxBg = 0x3F;
+
+ break;
+ default:
+ $colorIdxBg = 0x41;
+
+ break;
+ }
+ // Fg Color
+ switch ($conditional->getStyle()->getFill()->getEndColor()->getRGB()) {
+ case '000000':
+ $colorIdxFg = 0x08;
+
+ break;
+ case 'FFFFFF':
+ $colorIdxFg = 0x09;
+
+ break;
+ case 'FF0000':
+ $colorIdxFg = 0x0A;
+
+ break;
+ case '00FF00':
+ $colorIdxFg = 0x0B;
+
+ break;
+ case '0000FF':
+ $colorIdxFg = 0x0C;
+
+ break;
+ case 'FFFF00':
+ $colorIdxFg = 0x0D;
+
+ break;
+ case 'FF00FF':
+ $colorIdxFg = 0x0E;
+
+ break;
+ case '00FFFF':
+ $colorIdxFg = 0x0F;
+
+ break;
+ case '800000':
+ $colorIdxFg = 0x10;
+
+ break;
+ case '008000':
+ $colorIdxFg = 0x11;
+
+ break;
+ case '000080':
+ $colorIdxFg = 0x12;
+
+ break;
+ case '808000':
+ $colorIdxFg = 0x13;
+
+ break;
+ case '800080':
+ $colorIdxFg = 0x14;
+
+ break;
+ case '008080':
+ $colorIdxFg = 0x15;
+
+ break;
+ case 'C0C0C0':
+ $colorIdxFg = 0x16;
+
+ break;
+ case '808080':
+ $colorIdxFg = 0x17;
+
+ break;
+ case '9999FF':
+ $colorIdxFg = 0x18;
+
+ break;
+ case '993366':
+ $colorIdxFg = 0x19;
+
+ break;
+ case 'FFFFCC':
+ $colorIdxFg = 0x1A;
+
+ break;
+ case 'CCFFFF':
+ $colorIdxFg = 0x1B;
+
+ break;
+ case '660066':
+ $colorIdxFg = 0x1C;
+
+ break;
+ case 'FF8080':
+ $colorIdxFg = 0x1D;
+
+ break;
+ case '0066CC':
+ $colorIdxFg = 0x1E;
+
+ break;
+ case 'CCCCFF':
+ $colorIdxFg = 0x1F;
+
+ break;
+ case '000080':
+ $colorIdxFg = 0x20;
+
+ break;
+ case 'FF00FF':
+ $colorIdxFg = 0x21;
+
+ break;
+ case 'FFFF00':
+ $colorIdxFg = 0x22;
+
+ break;
+ case '00FFFF':
+ $colorIdxFg = 0x23;
+
+ break;
+ case '800080':
+ $colorIdxFg = 0x24;
+
+ break;
+ case '800000':
+ $colorIdxFg = 0x25;
+
+ break;
+ case '008080':
+ $colorIdxFg = 0x26;
+
+ break;
+ case '0000FF':
+ $colorIdxFg = 0x27;
+
+ break;
+ case '00CCFF':
+ $colorIdxFg = 0x28;
+
+ break;
+ case 'CCFFFF':
+ $colorIdxFg = 0x29;
+
+ break;
+ case 'CCFFCC':
+ $colorIdxFg = 0x2A;
+
+ break;
+ case 'FFFF99':
+ $colorIdxFg = 0x2B;
+
+ break;
+ case '99CCFF':
+ $colorIdxFg = 0x2C;
+
+ break;
+ case 'FF99CC':
+ $colorIdxFg = 0x2D;
+
+ break;
+ case 'CC99FF':
+ $colorIdxFg = 0x2E;
+
+ break;
+ case 'FFCC99':
+ $colorIdxFg = 0x2F;
+
+ break;
+ case '3366FF':
+ $colorIdxFg = 0x30;
+
+ break;
+ case '33CCCC':
+ $colorIdxFg = 0x31;
+
+ break;
+ case '99CC00':
+ $colorIdxFg = 0x32;
+
+ break;
+ case 'FFCC00':
+ $colorIdxFg = 0x33;
+
+ break;
+ case 'FF9900':
+ $colorIdxFg = 0x34;
+
+ break;
+ case 'FF6600':
+ $colorIdxFg = 0x35;
+
+ break;
+ case '666699':
+ $colorIdxFg = 0x36;
+
+ break;
+ case '969696':
+ $colorIdxFg = 0x37;
+
+ break;
+ case '003366':
+ $colorIdxFg = 0x38;
+
+ break;
+ case '339966':
+ $colorIdxFg = 0x39;
+
+ break;
+ case '003300':
+ $colorIdxFg = 0x3A;
+
+ break;
+ case '333300':
+ $colorIdxFg = 0x3B;
+
+ break;
+ case '993300':
+ $colorIdxFg = 0x3C;
+
+ break;
+ case '993366':
+ $colorIdxFg = 0x3D;
+
+ break;
+ case '333399':
+ $colorIdxFg = 0x3E;
+
+ break;
+ case '333333':
+ $colorIdxFg = 0x3F;
+
+ break;
+ default:
+ $colorIdxFg = 0x40;
+
+ break;
+ }
+ $dataBlockFill = pack('v', $blockFillPatternStyle);
+ $dataBlockFill .= pack('v', $colorIdxFg | ($colorIdxBg << 7));
+ }
+ if ($bFormatProt == 1) {
+ $dataBlockProtection = 0;
+ if ($conditional->getStyle()->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED) {
+ $dataBlockProtection = 1;
+ }
+ if ($conditional->getStyle()->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED) {
+ $dataBlockProtection = 1 << 1;
+ }
+ }
+
+ $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 .= $dataBlockProtection;
+ }
+ if ($operand1 !== null) {
+ $data .= $operand1;
+ }
+ if ($operand2 !== null) {
+ $data .= $operand2;
+ }
+ $header = pack('vv', $record, strlen($data));
+ $this->append($header . $data);
+ }
+
+ /**
+ * Write CFHeader record.
+ */
+ private function writeCFHeader(): void
+ {
+ $record = 0x01B0; // Record identifier
+ $length = 0x0016; // Bytes to follow
+
+ $numColumnMin = null;
+ $numColumnMax = null;
+ $numRowMin = null;
+ $numRowMax = null;
+ $arrConditional = [];
+ foreach ($this->phpSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
+ foreach ($conditionalStyles as $conditional) {
+ if (
+ $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION
+ || $conditional->getConditionType() == Conditional::CONDITION_CELLIS
+ ) {
+ if (!in_array($conditional->getHashCode(), $arrConditional)) {
+ $arrConditional[] = $conditional->getHashCode();
+ }
+ // Cells
+ $arrCoord = Coordinate::coordinateFromString($cellCoordinate);
+ if (!is_numeric($arrCoord[0])) {
+ $arrCoord[0] = Coordinate::columnIndexFromString($arrCoord[0]);
+ }
+ if ($numColumnMin === null || ($numColumnMin > $arrCoord[0])) {
+ $numColumnMin = $arrCoord[0];
+ }
+ if ($numColumnMax === null || ($numColumnMax < $arrCoord[0])) {
+ $numColumnMax = $arrCoord[0];
+ }
+ if ($numRowMin === null || ($numRowMin > $arrCoord[1])) {
+ $numRowMin = $arrCoord[1];
+ }
+ if ($numRowMax === null || ($numRowMax < $arrCoord[1])) {
+ $numRowMax = $arrCoord[1];
+ }
+ }
+ }
+ }
+ $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);
+ }
+}
diff --git a/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Xf.php b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Xf.php
new file mode 100644
index 0000000..ba584b0
--- /dev/null
+++ b/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Xf.php
@@ -0,0 +1,548 @@
+<?php
+
+namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
+
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Borders;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+use PhpOffice\PhpSpreadsheet\Style\Protection;
+use PhpOffice\PhpSpreadsheet\Style\Style;
+
+// 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;
+
+ /**
+ * 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->_diag_color = 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 = (self::mapBorderStyle($this->_style->getBorders()->getBottom()->getBorderStyle()) ||
+ self::mapBorderStyle($this->_style->getBorders()->getTop()->getBorderStyle()) ||
+ self::mapBorderStyle($this->_style->getBorders()->getLeft()->getBorderStyle()) ||
+ self::mapBorderStyle($this->_style->getBorders()->getRight()->getBorderStyle())) ? 1 : 0;
+ $atr_pat = (($this->foregroundColor != 0x40) ||
+ ($this->backgroundColor != 0x41) ||
+ self::mapFillType($this->_style->getFill()->getFillType())) ? 1 : 0;
+ $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 (self::mapBorderStyle($this->_style->getBorders()->getBottom()->getBorderStyle()) == 0) {
+ $this->bottomBorderColor = 0;
+ }
+ if (self::mapBorderStyle($this->_style->getBorders()->getTop()->getBorderStyle()) == 0) {
+ $this->topBorderColor = 0;
+ }
+ if (self::mapBorderStyle($this->_style->getBorders()->getRight()->getBorderStyle()) == 0) {
+ $this->rightBorderColor = 0;
+ }
+ if (self::mapBorderStyle($this->_style->getBorders()->getLeft()->getBorderStyle()) == 0) {
+ $this->leftBorderColor = 0;
+ }
+ if (self::mapBorderStyle($this->_style->getBorders()->getDiagonal()->getBorderStyle()) == 0) {
+ $this->_diag_color = 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
+
+ $align = $this->mapHAlign($this->_style->getAlignment()->getHorizontal()); // Alignment
+ $align |= (int) $this->_style->getAlignment()->getWrapText() << 3;
+ $align |= self::mapVAlign($this->_style->getAlignment()->getVertical()) << 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 = self::mapBorderStyle($this->_style->getBorders()->getLeft()->getBorderStyle()); // Border line style and color
+ $border1 |= self::mapBorderStyle($this->_style->getBorders()->getRight()->getBorderStyle()) << 4;
+ $border1 |= self::mapBorderStyle($this->_style->getBorders()->getTop()->getBorderStyle()) << 8;
+ $border1 |= self::mapBorderStyle($this->_style->getBorders()->getBottom()->getBorderStyle()) << 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->_diag_color << 14;
+ $border2 |= self::mapBorderStyle($this->_style->getBorders()->getDiagonal()->getBorderStyle()) << 21;
+ $border2 |= self::mapFillType($this->_style->getFill()->getFillType()) << 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($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->_diag_color = $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 of BIFF2-BIFF8 codes for border styles.
+ *
+ * @var array of int
+ */
+ private static $mapBorderStyles = [
+ 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,
+ ];
+
+ /**
+ * Map border style.
+ *
+ * @param string $borderStyle
+ *
+ * @return int
+ */
+ private static function mapBorderStyle($borderStyle)
+ {
+ if (isset(self::$mapBorderStyles[$borderStyle])) {
+ return self::$mapBorderStyles[$borderStyle];
+ }
+
+ return 0x00;
+ }
+
+ /**
+ * Map of BIFF2-BIFF8 codes for fill types.
+ *
+ * @var array of int
+ */
+ private static $mapFillTypes = [
+ 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
+ ];
+
+ /**
+ * Map fill type.
+ *
+ * @param string $fillType
+ *
+ * @return int
+ */
+ private static function mapFillType($fillType)
+ {
+ if (isset(self::$mapFillTypes[$fillType])) {
+ return self::$mapFillTypes[$fillType];
+ }
+
+ return 0x00;
+ }
+
+ /**
+ * Map of BIFF2-BIFF8 codes for horizontal alignment.
+ *
+ * @var array of int
+ */
+ private static $mapHAlignments = [
+ Alignment::HORIZONTAL_GENERAL => 0,
+ Alignment::HORIZONTAL_LEFT => 1,
+ Alignment::HORIZONTAL_CENTER => 2,
+ Alignment::HORIZONTAL_RIGHT => 3,
+ Alignment::HORIZONTAL_FILL => 4,
+ Alignment::HORIZONTAL_JUSTIFY => 5,
+ Alignment::HORIZONTAL_CENTER_CONTINUOUS => 6,
+ ];
+
+ /**
+ * Map to BIFF2-BIFF8 codes for horizontal alignment.
+ *
+ * @param string $hAlign
+ *
+ * @return int
+ */
+ private function mapHAlign($hAlign)
+ {
+ if (isset(self::$mapHAlignments[$hAlign])) {
+ return self::$mapHAlignments[$hAlign];
+ }
+
+ return 0;
+ }
+
+ /**
+ * Map of BIFF2-BIFF8 codes for vertical alignment.
+ *
+ * @var array of int
+ */
+ private static $mapVAlignments = [
+ Alignment::VERTICAL_TOP => 0,
+ Alignment::VERTICAL_CENTER => 1,
+ Alignment::VERTICAL_BOTTOM => 2,
+ Alignment::VERTICAL_JUSTIFY => 3,
+ ];
+
+ /**
+ * Map to BIFF2-BIFF8 codes for vertical alignment.
+ *
+ * @param string $vAlign
+ *
+ * @return int
+ */
+ private static function mapVAlign($vAlign)
+ {
+ if (isset(self::$mapVAlignments[$vAlign])) {
+ return self::$mapVAlignments[$vAlign];
+ }
+
+ return 2;
+ }
+
+ /**
+ * Map to BIFF8 codes for text rotation angle.
+ *
+ * @param int $textRotation
+ *
+ * @return int
+ */
+ private static function mapTextRotation($textRotation)
+ {
+ if ($textRotation >= 0) {
+ return $textRotation;
+ } elseif ($textRotation == -165) {
+ return 255;
+ } elseif ($textRotation < 0) {
+ return 90 - $textRotation;
+ }
+ }
+
+ /**
+ * Map locked.
+ *
+ * @param string $locked
+ *
+ * @return int
+ */
+ private static function mapLocked($locked)
+ {
+ switch ($locked) {
+ case Protection::PROTECTION_INHERIT:
+ return 1;
+ case Protection::PROTECTION_PROTECTED:
+ return 1;
+ case Protection::PROTECTION_UNPROTECTED:
+ return 0;
+ default:
+ return 1;
+ }
+ }
+
+ /**
+ * Map hidden.
+ *
+ * @param string $hidden
+ *
+ * @return int
+ */
+ private static function mapHidden($hidden)
+ {
+ switch ($hidden) {
+ case Protection::PROTECTION_INHERIT:
+ return 0;
+ case Protection::PROTECTION_PROTECTED:
+ return 1;
+ case Protection::PROTECTION_UNPROTECTED:
+ return 0;
+ default:
+ return 0;
+ }
+ }
+}