| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410 |
- <?php
- /**
- * Excel5 Parser
- *
- * @author Janson
- * @create 2017-11-27
- */
- namespace Asan\PHPExcel\Parser;
- use Asan\PHPExcel\Exception\ParserException;
- use Asan\PHPExcel\Parser\Excel5\OLERead;
- use Asan\PHPExcel\Parser\Excel5\RC4;
- class Excel5 {
- // 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_DATEMODE = 0x0022;
- const XLS_TYPE_FILEPASS = 0x002f;
- const XLS_TYPE_CONTINUE = 0x003c;
- const XLS_TYPE_CODEPAGE = 0x0042;
- const XLS_TYPE_OBJ = 0x005d;
- const XLS_TYPE_SHEET = 0x0085;
- const XLS_TYPE_MULRK = 0x00bd;
- const XLS_TYPE_MULBLANK = 0x00be;
- const XLS_TYPE_XF = 0x00e0;
- const XLS_TYPE_SST = 0x00fc;
- const XLS_TYPE_LABELSST = 0x00fd;
- 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_RK = 0x027e;
- const XLS_TYPE_FORMAT = 0x041e;
- const XLS_TYPE_BOF = 0x0809;
- // 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;
- // Sheet state
- const SHEETSTATE_VISIBLE = 'visible';
- const SHEETSTATE_HIDDEN = 'hidden';
- const SHEETSTATE_VERYHIDDEN = 'veryHidden';
- private static $errorCode = [
- 0x00 => '#NULL!',
- 0x07 => '#DIV/0!',
- 0x0F => '#VALUE!',
- 0x17 => '#REF!',
- 0x1D => '#NAME?',
- 0x24 => '#NUM!',
- 0x2A => '#N/A'
- ];
- /**
- * Base calendar year to use for calculations
- *
- * @var int
- */
- private static $excelBaseDate = Format::CALENDAR_WINDOWS_1900;
- /**
- * Decimal separator
- *
- * @var string
- */
- private static $decimalSeparator;
- /**
- * Thousands separator
- *
- * @var string
- */
- private static $thousandsSeparator;
- /**
- * Currency code
- *
- * @var string
- */
- private static $currencyCode;
- /**
- * Workbook stream data
- *
- * @var string
- */
- private $data;
- /**
- * Size in bytes of $this->data
- *
- * @var int
- */
- private $dataSize;
- /**
- * Current position in stream
- *
- * @var integer
- */
- private $pos;
- /**
- * Worksheets
- *
- * @var array
- */
- private $sheets;
- /**
- * 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;
- /**
- * Row data
- *
- * @var array
- */
- private $row;
- /**
- * Shared formats
- *
- * @var array
- */
- private $formats;
- /**
- * The current sheet of the file
- *
- * @var int
- */
- private $sheetIndex = 0;
- /**
- * Ignore empty row
- *
- * @var bool
- */
- private $ignoreEmpty = false;
- /**
- * The current row index of the sheet
- *
- * @var int
- */
- private $rowIndex = 0;
- /**
- * Max column number
- *
- * @var int
- */
- private $columnLimit = 0;
- /**
- * Whether to the end of the row
- *
- * @var bool
- */
- private $eor = false;
- /**
- * Extended format record
- *
- * @var array
- */
- private $xfRecords = [];
- /**
- * Shared strings. Only applies to BIFF8.
- *
- * @var array
- */
- private $sst = [];
- /**
- * 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 = false;
- /**
- * The current RC4 decryption object
- *
- * @var RC4
- */
- private $rc4Key = null;
- /**
- * 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 = null;
- /**
- * Use OLE reader to extract the relevant data streams from the OLE file
- *
- * @param string $file
- */
- public function loadOLE($file) {
- $oleRead = new OLERead();
- $oleRead->read($file);
- $this->data = $oleRead->getStream($oleRead->workbook);
- }
- /**
- * Ignore empty row
- *
- * @param bool $ignoreEmpty
- *
- * @return $this
- */
- public function ignoreEmptyRow($ignoreEmpty) {
- $this->ignoreEmpty = $ignoreEmpty;
- return $this;
- }
- /**
- * Whether is ignore empty row
- *
- * @return bool
- */
- public function isIgnoreEmptyRow() {
- return $this->ignoreEmpty;
- }
- /**
- * Set sheet index
- *
- * @param int $index
- *
- * @return $this
- */
- public function setSheetIndex($index) {
- $this->sheetIndex = $index;
- return $this;
- }
- /**
- * Get sheet index
- *
- * @return int
- */
- public function getSheetIndex() {
- return $this->sheetIndex;
- }
- /**
- * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns)
- *
- * @throws ParserException
- * @return array
- */
- public function parseWorksheetInfo() {
- if ($this->sheets === null) {
- // total byte size of Excel data (workbook global substream + sheet substreams)
- $this->dataSize = strlen($this->data);
- $this->pos = 0;
- $this->codePage = 'CP1252';
- $this->sheets = [];
- // Parse Workbook Global Substream
- while ($this->pos < $this->dataSize) {
- $code = Format::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_FORMAT:
- $this->readFormat();
- break;
- case self::XLS_TYPE_XF:
- $this->readXf();
- break;
- case self::XLS_TYPE_SST:
- $this->readSst();
- 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 $key => $sheet) {
- if ($sheet['sheetType'] != 0x00) {
- // 0x00: Worksheet
- // 0x02: Chart
- // 0x06: Visual Basic module
- continue;
- }
- $sheet['lastColumnLetter'] = '';
- $sheet['lastColumnIndex'] = null;
- $sheet['totalRows'] = 0;
- $sheet['totalColumns'] = 0;
- $lastRowIndex = 0;
- $this->pos = $sheet['offset'];
- while ($this->pos <= $this->dataSize - 4) {
- $code = Format::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 = Format::getUInt2d($this->data, $this->pos + 2);
- $recordData = substr($this->data, $this->pos + 4, $length);
- // move stream pointer to next record
- $this->pos += 4 + $length;
- $rowIndex = Format::getUInt2d($recordData, 0) + 1;
- $columnIndex = Format::getUInt2d($recordData, 2);
- if ($this->ignoreEmpty) {
- if ($lastRowIndex < $rowIndex) {
- $sheet['totalRows']++;
- }
- $lastRowIndex = $rowIndex;
- } else {
- $sheet['totalRows'] = max($sheet['totalRows'], $rowIndex);
- }
- $sheet['lastColumnIndex'] = max($columnIndex, $sheet['lastColumnIndex']);
- break;
- case self::XLS_TYPE_BOF:
- $this->readBof();
- break;
- case self::XLS_TYPE_EOF:
- $this->readDefault();
- break 2;
- default:
- $this->readDefault();
- break;
- }
- }
- if ($sheet['lastColumnIndex'] !== null) {
- $sheet['lastColumnLetter'] = Format::stringFromColumnIndex($sheet['lastColumnIndex']);
- } else {
- $sheet['lastColumnIndex'] = 0;
- }
- if ($sheet['lastColumnLetter']) {
- $sheet['totalColumns'] = $sheet['lastColumnIndex'] + 1;
- }
- $this->sheets[$key] = $sheet;
- }
- $this->pos = 0;
- }
- return $this->sheets;
- }
- /**
- * Get row data
- *
- * @param int $rowIndex
- * @param int $columnLimit
- *
- * @throws ParserException
- * @return array|bool
- */
- public function getRow($rowIndex, $columnLimit = 0) {
- $this->parseWorksheetInfo();
- // Rewind or change sheet
- if ($rowIndex === 0 || $this->pos < $this->sheets[$this->sheetIndex]['offset']) {
- $this->pos = $this->sheets[$this->sheetIndex]['offset'];
- }
- $endPos = $this->dataSize - 4;
- if (isset($this->sheets[$this->sheetIndex + 1]['offset'])) {
- $endPos = $this->sheets[$this->sheetIndex + 1]['offset'] - 4;
- }
- if ($this->pos >= $endPos) {
- return false;
- }
- $this->rowIndex = $rowIndex;
- $this->columnLimit = $columnLimit;
- $this->eor = false;
- $this->row = $columnLimit ? array_fill(0, $columnLimit, '') : [];
- while ($this->pos <= $endPos) {
- // Remember last position
- $lastPos = $this->pos;
- $code = Format::getUInt2d($this->data, $this->pos);
- switch ($code) {
- case self::XLS_TYPE_BOF:
- $this->readBof();
- 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_BOOLERR:
- $this->readBoolErr();
- break;
- case self::XLS_TYPE_MULBLANK:
- case self::XLS_TYPE_BLANK:
- $this->readBlank();
- break;
- case self::XLS_TYPE_LABEL:
- $this->readLabel();
- break;
- case self::XLS_TYPE_EOF:
- $this->readDefault();
- break 2;
- default:
- $this->readDefault();
- break;
- }
- //End of row
- if ($this->eor) {
- //Recover current position
- $this->pos = $lastPos;
- break;
- }
- }
- return $this->row;
- }
- /**
- * Add cell data
- *
- * @param int $row
- * @param int $column
- * @param mixed $value
- * @param int $xfIndex
- * @return bool
- */
- private function addCell($row, $column, $value, $xfIndex) {
- if ($this->rowIndex != $row) {
- $this->eor = true;
- return false;
- }
- if (!$this->columnLimit || $column < $this->columnLimit) {
- $xfRecord = $this->xfRecords[$xfIndex];
- $this->row[$column] = self::toFormattedString($value, $xfRecord['format']);
- }
- return true;
- }
- /**
- * Read BOF
- *
- * @throws ParserException
- */
- private function readBof() {
- $length = Format::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 = Format::getUInt2d($recordData, 2);
- switch ($substreamType) {
- case self::XLS_WORKBOOKGLOBALS:
- $version = Format::getUInt2d($recordData, 0);
- if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
- throw new ParserException('Cannot read this Excel file. Version is too old.', 1);
- }
- $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 = Format::getUInt2d($this->data, $this->pos);
- $this->readDefault();
- } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
- break;
- }
- }
- /**
- * 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.
- */
- private function readSheet() {
- $length = Format::getUInt2d($this->data, $this->pos + 2);
- $recordData = substr($this->data, $this->pos + 4, $length);
- // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
- // NOTE: not encrypted
- $offset = Format::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 = self::SHEETSTATE_VISIBLE;
- break;
- case 0x01:
- $sheetState = self::SHEETSTATE_HIDDEN;
- break;
- case 0x02:
- $sheetState = self::SHEETSTATE_VERYHIDDEN;
- break;
- default:
- $sheetState = self::SHEETSTATE_VISIBLE;
- break;
- }
- // offset: 5; size: 1; sheet type
- $sheetType = ord($recordData{5});
- // offset: 6; size: var; sheet name
- $name = '';
- if ($this->version == self::XLS_BIFF8) {
- $string = self::readUnicodeStringShort(substr($recordData, 6));
- $name = $string['value'];
- } elseif ($this->version == self::XLS_BIFF7) {
- $string = $this->readByteStringShort(substr($recordData, 6));
- $name = $string['value'];
- }
- // ignore hidden sheet
- if ($sheetState == self::SHEETSTATE_VISIBLE) {
- $this->sheets[] = [
- 'name' => $name, 'offset' => $offset, 'sheetState' => $sheetState, 'sheetType' => $sheetType
- ];
- }
- }
- /**
- * Reads a general type of BIFF record.
- * Does nothing except for moving stream pointer forward to next record.
- */
- private function readDefault() {
- $length = Format::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;
- }
- /**
- * 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.
- * The decryption functions and objects used from here on in are based on the source of Spreadsheet-ParseExcel:
- * http://search.cpan.org/~jmcnamara/Spreadsheet-ParseExcel/
- *
- * @throws ParserException
- */
- private function readFilepass() {
- $length = Format::getUInt2d($this->data, $this->pos + 2);
- if ($length != 54) {
- throw new ParserException('Unexpected file pass record length', 2);
- }
- $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 ParserException('Decryption password incorrect', 3);
- }
- $this->encryption = self::MS_BIFF_CRYPTO_RC4;
- // Decryption required from the record after next onwards
- $this->encryptionStartPos = $this->pos + Format::getUInt2d($this->data, $this->pos + 2);
- }
- /**
- * 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
- *
- * @throws ParserException
- * @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 ParserException('XOr encryption not supported', 4);
- }
- return $recordData;
- }
- /**
- * Make an RC4 decryptor for the given block
- *
- * @param int $block Block for which to create decrypto
- * @param string $valContext MD5 context state
- *
- * @return RC4
- */
- private function makeKey($block, $valContext) {
- $pw = str_repeat("\0", 64);
- for ($i = 0; $i < 5; $i++) {
- $pw[$i] = $valContext[$i];
- }
- $pw[5] = chr($block & 0xff);
- $pw[6] = chr(($block >> 8) & 0xff);
- $pw[7] = chr(($block >> 16) & 0xff);
- $pw[8] = chr(($block >> 24) & 0xff);
- $pw[9] = "\x80";
- $pw[56] = "\x48";
- return new RC4(md5($pw));
- }
- /**
- * Verify RC4 file password
- *
- * @var string $password Password to check
- * @var string $docid Document id
- * @var string $salt_data Salt data
- * @var string $hashedsalt_data Hashed salt data
- * @var string &$valContext Set to the MD5 context of the value
- *
- * @return bool Success
- */
- private function verifyPassword($password, $docid, $salt_data, $hashedsalt_data, &$valContext) {
- $pw = str_repeat("\0", 64);
- for ($i = 0; $i < strlen($password); $i++) {
- $o = ord(substr($password, $i, 1));
- $pw[2 * $i] = chr($o & 0xff);
- $pw[2 * $i + 1] = chr(($o >> 8) & 0xff);
- }
- $pw[2 * $i] = chr(0x80);
- $pw[56] = chr(($i << 4) & 0xff);
- $mdContext1 = md5($pw);
- $offset = 0;
- $keyOffset = 0;
- $toCopy = 5;
- while ($offset != 16) {
- if ((64 - $offset) < 5) {
- $toCopy = 64 - $offset;
- }
- for ($i = 0; $i <= $toCopy; $i++) {
- $pw[$offset + $i] = $mdContext1[$keyOffset + $i];
- }
- $offset += $toCopy;
- if ($offset == 64) {
- $keyOffset = $toCopy;
- $toCopy = 5 - $toCopy;
- $offset = 0;
- continue;
- }
- $keyOffset = 0;
- $toCopy = 5;
- for ($i = 0; $i < 16; $i++) {
- $pw[$offset + $i] = $docid[$i];
- }
- $offset += 16;
- }
- $pw[16] = "\x80";
- for ($i = 0; $i < 47; $i++) {
- $pw[17 + $i] = "\0";
- }
- $pw[56] = "\x80";
- $pw[57] = "\x0a";
- $valContext = md5($pw);
- $key = $this->makeKey(0, $valContext);
- $salt = $key->RC4($salt_data);
- $hashedsalt = $key->RC4($hashedsalt_data);
- $salt .= "\x80" . str_repeat("\0", 47);
- $salt[56] = "\x80";
- $mdContext2 = md5($salt);
- return $mdContext2 == $hashedsalt;
- }
- /**
- * CODEPAGE
- *
- * This record stores the text encoding used to write byte strings, stored as MS Windows code page identifier.
- *
- * @throws ParserException
- */
- private function readCodepage() {
- $length = Format::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: 0; size: 2; code page identifier
- $codePage = Format::getUInt2d($recordData, 0);
- $this->codePage = self::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.
- */
- private function readDateMode() {
- $length = Format::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: 0; size: 2; 0 = base 1900, 1 = base 1904
- self::$excelBaseDate = Format::CALENDAR_WINDOWS_1900;
- if (ord($recordData{0}) == 1) {
- self::$excelBaseDate = Format::CALENDAR_MAC_1904;
- }
- }
- /**
- * 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.
- */
- private function readFormat() {
- $length = Format::getUInt2d($this->data, $this->pos + 2);
- $recordData = substr($this->data, $this->pos + 4, $length);
- // move stream pointer to next record
- $this->pos += 4 + $length;
- $indexCode = Format::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'];
- $this->formats[$indexCode] = $formatString;
- }
- /**
- * XF - Extended Format
- *
- * This record contains formatting information for cells, rows, columns or styles.
- * According to http://support.microsoft.com/kb/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.
- */
- private function readXf() {
- $length = Format::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; Index to FORMAT record
- $numberFormatIndex = Format::getUInt2d($recordData, 2);
- if (isset($this->formats[$numberFormatIndex])) {
- // then we have user-defined format code
- $numberFormat = $this->formats[$numberFormatIndex];
- } elseif (isset(Format::$buildInFormats[$numberFormatIndex])) {
- // then we have built-in format code
- $numberFormat = Format::$buildInFormats[$numberFormatIndex];
- } else {
- // we set the general format code
- $numberFormat = Format::FORMAT_GENERAL;
- }
- $this->xfRecords[] = ['index' => $numberFormatIndex, 'format' => $numberFormat];
- }
- /**
- * 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.
- **/
- private function readSst() {
- // offset within (spliced) record data
- $pos = 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 = Format::getInt4d($recordData, 4);
- $pos += 4;
- // loop through the Unicode strings (16-bit length)
- for ($i = 0; $i < $nm; ++$i) {
- if (!isset($recordData[$pos + 2])) {
- break;
- }
- // number of characters in the Unicode string
- $numChars = Format::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
- $formattingRuns = 0;
- $hasRichText = (($optionFlags & 0x08) != 0);
- if ($hasRichText && isset($recordData[$pos])) {
- // number of Rich-Text formatting runs
- $formattingRuns = Format::getUInt2d($recordData, $pos);
- $pos += 2;
- }
- $extendedRunLength = 0;
- if ($hasAsian && isset($recordData[$pos])) {
- // size of Asian phonetic setting
- $extendedRunLength = Format::getInt4d($recordData, $pos);
- $pos += 4;
- }
- // expected byte length of character array if not split
- $len = ($isCompressed) ? $numChars : $numChars * 2;
- // look up limit position
- $limitPos = 0;
- 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;
- }
- }
- if (!isset($recordData[$pos])) {
- 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) {
- if (!isset($recordData[$pos + $j])) {
- break;
- }
- $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 = Format::getUInt2d($recordData, $pos + $j * 4);
- // index to font record
- $fontIndex = Format::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];
- }
- }
- /**
- * 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.
- */
- private function readRk() {
- $length = Format::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: 0; size: 2; index to row
- $row = Format::getUInt2d($recordData, 0);
- // offset: 2; size: 2; index to column
- $column = Format::getUInt2d($recordData, 2);
- // offset: 4; size: 2; index to XF record
- $xfIndex = Format::getUInt2d($recordData, 4);
- // offset: 6; size: 4; RK value
- $rkNum = Format::getInt4d($recordData, 6);
- $numValue = self::getIEEE754($rkNum);
- // add cell
- $this->addCell($row, $column, $numValue, $xfIndex);
- }
- /**
- * Read LABELSST record
- *
- * This record represents a cell that contains a string. It replaces the LABEL record and RSTRING record used in
- * BIFF2-BIFF5.
- */
- private function readLabelSst() {
- $length = Format::getUInt2d($this->data, $this->pos + 2);
- $recordData = substr($this->data, $this->pos + 4, $length);
- $this->pos += 4 + $length;
- $xfIndex = Format::getUInt2d($recordData, 4);
- $row = Format::getUInt2d($recordData, 0);
- $column = Format::getUInt2d($recordData, 2);
- // offset: 6; size: 4; index to SST record
- $index = Format::getInt4d($recordData, 6);
- $this->addCell($row, $column, $this->sst[$index]['value'], $xfIndex);
- }
- /**
- * Read MULRK record
- *
- * This record represents a cell range containing RK value cells. All cells are located in the same row.
- */
- private function readMulRk() {
- $length = Format::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: 0; size: 2; index to row
- $row = Format::getUInt2d($recordData, 0);
- // offset: 2; size: 2; index to first column
- $colFirst = Format::getUInt2d($recordData, 2);
- // offset: var; size: 2; index to last column
- $colLast = Format::getUInt2d($recordData, $length - 2);
- $columns = $colLast - $colFirst + 1;
- // offset within record data
- $offset = 4;
- for ($i = 0; $i < $columns; ++$i) {
- // offset: var; size: 2; index to XF record
- $xfIndex = Format::getUInt2d($recordData, $offset);
- // offset: var; size: 4; RK value
- $numValue = self::getIEEE754(Format::getInt4d($recordData, $offset + 2));
- $this->addCell($row, $colFirst + $i, $numValue, $xfIndex);
- $offset += 6;
- }
- }
- /**
- * Read NUMBER record
- *
- * This record represents a cell that contains a floating-point value.
- */
- private function readNumber() {
- $length = Format::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: 0; size: 2; index to row
- $row = Format::getUInt2d($recordData, 0);
- // offset: 2; size 2; index to column
- $column = Format::getUInt2d($recordData, 2);
- // offset 4; size: 2; index to XF record
- $xfIndex = Format::getUInt2d($recordData, 4);
- $numValue = self::extractNumber(substr($recordData, 6, 8));
- $this->addCell($row, $column, $numValue, $xfIndex);
- }
- /**
- * 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.
- */
- private function readFormula() {
- $length = Format::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: 0; size: 2; row index
- $row = Format::getUInt2d($recordData, 0);
- // offset: 2; size: 2; col index
- $column = Format::getUInt2d($recordData, 2);
- // offset 4; size: 2; index to XF record
- $xfIndex = Format::getUInt2d($recordData, 4);
- // offset: 6; size: 8; result of the formula
- if ((ord($recordData{6}) == 0) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
- // 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
- $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
- $value = self::mapErrorCode(ord($recordData{8}));
- } elseif ((ord($recordData{6}) == 3) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
- // Formula result is a null string
- $value = '';
- } else {
- // forumla result is a number, first 14 bytes like _NUMBER record
- $value = self::extractNumber(substr($recordData, 6, 8));
- }
- $this->addCell($row, $column, $value, $xfIndex);
- }
- /**
- * 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 = Format::getUInt2d($this->data, $this->pos + 2);
- $recordData = substr($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.
- */
- private function readBoolErr() {
- $length = Format::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: 0; size: 2; row index
- $row = Format::getUInt2d($recordData, 0);
- // offset: 2; size: 2; column index
- $column = Format::getUInt2d($recordData, 2);
- // offset: 4; size: 2; index to XF record
- $xfIndex = Format::getUInt2d($recordData, 4);
- // offset: 6; size: 1; the boolean value or error value
- $boolError = ord($recordData{6});
- // offset: 7; size: 1; 0=boolean; 1=error
- $isError = ord($recordData{7});
- switch ($isError) {
- case 0: // boolean
- $value = (bool)$boolError;
- // add cell value
- $this->addCell($row, $column, $value, $xfIndex);
- break;
- case 1: // error type
- $value = self::mapErrorCode($boolError);
- // add cell value
- $this->addCell($row, $column, $value, $xfIndex);
- break;
- }
- }
- /**
- * Read BLANK record
- */
- private function readBlank() {
- $length = Format::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: 0; size: 2; row index
- $row = Format::getUInt2d($recordData, 0);
- // offset: 2; size: 2; col index
- $column = Format::getUInt2d($recordData, 2);
- // offset: 4; size: 2; XF index
- $xfIndex = Format::getUInt2d($recordData, 4);
- $this->addCell($row, $column, '', $xfIndex);
- }
- /**
- * 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.
- */
- private function readLabel() {
- $length = Format::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: 0; size: 2; index to row
- $row = Format::getUInt2d($recordData, 0);
- // offset: 2; size: 2; index to column
- $column = Format::getUInt2d($recordData, 2);
- // offset: 4; size: 2; XF index
- $xfIndex = Format::getUInt2d($recordData, 4);
- // add cell value
- 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'];
- }
- $this->addCell($row, $column, $value, $xfIndex);
- }
- /**
- * Map error code, e.g. '#N/A'
- *
- * @param int $code
- * @return string
- */
- private static function mapErrorCode($code) {
- if (isset(self::$errorCode[$code])) {
- return self::$errorCode[$code];
- }
- return false;
- }
- /**
- * Convert a value in a pre-defined format to a PHP string
- *
- * @param mixed $value Value to format
- * @param string $format Format code
- * @return string
- */
- private static function toFormattedString($value = '0', $format = Format::FORMAT_GENERAL) {
- // For now we do not treat strings although section 4 of a format code affects strings
- if (!is_numeric($value)) {
- return $value;
- }
- // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
- // it seems to round numbers to a total of 10 digits.
- if (($format === Format::FORMAT_GENERAL) || ($format === Format::FORMAT_TEXT)) {
- return $value;
- }
- // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
- $format = preg_replace('/(\\\(.))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format);
- // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
- $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format);
- // Extract the relevant section depending on whether number is positive, negative, or zero?
- // Text not supported yet.
- // Here is how the sections apply to various values in Excel:
- // 1 section: [POSITIVE/NEGATIVE/ZERO/TEXT]
- // 2 sections: [POSITIVE/ZERO/TEXT] [NEGATIVE]
- // 3 sections: [POSITIVE/TEXT] [NEGATIVE] [ZERO]
- // 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT]
- switch (count($sections)) {
- case 1:
- $format = $sections[0];
- break;
- case 2:
- $format = ($value >= 0) ? $sections[0] : $sections[1];
- $value = abs($value); // Use the absolute value
- break;
- case 3:
- $format = ($value > 0) ? $sections[0] : ( ($value < 0) ? $sections[1] : $sections[2]);
- $value = abs($value); // Use the absolute value
- break;
- case 4:
- $format = ($value > 0) ? $sections[0] : ( ($value < 0) ? $sections[1] : $sections[2]);
- $value = abs($value); // Use the absolute value
- break;
- default:
- // something is wrong, just use first section
- $format = $sections[0];
- break;
- }
- // In Excel formats, "_" is used to add spacing,
- // The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
- $format = preg_replace('/_./', ' ', $format);
- // Save format with color information for later use below
- //$formatColor = $format;
- // Strip color information
- $colorRegex = '/^\\[[a-zA-Z]+\\]/';
- $format = preg_replace($colorRegex, '', $format);
- // Let's begin inspecting the format and converting the value to a formatted string
- // Check for date/time characters (not inside quotes)
- if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) {
- // datetime format
- self::formatAsDate($value, $format);
- } elseif (preg_match('/%$/', $format)) {
- // % number format
- self::formatAsPercentage($value, $format);
- } else {
- if ($format === Format::FORMAT_CURRENCY_EUR_SIMPLE) {
- $value = 'EUR ' . sprintf('%1.2f', $value);
- } else {
- // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
- $format = str_replace(['"', '*'], '', $format);
- // Find out if we need thousands separator
- // This is indicated by a comma enclosed by a digit placeholder:
- // #,# or 0,0
- $useThousands = preg_match('/(#,#|0,0)/', $format);
- if ($useThousands) {
- $format = preg_replace('/0,0/', '00', $format);
- $format = preg_replace('/#,#/', '##', $format);
- }
- // Scale thousands, millions,...
- // This is indicated by a number of commas after a digit placeholder:
- // #, or 0.0,,
- $scale = 1; // same as no scale
- $matches = [];
- if (preg_match('/(#|0)(,+)/', $format, $matches)) {
- $scale = pow(1000, strlen($matches[2]));
- // strip the commas
- $format = preg_replace('/0,+/', '0', $format);
- $format = preg_replace('/#,+/', '#', $format);
- }
- if (preg_match('/#?.*\?\/\?/', $format, $m)) {
- //echo 'Format mask is fractional '.$format.' <br />';
- if ($value != (int)$value) {
- self::formatAsFraction($value, $format);
- }
- } else {
- // Handle the number itself
- // scale number
- $value = $value / $scale;
- // Strip #
- $format = preg_replace('/\\#/', '0', $format);
- $n = "/\[[^\]]+\]/";
- $m = preg_replace($n, '', $format);
- $numberRegex = "/(0+)(\.?)(0*)/";
- if (preg_match($numberRegex, $m, $matches)) {
- $left = $matches[1];
- $dec = $matches[2];
- $right = $matches[3];
- // minimun width of formatted number (including dot)
- $minWidth = strlen($left) + strlen($dec) + strlen($right);
- if ($useThousands) {
- $value = number_format(
- $value,
- strlen($right),
- self::getDecimalSeparator(),
- self::getThousandsSeparator()
- );
- $value = preg_replace($numberRegex, $value, $format);
- } else {
- if (preg_match('/[0#]E[+-]0/i', $format)) {
- //Scientific format
- $value = sprintf('%5.2E', $value);
- } elseif (preg_match('/0([^\d\.]+)0/', $format)) {
- $value = self::complexNumberFormatMask($value, $format);
- } else {
- $sprintfPattern = "%0$minWidth." . strlen($right) . "f";
- $value = sprintf($sprintfPattern, $value);
- $value = preg_replace($numberRegex, $value, $format);
- }
- }
- }
- }
- if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
- // Currency or Accounting
- //$currencyFormat = $m[0];
- $currencyCode = $m[1];
- list($currencyCode) = explode('-', $currencyCode);
- if ($currencyCode == '') {
- $currencyCode = self::getCurrencyCode();
- }
- $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
- }
- }
- }
- return $value;
- }
- /**
- * 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() {
- $i = 0;
- $data = '';
- $spliceOffsets = [0];
- do {
- ++$i;
- // offset: 0; size: 2; identifier
- //$identifier = Cell::getInt2d($this->data, $this->pos);
- // offset: 2; size: 2; length
- $length = Format::getUInt2d($this->data, $this->pos + 2);
- $data .= substr($this->data, $this->pos + 4, $length);
- $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
- $this->pos += 4 + $length;
- $nextIdentifier = Format::getUInt2d($this->data, $this->pos);
- } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
- return ['recordData' => $data, 'spliceOffsets' => $spliceOffsets];
- }
- /**
- * Get the decimal separator. If it has not yet been set explicitly, try to obtain number formatting
- * information from locale.
- *
- * @return string
- */
- private static function getDecimalSeparator() {
- if (!isset(self::$decimalSeparator)) {
- $localeconv = localeconv();
- self::$decimalSeparator = ($localeconv['decimal_point'] != '') ? $localeconv['decimal_point']
- : $localeconv['mon_decimal_point'];
- if (self::$decimalSeparator == '') {
- // Default to .
- self::$decimalSeparator = '.';
- }
- }
- return self::$decimalSeparator;
- }
- /**
- * Get the thousands separator. If it has not yet been set explicitly, try to obtain number formatting
- * information from locale.
- *
- * @return string
- */
- private static function getThousandsSeparator() {
- if (!isset(self::$thousandsSeparator)) {
- $localeconv = localeconv();
- self::$thousandsSeparator = ($localeconv['thousands_sep'] != '') ? $localeconv['thousands_sep']
- : $localeconv['mon_thousands_sep'];
- if (self::$thousandsSeparator == '') {
- // Default to .
- self::$thousandsSeparator = ',';
- }
- }
- return self::$thousandsSeparator;
- }
- /**
- * Get the currency code. If it has not yet been set explicitly, try to obtain the symbol information from locale.
- *
- * @return string
- */
- private static function getCurrencyCode() {
- if (!isset(self::$currencyCode)) {
- $localeconv = localeconv();
- self::$currencyCode = ($localeconv['currency_symbol'] != '') ? $localeconv['currency_symbol']
- : $localeconv['int_curr_symbol'];
- if (self::$currencyCode == '') {
- // Default to $
- self::$currencyCode = '$';
- }
- }
- return self::$currencyCode;
- }
- private static function complexNumberFormatMask($number, $mask) {
- $sign = ($number < 0.0);
- $number = abs($number);
- if (strpos($mask, '.') !== false) {
- $numbers = explode('.', $number . '.0');
- $masks = explode('.', $mask . '.0');
- $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
- $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
- return (($sign) ? '-' : '') . $result1 . '.' . $result2;
- }
- $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
- if ($r > 1) {
- $result = array_reverse($result[0]);
- $offset = 0;
- foreach ($result as $block) {
- $divisor = 1 . $block[0];
- $size = strlen($block[0]);
- $offset = $block[1];
- $blockValue = sprintf('%0' . $size . 'd', fmod($number, $divisor));
- $number = floor($number / $divisor);
- $mask = substr_replace($mask, $blockValue, $offset, $size);
- }
- if ($number > 0) {
- $mask = substr_replace($mask, $number, $offset, 0);
- }
- $result = $mask;
- } else {
- $result = $number;
- }
- return (($sign) ? '-' : '') . $result;
- }
- /**
- * Convert Microsoft Code Page Identifier to Code Page Name which iconv and mbstring understands
- *
- * @param int $codePage Microsoft Code Page Indentifier
- *
- * @throws ParserException
- * @return string Code Page Name
- */
- private static function NumberToName($codePage = 1252) {
- switch ($codePage) {
- case 367:
- return 'ASCII'; //ASCII
- case 437:
- return 'CP437'; //OEM US
- case 720:
- throw new ParserException('Code page 720 not supported.', 5); //OEM Arabic
- case 737:
- return 'CP737'; //OEM Greek
- case 775:
- return 'CP775'; //OEM Baltic
- case 850:
- return 'CP850'; //OEM Latin I
- case 852:
- return 'CP852'; //OEM Latin II (Central European)
- case 855:
- return 'CP855'; //OEM Cyrillic
- case 857:
- return 'CP857'; //OEM Turkish
- case 858:
- return 'CP858'; //OEM Multilingual Latin I with Euro
- case 860:
- return 'CP860'; //OEM Portugese
- case 861:
- return 'CP861'; //OEM Icelandic
- case 862:
- return 'CP862'; //OEM Hebrew
- case 863:
- return 'CP863'; //OEM Canadian (French)
- case 864:
- return 'CP864'; //OEM Arabic
- case 865:
- return 'CP865'; //OEM Nordic
- case 866:
- return 'CP866'; //OEM Cyrillic (Russian)
- case 869:
- return 'CP869'; //OEM Greek (Modern)
- case 874:
- return 'CP874'; //ANSI Thai
- case 932:
- return 'CP932'; //ANSI Japanese Shift-JIS
- case 936:
- return 'CP936'; //ANSI Chinese Simplified GBK
- case 949:
- return 'CP949'; //ANSI Korean (Wansung)
- case 950:
- return 'CP950'; //ANSI Chinese Traditional BIG5
- case 1200:
- return 'UTF-16LE'; //UTF-16 (BIFF8)
- case 1250:
- return 'CP1250'; //ANSI Latin II (Central European)
- case 1251:
- return 'CP1251'; //ANSI Cyrillic
- case 0: //CodePage is not always correctly set when the xls file was saved by Apple's Numbers program
- case 1252:
- return 'CP1252'; //ANSI Latin I (BIFF4-BIFF7)
- case 1253:
- return 'CP1253'; //ANSI Greek
- case 1254:
- return 'CP1254'; //ANSI Turkish
- case 1255:
- return 'CP1255'; //ANSI Hebrew
- case 1256:
- return 'CP1256'; //ANSI Arabic
- case 1257:
- return 'CP1257'; //ANSI Baltic
- case 1258:
- return 'CP1258'; //ANSI Vietnamese
- case 1361:
- return 'CP1361'; //ANSI Korean (Johab)
- case 10000:
- return 'MAC'; //Apple Roman
- case 10001:
- return 'CP932'; //Macintosh Japanese
- case 10002:
- return 'CP950'; //Macintosh Chinese Traditional
- case 10003:
- return 'CP1361'; //Macintosh Korean
- case 10004:
- return 'MACARABIC'; // Apple Arabic
- case 10005:
- return 'MACHEBREW'; //Apple Hebrew
- case 10006:
- return 'MACGREEK'; //Macintosh Greek
- case 10007:
- return 'MACCYRILLIC'; //Macintosh Cyrillic
- case 10008:
- return 'CP936'; //Macintosh - Simplified Chinese (GB 2312)
- case 10010:
- return 'MACROMANIA'; //Macintosh Romania
- case 10017:
- return 'MACUKRAINE'; //Macintosh Ukraine
- case 10021:
- return 'MACTHAI'; //Macintosh Thai
- case 10029:
- return 'MACCENTRALEUROPE'; //Macintosh Central Europe
- case 10079:
- return 'MACICELAND'; //Macintosh Icelandic
- case 10081:
- return 'MACTURKISH'; //Macintosh Turkish
- case 10082:
- return 'MACCROATIAN'; //Macintosh Croatian
- case 21010:
- return 'UTF-16LE'; //UTF-16 (BIFF8) This isn't correct, but some Excel writer libraries erroneously
- // use Codepage 21010 for UTF-16LE
- case 32768:
- return 'MAC'; //Apple Roman
- case 32769:
- throw new ParserException('Code page 32769 not supported.', 6); //ANSI Latin I (BIFF2-BIFF3)
- case 65000:
- return 'UTF-7'; //Unicode (UTF-7)
- case 65001:
- return 'UTF-8'; //Unicode (UTF-8)
- }
- throw new ParserException("Unknown codepage: $codePage", 7);
- }
- /**
- * 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));
- // size in bytes of data structure
- return ['value' => $value, 'size' => 1 + $ln];
- }
- /**
- * 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 = Format::getUInt2d($subData, 0);
- // offset: 2: size: var; character array (8-bit characters)
- $value = $this->decodeCodepage(substr($subData, 2));
- // size in bytes of data structure
- return ['value' => $value, 'size' => 2 + $ln];
- }
- private static function formatAsDate(&$value, &$format) {
- // strip off first part containing e.g. [$-F800] or [$USD-409]
- // general syntax: [$<Currency string>-<language info>]
- // language info is in hexadecimal
- $format = preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
- // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
- // but we don't want to change any quoted strings
- $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
- // Only process the non-quoted blocks for date format characters
- $blocks = explode('"', $format);
- foreach($blocks as $key => &$block) {
- if ($key % 2 == 0) {
- $block = strtr($block, Format::$dateFormatReplacements);
- if (strpos($block, 'A') === false) {
- // 24-hour time format
- $block = strtr($block, Format::$dateFormatReplacements24);
- } else {
- // 12-hour time format
- $block = strtr($block, Format::$dateFormatReplacements12);
- }
- }
- }
- $format = implode('"', $blocks);
- // escape any quoted characters so that DateTime format() will render them correctly
- $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
- $dateObj = self::ExcelToPHPObject($value);
- $value = $dateObj->format($format);
- }
- private static function setLowercaseCallback($matches) {
- return mb_strtolower($matches[0]);
- }
- private static function escapeQuotesCallback($matches) {
- return '\\' . implode('\\', str_split($matches[1]));
- }
- /**
- * Convert a date from Excel to a PHP Date/Time object
- *
- * @param int $dateValue Excel date/time value
- *
- * @return \DateTime PHP date/time object
- */
- private static function ExcelToPHPObject($dateValue = 0) {
- $dateTime = self::ExcelToPHP($dateValue);
- $days = floor($dateTime / 86400);
- $time = round((($dateTime / 86400) - $days) * 86400);
- $hours = round($time / 3600);
- $minutes = round($time / 60) - ($hours * 60);
- $seconds = round($time) - ($hours * 3600) - ($minutes * 60);
- $dateObj = new \DateTime("1-Jan-1970+$days days");
- $dateObj->setTime($hours, $minutes, $seconds);
- return $dateObj;
- }
- /**
- * Convert a date from Excel to PHP
- *
- * @param int $dateValue Excel date/time value
- *
- * @return int PHP serialized date/time
- */
- private static function ExcelToPHP($dateValue = 0) {
- if (self::$excelBaseDate == Format::CALENDAR_WINDOWS_1900) {
- $excelBaseDate = 25569;
- //Adjust for the spurious 29-Feb-1900 (Day 60)
- if ($dateValue < 60) {
- --$excelBaseDate;
- }
- } else {
- $excelBaseDate = 24107;
- }
- // Perform conversion
- if ($dateValue >= 1) {
- $utcDays = $dateValue - $excelBaseDate;
- $returnValue = round($utcDays * 86400);
- if (($returnValue <= PHP_INT_MAX) && ($returnValue >= -PHP_INT_MAX)) {
- $returnValue = (integer) $returnValue;
- }
- } else {
- $hours = round($dateValue * 24);
- $mins = round($dateValue * 1440) - round($hours * 60);
- $secs = round($dateValue * 86400) - round($hours * 3600) - round($mins * 60);
- $returnValue = (integer) gmmktime($hours, $mins, $secs);
- }
- return $returnValue;
- }
- private static function formatAsPercentage(&$value, &$format) {
- if ($format === Format::FORMAT_PERCENTAGE) {
- $value = round((100 * $value), 0) . '%';
- } else {
- if (preg_match('/\.[#0]+/i', $format, $m)) {
- $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
- $format = str_replace($m[0], $s, $format);
- }
- if (preg_match('/^[#0]+/', $format, $m)) {
- $format = str_replace($m[0], strlen($m[0]), $format);
- }
- $format = '%' . str_replace('%', 'f%%', $format);
- $value = sprintf($format, 100 * $value);
- }
- }
- private static function formatAsFraction(&$value, &$format) {
- $sign = ($value < 0) ? '-' : '';
- $integerPart = floor(abs($value));
- $decimalPart = trim(fmod(abs($value), 1), '0.');
- $decimalLength = strlen($decimalPart);
- $decimalDivisor = pow(10, $decimalLength);
- $GCD = self::GCD([$decimalPart, $decimalDivisor]);
- $adjustedDecimalPart = $decimalPart/$GCD;
- $adjustedDecimalDivisor = $decimalDivisor/$GCD;
- if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
- if ($integerPart == 0) {
- $integerPart = '';
- }
- $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
- } else {
- $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
- $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
- }
- }
- /**
- * GCD
- *
- * Returns the greatest common divisor of a series of numbers. The greatest common divisor is the largest
- * integer that divides both number1 and number2 without a remainder.
- * Excel Function:
- * GCD(number1[,number2[, ...]])
- *
- * @param array $params
- *
- * @return integer Greatest Common Divisor
- */
- private static function GCD($params) {
- $returnValue = 1;
- $allValuesFactors = [];
- // Loop through arguments
- $flattenArr = self::flattenArray($params);
- foreach ($flattenArr as $value) {
- if (!is_numeric($value)) {
- return '#VALUE!';
- } elseif ($value == 0) {
- continue;
- } elseif ($value < 0) {
- return '#NULL!';
- }
- $factors = self::factors($value);
- $countedFactors = array_count_values($factors);
- $allValuesFactors[] = $countedFactors;
- }
- $allValuesCount = count($allValuesFactors);
- if ($allValuesCount == 0) {
- return 0;
- }
- $mergedArray = $allValuesFactors[0];
- for ($i=1; $i < $allValuesCount; ++$i) {
- $mergedArray = array_intersect_key($mergedArray, $allValuesFactors[$i]);
- }
- $mergedArrayValues = count($mergedArray);
- if ($mergedArrayValues == 0) {
- return $returnValue;
- } elseif ($mergedArrayValues > 1) {
- foreach ($mergedArray as $mergedKey => $mergedValue) {
- foreach ($allValuesFactors as $highestPowerTest) {
- foreach ($highestPowerTest as $testKey => $testValue) {
- if (($testKey == $mergedKey) && ($testValue < $mergedValue)) {
- $mergedArray[$mergedKey] = $testValue;
- $mergedValue = $testValue;
- }
- }
- }
- }
- $returnValue = 1;
- foreach ($mergedArray as $key => $value) {
- $returnValue *= pow($key, $value);
- }
- return $returnValue;
- } else {
- $keys = array_keys($mergedArray);
- $key = $keys[0];
- $value = $mergedArray[$key];
- foreach ($allValuesFactors as $testValue) {
- foreach ($testValue as $mergedKey => $mergedValue) {
- if (($mergedKey == $key) && ($mergedValue < $value)) {
- $value = $mergedValue;
- }
- }
- }
- return pow($key, $value);
- }
- }
- /**
- * Convert a multi-dimensional array to a simple 1-dimensional array
- *
- * @param array $array Array to be flattened
- *
- * @return array Flattened array
- */
- private static function flattenArray($array) {
- if (!is_array($array)) {
- return (array) $array;
- }
- $arrayValues = [];
- foreach ($array as $value) {
- if (is_array($value)) {
- foreach ($value as $val) {
- if (is_array($val)) {
- foreach ($val as $v) {
- $arrayValues[] = $v;
- }
- } else {
- $arrayValues[] = $val;
- }
- }
- } else {
- $arrayValues[] = $value;
- }
- }
- return $arrayValues;
- }
- /**
- * Return an array of the factors of the input value
- *
- * @param int $value
- *
- * @return array
- */
- private static function factors($value) {
- $startVal = floor(sqrt($value));
- $factorArray = [];
- for ($i = $startVal; $i > 1; --$i) {
- if (($value % $i) == 0) {
- $factorArray = array_merge($factorArray, self::factors($value / $i));
- $factorArray = array_merge($factorArray, self::factors($i));
- if ($i <= sqrt($value)) {
- break;
- }
- }
- }
- if (!empty($factorArray)) {
- rsort($factorArray);
- return $factorArray;
- }
- return [(int) $value];
- }
- /**
- * 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
- *
- * @param string $subData
- * @param int $characterCount
- *
- * @return array
- */
- private static function readUnicodeString($subData, $characterCount) {
- // 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);
- // 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
- );
- // the size in bytes including the option flags
- return ['value' => $value, 'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount];
- }
- /**
- * Extracts an Excel Unicode short string (8-bit string length), this function will automatically find out
- * where the Unicode string ends.
- *
- * @param string $subData
- *
- * @return array
- */
- private static function readUnicodeStringShort($subData) {
- // 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'] += 1;
- return $string;
- }
- /**
- * Extracts an Excel Unicode long string (16-bit string length), this function is under construction,
- * needs to support rich text, and Asian phonetic settings
- *
- * @param string $subData
- *
- * @return array
- */
- private static function readUnicodeStringLong($subData) {
- // offset: 0: size: 2; length of the string (character count)
- $characterCount = Format::getUInt2d($subData, 0);
- $string = self::readUnicodeString(substr($subData, 2), $characterCount);
- // add 2 for the string length
- $string['size'] += 2;
- return $string;
- }
- 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 / pow(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 mb_convert_encoding($string, 'UTF-8', 'UTF-16LE');
- }
- /**
- * Convert string to UTF-8. Only used for BIFF5.
- *
- * @param string $string
- *
- * @return string
- */
- private function decodeCodepage($string) {
- return mb_convert_encoding($string, 'UTF-8', $this->codePage);
- }
- /**
- * 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;
- }
- /**
- * 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 = Format::getInt4d($data, 4);
- $rkNumLow = Format::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 / pow(2, (20 - $exp));
- if ($mantissaLow1 != 0) {
- $value += 1 / pow(2, (21 - $exp));
- }
- $value += $mantissaLow2 / pow(2, (52 - $exp));
- if ($sign) {
- $value *= -1;
- }
- return $value;
- }
- }
|