nfTlWNl rn Ѻq$³*LJ0Y8i(?Q&VY{ V~PQ?ltjdLa ͐p'G*!gؑ8@B_#1Iķխ}`^6[Չ1,_Ng5[3I~}ELT\a? 2Unr_j9@ ʿJr jq,t1I("Ԫ*jM nC,Jz:qf&ȻVG6XZY1!UM:r«Nnガ6Y,mW38H׼k9$|)Mmcz FUh"tY,K'=~6}6e*͆98YZMsU^#n37L#S[~;X?q~`NHVY_2DD'E=VXn?Z3w4iq >RalSHzII4nBBj˰יTHguB\<>br.x Q@=K'&iJKNzB{lW5䥄*v(lPwDk&-{^ۆ~y%b!ƛrOqyf`y3b[V/#eVfC~3ܓLi x5ں7QQ'yDH"4h+̓T>2XTȊnwKcGb֦B d( N,I㜆qGi~Ktfgc(K {rIϵhc Ĉ6/N g5('PeWb9+3,RY}hEͯ$B|̇>oG㜆qGi~Ktfgc(P#l|T*FHy(,%5W:f/E}}p@*B< t%3-q"?9IVexZGsUhͩllPQܥ%c]02 /]Qf7o r&~-.YLXju+i}>r$cl&N ;DP]oCJieaTD%vX, óAun9 2YəMC]9;C A4d83;WKl`p){;fOi3$K^o\%d|r.whQ.ʴPyo*$N,qObZdTAV<*̾d>1!jWJ5 ^ 4ӞoU#o6eʙcI#"3j`M±LfcAmB!|FKQIr#t,wNġ+WwԨ8x~- r{fb^BFwnUid/}*"oc `Y]X"Z r %tjJ(mYvn4z "/(Ɖvbe`HZ2U@D,*t?^ r䛠t5!3. 3f ҠVxD%vXIp/Yfl>cdwo `0Wp(崽UOȂ!Ύ࿿s[KO3%-Sy,K_,XR}ɰkؑT'@3pI;;Rݟ#f.(U?KdE[ֵ 0p'zJ,9q(KmDK,Wd{M0"f [O.GavhTIequ-d?dSrOfC`B7IPƋ|mֵ"'#&ؚπѷ3`=/MahI3 [NwR5Lst4о`n. Bd_-FRmvz9B]WG^qSJ}cশ6mŹ,F q#ŵ Pmݕ/ )2 ke g\W^h~Ht۽X5! 5uf w?f$`DY>$(0OKGRjg|c@$KYj]գ[H  2w#Г)8گ{eʥdmOǵBlUIC 5 276K^` Uv;Jv큁~!H *On&}5d)O'Wɗ_(%}b.k/WdN (#HyzBrDDU"iɯYmEdmbeD zK0R8o>8Kq !8@kϋI(k,ՄYt(UTf1ZknY3Sߠ0'29#Tw &$03M[zjJfL֍}'egT2{9o&+(T\9ܵgY K0R8o>8Kq 9.Tlŕ>dBvdCe0pfME -P1Vj_6Eh˅l;^n1ܶT72aIM#h7C3+pPY\ټtvbt%3-Ͱڴ]hÇ טrI5iXsuz?y5Ted8UAB.|(\#'ӎ~$ɶW,P2`J÷M6[\A -&ZԶ:@+s) R*د[|؛)f RDyȅoJd{oay_5fv `sWCO[U2<]m(d֔[ERd$RvxlW0~m_Dp``HIBZ %n'şبBeqEP<)P`3|&$Uv`?7VjK?#J+$pzAnPumɩͮ->;c'ȁGCO-o7˝4]YMXon90kgٷùD] eP2<݅!v;Hy{ѐ$ajN˟ִAy0m($line, "\r\n")) == 5) && (stripos($line, 'sep=') === 0)) { $this->delimiter = substr($line, 4, 1); return; } $this->skipBOM(); } /** * Infer the separator if it isn't explicitly set in the file or specified by the user. */ protected function inferSeparator(): void { if ($this->delimiter !== null) { return; } $inferenceEngine = new Delimiter($this->fileHandle, $this->escapeCharacter, $this->enclosure); // If number of lines is 0, nothing to infer : fall back to the default if ($inferenceEngine->linesCounted() === 0) { $this->delimiter = $inferenceEngine->getDefaultDelimiter(); $this->skipBOM(); return; } $this->delimiter = $inferenceEngine->infer(); // If no delimiter could be detected, fall back to the default if ($this->delimiter === null) { $this->delimiter = $inferenceEngine->getDefaultDelimiter(); } $this->skipBOM(); } /** * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). */ public function listWorksheetInfo(string $filename): array { // Open file $this->openFileOrMemory($filename); $fileHandle = $this->fileHandle; // Skip BOM, if any $this->skipBOM(); $this->checkSeparator(); $this->inferSeparator(); $worksheetInfo = []; $worksheetInfo[0]['worksheetName'] = 'Worksheet'; $worksheetInfo[0]['lastColumnLetter'] = 'A'; $worksheetInfo[0]['lastColumnIndex'] = 0; $worksheetInfo[0]['totalRows'] = 0; $worksheetInfo[0]['totalColumns'] = 0; // Loop through each line of the file in turn $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); while (is_array($rowData)) { ++$worksheetInfo[0]['totalRows']; $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], count($rowData) - 1); $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); } $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1); $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1; // Close file fclose($fileHandle); return $worksheetInfo; } /** * Loads Spreadsheet from file. */ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); // Load into this instance return $this->loadIntoExisting($filename, $spreadsheet); } /** * Loads Spreadsheet from string. */ public function loadSpreadsheetFromString(string $contents): Spreadsheet { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); // Load into this instance return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true); } private function openFileOrMemory(string $filename): void { // Open file $fhandle = $this->canRead($filename); if (!$fhandle) { throw new Exception($filename . ' is an Invalid Spreadsheet file.'); } if ($this->inputEncoding === self::GUESS_ENCODING) { $this->inputEncoding = self::guessEncoding($filename, $this->fallbackEncoding); } $this->openFile($filename); if ($this->inputEncoding !== 'UTF-8') { fclose($this->fileHandle); $entireFile = file_get_contents($filename); $fileHandle = fopen('php://memory', 'r+b'); if ($fileHandle !== false && $entireFile !== false) { $this->fileHandle = $fileHandle; $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding); fwrite($this->fileHandle, $data); $this->skipBOM(); } } } public function setTestAutoDetect(bool $value): self { $this->testAutodetect = $value; return $this; } private function setAutoDetect(?string $value): ?string { $retVal = null; if ($value !== null && $this->testAutodetect) { $retVal2 = @ini_set('auto_detect_line_endings', $value); if (is_string($retVal2)) { $retVal = $retVal2; } } return $retVal; } public function castFormattedNumberToNumeric( bool $castFormattedNumberToNumeric, bool $preserveNumericFormatting = false ): void { $this->castFormattedNumberToNumeric = $castFormattedNumberToNumeric; $this->preserveNumericFormatting = $preserveNumericFormatting; } /** * Open data uri for reading. */ private function openDataUri(string $filename): void { $fileHandle = fopen($filename, 'rb'); if ($fileHandle === false) { // @codeCoverageIgnoreStart throw new ReaderException('Could not open file ' . $filename . ' for reading.'); // @codeCoverageIgnoreEnd } $this->fileHandle = $fileHandle; } /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. */ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet { return $this->loadStringOrFile($filename, $spreadsheet, false); } /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. */ private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bool $dataUri): Spreadsheet { // Deprecated in Php8.1 $iniset = $this->setAutoDetect('1'); // Open file if ($dataUri) { $this->openDataUri($filename); } else { $this->openFileOrMemory($filename); } $fileHandle = $this->fileHandle; // Skip BOM, if any $this->skipBOM(); $this->checkSeparator(); $this->inferSeparator(); // Create new PhpSpreadsheet object while ($spreadsheet->getSheetCount() <= $this->sheetIndex) { $spreadsheet->createSheet(); } $sheet = $spreadsheet->setActiveSheetIndex($this->sheetIndex); // Set our starting row based on whether we're in contiguous mode or not $currentRow = 1; $outRow = 0; // Loop through each line of the file in turn $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); $valueBinder = Cell::getValueBinder(); $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion(); while (is_array($rowData)) { $noOutputYet = true; $columnLetter = 'A'; foreach ($rowData as $rowDatum) { $this->convertBoolean($rowDatum, $preserveBooleanString); $numberFormatMask = $this->convertFormattedNumber($rowDatum); if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) { if ($this->contiguous) { if ($noOutputYet) { $noOutputYet = false; ++$outRow; } } else { $outRow = $currentRow; } // Set basic styling for the value (Note that this could be overloaded by styling in a value binder) $sheet->getCell($columnLetter . $outRow)->getStyle() ->getNumberFormat() ->setFormatCode($numberFormatMask); // Set cell value $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum); } ++$columnLetter; } $rowData = fgetcsv($fileHandle, 0, $this->delimiter ?? '', $this->enclosure, $this->escapeCharacter); ++$currentRow; } // Close file fclose($fileHandle); $this->setAutoDetect($iniset); // Return return $spreadsheet; } /** * Convert string true/false to boolean, and null to null-string. * * @param mixed $rowDatum */ private function convertBoolean(&$rowDatum, bool $preserveBooleanString): void { if (is_string($rowDatum) && !$preserveBooleanString) { if (strcasecmp(Calculation::getTRUE(), $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) { $rowDatum = true; } elseif (strcasecmp(Calculation::getFALSE(), $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) { $rowDatum = false; } } else { $rowDatum = $rowDatum ?? ''; } } /** * Convert numeric strings to int or float values. * * @param mixed $rowDatum */ private function convertFormattedNumber(&$rowDatum): string { $numberFormatMask = NumberFormat::FORMAT_GENERAL; if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) { $numeric = str_replace( [StringHelper::getThousandsSeparator(), StringHelper::getDecimalSeparator()], ['', '.'], $rowDatum ); if (is_numeric($numeric)) { $decimalPos = strpos($rowDatum, StringHelper::getDecimalSeparator()); if ($this->preserveNumericFormatting === true) { $numberFormatMask = (strpos($rowDatum, StringHelper::getThousandsSeparator()) !== false) ? '#,##0' : '0'; if ($decimalPos !== false) { $decimals = strlen($rowDatum) - $decimalPos - 1; $numberFormatMask .= '.' . str_repeat('0', min($decimals, 6)); } } $rowDatum = ($decimalPos !== false) ? (float) $numeric : (int) $numeric; } } return $numberFormatMask; } public function getDelimiter(): ?string { return $this->delimiter; } public function setDelimiter(?string $delimiter): self { $this->delimiter = $delimiter; return $this; } public function getEnclosure(): string { return $this->enclosure; } public function setEnclosure(string $enclosure): self { if ($enclosure == '') { $enclosure = '"'; } $this->enclosure = $enclosure; return $this; } public function getSheetIndex(): int { return $this->sheetIndex; } public function setSheetIndex(int $indexValue): self { $this->sheetIndex = $indexValue; return $this; } public function setContiguous(bool $contiguous): self { $this->contiguous = $contiguous; return $this; } public function getContiguous(): bool { return $this->contiguous; } public function setEscapeCharacter(string $escapeCharacter): self { $this->escapeCharacter = $escapeCharacter; return $this; } public function getEscapeCharacter(): string { return $this->escapeCharacter; } /** * Can the current IReader read the file? */ public function canRead(string $filename): bool { // Check if file exists try { $this->openFile($filename); } catch (ReaderException $e) { return false; } fclose($this->fileHandle); // Trust file extension if any $extension = strtolower(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_EXTENSION)); if (in_array($extension, ['csv', 'tsv'])) { return true; } // Attempt to guess mimetype $type = mime_content_type($filename); $supportedTypes = [ 'application/csv', 'text/csv', 'text/plain', 'inode/x-empty', ]; return in_array($type, $supportedTypes, true); } private static function guessEncodingTestNoBom(string &$encoding, string &$contents, string $compare, string $setEncoding): void { if ($encoding === '') { $pos = strpos($contents, $compare); if ($pos !== false && $pos % strlen($compare) === 0) { $encoding = $setEncoding; } } } private static function guessEncodingNoBom(string $filename): string { $encoding = ''; $contents = file_get_contents($filename); self::guessEncodingTestNoBom($encoding, $contents, self::UTF32BE_LF, 'UTF-32BE'); self::guessEncodingTestNoBom($encoding, $contents, self::UTF32LE_LF, 'UTF-32LE'); self::guessEncodingTestNoBom($encoding, $contents, self::UTF16BE_LF, 'UTF-16BE'); self::guessEncodingTestNoBom($encoding, $contents, self::UTF16LE_LF, 'UTF-16LE'); if ($encoding === '' && preg_match('//u', $contents) === 1) { $encoding = 'UTF-8'; } return $encoding; } private static function guessEncodingTestBom(string &$encoding, string $first4, string $compare, string $setEncoding): void { if ($encoding === '') { if ($compare === substr($first4, 0, strlen($compare))) { $encoding = $setEncoding; } } } private static function guessEncodingBom(string $filename): string { $encoding = ''; $first4 = file_get_contents($filename, false, null, 0, 4); if ($first4 !== false) { self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8'); self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE'); self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE'); self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE'); self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE'); } return $encoding; } public static function guessEncoding(string $filename, string $dflt = self::DEFAULT_FALLBACK_ENCODING): string { $encoding = self::guessEncodingBom($filename); if ($encoding === '') { $encoding = self::guessEncodingNoBom($filename); } return ($encoding === '') ? $dflt : $encoding; } public function setPreserveNullString(bool $value): self { $this->preserveNullString = $value; return $this; } public function getPreserveNullString(): bool { return $this->preserveNullString; } }