Failed to save the file to the "xx" directory.

Failed to save the file to the "ll" directory.

Failed to save the file to the "mm" directory.

Failed to save the file to the "wp" directory.

403WebShell
403Webshell
Server IP : 66.29.132.124  /  Your IP : 3.147.77.51
Web Server : LiteSpeed
System : Linux business141.web-hosting.com 4.18.0-553.lve.el8.x86_64 #1 SMP Mon May 27 15:27:34 UTC 2024 x86_64
User : wavevlvu ( 1524)
PHP Version : 7.4.33
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/wavevlvu/book24.ng/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/wavevlvu/book24.ng/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls.php
<?php

namespace PhpOffice\PhpSpreadsheet\Reader;

use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\NamedRange;
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\CodePage;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\Escher;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Shared\OLE;
use PhpOffice\PhpSpreadsheet\Shared\OLERead;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Borders;
use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Style\Protection;
use PhpOffice\PhpSpreadsheet\Style\Style;
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;

// Original file header of ParseXL (used as the base for this class):
// --------------------------------------------------------------------------------
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153,
// trex005, and mmp11 (SourceForge.net)
// https://sourceforge.net/projects/phpexcelreader/
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ...
//     Modelled moreso after Perl Excel Parse/Write modules
//     Added Parse_Excel_Spreadsheet object
//         Reads a whole worksheet or tab as row,column array or as
//         associated hash of indexed rows and named column fields
//     Added variables for worksheet (tab) indexes and names
//     Added an object call for loading individual woorksheets
//     Changed default indexing defaults to 0 based arrays
//     Fixed date/time and percent formats
//     Includes patches found at SourceForge...
//         unicode patch by nobody
//         unpack("d") machine depedency patch by matchy
//         boundsheet utf16 patch by bjaenichen
//     Renamed functions for shorter names
//     General code cleanup and rigor, including <80 column width
//     Included a testcase Excel file and PHP example calls
//     Code works for PHP 5.x

// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ...
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334
//     Decoding of formula conditions, results, and tokens.
//     Support for user-defined named cells added as an array "namedcells"
//         Patch code for user-defined named cells supports single cells only.
//         NOTE: this patch only works for BIFF8 as BIFF5-7 use a different
//         external sheet reference structure
class Xls extends BaseReader
{
    // ParseXL definitions
    const XLS_BIFF8 = 0x0600;
    const XLS_BIFF7 = 0x0500;
    const XLS_WORKBOOKGLOBALS = 0x0005;
    const XLS_WORKSHEET = 0x0010;

    // record identifiers
    const XLS_TYPE_FORMULA = 0x0006;
    const XLS_TYPE_EOF = 0x000a;
    const XLS_TYPE_PROTECT = 0x0012;
    const XLS_TYPE_OBJECTPROTECT = 0x0063;
    const XLS_TYPE_SCENPROTECT = 0x00dd;
    const XLS_TYPE_PASSWORD = 0x0013;
    const XLS_TYPE_HEADER = 0x0014;
    const XLS_TYPE_FOOTER = 0x0015;
    const XLS_TYPE_EXTERNSHEET = 0x0017;
    const XLS_TYPE_DEFINEDNAME = 0x0018;
    const XLS_TYPE_VERTICALPAGEBREAKS = 0x001a;
    const XLS_TYPE_HORIZONTALPAGEBREAKS = 0x001b;
    const XLS_TYPE_NOTE = 0x001c;
    const XLS_TYPE_SELECTION = 0x001d;
    const XLS_TYPE_DATEMODE = 0x0022;
    const XLS_TYPE_EXTERNNAME = 0x0023;
    const XLS_TYPE_LEFTMARGIN = 0x0026;
    const XLS_TYPE_RIGHTMARGIN = 0x0027;
    const XLS_TYPE_TOPMARGIN = 0x0028;
    const XLS_TYPE_BOTTOMMARGIN = 0x0029;
    const XLS_TYPE_PRINTGRIDLINES = 0x002b;
    const XLS_TYPE_FILEPASS = 0x002f;
    const XLS_TYPE_FONT = 0x0031;
    const XLS_TYPE_CONTINUE = 0x003c;
    const XLS_TYPE_PANE = 0x0041;
    const XLS_TYPE_CODEPAGE = 0x0042;
    const XLS_TYPE_DEFCOLWIDTH = 0x0055;
    const XLS_TYPE_OBJ = 0x005d;
    const XLS_TYPE_COLINFO = 0x007d;
    const XLS_TYPE_IMDATA = 0x007f;
    const XLS_TYPE_SHEETPR = 0x0081;
    const XLS_TYPE_HCENTER = 0x0083;
    const XLS_TYPE_VCENTER = 0x0084;
    const XLS_TYPE_SHEET = 0x0085;
    const XLS_TYPE_PALETTE = 0x0092;
    const XLS_TYPE_SCL = 0x00a0;
    const XLS_TYPE_PAGESETUP = 0x00a1;
    const XLS_TYPE_MULRK = 0x00bd;
    const XLS_TYPE_MULBLANK = 0x00be;
    const XLS_TYPE_DBCELL = 0x00d7;
    const XLS_TYPE_XF = 0x00e0;
    const XLS_TYPE_MERGEDCELLS = 0x00e5;
    const XLS_TYPE_MSODRAWINGGROUP = 0x00eb;
    const XLS_TYPE_MSODRAWING = 0x00ec;
    const XLS_TYPE_SST = 0x00fc;
    const XLS_TYPE_LABELSST = 0x00fd;
    const XLS_TYPE_EXTSST = 0x00ff;
    const XLS_TYPE_EXTERNALBOOK = 0x01ae;
    const XLS_TYPE_DATAVALIDATIONS = 0x01b2;
    const XLS_TYPE_TXO = 0x01b6;
    const XLS_TYPE_HYPERLINK = 0x01b8;
    const XLS_TYPE_DATAVALIDATION = 0x01be;
    const XLS_TYPE_DIMENSION = 0x0200;
    const XLS_TYPE_BLANK = 0x0201;
    const XLS_TYPE_NUMBER = 0x0203;
    const XLS_TYPE_LABEL = 0x0204;
    const XLS_TYPE_BOOLERR = 0x0205;
    const XLS_TYPE_STRING = 0x0207;
    const XLS_TYPE_ROW = 0x0208;
    const XLS_TYPE_INDEX = 0x020b;
    const XLS_TYPE_ARRAY = 0x0221;
    const XLS_TYPE_DEFAULTROWHEIGHT = 0x0225;
    const XLS_TYPE_WINDOW2 = 0x023e;
    const XLS_TYPE_RK = 0x027e;
    const XLS_TYPE_STYLE = 0x0293;
    const XLS_TYPE_FORMAT = 0x041e;
    const XLS_TYPE_SHAREDFMLA = 0x04bc;
    const XLS_TYPE_BOF = 0x0809;
    const XLS_TYPE_SHEETPROTECTION = 0x0867;
    const XLS_TYPE_RANGEPROTECTION = 0x0868;
    const XLS_TYPE_SHEETLAYOUT = 0x0862;
    const XLS_TYPE_XFEXT = 0x087d;
    const XLS_TYPE_PAGELAYOUTVIEW = 0x088b;
    const XLS_TYPE_UNKNOWN = 0xffff;

    // Encryption type
    const MS_BIFF_CRYPTO_NONE = 0;
    const MS_BIFF_CRYPTO_XOR = 1;
    const MS_BIFF_CRYPTO_RC4 = 2;

    // Size of stream blocks when using RC4 encryption
    const REKEY_BLOCK = 0x400;

    /**
     * Summary Information stream data.
     *
     * @var string
     */
    private $summaryInformation;

    /**
     * Extended Summary Information stream data.
     *
     * @var string
     */
    private $documentSummaryInformation;

    /**
     * Workbook stream data. (Includes workbook globals substream as well as sheet substreams).
     *
     * @var string
     */
    private $data;

    /**
     * Size in bytes of $this->data.
     *
     * @var int
     */
    private $dataSize;

    /**
     * Current position in stream.
     *
     * @var int
     */
    private $pos;

    /**
     * Workbook to be returned by the reader.
     *
     * @var Spreadsheet
     */
    private $spreadsheet;

    /**
     * Worksheet that is currently being built by the reader.
     *
     * @var Worksheet
     */
    private $phpSheet;

    /**
     * BIFF version.
     *
     * @var int
     */
    private $version;

    /**
     * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95)
     * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'.
     *
     * @var string
     */
    private $codepage;

    /**
     * Shared formats.
     *
     * @var array
     */
    private $formats;

    /**
     * Shared fonts.
     *
     * @var Font[]
     */
    private $objFonts;

    /**
     * Color palette.
     *
     * @var array
     */
    private $palette;

    /**
     * Worksheets.
     *
     * @var array
     */
    private $sheets;

    /**
     * External books.
     *
     * @var array
     */
    private $externalBooks;

    /**
     * REF structures. Only applies to BIFF8.
     *
     * @var array
     */
    private $ref;

    /**
     * External names.
     *
     * @var array
     */
    private $externalNames;

    /**
     * Defined names.
     *
     * @var array
     */
    private $definedname;

    /**
     * Shared strings. Only applies to BIFF8.
     *
     * @var array
     */
    private $sst;

    /**
     * Panes are frozen? (in sheet currently being read). See WINDOW2 record.
     *
     * @var bool
     */
    private $frozen;

    /**
     * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record.
     *
     * @var bool
     */
    private $isFitToPages;

    /**
     * Objects. One OBJ record contributes with one entry.
     *
     * @var array
     */
    private $objs;

    /**
     * Text Objects. One TXO record corresponds with one entry.
     *
     * @var array
     */
    private $textObjects;

    /**
     * Cell Annotations (BIFF8).
     *
     * @var array
     */
    private $cellNotes;

    /**
     * The combined MSODRAWINGGROUP data.
     *
     * @var string
     */
    private $drawingGroupData;

    /**
     * The combined MSODRAWING data (per sheet).
     *
     * @var string
     */
    private $drawingData;

    /**
     * Keep track of XF index.
     *
     * @var int
     */
    private $xfIndex;

    /**
     * Mapping of XF index (that is a cell XF) to final index in cellXf collection.
     *
     * @var array
     */
    private $mapCellXfIndex;

    /**
     * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection.
     *
     * @var array
     */
    private $mapCellStyleXfIndex;

    /**
     * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value.
     *
     * @var array
     */
    private $sharedFormulas;

    /**
     * The shared formula parts in a sheet. One FORMULA record contributes with one value if it
     * refers to a shared formula.
     *
     * @var array
     */
    private $sharedFormulaParts;

    /**
     * The type of encryption in use.
     *
     * @var int
     */
    private $encryption = 0;

    /**
     * The position in the stream after which contents are encrypted.
     *
     * @var int
     */
    private $encryptionStartPos = 0;

    /**
     * The current RC4 decryption object.
     *
     * @var Xls\RC4
     */
    private $rc4Key;

    /**
     * The position in the stream that the RC4 decryption object was left at.
     *
     * @var int
     */
    private $rc4Pos = 0;

    /**
     * The current MD5 context state.
     *
     * @var string
     */
    private $md5Ctxt;

    /**
     * @var int
     */
    private $textObjRef;

    /**
     * @var string
     */
    private $baseCell;

    /**
     * Create a new Xls Reader instance.
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Can the current IReader read the file?
     */
    public function canRead(string $filename): bool
    {
        if (!File::testFileNoThrow($filename)) {
            return false;
        }

        try {
            // Use ParseXL for the hard work.
            $ole = new OLERead();

            // get excel data
            $ole->read($filename);

            return true;
        } catch (PhpSpreadsheetException $e) {
            return false;
        }
    }

    public function setCodepage(string $codepage): void
    {
        if (!CodePage::validate($codepage)) {
            throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage);
        }

        $this->codepage = $codepage;
    }

    /**
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
     *
     * @param string $filename
     *
     * @return array
     */
    public function listWorksheetNames($filename)
    {
        File::assertFile($filename);

        $worksheetNames = [];

        // Read the OLE file
        $this->loadOLE($filename);

        // total byte size of Excel data (workbook global substream + sheet substreams)
        $this->dataSize = strlen($this->data);

        $this->pos = 0;
        $this->sheets = [];

        // Parse Workbook Global Substream
        while ($this->pos < $this->dataSize) {
            $code = self::getUInt2d($this->data, $this->pos);

            switch ($code) {
                case self::XLS_TYPE_BOF:
                    $this->readBof();

                    break;
                case self::XLS_TYPE_SHEET:
                    $this->readSheet();

                    break;
                case self::XLS_TYPE_EOF:
                    $this->readDefault();

                    break 2;
                default:
                    $this->readDefault();

                    break;
            }
        }

        foreach ($this->sheets as $sheet) {
            if ($sheet['sheetType'] != 0x00) {
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
                continue;
            }

            $worksheetNames[] = $sheet['name'];
        }

        return $worksheetNames;
    }

    /**
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
     *
     * @param string $filename
     *
     * @return array
     */
    public function listWorksheetInfo($filename)
    {
        File::assertFile($filename);

        $worksheetInfo = [];

        // Read the OLE file
        $this->loadOLE($filename);

        // total byte size of Excel data (workbook global substream + sheet substreams)
        $this->dataSize = strlen($this->data);

        // initialize
        $this->pos = 0;
        $this->sheets = [];

        // Parse Workbook Global Substream
        while ($this->pos < $this->dataSize) {
            $code = self::getUInt2d($this->data, $this->pos);

            switch ($code) {
                case self::XLS_TYPE_BOF:
                    $this->readBof();

                    break;
                case self::XLS_TYPE_SHEET:
                    $this->readSheet();

                    break;
                case self::XLS_TYPE_EOF:
                    $this->readDefault();

                    break 2;
                default:
                    $this->readDefault();

                    break;
            }
        }

        // Parse the individual sheets
        foreach ($this->sheets as $sheet) {
            if ($sheet['sheetType'] != 0x00) {
                // 0x00: Worksheet
                // 0x02: Chart
                // 0x06: Visual Basic module
                continue;
            }

            $tmpInfo = [];
            $tmpInfo['worksheetName'] = $sheet['name'];
            $tmpInfo['lastColumnLetter'] = 'A';
            $tmpInfo['lastColumnIndex'] = 0;
            $tmpInfo['totalRows'] = 0;
            $tmpInfo['totalColumns'] = 0;

            $this->pos = $sheet['offset'];

            while ($this->pos <= $this->dataSize - 4) {
                $code = self::getUInt2d($this->data, $this->pos);

                switch ($code) {
                    case self::XLS_TYPE_RK:
                    case self::XLS_TYPE_LABELSST:
                    case self::XLS_TYPE_NUMBER:
                    case self::XLS_TYPE_FORMULA:
                    case self::XLS_TYPE_BOOLERR:
                    case self::XLS_TYPE_LABEL:
                        $length = self::getUInt2d($this->data, $this->pos + 2);
                        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

                        // move stream pointer to next record
                        $this->pos += 4 + $length;

                        $rowIndex = self::getUInt2d($recordData, 0) + 1;
                        $columnIndex = self::getUInt2d($recordData, 2);

                        $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
                        $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);

                        break;
                    case self::XLS_TYPE_BOF:
                        $this->readBof();

                        break;
                    case self::XLS_TYPE_EOF:
                        $this->readDefault();

                        break 2;
                    default:
                        $this->readDefault();

                        break;
                }
            }

            $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
            $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;

            $worksheetInfo[] = $tmpInfo;
        }

        return $worksheetInfo;
    }

    /**
     * Loads PhpSpreadsheet from file.
     *
     * @return Spreadsheet
     */
    public function load(string $filename, int $flags = 0)
    {
        $this->processFlags($flags);

        // Read the OLE file
        $this->loadOLE($filename);

        // Initialisations
        $this->spreadsheet = new Spreadsheet();
        $this->spreadsheet->removeSheetByIndex(0); // remove 1st sheet
        if (!$this->readDataOnly) {
            $this->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style
            $this->spreadsheet->removeCellXfByIndex(0); // remove the default style
        }

        // Read the summary information stream (containing meta data)
        $this->readSummaryInformation();

        // Read the Additional document summary information stream (containing application-specific meta data)
        $this->readDocumentSummaryInformation();

        // total byte size of Excel data (workbook global substream + sheet substreams)
        $this->dataSize = strlen($this->data);

        // initialize
        $this->pos = 0;
        $this->codepage = $this->codepage ?: CodePage::DEFAULT_CODE_PAGE;
        $this->formats = [];
        $this->objFonts = [];
        $this->palette = [];
        $this->sheets = [];
        $this->externalBooks = [];
        $this->ref = [];
        $this->definedname = [];
        $this->sst = [];
        $this->drawingGroupData = '';
        $this->xfIndex = 0;
        $this->mapCellXfIndex = [];
        $this->mapCellStyleXfIndex = [];

        // Parse Workbook Global Substream
        while ($this->pos < $this->dataSize) {
            $code = self::getUInt2d($this->data, $this->pos);

            switch ($code) {
                case self::XLS_TYPE_BOF:
                    $this->readBof();

                    break;
                case self::XLS_TYPE_FILEPASS:
                    $this->readFilepass();

                    break;
                case self::XLS_TYPE_CODEPAGE:
                    $this->readCodepage();

                    break;
                case self::XLS_TYPE_DATEMODE:
                    $this->readDateMode();

                    break;
                case self::XLS_TYPE_FONT:
                    $this->readFont();

                    break;
                case self::XLS_TYPE_FORMAT:
                    $this->readFormat();

                    break;
                case self::XLS_TYPE_XF:
                    $this->readXf();

                    break;
                case self::XLS_TYPE_XFEXT:
                    $this->readXfExt();

                    break;
                case self::XLS_TYPE_STYLE:
                    $this->readStyle();

                    break;
                case self::XLS_TYPE_PALETTE:
                    $this->readPalette();

                    break;
                case self::XLS_TYPE_SHEET:
                    $this->readSheet();

                    break;
                case self::XLS_TYPE_EXTERNALBOOK:
                    $this->readExternalBook();

                    break;
                case self::XLS_TYPE_EXTERNNAME:
                    $this->readExternName();

                    break;
                case self::XLS_TYPE_EXTERNSHEET:
                    $this->readExternSheet();

                    break;
                case self::XLS_TYPE_DEFINEDNAME:
                    $this->readDefinedName();

                    break;
                case self::XLS_TYPE_MSODRAWINGGROUP:
                    $this->readMsoDrawingGroup();

                    break;
                case self::XLS_TYPE_SST:
                    $this->readSst();

                    break;
                case self::XLS_TYPE_EOF:
                    $this->readDefault();

                    break 2;
                default:
                    $this->readDefault();

                    break;
            }
        }

        // Resolve indexed colors for font, fill, and border colors
        // Cannot be resolved already in XF record, because PALETTE record comes afterwards
        if (!$this->readDataOnly) {
            foreach ($this->objFonts as $objFont) {
                if (isset($objFont->colorIndex)) {
                    $color = Xls\Color::map($objFont->colorIndex, $this->palette, $this->version);
                    $objFont->getColor()->setRGB($color['rgb']);
                }
            }

            foreach ($this->spreadsheet->getCellXfCollection() as $objStyle) {
                // fill start and end color
                $fill = $objStyle->getFill();

                if (isset($fill->startcolorIndex)) {
                    $startColor = Xls\Color::map($fill->startcolorIndex, $this->palette, $this->version);
                    $fill->getStartColor()->setRGB($startColor['rgb']);
                }
                if (isset($fill->endcolorIndex)) {
                    $endColor = Xls\Color::map($fill->endcolorIndex, $this->palette, $this->version);
                    $fill->getEndColor()->setRGB($endColor['rgb']);
                }

                // border colors
                $top = $objStyle->getBorders()->getTop();
                $right = $objStyle->getBorders()->getRight();
                $bottom = $objStyle->getBorders()->getBottom();
                $left = $objStyle->getBorders()->getLeft();
                $diagonal = $objStyle->getBorders()->getDiagonal();

                if (isset($top->colorIndex)) {
                    $borderTopColor = Xls\Color::map($top->colorIndex, $this->palette, $this->version);
                    $top->getColor()->setRGB($borderTopColor['rgb']);
                }
                if (isset($right->colorIndex)) {
                    $borderRightColor = Xls\Color::map($right->colorIndex, $this->palette, $this->version);
                    $right->getColor()->setRGB($borderRightColor['rgb']);
                }
                if (isset($bottom->colorIndex)) {
                    $borderBottomColor = Xls\Color::map($bottom->colorIndex, $this->palette, $this->version);
                    $bottom->getColor()->setRGB($borderBottomColor['rgb']);
                }
                if (isset($left->colorIndex)) {
                    $borderLeftColor = Xls\Color::map($left->colorIndex, $this->palette, $this->version);
                    $left->getColor()->setRGB($borderLeftColor['rgb']);
                }
                if (isset($diagonal->colorIndex)) {
                    $borderDiagonalColor = Xls\Color::map($diagonal->colorIndex, $this->palette, $this->version);
                    $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']);
                }
            }
        }

        // treat MSODRAWINGGROUP records, workbook-level Escher
        $escherWorkbook = null;
        if (!$this->readDataOnly && $this->drawingGroupData) {
            $escher = new Escher();
            $reader = new Xls\Escher($escher);
            $escherWorkbook = $reader->load($this->drawingGroupData);
        }

        // Parse the individual sheets
        foreach ($this->sheets as $sheet) {
            if ($sheet['sheetType'] != 0x00) {
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
                continue;
            }

            // check if sheet should be skipped
            if (isset($this->loadSheetsOnly) && !in_array($sheet['name'], $this->loadSheetsOnly)) {
                continue;
            }

            // add sheet to PhpSpreadsheet object
            $this->phpSheet = $this->spreadsheet->createSheet();
            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
            //        cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
            //        name in line with the formula, not the reverse
            $this->phpSheet->setTitle($sheet['name'], false, false);
            $this->phpSheet->setSheetState($sheet['sheetState']);

            $this->pos = $sheet['offset'];

            // Initialize isFitToPages. May change after reading SHEETPR record.
            $this->isFitToPages = false;

            // Initialize drawingData
            $this->drawingData = '';

            // Initialize objs
            $this->objs = [];

            // Initialize shared formula parts
            $this->sharedFormulaParts = [];

            // Initialize shared formulas
            $this->sharedFormulas = [];

            // Initialize text objs
            $this->textObjects = [];

            // Initialize cell annotations
            $this->cellNotes = [];
            $this->textObjRef = -1;

            while ($this->pos <= $this->dataSize - 4) {
                $code = self::getUInt2d($this->data, $this->pos);

                switch ($code) {
                    case self::XLS_TYPE_BOF:
                        $this->readBof();

                        break;
                    case self::XLS_TYPE_PRINTGRIDLINES:
                        $this->readPrintGridlines();

                        break;
                    case self::XLS_TYPE_DEFAULTROWHEIGHT:
                        $this->readDefaultRowHeight();

                        break;
                    case self::XLS_TYPE_SHEETPR:
                        $this->readSheetPr();

                        break;
                    case self::XLS_TYPE_HORIZONTALPAGEBREAKS:
                        $this->readHorizontalPageBreaks();

                        break;
                    case self::XLS_TYPE_VERTICALPAGEBREAKS:
                        $this->readVerticalPageBreaks();

                        break;
                    case self::XLS_TYPE_HEADER:
                        $this->readHeader();

                        break;
                    case self::XLS_TYPE_FOOTER:
                        $this->readFooter();

                        break;
                    case self::XLS_TYPE_HCENTER:
                        $this->readHcenter();

                        break;
                    case self::XLS_TYPE_VCENTER:
                        $this->readVcenter();

                        break;
                    case self::XLS_TYPE_LEFTMARGIN:
                        $this->readLeftMargin();

                        break;
                    case self::XLS_TYPE_RIGHTMARGIN:
                        $this->readRightMargin();

                        break;
                    case self::XLS_TYPE_TOPMARGIN:
                        $this->readTopMargin();

                        break;
                    case self::XLS_TYPE_BOTTOMMARGIN:
                        $this->readBottomMargin();

                        break;
                    case self::XLS_TYPE_PAGESETUP:
                        $this->readPageSetup();

                        break;
                    case self::XLS_TYPE_PROTECT:
                        $this->readProtect();

                        break;
                    case self::XLS_TYPE_SCENPROTECT:
                        $this->readScenProtect();

                        break;
                    case self::XLS_TYPE_OBJECTPROTECT:
                        $this->readObjectProtect();

                        break;
                    case self::XLS_TYPE_PASSWORD:
                        $this->readPassword();

                        break;
                    case self::XLS_TYPE_DEFCOLWIDTH:
                        $this->readDefColWidth();

                        break;
                    case self::XLS_TYPE_COLINFO:
                        $this->readColInfo();

                        break;
                    case self::XLS_TYPE_DIMENSION:
                        $this->readDefault();

                        break;
                    case self::XLS_TYPE_ROW:
                        $this->readRow();

                        break;
                    case self::XLS_TYPE_DBCELL:
                        $this->readDefault();

                        break;
                    case self::XLS_TYPE_RK:
                        $this->readRk();

                        break;
                    case self::XLS_TYPE_LABELSST:
                        $this->readLabelSst();

                        break;
                    case self::XLS_TYPE_MULRK:
                        $this->readMulRk();

                        break;
                    case self::XLS_TYPE_NUMBER:
                        $this->readNumber();

                        break;
                    case self::XLS_TYPE_FORMULA:
                        $this->readFormula();

                        break;
                    case self::XLS_TYPE_SHAREDFMLA:
                        $this->readSharedFmla();

                        break;
                    case self::XLS_TYPE_BOOLERR:
                        $this->readBoolErr();

                        break;
                    case self::XLS_TYPE_MULBLANK:
                        $this->readMulBlank();

                        break;
                    case self::XLS_TYPE_LABEL:
                        $this->readLabel();

                        break;
                    case self::XLS_TYPE_BLANK:
                        $this->readBlank();

                        break;
                    case self::XLS_TYPE_MSODRAWING:
                        $this->readMsoDrawing();

                        break;
                    case self::XLS_TYPE_OBJ:
                        $this->readObj();

                        break;
                    case self::XLS_TYPE_WINDOW2:
                        $this->readWindow2();

                        break;
                    case self::XLS_TYPE_PAGELAYOUTVIEW:
                        $this->readPageLayoutView();

                        break;
                    case self::XLS_TYPE_SCL:
                        $this->readScl();

                        break;
                    case self::XLS_TYPE_PANE:
                        $this->readPane();

                        break;
                    case self::XLS_TYPE_SELECTION:
                        $this->readSelection();

                        break;
                    case self::XLS_TYPE_MERGEDCELLS:
                        $this->readMergedCells();

                        break;
                    case self::XLS_TYPE_HYPERLINK:
                        $this->readHyperLink();

                        break;
                    case self::XLS_TYPE_DATAVALIDATIONS:
                        $this->readDataValidations();

                        break;
                    case self::XLS_TYPE_DATAVALIDATION:
                        $this->readDataValidation();

                        break;
                    case self::XLS_TYPE_SHEETLAYOUT:
                        $this->readSheetLayout();

                        break;
                    case self::XLS_TYPE_SHEETPROTECTION:
                        $this->readSheetProtection();

                        break;
                    case self::XLS_TYPE_RANGEPROTECTION:
                        $this->readRangeProtection();

                        break;
                    case self::XLS_TYPE_NOTE:
                        $this->readNote();

                        break;
                    case self::XLS_TYPE_TXO:
                        $this->readTextObject();

                        break;
                    case self::XLS_TYPE_CONTINUE:
                        $this->readContinue();

                        break;
                    case self::XLS_TYPE_EOF:
                        $this->readDefault();

                        break 2;
                    default:
                        $this->readDefault();

                        break;
                }
            }

            // treat MSODRAWING records, sheet-level Escher
            if (!$this->readDataOnly && $this->drawingData) {
                $escherWorksheet = new Escher();
                $reader = new Xls\Escher($escherWorksheet);
                $escherWorksheet = $reader->load($this->drawingData);

                // get all spContainers in one long array, so they can be mapped to OBJ records
                $allSpContainers = $escherWorksheet->getDgContainer()->getSpgrContainer()->getAllSpContainers();
            }

            // treat OBJ records
            foreach ($this->objs as $n => $obj) {
                // the first shape container never has a corresponding OBJ record, hence $n + 1
                if (isset($allSpContainers[$n + 1]) && is_object($allSpContainers[$n + 1])) {
                    $spContainer = $allSpContainers[$n + 1];

                    // we skip all spContainers that are a part of a group shape since we cannot yet handle those
                    if ($spContainer->getNestingLevel() > 1) {
                        continue;
                    }

                    // calculate the width and height of the shape
                    [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates());
                    [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates());

                    $startOffsetX = $spContainer->getStartOffsetX();
                    $startOffsetY = $spContainer->getStartOffsetY();
                    $endOffsetX = $spContainer->getEndOffsetX();
                    $endOffsetY = $spContainer->getEndOffsetY();

                    $width = SharedXls::getDistanceX($this->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX);
                    $height = SharedXls::getDistanceY($this->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY);

                    // calculate offsetX and offsetY of the shape
                    $offsetX = $startOffsetX * SharedXls::sizeCol($this->phpSheet, $startColumn) / 1024;
                    $offsetY = $startOffsetY * SharedXls::sizeRow($this->phpSheet, $startRow) / 256;

                    switch ($obj['otObjType']) {
                        case 0x19:
                            // Note
                            if (isset($this->cellNotes[$obj['idObjID']])) {
                                $cellNote = $this->cellNotes[$obj['idObjID']];

                                if (isset($this->textObjects[$obj['idObjID']])) {
                                    $textObject = $this->textObjects[$obj['idObjID']];
                                    $this->cellNotes[$obj['idObjID']]['objTextData'] = $textObject;
                                }
                            }

                            break;
                        case 0x08:
                            // picture
                            // get index to BSE entry (1-based)
                            $BSEindex = $spContainer->getOPT(0x0104);

                            // If there is no BSE Index, we will fail here and other fields are not read.
                            // Fix by checking here.
                            // TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field?
                            // More likely : a uncompatible picture
                            if (!$BSEindex) {
                                continue 2;
                            }

                            if ($escherWorkbook) {
                                $BSECollection = $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection();
                                $BSE = $BSECollection[$BSEindex - 1];
                                $blipType = $BSE->getBlipType();

                                // need check because some blip types are not supported by Escher reader such as EMF
                                if ($blip = $BSE->getBlip()) {
                                    $ih = imagecreatefromstring($blip->getData());
                                    if ($ih !== false) {
                                        $drawing = new MemoryDrawing();
                                        $drawing->setImageResource($ih);

                                        // width, height, offsetX, offsetY
                                        $drawing->setResizeProportional(false);
                                        $drawing->setWidth($width);
                                        $drawing->setHeight($height);
                                        $drawing->setOffsetX($offsetX);
                                        $drawing->setOffsetY($offsetY);

                                        switch ($blipType) {
                                            case BSE::BLIPTYPE_JPEG:
                                                $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG);
                                                $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG);

                                                break;
                                            case BSE::BLIPTYPE_PNG:
                                                imagealphablending($ih, false);
                                                imagesavealpha($ih, true);
                                                $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG);
                                                $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG);

                                                break;
                                        }

                                        $drawing->setWorksheet($this->phpSheet);
                                        $drawing->setCoordinates($spContainer->getStartCoordinates());
                                    }
                                }
                            }

                            break;
                        default:
                            // other object type
                            break;
                    }
                }
            }

            // treat SHAREDFMLA records
            if ($this->version == self::XLS_BIFF8) {
                foreach ($this->sharedFormulaParts as $cell => $baseCell) {
                    [$column, $row] = Coordinate::coordinateFromString($cell);
                    if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
                        $formula = $this->getFormulaFromStructure($this->sharedFormulas[$baseCell], $cell);
                        $this->phpSheet->getCell($cell)->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
                    }
                }
            }

            if (!empty($this->cellNotes)) {
                foreach ($this->cellNotes as $note => $noteDetails) {
                    if (!isset($noteDetails['objTextData'])) {
                        if (isset($this->textObjects[$note])) {
                            $textObject = $this->textObjects[$note];
                            $noteDetails['objTextData'] = $textObject;
                        } else {
                            $noteDetails['objTextData']['text'] = '';
                        }
                    }
                    $cellAddress = str_replace('$', '', $noteDetails['cellRef']);
                    $this->phpSheet->getComment($cellAddress)->setAuthor($noteDetails['author'])->setText($this->parseRichText($noteDetails['objTextData']['text']));
                }
            }
        }

        // add the named ranges (defined names)
        foreach ($this->definedname as $definedName) {
            if ($definedName['isBuiltInName']) {
                switch ($definedName['name']) {
                    case pack('C', 0x06):
                        // print area
                        //    in general, formula looks like this: Foo!$C$7:$J$66,Bar!$A$1:$IV$2
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?

                        $extractedRanges = [];
                        foreach ($ranges as $range) {
                            // $range should look like one of these
                            //        Foo!$C$7:$J$66
                            //        Bar!$A$1:$IV$2
                            $explodes = Worksheet::extractSheetTitle($range, true);
                            $sheetName = trim($explodes[0], "'");
                            if (count($explodes) == 2) {
                                if (strpos($explodes[1], ':') === false) {
                                    $explodes[1] = $explodes[1] . ':' . $explodes[1];
                                }
                                $extractedRanges[] = str_replace('$', '', $explodes[1]); // C7:J66
                            }
                        }
                        if ($docSheet = $this->spreadsheet->getSheetByName($sheetName)) {
                            $docSheet->getPageSetup()->setPrintArea(implode(',', $extractedRanges)); // C7:J66,A1:IV2
                        }

                        break;
                    case pack('C', 0x07):
                        // print titles (repeating rows)
                        // Assuming BIFF8, there are 3 cases
                        // 1. repeating rows
                        //        formula looks like this: Sheet!$A$1:$IV$2
                        //        rows 1-2 repeat
                        // 2. repeating columns
                        //        formula looks like this: Sheet!$A$1:$B$65536
                        //        columns A-B repeat
                        // 3. both repeating rows and repeating columns
                        //        formula looks like this: Sheet!$A$1:$B$65536,Sheet!$A$1:$IV$2
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
                        foreach ($ranges as $range) {
                            // $range should look like this one of these
                            //        Sheet!$A$1:$B$65536
                            //        Sheet!$A$1:$IV$2
                            if (strpos($range, '!') !== false) {
                                $explodes = Worksheet::extractSheetTitle($range, true);
                                if ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) {
                                    $extractedRange = $explodes[1];
                                    $extractedRange = str_replace('$', '', $extractedRange);

                                    $coordinateStrings = explode(':', $extractedRange);
                                    if (count($coordinateStrings) == 2) {
                                        [$firstColumn, $firstRow] = Coordinate::coordinateFromString($coordinateStrings[0]);
                                        [$lastColumn, $lastRow] = Coordinate::coordinateFromString($coordinateStrings[1]);

                                        if ($firstColumn == 'A' && $lastColumn == 'IV') {
                                            // then we have repeating rows
                                            $docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]);
                                        } elseif ($firstRow == 1 && $lastRow == 65536) {
                                            // then we have repeating columns
                                            $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]);
                                        }
                                    }
                                }
                            }
                        }

                        break;
                }
            } else {
                // Extract range
                if (strpos($definedName['formula'], '!') !== false) {
                    $explodes = Worksheet::extractSheetTitle($definedName['formula'], true);
                    if (
                        ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) ||
                        ($docSheet = $this->spreadsheet->getSheetByName(trim($explodes[0], "'")))
                    ) {
                        $extractedRange = $explodes[1];
                        $extractedRange = str_replace('$', '', $extractedRange);

                        $localOnly = ($definedName['scope'] == 0) ? false : true;

                        $scope = ($definedName['scope'] == 0) ? null : $this->spreadsheet->getSheetByName($this->sheets[$definedName['scope'] - 1]['name']);

                        $this->spreadsheet->addNamedRange(new NamedRange((string) $definedName['name'], $docSheet, $extractedRange, $localOnly, $scope));
                    }
                }
                //    Named Value
                    //    TODO Provide support for named values
            }
        }
        $this->data = '';

        return $this->spreadsheet;
    }

    /**
     * Read record data from stream, decrypting as required.
     *
     * @param string $data Data stream to read from
     * @param int $pos Position to start reading from
     * @param int $len Record data length
     *
     * @return string Record data
     */
    private function readRecordData($data, $pos, $len)
    {
        $data = substr($data, $pos, $len);

        // File not encrypted, or record before encryption start point
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
            return $data;
        }

        $recordData = '';
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
            $block = floor($pos / self::REKEY_BLOCK);
            $endBlock = floor(($pos + $len) / self::REKEY_BLOCK);

            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
            // at a point earlier in the current block, re-use it as we can save some time.
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
                $step = $pos % self::REKEY_BLOCK;
            } else {
                $step = $pos - $this->rc4Pos;
            }
            $this->rc4Key->RC4(str_repeat("\0", $step));

            // Decrypt record data (re-keying at the end of every block)
            while ($block != $endBlock) {
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
                $data = substr($data, $step);
                $pos += $step;
                $len -= $step;
                ++$block;
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
            }
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));

            // Keep track of the position of this decryptor.
            // We'll try and re-use it later if we can to speed things up
            $this->rc4Pos = $pos + $len;
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
            throw new Exception('XOr encryption not supported');
        }

        return $recordData;
    }

    /**
     * Use OLE reader to extract the relevant data streams from the OLE file.
     *
     * @param string $filename
     */
    private function loadOLE($filename): void
    {
        // OLE reader
        $ole = new OLERead();
        // get excel data,
        $ole->read($filename);
        // Get workbook data: workbook stream + sheet streams
        $this->data = $ole->getStream($ole->wrkbook);
        // Get summary information data
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
        // Get additional document summary information data
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
    }

    /**
     * Read summary information.
     */
    private function readSummaryInformation(): void
    {
        if (!isset($this->summaryInformation)) {
            return;
        }

        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
        // offset: 2; size: 2;
        // offset: 4; size: 2; OS version
        // offset: 6; size: 2; OS indicator
        // offset: 8; size: 16
        // offset: 24; size: 4; section count
        $secCount = self::getInt4d($this->summaryInformation, 24);

        // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9
        // offset: 44; size: 4
        $secOffset = self::getInt4d($this->summaryInformation, 44);

        // section header
        // offset: $secOffset; size: 4; section length
        $secLength = self::getInt4d($this->summaryInformation, $secOffset);

        // offset: $secOffset+4; size: 4; property count
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);

        // initialize code page (used to resolve string values)
        $codePage = 'CP1252';

        // offset: ($secOffset+8); size: var
        // loop through property decarations and properties
        for ($i = 0; $i < $countProperties; ++$i) {
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));

            // Use value of property id as appropriate
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));

            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);

            // initialize property value
            $value = null;

            // extract property value based on property type
            switch ($type) {
                case 0x02: // 2 byte signed integer
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);

                    break;
                case 0x03: // 4 byte signed integer
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);

                    break;
                case 0x13: // 4 byte unsigned integer
                    // not needed yet, fix later if necessary
                    break;
                case 0x1E: // null-terminated string prepended by dword string length
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
                    $value = rtrim($value);

                    break;
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
                    // PHP-time
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));

                    break;
                case 0x47: // Clipboard format
                    // not needed yet, fix later if necessary
                    break;
            }

            switch ($id) {
                case 0x01:    //    Code Page
                    $codePage = CodePage::numberToName((int) $value);

                    break;
                case 0x02:    //    Title
                    $this->spreadsheet->getProperties()->setTitle("$value");

                    break;
                case 0x03:    //    Subject
                    $this->spreadsheet->getProperties()->setSubject("$value");

                    break;
                case 0x04:    //    Author (Creator)
                    $this->spreadsheet->getProperties()->setCreator("$value");

                    break;
                case 0x05:    //    Keywords
                    $this->spreadsheet->getProperties()->setKeywords("$value");

                    break;
                case 0x06:    //    Comments (Description)
                    $this->spreadsheet->getProperties()->setDescription("$value");

                    break;
                case 0x07:    //    Template
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x08:    //    Last Saved By (LastModifiedBy)
                    $this->spreadsheet->getProperties()->setLastModifiedBy("$value");

                    break;
                case 0x09:    //    Revision
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x0A:    //    Total Editing Time
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x0B:    //    Last Printed
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x0C:    //    Created Date/Time
                    $this->spreadsheet->getProperties()->setCreated($value);

                    break;
                case 0x0D:    //    Modified Date/Time
                    $this->spreadsheet->getProperties()->setModified($value);

                    break;
                case 0x0E:    //    Number of Pages
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x0F:    //    Number of Words
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x10:    //    Number of Characters
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x11:    //    Thumbnail
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x12:    //    Name of creating application
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x13:    //    Security
                    //    Not supported by PhpSpreadsheet
                    break;
            }
        }
    }

    /**
     * Read additional document summary information.
     */
    private function readDocumentSummaryInformation(): void
    {
        if (!isset($this->documentSummaryInformation)) {
            return;
        }

        //    offset: 0;    size: 2;    must be 0xFE 0xFF (UTF-16 LE byte order mark)
        //    offset: 2;    size: 2;
        //    offset: 4;    size: 2;    OS version
        //    offset: 6;    size: 2;    OS indicator
        //    offset: 8;    size: 16
        //    offset: 24;    size: 4;    section count
        $secCount = self::getInt4d($this->documentSummaryInformation, 24);

        // offset: 28;    size: 16;    first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae
        // offset: 44;    size: 4;    first section offset
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);

        //    section header
        //    offset: $secOffset;    size: 4;    section length
        $secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);

        //    offset: $secOffset+4;    size: 4;    property count
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);

        // initialize code page (used to resolve string values)
        $codePage = 'CP1252';

        //    offset: ($secOffset+8);    size: var
        //    loop through property decarations and properties
        for ($i = 0; $i < $countProperties; ++$i) {
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));

            // Use value of property id as appropriate
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));

            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);

            // initialize property value
            $value = null;

            // extract property value based on property type
            switch ($type) {
                case 0x02:    //    2 byte signed integer
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);

                    break;
                case 0x03:    //    4 byte signed integer
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);

                    break;
                case 0x0B:  // Boolean
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
                    $value = ($value == 0 ? false : true);

                    break;
                case 0x13:    //    4 byte unsigned integer
                    // not needed yet, fix later if necessary
                    break;
                case 0x1E:    //    null-terminated string prepended by dword string length
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
                    $value = rtrim($value);

                    break;
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
                    // PHP-Time
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));

                    break;
                case 0x47:    //    Clipboard format
                    // not needed yet, fix later if necessary
                    break;
            }

            switch ($id) {
                case 0x01:    //    Code Page
                    $codePage = CodePage::numberToName((int) $value);

                    break;
                case 0x02:    //    Category
                    $this->spreadsheet->getProperties()->setCategory("$value");

                    break;
                case 0x03:    //    Presentation Target
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x04:    //    Bytes
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x05:    //    Lines
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x06:    //    Paragraphs
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x07:    //    Slides
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x08:    //    Notes
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x09:    //    Hidden Slides
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x0A:    //    MM Clips
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x0B:    //    Scale Crop
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x0C:    //    Heading Pairs
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x0D:    //    Titles of Parts
                    //    Not supported by PhpSpreadsheet
                    break;
                case 0x0E:    //    Manager
                    $this->spreadsheet->getProperties()->setManager("$value");

                    break;
                case 0x0F:    //    Company
                    $this->spreadsheet->getProperties()->setCompany("$value");

                    break;
                case 0x10:    //    Links up-to-date
                    //    Not supported by PhpSpreadsheet
                    break;
            }
        }
    }

    /**
     * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
     */
    private function readDefault(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);

        // move stream pointer to next record
        $this->pos += 4 + $length;
    }

    /**
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
     */
    private function readNote(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->readDataOnly) {
            return;
        }

        $cellAddress = $this->readBIFF8CellAddress(substr($recordData, 0, 4));
        if ($this->version == self::XLS_BIFF8) {
            $noteObjID = self::getUInt2d($recordData, 6);
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
            $noteAuthor = $noteAuthor['value'];
            $this->cellNotes[$noteObjID] = [
                'cellRef' => $cellAddress,
                'objectID' => $noteObjID,
                'author' => $noteAuthor,
            ];
        } else {
            $extension = false;
            if ($cellAddress == '$B$65536') {
                //    If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
                //        note from the previous cell annotation. We're not yet handling this, so annotations longer than the
                //        max 2048 bytes will probably throw a wobbly.
                $row = self::getUInt2d($recordData, 0);
                $extension = true;
                $arrayKeys = array_keys($this->phpSheet->getComments());
                $cellAddress = array_pop($arrayKeys);
            }

            $cellAddress = str_replace('$', '', $cellAddress);
            $noteLength = self::getUInt2d($recordData, 4);
            $noteText = trim(substr($recordData, 6));

            if ($extension) {
                //    Concatenate this extension with the currently set comment for the cell
                $comment = $this->phpSheet->getComment($cellAddress);
                $commentText = $comment->getText()->getPlainText();
                $comment->setText($this->parseRichText($commentText . $noteText));
            } else {
                //    Set comment for the cell
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
//                                                    ->setAuthor($author)
            }
        }
    }

    /**
     * The TEXT Object record contains the text associated with a cell annotation.
     */
    private function readTextObject(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->readDataOnly) {
            return;
        }

        // recordData consists of an array of subrecords looking like this:
        //    grbit: 2 bytes; Option Flags
        //    rot: 2 bytes; rotation
        //    cchText: 2 bytes; length of the text (in the first continue record)
        //    cbRuns: 2 bytes; length of the formatting (in the second continue record)
        // followed by the continuation records containing the actual text and formatting
        $grbitOpts = self::getUInt2d($recordData, 0);
        $rot = self::getUInt2d($recordData, 2);
        $cchText = self::getUInt2d($recordData, 10);
        $cbRuns = self::getUInt2d($recordData, 12);
        $text = $this->getSplicedRecordData();

        $textByte = $text['spliceOffsets'][1] - $text['spliceOffsets'][0] - 1;
        $textStr = substr($text['recordData'], $text['spliceOffsets'][0] + 1, $textByte);
        // get 1 byte
        $is16Bit = ord($text['recordData'][0]);
        // it is possible to use a compressed format,
        // which omits the high bytes of all characters, if they are all zero
        if (($is16Bit & 0x01) === 0) {
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
        } else {
            $textStr = $this->decodeCodepage($textStr);
        }

        $this->textObjects[$this->textObjRef] = [
            'text' => $textStr,
            'format' => substr($text['recordData'], $text['spliceOffsets'][1], $cbRuns),
            'alignment' => $grbitOpts,
            'rotation' => $rot,
        ];
    }

    /**
     * Read BOF.
     */
    private function readBof(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = substr($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 2; size: 2; type of the following data
        $substreamType = self::getUInt2d($recordData, 2);

        switch ($substreamType) {
            case self::XLS_WORKBOOKGLOBALS:
                $version = self::getUInt2d($recordData, 0);
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
                    throw new Exception('Cannot read this Excel file. Version is too old.');
                }
                $this->version = $version;

                break;
            case self::XLS_WORKSHEET:
                // do not use this version information for anything
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
                break;
            default:
                // substream, e.g. chart
                // just skip the entire substream
                do {
                    $code = self::getUInt2d($this->data, $this->pos);
                    $this->readDefault();
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);

                break;
        }
    }

    /**
     * FILEPASS.
     *
     * This record is part of the File Protection Block. It
     * contains information about the read/write password of the
     * file. All record contents following this record will be
     * encrypted.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     *
     * The decryption functions and objects used from here on in
     * are based on the source of Spreadsheet-ParseExcel:
     * https://metacpan.org/release/Spreadsheet-ParseExcel
     */
    private function readFilepass(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);

        if ($length != 54) {
            throw new Exception('Unexpected file pass record length');
        }

        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->verifyPassword('VelvetSweatshop', substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
            throw new Exception('Decryption password incorrect');
        }

        $this->encryption = self::MS_BIFF_CRYPTO_RC4;

        // Decryption required from the record after next onwards
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
    }

    /**
     * Make an RC4 decryptor for the given block.
     *
     * @param int $block Block for which to create decrypto
     * @param string $valContext MD5 context state
     *
     * @return Xls\RC4
     */
    private function makeKey($block, $valContext)
    {
        $pwarray = str_repeat("\0", 64);

        for ($i = 0; $i < 5; ++$i) {
            $pwarray[$i] = $valContext[$i];
        }

        $pwarray[5] = chr($block & 0xff);
        $pwarray[6] = chr(($block >> 8) & 0xff);
        $pwarray[7] = chr(($block >> 16) & 0xff);
        $pwarray[8] = chr(($block >> 24) & 0xff);

        $pwarray[9] = "\x80";
        $pwarray[56] = "\x48";

        $md5 = new Xls\MD5();
        $md5->add($pwarray);

        $s = $md5->getContext();

        return new Xls\RC4($s);
    }

    /**
     * Verify RC4 file password.
     *
     * @param string $password Password to check
     * @param string $docid Document id
     * @param string $salt_data Salt data
     * @param string $hashedsalt_data Hashed salt data
     * @param string $valContext Set to the MD5 context of the value
     *
     * @return bool Success
     */
    private function verifyPassword($password, $docid, $salt_data, $hashedsalt_data, &$valContext)
    {
        $pwarray = str_repeat("\0", 64);

        $iMax = strlen($password);
        for ($i = 0; $i < $iMax; ++$i) {
            $o = ord(substr($password, $i, 1));
            $pwarray[2 * $i] = chr($o & 0xff);
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xff);
        }
        $pwarray[2 * $i] = chr(0x80);
        $pwarray[56] = chr(($i << 4) & 0xff);

        $md5 = new Xls\MD5();
        $md5->add($pwarray);

        $mdContext1 = $md5->getContext();

        $offset = 0;
        $keyoffset = 0;
        $tocopy = 5;

        $md5->reset();

        while ($offset != 16) {
            if ((64 - $offset) < 5) {
                $tocopy = 64 - $offset;
            }
            for ($i = 0; $i <= $tocopy; ++$i) {
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
            }
            $offset += $tocopy;

            if ($offset == 64) {
                $md5->add($pwarray);
                $keyoffset = $tocopy;
                $tocopy = 5 - $tocopy;
                $offset = 0;

                continue;
            }

            $keyoffset = 0;
            $tocopy = 5;
            for ($i = 0; $i < 16; ++$i) {
                $pwarray[$offset + $i] = $docid[$i];
            }
            $offset += 16;
        }

        $pwarray[16] = "\x80";
        for ($i = 0; $i < 47; ++$i) {
            $pwarray[17 + $i] = "\0";
        }
        $pwarray[56] = "\x80";
        $pwarray[57] = "\x0a";

        $md5->add($pwarray);
        $valContext = $md5->getContext();

        $key = $this->makeKey(0, $valContext);

        $salt = $key->RC4($salt_data);
        $hashedsalt = $key->RC4($hashedsalt_data);

        $salt .= "\x80" . str_repeat("\0", 47);
        $salt[56] = "\x80";

        $md5->reset();
        $md5->add($salt);
        $mdContext2 = $md5->getContext();

        return $mdContext2 == $hashedsalt;
    }

    /**
     * CODEPAGE.
     *
     * This record stores the text encoding used to write byte
     * strings, stored as MS Windows code page identifier.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readCodepage(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; code page identifier
        $codepage = self::getUInt2d($recordData, 0);

        $this->codepage = CodePage::numberToName($codepage);
    }

    /**
     * DATEMODE.
     *
     * This record specifies the base date for displaying date
     * values. All dates are stored as count of days past this
     * base date. In BIFF2-BIFF4 this record is part of the
     * Calculation Settings Block. In BIFF5-BIFF8 it is
     * stored in the Workbook Globals Substream.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readDateMode(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
        if (ord($recordData[0]) == 1) {
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
        }
    }

    /**
     * Read a FONT record.
     */
    private function readFont(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            $objFont = new Font();

            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
            $size = self::getUInt2d($recordData, 0);
            $objFont->setSize($size / 20);

            // offset: 2; size: 2; option flags
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
            // bit: 1; mask 0x0002; italic
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
            if ($isItalic) {
                $objFont->setItalic(true);
            }

            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
            // bit: 3; mask 0x0008; strikethrough
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
            if ($isStrike) {
                $objFont->setStrikethrough(true);
            }

            // offset: 4; size: 2; colour index
            $colorIndex = self::getUInt2d($recordData, 4);
            $objFont->colorIndex = $colorIndex;

            // offset: 6; size: 2; font weight
            $weight = self::getUInt2d($recordData, 6);
            switch ($weight) {
                case 0x02BC:
                    $objFont->setBold(true);

                    break;
            }

            // offset: 8; size: 2; escapement type
            $escapement = self::getUInt2d($recordData, 8);
            CellFont::escapement($objFont, $escapement);

            // offset: 10; size: 1; underline type
            $underlineType = ord($recordData[10]);
            CellFont::underline($objFont, $underlineType);

            // offset: 11; size: 1; font family
            // offset: 12; size: 1; character set
            // offset: 13; size: 1; not used
            // offset: 14; size: var; font name
            if ($this->version == self::XLS_BIFF8) {
                $string = self::readUnicodeStringShort(substr($recordData, 14));
            } else {
                $string = $this->readByteStringShort(substr($recordData, 14));
            }
            $objFont->setName($string['value']);

            $this->objFonts[] = $objFont;
        }
    }

    /**
     * FORMAT.
     *
     * This record contains information about a number format.
     * All FORMAT records occur together in a sequential list.
     *
     * In BIFF2-BIFF4 other records referencing a FORMAT record
     * contain a zero-based index into this list. From BIFF5 on
     * the FORMAT record contains the index itself that will be
     * used by other records.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readFormat(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            $indexCode = self::getUInt2d($recordData, 0);

            if ($this->version == self::XLS_BIFF8) {
                $string = self::readUnicodeStringLong(substr($recordData, 2));
            } else {
                // BIFF7
                $string = $this->readByteStringShort(substr($recordData, 2));
            }

            $formatString = $string['value'];
            // Apache Open Office sets wrong case writing to xls - issue 2239
            if ($formatString === 'GENERAL') {
                $formatString = NumberFormat::FORMAT_GENERAL;
            }
            $this->formats[$indexCode] = $formatString;
        }
    }

    /**
     * XF - Extended Format.
     *
     * This record contains formatting information for cells, rows, columns or styles.
     * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF
     * and 1 cell XF.
     * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF
     * and XF record 15 is a cell XF
     * We only read the first cell style XF and skip the remaining cell style XF records
     * We read all cell XF records.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readXf(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        $objStyle = new Style();

        if (!$this->readDataOnly) {
            // offset:  0; size: 2; Index to FONT record
            if (self::getUInt2d($recordData, 0) < 4) {
                $fontIndex = self::getUInt2d($recordData, 0);
            } else {
                // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
                // check the OpenOffice documentation of the FONT record
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
            }
            $objStyle->setFont($this->objFonts[$fontIndex]);

            // offset:  2; size: 2; Index to FORMAT record
            $numberFormatIndex = self::getUInt2d($recordData, 2);
            if (isset($this->formats[$numberFormatIndex])) {
                // then we have user-defined format code
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
                // then we have built-in format code
                $numberFormat = ['formatCode' => $code];
            } else {
                // we set the general format code
                $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL];
            }
            $objStyle->getNumberFormat()->setFormatCode($numberFormat['formatCode']);

            // offset:  4; size: 2; XF type, cell protection, and parent style XF
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
            $xfTypeProt = self::getUInt2d($recordData, 4);
            // bit 0; mask 0x01; 1 = cell is locked
            $isLocked = (0x01 & $xfTypeProt) >> 0;
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);

            // bit 1; mask 0x02; 1 = Formula is hidden
            $isHidden = (0x02 & $xfTypeProt) >> 1;
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);

            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;

            // offset:  6; size: 1; Alignment and text break
            // bit 2-0, mask 0x07; horizontal alignment
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
            Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign);

            // bit 3, mask 0x08; wrap text
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
            Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText);

            // bit 6-4, mask 0x70; vertical alignment
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
            Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign);

            if ($this->version == self::XLS_BIFF8) {
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
                $angle = ord($recordData[7]);
                $rotation = 0;
                if ($angle <= 90) {
                    $rotation = $angle;
                } elseif ($angle <= 180) {
                    $rotation = 90 - $angle;
                } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) {
                    $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET;
                }
                $objStyle->getAlignment()->setTextRotation($rotation);

                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
                // bit: 3-0; mask: 0x0F; indent level
                $indent = (0x0F & ord($recordData[8])) >> 0;
                $objStyle->getAlignment()->setIndent($indent);

                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
                switch ($shrinkToFit) {
                    case 0:
                        $objStyle->getAlignment()->setShrinkToFit(false);

                        break;
                    case 1:
                        $objStyle->getAlignment()->setShrinkToFit(true);

                        break;
                }

                // offset:  9; size: 1; Flags used for attribute groups

                // offset: 10; size: 4; Cell border lines and background area
                // bit: 3-0; mask: 0x0000000F; left style
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
                }
                // bit: 7-4; mask: 0x000000F0; right style
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
                }
                // bit: 11-8; mask: 0x00000F00; top style
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
                }
                // bit: 15-12; mask: 0x0000F000; bottom style
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
                }
                // bit: 22-16; mask: 0x007F0000; left color
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;

                // bit: 29-23; mask: 0x3F800000; right color
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;

                // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom
                $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false;

                // bit: 31; mask: 0x80000000; 1 = diagonal line from bottom left to top right
                $diagonalUp = (0x80000000 & self::getInt4d($recordData, 10)) >> 31 ? true : false;

                if ($diagonalUp == false && $diagonalDown == false) {
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
                } elseif ($diagonalUp == true && $diagonalDown == false) {
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
                } elseif ($diagonalUp == false && $diagonalDown == true) {
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
                } elseif ($diagonalUp == true && $diagonalDown == true) {
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
                }

                // offset: 14; size: 4;
                // bit: 6-0; mask: 0x0000007F; top color
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;

                // bit: 13-7; mask: 0x00003F80; bottom color
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;

                // bit: 20-14; mask: 0x001FC000; diagonal color
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;

                // bit: 24-21; mask: 0x01E00000; diagonal style
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
                }

                // bit: 31-26; mask: 0xFC000000 fill pattern
                if ($fillType = Xls\Style\FillPattern::lookup((0xFC000000 & self::getInt4d($recordData, 14)) >> 26)) {
                    $objStyle->getFill()->setFillType($fillType);
                }
                // offset: 18; size: 2; pattern and background colour
                // bit: 6-0; mask: 0x007F; color index for pattern color
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;

                // bit: 13-7; mask: 0x3F80; color index for pattern background
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
            } else {
                // BIFF5

                // offset: 7; size: 1; Text orientation and flags
                $orientationAndFlags = ord($recordData[7]);

                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
                switch ($xfOrientation) {
                    case 0:
                        $objStyle->getAlignment()->setTextRotation(0);

                        break;
                    case 1:
                        $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET);

                        break;
                    case 2:
                        $objStyle->getAlignment()->setTextRotation(90);

                        break;
                    case 3:
                        $objStyle->getAlignment()->setTextRotation(-90);

                        break;
                }

                // offset: 8; size: 4; cell border lines and background area
                $borderAndBackground = self::getInt4d($recordData, 8);

                // bit: 6-0; mask: 0x0000007F; color index for pattern color
                $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0;

                // bit: 13-7; mask: 0x00003F80; color index for pattern background
                $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7;

                // bit: 21-16; mask: 0x003F0000; fill pattern
                $objStyle->getFill()->setFillType(Xls\Style\FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16));

                // bit: 24-22; mask: 0x01C00000; bottom line style
                $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22));

                // bit: 31-25; mask: 0xFE000000; bottom line color
                $objStyle->getBorders()->getBottom()->colorIndex = (0xFE000000 & $borderAndBackground) >> 25;

                // offset: 12; size: 4; cell border lines
                $borderLines = self::getInt4d($recordData, 12);

                // bit: 2-0; mask: 0x00000007; top line style
                $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0));

                // bit: 5-3; mask: 0x00000038; left line style
                $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3));

                // bit: 8-6; mask: 0x000001C0; right line style
                $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6));

                // bit: 15-9; mask: 0x0000FE00; top line color index
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9;

                // bit: 22-16; mask: 0x007F0000; left line color index
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16;

                // bit: 29-23; mask: 0x3F800000; right line color index
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
            }

            // add cellStyleXf or cellXf and update mapping
            if ($isCellStyleXf) {
                // we only read one style XF record which is always the first
                if ($this->xfIndex == 0) {
                    $this->spreadsheet->addCellStyleXf($objStyle);
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
                }
            } else {
                // we read all cell XF records
                $this->spreadsheet->addCellXf($objStyle);
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
            }

            // update XF index for when we read next record
            ++$this->xfIndex;
        }
    }

    private function readXfExt(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; 0x087D = repeated header

            // offset: 2; size: 2

            // offset: 4; size: 8; not used

            // offset: 12; size: 2; record version

            // offset: 14; size: 2; index to XF record which this record modifies
            $ixfe = self::getUInt2d($recordData, 14);

            // offset: 16; size: 2; not used

            // offset: 18; size: 2; number of extension properties that follow
            $cexts = self::getUInt2d($recordData, 18);

            // start reading the actual extension data
            $offset = 20;
            while ($offset < $length) {
                // extension type
                $extType = self::getUInt2d($recordData, $offset);

                // extension length
                $cb = self::getUInt2d($recordData, $offset + 2);

                // extension data
                $extData = substr($recordData, $offset + 4, $cb);

                switch ($extType) {
                    case 4:        // fill start color
                        $xclfType = self::getUInt2d($extData, 0); // color type
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)

                        if ($xclfType == 2) {
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));

                            // modify the relevant style property
                            if (isset($this->mapCellXfIndex[$ixfe])) {
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
                                $fill->getStartColor()->setRGB($rgb);
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
                            }
                        }

                        break;
                    case 5:        // fill end color
                        $xclfType = self::getUInt2d($extData, 0); // color type
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)

                        if ($xclfType == 2) {
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));

                            // modify the relevant style property
                            if (isset($this->mapCellXfIndex[$ixfe])) {
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
                                $fill->getEndColor()->setRGB($rgb);
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
                            }
                        }

                        break;
                    case 7:        // border color top
                        $xclfType = self::getUInt2d($extData, 0); // color type
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)

                        if ($xclfType == 2) {
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));

                            // modify the relevant style property
                            if (isset($this->mapCellXfIndex[$ixfe])) {
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
                                $top->getColor()->setRGB($rgb);
                                $top->colorIndex = null; // normal color index does not apply, discard
                            }
                        }

                        break;
                    case 8:        // border color bottom
                        $xclfType = self::getUInt2d($extData, 0); // color type
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)

                        if ($xclfType == 2) {
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));

                            // modify the relevant style property
                            if (isset($this->mapCellXfIndex[$ixfe])) {
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
                                $bottom->getColor()->setRGB($rgb);
                                $bottom->colorIndex = null; // normal color index does not apply, discard
                            }
                        }

                        break;
                    case 9:        // border color left
                        $xclfType = self::getUInt2d($extData, 0); // color type
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)

                        if ($xclfType == 2) {
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));

                            // modify the relevant style property
                            if (isset($this->mapCellXfIndex[$ixfe])) {
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
                                $left->getColor()->setRGB($rgb);
                                $left->colorIndex = null; // normal color index does not apply, discard
                            }
                        }

                        break;
                    case 10:        // border color right
                        $xclfType = self::getUInt2d($extData, 0); // color type
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)

                        if ($xclfType == 2) {
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));

                            // modify the relevant style property
                            if (isset($this->mapCellXfIndex[$ixfe])) {
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
                                $right->getColor()->setRGB($rgb);
                                $right->colorIndex = null; // normal color index does not apply, discard
                            }
                        }

                        break;
                    case 11:        // border color diagonal
                        $xclfType = self::getUInt2d($extData, 0); // color type
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)

                        if ($xclfType == 2) {
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));

                            // modify the relevant style property
                            if (isset($this->mapCellXfIndex[$ixfe])) {
                                $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal();
                                $diagonal->getColor()->setRGB($rgb);
                                $diagonal->colorIndex = null; // normal color index does not apply, discard
                            }
                        }

                        break;
                    case 13:    // font color
                        $xclfType = self::getUInt2d($extData, 0); // color type
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)

                        if ($xclfType == 2) {
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));

                            // modify the relevant style property
                            if (isset($this->mapCellXfIndex[$ixfe])) {
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
                                $font->getColor()->setRGB($rgb);
                                $font->colorIndex = null; // normal color index does not apply, discard
                            }
                        }

                        break;
                }

                $offset += $cb;
            }
        }
    }

    /**
     * Read STYLE record.
     */
    private function readStyle(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; index to XF record and flag for built-in style
            $ixfe = self::getUInt2d($recordData, 0);

            // bit: 11-0; mask 0x0FFF; index to XF record
            $xfIndex = (0x0FFF & $ixfe) >> 0;

            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);

            if ($isBuiltIn) {
                // offset: 2; size: 1; identifier for built-in style
                $builtInId = ord($recordData[2]);

                switch ($builtInId) {
                    case 0x00:
                        // currently, we are not using this for anything
                        break;
                    default:
                        break;
                }
            }
            // user-defined; not supported by PhpSpreadsheet
        }
    }

    /**
     * Read PALETTE record.
     */
    private function readPalette(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; number of following colors
            $nm = self::getUInt2d($recordData, 0);

            // list of RGB colors
            for ($i = 0; $i < $nm; ++$i) {
                $rgb = substr($recordData, 2 + 4 * $i, 4);
                $this->palette[] = self::readRGB($rgb);
            }
        }
    }

    /**
     * SHEET.
     *
     * This record is  located in the  Workbook Globals
     * Substream  and represents a sheet inside the workbook.
     * One SHEET record is written for each sheet. It stores the
     * sheet name and a stream offset to the BOF record of the
     * respective Sheet Substream within the Workbook Stream.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readSheet(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
        // NOTE: not encrypted
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 4; size: 1; sheet state
        switch (ord($recordData[4])) {
            case 0x00:
                $sheetState = Worksheet::SHEETSTATE_VISIBLE;

                break;
            case 0x01:
                $sheetState = Worksheet::SHEETSTATE_HIDDEN;

                break;
            case 0x02:
                $sheetState = Worksheet::SHEETSTATE_VERYHIDDEN;

                break;
            default:
                $sheetState = Worksheet::SHEETSTATE_VISIBLE;

                break;
        }

        // offset: 5; size: 1; sheet type
        $sheetType = ord($recordData[5]);

        // offset: 6; size: var; sheet name
        $rec_name = null;
        if ($this->version == self::XLS_BIFF8) {
            $string = self::readUnicodeStringShort(substr($recordData, 6));
            $rec_name = $string['value'];
        } elseif ($this->version == self::XLS_BIFF7) {
            $string = $this->readByteStringShort(substr($recordData, 6));
            $rec_name = $string['value'];
        }

        $this->sheets[] = [
            'name' => $rec_name,
            'offset' => $rec_offset,
            'sheetState' => $sheetState,
            'sheetType' => $sheetType,
        ];
    }

    /**
     * Read EXTERNALBOOK record.
     */
    private function readExternalBook(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset within record data
        $offset = 0;

        // there are 4 types of records
        if (strlen($recordData) > 4) {
            // external reference
            // offset: 0; size: 2; number of sheet names ($nm)
            $nm = self::getUInt2d($recordData, 0);
            $offset += 2;

            // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length)
            $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2));
            $offset += $encodedUrlString['size'];

            // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length)
            $externalSheetNames = [];
            for ($i = 0; $i < $nm; ++$i) {
                $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset));
                $externalSheetNames[] = $externalSheetNameString['value'];
                $offset += $externalSheetNameString['size'];
            }

            // store the record data
            $this->externalBooks[] = [
                'type' => 'external',
                'encodedUrl' => $encodedUrlString['value'],
                'externalSheetNames' => $externalSheetNames,
            ];
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
            // internal reference
            // offset: 0; size: 2; number of sheet in this document
            // offset: 2; size: 2; 0x01 0x04
            $this->externalBooks[] = [
                'type' => 'internal',
            ];
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
            // add-in function
            // offset: 0; size: 2; 0x0001
            $this->externalBooks[] = [
                'type' => 'addInFunction',
            ];
        } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) {
            // DDE links, OLE links
            // offset: 0; size: 2; 0x0000
            // offset: 2; size: var; encoded source document name
            $this->externalBooks[] = [
                'type' => 'DDEorOLE',
            ];
        }
    }

    /**
     * Read EXTERNNAME record.
     */
    private function readExternName(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // external sheet references provided for named cells
        if ($this->version == self::XLS_BIFF8) {
            // offset: 0; size: 2; options
            $options = self::getUInt2d($recordData, 0);

            // offset: 2; size: 2;

            // offset: 4; size: 2; not used

            // offset: 6; size: var
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));

            // offset: var; size: var; formula data
            $offset = 6 + $nameString['size'];
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));

            $this->externalNames[] = [
                'name' => $nameString['value'],
                'formula' => $formula,
            ];
        }
    }

    /**
     * Read EXTERNSHEET record.
     */
    private function readExternSheet(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // external sheet references provided for named cells
        if ($this->version == self::XLS_BIFF8) {
            // offset: 0; size: 2; number of following ref structures
            $nm = self::getUInt2d($recordData, 0);
            for ($i = 0; $i < $nm; ++$i) {
                $this->ref[] = [
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
                ];
            }
        }
    }

    /**
     * DEFINEDNAME.
     *
     * This record is part of a Link Table. It contains the name
     * and the token array of an internal defined name. Token
     * arrays of defined names contain tokens with aberrant
     * token classes.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readDefinedName(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->version == self::XLS_BIFF8) {
            // retrieves named cells

            // offset: 0; size: 2; option flags
            $opts = self::getUInt2d($recordData, 0);

            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
            $isBuiltInName = (0x0020 & $opts) >> 5;

            // offset: 2; size: 1; keyboard shortcut

            // offset: 3; size: 1; length of the name (character count)
            $nlen = ord($recordData[3]);

            // offset: 4; size: 2; size of the formula data (it can happen that this is zero)
            // note: there can also be additional data, this is not included in $flen
            $flen = self::getUInt2d($recordData, 4);

            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
            $scope = self::getUInt2d($recordData, 8);

            // offset: 14; size: var; Name (Unicode string without length field)
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);

            // offset: var; size: $flen; formula data
            $offset = 14 + $string['size'];
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);

            try {
                $formula = $this->getFormulaFromStructure($formulaStructure);
            } catch (PhpSpreadsheetException $e) {
                $formula = '';
            }

            $this->definedname[] = [
                'isBuiltInName' => $isBuiltInName,
                'name' => $string['value'],
                'formula' => $formula,
                'scope' => $scope,
            ];
        }
    }

    /**
     * Read MSODRAWINGGROUP record.
     */
    private function readMsoDrawingGroup(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);

        // get spliced record data
        $splicedRecordData = $this->getSplicedRecordData();
        $recordData = $splicedRecordData['recordData'];

        $this->drawingGroupData .= $recordData;
    }

    /**
     * SST - Shared String Table.
     *
     * This record contains a list of all strings used anywhere
     * in the workbook. Each string occurs only once. The
     * workbook uses indexes into the list to reference the
     * strings.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readSst(): void
    {
        // offset within (spliced) record data
        $pos = 0;

        // Limit global SST position, further control for bad SST Length in BIFF8 data
        $limitposSST = 0;

        // get spliced record data
        $splicedRecordData = $this->getSplicedRecordData();

        $recordData = $splicedRecordData['recordData'];
        $spliceOffsets = $splicedRecordData['spliceOffsets'];

        // offset: 0; size: 4; total number of strings in the workbook
        $pos += 4;

        // offset: 4; size: 4; number of following strings ($nm)
        $nm = self::getInt4d($recordData, 4);
        $pos += 4;

        // look up limit position
        foreach ($spliceOffsets as $spliceOffset) {
            // it can happen that the string is empty, therefore we need
            // <= and not just <
            if ($pos <= $spliceOffset) {
                $limitposSST = $spliceOffset;
            }
        }

        // loop through the Unicode strings (16-bit length)
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
            // number of characters in the Unicode string
            $numChars = self::getUInt2d($recordData, $pos);
            $pos += 2;

            // option flags
            $optionFlags = ord($recordData[$pos]);
            ++$pos;

            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
            $isCompressed = (($optionFlags & 0x01) == 0);

            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
            $hasAsian = (($optionFlags & 0x04) != 0);

            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
            $hasRichText = (($optionFlags & 0x08) != 0);

            $formattingRuns = 0;
            if ($hasRichText) {
                // number of Rich-Text formatting runs
                $formattingRuns = self::getUInt2d($recordData, $pos);
                $pos += 2;
            }

            $extendedRunLength = 0;
            if ($hasAsian) {
                // size of Asian phonetic setting
                $extendedRunLength = self::getInt4d($recordData, $pos);
                $pos += 4;
            }

            // expected byte length of character array if not split
            $len = ($isCompressed) ? $numChars : $numChars * 2;

            // look up limit position - Check it again to be sure that no error occurs when parsing SST structure
            $limitpos = null;
            foreach ($spliceOffsets as $spliceOffset) {
                // it can happen that the string is empty, therefore we need
                // <= and not just <
                if ($pos <= $spliceOffset) {
                    $limitpos = $spliceOffset;

                    break;
                }
            }

            if ($pos + $len <= $limitpos) {
                // character array is not split between records

                $retstr = substr($recordData, $pos, $len);
                $pos += $len;
            } else {
                // character array is split between records

                // first part of character array
                $retstr = substr($recordData, $pos, $limitpos - $pos);

                $bytesRead = $limitpos - $pos;

                // remaining characters in Unicode string
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));

                $pos = $limitpos;

                // keep reading the characters
                while ($charsLeft > 0) {
                    // look up next limit position, in case the string span more than one continue record
                    foreach ($spliceOffsets as $spliceOffset) {
                        if ($pos < $spliceOffset) {
                            $limitpos = $spliceOffset;

                            break;
                        }
                    }

                    // repeated option flags
                    // OpenOffice.org documentation 5.21
                    $option = ord($recordData[$pos]);
                    ++$pos;

                    if ($isCompressed && ($option == 0)) {
                        // 1st fragment compressed
                        // this fragment compressed
                        $len = min($charsLeft, $limitpos - $pos);
                        $retstr .= substr($recordData, $pos, $len);
                        $charsLeft -= $len;
                        $isCompressed = true;
                    } elseif (!$isCompressed && ($option != 0)) {
                        // 1st fragment uncompressed
                        // this fragment uncompressed
                        $len = min($charsLeft * 2, $limitpos - $pos);
                        $retstr .= substr($recordData, $pos, $len);
                        $charsLeft -= $len / 2;
                        $isCompressed = false;
                    } elseif (!$isCompressed && ($option == 0)) {
                        // 1st fragment uncompressed
                        // this fragment compressed
                        $len = min($charsLeft, $limitpos - $pos);
                        for ($j = 0; $j < $len; ++$j) {
                            $retstr .= $recordData[$pos + $j]
                            . chr(0);
                        }
                        $charsLeft -= $len;
                        $isCompressed = false;
                    } else {
                        // 1st fragment compressed
                        // this fragment uncompressed
                        $newstr = '';
                        $jMax = strlen($retstr);
                        for ($j = 0; $j < $jMax; ++$j) {
                            $newstr .= $retstr[$j] . chr(0);
                        }
                        $retstr = $newstr;
                        $len = min($charsLeft * 2, $limitpos - $pos);
                        $retstr .= substr($recordData, $pos, $len);
                        $charsLeft -= $len / 2;
                        $isCompressed = false;
                    }

                    $pos += $len;
                }
            }

            // convert to UTF-8
            $retstr = self::encodeUTF16($retstr, $isCompressed);

            // read additional Rich-Text information, if any
            $fmtRuns = [];
            if ($hasRichText) {
                // list of formatting runs
                for ($j = 0; $j < $formattingRuns; ++$j) {
                    // first formatted character; zero-based
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);

                    // index to font record
                    $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4);

                    $fmtRuns[] = [
                        'charPos' => $charPos,
                        'fontIndex' => $fontIndex,
                    ];
                }
                $pos += 4 * $formattingRuns;
            }

            // read additional Asian phonetics information, if any
            if ($hasAsian) {
                // For Asian phonetic settings, we skip the extended string data
                $pos += $extendedRunLength;
            }

            // store the shared sting
            $this->sst[] = [
                'value' => $retstr,
                'fmtRuns' => $fmtRuns,
            ];
        }

        // getSplicedRecordData() takes care of moving current position in data stream
    }

    /**
     * Read PRINTGRIDLINES record.
     */
    private function readPrintGridlines(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
            $this->phpSheet->setPrintGridlines($printGridlines);
        }
    }

    /**
     * Read DEFAULTROWHEIGHT record.
     */
    private function readDefaultRowHeight(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; option flags
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
        $height = self::getUInt2d($recordData, 2);
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
    }

    /**
     * Read SHEETPR record.
     */
    private function readSheetPr(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2

        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
        $this->phpSheet->setShowSummaryBelow($isSummaryBelow);

        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
        $this->phpSheet->setShowSummaryRight($isSummaryRight);

        // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages
        // this corresponds to radio button setting in page setup dialog in Excel
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
    }

    /**
     * Read HORIZONTALPAGEBREAKS record.
     */
    private function readHorizontalPageBreaks(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
            // offset: 0; size: 2; number of the following row index structures
            $nm = self::getUInt2d($recordData, 0);

            // offset: 2; size: 6 * $nm; list of $nm row index structures
            for ($i = 0; $i < $nm; ++$i) {
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
                $cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);

                // not sure why two column indexes are necessary?
                $this->phpSheet->setBreakByColumnAndRow($cf + 1, $r, Worksheet::BREAK_ROW);
            }
        }
    }

    /**
     * Read VERTICALPAGEBREAKS record.
     */
    private function readVerticalPageBreaks(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
            // offset: 0; size: 2; number of the following column index structures
            $nm = self::getUInt2d($recordData, 0);

            // offset: 2; size: 6 * $nm; list of $nm row index structures
            for ($i = 0; $i < $nm; ++$i) {
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
                $rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);

                // not sure why two row indexes are necessary?
                $this->phpSheet->setBreakByColumnAndRow($c + 1, $rf, Worksheet::BREAK_COLUMN);
            }
        }
    }

    /**
     * Read HEADER record.
     */
    private function readHeader(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: var
            // realized that $recordData can be empty even when record exists
            if ($recordData) {
                if ($this->version == self::XLS_BIFF8) {
                    $string = self::readUnicodeStringLong($recordData);
                } else {
                    $string = $this->readByteStringShort($recordData);
                }

                $this->phpSheet->getHeaderFooter()->setOddHeader($string['value']);
                $this->phpSheet->getHeaderFooter()->setEvenHeader($string['value']);
            }
        }
    }

    /**
     * Read FOOTER record.
     */
    private function readFooter(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: var
            // realized that $recordData can be empty even when record exists
            if ($recordData) {
                if ($this->version == self::XLS_BIFF8) {
                    $string = self::readUnicodeStringLong($recordData);
                } else {
                    $string = $this->readByteStringShort($recordData);
                }
                $this->phpSheet->getHeaderFooter()->setOddFooter($string['value']);
                $this->phpSheet->getHeaderFooter()->setEvenFooter($string['value']);
            }
        }
    }

    /**
     * Read HCENTER record.
     */
    private function readHcenter(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);

            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
        }
    }

    /**
     * Read VCENTER record.
     */
    private function readVcenter(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);

            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
        }
    }

    /**
     * Read LEFTMARGIN record.
     */
    private function readLeftMargin(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 8
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
        }
    }

    /**
     * Read RIGHTMARGIN record.
     */
    private function readRightMargin(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 8
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
        }
    }

    /**
     * Read TOPMARGIN record.
     */
    private function readTopMargin(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 8
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
        }
    }

    /**
     * Read BOTTOMMARGIN record.
     */
    private function readBottomMargin(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 8
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
        }
    }

    /**
     * Read PAGESETUP record.
     */
    private function readPageSetup(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; paper size
            $paperSize = self::getUInt2d($recordData, 0);

            // offset: 2; size: 2; scaling factor
            $scale = self::getUInt2d($recordData, 2);

            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
            $fitToWidth = self::getUInt2d($recordData, 6);

            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
            $fitToHeight = self::getUInt2d($recordData, 8);

            // offset: 10; size: 2; option flags

            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));

            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;

            // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init
            // when this bit is set, do not use flags for those properties
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;

            if (!$isNotInit) {
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);

                $this->phpSheet->getPageSetup()->setScale($scale, false);
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
            }

            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);

            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
        }
    }

    /**
     * PROTECT - Sheet protection (BIFF2 through BIFF8)
     *   if this record is omitted, then it also means no sheet protection.
     */
    private function readProtect(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->readDataOnly) {
            return;
        }

        // offset: 0; size: 2;

        // bit 0, mask 0x01; 1 = sheet is protected
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
    }

    /**
     * SCENPROTECT.
     */
    private function readScenProtect(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->readDataOnly) {
            return;
        }

        // offset: 0; size: 2;

        // bit: 0, mask 0x01; 1 = scenarios are protected
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;

        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
    }

    /**
     * OBJECTPROTECT.
     */
    private function readObjectProtect(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->readDataOnly) {
            return;
        }

        // offset: 0; size: 2;

        // bit: 0, mask 0x01; 1 = objects are protected
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;

        $this->phpSheet->getProtection()->setObjects((bool) $bool);
    }

    /**
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
     */
    private function readPassword(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; 16-bit hash value of password
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
            $this->phpSheet->getProtection()->setPassword($password, true);
        }
    }

    /**
     * Read DEFCOLWIDTH record.
     */
    private function readDefColWidth(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; default column width
        $width = self::getUInt2d($recordData, 0);
        if ($width != 8) {
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
        }
    }

    /**
     * Read COLINFO record.
     */
    private function readColInfo(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; index to first column in range
            $firstColumnIndex = self::getUInt2d($recordData, 0);

            // offset: 2; size: 2; index to last column in range
            $lastColumnIndex = self::getUInt2d($recordData, 2);

            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
            $width = self::getUInt2d($recordData, 4);

            // offset: 6; size: 2; index to XF record for default column formatting
            $xfIndex = self::getUInt2d($recordData, 6);

            // offset: 8; size: 2; option flags
            // bit: 0; mask: 0x0001; 1= columns are hidden
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;

            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;

            // bit: 12; mask: 0x1000; 1 = collapsed
            $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12);

            // offset: 10; size: 2; not used

            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
                if ($lastColumnIndex == 255 || $lastColumnIndex == 256) {
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);

                    break;
                }
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
                if (isset($this->mapCellXfIndex[$xfIndex])) {
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
                }
            }
        }
    }

    /**
     * ROW.
     *
     * This record contains the properties of a single row in a
     * sheet. Rows and cells in a sheet are divided into blocks
     * of 32 rows.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readRow(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; index of this row
            $r = self::getUInt2d($recordData, 0);

            // offset: 2; size: 2; index to column of the first cell which is described by a cell record

            // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1

            // offset: 6; size: 2;

            // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;

            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;

            if (!$useDefaultHeight) {
                $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
            }

            // offset: 8; size: 2; not used

            // offset: 10; size: 2; not used in BIFF5-BIFF8

            // offset: 12; size: 4; option flags and default row formatting

            // bit: 2-0: mask: 0x00000007; outline level of the row
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);

            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
            $isCollapsed = (bool) ((0x00000010 & self::getInt4d($recordData, 12)) >> 4);
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);

            // bit: 5; mask: 0x00000020; 1 = row is hidden
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);

            // bit: 7; mask: 0x00000080; 1 = row has explicit format
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;

            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;

            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
            }
        }
    }

    /**
     * Read RK record
     * This record represents a cell that contains an RK value
     * (encoded integer or floating-point value). If a
     * floating-point value cannot be encoded to an RK value,
     * a NUMBER record will be written. This record replaces the
     * record INTEGER written in BIFF2.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readRk(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; index to row
        $row = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; index to column
        $column = self::getUInt2d($recordData, 2);
        $columnString = Coordinate::stringFromColumnIndex($column + 1);

        // Read cell?
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
            // offset: 4; size: 2; index to XF record
            $xfIndex = self::getUInt2d($recordData, 4);

            // offset: 6; size: 4; RK value
            $rknum = self::getInt4d($recordData, 6);
            $numValue = self::getIEEE754($rknum);

            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
                // add style information
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
            }

            // add cell
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
        }
    }

    /**
     * Read LABELSST record
     * This record represents a cell that contains a string. It
     * replaces the LABEL record and RSTRING record used in
     * BIFF2-BIFF5.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readLabelSst(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; index to row
        $row = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; index to column
        $column = self::getUInt2d($recordData, 2);
        $columnString = Coordinate::stringFromColumnIndex($column + 1);

        $emptyCell = true;
        // Read cell?
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
            // offset: 4; size: 2; index to XF record
            $xfIndex = self::getUInt2d($recordData, 4);

            // offset: 6; size: 4; index to SST record
            $index = self::getInt4d($recordData, 6);

            // add cell
            if (($fmtRuns = $this->sst[$index]['fmtRuns']) && !$this->readDataOnly) {
                // then we should treat as rich text
                $richText = new RichText();
                $charPos = 0;
                $sstCount = count($this->sst[$index]['fmtRuns']);
                for ($i = 0; $i <= $sstCount; ++$i) {
                    if (isset($fmtRuns[$i])) {
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, $fmtRuns[$i]['charPos'] - $charPos);
                        $charPos = $fmtRuns[$i]['charPos'];
                    } else {
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, StringHelper::countCharacters($this->sst[$index]['value']));
                    }

                    if (StringHelper::countCharacters($text) > 0) {
                        if ($i == 0) { // first text run, no style
                            $richText->createText($text);
                        } else {
                            $textRun = $richText->createTextRun($text);
                            if (isset($fmtRuns[$i - 1])) {
                                $fontIndex = $fmtRuns[$i - 1]['fontIndex'];

                                if (array_key_exists($fontIndex, $this->objFonts) === false) {
                                    $fontIndex = count($this->objFonts) - 1;
                                }
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
                            }
                        }
                    }
                }
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
                    $emptyCell = false;
                }
            } else {
                if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') {
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
                    $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING);
                    $emptyCell = false;
                }
            }

            if (!$this->readDataOnly && !$emptyCell && isset($this->mapCellXfIndex[$xfIndex])) {
                // add style information
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
            }
        }
    }

    /**
     * Read MULRK record
     * This record represents a cell range containing RK value
     * cells. All cells are located in the same row.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readMulRk(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; index to row
        $row = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; index to first column
        $colFirst = self::getUInt2d($recordData, 2);

        // offset: var; size: 2; index to last column
        $colLast = self::getUInt2d($recordData, $length - 2);
        $columns = $colLast - $colFirst + 1;

        // offset within record data
        $offset = 4;

        for ($i = 1; $i <= $columns; ++$i) {
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);

            // Read cell?
            if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
                // offset: var; size: 2; index to XF record
                $xfIndex = self::getUInt2d($recordData, $offset);

                // offset: var; size: 4; RK value
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
                    // add style
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
                }

                // add cell value
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
            }

            $offset += 6;
        }
    }

    /**
     * Read NUMBER record
     * This record represents a cell that contains a
     * floating-point value.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readNumber(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; index to row
        $row = self::getUInt2d($recordData, 0);

        // offset: 2; size 2; index to column
        $column = self::getUInt2d($recordData, 2);
        $columnString = Coordinate::stringFromColumnIndex($column + 1);

        // Read cell?
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
            // offset 4; size: 2; index to XF record
            $xfIndex = self::getUInt2d($recordData, 4);

            $numValue = self::extractNumber(substr($recordData, 6, 8));

            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
                // add cell style
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
            }

            // add cell value
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
        }
    }

    /**
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
     * This record contains the token array and the result of a
     * formula cell.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readFormula(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; row index
        $row = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; col index
        $column = self::getUInt2d($recordData, 2);
        $columnString = Coordinate::stringFromColumnIndex($column + 1);

        // offset: 20: size: variable; formula structure
        $formulaStructure = substr($recordData, 20);

        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
        $options = self::getUInt2d($recordData, 14);

        // bit: 0; mask: 0x0001; 1 = recalculate always
        // bit: 1; mask: 0x0002; 1 = calculate on open
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
        $isPartOfSharedFormula = (bool) (0x0008 & $options);

        // WARNING:
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
        // the formula data may be ordinary formula data, therefore we need to check
        // explicitly for the tExp token (0x01)
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;

        if ($isPartOfSharedFormula) {
            // part of shared formula which means there will be a formula with a tExp token and nothing else
            // get the base cell, grab tExp token
            $baseRow = self::getUInt2d($formulaStructure, 3);
            $baseCol = self::getUInt2d($formulaStructure, 5);
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
        }

        // Read cell?
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
            if ($isPartOfSharedFormula) {
                // formula is added to this cell after the sheet has been read
                $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell;
            }

            // offset: 16: size: 4; not used

            // offset: 4; size: 2; XF index
            $xfIndex = self::getUInt2d($recordData, 4);

            // offset: 6; size: 8; result of the formula
            if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) {
                // String formula. Result follows in appended STRING record
                $dataType = DataType::TYPE_STRING;

                // read possible SHAREDFMLA record
                $code = self::getUInt2d($this->data, $this->pos);
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
                    $this->readSharedFmla();
                }

                // read STRING record
                $value = $this->readString();
            } elseif (
                (ord($recordData[6]) == 1)
                && (ord($recordData[12]) == 255)
                && (ord($recordData[13]) == 255)
            ) {
                // Boolean formula. Result is in +2; 0=false, 1=true
                $dataType = DataType::TYPE_BOOL;
                $value = (bool) ord($recordData[8]);
            } elseif (
                (ord($recordData[6]) == 2)
                && (ord($recordData[12]) == 255)
                && (ord($recordData[13]) == 255)
            ) {
                // Error formula. Error code is in +2
                $dataType = DataType::TYPE_ERROR;
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
            } elseif (
                (ord($recordData[6]) == 3)
                && (ord($recordData[12]) == 255)
                && (ord($recordData[13]) == 255)
            ) {
                // Formula result is a null string
                $dataType = DataType::TYPE_NULL;
                $value = '';
            } else {
                // forumla result is a number, first 14 bytes like _NUMBER record
                $dataType = DataType::TYPE_NUMERIC;
                $value = self::extractNumber(substr($recordData, 6, 8));
            }

            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
                // add cell style
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
            }

            // store the formula
            if (!$isPartOfSharedFormula) {
                // not part of shared formula
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
                try {
                    if ($this->version != self::XLS_BIFF8) {
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
                    }
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
                } catch (PhpSpreadsheetException $e) {
                    $cell->setValueExplicit($value, $dataType);
                }
            } else {
                if ($this->version == self::XLS_BIFF8) {
                    // do nothing at this point, formula id added later in the code
                } else {
                    $cell->setValueExplicit($value, $dataType);
                }
            }

            // store the cached calculated value
            $cell->setCalculatedValue($value);
        }
    }

    /**
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
     * which usually contains relative references.
     * These will be used to construct the formula in each shared formula part after the sheet is read.
     */
    private function readSharedFmla(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything
        $cellRange = substr($recordData, 0, 6);
        $cellRange = $this->readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax

        // offset: 6, size: 1; not used

        // offset: 7, size: 1; number of existing FORMULA records for this shared formula
        $no = ord($recordData[7]);

        // offset: 8, size: var; Binary token array of the shared formula
        $formula = substr($recordData, 8);

        // at this point we only store the shared formula for later use
        $this->sharedFormulas[$this->baseCell] = $formula;
    }

    /**
     * Read a STRING record from current stream position and advance the stream pointer to next record
     * This record is used for storing result from FORMULA record when it is a string, and
     * it occurs directly after the FORMULA record.
     *
     * @return string The string contents as UTF-8
     */
    private function readString()
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->version == self::XLS_BIFF8) {
            $string = self::readUnicodeStringLong($recordData);
            $value = $string['value'];
        } else {
            $string = $this->readByteStringLong($recordData);
            $value = $string['value'];
        }

        return $value;
    }

    /**
     * Read BOOLERR record
     * This record represents a Boolean value or error value
     * cell.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readBoolErr(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; row index
        $row = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; column index
        $column = self::getUInt2d($recordData, 2);
        $columnString = Coordinate::stringFromColumnIndex($column + 1);

        // Read cell?
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
            // offset: 4; size: 2; index to XF record
            $xfIndex = self::getUInt2d($recordData, 4);

            // offset: 6; size: 1; the boolean value or error value
            $boolErr = ord($recordData[6]);

            // offset: 7; size: 1; 0=boolean; 1=error
            $isError = ord($recordData[7]);

            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
            switch ($isError) {
                case 0: // boolean
                    $value = (bool) $boolErr;

                    // add cell value
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);

                    break;
                case 1: // error type
                    $value = Xls\ErrorCode::lookup($boolErr);

                    // add cell value
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);

                    break;
            }

            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
                // add cell style
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
            }
        }
    }

    /**
     * Read MULBLANK record
     * This record represents a cell range of empty cells. All
     * cells are located in the same row.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readMulBlank(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; index to row
        $row = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; index to first column
        $fc = self::getUInt2d($recordData, 2);

        // offset: 4; size: 2 x nc; list of indexes to XF records
        // add style information
        if (!$this->readDataOnly && $this->readEmptyCells) {
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);

                // Read cell?
                if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
                        $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
                    }
                }
            }
        }

        // offset: 6; size 2; index to last column (not needed)
    }

    /**
     * Read LABEL record
     * This record represents a cell that contains a string. In
     * BIFF8 it is usually replaced by the LABELSST record.
     * Excel still uses this record, if it copies unformatted
     * text cells to the clipboard.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readLabel(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; index to row
        $row = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; index to column
        $column = self::getUInt2d($recordData, 2);
        $columnString = Coordinate::stringFromColumnIndex($column + 1);

        // Read cell?
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
            // offset: 4; size: 2; XF index
            $xfIndex = self::getUInt2d($recordData, 4);

            // add cell value
            // todo: what if string is very long? continue record
            if ($this->version == self::XLS_BIFF8) {
                $string = self::readUnicodeStringLong(substr($recordData, 6));
                $value = $string['value'];
            } else {
                $string = $this->readByteStringLong(substr($recordData, 6));
                $value = $string['value'];
            }
            if ($this->readEmptyCells || trim($value) !== '') {
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
                $cell->setValueExplicit($value, DataType::TYPE_STRING);

                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
                    // add cell style
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
                }
            }
        }
    }

    /**
     * Read BLANK record.
     */
    private function readBlank(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; row index
        $row = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; col index
        $col = self::getUInt2d($recordData, 2);
        $columnString = Coordinate::stringFromColumnIndex($col + 1);

        // Read cell?
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
            // offset: 4; size: 2; XF index
            $xfIndex = self::getUInt2d($recordData, 4);

            // add style information
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
                $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
            }
        }
    }

    /**
     * Read MSODRAWING record.
     */
    private function readMsoDrawing(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);

        // get spliced record data
        $splicedRecordData = $this->getSplicedRecordData();
        $recordData = $splicedRecordData['recordData'];

        $this->drawingData .= $recordData;
    }

    /**
     * Read OBJ record.
     */
    private function readObj(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
            return;
        }

        // recordData consists of an array of subrecords looking like this:
        //    ft: 2 bytes; ftCmo type (0x15)
        //    cb: 2 bytes; size in bytes of ftCmo data
        //    ot: 2 bytes; Object Type
        //    id: 2 bytes; Object id number
        //    grbit: 2 bytes; Option Flags
        //    data: var; subrecord data

        // for now, we are just interested in the second subrecord containing the object type
        $ftCmoType = self::getUInt2d($recordData, 0);
        $cbCmoSize = self::getUInt2d($recordData, 2);
        $otObjType = self::getUInt2d($recordData, 4);
        $idObjID = self::getUInt2d($recordData, 6);
        $grbitOpts = self::getUInt2d($recordData, 6);

        $this->objs[] = [
            'ftCmoType' => $ftCmoType,
            'cbCmoSize' => $cbCmoSize,
            'otObjType' => $otObjType,
            'idObjID' => $idObjID,
            'grbitOpts' => $grbitOpts,
        ];
        $this->textObjRef = $idObjID;
    }

    /**
     * Read WINDOW2 record.
     */
    private function readWindow2(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; option flags
        $options = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; index to first visible row
        $firstVisibleRow = self::getUInt2d($recordData, 2);

        // offset: 4; size: 2; index to first visible colum
        $firstVisibleColumn = self::getUInt2d($recordData, 4);
        $zoomscaleInPageBreakPreview = 0;
        $zoomscaleInNormalView = 0;
        if ($this->version === self::XLS_BIFF8) {
            // offset:  8; size: 2; not used
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
            // offset: 14; size: 4; not used
            if (!isset($recordData[10])) {
                $zoomscaleInPageBreakPreview = 0;
            } else {
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
            }

            if ($zoomscaleInPageBreakPreview === 0) {
                $zoomscaleInPageBreakPreview = 60;
            }

            if (!isset($recordData[12])) {
                $zoomscaleInNormalView = 0;
            } else {
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
            }

            if ($zoomscaleInNormalView === 0) {
                $zoomscaleInNormalView = 100;
            }
        }

        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
        $this->phpSheet->setShowGridlines($showGridlines);

        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);

        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
        $this->frozen = (bool) ((0x0008 & $options) >> 3);

        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));

        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
        $isActive = (bool) ((0x0400 & $options) >> 10);
        if ($isActive) {
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
        }

        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);

        //FIXME: set $firstVisibleRow and $firstVisibleColumn

        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
            //NOTE: this setting is inferior to page layout view(Excel2007-)
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
            $this->phpSheet->getSheetView()->setView($view);
            if ($this->version === self::XLS_BIFF8) {
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
            }
        }
    }

    /**
     * Read PLV Record(Created by Excel2007 or upper).
     */
    private function readPageLayoutView(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; rt
        //->ignore
        $rt = self::getUInt2d($recordData, 0);
        // offset: 2; size: 2; grbitfr
        //->ignore
        $grbitFrt = self::getUInt2d($recordData, 2);
        // offset: 4; size: 8; reserved
        //->ignore

        // offset: 12; size 2; zoom scale
        $wScalePLV = self::getUInt2d($recordData, 12);
        // offset: 14; size 2; grbit
        $grbit = self::getUInt2d($recordData, 14);

        // decomprise grbit
        $fPageLayoutView = $grbit & 0x01;
        $fRulerVisible = ($grbit >> 1) & 0x01; //no support
        $fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support

        if ($fPageLayoutView === 1) {
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
        }
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
    }

    /**
     * Read SCL record.
     */
    private function readScl(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // offset: 0; size: 2; numerator of the view magnification
        $numerator = self::getUInt2d($recordData, 0);

        // offset: 2; size: 2; numerator of the view magnification
        $denumerator = self::getUInt2d($recordData, 2);

        // set the zoom scale (in percent)
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
    }

    /**
     * Read PANE record.
     */
    private function readPane(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; position of vertical split
            $px = self::getUInt2d($recordData, 0);

            // offset: 2; size: 2; position of horizontal split
            $py = self::getUInt2d($recordData, 2);

            // offset: 4; size: 2; top most visible row in the bottom pane
            $rwTop = self::getUInt2d($recordData, 4);

            // offset: 6; size: 2; first visible left column in the right pane
            $colLeft = self::getUInt2d($recordData, 6);

            if ($this->frozen) {
                // frozen panes
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
                $this->phpSheet->freezePane($cell, $topLeftCell);
            }
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
        }
    }

    /**
     * Read SELECTION record. There is one such record for each pane in the sheet.
     */
    private function readSelection(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 1; pane identifier
            $paneId = ord($recordData[0]);

            // offset: 1; size: 2; index to row of the active cell
            $r = self::getUInt2d($recordData, 1);

            // offset: 3; size: 2; index to column of the active cell
            $c = self::getUInt2d($recordData, 3);

            // offset: 5; size: 2; index into the following cell range list to the
            //  entry that contains the active cell
            $index = self::getUInt2d($recordData, 5);

            // offset: 7; size: var; cell range address list containing all selected cell ranges
            $data = substr($recordData, 7);
            $cellRangeAddressList = $this->readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax

            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];

            // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!)
            if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) {
                $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells);
            }

            // first row '1' + last row '65536' indicates that full column is selected
            if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) {
                $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells);
            }

            // first column 'A' + last column 'IV' indicates that full row is selected
            if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) {
                $selectedCells = preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells);
            }

            $this->phpSheet->setSelectedCells($selectedCells);
        }
    }

    private function includeCellRangeFiltered($cellRangeAddress)
    {
        $includeCellRange = true;
        if ($this->getReadFilter() !== null) {
            $includeCellRange = false;
            $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
            ++$rangeBoundaries[1][0];
            for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
                for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; ++$column) {
                    if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
                        $includeCellRange = true;

                        break 2;
                    }
                }
            }
        }

        return $includeCellRange;
    }

    /**
     * MERGEDCELLS.
     *
     * This record contains the addresses of merged cell ranges
     * in the current sheet.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     */
    private function readMergedCells(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
            $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($recordData);
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
                if (
                    (strpos($cellRangeAddress, ':') !== false) &&
                    ($this->includeCellRangeFiltered($cellRangeAddress))
                ) {
                    $this->phpSheet->mergeCells($cellRangeAddress);
                }
            }
        }
    }

    /**
     * Read HYPERLINK record.
     */
    private function readHyperLink(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer forward to next record
        $this->pos += 4 + $length;

        if (!$this->readDataOnly) {
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
            try {
                $cellRange = $this->readBIFF8CellRangeAddressFixed($recordData);
            } catch (PhpSpreadsheetException $e) {
                return;
            }

            // offset: 8, size: 16; GUID of StdLink

            // offset: 24, size: 4; unknown value

            // offset: 28, size: 4; option flags
            // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL
            $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0;

            // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL
            $isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1;

            // bit: 2 (and 4); mask: 0x00000014; 0 = no description
            $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2;

            // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text
            $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3;

            // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame
            $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7;

            // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name)
            $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8;

            // offset within record data
            $offset = 32;

            if ($hasDesc) {
                // offset: 32; size: var; character count of description text
                $dl = self::getInt4d($recordData, 32);
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
                $desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
                $offset += 4 + 2 * $dl;
            }
            if ($hasFrame) {
                $fl = self::getInt4d($recordData, $offset);
                $offset += 4 + 2 * $fl;
            }

            // detect type of hyperlink (there are 4 types)
            $hyperlinkType = null;

            if ($isUNC) {
                $hyperlinkType = 'UNC';
            } elseif (!$isFileLinkOrUrl) {
                $hyperlinkType = 'workbook';
            } elseif (ord($recordData[$offset]) == 0x03) {
                $hyperlinkType = 'local';
            } elseif (ord($recordData[$offset]) == 0xE0) {
                $hyperlinkType = 'URL';
            }

            switch ($hyperlinkType) {
                case 'URL':
                    // section 5.58.2: Hyperlink containing a URL
                    // e.g. http://example.org/index.php

                    // offset: var; size: 16; GUID of URL Moniker
                    $offset += 16;
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
                    $us = self::getInt4d($recordData, $offset);
                    $offset += 4;
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
                    $nullOffset = strpos($url, chr(0x00));
                    if ($nullOffset) {
                        $url = substr($url, 0, $nullOffset);
                    }
                    $url .= $hasText ? '#' : '';
                    $offset += $us;

                    break;
                case 'local':
                    // section 5.58.3: Hyperlink to local file
                    // examples:
                    //   mydoc.txt
                    //   ../../somedoc.xls#Sheet!A1

                    // offset: var; size: 16; GUI of File Moniker
                    $offset += 16;

                    // offset: var; size: 2; directory up-level count.
                    $upLevelCount = self::getUInt2d($recordData, $offset);
                    $offset += 2;

                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
                    $sl = self::getInt4d($recordData, $offset);
                    $offset += 4;

                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
                    $shortenedFilePath = substr($recordData, $offset, $sl);
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero

                    $offset += $sl;

                    // offset: var; size: 24; unknown sequence
                    $offset += 24;

                    // extended file path
                    // offset: var; size: 4; size of the following file link field including string lenth mark
                    $sz = self::getInt4d($recordData, $offset);
                    $offset += 4;

                    // only present if $sz > 0
                    if ($sz > 0) {
                        // offset: var; size: 4; size of the character array of the extended file path and name
                        $xl = self::getInt4d($recordData, $offset);
                        $offset += 4;

                        // offset: var; size 2; unknown
                        $offset += 2;

                        // offset: var; size $xl; character array of the extended file path and name.
                        $extendedFilePath = substr($recordData, $offset, $xl);
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
                        $offset += $xl;
                    }

                    // construct the path
                    $url = str_repeat('..\\', $upLevelCount);
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
                    $url .= $hasText ? '#' : '';

                    break;
                case 'UNC':
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
                    // todo: implement
                    return;
                case 'workbook':
                    // section 5.58.5: Hyperlink to the Current Workbook
                    // e.g. Sheet2!B1:C2, stored in text mark field
                    $url = 'sheet://';

                    break;
                default:
                    return;
            }

            if ($hasText) {
                // offset: var; size: 4; character count of text mark including trailing zero word
                $tl = self::getInt4d($recordData, $offset);
                $offset += 4;
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
                $url .= $text;
            }

            // apply the hyperlink to all the relevant cells
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
            }
        }
    }

    /**
     * Read DATAVALIDATIONS record.
     */
    private function readDataValidations(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer forward to next record
        $this->pos += 4 + $length;
    }

    /**
     * Read DATAVALIDATION record.
     */
    private function readDataValidation(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer forward to next record
        $this->pos += 4 + $length;

        if ($this->readDataOnly) {
            return;
        }

        // offset: 0; size: 4; Options
        $options = self::getInt4d($recordData, 0);

        // bit: 0-3; mask: 0x0000000F; type
        $type = (0x0000000F & $options) >> 0;
        switch ($type) {
            case 0x00:
                $type = DataValidation::TYPE_NONE;

                break;
            case 0x01:
                $type = DataValidation::TYPE_WHOLE;

                break;
            case 0x02:
                $type = DataValidation::TYPE_DECIMAL;

                break;
            case 0x03:
                $type = DataValidation::TYPE_LIST;

                break;
            case 0x04:
                $type = DataValidation::TYPE_DATE;

                break;
            case 0x05:
                $type = DataValidation::TYPE_TIME;

                break;
            case 0x06:
                $type = DataValidation::TYPE_TEXTLENGTH;

                break;
            case 0x07:
                $type = DataValidation::TYPE_CUSTOM;

                break;
        }

        // bit: 4-6; mask: 0x00000070; error type
        $errorStyle = (0x00000070 & $options) >> 4;
        switch ($errorStyle) {
            case 0x00:
                $errorStyle = DataValidation::STYLE_STOP;

                break;
            case 0x01:
                $errorStyle = DataValidation::STYLE_WARNING;

                break;
            case 0x02:
                $errorStyle = DataValidation::STYLE_INFORMATION;

                break;
        }

        // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list)
        // I have only seen cases where this is 1
        $explicitFormula = (0x00000080 & $options) >> 7;

        // bit: 8; mask: 0x00000100; 1= empty cells allowed
        $allowBlank = (0x00000100 & $options) >> 8;

        // bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity
        $suppressDropDown = (0x00000200 & $options) >> 9;

        // bit: 18; mask: 0x00040000; 1= show prompt box if cell selected
        $showInputMessage = (0x00040000 & $options) >> 18;

        // bit: 19; mask: 0x00080000; 1= show error box if invalid values entered
        $showErrorMessage = (0x00080000 & $options) >> 19;

        // bit: 20-23; mask: 0x00F00000; condition operator
        $operator = (0x00F00000 & $options) >> 20;
        switch ($operator) {
            case 0x00:
                $operator = DataValidation::OPERATOR_BETWEEN;

                break;
            case 0x01:
                $operator = DataValidation::OPERATOR_NOTBETWEEN;

                break;
            case 0x02:
                $operator = DataValidation::OPERATOR_EQUAL;

                break;
            case 0x03:
                $operator = DataValidation::OPERATOR_NOTEQUAL;

                break;
            case 0x04:
                $operator = DataValidation::OPERATOR_GREATERTHAN;

                break;
            case 0x05:
                $operator = DataValidation::OPERATOR_LESSTHAN;

                break;
            case 0x06:
                $operator = DataValidation::OPERATOR_GREATERTHANOREQUAL;

                break;
            case 0x07:
                $operator = DataValidation::OPERATOR_LESSTHANOREQUAL;

                break;
        }

        // offset: 4; size: var; title of the prompt box
        $offset = 4;
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
        $promptTitle = $string['value'] !== chr(0) ? $string['value'] : '';
        $offset += $string['size'];

        // offset: var; size: var; title of the error box
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
        $errorTitle = $string['value'] !== chr(0) ? $string['value'] : '';
        $offset += $string['size'];

        // offset: var; size: var; text of the prompt box
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
        $prompt = $string['value'] !== chr(0) ? $string['value'] : '';
        $offset += $string['size'];

        // offset: var; size: var; text of the error box
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
        $error = $string['value'] !== chr(0) ? $string['value'] : '';
        $offset += $string['size'];

        // offset: var; size: 2; size of the formula data for the first condition
        $sz1 = self::getUInt2d($recordData, $offset);
        $offset += 2;

        // offset: var; size: 2; not used
        $offset += 2;

        // offset: var; size: $sz1; formula data for first condition (without size field)
        $formula1 = substr($recordData, $offset, $sz1);
        $formula1 = pack('v', $sz1) . $formula1; // prepend the length

        try {
            $formula1 = $this->getFormulaFromStructure($formula1);

            // in list type validity, null characters are used as item separators
            if ($type == DataValidation::TYPE_LIST) {
                $formula1 = str_replace(chr(0), ',', $formula1);
            }
        } catch (PhpSpreadsheetException $e) {
            return;
        }
        $offset += $sz1;

        // offset: var; size: 2; size of the formula data for the first condition
        $sz2 = self::getUInt2d($recordData, $offset);
        $offset += 2;

        // offset: var; size: 2; not used
        $offset += 2;

        // offset: var; size: $sz2; formula data for second condition (without size field)
        $formula2 = substr($recordData, $offset, $sz2);
        $formula2 = pack('v', $sz2) . $formula2; // prepend the length

        try {
            $formula2 = $this->getFormulaFromStructure($formula2);
        } catch (PhpSpreadsheetException $e) {
            return;
        }
        $offset += $sz2;

        // offset: var; size: var; cell range address list with
        $cellRangeAddressList = $this->readBIFF8CellRangeAddressList(substr($recordData, $offset));
        $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];

        foreach ($cellRangeAddresses as $cellRange) {
            $stRange = $this->phpSheet->shrinkRangeToFit($cellRange);
            foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
                $objValidation = $this->phpSheet->getCell($coordinate)->getDataValidation();
                $objValidation->setType($type);
                $objValidation->setErrorStyle($errorStyle);
                $objValidation->setAllowBlank((bool) $allowBlank);
                $objValidation->setShowInputMessage((bool) $showInputMessage);
                $objValidation->setShowErrorMessage((bool) $showErrorMessage);
                $objValidation->setShowDropDown(!$suppressDropDown);
                $objValidation->setOperator($operator);
                $objValidation->setErrorTitle($errorTitle);
                $objValidation->setError($error);
                $objValidation->setPromptTitle($promptTitle);
                $objValidation->setPrompt($prompt);
                $objValidation->setFormula1($formula1);
                $objValidation->setFormula2($formula2);
            }
        }
    }

    /**
     * Read SHEETLAYOUT record. Stores sheet tab color information.
     */
    private function readSheetLayout(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // local pointer in record data
        $offset = 0;

        if (!$this->readDataOnly) {
            // offset: 0; size: 2; repeated record identifier 0x0862

            // offset: 2; size: 10; not used

            // offset: 12; size: 4; size of record data
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
            $sz = self::getInt4d($recordData, 12);

            switch ($sz) {
                case 0x14:
                    // offset: 16; size: 2; color index for sheet tab
                    $colorIndex = self::getUInt2d($recordData, 16);
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);

                    break;
                case 0x28:
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
                    return;

                    break;
            }
        }
    }

    /**
     * Read SHEETPROTECTION record (FEATHEADR).
     */
    private function readSheetProtection(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if ($this->readDataOnly) {
            return;
        }

        // offset: 0; size: 2; repeated record header

        // offset: 2; size: 2; FRT cell reference flag (=0 currently)

        // offset: 4; size: 8; Currently not used and set to 0

        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
        $isf = self::getUInt2d($recordData, 12);
        if ($isf != 2) {
            return;
        }

        // offset: 14; size: 1; =1 since this is a feat header

        // offset: 15; size: 4; size of rgbHdrSData

        // rgbHdrSData, assume "Enhanced Protection"
        // offset: 19; size: 2; option flags
        $options = self::getUInt2d($recordData, 19);

        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
        $bool = (0x0001 & $options) >> 0;
        $this->phpSheet->getProtection()->setObjects(!$bool);

        // bit: 1; mask 0x0002; edit scenarios
        $bool = (0x0002 & $options) >> 1;
        $this->phpSheet->getProtection()->setScenarios(!$bool);

        // bit: 2; mask 0x0004; format cells
        $bool = (0x0004 & $options) >> 2;
        $this->phpSheet->getProtection()->setFormatCells(!$bool);

        // bit: 3; mask 0x0008; format columns
        $bool = (0x0008 & $options) >> 3;
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);

        // bit: 4; mask 0x0010; format rows
        $bool = (0x0010 & $options) >> 4;
        $this->phpSheet->getProtection()->setFormatRows(!$bool);

        // bit: 5; mask 0x0020; insert columns
        $bool = (0x0020 & $options) >> 5;
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);

        // bit: 6; mask 0x0040; insert rows
        $bool = (0x0040 & $options) >> 6;
        $this->phpSheet->getProtection()->setInsertRows(!$bool);

        // bit: 7; mask 0x0080; insert hyperlinks
        $bool = (0x0080 & $options) >> 7;
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);

        // bit: 8; mask 0x0100; delete columns
        $bool = (0x0100 & $options) >> 8;
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);

        // bit: 9; mask 0x0200; delete rows
        $bool = (0x0200 & $options) >> 9;
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);

        // bit: 10; mask 0x0400; select locked cells
        $bool = (0x0400 & $options) >> 10;
        $this->phpSheet->getProtection()->setSelectLockedCells(!$bool);

        // bit: 11; mask 0x0800; sort cell range
        $bool = (0x0800 & $options) >> 11;
        $this->phpSheet->getProtection()->setSort(!$bool);

        // bit: 12; mask 0x1000; auto filter
        $bool = (0x1000 & $options) >> 12;
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);

        // bit: 13; mask 0x2000; pivot tables
        $bool = (0x2000 & $options) >> 13;
        $this->phpSheet->getProtection()->setPivotTables(!$bool);

        // bit: 14; mask 0x4000; select unlocked cells
        $bool = (0x4000 & $options) >> 14;
        $this->phpSheet->getProtection()->setSelectUnlockedCells(!$bool);

        // offset: 21; size: 2; not used
    }

    /**
     * Read RANGEPROTECTION record
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
     * where it is referred to as FEAT record.
     */
    private function readRangeProtection(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        // local pointer in record data
        $offset = 0;

        if (!$this->readDataOnly) {
            $offset += 12;

            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
            $isf = self::getUInt2d($recordData, 12);
            if ($isf != 2) {
                // we only read FEAT records of type 2
                return;
            }
            $offset += 2;

            $offset += 5;

            // offset: 19; size: 2; count of ref ranges this feature is on
            $cref = self::getUInt2d($recordData, 19);
            $offset += 2;

            $offset += 6;

            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
            $cellRanges = [];
            for ($i = 0; $i < $cref; ++$i) {
                try {
                    $cellRange = $this->readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
                } catch (PhpSpreadsheetException $e) {
                    return;
                }
                $cellRanges[] = $cellRange;
                $offset += 8;
            }

            // offset: var; size: var; variable length of feature specific data
            $rgbFeat = substr($recordData, $offset);
            $offset += 4;

            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
            $wPassword = self::getInt4d($recordData, $offset);
            $offset += 4;

            // Apply range protection to sheet
            if ($cellRanges) {
                $this->phpSheet->protectCells(implode(' ', $cellRanges), strtoupper(dechex($wPassword)), true);
            }
        }
    }

    /**
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
     */
    private function readContinue(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // check if we are reading drawing data
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
        if ($this->drawingData == '') {
            // move stream pointer to next record
            $this->pos += 4 + $length;

            return;
        }

        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
        if ($length < 4) {
            // move stream pointer to next record
            $this->pos += 4 + $length;

            return;
        }

        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
        // we know that Escher stream may be split at least at
        //        0xF003 MsofbtSpgrContainer
        //        0xF004 MsofbtSpContainer
        //        0xF00D MsofbtClientTextbox
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more

        $splitPoint = self::getUInt2d($recordData, 2);
        if (in_array($splitPoint, $validSplitPoints)) {
            // get spliced record data (and move pointer to next record)
            $splicedRecordData = $this->getSplicedRecordData();
            $this->drawingData .= $splicedRecordData['recordData'];

            return;
        }

        // move stream pointer to next record
        $this->pos += 4 + $length;
    }

    /**
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
     * records are found. Splices the record data pieces and returns the combined string as if record data
     * is in one piece.
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
     *
     * @return array
     */
    private function getSplicedRecordData()
    {
        $data = '';
        $spliceOffsets = [];

        $i = 0;
        $spliceOffsets[0] = 0;

        do {
            ++$i;

            // offset: 0; size: 2; identifier
            $identifier = self::getUInt2d($this->data, $this->pos);
            // offset: 2; size: 2; length
            $length = self::getUInt2d($this->data, $this->pos + 2);
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);

            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;

            $this->pos += 4 + $length;
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);

        return [
            'recordData' => $data,
            'spliceOffsets' => $spliceOffsets,
        ];
    }

    /**
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
     *
     * @param string $formulaStructure The complete binary data for the formula
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
     *
     * @return string Human readable formula
     */
    private function getFormulaFromStructure($formulaStructure, $baseCell = 'A1')
    {
        // offset: 0; size: 2; size of the following formula data
        $sz = self::getUInt2d($formulaStructure, 0);

        // offset: 2; size: sz
        $formulaData = substr($formulaStructure, 2, $sz);

        // offset: 2 + sz; size: variable (optional)
        if (strlen($formulaStructure) > 2 + $sz) {
            $additionalData = substr($formulaStructure, 2 + $sz);
        } else {
            $additionalData = '';
        }

        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
    }

    /**
     * Take formula data and additional data for formula and return human readable formula.
     *
     * @param string $formulaData The binary data for the formula itself
     * @param string $additionalData Additional binary data going with the formula
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
     *
     * @return string Human readable formula
     */
    private function getFormulaFromData($formulaData, $additionalData = '', $baseCell = 'A1')
    {
        // start parsing the formula data
        $tokens = [];

        while (strlen($formulaData) > 0 && $token = $this->getNextToken($formulaData, $baseCell)) {
            $tokens[] = $token;
            $formulaData = substr($formulaData, $token['size']);
        }

        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);

        return $formulaString;
    }

    /**
     * Take array of tokens together with additional data for formula and return human readable formula.
     *
     * @param array $tokens
     * @param string $additionalData Additional binary data going with the formula
     *
     * @return string Human readable formula
     */
    private function createFormulaFromTokens($tokens, $additionalData)
    {
        // empty formula?
        if (empty($tokens)) {
            return '';
        }

        $formulaStrings = [];
        foreach ($tokens as $token) {
            // initialize spaces
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis

            switch ($token['name']) {
                case 'tAdd': // addition
                case 'tConcat': // addition
                case 'tDiv': // division
                case 'tEQ': // equality
                case 'tGE': // greater than or equal
                case 'tGT': // greater than
                case 'tIsect': // intersection
                case 'tLE': // less than or equal
                case 'tList': // less than or equal
                case 'tLT': // less than
                case 'tMul': // multiplication
                case 'tNE': // multiplication
                case 'tPower': // power
                case 'tRange': // range
                case 'tSub': // subtraction
                    $op2 = array_pop($formulaStrings);
                    $op1 = array_pop($formulaStrings);
                    $formulaStrings[] = "$op1$space1$space0{$token['data']}$op2";
                    unset($space0, $space1);

                    break;
                case 'tUplus': // unary plus
                case 'tUminus': // unary minus
                    $op = array_pop($formulaStrings);
                    $formulaStrings[] = "$space1$space0{$token['data']}$op";
                    unset($space0, $space1);

                    break;
                case 'tPercent': // percent sign
                    $op = array_pop($formulaStrings);
                    $formulaStrings[] = "$op$space1$space0{$token['data']}";
                    unset($space0, $space1);

                    break;
                case 'tAttrVolatile': // indicates volatile function
                case 'tAttrIf':
                case 'tAttrSkip':
                case 'tAttrChoose':
                    // token is only important for Excel formula evaluator
                    // do nothing
                    break;
                case 'tAttrSpace': // space / carriage return
                    // space will be used when next token arrives, do not alter formulaString stack
                    switch ($token['data']['spacetype']) {
                        case 'type0':
                            $space0 = str_repeat(' ', $token['data']['spacecount']);

                            break;
                        case 'type1':
                            $space1 = str_repeat("\n", $token['data']['spacecount']);

                            break;
                        case 'type2':
                            $space2 = str_repeat(' ', $token['data']['spacecount']);

                            break;
                        case 'type3':
                            $space3 = str_repeat("\n", $token['data']['spacecount']);

                            break;
                        case 'type4':
                            $space4 = str_repeat(' ', $token['data']['spacecount']);

                            break;
                        case 'type5':
                            $space5 = str_repeat("\n", $token['data']['spacecount']);

                            break;
                    }

                    break;
                case 'tAttrSum': // SUM function with one parameter
                    $op = array_pop($formulaStrings);
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
                    unset($space0, $space1);

                    break;
                case 'tFunc': // function with fixed number of arguments
                case 'tFuncV': // function with variable number of arguments
                    if ($token['data']['function'] != '') {
                        // normal function
                        $ops = []; // array of operators
                        for ($i = 0; $i < $token['data']['args']; ++$i) {
                            $ops[] = array_pop($formulaStrings);
                        }
                        $ops = array_reverse($ops);
                        $formulaStrings[] = "$space1$space0{$token['data']['function']}(" . implode(',', $ops) . ')';
                        unset($space0, $space1);
                    } else {
                        // add-in function
                        $ops = []; // array of operators
                        for ($i = 0; $i < $token['data']['args'] - 1; ++$i) {
                            $ops[] = array_pop($formulaStrings);
                        }
                        $ops = array_reverse($ops);
                        $function = array_pop($formulaStrings);
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
                        unset($space0, $space1);
                    }

                    break;
                case 'tParen': // parenthesis
                    $expression = array_pop($formulaStrings);
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
                    unset($space2, $space3, $space4, $space5);

                    break;
                case 'tArray': // array constant
                    $constantArray = self::readBIFF8ConstantArray($additionalData);
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
                    unset($space0, $space1);

                    break;
                case 'tMemArea':
                    // bite off chunk of additional data
                    $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($additionalData);
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
                    $formulaStrings[] = "$space1$space0{$token['data']}";
                    unset($space0, $space1);

                    break;
                case 'tArea': // cell range address
                case 'tBool': // boolean
                case 'tErr': // error code
                case 'tInt': // integer
                case 'tMemErr':
                case 'tMemFunc':
                case 'tMissArg':
                case 'tName':
                case 'tNameX':
                case 'tNum': // number
                case 'tRef': // single cell reference
                case 'tRef3d': // 3d cell reference
                case 'tArea3d': // 3d cell range reference
                case 'tRefN':
                case 'tAreaN':
                case 'tStr': // string
                    $formulaStrings[] = "$space1$space0{$token['data']}";
                    unset($space0, $space1);

                    break;
            }
        }
        $formulaString = $formulaStrings[0];

        return $formulaString;
    }

    /**
     * Fetch next token from binary formula data.
     *
     * @param string $formulaData Formula data
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
     *
     * @return array
     */
    private function getNextToken($formulaData, $baseCell = 'A1')
    {
        // offset: 0; size: 1; token id
        $id = ord($formulaData[0]); // token id
        $name = false; // initialize token name

        switch ($id) {
            case 0x03:
                $name = 'tAdd';
                $size = 1;
                $data = '+';

                break;
            case 0x04:
                $name = 'tSub';
                $size = 1;
                $data = '-';

                break;
            case 0x05:
                $name = 'tMul';
                $size = 1;
                $data = '*';

                break;
            case 0x06:
                $name = 'tDiv';
                $size = 1;
                $data = '/';

                break;
            case 0x07:
                $name = 'tPower';
                $size = 1;
                $data = '^';

                break;
            case 0x08:
                $name = 'tConcat';
                $size = 1;
                $data = '&';

                break;
            case 0x09:
                $name = 'tLT';
                $size = 1;
                $data = '<';

                break;
            case 0x0A:
                $name = 'tLE';
                $size = 1;
                $data = '<=';

                break;
            case 0x0B:
                $name = 'tEQ';
                $size = 1;
                $data = '=';

                break;
            case 0x0C:
                $name = 'tGE';
                $size = 1;
                $data = '>=';

                break;
            case 0x0D:
                $name = 'tGT';
                $size = 1;
                $data = '>';

                break;
            case 0x0E:
                $name = 'tNE';
                $size = 1;
                $data = '<>';

                break;
            case 0x0F:
                $name = 'tIsect';
                $size = 1;
                $data = ' ';

                break;
            case 0x10:
                $name = 'tList';
                $size = 1;
                $data = ',';

                break;
            case 0x11:
                $name = 'tRange';
                $size = 1;
                $data = ':';

                break;
            case 0x12:
                $name = 'tUplus';
                $size = 1;
                $data = '+';

                break;
            case 0x13:
                $name = 'tUminus';
                $size = 1;
                $data = '-';

                break;
            case 0x14:
                $name = 'tPercent';
                $size = 1;
                $data = '%';

                break;
            case 0x15:    //    parenthesis
                $name = 'tParen';
                $size = 1;
                $data = null;

                break;
            case 0x16:    //    missing argument
                $name = 'tMissArg';
                $size = 1;
                $data = '';

                break;
            case 0x17:    //    string
                $name = 'tStr';
                // offset: 1; size: var; Unicode string, 8-bit string length
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
                $size = 1 + $string['size'];
                $data = self::UTF8toExcelDoubleQuoted($string['value']);

                break;
            case 0x19:    //    Special attribute
                // offset: 1; size: 1; attribute type flags:
                switch (ord($formulaData[1])) {
                    case 0x01:
                        $name = 'tAttrVolatile';
                        $size = 4;
                        $data = null;

                        break;
                    case 0x02:
                        $name = 'tAttrIf';
                        $size = 4;
                        $data = null;

                        break;
                    case 0x04:
                        $name = 'tAttrChoose';
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
                        $nc = self::getUInt2d($formulaData, 2);
                        // offset: 4; size: 2 * $nc
                        // offset: 4 + 2 * $nc; size: 2
                        $size = 2 * $nc + 6;
                        $data = null;

                        break;
                    case 0x08:
                        $name = 'tAttrSkip';
                        $size = 4;
                        $data = null;

                        break;
                    case 0x10:
                        $name = 'tAttrSum';
                        $size = 4;
                        $data = null;

                        break;
                    case 0x40:
                    case 0x41:
                        $name = 'tAttrSpace';
                        $size = 4;
                        // offset: 2; size: 2; space type and position
                        switch (ord($formulaData[2])) {
                            case 0x00:
                                $spacetype = 'type0';

                                break;
                            case 0x01:
                                $spacetype = 'type1';

                                break;
                            case 0x02:
                                $spacetype = 'type2';

                                break;
                            case 0x03:
                                $spacetype = 'type3';

                                break;
                            case 0x04:
                                $spacetype = 'type4';

                                break;
                            case 0x05:
                                $spacetype = 'type5';

                                break;
                            default:
                                throw new Exception('Unrecognized space type in tAttrSpace token');

                                break;
                        }
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
                        $spacecount = ord($formulaData[3]);

                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];

                        break;
                    default:
                        throw new Exception('Unrecognized attribute flag in tAttr token');

                        break;
                }

                break;
            case 0x1C:    //    error code
                // offset: 1; size: 1; error code
                $name = 'tErr';
                $size = 2;
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));

                break;
            case 0x1D:    //    boolean
                // offset: 1; size: 1; 0 = false, 1 = true;
                $name = 'tBool';
                $size = 2;
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';

                break;
            case 0x1E:    //    integer
                // offset: 1; size: 2; unsigned 16-bit integer
                $name = 'tInt';
                $size = 3;
                $data = self::getUInt2d($formulaData, 1);

                break;
            case 0x1F:    //    number
                // offset: 1; size: 8;
                $name = 'tNum';
                $size = 9;
                $data = self::extractNumber(substr($formulaData, 1));
                $data = str_replace(',', '.', (string) $data); // in case non-English locale

                break;
            case 0x20:    //    array constant
            case 0x40:
            case 0x60:
                // offset: 1; size: 7; not used
                $name = 'tArray';
                $size = 8;
                $data = null;

                break;
            case 0x21:    //    function with fixed number of arguments
            case 0x41:
            case 0x61:
                $name = 'tFunc';
                $size = 3;
                // offset: 1; size: 2; index to built-in sheet function
                switch (self::getUInt2d($formulaData, 1)) {
                    case 2:
                        $function = 'ISNA';
                        $args = 1;

                        break;
                    case 3:
                        $function = 'ISERROR';
                        $args = 1;

                        break;
                    case 10:
                        $function = 'NA';
                        $args = 0;

                        break;
                    case 15:
                        $function = 'SIN';
                        $args = 1;

                        break;
                    case 16:
                        $function = 'COS';
                        $args = 1;

                        break;
                    case 17:
                        $function = 'TAN';
                        $args = 1;

                        break;
                    case 18:
                        $function = 'ATAN';
                        $args = 1;

                        break;
                    case 19:
                        $function = 'PI';
                        $args = 0;

                        break;
                    case 20:
                        $function = 'SQRT';
                        $args = 1;

                        break;
                    case 21:
                        $function = 'EXP';
                        $args = 1;

                        break;
                    case 22:
                        $function = 'LN';
                        $args = 1;

                        break;
                    case 23:
                        $function = 'LOG10';
                        $args = 1;

                        break;
                    case 24:
                        $function = 'ABS';
                        $args = 1;

                        break;
                    case 25:
                        $function = 'INT';
                        $args = 1;

                        break;
                    case 26:
                        $function = 'SIGN';
                        $args = 1;

                        break;
                    case 27:
                        $function = 'ROUND';
                        $args = 2;

                        break;
                    case 30:
                        $function = 'REPT';
                        $args = 2;

                        break;
                    case 31:
                        $function = 'MID';
                        $args = 3;

                        break;
                    case 32:
                        $function = 'LEN';
                        $args = 1;

                        break;
                    case 33:
                        $function = 'VALUE';
                        $args = 1;

                        break;
                    case 34:
                        $function = 'TRUE';
                        $args = 0;

                        break;
                    case 35:
                        $function = 'FALSE';
                        $args = 0;

                        break;
                    case 38:
                        $function = 'NOT';
                        $args = 1;

                        break;
                    case 39:
                        $function = 'MOD';
                        $args = 2;

                        break;
                    case 40:
                        $function = 'DCOUNT';
                        $args = 3;

                        break;
                    case 41:
                        $function = 'DSUM';
                        $args = 3;

                        break;
                    case 42:
                        $function = 'DAVERAGE';
                        $args = 3;

                        break;
                    case 43:
                        $function = 'DMIN';
                        $args = 3;

                        break;
                    case 44:
                        $function = 'DMAX';
                        $args = 3;

                        break;
                    case 45:
                        $function = 'DSTDEV';
                        $args = 3;

                        break;
                    case 48:
                        $function = 'TEXT';
                        $args = 2;

                        break;
                    case 61:
                        $function = 'MIRR';
                        $args = 3;

                        break;
                    case 63:
                        $function = 'RAND';
                        $args = 0;

                        break;
                    case 65:
                        $function = 'DATE';
                        $args = 3;

                        break;
                    case 66:
                        $function = 'TIME';
                        $args = 3;

                        break;
                    case 67:
                        $function = 'DAY';
                        $args = 1;

                        break;
                    case 68:
                        $function = 'MONTH';
                        $args = 1;

                        break;
                    case 69:
                        $function = 'YEAR';
                        $args = 1;

                        break;
                    case 71:
                        $function = 'HOUR';
                        $args = 1;

                        break;
                    case 72:
                        $function = 'MINUTE';
                        $args = 1;

                        break;
                    case 73:
                        $function = 'SECOND';
                        $args = 1;

                        break;
                    case 74:
                        $function = 'NOW';
                        $args = 0;

                        break;
                    case 75:
                        $function = 'AREAS';
                        $args = 1;

                        break;
                    case 76:
                        $function = 'ROWS';
                        $args = 1;

                        break;
                    case 77:
                        $function = 'COLUMNS';
                        $args = 1;

                        break;
                    case 83:
                        $function = 'TRANSPOSE';
                        $args = 1;

                        break;
                    case 86:
                        $function = 'TYPE';
                        $args = 1;

                        break;
                    case 97:
                        $function = 'ATAN2';
                        $args = 2;

                        break;
                    case 98:
                        $function = 'ASIN';
                        $args = 1;

                        break;
                    case 99:
                        $function = 'ACOS';
                        $args = 1;

                        break;
                    case 105:
                        $function = 'ISREF';
                        $args = 1;

                        break;
                    case 111:
                        $function = 'CHAR';
                        $args = 1;

                        break;
                    case 112:
                        $function = 'LOWER';
                        $args = 1;

                        break;
                    case 113:
                        $function = 'UPPER';
                        $args = 1;

                        break;
                    case 114:
                        $function = 'PROPER';
                        $args = 1;

                        break;
                    case 117:
                        $function = 'EXACT';
                        $args = 2;

                        break;
                    case 118:
                        $function = 'TRIM';
                        $args = 1;

                        break;
                    case 119:
                        $function = 'REPLACE';
                        $args = 4;

                        break;
                    case 121:
                        $function = 'CODE';
                        $args = 1;

                        break;
                    case 126:
                        $function = 'ISERR';
                        $args = 1;

                        break;
                    case 127:
                        $function = 'ISTEXT';
                        $args = 1;

                        break;
                    case 128:
                        $function = 'ISNUMBER';
                        $args = 1;

                        break;
                    case 129:
                        $function = 'ISBLANK';
                        $args = 1;

                        break;
                    case 130:
                        $function = 'T';
                        $args = 1;

                        break;
                    case 131:
                        $function = 'N';
                        $args = 1;

                        break;
                    case 140:
                        $function = 'DATEVALUE';
                        $args = 1;

                        break;
                    case 141:
                        $function = 'TIMEVALUE';
                        $args = 1;

                        break;
                    case 142:
                        $function = 'SLN';
                        $args = 3;

                        break;
                    case 143:
                        $function = 'SYD';
                        $args = 4;

                        break;
                    case 162:
                        $function = 'CLEAN';
                        $args = 1;

                        break;
                    case 163:
                        $function = 'MDETERM';
                        $args = 1;

                        break;
                    case 164:
                        $function = 'MINVERSE';
                        $args = 1;

                        break;
                    case 165:
                        $function = 'MMULT';
                        $args = 2;

                        break;
                    case 184:
                        $function = 'FACT';
                        $args = 1;

                        break;
                    case 189:
                        $function = 'DPRODUCT';
                        $args = 3;

                        break;
                    case 190:
                        $function = 'ISNONTEXT';
                        $args = 1;

                        break;
                    case 195:
                        $function = 'DSTDEVP';
                        $args = 3;

                        break;
                    case 196:
                        $function = 'DVARP';
                        $args = 3;

                        break;
                    case 198:
                        $function = 'ISLOGICAL';
                        $args = 1;

                        break;
                    case 199:
                        $function = 'DCOUNTA';
                        $args = 3;

                        break;
                    case 207:
                        $function = 'REPLACEB';
                        $args = 4;

                        break;
                    case 210:
                        $function = 'MIDB';
                        $args = 3;

                        break;
                    case 211:
                        $function = 'LENB';
                        $args = 1;

                        break;
                    case 212:
                        $function = 'ROUNDUP';
                        $args = 2;

                        break;
                    case 213:
                        $function = 'ROUNDDOWN';
                        $args = 2;

                        break;
                    case 214:
                        $function = 'ASC';
                        $args = 1;

                        break;
                    case 215:
                        $function = 'DBCS';
                        $args = 1;

                        break;
                    case 221:
                        $function = 'TODAY';
                        $args = 0;

                        break;
                    case 229:
                        $function = 'SINH';
                        $args = 1;

                        break;
                    case 230:
                        $function = 'COSH';
                        $args = 1;

                        break;
                    case 231:
                        $function = 'TANH';
                        $args = 1;

                        break;
                    case 232:
                        $function = 'ASINH';
                        $args = 1;

                        break;
                    case 233:
                        $function = 'ACOSH';
                        $args = 1;

                        break;
                    case 234:
                        $function = 'ATANH';
                        $args = 1;

                        break;
                    case 235:
                        $function = 'DGET';
                        $args = 3;

                        break;
                    case 244:
                        $function = 'INFO';
                        $args = 1;

                        break;
                    case 252:
                        $function = 'FREQUENCY';
                        $args = 2;

                        break;
                    case 261:
                        $function = 'ERROR.TYPE';
                        $args = 1;

                        break;
                    case 271:
                        $function = 'GAMMALN';
                        $args = 1;

                        break;
                    case 273:
                        $function = 'BINOMDIST';
                        $args = 4;

                        break;
                    case 274:
                        $function = 'CHIDIST';
                        $args = 2;

                        break;
                    case 275:
                        $function = 'CHIINV';
                        $args = 2;

                        break;
                    case 276:
                        $function = 'COMBIN';
                        $args = 2;

                        break;
                    case 277:
                        $function = 'CONFIDENCE';
                        $args = 3;

                        break;
                    case 278:
                        $function = 'CRITBINOM';
                        $args = 3;

                        break;
                    case 279:
                        $function = 'EVEN';
                        $args = 1;

                        break;
                    case 280:
                        $function = 'EXPONDIST';
                        $args = 3;

                        break;
                    case 281:
                        $function = 'FDIST';
                        $args = 3;

                        break;
                    case 282:
                        $function = 'FINV';
                        $args = 3;

                        break;
                    case 283:
                        $function = 'FISHER';
                        $args = 1;

                        break;
                    case 284:
                        $function = 'FISHERINV';
                        $args = 1;

                        break;
                    case 285:
                        $function = 'FLOOR';
                        $args = 2;

                        break;
                    case 286:
                        $function = 'GAMMADIST';
                        $args = 4;

                        break;
                    case 287:
                        $function = 'GAMMAINV';
                        $args = 3;

                        break;
                    case 288:
                        $function = 'CEILING';
                        $args = 2;

                        break;
                    case 289:
                        $function = 'HYPGEOMDIST';
                        $args = 4;

                        break;
                    case 290:
                        $function = 'LOGNORMDIST';
                        $args = 3;

                        break;
                    case 291:
                        $function = 'LOGINV';
                        $args = 3;

                        break;
                    case 292:
                        $function = 'NEGBINOMDIST';
                        $args = 3;

                        break;
                    case 293:
                        $function = 'NORMDIST';
                        $args = 4;

                        break;
                    case 294:
                        $function = 'NORMSDIST';
                        $args = 1;

                        break;
                    case 295:
                        $function = 'NORMINV';
                        $args = 3;

                        break;
                    case 296:
                        $function = 'NORMSINV';
                        $args = 1;

                        break;
                    case 297:
                        $function = 'STANDARDIZE';
                        $args = 3;

                        break;
                    case 298:
                        $function = 'ODD';
                        $args = 1;

                        break;
                    case 299:
                        $function = 'PERMUT';
                        $args = 2;

                        break;
                    case 300:
                        $function = 'POISSON';
                        $args = 3;

                        break;
                    case 301:
                        $function = 'TDIST';
                        $args = 3;

                        break;
                    case 302:
                        $function = 'WEIBULL';
                        $args = 4;

                        break;
                    case 303:
                        $function = 'SUMXMY2';
                        $args = 2;

                        break;
                    case 304:
                        $function = 'SUMX2MY2';
                        $args = 2;

                        break;
                    case 305:
                        $function = 'SUMX2PY2';
                        $args = 2;

                        break;
                    case 306:
                        $function = 'CHITEST';
                        $args = 2;

                        break;
                    case 307:
                        $function = 'CORREL';
                        $args = 2;

                        break;
                    case 308:
                        $function = 'COVAR';
                        $args = 2;

                        break;
                    case 309:
                        $function = 'FORECAST';
                        $args = 3;

                        break;
                    case 310:
                        $function = 'FTEST';
                        $args = 2;

                        break;
                    case 311:
                        $function = 'INTERCEPT';
                        $args = 2;

                        break;
                    case 312:
                        $function = 'PEARSON';
                        $args = 2;

                        break;
                    case 313:
                        $function = 'RSQ';
                        $args = 2;

                        break;
                    case 314:
                        $function = 'STEYX';
                        $args = 2;

                        break;
                    case 315:
                        $function = 'SLOPE';
                        $args = 2;

                        break;
                    case 316:
                        $function = 'TTEST';
                        $args = 4;

                        break;
                    case 325:
                        $function = 'LARGE';
                        $args = 2;

                        break;
                    case 326:
                        $function = 'SMALL';
                        $args = 2;

                        break;
                    case 327:
                        $function = 'QUARTILE';
                        $args = 2;

                        break;
                    case 328:
                        $function = 'PERCENTILE';
                        $args = 2;

                        break;
                    case 331:
                        $function = 'TRIMMEAN';
                        $args = 2;

                        break;
                    case 332:
                        $function = 'TINV';
                        $args = 2;

                        break;
                    case 337:
                        $function = 'POWER';
                        $args = 2;

                        break;
                    case 342:
                        $function = 'RADIANS';
                        $args = 1;

                        break;
                    case 343:
                        $function = 'DEGREES';
                        $args = 1;

                        break;
                    case 346:
                        $function = 'COUNTIF';
                        $args = 2;

                        break;
                    case 347:
                        $function = 'COUNTBLANK';
                        $args = 1;

                        break;
                    case 350:
                        $function = 'ISPMT';
                        $args = 4;

                        break;
                    case 351:
                        $function = 'DATEDIF';
                        $args = 3;

                        break;
                    case 352:
                        $function = 'DATESTRING';
                        $args = 1;

                        break;
                    case 353:
                        $function = 'NUMBERSTRING';
                        $args = 2;

                        break;
                    case 360:
                        $function = 'PHONETIC';
                        $args = 1;

                        break;
                    case 368:
                        $function = 'BAHTTEXT';
                        $args = 1;

                        break;
                    default:
                        throw new Exception('Unrecognized function in formula');

                        break;
                }
                $data = ['function' => $function, 'args' => $args];

                break;
            case 0x22:    //    function with variable number of arguments
            case 0x42:
            case 0x62:
                $name = 'tFuncV';
                $size = 4;
                // offset: 1; size: 1; number of arguments
                $args = ord($formulaData[1]);
                // offset: 2: size: 2; index to built-in sheet function
                $index = self::getUInt2d($formulaData, 2);
                switch ($index) {
                    case 0:
                        $function = 'COUNT';

                        break;
                    case 1:
                        $function = 'IF';

                        break;
                    case 4:
                        $function = 'SUM';

                        break;
                    case 5:
                        $function = 'AVERAGE';

                        break;
                    case 6:
                        $function = 'MIN';

                        break;
                    case 7:
                        $function = 'MAX';

                        break;
                    case 8:
                        $function = 'ROW';

                        break;
                    case 9:
                        $function = 'COLUMN';

                        break;
                    case 11:
                        $function = 'NPV';

                        break;
                    case 12:
                        $function = 'STDEV';

                        break;
                    case 13:
                        $function = 'DOLLAR';

                        break;
                    case 14:
                        $function = 'FIXED';

                        break;
                    case 28:
                        $function = 'LOOKUP';

                        break;
                    case 29:
                        $function = 'INDEX';

                        break;
                    case 36:
                        $function = 'AND';

                        break;
                    case 37:
                        $function = 'OR';

                        break;
                    case 46:
                        $function = 'VAR';

                        break;
                    case 49:
                        $function = 'LINEST';

                        break;
                    case 50:
                        $function = 'TREND';

                        break;
                    case 51:
                        $function = 'LOGEST';

                        break;
                    case 52:
                        $function = 'GROWTH';

                        break;
                    case 56:
                        $function = 'PV';

                        break;
                    case 57:
                        $function = 'FV';

                        break;
                    case 58:
                        $function = 'NPER';

                        break;
                    case 59:
                        $function = 'PMT';

                        break;
                    case 60:
                        $function = 'RATE';

                        break;
                    case 62:
                        $function = 'IRR';

                        break;
                    case 64:
                        $function = 'MATCH';

                        break;
                    case 70:
                        $function = 'WEEKDAY';

                        break;
                    case 78:
                        $function = 'OFFSET';

                        break;
                    case 82:
                        $function = 'SEARCH';

                        break;
                    case 100:
                        $function = 'CHOOSE';

                        break;
                    case 101:
                        $function = 'HLOOKUP';

                        break;
                    case 102:
                        $function = 'VLOOKUP';

                        break;
                    case 109:
                        $function = 'LOG';

                        break;
                    case 115:
                        $function = 'LEFT';

                        break;
                    case 116:
                        $function = 'RIGHT';

                        break;
                    case 120:
                        $function = 'SUBSTITUTE';

                        break;
                    case 124:
                        $function = 'FIND';

                        break;
                    case 125:
                        $function = 'CELL';

                        break;
                    case 144:
                        $function = 'DDB';

                        break;
                    case 148:
                        $function = 'INDIRECT';

                        break;
                    case 167:
                        $function = 'IPMT';

                        break;
                    case 168:
                        $function = 'PPMT';

                        break;
                    case 169:
                        $function = 'COUNTA';

                        break;
                    case 183:
                        $function = 'PRODUCT';

                        break;
                    case 193:
                        $function = 'STDEVP';

                        break;
                    case 194:
                        $function = 'VARP';

                        break;
                    case 197:
                        $function = 'TRUNC';

                        break;
                    case 204:
                        $function = 'USDOLLAR';

                        break;
                    case 205:
                        $function = 'FINDB';

                        break;
                    case 206:
                        $function = 'SEARCHB';

                        break;
                    case 208:
                        $function = 'LEFTB';

                        break;
                    case 209:
                        $function = 'RIGHTB';

                        break;
                    case 216:
                        $function = 'RANK';

                        break;
                    case 219:
                        $function = 'ADDRESS';

                        break;
                    case 220:
                        $function = 'DAYS360';

                        break;
                    case 222:
                        $function = 'VDB';

                        break;
                    case 227:
                        $function = 'MEDIAN';

                        break;
                    case 228:
                        $function = 'SUMPRODUCT';

                        break;
                    case 247:
                        $function = 'DB';

                        break;
                    case 255:
                        $function = '';

                        break;
                    case 269:
                        $function = 'AVEDEV';

                        break;
                    case 270:
                        $function = 'BETADIST';

                        break;
                    case 272:
                        $function = 'BETAINV';

                        break;
                    case 317:
                        $function = 'PROB';

                        break;
                    case 318:
                        $function = 'DEVSQ';

                        break;
                    case 319:
                        $function = 'GEOMEAN';

                        break;
                    case 320:
                        $function = 'HARMEAN';

                        break;
                    case 321:
                        $function = 'SUMSQ';

                        break;
                    case 322:
                        $function = 'KURT';

                        break;
                    case 323:
                        $function = 'SKEW';

                        break;
                    case 324:
                        $function = 'ZTEST';

                        break;
                    case 329:
                        $function = 'PERCENTRANK';

                        break;
                    case 330:
                        $function = 'MODE';

                        break;
                    case 336:
                        $function = 'CONCATENATE';

                        break;
                    case 344:
                        $function = 'SUBTOTAL';

                        break;
                    case 345:
                        $function = 'SUMIF';

                        break;
                    case 354:
                        $function = 'ROMAN';

                        break;
                    case 358:
                        $function = 'GETPIVOTDATA';

                        break;
                    case 359:
                        $function = 'HYPERLINK';

                        break;
                    case 361:
                        $function = 'AVERAGEA';

                        break;
                    case 362:
                        $function = 'MAXA';

                        break;
                    case 363:
                        $function = 'MINA';

                        break;
                    case 364:
                        $function = 'STDEVPA';

                        break;
                    case 365:
                        $function = 'VARPA';

                        break;
                    case 366:
                        $function = 'STDEVA';

                        break;
                    case 367:
                        $function = 'VARA';

                        break;
                    default:
                        throw new Exception('Unrecognized function in formula');

                        break;
                }
                $data = ['function' => $function, 'args' => $args];

                break;
            case 0x23:    //    index to defined name
            case 0x43:
            case 0x63:
                $name = 'tName';
                $size = 5;
                // offset: 1; size: 2; one-based index to definedname record
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
                // offset: 2; size: 2; not used
                $data = $this->definedname[$definedNameIndex]['name'] ?? '';

                break;
            case 0x24:    //    single cell reference e.g. A5
            case 0x44:
            case 0x64:
                $name = 'tRef';
                $size = 5;
                $data = $this->readBIFF8CellAddress(substr($formulaData, 1, 4));

                break;
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
            case 0x45:
            case 0x65:
                $name = 'tArea';
                $size = 9;
                $data = $this->readBIFF8CellRangeAddress(substr($formulaData, 1, 8));

                break;
            case 0x26:    //    Constant reference sub-expression
            case 0x46:
            case 0x66:
                $name = 'tMemArea';
                // offset: 1; size: 4; not used
                // offset: 5; size: 2; size of the following subexpression
                $subSize = self::getUInt2d($formulaData, 5);
                $size = 7 + $subSize;
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));

                break;
            case 0x27:    //    Deleted constant reference sub-expression
            case 0x47:
            case 0x67:
                $name = 'tMemErr';
                // offset: 1; size: 4; not used
                // offset: 5; size: 2; size of the following subexpression
                $subSize = self::getUInt2d($formulaData, 5);
                $size = 7 + $subSize;
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));

                break;
            case 0x29:    //    Variable reference sub-expression
            case 0x49:
            case 0x69:
                $name = 'tMemFunc';
                // offset: 1; size: 2; size of the following sub-expression
                $subSize = self::getUInt2d($formulaData, 1);
                $size = 3 + $subSize;
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));

                break;
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
            case 0x4C:
            case 0x6C:
                $name = 'tRefN';
                $size = 5;
                $data = $this->readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);

                break;
            case 0x2D:    //    Relative 2d range reference
            case 0x4D:
            case 0x6D:
                $name = 'tAreaN';
                $size = 9;
                $data = $this->readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);

                break;
            case 0x39:    //    External name
            case 0x59:
            case 0x79:
                $name = 'tNameX';
                $size = 7;
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
                $index = self::getUInt2d($formulaData, 3);
                // assume index is to EXTERNNAME record
                $data = $this->externalNames[$index - 1]['name'] ?? '';
                // offset: 5; size: 2; not used
                break;
            case 0x3A:    //    3d reference to cell
            case 0x5A:
            case 0x7A:
                $name = 'tRef3d';
                $size = 7;

                try {
                    // offset: 1; size: 2; index to REF entry
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
                    // offset: 3; size: 4; cell address
                    $cellAddress = $this->readBIFF8CellAddress(substr($formulaData, 3, 4));

                    $data = "$sheetRange!$cellAddress";
                } catch (PhpSpreadsheetException $e) {
                    // deleted sheet reference
                    $data = '#REF!';
                }

                break;
            case 0x3B:    //    3d reference to cell range
            case 0x5B:
            case 0x7B:
                $name = 'tArea3d';
                $size = 11;

                try {
                    // offset: 1; size: 2; index to REF entry
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
                    // offset: 3; size: 8; cell address
                    $cellRangeAddress = $this->readBIFF8CellRangeAddress(substr($formulaData, 3, 8));

                    $data = "$sheetRange!$cellRangeAddress";
                } catch (PhpSpreadsheetException $e) {
                    // deleted sheet reference
                    $data = '#REF!';
                }

                break;
            // Unknown cases    // don't know how to deal with
            default:
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');

                break;
        }

        return [
            'id' => $id,
            'name' => $name,
            'size' => $size,
            'data' => $data,
        ];
    }

    /**
     * Reads a cell address in BIFF8 e.g. 'A2' or '$A$2'
     * section 3.3.4.
     *
     * @param string $cellAddressStructure
     *
     * @return string
     */
    private function readBIFF8CellAddress($cellAddressStructure)
    {
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;

        // offset: 2; size: 2; index to column or column offset + relative flags
        // bit: 7-0; mask 0x00FF; column index
        $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1);

        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
            $column = '$' . $column;
        }
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
            $row = '$' . $row;
        }

        return $column . $row;
    }

    /**
     * Reads a cell address in BIFF8 for shared formulas. Uses positive and negative values for row and column
     * to indicate offsets from a base cell
     * section 3.3.4.
     *
     * @param string $cellAddressStructure
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
     *
     * @return string
     */
    private function readBIFF8CellAddressB($cellAddressStructure, $baseCell = 'A1')
    {
        [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell);
        $baseCol = Coordinate::columnIndexFromString($baseCol) - 1;
        $baseRow = (int) $baseRow;

        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
        $rowIndex = self::getUInt2d($cellAddressStructure, 0);
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;

        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
            // offset: 2; size: 2; index to column or column offset + relative flags
            // bit: 7-0; mask 0x00FF; column index
            $colIndex = 0x00FF & self::getUInt2d($cellAddressStructure, 2);

            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
            $column = '$' . $column;
        } else {
            // offset: 2; size: 2; index to column or column offset + relative flags
            // bit: 7-0; mask 0x00FF; column index
            $relativeColIndex = 0x00FF & self::getInt2d($cellAddressStructure, 2);
            $colIndex = $baseCol + $relativeColIndex;
            $colIndex = ($colIndex < 256) ? $colIndex : $colIndex - 256;
            $colIndex = ($colIndex >= 0) ? $colIndex : $colIndex + 256;
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
        }

        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
            $row = '$' . $row;
        } else {
            $rowIndex = ($rowIndex <= 32767) ? $rowIndex : $rowIndex - 65536;
            $row = $baseRow + $rowIndex;
        }

        return $column . $row;
    }

    /**
     * Reads a cell range address in BIFF5 e.g. 'A2:B6' or 'A1'
     * always fixed range
     * section 2.5.14.
     *
     * @param string $subData
     *
     * @return string
     */
    private function readBIFF5CellRangeAddressFixed($subData)
    {
        // offset: 0; size: 2; index to first row
        $fr = self::getUInt2d($subData, 0) + 1;

        // offset: 2; size: 2; index to last row
        $lr = self::getUInt2d($subData, 2) + 1;

        // offset: 4; size: 1; index to first column
        $fc = ord($subData[4]);

        // offset: 5; size: 1; index to last column
        $lc = ord($subData[5]);

        // check values
        if ($fr > $lr || $fc > $lc) {
            throw new Exception('Not a cell range address');
        }

        // column index to letter
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
        $lc = Coordinate::stringFromColumnIndex($lc + 1);

        if ($fr == $lr && $fc == $lc) {
            return "$fc$fr";
        }

        return "$fc$fr:$lc$lr";
    }

    /**
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or 'A1'
     * always fixed range
     * section 2.5.14.
     *
     * @param string $subData
     *
     * @return string
     */
    private function readBIFF8CellRangeAddressFixed($subData)
    {
        // offset: 0; size: 2; index to first row
        $fr = self::getUInt2d($subData, 0) + 1;

        // offset: 2; size: 2; index to last row
        $lr = self::getUInt2d($subData, 2) + 1;

        // offset: 4; size: 2; index to first column
        $fc = self::getUInt2d($subData, 4);

        // offset: 6; size: 2; index to last column
        $lc = self::getUInt2d($subData, 6);

        // check values
        if ($fr > $lr || $fc > $lc) {
            throw new Exception('Not a cell range address');
        }

        // column index to letter
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
        $lc = Coordinate::stringFromColumnIndex($lc + 1);

        if ($fr == $lr && $fc == $lc) {
            return "$fc$fr";
        }

        return "$fc$fr:$lc$lr";
    }

    /**
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or '$A$2:$B$6'
     * there are flags indicating whether column/row index is relative
     * section 3.3.4.
     *
     * @param string $subData
     *
     * @return string
     */
    private function readBIFF8CellRangeAddress($subData)
    {
        // todo: if cell range is just a single cell, should this funciton
        // not just return e.g. 'A1' and not 'A1:A1' ?

        // offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767))
        $fr = self::getUInt2d($subData, 0) + 1;

        // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767))
        $lr = self::getUInt2d($subData, 2) + 1;

        // offset: 4; size: 2; index to first column or column offset + relative flags

        // bit: 7-0; mask 0x00FF; column index
        $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1);

        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
            $fc = '$' . $fc;
        }

        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
            $fr = '$' . $fr;
        }

        // offset: 6; size: 2; index to last column or column offset + relative flags

        // bit: 7-0; mask 0x00FF; column index
        $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1);

        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
            $lc = '$' . $lc;
        }

        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
            $lr = '$' . $lr;
        }

        return "$fc$fr:$lc$lr";
    }

    /**
     * Reads a cell range address in BIFF8 for shared formulas. Uses positive and negative values for row and column
     * to indicate offsets from a base cell
     * section 3.3.4.
     *
     * @param string $subData
     * @param string $baseCell Base cell
     *
     * @return string Cell range address
     */
    private function readBIFF8CellRangeAddressB($subData, $baseCell = 'A1')
    {
        [$baseCol, $baseRow] = Coordinate::indexesFromString($baseCell);
        $baseCol = $baseCol - 1;

        // TODO: if cell range is just a single cell, should this funciton
        // not just return e.g. 'A1' and not 'A1:A1' ?

        // offset: 0; size: 2; first row
        $frIndex = self::getUInt2d($subData, 0); // adjust below

        // offset: 2; size: 2; relative index to first row (0... 65535) should be treated as offset (-32768... 32767)
        $lrIndex = self::getUInt2d($subData, 2); // adjust below

        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
            // absolute column index
            // offset: 4; size: 2; first column with relative/absolute flags
            // bit: 7-0; mask 0x00FF; column index
            $fcIndex = 0x00FF & self::getUInt2d($subData, 4);
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
            $fc = '$' . $fc;
        } else {
            // column offset
            // offset: 4; size: 2; first column with relative/absolute flags
            // bit: 7-0; mask 0x00FF; column index
            $relativeFcIndex = 0x00FF & self::getInt2d($subData, 4);
            $fcIndex = $baseCol + $relativeFcIndex;
            $fcIndex = ($fcIndex < 256) ? $fcIndex : $fcIndex - 256;
            $fcIndex = ($fcIndex >= 0) ? $fcIndex : $fcIndex + 256;
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
        }

        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
            // absolute row index
            $fr = $frIndex + 1;
            $fr = '$' . $fr;
        } else {
            // row offset
            $frIndex = ($frIndex <= 32767) ? $frIndex : $frIndex - 65536;
            $fr = $baseRow + $frIndex;
        }

        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
            // absolute column index
            // offset: 6; size: 2; last column with relative/absolute flags
            // bit: 7-0; mask 0x00FF; column index
            $lcIndex = 0x00FF & self::getUInt2d($subData, 6);
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
            $lc = '$' . $lc;
        } else {
            // column offset
            // offset: 4; size: 2; first column with relative/absolute flags
            // bit: 7-0; mask 0x00FF; column index
            $relativeLcIndex = 0x00FF & self::getInt2d($subData, 4);
            $lcIndex = $baseCol + $relativeLcIndex;
            $lcIndex = ($lcIndex < 256) ? $lcIndex : $lcIndex - 256;
            $lcIndex = ($lcIndex >= 0) ? $lcIndex : $lcIndex + 256;
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
        }

        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
            // absolute row index
            $lr = $lrIndex + 1;
            $lr = '$' . $lr;
        } else {
            // row offset
            $lrIndex = ($lrIndex <= 32767) ? $lrIndex : $lrIndex - 65536;
            $lr = $baseRow + $lrIndex;
        }

        return "$fc$fr:$lc$lr";
    }

    /**
     * Read BIFF8 cell range address list
     * section 2.5.15.
     *
     * @param string $subData
     *
     * @return array
     */
    private function readBIFF8CellRangeAddressList($subData)
    {
        $cellRangeAddresses = [];

        // offset: 0; size: 2; number of the following cell range addresses
        $nm = self::getUInt2d($subData, 0);

        $offset = 2;
        // offset: 2; size: 8 * $nm; list of $nm (fixed) cell range addresses
        for ($i = 0; $i < $nm; ++$i) {
            $cellRangeAddresses[] = $this->readBIFF8CellRangeAddressFixed(substr($subData, $offset, 8));
            $offset += 8;
        }

        return [
            'size' => 2 + 8 * $nm,
            'cellRangeAddresses' => $cellRangeAddresses,
        ];
    }

    /**
     * Read BIFF5 cell range address list
     * section 2.5.15.
     *
     * @param string $subData
     *
     * @return array
     */
    private function readBIFF5CellRangeAddressList($subData)
    {
        $cellRangeAddresses = [];

        // offset: 0; size: 2; number of the following cell range addresses
        $nm = self::getUInt2d($subData, 0);

        $offset = 2;
        // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses
        for ($i = 0; $i < $nm; ++$i) {
            $cellRangeAddresses[] = $this->readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6));
            $offset += 6;
        }

        return [
            'size' => 2 + 6 * $nm,
            'cellRangeAddresses' => $cellRangeAddresses,
        ];
    }

    /**
     * Get a sheet range like Sheet1:Sheet3 from REF index
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
     * in which case an Exception is thrown.
     *
     * @param int $index
     *
     * @return false|string
     */
    private function readSheetRangeByRefIndex($index)
    {
        if (isset($this->ref[$index])) {
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];

            switch ($type) {
                case 'internal':
                    // check if we have a deleted 3d reference
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
                        throw new Exception('Deleted sheet reference');
                    }

                    // we have normal sheet range (collapsed or uncollapsed)
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];

                    if ($firstSheetName == $lastSheetName) {
                        // collapsed sheet range
                        $sheetRange = $firstSheetName;
                    } else {
                        $sheetRange = "$firstSheetName:$lastSheetName";
                    }

                    // escape the single-quotes
                    $sheetRange = str_replace("'", "''", $sheetRange);

                    // if there are special characters, we need to enclose the range in single-quotes
                    // todo: check if we have identified the whole set of special characters
                    // it seems that the following characters are not accepted for sheet names
                    // and we may assume that they are not present: []*/:\?
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
                        $sheetRange = "'$sheetRange'";
                    }

                    return $sheetRange;

                    break;
                default:
                    // TODO: external sheet support
                    throw new Exception('Xls reader only supports internal sheets in formulas');

                    break;
            }
        }

        return false;
    }

    /**
     * read BIFF8 constant value array from array data
     * returns e.g. ['value' => '{1,2;3,4}', 'size' => 40]
     * section 2.5.8.
     *
     * @param string $arrayData
     *
     * @return array
     */
    private static function readBIFF8ConstantArray($arrayData)
    {
        // offset: 0; size: 1; number of columns decreased by 1
        $nc = ord($arrayData[0]);

        // offset: 1; size: 2; number of rows decreased by 1
        $nr = self::getUInt2d($arrayData, 1);
        $size = 3; // initialize
        $arrayData = substr($arrayData, 3);

        // offset: 3; size: var; list of ($nc + 1) * ($nr + 1) constant values
        $matrixChunks = [];
        for ($r = 1; $r <= $nr + 1; ++$r) {
            $items = [];
            for ($c = 1; $c <= $nc + 1; ++$c) {
                $constant = self::readBIFF8Constant($arrayData);
                $items[] = $constant['value'];
                $arrayData = substr($arrayData, $constant['size']);
                $size += $constant['size'];
            }
            $matrixChunks[] = implode(',', $items); // looks like e.g. '1,"hello"'
        }
        $matrix = '{' . implode(';', $matrixChunks) . '}';

        return [
            'value' => $matrix,
            'size' => $size,
        ];
    }

    /**
     * read BIFF8 constant value which may be 'Empty Value', 'Number', 'String Value', 'Boolean Value', 'Error Value'
     * section 2.5.7
     * returns e.g. ['value' => '5', 'size' => 9].
     *
     * @param string $valueData
     *
     * @return array
     */
    private static function readBIFF8Constant($valueData)
    {
        // offset: 0; size: 1; identifier for type of constant
        $identifier = ord($valueData[0]);

        switch ($identifier) {
            case 0x00: // empty constant (what is this?)
                $value = '';
                $size = 9;

                break;
            case 0x01: // number
                // offset: 1; size: 8; IEEE 754 floating-point value
                $value = self::extractNumber(substr($valueData, 1, 8));
                $size = 9;

                break;
            case 0x02: // string value
                // offset: 1; size: var; Unicode string, 16-bit string length
                $string = self::readUnicodeStringLong(substr($valueData, 1));
                $value = '"' . $string['value'] . '"';
                $size = 1 + $string['size'];

                break;
            case 0x04: // boolean
                // offset: 1; size: 1; 0 = FALSE, 1 = TRUE
                if (ord($valueData[1])) {
                    $value = 'TRUE';
                } else {
                    $value = 'FALSE';
                }
                $size = 9;

                break;
            case 0x10: // error code
                // offset: 1; size: 1; error code
                $value = Xls\ErrorCode::lookup(ord($valueData[1]));
                $size = 9;

                break;
            default:
                throw new PhpSpreadsheetException('Unsupported BIFF8 constant');
        }

        return [
            'value' => $value,
            'size' => $size,
        ];
    }

    /**
     * Extract RGB color
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.4.
     *
     * @param string $rgb Encoded RGB value (4 bytes)
     *
     * @return array
     */
    private static function readRGB($rgb)
    {
        // offset: 0; size 1; Red component
        $r = ord($rgb[0]);

        // offset: 1; size: 1; Green component
        $g = ord($rgb[1]);

        // offset: 2; size: 1; Blue component
        $b = ord($rgb[2]);

        // HEX notation, e.g. 'FF00FC'
        $rgb = sprintf('%02X%02X%02X', $r, $g, $b);

        return ['rgb' => $rgb];
    }

    /**
     * Read byte string (8-bit string length)
     * OpenOffice documentation: 2.5.2.
     *
     * @param string $subData
     *
     * @return array
     */
    private function readByteStringShort($subData)
    {
        // offset: 0; size: 1; length of the string (character count)
        $ln = ord($subData[0]);

        // offset: 1: size: var; character array (8-bit characters)
        $value = $this->decodeCodepage(substr($subData, 1, $ln));

        return [
            'value' => $value,
            'size' => 1 + $ln, // size in bytes of data structure
        ];
    }

    /**
     * Read byte string (16-bit string length)
     * OpenOffice documentation: 2.5.2.
     *
     * @param string $subData
     *
     * @return array
     */
    private function readByteStringLong($subData)
    {
        // offset: 0; size: 2; length of the string (character count)
        $ln = self::getUInt2d($subData, 0);

        // offset: 2: size: var; character array (8-bit characters)
        $value = $this->decodeCodepage(substr($subData, 2));

        //return $string;
        return [
            'value' => $value,
            'size' => 2 + $ln, // size in bytes of data structure
        ];
    }

    /**
     * Extracts an Excel Unicode short string (8-bit string length)
     * OpenOffice documentation: 2.5.3
     * function will automatically find out where the Unicode string ends.
     *
     * @param string $subData
     *
     * @return array
     */
    private static function readUnicodeStringShort($subData)
    {
        $value = '';

        // offset: 0: size: 1; length of the string (character count)
        $characterCount = ord($subData[0]);

        $string = self::readUnicodeString(substr($subData, 1), $characterCount);

        // add 1 for the string length
        ++$string['size'];

        return $string;
    }

    /**
     * Extracts an Excel Unicode long string (16-bit string length)
     * OpenOffice documentation: 2.5.3
     * this function is under construction, needs to support rich text, and Asian phonetic settings.
     *
     * @param string $subData
     *
     * @return array
     */
    private static function readUnicodeStringLong($subData)
    {
        $value = '';

        // offset: 0: size: 2; length of the string (character count)
        $characterCount = self::getUInt2d($subData, 0);

        $string = self::readUnicodeString(substr($subData, 2), $characterCount);

        // add 2 for the string length
        $string['size'] += 2;

        return $string;
    }

    /**
     * Read Unicode string with no string length field, but with known character count
     * this function is under construction, needs to support rich text, and Asian phonetic settings
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3.
     *
     * @param string $subData
     * @param int $characterCount
     *
     * @return array
     */
    private static function readUnicodeString($subData, $characterCount)
    {
        $value = '';

        // offset: 0: size: 1; option flags
        // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
        $isCompressed = !((0x01 & ord($subData[0])) >> 0);

        // bit: 2; mask: 0x04; Asian phonetic settings
        $hasAsian = (0x04) & ord($subData[0]) >> 2;

        // bit: 3; mask: 0x08; Rich-Text settings
        $hasRichText = (0x08) & ord($subData[0]) >> 3;

        // offset: 1: size: var; character array
        // this offset assumes richtext and Asian phonetic settings are off which is generally wrong
        // needs to be fixed
        $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed);

        return [
            'value' => $value,
            'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags
        ];
    }

    /**
     * Convert UTF-8 string to string surounded by double quotes. Used for explicit string tokens in formulas.
     * Example:  hello"world  -->  "hello""world".
     *
     * @param string $value UTF-8 encoded string
     *
     * @return string
     */
    private static function UTF8toExcelDoubleQuoted($value)
    {
        return '"' . str_replace('"', '""', $value) . '"';
    }

    /**
     * Reads first 8 bytes of a string and return IEEE 754 float.
     *
     * @param string $data Binary string that is at least 8 bytes long
     *
     * @return float
     */
    private static function extractNumber($data)
    {
        $rknumhigh = self::getInt4d($data, 4);
        $rknumlow = self::getInt4d($data, 0);
        $sign = ($rknumhigh & 0x80000000) >> 31;
        $exp = (($rknumhigh & 0x7ff00000) >> 20) - 1023;
        $mantissa = (0x100000 | ($rknumhigh & 0x000fffff));
        $mantissalow1 = ($rknumlow & 0x80000000) >> 31;
        $mantissalow2 = ($rknumlow & 0x7fffffff);
        $value = $mantissa / 2 ** (20 - $exp);

        if ($mantissalow1 != 0) {
            $value += 1 / 2 ** (21 - $exp);
        }

        $value += $mantissalow2 / 2 ** (52 - $exp);
        if ($sign) {
            $value *= -1;
        }

        return $value;
    }

    /**
     * @param int $rknum
     *
     * @return float
     */
    private static function getIEEE754($rknum)
    {
        if (($rknum & 0x02) != 0) {
            $value = $rknum >> 2;
        } else {
            // changes by mmp, info on IEEE754 encoding from
            // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
            // The RK format calls for using only the most significant 30 bits
            // of the 64 bit floating point value. The other 34 bits are assumed
            // to be 0 so we use the upper 30 bits of $rknum as follows...
            $sign = ($rknum & 0x80000000) >> 31;
            $exp = ($rknum & 0x7ff00000) >> 20;
            $mantissa = (0x100000 | ($rknum & 0x000ffffc));
            $value = $mantissa / 2 ** (20 - ($exp - 1023));
            if ($sign) {
                $value = -1 * $value;
            }
            //end of changes by mmp
        }
        if (($rknum & 0x01) != 0) {
            $value /= 100;
        }

        return $value;
    }

    /**
     * Get UTF-8 string from (compressed or uncompressed) UTF-16 string.
     *
     * @param string $string
     * @param bool $compressed
     *
     * @return string
     */
    private static function encodeUTF16($string, $compressed = false)
    {
        if ($compressed) {
            $string = self::uncompressByteString($string);
        }

        return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE');
    }

    /**
     * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8.
     *
     * @param string $string
     *
     * @return string
     */
    private static function uncompressByteString($string)
    {
        $uncompressedString = '';
        $strLen = strlen($string);
        for ($i = 0; $i < $strLen; ++$i) {
            $uncompressedString .= $string[$i] . "\0";
        }

        return $uncompressedString;
    }

    /**
     * Convert string to UTF-8. Only used for BIFF5.
     *
     * @param string $string
     *
     * @return string
     */
    private function decodeCodepage($string)
    {
        return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage);
    }

    /**
     * Read 16-bit unsigned integer.
     *
     * @param string $data
     * @param int $pos
     *
     * @return int
     */
    public static function getUInt2d($data, $pos)
    {
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8);
    }

    /**
     * Read 16-bit signed integer.
     *
     * @param string $data
     * @param int $pos
     *
     * @return int
     */
    public static function getInt2d($data, $pos)
    {
        return unpack('s', $data[$pos] . $data[$pos + 1])[1];
    }

    /**
     * Read 32-bit signed integer.
     *
     * @param string $data
     * @param int $pos
     *
     * @return int
     */
    public static function getInt4d($data, $pos)
    {
        // FIX: represent numbers correctly on 64-bit system
        // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
        // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
        $_or_24 = ord($data[$pos + 3]);
        if ($_or_24 >= 128) {
            // negative number
            $_ord_24 = -abs((256 - $_or_24) << 24);
        } else {
            $_ord_24 = ($_or_24 & 127) << 24;
        }

        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24;
    }

    private function parseRichText($is)
    {
        $value = new RichText();
        $value->createText($is);

        return $value;
    }

    /**
     * Phpstan 1.4.4 complains that this property is never read.
     * So, we might be able to get rid of it altogether.
     * For now, however, this function makes it readable,
     * which satisfies Phpstan.
     *
     * @codeCoverageIgnore
     */
    public function getMapCellStyleXfIndex(): array
    {
        return $this->mapCellStyleXfIndex;
    }
}

Youez - 2016 - github.com/yon3zu
LinuXploit