This commit is contained in:
steven 2025-08-11 22:23:30 +02:00
commit 72a26edcff
22092 changed files with 2101903 additions and 0 deletions

View file

@ -0,0 +1,223 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
abstract class BaseReader implements IReader
{
/**
* Read data only?
* Identifies whether the Reader should only read data values for cells, and ignore any formatting information;
* or whether it should read both data and formatting.
*/
protected bool $readDataOnly = false;
/**
* Read empty cells?
* Identifies whether the Reader should read data values for cells all cells, or should ignore cells containing
* null value or empty string.
*/
protected bool $readEmptyCells = true;
/**
* Read charts that are defined in the workbook?
* Identifies whether the Reader should read the definitions for any charts that exist in the workbook;.
*/
protected bool $includeCharts = false;
/**
* Restrict which sheets should be loaded?
* This property holds an array of worksheet names to be loaded. If null, then all worksheets will be loaded.
* This property is ignored for Csv, Html, and Slk.
*
* @var null|string[]
*/
protected ?array $loadSheetsOnly = null;
/**
* IReadFilter instance.
*/
protected IReadFilter $readFilter;
/** @var resource */
protected $fileHandle;
protected ?XmlScanner $securityScanner = null;
public function __construct()
{
$this->readFilter = new DefaultReadFilter();
}
public function getReadDataOnly(): bool
{
return $this->readDataOnly;
}
public function setReadDataOnly(bool $readCellValuesOnly): self
{
$this->readDataOnly = $readCellValuesOnly;
return $this;
}
public function getReadEmptyCells(): bool
{
return $this->readEmptyCells;
}
public function setReadEmptyCells(bool $readEmptyCells): self
{
$this->readEmptyCells = $readEmptyCells;
return $this;
}
public function getIncludeCharts(): bool
{
return $this->includeCharts;
}
public function setIncludeCharts(bool $includeCharts): self
{
$this->includeCharts = $includeCharts;
return $this;
}
public function getLoadSheetsOnly(): ?array
{
return $this->loadSheetsOnly;
}
public function setLoadSheetsOnly(string|array|null $sheetList): self
{
if ($sheetList === null) {
return $this->setLoadAllSheets();
}
$this->loadSheetsOnly = is_array($sheetList) ? $sheetList : [$sheetList];
return $this;
}
public function setLoadAllSheets(): self
{
$this->loadSheetsOnly = null;
return $this;
}
public function getReadFilter(): IReadFilter
{
return $this->readFilter;
}
public function setReadFilter(IReadFilter $readFilter): self
{
$this->readFilter = $readFilter;
return $this;
}
public function getSecurityScanner(): ?XmlScanner
{
return $this->securityScanner;
}
public function getSecurityScannerOrThrow(): XmlScanner
{
if ($this->securityScanner === null) {
throw new ReaderException('Security scanner is unexpectedly null');
}
return $this->securityScanner;
}
protected function processFlags(int $flags): void
{
if (((bool) ($flags & self::LOAD_WITH_CHARTS)) === true) {
$this->setIncludeCharts(true);
}
if (((bool) ($flags & self::READ_DATA_ONLY)) === true) {
$this->setReadDataOnly(true);
}
if (((bool) ($flags & self::SKIP_EMPTY_CELLS) || (bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) {
$this->setReadEmptyCells(false);
}
}
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
throw new PhpSpreadsheetException('Reader classes must implement their own loadSpreadsheetFromFile() method');
}
/**
* Loads Spreadsheet from file.
*
* @param int $flags the optional second parameter flags may be used to identify specific elements
* that should be loaded, but which won't be loaded by default, using these values:
* IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file
*/
public function load(string $filename, int $flags = 0): Spreadsheet
{
$this->processFlags($flags);
try {
return $this->loadSpreadsheetFromFile($filename);
} catch (ReaderException $e) {
throw $e;
}
}
/**
* Open file for reading.
*/
protected function openFile(string $filename): void
{
$fileHandle = false;
if ($filename) {
File::assertFile($filename);
// Open file
$fileHandle = fopen($filename, 'rb');
}
if ($fileHandle === false) {
throw new ReaderException('Could not open file ' . $filename . ' for reading.');
}
$this->fileHandle = $fileHandle;
}
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*/
public function listWorksheetInfo(string $filename): array
{
throw new PhpSpreadsheetException('Reader classes must implement their own listWorksheetInfo() method');
}
/**
* Returns names of the worksheets from a file,
* possibly without parsing the whole file to a Spreadsheet object.
* Readers will often have a more efficient method with which
* they can override this method.
*/
public function listWorksheetNames(string $filename): array
{
$returnArray = [];
$info = $this->listWorksheetInfo($filename);
foreach ($info as $infoArray) {
if (isset($infoArray['worksheetName'])) {
$returnArray[] = $infoArray['worksheetName'];
}
}
return $returnArray;
}
}

View file

@ -0,0 +1,651 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Reader\Csv\Delimiter;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Csv extends BaseReader
{
const DEFAULT_FALLBACK_ENCODING = 'CP1252';
const GUESS_ENCODING = 'guess';
const UTF8_BOM = "\xEF\xBB\xBF";
const UTF8_BOM_LEN = 3;
const UTF16BE_BOM = "\xfe\xff";
const UTF16BE_BOM_LEN = 2;
const UTF16BE_LF = "\x00\x0a";
const UTF16LE_BOM = "\xff\xfe";
const UTF16LE_BOM_LEN = 2;
const UTF16LE_LF = "\x0a\x00";
const UTF32BE_BOM = "\x00\x00\xfe\xff";
const UTF32BE_BOM_LEN = 4;
const UTF32BE_LF = "\x00\x00\x00\x0a";
const UTF32LE_BOM = "\xff\xfe\x00\x00";
const UTF32LE_BOM_LEN = 4;
const UTF32LE_LF = "\x0a\x00\x00\x00";
/**
* Input encoding.
*/
private string $inputEncoding = 'UTF-8';
/**
* Fallback encoding if guess strikes out.
*/
private string $fallbackEncoding = self::DEFAULT_FALLBACK_ENCODING;
/**
* Delimiter.
*/
private ?string $delimiter = null;
/**
* Enclosure.
*/
private string $enclosure = '"';
/**
* Sheet index to read.
*/
private int $sheetIndex = 0;
/**
* Load rows contiguously.
*/
private bool $contiguous = false;
/**
* The character that can escape the enclosure.
*/
private string $escapeCharacter = '\\';
/**
* Callback for setting defaults in construction.
*
* @var ?callable
*/
private static $constructorCallback;
/**
* Attempt autodetect line endings (deprecated after PHP8.1)?
*/
private bool $testAutodetect = true;
protected bool $castFormattedNumberToNumeric = false;
protected bool $preserveNumericFormatting = false;
private bool $preserveNullString = false;
private bool $sheetNameIsFileName = false;
private string $getTrue = 'true';
private string $getFalse = 'false';
private string $thousandsSeparator = ',';
private string $decimalSeparator = '.';
/**
* Create a new CSV Reader instance.
*/
public function __construct()
{
parent::__construct();
$callback = self::$constructorCallback;
if ($callback !== null) {
$callback($this);
}
}
/**
* Set a callback to change the defaults.
*
* The callback must accept the Csv Reader object as the first parameter,
* and it should return void.
*/
public static function setConstructorCallback(?callable $callback): void
{
self::$constructorCallback = $callback;
}
public static function getConstructorCallback(): ?callable
{
return self::$constructorCallback;
}
public function setInputEncoding(string $encoding): self
{
$this->inputEncoding = $encoding;
return $this;
}
public function getInputEncoding(): string
{
return $this->inputEncoding;
}
public function setFallbackEncoding(string $fallbackEncoding): self
{
$this->fallbackEncoding = $fallbackEncoding;
return $this;
}
public function getFallbackEncoding(): string
{
return $this->fallbackEncoding;
}
/**
* Move filepointer past any BOM marker.
*/
protected function skipBOM(): void
{
rewind($this->fileHandle);
if (fgets($this->fileHandle, self::UTF8_BOM_LEN + 1) !== self::UTF8_BOM) {
rewind($this->fileHandle);
}
}
/**
* Identify any separator that is explicitly set in the file.
*/
protected function checkSeparator(): void
{
$line = fgets($this->fileHandle);
if ($line === false) {
return;
}
if ((strlen(trim($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;
$delimiter = $this->delimiter ?? '';
// Loop through each line of the file in turn
$rowData = fgetcsv($fileHandle, 0, $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, $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);
if ($this->sheetNameIsFileName) {
$sheet->setTitle(substr(basename($filename, '.csv'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH));
}
// 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
$delimiter = $this->delimiter ?? '';
$rowData = fgetcsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
$valueBinder = Cell::getValueBinder();
$preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion();
$this->getTrue = Calculation::getTRUE();
$this->getFalse = Calculation::getFALSE();
$this->thousandsSeparator = StringHelper::getThousandsSeparator();
$this->decimalSeparator = StringHelper::getDecimalSeparator();
while (is_array($rowData)) {
$noOutputYet = true;
$columnLetter = 'A';
foreach ($rowData as $rowDatum) {
if ($preserveBooleanString) {
$rowDatum = $rowDatum ?? '';
} else {
$this->convertBoolean($rowDatum);
}
$numberFormatMask = $this->castFormattedNumberToNumeric ? $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)
if ($numberFormatMask !== '') {
$sheet->getStyle($columnLetter . $outRow)
->getNumberFormat()
->setFormatCode($numberFormatMask);
}
// Set cell value
$sheet->getCell($columnLetter . $outRow)->setValue($rowDatum);
}
++$columnLetter;
}
$rowData = fgetcsv($fileHandle, 0, $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.
*/
private function convertBoolean(mixed &$rowDatum): void
{
if (is_string($rowDatum)) {
if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) {
$rowDatum = true;
} elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) {
$rowDatum = false;
}
} else {
$rowDatum = $rowDatum ?? '';
}
}
/**
* Convert numeric strings to int or float values.
*/
private function convertFormattedNumber(mixed &$rowDatum): string
{
$numberFormatMask = '';
if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) {
$numeric = str_replace(
[$this->thousandsSeparator, $this->decimalSeparator],
['', '.'],
$rowDatum
);
if (is_numeric($numeric)) {
$decimalPos = strpos($rowDatum, $this->decimalSeparator);
if ($this->preserveNumericFormatting === true) {
$numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator))
? '#,##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) {
return false;
}
fclose($this->fileHandle);
// Trust file extension if any
$extension = strtolower(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 (str_starts_with($first4, $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;
}
public function setSheetNameIsFileName(bool $sheetNameIsFileName): self
{
$this->sheetNameIsFileName = $sheetNameIsFileName;
return $this;
}
}

View file

@ -0,0 +1,145 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Csv;
class Delimiter
{
protected const POTENTIAL_DELIMETERS = [',', ';', "\t", '|', ':', ' ', '~'];
/** @var resource */
protected $fileHandle;
protected string $escapeCharacter;
protected string $enclosure;
protected array $counts = [];
protected int $numberLines = 0;
/** @var ?string */
protected ?string $delimiter = null;
/**
* @param resource $fileHandle
*/
public function __construct($fileHandle, string $escapeCharacter, string $enclosure)
{
$this->fileHandle = $fileHandle;
$this->escapeCharacter = $escapeCharacter;
$this->enclosure = $enclosure;
$this->countPotentialDelimiters();
}
public function getDefaultDelimiter(): string
{
return self::POTENTIAL_DELIMETERS[0];
}
public function linesCounted(): int
{
return $this->numberLines;
}
protected function countPotentialDelimiters(): void
{
$this->counts = array_fill_keys(self::POTENTIAL_DELIMETERS, []);
$delimiterKeys = array_flip(self::POTENTIAL_DELIMETERS);
// Count how many times each of the potential delimiters appears in each line
$this->numberLines = 0;
while (($line = $this->getNextLine()) !== false && (++$this->numberLines < 1000)) {
$this->countDelimiterValues($line, $delimiterKeys);
}
}
protected function countDelimiterValues(string $line, array $delimiterKeys): void
{
$splitString = str_split($line, 1);
if (is_array($splitString)) {
$distribution = array_count_values($splitString);
$countLine = array_intersect_key($distribution, $delimiterKeys);
foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
$this->counts[$delimiter][] = $countLine[$delimiter] ?? 0;
}
}
}
public function infer(): ?string
{
// Calculate the mean square deviations for each delimiter
// (ignoring delimiters that haven't been found consistently)
$meanSquareDeviations = [];
$middleIdx = floor(($this->numberLines - 1) / 2);
foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
$series = $this->counts[$delimiter];
sort($series);
$median = ($this->numberLines % 2)
? $series[$middleIdx]
: ($series[$middleIdx] + $series[$middleIdx + 1]) / 2;
if ($median === 0) {
continue;
}
$meanSquareDeviations[$delimiter] = array_reduce(
$series,
fn ($sum, $value): int|float => $sum + ($value - $median) ** 2
) / count($series);
}
// ... and pick the delimiter with the smallest mean square deviation
// (in case of ties, the order in potentialDelimiters is respected)
$min = INF;
foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
if (!isset($meanSquareDeviations[$delimiter])) {
continue;
}
if ($meanSquareDeviations[$delimiter] < $min) {
$min = $meanSquareDeviations[$delimiter];
$this->delimiter = $delimiter;
}
}
return $this->delimiter;
}
/**
* Get the next full line from the file.
*
* @return false|string
*/
public function getNextLine()
{
$line = '';
$enclosure = ($this->escapeCharacter === '' ? ''
: ('(?<!' . preg_quote($this->escapeCharacter, '/') . ')'))
. preg_quote($this->enclosure, '/');
do {
// Get the next line in the file
$newLine = fgets($this->fileHandle);
// Return false if there is no next line
if ($newLine === false) {
return false;
}
// Add the new line to the line passed in
$line = $line . $newLine;
// Drop everything that is enclosed to avoid counting false positives in enclosures
$line = (string) preg_replace('/(' . $enclosure . '.*' . $enclosure . ')/Us', '', $line);
// See if we have any enclosures left in the line
// if we still have an enclosure then we need to read the next line as well
} while (preg_match('/(' . $enclosure . ')/', $line) > 0);
return ($line !== '') ? $line : false;
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader;
class DefaultReadFilter implements IReadFilter
{
/**
* Should this cell be read?
*
* @param string $columnAddress Column address (as a string value like "A", or "IV")
* @param int $row Row number
* @param string $worksheetName Optional worksheet name
*/
public function readCell(string $columnAddress, int $row, string $worksheetName = ''): bool
{
return true;
}
}

View file

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

View file

@ -0,0 +1,585 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\DefinedName;
use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\PageSetup;
use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\Properties;
use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\Styles;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
use XMLReader;
class Gnumeric extends BaseReader
{
const NAMESPACE_GNM = 'http://www.gnumeric.org/v10.dtd'; // gmr in old sheets
const NAMESPACE_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
const NAMESPACE_OFFICE = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0';
const NAMESPACE_XLINK = 'http://www.w3.org/1999/xlink';
const NAMESPACE_DC = 'http://purl.org/dc/elements/1.1/';
const NAMESPACE_META = 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0';
const NAMESPACE_OOO = 'http://openoffice.org/2004/office';
/**
* Shared Expressions.
*/
private array $expressions = [];
/**
* Spreadsheet shared across all functions.
*/
private Spreadsheet $spreadsheet;
private ReferenceHelper $referenceHelper;
public static array $mappings = [
'dataType' => [
'10' => DataType::TYPE_NULL,
'20' => DataType::TYPE_BOOL,
'30' => DataType::TYPE_NUMERIC, // Integer doesn't exist in Excel
'40' => DataType::TYPE_NUMERIC, // Float
'50' => DataType::TYPE_ERROR,
'60' => DataType::TYPE_STRING,
//'70': // Cell Range
//'80': // Array
],
];
/**
* Create a new Gnumeric.
*/
public function __construct()
{
parent::__construct();
$this->referenceHelper = ReferenceHelper::getInstance();
$this->securityScanner = XmlScanner::getInstance($this);
}
/**
* Can the current IReader read the file?
*/
public function canRead(string $filename): bool
{
$data = null;
if (File::testFileNoThrow($filename)) {
$data = $this->gzfileGetContents($filename);
if (!str_contains($data, self::NAMESPACE_GNM)) {
$data = '';
}
}
return !empty($data);
}
private static function matchXml(XMLReader $xml, string $expectedLocalName): bool
{
return $xml->namespaceURI === self::NAMESPACE_GNM
&& $xml->localName === $expectedLocalName
&& $xml->nodeType === XMLReader::ELEMENT;
}
/**
* Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
*/
public function listWorksheetNames(string $filename): array
{
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an invalid Gnumeric file.');
}
$xml = new XMLReader();
$contents = $this->gzfileGetContents($filename);
$xml->xml($contents, null, Settings::getLibXmlLoaderOptions());
$xml->setParserProperty(2, true);
$worksheetNames = [];
while ($xml->read()) {
if (self::matchXml($xml, 'SheetName')) {
$xml->read(); // Move onto the value node
$worksheetNames[] = (string) $xml->value;
} elseif (self::matchXml($xml, 'Sheets')) {
// break out of the loop once we've got our sheet names rather than parse the entire file
break;
}
}
return $worksheetNames;
}
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*/
public function listWorksheetInfo(string $filename): array
{
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an invalid Gnumeric file.');
}
$xml = new XMLReader();
$contents = $this->gzfileGetContents($filename);
$xml->xml($contents, null, Settings::getLibXmlLoaderOptions());
$xml->setParserProperty(2, true);
$worksheetInfo = [];
while ($xml->read()) {
if (self::matchXml($xml, 'Sheet')) {
$tmpInfo = [
'worksheetName' => '',
'lastColumnLetter' => 'A',
'lastColumnIndex' => 0,
'totalRows' => 0,
'totalColumns' => 0,
];
while ($xml->read()) {
if (self::matchXml($xml, 'Name')) {
$xml->read(); // Move onto the value node
$tmpInfo['worksheetName'] = (string) $xml->value;
} elseif (self::matchXml($xml, 'MaxCol')) {
$xml->read(); // Move onto the value node
$tmpInfo['lastColumnIndex'] = (int) $xml->value;
$tmpInfo['totalColumns'] = (int) $xml->value + 1;
} elseif (self::matchXml($xml, 'MaxRow')) {
$xml->read(); // Move onto the value node
$tmpInfo['totalRows'] = (int) $xml->value + 1;
break;
}
}
$tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
$worksheetInfo[] = $tmpInfo;
}
}
return $worksheetInfo;
}
private function gzfileGetContents(string $filename): string
{
$data = '';
$contents = @file_get_contents($filename);
if ($contents !== false) {
if (str_starts_with($contents, "\x1f\x8b")) {
// Check if gzlib functions are available
if (function_exists('gzdecode')) {
$contents = @gzdecode($contents);
if ($contents !== false) {
$data = $contents;
}
}
} else {
$data = $contents;
}
}
if ($data !== '') {
$data = $this->getSecurityScannerOrThrow()->scan($data);
}
return $data;
}
public static function gnumericMappings(): array
{
return array_merge(self::$mappings, Styles::$mappings);
}
private function processComments(SimpleXMLElement $sheet): void
{
if ((!$this->readDataOnly) && (isset($sheet->Objects))) {
foreach ($sheet->Objects->children(self::NAMESPACE_GNM) as $key => $comment) {
$commentAttributes = $comment->attributes();
// Only comment objects are handled at the moment
if ($commentAttributes && $commentAttributes->Text) {
$this->spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound)
->setAuthor((string) $commentAttributes->Author)
->setText($this->parseRichText((string) $commentAttributes->Text));
}
}
}
}
private static function testSimpleXml(mixed $value): SimpleXMLElement
{
return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><root></root>');
}
/**
* Loads Spreadsheet from file.
*/
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet->removeSheetByIndex(0);
// Load into this instance
return $this->loadIntoExisting($filename, $spreadsheet);
}
/**
* Loads from file into Spreadsheet instance.
*/
public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
{
$this->spreadsheet = $spreadsheet;
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an invalid Gnumeric file.');
}
$gFileData = $this->gzfileGetContents($filename);
/** @var XmlScanner */
$securityScanner = $this->securityScanner;
$xml2 = simplexml_load_string($securityScanner->scan($gFileData), 'SimpleXMLElement', Settings::getLibXmlLoaderOptions());
$xml = self::testSimpleXml($xml2);
$gnmXML = $xml->children(self::NAMESPACE_GNM);
(new Properties($this->spreadsheet))->readProperties($xml, $gnmXML);
$worksheetID = 0;
foreach ($gnmXML->Sheets->Sheet as $sheetOrNull) {
$sheet = self::testSimpleXml($sheetOrNull);
$worksheetName = (string) $sheet->Name;
if (is_array($this->loadSheetsOnly) && !in_array($worksheetName, $this->loadSheetsOnly, true)) {
continue;
}
$maxRow = $maxCol = 0;
// Create new Worksheet
$this->spreadsheet->createSheet();
$this->spreadsheet->setActiveSheetIndex($worksheetID);
// 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->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
$visibility = $sheet->attributes()['Visibility'] ?? 'GNM_SHEET_VISIBILITY_VISIBLE';
if ((string) $visibility !== 'GNM_SHEET_VISIBILITY_VISIBLE') {
$this->spreadsheet->getActiveSheet()->setSheetState(Worksheet::SHEETSTATE_HIDDEN);
}
if (!$this->readDataOnly) {
(new PageSetup($this->spreadsheet))
->printInformation($sheet)
->sheetMargins($sheet);
}
foreach ($sheet->Cells->Cell as $cellOrNull) {
$cell = self::testSimpleXml($cellOrNull);
$cellAttributes = self::testSimpleXml($cell->attributes());
$row = (int) $cellAttributes->Row + 1;
$column = (int) $cellAttributes->Col;
$maxRow = max($maxRow, $row);
$maxCol = max($maxCol, $column);
$column = Coordinate::stringFromColumnIndex($column + 1);
// Read cell?
if ($this->getReadFilter() !== null) {
if (!$this->getReadFilter()->readCell($column, $row, $worksheetName)) {
continue;
}
}
$this->loadCell($cell, $worksheetName, $cellAttributes, $column, $row);
}
if ($sheet->Styles !== null) {
(new Styles($this->spreadsheet, $this->readDataOnly))->read($sheet, $maxRow, $maxCol);
}
$this->processComments($sheet);
$this->processColumnWidths($sheet, $maxCol);
$this->processRowHeights($sheet, $maxRow);
$this->processMergedCells($sheet);
$this->processAutofilter($sheet);
$this->setSelectedCells($sheet);
++$worksheetID;
}
$this->processDefinedNames($gnmXML);
$this->setSelectedSheet($gnmXML);
// Return
return $this->spreadsheet;
}
private function setSelectedSheet(SimpleXMLElement $gnmXML): void
{
if (isset($gnmXML->UIData)) {
$attributes = self::testSimpleXml($gnmXML->UIData->attributes());
$selectedSheet = (int) $attributes['SelectedTab'];
$this->spreadsheet->setActiveSheetIndex($selectedSheet);
}
}
private function setSelectedCells(?SimpleXMLElement $sheet): void
{
if ($sheet !== null && isset($sheet->Selections)) {
foreach ($sheet->Selections as $selection) {
$startCol = (int) ($selection->StartCol ?? 0);
$startRow = (int) ($selection->StartRow ?? 0) + 1;
$endCol = (int) ($selection->EndCol ?? $startCol);
$endRow = (int) ($selection->endRow ?? 0) + 1;
$startColumn = Coordinate::stringFromColumnIndex($startCol + 1);
$endColumn = Coordinate::stringFromColumnIndex($endCol + 1);
$startCell = "{$startColumn}{$startRow}";
$endCell = "{$endColumn}{$endRow}";
$selectedRange = $startCell . (($endCell !== $startCell) ? ':' . $endCell : '');
$this->spreadsheet->getActiveSheet()->setSelectedCell($selectedRange);
break;
}
}
}
private function processMergedCells(?SimpleXMLElement $sheet): void
{
// Handle Merged Cells in this worksheet
if ($sheet !== null && isset($sheet->MergedRegions)) {
foreach ($sheet->MergedRegions->Merge as $mergeCells) {
if (str_contains((string) $mergeCells, ':')) {
$this->spreadsheet->getActiveSheet()->mergeCells($mergeCells, Worksheet::MERGE_CELL_CONTENT_HIDE);
}
}
}
}
private function processAutofilter(?SimpleXMLElement $sheet): void
{
if ($sheet !== null && isset($sheet->Filters)) {
foreach ($sheet->Filters->Filter as $autofilter) {
if ($autofilter !== null) {
$attributes = $autofilter->attributes();
if (isset($attributes['Area'])) {
$this->spreadsheet->getActiveSheet()->setAutoFilter((string) $attributes['Area']);
}
}
}
}
}
private function setColumnWidth(int $whichColumn, float $defaultWidth): void
{
$columnDimension = $this->spreadsheet->getActiveSheet()
->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1));
if ($columnDimension !== null) {
$columnDimension->setWidth($defaultWidth);
}
}
private function setColumnInvisible(int $whichColumn): void
{
$columnDimension = $this->spreadsheet->getActiveSheet()
->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1));
if ($columnDimension !== null) {
$columnDimension->setVisible(false);
}
}
private function processColumnLoop(int $whichColumn, int $maxCol, ?SimpleXMLElement $columnOverride, float $defaultWidth): int
{
$columnOverride = self::testSimpleXml($columnOverride);
$columnAttributes = self::testSimpleXml($columnOverride->attributes());
$column = $columnAttributes['No'];
$columnWidth = ((float) $columnAttributes['Unit']) / 5.4;
$hidden = (isset($columnAttributes['Hidden'])) && ((string) $columnAttributes['Hidden'] == '1');
$columnCount = (int) ($columnAttributes['Count'] ?? 1);
while ($whichColumn < $column) {
$this->setColumnWidth($whichColumn, $defaultWidth);
++$whichColumn;
}
while (($whichColumn < ($column + $columnCount)) && ($whichColumn <= $maxCol)) {
$this->setColumnWidth($whichColumn, $columnWidth);
if ($hidden) {
$this->setColumnInvisible($whichColumn);
}
++$whichColumn;
}
return $whichColumn;
}
private function processColumnWidths(?SimpleXMLElement $sheet, int $maxCol): void
{
if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Cols))) {
// Column Widths
$defaultWidth = 0;
$columnAttributes = $sheet->Cols->attributes();
if ($columnAttributes !== null) {
$defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4;
}
$whichColumn = 0;
foreach ($sheet->Cols->ColInfo as $columnOverride) {
$whichColumn = $this->processColumnLoop($whichColumn, $maxCol, $columnOverride, $defaultWidth);
}
while ($whichColumn <= $maxCol) {
$this->setColumnWidth($whichColumn, $defaultWidth);
++$whichColumn;
}
}
}
private function setRowHeight(int $whichRow, float $defaultHeight): void
{
$rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow);
if ($rowDimension !== null) {
$rowDimension->setRowHeight($defaultHeight);
}
}
private function setRowInvisible(int $whichRow): void
{
$rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow);
if ($rowDimension !== null) {
$rowDimension->setVisible(false);
}
}
private function processRowLoop(int $whichRow, int $maxRow, ?SimpleXMLElement $rowOverride, float $defaultHeight): int
{
$rowOverride = self::testSimpleXml($rowOverride);
$rowAttributes = self::testSimpleXml($rowOverride->attributes());
$row = $rowAttributes['No'];
$rowHeight = (float) $rowAttributes['Unit'];
$hidden = (isset($rowAttributes['Hidden'])) && ((string) $rowAttributes['Hidden'] == '1');
$rowCount = (int) ($rowAttributes['Count'] ?? 1);
while ($whichRow < $row) {
++$whichRow;
$this->setRowHeight($whichRow, $defaultHeight);
}
while (($whichRow < ($row + $rowCount)) && ($whichRow < $maxRow)) {
++$whichRow;
$this->setRowHeight($whichRow, $rowHeight);
if ($hidden) {
$this->setRowInvisible($whichRow);
}
}
return $whichRow;
}
private function processRowHeights(?SimpleXMLElement $sheet, int $maxRow): void
{
if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Rows))) {
// Row Heights
$defaultHeight = 0;
$rowAttributes = $sheet->Rows->attributes();
if ($rowAttributes !== null) {
$defaultHeight = (float) $rowAttributes['DefaultSizePts'];
}
$whichRow = 0;
foreach ($sheet->Rows->RowInfo as $rowOverride) {
$whichRow = $this->processRowLoop($whichRow, $maxRow, $rowOverride, $defaultHeight);
}
// never executed, I can't figure out any circumstances
// under which it would be executed, and, even if
// such exist, I'm not convinced this is needed.
//while ($whichRow < $maxRow) {
// ++$whichRow;
// $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow)->setRowHeight($defaultHeight);
//}
}
}
private function processDefinedNames(?SimpleXMLElement $gnmXML): void
{
// Loop through definedNames (global named ranges)
if ($gnmXML !== null && isset($gnmXML->Names)) {
foreach ($gnmXML->Names->Name as $definedName) {
$name = (string) $definedName->name;
$value = (string) $definedName->value;
if (stripos($value, '#REF!') !== false || empty($value)) {
continue;
}
[$worksheetName] = Worksheet::extractSheetTitle($value, true);
$worksheetName = trim($worksheetName, "'");
$worksheet = $this->spreadsheet->getSheetByName($worksheetName);
// Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet
if ($worksheet !== null) {
$this->spreadsheet->addDefinedName(DefinedName::createInstance($name, $worksheet, $value));
}
}
}
}
private function parseRichText(string $is): RichText
{
$value = new RichText();
$value->createText($is);
return $value;
}
private function loadCell(
SimpleXMLElement $cell,
string $worksheetName,
SimpleXMLElement $cellAttributes,
string $column,
int $row
): void {
$ValueType = $cellAttributes->ValueType;
$ExprID = (string) $cellAttributes->ExprID;
$type = DataType::TYPE_FORMULA;
if ($ExprID > '') {
if (((string) $cell) > '') {
$this->expressions[$ExprID] = [
'column' => $cellAttributes->Col,
'row' => $cellAttributes->Row,
'formula' => (string) $cell,
];
} else {
$expression = $this->expressions[$ExprID];
$cell = $this->referenceHelper->updateFormulaReferences(
$expression['formula'],
'A1',
$cellAttributes->Col - $expression['column'],
$cellAttributes->Row - $expression['row'],
$worksheetName
);
}
$type = DataType::TYPE_FORMULA;
} else {
$vtype = (string) $ValueType;
if (array_key_exists($vtype, self::$mappings['dataType'])) {
$type = self::$mappings['dataType'][$vtype];
}
if ($vtype === '20') { // Boolean
$cell = $cell == 'TRUE';
}
}
$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type);
if (isset($cellAttributes->ValueFormat)) {
$this->spreadsheet->getActiveSheet()->getCell($column . $row)
->getStyle()->getNumberFormat()
->setFormatCode((string) $cellAttributes->ValueFormat);
}
}
}

View file

@ -0,0 +1,147 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric;
use PhpOffice\PhpSpreadsheet\Reader\Gnumeric;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\PageMargins;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup as WorksheetPageSetup;
use SimpleXMLElement;
class PageSetup
{
private Spreadsheet $spreadsheet;
public function __construct(Spreadsheet $spreadsheet)
{
$this->spreadsheet = $spreadsheet;
}
public function printInformation(SimpleXMLElement $sheet): self
{
if (isset($sheet->PrintInformation, $sheet->PrintInformation[0])) {
$printInformation = $sheet->PrintInformation[0];
$setup = $this->spreadsheet->getActiveSheet()->getPageSetup();
$attributes = $printInformation->Scale->attributes();
if (isset($attributes['percentage'])) {
$setup->setScale((int) $attributes['percentage']);
}
$pageOrder = (string) $printInformation->order;
if ($pageOrder === 'r_then_d') {
$setup->setPageOrder(WorksheetPageSetup::PAGEORDER_OVER_THEN_DOWN);
} elseif ($pageOrder === 'd_then_r') {
$setup->setPageOrder(WorksheetPageSetup::PAGEORDER_DOWN_THEN_OVER);
}
$orientation = (string) $printInformation->orientation;
if ($orientation !== '') {
$setup->setOrientation($orientation);
}
$attributes = $printInformation->hcenter->attributes();
if (isset($attributes['value'])) {
$setup->setHorizontalCentered((bool) (string) $attributes['value']);
}
$attributes = $printInformation->vcenter->attributes();
if (isset($attributes['value'])) {
$setup->setVerticalCentered((bool) (string) $attributes['value']);
}
}
return $this;
}
public function sheetMargins(SimpleXMLElement $sheet): self
{
if (isset($sheet->PrintInformation, $sheet->PrintInformation->Margins)) {
$marginSet = [
// Default Settings
'top' => 0.75,
'header' => 0.3,
'left' => 0.7,
'right' => 0.7,
'bottom' => 0.75,
'footer' => 0.3,
];
$marginSet = $this->buildMarginSet($sheet, $marginSet);
$this->adjustMargins($marginSet);
}
return $this;
}
private function buildMarginSet(SimpleXMLElement $sheet, array $marginSet): array
{
foreach ($sheet->PrintInformation->Margins->children(Gnumeric::NAMESPACE_GNM) as $key => $margin) {
$marginAttributes = $margin->attributes();
$marginSize = ($marginAttributes['Points']) ?? 72; // Default is 72pt
// Convert value in points to inches
$marginSize = PageMargins::fromPoints((float) $marginSize);
$marginSet[$key] = $marginSize;
}
return $marginSet;
}
private function adjustMargins(array $marginSet): void
{
foreach ($marginSet as $key => $marginSize) {
// Gnumeric is quirky in the way it displays the header/footer values:
// header is actually the sum of top and header; footer is actually the sum of bottom and footer
// then top is actually the header value, and bottom is actually the footer value
switch ($key) {
case 'left':
case 'right':
$this->sheetMargin($key, $marginSize);
break;
case 'top':
$this->sheetMargin($key, $marginSet['header'] ?? 0);
break;
case 'bottom':
$this->sheetMargin($key, $marginSet['footer'] ?? 0);
break;
case 'header':
$this->sheetMargin($key, ($marginSet['top'] ?? 0) - $marginSize);
break;
case 'footer':
$this->sheetMargin($key, ($marginSet['bottom'] ?? 0) - $marginSize);
break;
}
}
}
private function sheetMargin(string $key, float $marginSize): void
{
switch ($key) {
case 'top':
$this->spreadsheet->getActiveSheet()->getPageMargins()->setTop($marginSize);
break;
case 'bottom':
$this->spreadsheet->getActiveSheet()->getPageMargins()->setBottom($marginSize);
break;
case 'left':
$this->spreadsheet->getActiveSheet()->getPageMargins()->setLeft($marginSize);
break;
case 'right':
$this->spreadsheet->getActiveSheet()->getPageMargins()->setRight($marginSize);
break;
case 'header':
$this->spreadsheet->getActiveSheet()->getPageMargins()->setHeader($marginSize);
break;
case 'footer':
$this->spreadsheet->getActiveSheet()->getPageMargins()->setFooter($marginSize);
break;
}
}
}

View file

@ -0,0 +1,161 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric;
use PhpOffice\PhpSpreadsheet\Reader\Gnumeric;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use SimpleXMLElement;
class Properties
{
protected Spreadsheet $spreadsheet;
public function __construct(Spreadsheet $spreadsheet)
{
$this->spreadsheet = $spreadsheet;
}
private function docPropertiesOld(SimpleXMLElement $gnmXML): void
{
$docProps = $this->spreadsheet->getProperties();
foreach ($gnmXML->Summary->Item as $summaryItem) {
$propertyName = $summaryItem->name;
$propertyValue = $summaryItem->{'val-string'};
switch ($propertyName) {
case 'title':
$docProps->setTitle(trim($propertyValue));
break;
case 'comments':
$docProps->setDescription(trim($propertyValue));
break;
case 'keywords':
$docProps->setKeywords(trim($propertyValue));
break;
case 'category':
$docProps->setCategory(trim($propertyValue));
break;
case 'manager':
$docProps->setManager(trim($propertyValue));
break;
case 'author':
$docProps->setCreator(trim($propertyValue));
$docProps->setLastModifiedBy(trim($propertyValue));
break;
case 'company':
$docProps->setCompany(trim($propertyValue));
break;
}
}
}
private function docPropertiesDC(SimpleXMLElement $officePropertyDC): void
{
$docProps = $this->spreadsheet->getProperties();
foreach ($officePropertyDC as $propertyName => $propertyValue) {
$propertyValue = trim((string) $propertyValue);
switch ($propertyName) {
case 'title':
$docProps->setTitle($propertyValue);
break;
case 'subject':
$docProps->setSubject($propertyValue);
break;
case 'creator':
$docProps->setCreator($propertyValue);
$docProps->setLastModifiedBy($propertyValue);
break;
case 'date':
$creationDate = $propertyValue;
$docProps->setModified($creationDate);
break;
case 'description':
$docProps->setDescription($propertyValue);
break;
}
}
}
private function docPropertiesMeta(SimpleXMLElement $officePropertyMeta): void
{
$docProps = $this->spreadsheet->getProperties();
foreach ($officePropertyMeta as $propertyName => $propertyValue) {
if ($propertyValue !== null) {
$attributes = $propertyValue->attributes(Gnumeric::NAMESPACE_META);
$propertyValue = trim((string) $propertyValue);
switch ($propertyName) {
case 'keyword':
$docProps->setKeywords($propertyValue);
break;
case 'initial-creator':
$docProps->setCreator($propertyValue);
$docProps->setLastModifiedBy($propertyValue);
break;
case 'creation-date':
$creationDate = $propertyValue;
$docProps->setCreated($creationDate);
break;
case 'user-defined':
if ($attributes) {
[, $attrName] = explode(':', (string) $attributes['name']);
$this->userDefinedProperties($attrName, $propertyValue);
}
break;
}
}
}
}
private function userDefinedProperties(string $attrName, string $propertyValue): void
{
$docProps = $this->spreadsheet->getProperties();
switch ($attrName) {
case 'publisher':
$docProps->setCompany($propertyValue);
break;
case 'category':
$docProps->setCategory($propertyValue);
break;
case 'manager':
$docProps->setManager($propertyValue);
break;
}
}
public function readProperties(SimpleXMLElement $xml, SimpleXMLElement $gnmXML): void
{
$officeXML = $xml->children(Gnumeric::NAMESPACE_OFFICE);
if (!empty($officeXML)) {
$officeDocXML = $officeXML->{'document-meta'};
$officeDocMetaXML = $officeDocXML->meta;
foreach ($officeDocMetaXML as $officePropertyData) {
$officePropertyDC = $officePropertyData->children(Gnumeric::NAMESPACE_DC);
$this->docPropertiesDC($officePropertyDC);
$officePropertyMeta = $officePropertyData->children(Gnumeric::NAMESPACE_META);
$this->docPropertiesMeta($officePropertyMeta);
}
} elseif (isset($gnmXML->Summary)) {
$this->docPropertiesOld($gnmXML);
}
}
}

View file

@ -0,0 +1,273 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Gnumeric;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Borders;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Font;
use SimpleXMLElement;
class Styles
{
private Spreadsheet $spreadsheet;
protected bool $readDataOnly;
public static array $mappings = [
'borderStyle' => [
'0' => Border::BORDER_NONE,
'1' => Border::BORDER_THIN,
'2' => Border::BORDER_MEDIUM,
'3' => Border::BORDER_SLANTDASHDOT,
'4' => Border::BORDER_DASHED,
'5' => Border::BORDER_THICK,
'6' => Border::BORDER_DOUBLE,
'7' => Border::BORDER_DOTTED,
'8' => Border::BORDER_MEDIUMDASHED,
'9' => Border::BORDER_DASHDOT,
'10' => Border::BORDER_MEDIUMDASHDOT,
'11' => Border::BORDER_DASHDOTDOT,
'12' => Border::BORDER_MEDIUMDASHDOTDOT,
'13' => Border::BORDER_MEDIUMDASHDOTDOT,
],
'fillType' => [
'1' => Fill::FILL_SOLID,
'2' => Fill::FILL_PATTERN_DARKGRAY,
'3' => Fill::FILL_PATTERN_MEDIUMGRAY,
'4' => Fill::FILL_PATTERN_LIGHTGRAY,
'5' => Fill::FILL_PATTERN_GRAY125,
'6' => Fill::FILL_PATTERN_GRAY0625,
'7' => Fill::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe
'8' => Fill::FILL_PATTERN_DARKVERTICAL, // vertical stripe
'9' => Fill::FILL_PATTERN_DARKDOWN, // diagonal stripe
'10' => Fill::FILL_PATTERN_DARKUP, // reverse diagonal stripe
'11' => Fill::FILL_PATTERN_DARKGRID, // diagoanl crosshatch
'12' => Fill::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch
'13' => Fill::FILL_PATTERN_LIGHTHORIZONTAL,
'14' => Fill::FILL_PATTERN_LIGHTVERTICAL,
'15' => Fill::FILL_PATTERN_LIGHTUP,
'16' => Fill::FILL_PATTERN_LIGHTDOWN,
'17' => Fill::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch
'18' => Fill::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch
],
'horizontal' => [
'1' => Alignment::HORIZONTAL_GENERAL,
'2' => Alignment::HORIZONTAL_LEFT,
'4' => Alignment::HORIZONTAL_RIGHT,
'8' => Alignment::HORIZONTAL_CENTER,
'16' => Alignment::HORIZONTAL_CENTER_CONTINUOUS,
'32' => Alignment::HORIZONTAL_JUSTIFY,
'64' => Alignment::HORIZONTAL_CENTER_CONTINUOUS,
],
'underline' => [
'1' => Font::UNDERLINE_SINGLE,
'2' => Font::UNDERLINE_DOUBLE,
'3' => Font::UNDERLINE_SINGLEACCOUNTING,
'4' => Font::UNDERLINE_DOUBLEACCOUNTING,
],
'vertical' => [
'1' => Alignment::VERTICAL_TOP,
'2' => Alignment::VERTICAL_BOTTOM,
'4' => Alignment::VERTICAL_CENTER,
'8' => Alignment::VERTICAL_JUSTIFY,
],
];
public function __construct(Spreadsheet $spreadsheet, bool $readDataOnly)
{
$this->spreadsheet = $spreadsheet;
$this->readDataOnly = $readDataOnly;
}
public function read(SimpleXMLElement $sheet, int $maxRow, int $maxCol): void
{
if ($sheet->Styles->StyleRegion !== null) {
$this->readStyles($sheet->Styles->StyleRegion, $maxRow, $maxCol);
}
}
private function readStyles(SimpleXMLElement $styleRegion, int $maxRow, int $maxCol): void
{
foreach ($styleRegion as $style) {
$styleAttributes = $style->attributes();
if ($styleAttributes !== null && ($styleAttributes['startRow'] <= $maxRow) && ($styleAttributes['startCol'] <= $maxCol)) {
$cellRange = $this->readStyleRange($styleAttributes, $maxCol, $maxRow);
$styleAttributes = $style->Style->attributes();
$styleArray = [];
// We still set the number format mask for date/time values, even if readDataOnly is true
// so that we can identify whether a float is a float or a date value
$formatCode = $styleAttributes ? (string) $styleAttributes['Format'] : null;
if ($formatCode && Date::isDateTimeFormatCode($formatCode)) {
$styleArray['numberFormat']['formatCode'] = $formatCode;
}
if ($this->readDataOnly === false && $styleAttributes !== null) {
// If readDataOnly is false, we set all formatting information
$styleArray['numberFormat']['formatCode'] = $formatCode;
$styleArray = $this->readStyle($styleArray, $styleAttributes, $style);
}
$this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray);
}
}
}
private function addBorderDiagonal(SimpleXMLElement $srssb, array &$styleArray): void
{
if (isset($srssb->Diagonal, $srssb->{'Rev-Diagonal'})) {
$styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes());
$styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH;
} elseif (isset($srssb->Diagonal)) {
$styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes());
$styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP;
} elseif (isset($srssb->{'Rev-Diagonal'})) {
$styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes());
$styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN;
}
}
private function addBorderStyle(SimpleXMLElement $srssb, array &$styleArray, string $direction): void
{
$ucDirection = ucfirst($direction);
if (isset($srssb->$ucDirection)) {
$styleArray['borders'][$direction] = self::parseBorderAttributes($srssb->$ucDirection->attributes());
}
}
private function calcRotation(SimpleXMLElement $styleAttributes): int
{
$rotation = (int) $styleAttributes->Rotation;
if ($rotation >= 270 && $rotation <= 360) {
$rotation -= 360;
}
$rotation = (abs($rotation) > 90) ? 0 : $rotation;
return $rotation;
}
private static function addStyle(array &$styleArray, string $key, string $value): void
{
if (array_key_exists($value, self::$mappings[$key])) {
$styleArray[$key] = self::$mappings[$key][$value];
}
}
private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void
{
if (array_key_exists($value, self::$mappings[$key])) {
$styleArray[$key1][$key] = self::$mappings[$key][$value];
}
}
private static function parseBorderAttributes(?SimpleXMLElement $borderAttributes): array
{
$styleArray = [];
if ($borderAttributes !== null) {
if (isset($borderAttributes['Color'])) {
$styleArray['color']['rgb'] = self::parseGnumericColour($borderAttributes['Color']);
}
self::addStyle($styleArray, 'borderStyle', (string) $borderAttributes['Style']);
}
return $styleArray;
}
private static function parseGnumericColour(string $gnmColour): string
{
[$gnmR, $gnmG, $gnmB] = explode(':', $gnmColour);
$gnmR = substr(str_pad($gnmR, 4, '0', STR_PAD_RIGHT), 0, 2);
$gnmG = substr(str_pad($gnmG, 4, '0', STR_PAD_RIGHT), 0, 2);
$gnmB = substr(str_pad($gnmB, 4, '0', STR_PAD_RIGHT), 0, 2);
return $gnmR . $gnmG . $gnmB;
}
private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes): void
{
$RGB = self::parseGnumericColour((string) $styleAttributes['Fore']);
$styleArray['font']['color']['rgb'] = $RGB;
$RGB = self::parseGnumericColour((string) $styleAttributes['Back']);
$shade = (string) $styleAttributes['Shade'];
if (($RGB !== '000000') || ($shade !== '0')) {
$RGB2 = self::parseGnumericColour((string) $styleAttributes['PatternColor']);
if ($shade === '1') {
$styleArray['fill']['startColor']['rgb'] = $RGB;
$styleArray['fill']['endColor']['rgb'] = $RGB2;
} else {
$styleArray['fill']['endColor']['rgb'] = $RGB;
$styleArray['fill']['startColor']['rgb'] = $RGB2;
}
self::addStyle2($styleArray, 'fill', 'fillType', $shade);
}
}
private function readStyleRange(SimpleXMLElement $styleAttributes, int $maxCol, int $maxRow): string
{
$startColumn = Coordinate::stringFromColumnIndex((int) $styleAttributes['startCol'] + 1);
$startRow = $styleAttributes['startRow'] + 1;
$endColumn = ($styleAttributes['endCol'] > $maxCol) ? $maxCol : (int) $styleAttributes['endCol'];
$endColumn = Coordinate::stringFromColumnIndex($endColumn + 1);
$endRow = 1 + (($styleAttributes['endRow'] > $maxRow) ? $maxRow : (int) $styleAttributes['endRow']);
$cellRange = $startColumn . $startRow . ':' . $endColumn . $endRow;
return $cellRange;
}
private function readStyle(array $styleArray, SimpleXMLElement $styleAttributes, SimpleXMLElement $style): array
{
self::addStyle2($styleArray, 'alignment', 'horizontal', (string) $styleAttributes['HAlign']);
self::addStyle2($styleArray, 'alignment', 'vertical', (string) $styleAttributes['VAlign']);
$styleArray['alignment']['wrapText'] = $styleAttributes['WrapText'] == '1';
$styleArray['alignment']['textRotation'] = $this->calcRotation($styleAttributes);
$styleArray['alignment']['shrinkToFit'] = $styleAttributes['ShrinkToFit'] == '1';
$styleArray['alignment']['indent'] = ((int) ($styleAttributes['Indent']) > 0) ? $styleAttributes['indent'] : 0;
$this->addColors($styleArray, $styleAttributes);
$fontAttributes = $style->Style->Font->attributes();
if ($fontAttributes !== null) {
$styleArray['font']['name'] = (string) $style->Style->Font;
$styleArray['font']['size'] = (int) ($fontAttributes['Unit']);
$styleArray['font']['bold'] = $fontAttributes['Bold'] == '1';
$styleArray['font']['italic'] = $fontAttributes['Italic'] == '1';
$styleArray['font']['strikethrough'] = $fontAttributes['StrikeThrough'] == '1';
self::addStyle2($styleArray, 'font', 'underline', (string) $fontAttributes['Underline']);
switch ($fontAttributes['Script']) {
case '1':
$styleArray['font']['superscript'] = true;
break;
case '-1':
$styleArray['font']['subscript'] = true;
break;
}
}
if (isset($style->Style->StyleBorder)) {
$srssb = $style->Style->StyleBorder;
$this->addBorderStyle($srssb, $styleArray, 'top');
$this->addBorderStyle($srssb, $styleArray, 'bottom');
$this->addBorderStyle($srssb, $styleArray, 'left');
$this->addBorderStyle($srssb, $styleArray, 'right');
$this->addBorderDiagonal($srssb, $styleArray);
}
// TO DO
/*
if (isset($style->Style->HyperLink)) {
$hyperlink = $style->Style->HyperLink->attributes();
}
*/
return $styleArray;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader;
interface IReadFilter
{
/**
* Should this cell be read?
*
* @param string $columnAddress Column address (as a string value like "A", or "IV")
* @param int $row Row number
* @param string $worksheetName Optional worksheet name
*/
public function readCell(string $columnAddress, int $row, string $worksheetName = ''): bool;
}

View file

@ -0,0 +1,124 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
interface IReader
{
public const LOAD_WITH_CHARTS = 1;
public const READ_DATA_ONLY = 2;
public const SKIP_EMPTY_CELLS = 4;
public const IGNORE_EMPTY_CELLS = 4;
public function __construct();
/**
* Can the current IReader read the file?
*/
public function canRead(string $filename): bool;
/**
* Read data only?
* If this is true, then the Reader will only read data values for cells, it will not read any formatting
* or structural information (like merges).
* If false (the default) it will read data and formatting.
*/
public function getReadDataOnly(): bool;
/**
* Set read data only
* Set to true, to advise the Reader only to read data values for cells, and to ignore any formatting
* or structural information (like merges).
* Set to false (the default) to advise the Reader to read both data and formatting for cells.
*
* @return $this
*/
public function setReadDataOnly(bool $readDataOnly): self;
/**
* Read empty cells?
* If this is true (the default), then the Reader will read data values for all cells, irrespective of value.
* If false it will not read data for cells containing a null value or an empty string.
*/
public function getReadEmptyCells(): bool;
/**
* Set read empty cells
* Set to true (the default) to advise the Reader read data values for all cells, irrespective of value.
* Set to false to advise the Reader to ignore cells containing a null value or an empty string.
*
* @return $this
*/
public function setReadEmptyCells(bool $readEmptyCells): self;
/**
* Read charts in workbook?
* If this is true, then the Reader will include any charts that exist in the workbook.
* Note that a ReadDataOnly value of false overrides, and charts won't be read regardless of the IncludeCharts value.
* If false (the default) it will ignore any charts defined in the workbook file.
*/
public function getIncludeCharts(): bool;
/**
* Set read charts in workbook
* Set to true, to advise the Reader to include any charts that exist in the workbook.
* Note that a ReadDataOnly value of false overrides, and charts won't be read regardless of the IncludeCharts value.
* Set to false (the default) to discard charts.
*
* @return $this
*/
public function setIncludeCharts(bool $includeCharts): self;
/**
* Get which sheets to load
* Returns either an array of worksheet names (the list of worksheets that should be loaded), or a null
* indicating that all worksheets in the workbook should be loaded.
*/
public function getLoadSheetsOnly(): ?array;
/**
* Set which sheets to load.
*
* @param null|array|string $value This should be either an array of worksheet names to be loaded,
* or a string containing a single worksheet name. If NULL, then it tells the Reader to
* read all worksheets in the workbook
*
* @return $this
*/
public function setLoadSheetsOnly(string|array|null $value): self;
/**
* Set all sheets to load
* Tells the Reader to load all worksheets from the workbook.
*
* @return $this
*/
public function setLoadAllSheets(): self;
/**
* Read filter.
*/
public function getReadFilter(): IReadFilter;
/**
* Set read filter.
*
* @return $this
*/
public function setReadFilter(IReadFilter $readFilter): self;
/**
* Loads PhpSpreadsheet from file.
*
* @param string $filename The name of the file to load
* @param int $flags Flags that can change the behaviour of the Writer:
* self::LOAD_WITH_CHARTS Load any charts that are defined (if the Reader supports Charts)
* self::READ_DATA_ONLY Read only data, not style or structure information, from the file
* self::SKIP_EMPTY_CELLS Don't read empty cells (cells that contain a null value,
* empty string, or a string containing only whitespace characters)
*/
public function load(string $filename, int $flags = 0): Spreadsheet;
}

View file

@ -0,0 +1,806 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader;
use DOMAttr;
use DOMDocument;
use DOMElement;
use DOMNode;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension;
use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter;
use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames;
use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator;
use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings;
use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Throwable;
use XMLReader;
use ZipArchive;
class Ods extends BaseReader
{
const INITIAL_FILE = 'content.xml';
/**
* Create a new Ods Reader instance.
*/
public function __construct()
{
parent::__construct();
$this->securityScanner = XmlScanner::getInstance($this);
}
/**
* Can the current IReader read the file?
*/
public function canRead(string $filename): bool
{
$mimeType = 'UNKNOWN';
// Load file
if (File::testFileNoThrow($filename, '')) {
$zip = new ZipArchive();
if ($zip->open($filename) === true) {
// check if it is an OOXML archive
$stat = $zip->statName('mimetype');
if (!empty($stat) && ($stat['size'] <= 255)) {
$mimeType = $zip->getFromName($stat['name']);
} elseif ($zip->statName('META-INF/manifest.xml')) {
$xml = simplexml_load_string(
$this->getSecurityScannerOrThrow()->scan($zip->getFromName('META-INF/manifest.xml')),
'SimpleXMLElement',
Settings::getLibXmlLoaderOptions()
);
if ($xml !== false) {
$namespacesContent = $xml->getNamespaces(true);
if (isset($namespacesContent['manifest'])) {
$manifest = $xml->children($namespacesContent['manifest']);
foreach ($manifest as $manifestDataSet) {
$manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']);
if ($manifestAttributes && $manifestAttributes->{'full-path'} == '/') {
$mimeType = (string) $manifestAttributes->{'media-type'};
break;
}
}
}
}
}
$zip->close();
}
}
return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet';
}
/**
* Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
*
* @return string[]
*/
public function listWorksheetNames(string $filename): array
{
File::assertFile($filename, self::INITIAL_FILE);
$worksheetNames = [];
$xml = new XMLReader();
$xml->xml(
$this->getSecurityScannerOrThrow()->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE),
null,
Settings::getLibXmlLoaderOptions()
);
$xml->setParserProperty(2, true);
// Step into the first level of content of the XML
$xml->read();
while ($xml->read()) {
// Quickly jump through to the office:body node
while (self::getXmlName($xml) !== 'office:body') {
if ($xml->isEmptyElement) {
$xml->read();
} else {
$xml->next();
}
}
// Now read each node until we find our first table:table node
while ($xml->read()) {
$xmlName = self::getXmlName($xml);
if ($xmlName == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
// Loop through each table:table node reading the table:name attribute for each worksheet name
do {
$worksheetName = $xml->getAttribute('table:name');
if (!empty($worksheetName)) {
$worksheetNames[] = $worksheetName;
}
$xml->next();
} while (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT);
}
}
}
return $worksheetNames;
}
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*/
public function listWorksheetInfo(string $filename): array
{
File::assertFile($filename, self::INITIAL_FILE);
$worksheetInfo = [];
$xml = new XMLReader();
$xml->xml(
$this->getSecurityScannerOrThrow()->scanFile('zip://' . realpath($filename) . '#' . self::INITIAL_FILE),
null,
Settings::getLibXmlLoaderOptions()
);
$xml->setParserProperty(2, true);
// Step into the first level of content of the XML
$xml->read();
while ($xml->read()) {
// Quickly jump through to the office:body node
while (self::getXmlName($xml) !== 'office:body') {
if ($xml->isEmptyElement) {
$xml->read();
} else {
$xml->next();
}
}
// Now read each node until we find our first table:table node
while ($xml->read()) {
if (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
$worksheetNames[] = $xml->getAttribute('table:name');
$tmpInfo = [
'worksheetName' => $xml->getAttribute('table:name'),
'lastColumnLetter' => 'A',
'lastColumnIndex' => 0,
'totalRows' => 0,
'totalColumns' => 0,
];
// Loop through each child node of the table:table element reading
$currCells = 0;
do {
$xml->read();
if (self::getXmlName($xml) == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
$rowspan = $xml->getAttribute('table:number-rows-repeated');
$rowspan = empty($rowspan) ? 1 : $rowspan;
$tmpInfo['totalRows'] += $rowspan;
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
$currCells = 0;
// Step into the row
$xml->read();
do {
$doread = true;
if (self::getXmlName($xml) == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
if (!$xml->isEmptyElement) {
++$currCells;
$xml->next();
$doread = false;
}
} elseif (self::getXmlName($xml) == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
$mergeSize = $xml->getAttribute('table:number-columns-repeated');
$currCells += (int) $mergeSize;
}
if ($doread) {
$xml->read();
}
} while (self::getXmlName($xml) != 'table:table-row');
}
} while (self::getXmlName($xml) != 'table:table');
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
$tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
$tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
$worksheetInfo[] = $tmpInfo;
}
}
}
return $worksheetInfo;
}
/**
* Counteract Phpstan caching.
*
* @phpstan-impure
*/
private static function getXmlName(XMLReader $xml): string
{
return $xml->name;
}
/**
* Loads PhpSpreadsheet from file.
*/
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet->removeSheetByIndex(0);
// Load into this instance
return $this->loadIntoExisting($filename, $spreadsheet);
}
/**
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
*/
public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
{
File::assertFile($filename, self::INITIAL_FILE);
$zip = new ZipArchive();
$zip->open($filename);
// Meta
$xml = @simplexml_load_string(
$this->getSecurityScannerOrThrow()->scan($zip->getFromName('meta.xml')),
'SimpleXMLElement',
Settings::getLibXmlLoaderOptions()
);
if ($xml === false) {
throw new Exception('Unable to read data from {$pFilename}');
}
$namespacesMeta = $xml->getNamespaces(true);
(new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta);
// Styles
$dom = new DOMDocument('1.01', 'UTF-8');
$dom->loadXML(
$this->getSecurityScannerOrThrow()->scan($zip->getFromName('styles.xml')),
Settings::getLibXmlLoaderOptions()
);
$pageSettings = new PageSettings($dom);
// Main Content
$dom = new DOMDocument('1.01', 'UTF-8');
$dom->loadXML(
$this->getSecurityScannerOrThrow()->scan($zip->getFromName(self::INITIAL_FILE)),
Settings::getLibXmlLoaderOptions()
);
$officeNs = (string) $dom->lookupNamespaceUri('office');
$tableNs = (string) $dom->lookupNamespaceUri('table');
$textNs = (string) $dom->lookupNamespaceUri('text');
$xlinkNs = (string) $dom->lookupNamespaceUri('xlink');
$styleNs = (string) $dom->lookupNamespaceUri('style');
$pageSettings->readStyleCrossReferences($dom);
$autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
$definedNameReader = new DefinedNames($spreadsheet, $tableNs);
$columnWidths = [];
$automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
$automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
foreach ($automaticStyles as $automaticStyle) {
$styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
$styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
if ($styleFamily === 'table-column') {
$tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties');
if ($tcprops !== null) {
$tcprop = $tcprops->item(0);
if ($tcprop !== null) {
$columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width');
$columnWidths[$styleName] = $columnWidth;
}
}
}
}
// Content
$item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0);
$spreadsheets = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($officeNs, 'spreadsheet');
foreach ($spreadsheets as $workbookData) {
/** @var DOMElement $workbookData */
$tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
$worksheetID = 0;
foreach ($tables as $worksheetDataSet) {
/** @var DOMElement $worksheetDataSet */
$worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
// Check loadSheetsOnly
if (
$this->loadSheetsOnly !== null
&& $worksheetName
&& !in_array($worksheetName, $this->loadSheetsOnly)
) {
continue;
}
$worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name');
// Create sheet
$spreadsheet->createSheet();
$spreadsheet->setActiveSheetIndex($worksheetID);
if ($worksheetName || is_numeric($worksheetName)) {
// 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
$spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false);
}
// Go through every child of table element
$rowID = 1;
$tableColumnIndex = 1;
foreach ($worksheetDataSet->childNodes as $childNode) {
/** @var DOMElement $childNode */
// Filter elements which are not under the "table" ns
if ($childNode->namespaceURI != $tableNs) {
continue;
}
$key = $childNode->nodeName;
// Remove ns from node name
if (str_contains($key, ':')) {
$keyChunks = explode(':', $key);
$key = array_pop($keyChunks);
}
switch ($key) {
case 'table-header-rows':
/// TODO :: Figure this out. This is only a partial implementation I guess.
// ($rowData it's not used at all and I'm not sure that PHPExcel
// has an API for this)
// foreach ($rowData as $keyRowData => $cellData) {
// $rowData = $cellData;
// break;
// }
break;
case 'table-column':
if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
} else {
$rowRepeats = 1;
}
$tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
if (isset($columnWidths[$tableStyleName])) {
$columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
$tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex);
for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) {
$spreadsheet->getActiveSheet()
->getColumnDimension($tableColumnString)
->setWidth($columnWidth->toUnit('cm'), 'cm');
++$tableColumnString;
}
}
$tableColumnIndex += $rowRepeats;
break;
case 'table-row':
if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
} else {
$rowRepeats = 1;
}
$columnID = 'A';
/** @var DOMElement $cellData */
foreach ($childNode->childNodes as $cellData) {
if ($this->getReadFilter() !== null) {
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
$colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
} else {
$colRepeats = 1;
}
for ($i = 0; $i < $colRepeats; ++$i) {
++$columnID;
}
continue;
}
}
// Initialize variables
$formatting = $hyperlink = null;
$hasCalculatedValue = false;
$cellDataFormula = '';
if ($cellData->hasAttributeNS($tableNs, 'formula')) {
$cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
$hasCalculatedValue = true;
}
// Annotations
$annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
if ($annotation->length > 0 && $annotation->item(0) !== null) {
$textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
if ($textNode->length > 0 && $textNode->item(0) !== null) {
$text = $this->scanElementForText($textNode->item(0));
$spreadsheet->getActiveSheet()
->getComment($columnID . $rowID)
->setText($this->parseRichText($text));
// ->setAuthor( $author )
}
}
// Content
/** @var DOMElement[] $paragraphs */
$paragraphs = [];
foreach ($cellData->childNodes as $item) {
/** @var DOMElement $item */
// Filter text:p elements
if ($item->nodeName == 'text:p') {
$paragraphs[] = $item;
}
}
if (count($paragraphs) > 0) {
// Consolidate if there are multiple p records (maybe with spans as well)
$dataArray = [];
// Text can have multiple text:p and within those, multiple text:span.
// text:p newlines, but text:span does not.
// Also, here we assume there is no text data is span fields are specified, since
// we have no way of knowing proper positioning anyway.
foreach ($paragraphs as $pData) {
$dataArray[] = $this->scanElementForText($pData);
}
$allCellDataText = implode("\n", $dataArray);
$type = $cellData->getAttributeNS($officeNs, 'value-type');
switch ($type) {
case 'string':
$type = DataType::TYPE_STRING;
$dataValue = $allCellDataText;
foreach ($paragraphs as $paragraph) {
$link = $paragraph->getElementsByTagNameNS($textNs, 'a');
if ($link->length > 0 && $link->item(0) !== null) {
$hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
}
}
break;
case 'boolean':
$type = DataType::TYPE_BOOL;
$dataValue = ($allCellDataText == 'TRUE') ? true : false;
break;
case 'percentage':
$type = DataType::TYPE_NUMERIC;
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
// percentage should always be float
//if (floor($dataValue) == $dataValue) {
// $dataValue = (int) $dataValue;
//}
$formatting = NumberFormat::FORMAT_PERCENTAGE_00;
break;
case 'currency':
$type = DataType::TYPE_NUMERIC;
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
if (floor($dataValue) == $dataValue) {
$dataValue = (int) $dataValue;
}
$formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
break;
case 'float':
$type = DataType::TYPE_NUMERIC;
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
if (floor($dataValue) == $dataValue) {
if ($dataValue == (int) $dataValue) {
$dataValue = (int) $dataValue;
}
}
break;
case 'date':
$type = DataType::TYPE_NUMERIC;
$value = $cellData->getAttributeNS($officeNs, 'date-value');
$dataValue = Date::convertIsoDate($value);
if ($dataValue != floor($dataValue)) {
$formatting = NumberFormat::FORMAT_DATE_XLSX15
. ' '
. NumberFormat::FORMAT_DATE_TIME4;
} else {
$formatting = NumberFormat::FORMAT_DATE_XLSX15;
}
break;
case 'time':
$type = DataType::TYPE_NUMERIC;
$timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
$dataValue = Date::PHPToExcel(
strtotime(
'01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? [])
)
);
$formatting = NumberFormat::FORMAT_DATE_TIME4;
break;
default:
$dataValue = null;
}
} else {
$type = DataType::TYPE_NULL;
$dataValue = null;
}
if ($hasCalculatedValue) {
$type = DataType::TYPE_FORMULA;
$cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
$cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula);
}
if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
$colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
} else {
$colRepeats = 1;
}
if ($type !== null) {
for ($i = 0; $i < $colRepeats; ++$i) {
if ($i > 0) {
++$columnID;
}
if ($type !== DataType::TYPE_NULL) {
for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
$rID = $rowID + $rowAdjust;
$cell = $spreadsheet->getActiveSheet()
->getCell($columnID . $rID);
// Set value
if ($hasCalculatedValue) {
$cell->setValueExplicit($cellDataFormula, $type);
} else {
$cell->setValueExplicit($dataValue, $type);
}
if ($hasCalculatedValue) {
$cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC);
}
// Set other properties
if ($formatting !== null) {
$spreadsheet->getActiveSheet()
->getStyle($columnID . $rID)
->getNumberFormat()
->setFormatCode($formatting);
} else {
$spreadsheet->getActiveSheet()
->getStyle($columnID . $rID)
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_GENERAL);
}
if ($hyperlink !== null) {
if ($hyperlink[0] === '#') {
$hyperlink = 'sheet://' . substr($hyperlink, 1);
}
$cell->getHyperlink()
->setUrl($hyperlink);
}
}
}
}
}
// Merged cells
$this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet);
++$columnID;
}
$rowID += $rowRepeats;
break;
}
}
$pageSettings->setVisibilityForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName);
$pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName);
++$worksheetID;
}
$autoFilterReader->read($workbookData);
$definedNameReader->read($workbookData);
}
$spreadsheet->setActiveSheetIndex(0);
if ($zip->locateName('settings.xml') !== false) {
$this->processSettings($zip, $spreadsheet);
}
// Return
return $spreadsheet;
}
private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void
{
$dom = new DOMDocument('1.01', 'UTF-8');
$dom->loadXML(
$this->getSecurityScannerOrThrow()->scan($zip->getFromName('settings.xml')),
Settings::getLibXmlLoaderOptions()
);
//$xlinkNs = $dom->lookupNamespaceUri('xlink');
$configNs = (string) $dom->lookupNamespaceUri('config');
//$oooNs = $dom->lookupNamespaceUri('ooo');
$officeNs = (string) $dom->lookupNamespaceUri('office');
$settings = $dom->getElementsByTagNameNS($officeNs, 'settings')
->item(0);
if ($settings !== null) {
$this->lookForActiveSheet($settings, $spreadsheet, $configNs);
$this->lookForSelectedCells($settings, $spreadsheet, $configNs);
}
}
private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
{
/** @var DOMElement $t */
foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) {
if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') {
try {
$spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? '');
} catch (Throwable) {
// do nothing
}
break;
}
}
}
private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
{
/** @var DOMElement $t */
foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) {
if ($t->getAttributeNs($configNs, 'name') === 'Tables') {
foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) {
$setRow = $setCol = '';
$wsname = $ws->getAttributeNs($configNs, 'name');
foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) {
$attrName = $configItem->getAttributeNs($configNs, 'name');
if ($attrName === 'CursorPositionX') {
$setCol = $configItem->nodeValue;
}
if ($attrName === 'CursorPositionY') {
$setRow = $configItem->nodeValue;
}
}
$this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow");
}
break;
}
}
}
private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void
{
if (is_numeric($setCol) && is_numeric($setRow)) {
$sheet = $spreadsheet->getSheetByName($wsname);
if ($sheet !== null) {
$sheet->setSelectedCells([(int) $setCol + 1, (int) $setRow + 1]);
}
}
}
/**
* Recursively scan element.
*/
protected function scanElementForText(DOMNode $element): string
{
$str = '';
foreach ($element->childNodes as $child) {
/** @var DOMNode $child */
if ($child->nodeType == XML_TEXT_NODE) {
$str .= $child->nodeValue;
} elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') {
// It's a space
// Multiple spaces?
$attributes = $child->attributes;
/** @var ?DOMAttr $cAttr */
$cAttr = ($attributes === null) ? null : $attributes->getNamedItem('c');
$multiplier = self::getMultiplier($cAttr);
$str .= str_repeat(' ', $multiplier);
}
if ($child->hasChildNodes()) {
$str .= $this->scanElementForText($child);
}
}
return $str;
}
private static function getMultiplier(?DOMAttr $cAttr): int
{
if ($cAttr) {
$multiplier = (int) $cAttr->nodeValue;
} else {
$multiplier = 1;
}
return $multiplier;
}
private function parseRichText(string $is): RichText
{
$value = new RichText();
$value->createText($is);
return $value;
}
private function processMergedCells(
DOMElement $cellData,
string $tableNs,
string $type,
string $columnID,
int $rowID,
Spreadsheet $spreadsheet
): void {
if (
$cellData->hasAttributeNS($tableNs, 'number-columns-spanned')
|| $cellData->hasAttributeNS($tableNs, 'number-rows-spanned')
) {
if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) {
$columnTo = $columnID;
if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) {
$columnIndex = Coordinate::columnIndexFromString($columnID);
$columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
$columnIndex -= 2;
$columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1);
}
$rowTo = $rowID;
if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) {
$rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1;
}
$cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo;
$spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE);
}
}
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use DOMElement;
use DOMNode;
class AutoFilter extends BaseLoader
{
public function read(DOMElement $workbookData): void
{
$this->readAutoFilters($workbookData);
}
protected function readAutoFilters(DOMElement $workbookData): void
{
$databases = $workbookData->getElementsByTagNameNS($this->tableNs, 'database-ranges');
foreach ($databases as $autofilters) {
foreach ($autofilters->childNodes as $autofilter) {
$autofilterRange = $this->getAttributeValue($autofilter, 'target-range-address');
if ($autofilterRange !== null) {
$baseAddress = FormulaTranslator::convertToExcelAddressValue($autofilterRange);
$this->spreadsheet->getActiveSheet()->setAutoFilter($baseAddress);
}
}
}
}
protected function getAttributeValue(?DOMNode $node, string $attributeName): ?string
{
if ($node !== null && $node->attributes !== null) {
$attribute = $node->attributes->getNamedItemNS(
$this->tableNs,
$attributeName
);
if ($attribute !== null) {
return $attribute->nodeValue;
}
}
return null;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use DOMElement;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
abstract class BaseLoader
{
protected Spreadsheet $spreadsheet;
protected string $tableNs;
public function __construct(Spreadsheet $spreadsheet, string $tableNs)
{
$this->spreadsheet = $spreadsheet;
$this->tableNs = $tableNs;
}
abstract public function read(DOMElement $workbookData): void;
}

View file

@ -0,0 +1,70 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use DOMElement;
use PhpOffice\PhpSpreadsheet\DefinedName;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DefinedNames extends BaseLoader
{
public function read(DOMElement $workbookData): void
{
$this->readDefinedRanges($workbookData);
$this->readDefinedExpressions($workbookData);
}
/**
* Read any Named Ranges that are defined in this spreadsheet.
*/
protected function readDefinedRanges(DOMElement $workbookData): void
{
$namedRanges = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-range');
foreach ($namedRanges as $definedNameElement) {
$definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name');
$baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address');
$range = $definedNameElement->getAttributeNS($this->tableNs, 'cell-range-address');
/** @var non-empty-string $baseAddress */
$baseAddress = FormulaTranslator::convertToExcelAddressValue($baseAddress);
$range = FormulaTranslator::convertToExcelAddressValue($range);
$this->addDefinedName($baseAddress, $definedName, $range);
}
}
/**
* Read any Named Formulae that are defined in this spreadsheet.
*/
protected function readDefinedExpressions(DOMElement $workbookData): void
{
$namedExpressions = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-expression');
foreach ($namedExpressions as $definedNameElement) {
$definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name');
$baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address');
$expression = $definedNameElement->getAttributeNS($this->tableNs, 'expression');
/** @var non-empty-string $baseAddress */
$baseAddress = FormulaTranslator::convertToExcelAddressValue($baseAddress);
$expression = substr($expression, strpos($expression, ':=') + 1);
$expression = FormulaTranslator::convertToExcelFormulaValue($expression);
$this->addDefinedName($baseAddress, $definedName, $expression);
}
}
/**
* Assess scope and store the Defined Name.
*
* @param non-empty-string $baseAddress
*/
private function addDefinedName(string $baseAddress, string $definedName, string $value): void
{
[$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true);
$worksheet = $this->spreadsheet->getSheetByName($sheetReference);
// Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet
if ($worksheet !== null) {
$this->spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value));
}
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
class FormulaTranslator
{
public static function convertToExcelAddressValue(string $openOfficeAddress): string
{
$excelAddress = $openOfficeAddress;
// Cell range 3-d reference
// As we don't support 3-d ranges, we're just going to take a quick and dirty approach
// and assume that the second worksheet reference is the same as the first
$excelAddress = (string) preg_replace(
[
'/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu',
'/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', // Cell range reference in another sheet
'/\$?([^\.]+)\.([^\.]+)/miu', // Cell reference in another sheet
'/\.([^\.]+):\.([^\.]+)/miu', // Cell range reference
'/\.([^\.]+)/miu', // Simple cell reference
],
[
'$1!$2:$4',
'$1!$2:$3',
'$1!$2',
'$1:$2',
'$1',
],
$excelAddress
);
return $excelAddress;
}
public static function convertToExcelFormulaValue(string $openOfficeFormula): string
{
$temp = explode(Calculation::FORMULA_STRING_QUOTE, $openOfficeFormula);
$tKey = false;
$inMatrixBracesLevel = 0;
$inFunctionBracesLevel = 0;
foreach ($temp as &$value) {
// @var string $value
// Only replace in alternate array entries (i.e. non-quoted blocks)
// so that conversion isn't done in string values
$tKey = $tKey === false;
if ($tKey) {
$value = (string) preg_replace(
[
'/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference in another sheet
'/\[\$?([^\.]+)\.([^\.]+)\]/miu', // Cell reference in another sheet
'/\[\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference
'/\[\.([^\.]+)\]/miu', // Simple cell reference
],
[
'$1!$2:$3',
'$1!$2',
'$1:$2',
'$1',
],
$value
);
// Convert references to defined names/formulae
$value = str_replace('$$', '', $value);
// Convert ODS function argument separators to Excel function argument separators
$value = Calculation::translateSeparator(';', ',', $value, $inFunctionBracesLevel);
// Convert ODS matrix separators to Excel matrix separators
$value = Calculation::translateSeparator(
';',
',',
$value,
$inMatrixBracesLevel,
Calculation::FORMULA_OPEN_MATRIX_BRACE,
Calculation::FORMULA_CLOSE_MATRIX_BRACE
);
$value = Calculation::translateSeparator(
'|',
';',
$value,
$inMatrixBracesLevel,
Calculation::FORMULA_OPEN_MATRIX_BRACE,
Calculation::FORMULA_CLOSE_MATRIX_BRACE
);
$value = (string) preg_replace('/COM\.MICROSOFT\./ui', '', $value);
}
}
// Then rebuild the formula string
$excelFormula = implode('"', $temp);
return $excelFormula;
}
}

View file

@ -0,0 +1,171 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use DOMDocument;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class PageSettings
{
private string $officeNs = '';
private string $stylesNs = '';
private string $stylesFo = '';
private string $tableNs = '';
/**
* @var string[]
*/
private array $tableStylesCrossReference = [];
private array $pageLayoutStyles = [];
/**
* @var string[]
*/
private array $masterStylesCrossReference = [];
/**
* @var string[]
*/
private array $masterPrintStylesCrossReference = [];
public function __construct(DOMDocument $styleDom)
{
$this->setDomNameSpaces($styleDom);
$this->readPageSettingStyles($styleDom);
$this->readStyleMasterLookup($styleDom);
}
private function setDomNameSpaces(DOMDocument $styleDom): void
{
$this->officeNs = (string) $styleDom->lookupNamespaceUri('office');
$this->stylesNs = (string) $styleDom->lookupNamespaceUri('style');
$this->stylesFo = (string) $styleDom->lookupNamespaceUri('fo');
$this->tableNs = (string) $styleDom->lookupNamespaceUri('table');
}
private function readPageSettingStyles(DOMDocument $styleDom): void
{
$item0 = $styleDom->getElementsByTagNameNS($this->officeNs, 'automatic-styles')->item(0);
$styles = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($this->stylesNs, 'page-layout');
foreach ($styles as $styleSet) {
$styleName = $styleSet->getAttributeNS($this->stylesNs, 'name');
$pageLayoutProperties = $styleSet->getElementsByTagNameNS($this->stylesNs, 'page-layout-properties')[0];
$styleOrientation = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-orientation');
$styleScale = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'scale-to');
$stylePrintOrder = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'print-page-order');
$centered = $pageLayoutProperties->getAttributeNS($this->stylesNs, 'table-centering');
$marginLeft = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-left');
$marginRight = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-right');
$marginTop = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-top');
$marginBottom = $pageLayoutProperties->getAttributeNS($this->stylesFo, 'margin-bottom');
$header = $styleSet->getElementsByTagNameNS($this->stylesNs, 'header-style')[0];
$headerProperties = $header->getElementsByTagNameNS($this->stylesNs, 'header-footer-properties')[0];
$marginHeader = isset($headerProperties) ? $headerProperties->getAttributeNS($this->stylesFo, 'min-height') : null;
$footer = $styleSet->getElementsByTagNameNS($this->stylesNs, 'footer-style')[0];
$footerProperties = $footer->getElementsByTagNameNS($this->stylesNs, 'header-footer-properties')[0];
$marginFooter = isset($footerProperties) ? $footerProperties->getAttributeNS($this->stylesFo, 'min-height') : null;
$this->pageLayoutStyles[$styleName] = (object) [
'orientation' => $styleOrientation ?: PageSetup::ORIENTATION_DEFAULT,
'scale' => $styleScale ?: 100,
'printOrder' => $stylePrintOrder,
'horizontalCentered' => $centered === 'horizontal' || $centered === 'both',
'verticalCentered' => $centered === 'vertical' || $centered === 'both',
// margin size is already stored in inches, so no UOM conversion is required
'marginLeft' => (float) ($marginLeft ?? 0.7),
'marginRight' => (float) ($marginRight ?? 0.7),
'marginTop' => (float) ($marginTop ?? 0.3),
'marginBottom' => (float) ($marginBottom ?? 0.3),
'marginHeader' => (float) ($marginHeader ?? 0.45),
'marginFooter' => (float) ($marginFooter ?? 0.45),
];
}
}
private function readStyleMasterLookup(DOMDocument $styleDom): void
{
$item0 = $styleDom->getElementsByTagNameNS($this->officeNs, 'master-styles')->item(0);
$styleMasterLookup = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($this->stylesNs, 'master-page');
foreach ($styleMasterLookup as $styleMasterSet) {
$styleMasterName = $styleMasterSet->getAttributeNS($this->stylesNs, 'name');
$pageLayoutName = $styleMasterSet->getAttributeNS($this->stylesNs, 'page-layout-name');
$this->masterPrintStylesCrossReference[$styleMasterName] = $pageLayoutName;
}
}
public function readStyleCrossReferences(DOMDocument $contentDom): void
{
$item0 = $contentDom->getElementsByTagNameNS($this->officeNs, 'automatic-styles')->item(0);
$styleXReferences = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($this->stylesNs, 'style');
foreach ($styleXReferences as $styleXreferenceSet) {
$styleXRefName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'name');
$stylePageLayoutName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'master-page-name');
$styleFamilyName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'family');
if (!empty($styleFamilyName) && $styleFamilyName === 'table') {
$styleVisibility = 'true';
foreach ($styleXreferenceSet->getElementsByTagNameNS($this->stylesNs, 'table-properties') as $tableProperties) {
$styleVisibility = $tableProperties->getAttributeNS($this->tableNs, 'display');
}
$this->tableStylesCrossReference[$styleXRefName] = $styleVisibility;
}
if (!empty($stylePageLayoutName)) {
$this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName;
}
}
}
public function setVisibilityForWorksheet(Worksheet $worksheet, string $styleName): void
{
if (!array_key_exists($styleName, $this->tableStylesCrossReference)) {
return;
}
$worksheet->setSheetState(
$this->tableStylesCrossReference[$styleName] === 'false'
? Worksheet::SHEETSTATE_HIDDEN
: Worksheet::SHEETSTATE_VISIBLE
);
}
public function setPrintSettingsForWorksheet(Worksheet $worksheet, string $styleName): void
{
if (!array_key_exists($styleName, $this->masterStylesCrossReference)) {
return;
}
$masterStyleName = $this->masterStylesCrossReference[$styleName];
if (!array_key_exists($masterStyleName, $this->masterPrintStylesCrossReference)) {
return;
}
$printSettingsIndex = $this->masterPrintStylesCrossReference[$masterStyleName];
if (!array_key_exists($printSettingsIndex, $this->pageLayoutStyles)) {
return;
}
$printSettings = $this->pageLayoutStyles[$printSettingsIndex];
$worksheet->getPageSetup()
->setOrientation($printSettings->orientation ?? PageSetup::ORIENTATION_DEFAULT)
->setPageOrder($printSettings->printOrder === 'ltr' ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER)
->setScale((int) trim($printSettings->scale, '%'))
->setHorizontalCentered($printSettings->horizontalCentered)
->setVerticalCentered($printSettings->verticalCentered);
$worksheet->getPageMargins()
->setLeft($printSettings->marginLeft)
->setRight($printSettings->marginRight)
->setTop($printSettings->marginTop)
->setBottom($printSettings->marginBottom)
->setHeader($printSettings->marginHeader)
->setFooter($printSettings->marginFooter);
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use PhpOffice\PhpSpreadsheet\Document\Properties as DocumentProperties;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use SimpleXMLElement;
class Properties
{
private Spreadsheet $spreadsheet;
public function __construct(Spreadsheet $spreadsheet)
{
$this->spreadsheet = $spreadsheet;
}
public function load(SimpleXMLElement $xml, array $namespacesMeta): void
{
$docProps = $this->spreadsheet->getProperties();
$officeProperty = $xml->children($namespacesMeta['office']);
foreach ($officeProperty as $officePropertyData) {
if (isset($namespacesMeta['dc'])) {
$officePropertiesDC = $officePropertyData->children($namespacesMeta['dc']);
$this->setCoreProperties($docProps, $officePropertiesDC);
}
$officePropertyMeta = null;
if (isset($namespacesMeta['dc'])) {
$officePropertyMeta = $officePropertyData->children($namespacesMeta['meta']);
}
$officePropertyMeta = $officePropertyMeta ?? [];
foreach ($officePropertyMeta as $propertyName => $propertyValue) {
$this->setMetaProperties($namespacesMeta, $propertyValue, $propertyName, $docProps);
}
}
}
private function setCoreProperties(DocumentProperties $docProps, SimpleXMLElement $officePropertyDC): void
{
foreach ($officePropertyDC as $propertyName => $propertyValue) {
$propertyValue = (string) $propertyValue;
switch ($propertyName) {
case 'title':
$docProps->setTitle($propertyValue);
break;
case 'subject':
$docProps->setSubject($propertyValue);
break;
case 'creator':
$docProps->setCreator($propertyValue);
$docProps->setLastModifiedBy($propertyValue);
break;
case 'date':
$docProps->setModified($propertyValue);
break;
case 'description':
$docProps->setDescription($propertyValue);
break;
}
}
}
private function setMetaProperties(
array $namespacesMeta,
SimpleXMLElement $propertyValue,
string $propertyName,
DocumentProperties $docProps
): void {
$propertyValueAttributes = $propertyValue->attributes($namespacesMeta['meta']);
$propertyValue = (string) $propertyValue;
switch ($propertyName) {
case 'initial-creator':
$docProps->setCreator($propertyValue);
break;
case 'keyword':
$docProps->setKeywords($propertyValue);
break;
case 'creation-date':
$docProps->setCreated($propertyValue);
break;
case 'user-defined':
$name2 = (string) ($propertyValueAttributes['name'] ?? '');
if ($name2 === 'Company') {
$docProps->setCompany($propertyValue);
} elseif ($name2 === 'category') {
$docProps->setCategory($propertyValue);
} else {
$this->setUserDefinedProperty($propertyValueAttributes, $propertyValue, $docProps);
}
break;
}
}
private function setUserDefinedProperty(iterable $propertyValueAttributes, string $propertyValue, DocumentProperties $docProps): void
{
$propertyValueName = '';
$propertyValueType = DocumentProperties::PROPERTY_TYPE_STRING;
foreach ($propertyValueAttributes as $key => $value) {
if ($key == 'name') {
$propertyValueName = (string) $value;
} elseif ($key == 'value-type') {
switch ($value) {
case 'date':
$propertyValue = DocumentProperties::convertProperty($propertyValue, 'date');
$propertyValueType = DocumentProperties::PROPERTY_TYPE_DATE;
break;
case 'boolean':
$propertyValue = DocumentProperties::convertProperty($propertyValue, 'bool');
$propertyValueType = DocumentProperties::PROPERTY_TYPE_BOOLEAN;
break;
case 'float':
$propertyValue = DocumentProperties::convertProperty($propertyValue, 'r4');
$propertyValueType = DocumentProperties::PROPERTY_TYPE_FLOAT;
break;
default:
$propertyValueType = DocumentProperties::PROPERTY_TYPE_STRING;
}
}
}
$docProps->setCustomProperty($propertyValueName, $propertyValue, $propertyValueType);
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Security;
use PhpOffice\PhpSpreadsheet\Reader;
class XmlScanner
{
private string $pattern;
/** @var ?callable */
private $callback;
public function __construct(string $pattern = '<!DOCTYPE')
{
$this->pattern = $pattern;
}
public static function getInstance(Reader\IReader $reader): self
{
$pattern = ($reader instanceof Reader\Html) ? '<!ENTITY' : '<!DOCTYPE';
return new self($pattern);
}
public function setAdditionalCallback(callable $callback): void
{
$this->callback = $callback;
}
private static function forceString(mixed $arg): string
{
return is_string($arg) ? $arg : '';
}
private function toUtf8(string $xml): string
{
$pattern = '/encoding="(.*?)"/';
$result = preg_match($pattern, $xml, $matches);
$charset = strtoupper($result ? $matches[1] : 'UTF-8');
if ($charset !== 'UTF-8') {
$xml = self::forceString(mb_convert_encoding($xml, 'UTF-8', $charset));
$result = preg_match($pattern, $xml, $matches);
$charset = strtoupper($result ? $matches[1] : 'UTF-8');
if ($charset !== 'UTF-8') {
throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
}
}
return $xml;
}
/**
* Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
*
* @param false|string $xml
*/
public function scan($xml): string
{
$xml = "$xml";
$xml = $this->toUtf8($xml);
// Don't rely purely on libxml_disable_entity_loader()
$pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/';
if (preg_match($pattern, $xml)) {
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
}
if ($this->callback !== null) {
$xml = call_user_func($this->callback, $xml);
}
return $xml;
}
/**
* Scan theXML for use of <!ENTITY to prevent XXE/XEE attacks.
*/
public function scanFile(string $filestream): string
{
return $this->scan(file_get_contents($filestream));
}
}

View file

@ -0,0 +1,564 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Slk extends BaseReader
{
/**
* Sheet index to read.
*/
private int $sheetIndex = 0;
/**
* Formats.
*/
private array $formats = [];
/**
* Format Count.
*/
private int $format = 0;
/**
* Fonts.
*/
private array $fonts = [];
/**
* Font Count.
*/
private int $fontcount = 0;
/**
* Create a new SYLK Reader instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Validate that the current file is a SYLK file.
*/
public function canRead(string $filename): bool
{
try {
$this->openFile($filename);
} catch (ReaderException) {
return false;
}
// Read sample data (first 2 KB will do)
$data = (string) fread($this->fileHandle, 2048);
// Count delimiters in file
$delimiterCount = substr_count($data, ';');
$hasDelimiter = $delimiterCount > 0;
// Analyze first line looking for ID; signature
$lines = explode("\n", $data);
$hasId = str_starts_with($lines[0], 'ID;P');
fclose($this->fileHandle);
return $hasDelimiter && $hasId;
}
private function canReadOrBust(string $filename): void
{
if (!$this->canRead($filename)) {
throw new ReaderException($filename . ' is an Invalid SYLK file.');
}
$this->openFile($filename);
}
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*/
public function listWorksheetInfo(string $filename): array
{
// Open file
$this->canReadOrBust($filename);
$fileHandle = $this->fileHandle;
rewind($fileHandle);
$worksheetInfo = [];
$worksheetInfo[0]['worksheetName'] = basename($filename, '.slk');
// loop through one row (line) at a time in the file
$rowIndex = 0;
$columnIndex = 0;
while (($rowData = fgets($fileHandle)) !== false) {
$columnIndex = 0;
// convert SYLK encoded $rowData to UTF-8
$rowData = StringHelper::SYLKtoUTF8($rowData);
// explode each row at semicolons while taking into account that literal semicolon (;)
// is escaped like this (;;)
$rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowData)))));
$dataType = array_shift($rowData);
if ($dataType == 'B') {
foreach ($rowData as $rowDatum) {
switch ($rowDatum[0]) {
case 'X':
$columnIndex = (int) substr($rowDatum, 1) - 1;
break;
case 'Y':
$rowIndex = (int) substr($rowDatum, 1);
break;
}
}
break;
}
}
$worksheetInfo[0]['lastColumnIndex'] = $columnIndex;
$worksheetInfo[0]['totalRows'] = $rowIndex;
$worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
$worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
// Close file
fclose($fileHandle);
return $worksheetInfo;
}
/**
* Loads PhpSpreadsheet from file.
*/
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
// Load into this instance
return $this->loadIntoExisting($filename, $spreadsheet);
}
private const COLOR_ARRAY = [
'FF00FFFF', // 0 - cyan
'FF000000', // 1 - black
'FFFFFFFF', // 2 - white
'FFFF0000', // 3 - red
'FF00FF00', // 4 - green
'FF0000FF', // 5 - blue
'FFFFFF00', // 6 - yellow
'FFFF00FF', // 7 - magenta
];
private const FONT_STYLE_MAPPINGS = [
'B' => 'bold',
'I' => 'italic',
'U' => 'underline',
];
private function processFormula(string $rowDatum, bool &$hasCalculatedValue, string &$cellDataFormula, string $row, string $column): void
{
$cellDataFormula = '=' . substr($rowDatum, 1);
// Convert R1C1 style references to A1 style references (but only when not quoted)
$temp = explode('"', $cellDataFormula);
$key = false;
foreach ($temp as &$value) {
// Only count/replace in alternate array entries
$key = $key === false;
if ($key) {
preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);
// Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way
// through the formula from left to right. Reversing means that we work right to left.through
// the formula
$cellReferences = array_reverse($cellReferences);
// Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent,
// then modify the formula to use that new reference
foreach ($cellReferences as $cellReference) {
$rowReference = $cellReference[2][0];
// Empty R reference is the current row
if ($rowReference == '') {
$rowReference = $row;
}
// Bracketed R references are relative to the current row
if ($rowReference[0] == '[') {
$rowReference = (int) $row + (int) trim($rowReference, '[]');
}
$columnReference = $cellReference[4][0];
// Empty C reference is the current column
if ($columnReference == '') {
$columnReference = $column;
}
// Bracketed C references are relative to the current column
if ($columnReference[0] == '[') {
$columnReference = (int) $column + (int) trim($columnReference, '[]');
}
$A1CellReference = Coordinate::stringFromColumnIndex((int) $columnReference) . $rowReference;
$value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0]));
}
}
}
unset($value);
// Then rebuild the formula string
$cellDataFormula = implode('"', $temp);
$hasCalculatedValue = true;
}
private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void
{
// Read cell value data
$hasCalculatedValue = false;
$tryNumeric = false;
$cellDataFormula = $cellData = '';
$sharedColumn = $sharedRow = -1;
$sharedFormula = false;
foreach ($rowData as $rowDatum) {
switch ($rowDatum[0]) {
case 'X':
$column = substr($rowDatum, 1);
break;
case 'Y':
$row = substr($rowDatum, 1);
break;
case 'K':
$cellData = substr($rowDatum, 1);
$tryNumeric = is_numeric($cellData);
break;
case 'E':
$this->processFormula($rowDatum, $hasCalculatedValue, $cellDataFormula, $row, $column);
break;
case 'A':
$comment = substr($rowDatum, 1);
$columnLetter = Coordinate::stringFromColumnIndex((int) $column);
$spreadsheet->getActiveSheet()
->getComment("$columnLetter$row")
->getText()
->createText($comment);
break;
case 'C':
$sharedColumn = (int) substr($rowDatum, 1);
break;
case 'R':
$sharedRow = (int) substr($rowDatum, 1);
break;
case 'S':
$sharedFormula = true;
break;
}
}
if ($sharedFormula === true && $sharedRow >= 0 && $sharedColumn >= 0) {
$thisCoordinate = Coordinate::stringFromColumnIndex((int) $column) . $row;
$sharedCoordinate = Coordinate::stringFromColumnIndex($sharedColumn) . $sharedRow;
$formula = $spreadsheet->getActiveSheet()->getCell($sharedCoordinate)->getValue();
$spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($formula);
$referenceHelper = ReferenceHelper::getInstance();
$newFormula = $referenceHelper->updateFormulaReferences($formula, 'A1', (int) $column - $sharedColumn, (int) $row - $sharedRow, '', true, false);
$spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($newFormula);
//$calc = $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->getCalculatedValue();
//$spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($calc);
$cellData = Calculation::unwrapResult($cellData);
$spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($cellData, $tryNumeric);
return;
}
$columnLetter = Coordinate::stringFromColumnIndex((int) $column);
$cellData = Calculation::unwrapResult($cellData);
// Set cell value
$this->processCFinal($spreadsheet, $hasCalculatedValue, $cellDataFormula, $cellData, "$columnLetter$row", $tryNumeric);
}
private function processCFinal(Spreadsheet &$spreadsheet, bool $hasCalculatedValue, string $cellDataFormula, string $cellData, string $coordinate, bool $tryNumeric): void
{
// Set cell value
$spreadsheet->getActiveSheet()->getCell($coordinate)->setValue(($hasCalculatedValue) ? $cellDataFormula : $cellData);
if ($hasCalculatedValue) {
$cellData = Calculation::unwrapResult($cellData);
$spreadsheet->getActiveSheet()->getCell($coordinate)->setCalculatedValue($cellData, $tryNumeric);
}
}
private function processFRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void
{
// Read cell formatting
$formatStyle = $columnWidth = '';
$startCol = $endCol = '';
$fontStyle = '';
$styleData = [];
foreach ($rowData as $rowDatum) {
switch ($rowDatum[0]) {
case 'C':
case 'X':
$column = substr($rowDatum, 1);
break;
case 'R':
case 'Y':
$row = substr($rowDatum, 1);
break;
case 'P':
$formatStyle = $rowDatum;
break;
case 'W':
[$startCol, $endCol, $columnWidth] = explode(' ', substr($rowDatum, 1));
break;
case 'S':
$this->styleSettings($rowDatum, $styleData, $fontStyle);
break;
}
}
$this->addFormats($spreadsheet, $formatStyle, $row, $column);
$this->addFonts($spreadsheet, $fontStyle, $row, $column);
$this->addStyle($spreadsheet, $styleData, $row, $column);
$this->addWidth($spreadsheet, $columnWidth, $startCol, $endCol);
}
private const STYLE_SETTINGS_FONT = ['D' => 'bold', 'I' => 'italic'];
private const STYLE_SETTINGS_BORDER = [
'B' => 'bottom',
'L' => 'left',
'R' => 'right',
'T' => 'top',
];
private function styleSettings(string $rowDatum, array &$styleData, string &$fontStyle): void
{
$styleSettings = substr($rowDatum, 1);
$iMax = strlen($styleSettings);
for ($i = 0; $i < $iMax; ++$i) {
$char = $styleSettings[$i];
if (array_key_exists($char, self::STYLE_SETTINGS_FONT)) {
$styleData['font'][self::STYLE_SETTINGS_FONT[$char]] = true;
} elseif (array_key_exists($char, self::STYLE_SETTINGS_BORDER)) {
$styleData['borders'][self::STYLE_SETTINGS_BORDER[$char]]['borderStyle'] = Border::BORDER_THIN;
} elseif ($char == 'S') {
$styleData['fill']['fillType'] = Fill::FILL_PATTERN_GRAY125;
} elseif ($char == 'M') {
if (preg_match('/M([1-9]\\d*)/', $styleSettings, $matches)) {
$fontStyle = $matches[1];
}
}
}
}
private function addFormats(Spreadsheet &$spreadsheet, string $formatStyle, string $row, string $column): void
{
if ($formatStyle && $column > '' && $row > '') {
$columnLetter = Coordinate::stringFromColumnIndex((int) $column);
if (isset($this->formats[$formatStyle])) {
$spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->formats[$formatStyle]);
}
}
}
private function addFonts(Spreadsheet &$spreadsheet, string $fontStyle, string $row, string $column): void
{
if ($fontStyle && $column > '' && $row > '') {
$columnLetter = Coordinate::stringFromColumnIndex((int) $column);
if (isset($this->fonts[$fontStyle])) {
$spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->fonts[$fontStyle]);
}
}
}
private function addStyle(Spreadsheet &$spreadsheet, array $styleData, string $row, string $column): void
{
if ((!empty($styleData)) && $column > '' && $row > '') {
$columnLetter = Coordinate::stringFromColumnIndex((int) $column);
$spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($styleData);
}
}
private function addWidth(Spreadsheet $spreadsheet, string $columnWidth, string $startCol, string $endCol): void
{
if ($columnWidth > '') {
if ($startCol == $endCol) {
$startCol = Coordinate::stringFromColumnIndex((int) $startCol);
$spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth);
} else {
$startCol = Coordinate::stringFromColumnIndex((int) $startCol);
$endCol = Coordinate::stringFromColumnIndex((int) $endCol);
$spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth);
do {
$spreadsheet->getActiveSheet()->getColumnDimension(++$startCol)->setWidth((float) $columnWidth);
} while ($startCol !== $endCol);
}
}
}
private function processPRecord(array $rowData, Spreadsheet &$spreadsheet): void
{
// Read shared styles
$formatArray = [];
$fromFormats = ['\-', '\ '];
$toFormats = ['-', ' '];
foreach ($rowData as $rowDatum) {
switch ($rowDatum[0]) {
case 'P':
$formatArray['numberFormat']['formatCode'] = str_replace($fromFormats, $toFormats, substr($rowDatum, 1));
break;
case 'E':
case 'F':
$formatArray['font']['name'] = substr($rowDatum, 1);
break;
case 'M':
$formatArray['font']['size'] = ((float) substr($rowDatum, 1)) / 20;
break;
case 'L':
$this->processPColors($rowDatum, $formatArray);
break;
case 'S':
$this->processPFontStyles($rowDatum, $formatArray);
break;
}
}
$this->processPFinal($spreadsheet, $formatArray);
}
private function processPColors(string $rowDatum, array &$formatArray): void
{
if (preg_match('/L([1-9]\\d*)/', $rowDatum, $matches)) {
$fontColor = $matches[1] % 8;
$formatArray['font']['color']['argb'] = self::COLOR_ARRAY[$fontColor];
}
}
private function processPFontStyles(string $rowDatum, array &$formatArray): void
{
$styleSettings = substr($rowDatum, 1);
$iMax = strlen($styleSettings);
for ($i = 0; $i < $iMax; ++$i) {
if (array_key_exists($styleSettings[$i], self::FONT_STYLE_MAPPINGS)) {
$formatArray['font'][self::FONT_STYLE_MAPPINGS[$styleSettings[$i]]] = true;
}
}
}
private function processPFinal(Spreadsheet &$spreadsheet, array $formatArray): void
{
if (array_key_exists('numberFormat', $formatArray)) {
$this->formats['P' . $this->format] = $formatArray;
++$this->format;
} elseif (array_key_exists('font', $formatArray)) {
++$this->fontcount;
$this->fonts[$this->fontcount] = $formatArray;
if ($this->fontcount === 1) {
$spreadsheet->getDefaultStyle()->applyFromArray($formatArray);
}
}
}
/**
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
*/
public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
{
// Open file
$this->canReadOrBust($filename);
$fileHandle = $this->fileHandle;
rewind($fileHandle);
// Create new Worksheets
while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
$spreadsheet->createSheet();
}
$spreadsheet->setActiveSheetIndex($this->sheetIndex);
$spreadsheet->getActiveSheet()->setTitle(substr(basename($filename, '.slk'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH));
// Loop through file
$column = $row = '';
// loop through one row (line) at a time in the file
while (($rowDataTxt = fgets($fileHandle)) !== false) {
// convert SYLK encoded $rowData to UTF-8
$rowDataTxt = StringHelper::SYLKtoUTF8($rowDataTxt);
// explode each row at semicolons while taking into account that literal semicolon (;)
// is escaped like this (;;)
$rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowDataTxt)))));
$dataType = array_shift($rowData);
if ($dataType == 'P') {
// Read shared styles
$this->processPRecord($rowData, $spreadsheet);
} elseif ($dataType == 'C') {
// Read cell value data
$this->processCRecord($rowData, $spreadsheet, $row, $column);
} elseif ($dataType == 'F') {
// Read cell formatting
$this->processFRecord($rowData, $spreadsheet, $row, $column);
} else {
$this->columnRowFromRowData($rowData, $column, $row);
}
}
// Close file
fclose($fileHandle);
// Return
return $spreadsheet;
}
private function columnRowFromRowData(array $rowData, string &$column, string &$row): void
{
foreach ($rowData as $rowDatum) {
$char0 = $rowDatum[0];
if ($char0 === 'X' || $char0 == 'C') {
$column = substr($rowDatum, 1);
} elseif ($char0 === 'Y' || $char0 == 'R') {
$row = substr($rowDatum, 1);
}
}
}
/**
* Get sheet index.
*/
public function getSheetIndex(): int
{
return $this->sheetIndex;
}
/**
* Set sheet index.
*
* @param int $sheetIndex Sheet index
*
* @return $this
*/
public function setSheetIndex(int $sheetIndex): static
{
$this->sheetIndex = $sheetIndex;
return $this;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
class Color
{
/**
* Read color.
*
* @param int $color Indexed color
* @param array $palette Color palette
*
* @return array RGB color value, example: ['rgb' => 'FF0000']
*/
public static function map(int $color, array $palette, int $version): array
{
if ($color <= 0x07 || $color >= 0x40) {
// special built-in color
return Color\BuiltIn::lookup($color);
} elseif (isset($palette[$color - 8])) {
// palette color, color index 0x08 maps to pallete index 0
return $palette[$color - 8];
}
// default color table
if ($version == Xls::XLS_BIFF8) {
return Color\BIFF8::lookup($color);
}
// BIFF5
return Color\BIFF5::lookup($color);
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color;
class BIFF5
{
private const BIFF5_COLOR_MAP = [
0x08 => '000000',
0x09 => 'FFFFFF',
0x0A => 'FF0000',
0x0B => '00FF00',
0x0C => '0000FF',
0x0D => 'FFFF00',
0x0E => 'FF00FF',
0x0F => '00FFFF',
0x10 => '800000',
0x11 => '008000',
0x12 => '000080',
0x13 => '808000',
0x14 => '800080',
0x15 => '008080',
0x16 => 'C0C0C0',
0x17 => '808080',
0x18 => '8080FF',
0x19 => '802060',
0x1A => 'FFFFC0',
0x1B => 'A0E0F0',
0x1C => '600080',
0x1D => 'FF8080',
0x1E => '0080C0',
0x1F => 'C0C0FF',
0x20 => '000080',
0x21 => 'FF00FF',
0x22 => 'FFFF00',
0x23 => '00FFFF',
0x24 => '800080',
0x25 => '800000',
0x26 => '008080',
0x27 => '0000FF',
0x28 => '00CFFF',
0x29 => '69FFFF',
0x2A => 'E0FFE0',
0x2B => 'FFFF80',
0x2C => 'A6CAF0',
0x2D => 'DD9CB3',
0x2E => 'B38FEE',
0x2F => 'E3E3E3',
0x30 => '2A6FF9',
0x31 => '3FB8CD',
0x32 => '488436',
0x33 => '958C41',
0x34 => '8E5E42',
0x35 => 'A0627A',
0x36 => '624FAC',
0x37 => '969696',
0x38 => '1D2FBE',
0x39 => '286676',
0x3A => '004500',
0x3B => '453E01',
0x3C => '6A2813',
0x3D => '85396A',
0x3E => '4A3285',
0x3F => '424242',
];
/**
* Map color array from BIFF5 built-in color index.
*/
public static function lookup(int $color): array
{
return ['rgb' => self::BIFF5_COLOR_MAP[$color] ?? '000000'];
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color;
class BIFF8
{
private const BIFF8_COLOR_MAP = [
0x08 => '000000',
0x09 => 'FFFFFF',
0x0A => 'FF0000',
0x0B => '00FF00',
0x0C => '0000FF',
0x0D => 'FFFF00',
0x0E => 'FF00FF',
0x0F => '00FFFF',
0x10 => '800000',
0x11 => '008000',
0x12 => '000080',
0x13 => '808000',
0x14 => '800080',
0x15 => '008080',
0x16 => 'C0C0C0',
0x17 => '808080',
0x18 => '9999FF',
0x19 => '993366',
0x1A => 'FFFFCC',
0x1B => 'CCFFFF',
0x1C => '660066',
0x1D => 'FF8080',
0x1E => '0066CC',
0x1F => 'CCCCFF',
0x20 => '000080',
0x21 => 'FF00FF',
0x22 => 'FFFF00',
0x23 => '00FFFF',
0x24 => '800080',
0x25 => '800000',
0x26 => '008080',
0x27 => '0000FF',
0x28 => '00CCFF',
0x29 => 'CCFFFF',
0x2A => 'CCFFCC',
0x2B => 'FFFF99',
0x2C => '99CCFF',
0x2D => 'FF99CC',
0x2E => 'CC99FF',
0x2F => 'FFCC99',
0x30 => '3366FF',
0x31 => '33CCCC',
0x32 => '99CC00',
0x33 => 'FFCC00',
0x34 => 'FF9900',
0x35 => 'FF6600',
0x36 => '666699',
0x37 => '969696',
0x38 => '003366',
0x39 => '339966',
0x3A => '003300',
0x3B => '333300',
0x3C => '993300',
0x3D => '993366',
0x3E => '333399',
0x3F => '333333',
];
/**
* Map color array from BIFF8 built-in color index.
*/
public static function lookup(int $color): array
{
return ['rgb' => self::BIFF8_COLOR_MAP[$color] ?? '000000'];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color;
class BuiltIn
{
private const BUILTIN_COLOR_MAP = [
0x00 => '000000',
0x01 => 'FFFFFF',
0x02 => 'FF0000',
0x03 => '00FF00',
0x04 => '0000FF',
0x05 => 'FFFF00',
0x06 => 'FF00FF',
0x07 => '00FFFF',
0x40 => '000000', // system window text color
0x41 => 'FFFFFF', // system window background color
];
/**
* Map built-in color to RGB value.
*
* @param int $color Indexed color
*/
public static function lookup(int $color): array
{
return ['rgb' => self::BUILTIN_COLOR_MAP[$color] ?? '000000'];
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
class ConditionalFormatting
{
/**
* @var array<int, string>
*/
private static array $types = [
0x01 => Conditional::CONDITION_CELLIS,
0x02 => Conditional::CONDITION_EXPRESSION,
];
/**
* @var array<int, string>
*/
private static array $operators = [
0x00 => Conditional::OPERATOR_NONE,
0x01 => Conditional::OPERATOR_BETWEEN,
0x02 => Conditional::OPERATOR_NOTBETWEEN,
0x03 => Conditional::OPERATOR_EQUAL,
0x04 => Conditional::OPERATOR_NOTEQUAL,
0x05 => Conditional::OPERATOR_GREATERTHAN,
0x06 => Conditional::OPERATOR_LESSTHAN,
0x07 => Conditional::OPERATOR_GREATERTHANOREQUAL,
0x08 => Conditional::OPERATOR_LESSTHANOREQUAL,
];
public static function type(int $type): ?string
{
if (isset(self::$types[$type])) {
return self::$types[$type];
}
return null;
}
public static function operator(int $operator): ?string
{
if (isset(self::$operators[$operator])) {
return self::$operators[$operator];
}
return null;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
class DataValidationHelper
{
/**
* @var array<int, string>
*/
private static array $types = [
0x00 => DataValidation::TYPE_NONE,
0x01 => DataValidation::TYPE_WHOLE,
0x02 => DataValidation::TYPE_DECIMAL,
0x03 => DataValidation::TYPE_LIST,
0x04 => DataValidation::TYPE_DATE,
0x05 => DataValidation::TYPE_TIME,
0x06 => DataValidation::TYPE_TEXTLENGTH,
0x07 => DataValidation::TYPE_CUSTOM,
];
/**
* @var array<int, string>
*/
private static array $errorStyles = [
0x00 => DataValidation::STYLE_STOP,
0x01 => DataValidation::STYLE_WARNING,
0x02 => DataValidation::STYLE_INFORMATION,
];
/**
* @var array<int, string>
*/
private static array $operators = [
0x00 => DataValidation::OPERATOR_BETWEEN,
0x01 => DataValidation::OPERATOR_NOTBETWEEN,
0x02 => DataValidation::OPERATOR_EQUAL,
0x03 => DataValidation::OPERATOR_NOTEQUAL,
0x04 => DataValidation::OPERATOR_GREATERTHAN,
0x05 => DataValidation::OPERATOR_LESSTHAN,
0x06 => DataValidation::OPERATOR_GREATERTHANOREQUAL,
0x07 => DataValidation::OPERATOR_LESSTHANOREQUAL,
];
public static function type(int $type): ?string
{
if (isset(self::$types[$type])) {
return self::$types[$type];
}
return null;
}
public static function errorStyle(int $errorStyle): ?string
{
if (isset(self::$errorStyles[$errorStyle])) {
return self::$errorStyles[$errorStyle];
}
return null;
}
public static function operator(int $operator): ?string
{
if (isset(self::$operators[$operator])) {
return self::$operators[$operator];
}
return null;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
class ErrorCode
{
private const ERROR_CODE_MAP = [
0x00 => '#NULL!',
0x07 => '#DIV/0!',
0x0F => '#VALUE!',
0x17 => '#REF!',
0x1D => '#NAME?',
0x24 => '#NUM!',
0x2A => '#N/A',
];
/**
* Map error code, e.g. '#N/A'.
*/
public static function lookup(int $code): string|bool
{
return self::ERROR_CODE_MAP[$code] ?? false;
}
}

View file

@ -0,0 +1,609 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE\Blip;
class Escher
{
const DGGCONTAINER = 0xF000;
const BSTORECONTAINER = 0xF001;
const DGCONTAINER = 0xF002;
const SPGRCONTAINER = 0xF003;
const SPCONTAINER = 0xF004;
const DGG = 0xF006;
const BSE = 0xF007;
const DG = 0xF008;
const SPGR = 0xF009;
const SP = 0xF00A;
const OPT = 0xF00B;
const CLIENTTEXTBOX = 0xF00D;
const CLIENTANCHOR = 0xF010;
const CLIENTDATA = 0xF011;
const BLIPJPEG = 0xF01D;
const BLIPPNG = 0xF01E;
const SPLITMENUCOLORS = 0xF11E;
const TERTIARYOPT = 0xF122;
/**
* Escher stream data (binary).
*/
private string $data;
/**
* Size in bytes of the Escher stream data.
*/
private int $dataSize;
/**
* Current position of stream pointer in Escher stream data.
*/
private int $pos;
/**
* The object to be returned by the reader. Modified during load.
*
* @var BSE|BstoreContainer|DgContainer|DggContainer|\PhpOffice\PhpSpreadsheet\Shared\Escher|SpContainer|SpgrContainer
*/
private $object;
/**
* Create a new Escher instance.
*/
public function __construct(mixed $object)
{
$this->object = $object;
}
private const WHICH_ROUTINE = [
self::DGGCONTAINER => 'readDggContainer',
self::DGG => 'readDgg',
self::BSTORECONTAINER => 'readBstoreContainer',
self::BSE => 'readBSE',
self::BLIPJPEG => 'readBlipJPEG',
self::BLIPPNG => 'readBlipPNG',
self::OPT => 'readOPT',
self::TERTIARYOPT => 'readTertiaryOPT',
self::SPLITMENUCOLORS => 'readSplitMenuColors',
self::DGCONTAINER => 'readDgContainer',
self::DG => 'readDg',
self::SPGRCONTAINER => 'readSpgrContainer',
self::SPCONTAINER => 'readSpContainer',
self::SPGR => 'readSpgr',
self::SP => 'readSp',
self::CLIENTTEXTBOX => 'readClientTextbox',
self::CLIENTANCHOR => 'readClientAnchor',
self::CLIENTDATA => 'readClientData',
];
/**
* Load Escher stream data. May be a partial Escher stream.
*/
public function load(string $data): BSE|BstoreContainer|DgContainer|DggContainer|\PhpOffice\PhpSpreadsheet\Shared\Escher|SpContainer|SpgrContainer
{
$this->data = $data;
// total byte size of Excel data (workbook global substream + sheet substreams)
$this->dataSize = strlen($this->data);
$this->pos = 0;
// Parse Escher stream
while ($this->pos < $this->dataSize) {
// offset: 2; size: 2: Record Type
$fbt = Xls::getUInt2d($this->data, $this->pos + 2);
$routine = self::WHICH_ROUTINE[$fbt] ?? 'readDefault';
if (method_exists($this, $routine)) {
$this->$routine();
}
}
return $this->object;
}
/**
* Read a generic record.
*/
private function readDefault(): void
{
// offset 0; size: 2; recVer and recInstance
//$verInstance = Xls::getUInt2d($this->data, $this->pos);
// offset: 2; size: 2: Record Type
//$fbt = Xls::getUInt2d($this->data, $this->pos + 2);
// bit: 0-3; mask: 0x000F; recVer
//$recVer = (0x000F & $verInstance) >> 0;
$length = Xls::getInt4d($this->data, $this->pos + 4);
//$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
}
/**
* Read DggContainer record (Drawing Group Container).
*/
private function readDggContainer(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
// record is a container, read contents
$dggContainer = new DggContainer();
$this->applyAttribute('setDggContainer', $dggContainer);
$reader = new self($dggContainer);
$reader->load($recordData);
}
/**
* Read Dgg record (Drawing Group).
*/
private function readDgg(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
//$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
}
/**
* Read BstoreContainer record (Blip Store Container).
*/
private function readBstoreContainer(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
// record is a container, read contents
$bstoreContainer = new BstoreContainer();
$this->applyAttribute('setBstoreContainer', $bstoreContainer);
$reader = new self($bstoreContainer);
$reader->load($recordData);
}
/**
* Read BSE record.
*/
private function readBSE(): void
{
// offset: 0; size: 2; recVer and recInstance
// bit: 4-15; mask: 0xFFF0; recInstance
$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
// add BSE to BstoreContainer
$BSE = new BSE();
$this->applyAttribute('addBSE', $BSE);
$BSE->setBLIPType($recInstance);
// offset: 0; size: 1; btWin32 (MSOBLIPTYPE)
//$btWin32 = ord($recordData[0]);
// offset: 1; size: 1; btWin32 (MSOBLIPTYPE)
//$btMacOS = ord($recordData[1]);
// offset: 2; size: 16; MD4 digest
//$rgbUid = substr($recordData, 2, 16);
// offset: 18; size: 2; tag
//$tag = Xls::getUInt2d($recordData, 18);
// offset: 20; size: 4; size of BLIP in bytes
//$size = Xls::getInt4d($recordData, 20);
// offset: 24; size: 4; number of references to this BLIP
//$cRef = Xls::getInt4d($recordData, 24);
// offset: 28; size: 4; MSOFO file offset
//$foDelay = Xls::getInt4d($recordData, 28);
// offset: 32; size: 1; unused1
//$unused1 = ord($recordData[32]);
// offset: 33; size: 1; size of nameData in bytes (including null terminator)
$cbName = ord($recordData[33]);
// offset: 34; size: 1; unused2
//$unused2 = ord($recordData[34]);
// offset: 35; size: 1; unused3
//$unused3 = ord($recordData[35]);
// offset: 36; size: $cbName; nameData
//$nameData = substr($recordData, 36, $cbName);
// offset: 36 + $cbName, size: var; the BLIP data
$blipData = substr($recordData, 36 + $cbName);
// record is a container, read contents
$reader = new self($BSE);
$reader->load($blipData);
}
/**
* Read BlipJPEG record. Holds raw JPEG image data.
*/
private function readBlipJPEG(): void
{
// offset: 0; size: 2; recVer and recInstance
// bit: 4-15; mask: 0xFFF0; recInstance
$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
$pos = 0;
// offset: 0; size: 16; rgbUid1 (MD4 digest of)
//$rgbUid1 = substr($recordData, 0, 16);
$pos += 16;
// offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3
if (in_array($recInstance, [0x046B, 0x06E3])) {
//$rgbUid2 = substr($recordData, 16, 16);
$pos += 16;
}
// offset: var; size: 1; tag
//$tag = ord($recordData[$pos]);
++$pos;
// offset: var; size: var; the raw image data
$data = substr($recordData, $pos);
$blip = new Blip();
$blip->setData($data);
$this->applyAttribute('setBlip', $blip);
}
/**
* Read BlipPNG record. Holds raw PNG image data.
*/
private function readBlipPNG(): void
{
// offset: 0; size: 2; recVer and recInstance
// bit: 4-15; mask: 0xFFF0; recInstance
$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
$pos = 0;
// offset: 0; size: 16; rgbUid1 (MD4 digest of)
//$rgbUid1 = substr($recordData, 0, 16);
$pos += 16;
// offset: 16; size: 16; rgbUid2 (MD4 digest), only if $recInstance = 0x46B or 0x6E3
if ($recInstance == 0x06E1) {
//$rgbUid2 = substr($recordData, 16, 16);
$pos += 16;
}
// offset: var; size: 1; tag
//$tag = ord($recordData[$pos]);
++$pos;
// offset: var; size: var; the raw image data
$data = substr($recordData, $pos);
$blip = new Blip();
$blip->setData($data);
$this->applyAttribute('setBlip', $blip);
}
/**
* Read OPT record. This record may occur within DggContainer record or SpContainer.
*/
private function readOPT(): void
{
// offset: 0; size: 2; recVer and recInstance
// bit: 4-15; mask: 0xFFF0; recInstance
$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
$this->readOfficeArtRGFOPTE($recordData, $recInstance);
}
/**
* Read TertiaryOPT record.
*/
private function readTertiaryOPT(): void
{
// offset: 0; size: 2; recVer and recInstance
// bit: 4-15; mask: 0xFFF0; recInstance
//$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
$length = Xls::getInt4d($this->data, $this->pos + 4);
//$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
}
/**
* Read SplitMenuColors record.
*/
private function readSplitMenuColors(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
//$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
}
/**
* Read DgContainer record (Drawing Container).
*/
private function readDgContainer(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
// record is a container, read contents
$dgContainer = new DgContainer();
$this->applyAttribute('setDgContainer', $dgContainer);
$reader = new self($dgContainer);
$reader->load($recordData);
}
/**
* Read Dg record (Drawing).
*/
private function readDg(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
//$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
}
/**
* Read SpgrContainer record (Shape Group Container).
*/
private function readSpgrContainer(): void
{
// context is either context DgContainer or SpgrContainer
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
// record is a container, read contents
$spgrContainer = new SpgrContainer();
if ($this->object instanceof DgContainer) {
// DgContainer
$this->object->setSpgrContainer($spgrContainer);
} elseif ($this->object instanceof SpgrContainer) {
// SpgrContainer
$this->object->addChild($spgrContainer);
}
$reader = new self($spgrContainer);
$reader->load($recordData);
}
/**
* Read SpContainer record (Shape Container).
*/
private function readSpContainer(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// add spContainer to spgrContainer
$spContainer = new SpContainer();
$this->applyAttribute('addChild', $spContainer);
// move stream pointer to next record
$this->pos += 8 + $length;
// record is a container, read contents
$reader = new self($spContainer);
$reader->load($recordData);
}
/**
* Read Spgr record (Shape Group).
*/
private function readSpgr(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
//$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
}
/**
* Read Sp record (Shape).
*/
private function readSp(): void
{
// offset: 0; size: 2; recVer and recInstance
// bit: 4-15; mask: 0xFFF0; recInstance
//$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
$length = Xls::getInt4d($this->data, $this->pos + 4);
//$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
}
/**
* Read ClientTextbox record.
*/
private function readClientTextbox(): void
{
// offset: 0; size: 2; recVer and recInstance
// bit: 4-15; mask: 0xFFF0; recInstance
//$recInstance = (0xFFF0 & Xls::getUInt2d($this->data, $this->pos)) >> 4;
$length = Xls::getInt4d($this->data, $this->pos + 4);
//$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
}
/**
* Read ClientAnchor record. This record holds information about where the shape is anchored in worksheet.
*/
private function readClientAnchor(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
// offset: 2; size: 2; upper-left corner column index (0-based)
$c1 = Xls::getUInt2d($recordData, 2);
// offset: 4; size: 2; upper-left corner horizontal offset in 1/1024 of column width
$startOffsetX = Xls::getUInt2d($recordData, 4);
// offset: 6; size: 2; upper-left corner row index (0-based)
$r1 = Xls::getUInt2d($recordData, 6);
// offset: 8; size: 2; upper-left corner vertical offset in 1/256 of row height
$startOffsetY = Xls::getUInt2d($recordData, 8);
// offset: 10; size: 2; bottom-right corner column index (0-based)
$c2 = Xls::getUInt2d($recordData, 10);
// offset: 12; size: 2; bottom-right corner horizontal offset in 1/1024 of column width
$endOffsetX = Xls::getUInt2d($recordData, 12);
// offset: 14; size: 2; bottom-right corner row index (0-based)
$r2 = Xls::getUInt2d($recordData, 14);
// offset: 16; size: 2; bottom-right corner vertical offset in 1/256 of row height
$endOffsetY = Xls::getUInt2d($recordData, 16);
$this->applyAttribute('setStartCoordinates', Coordinate::stringFromColumnIndex($c1 + 1) . ($r1 + 1));
$this->applyAttribute('setStartOffsetX', $startOffsetX);
$this->applyAttribute('setStartOffsetY', $startOffsetY);
$this->applyAttribute('setEndCoordinates', Coordinate::stringFromColumnIndex($c2 + 1) . ($r2 + 1));
$this->applyAttribute('setEndOffsetX', $endOffsetX);
$this->applyAttribute('setEndOffsetY', $endOffsetY);
}
private function applyAttribute(string $name, mixed $value): void
{
if (method_exists($this->object, $name)) {
$this->object->$name($value);
}
}
/**
* Read ClientData record.
*/
private function readClientData(): void
{
$length = Xls::getInt4d($this->data, $this->pos + 4);
//$recordData = substr($this->data, $this->pos + 8, $length);
// move stream pointer to next record
$this->pos += 8 + $length;
}
/**
* Read OfficeArtRGFOPTE table of property-value pairs.
*
* @param string $data Binary data
* @param int $n Number of properties
*/
private function readOfficeArtRGFOPTE(string $data, int $n): void
{
$splicedComplexData = substr($data, 6 * $n);
// loop through property-value pairs
for ($i = 0; $i < $n; ++$i) {
// read 6 bytes at a time
$fopte = substr($data, 6 * $i, 6);
// offset: 0; size: 2; opid
$opid = Xls::getUInt2d($fopte, 0);
// bit: 0-13; mask: 0x3FFF; opid.opid
$opidOpid = (0x3FFF & $opid) >> 0;
// bit: 14; mask 0x4000; 1 = value in op field is BLIP identifier
//$opidFBid = (0x4000 & $opid) >> 14;
// bit: 15; mask 0x8000; 1 = this is a complex property, op field specifies size of complex data
$opidFComplex = (0x8000 & $opid) >> 15;
// offset: 2; size: 4; the value for this property
$op = Xls::getInt4d($fopte, 2);
if ($opidFComplex) {
$complexData = substr($splicedComplexData, 0, $op);
$splicedComplexData = substr($splicedComplexData, $op);
// we store string value with complex data
$value = $complexData;
} else {
// we store integer value
$value = $op;
}
if (method_exists($this->object, 'setOPT')) {
$this->object->setOPT($opidOpid, $value);
}
}
}
}

View file

@ -0,0 +1,193 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
class MD5
{
private int $a;
private int $b;
private int $c;
private int $d;
private static int $allOneBits;
/**
* MD5 stream constructor.
*/
public function __construct()
{
self::$allOneBits = self::signedInt(0xFFFFFFFF);
$this->reset();
}
/**
* Reset the MD5 stream context.
*/
public function reset(): void
{
$this->a = 0x67452301;
$this->b = self::signedInt(0xEFCDAB89);
$this->c = self::signedInt(0x98BADCFE);
$this->d = 0x10325476;
}
/**
* Get MD5 stream context.
*/
public function getContext(): string
{
$s = '';
foreach (['a', 'b', 'c', 'd'] as $i) {
$v = $this->{$i};
$s .= chr($v & 0xFF);
$s .= chr(($v >> 8) & 0xFF);
$s .= chr(($v >> 16) & 0xFF);
$s .= chr(($v >> 24) & 0xFF);
}
return $s;
}
/**
* Add data to context.
*
* @param string $data Data to add
*/
public function add(string $data): void
{
// @phpstan-ignore-next-line
$words = array_values(unpack('V16', $data));
$A = $this->a;
$B = $this->b;
$C = $this->c;
$D = $this->d;
$F = [self::class, 'f'];
$G = [self::class, 'g'];
$H = [self::class, 'h'];
$I = [self::class, 'i'];
// ROUND 1
self::step($F, $A, $B, $C, $D, $words[0], 7, 0xD76AA478);
self::step($F, $D, $A, $B, $C, $words[1], 12, 0xE8C7B756);
self::step($F, $C, $D, $A, $B, $words[2], 17, 0x242070DB);
self::step($F, $B, $C, $D, $A, $words[3], 22, 0xC1BDCEEE);
self::step($F, $A, $B, $C, $D, $words[4], 7, 0xF57C0FAF);
self::step($F, $D, $A, $B, $C, $words[5], 12, 0x4787C62A);
self::step($F, $C, $D, $A, $B, $words[6], 17, 0xA8304613);
self::step($F, $B, $C, $D, $A, $words[7], 22, 0xFD469501);
self::step($F, $A, $B, $C, $D, $words[8], 7, 0x698098D8);
self::step($F, $D, $A, $B, $C, $words[9], 12, 0x8B44F7AF);
self::step($F, $C, $D, $A, $B, $words[10], 17, 0xFFFF5BB1);
self::step($F, $B, $C, $D, $A, $words[11], 22, 0x895CD7BE);
self::step($F, $A, $B, $C, $D, $words[12], 7, 0x6B901122);
self::step($F, $D, $A, $B, $C, $words[13], 12, 0xFD987193);
self::step($F, $C, $D, $A, $B, $words[14], 17, 0xA679438E);
self::step($F, $B, $C, $D, $A, $words[15], 22, 0x49B40821);
// ROUND 2
self::step($G, $A, $B, $C, $D, $words[1], 5, 0xF61E2562);
self::step($G, $D, $A, $B, $C, $words[6], 9, 0xC040B340);
self::step($G, $C, $D, $A, $B, $words[11], 14, 0x265E5A51);
self::step($G, $B, $C, $D, $A, $words[0], 20, 0xE9B6C7AA);
self::step($G, $A, $B, $C, $D, $words[5], 5, 0xD62F105D);
self::step($G, $D, $A, $B, $C, $words[10], 9, 0x02441453);
self::step($G, $C, $D, $A, $B, $words[15], 14, 0xD8A1E681);
self::step($G, $B, $C, $D, $A, $words[4], 20, 0xE7D3FBC8);
self::step($G, $A, $B, $C, $D, $words[9], 5, 0x21E1CDE6);
self::step($G, $D, $A, $B, $C, $words[14], 9, 0xC33707D6);
self::step($G, $C, $D, $A, $B, $words[3], 14, 0xF4D50D87);
self::step($G, $B, $C, $D, $A, $words[8], 20, 0x455A14ED);
self::step($G, $A, $B, $C, $D, $words[13], 5, 0xA9E3E905);
self::step($G, $D, $A, $B, $C, $words[2], 9, 0xFCEFA3F8);
self::step($G, $C, $D, $A, $B, $words[7], 14, 0x676F02D9);
self::step($G, $B, $C, $D, $A, $words[12], 20, 0x8D2A4C8A);
// ROUND 3
self::step($H, $A, $B, $C, $D, $words[5], 4, 0xFFFA3942);
self::step($H, $D, $A, $B, $C, $words[8], 11, 0x8771F681);
self::step($H, $C, $D, $A, $B, $words[11], 16, 0x6D9D6122);
self::step($H, $B, $C, $D, $A, $words[14], 23, 0xFDE5380C);
self::step($H, $A, $B, $C, $D, $words[1], 4, 0xA4BEEA44);
self::step($H, $D, $A, $B, $C, $words[4], 11, 0x4BDECFA9);
self::step($H, $C, $D, $A, $B, $words[7], 16, 0xF6BB4B60);
self::step($H, $B, $C, $D, $A, $words[10], 23, 0xBEBFBC70);
self::step($H, $A, $B, $C, $D, $words[13], 4, 0x289B7EC6);
self::step($H, $D, $A, $B, $C, $words[0], 11, 0xEAA127FA);
self::step($H, $C, $D, $A, $B, $words[3], 16, 0xD4EF3085);
self::step($H, $B, $C, $D, $A, $words[6], 23, 0x04881D05);
self::step($H, $A, $B, $C, $D, $words[9], 4, 0xD9D4D039);
self::step($H, $D, $A, $B, $C, $words[12], 11, 0xE6DB99E5);
self::step($H, $C, $D, $A, $B, $words[15], 16, 0x1FA27CF8);
self::step($H, $B, $C, $D, $A, $words[2], 23, 0xC4AC5665);
// ROUND 4
self::step($I, $A, $B, $C, $D, $words[0], 6, 0xF4292244);
self::step($I, $D, $A, $B, $C, $words[7], 10, 0x432AFF97);
self::step($I, $C, $D, $A, $B, $words[14], 15, 0xAB9423A7);
self::step($I, $B, $C, $D, $A, $words[5], 21, 0xFC93A039);
self::step($I, $A, $B, $C, $D, $words[12], 6, 0x655B59C3);
self::step($I, $D, $A, $B, $C, $words[3], 10, 0x8F0CCC92);
self::step($I, $C, $D, $A, $B, $words[10], 15, 0xFFEFF47D);
self::step($I, $B, $C, $D, $A, $words[1], 21, 0x85845DD1);
self::step($I, $A, $B, $C, $D, $words[8], 6, 0x6FA87E4F);
self::step($I, $D, $A, $B, $C, $words[15], 10, 0xFE2CE6E0);
self::step($I, $C, $D, $A, $B, $words[6], 15, 0xA3014314);
self::step($I, $B, $C, $D, $A, $words[13], 21, 0x4E0811A1);
self::step($I, $A, $B, $C, $D, $words[4], 6, 0xF7537E82);
self::step($I, $D, $A, $B, $C, $words[11], 10, 0xBD3AF235);
self::step($I, $C, $D, $A, $B, $words[2], 15, 0x2AD7D2BB);
self::step($I, $B, $C, $D, $A, $words[9], 21, 0xEB86D391);
$this->a = ($this->a + $A) & self::$allOneBits;
$this->b = ($this->b + $B) & self::$allOneBits;
$this->c = ($this->c + $C) & self::$allOneBits;
$this->d = ($this->d + $D) & self::$allOneBits;
}
private static function f(int $X, int $Y, int $Z): int
{
return ($X & $Y) | ((~$X) & $Z); // X AND Y OR NOT X AND Z
}
private static function g(int $X, int $Y, int $Z): int
{
return ($X & $Z) | ($Y & (~$Z)); // X AND Z OR Y AND NOT Z
}
private static function h(int $X, int $Y, int $Z): int
{
return $X ^ $Y ^ $Z; // X XOR Y XOR Z
}
private static function i(int $X, int $Y, int $Z): int
{
return $Y ^ ($X | (~$Z)); // Y XOR (X OR NOT Z)
}
/** @param float|int $t may be float on 32-bit system */
private static function step(callable $func, int &$A, int $B, int $C, int $D, int $M, int $s, $t): void
{
$t = self::signedInt($t);
$A = (int) ($A + call_user_func($func, $B, $C, $D) + $M + $t) & self::$allOneBits;
$A = self::rotate($A, $s);
$A = (int) ($B + $A) & self::$allOneBits;
}
/** @param float|int $result may be float on 32-bit system */
private static function signedInt($result): int
{
return is_int($result) ? $result : (int) (PHP_INT_MIN + $result - 1 - PHP_INT_MAX);
}
private static function rotate(int $decimal, int $bits): int
{
$binary = str_pad(decbin($decimal), 32, '0', STR_PAD_LEFT);
return self::signedInt(bindec(substr($binary, $bits) . substr($binary, 0, $bits)));
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
class RC4
{
/** @var int[] */
protected array $s = []; // Context
protected int $i = 0;
protected int $j = 0;
/**
* RC4 stream decryption/encryption constrcutor.
*
* @param string $key Encryption key/passphrase
*/
public function __construct(string $key)
{
$len = strlen($key);
for ($this->i = 0; $this->i < 256; ++$this->i) {
$this->s[$this->i] = $this->i;
}
$this->j = 0;
for ($this->i = 0; $this->i < 256; ++$this->i) {
$this->j = ($this->j + $this->s[$this->i] + ord($key[$this->i % $len])) % 256;
$t = $this->s[$this->i];
$this->s[$this->i] = $this->s[$this->j];
$this->s[$this->j] = $t;
}
$this->i = $this->j = 0;
}
/**
* Symmetric decryption/encryption function.
*
* @param string $data Data to encrypt/decrypt
*/
public function RC4(string $data): string
{
$len = strlen($data);
for ($c = 0; $c < $len; ++$c) {
$this->i = ($this->i + 1) % 256;
$this->j = ($this->j + $this->s[$this->i]) % 256;
$t = $this->s[$this->i];
$this->s[$this->i] = $this->s[$this->j];
$this->s[$this->j] = $t;
$t = ($this->s[$this->i] + $this->s[$this->j]) % 256;
$data[$c] = chr(ord($data[$c]) ^ $this->s[$t]);
}
return $data;
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Style;
use PhpOffice\PhpSpreadsheet\Style\Border as StyleBorder;
class Border
{
/**
* @var array<int, string>
*/
protected static array $borderStyleMap = [
0x00 => StyleBorder::BORDER_NONE,
0x01 => StyleBorder::BORDER_THIN,
0x02 => StyleBorder::BORDER_MEDIUM,
0x03 => StyleBorder::BORDER_DASHED,
0x04 => StyleBorder::BORDER_DOTTED,
0x05 => StyleBorder::BORDER_THICK,
0x06 => StyleBorder::BORDER_DOUBLE,
0x07 => StyleBorder::BORDER_HAIR,
0x08 => StyleBorder::BORDER_MEDIUMDASHED,
0x09 => StyleBorder::BORDER_DASHDOT,
0x0A => StyleBorder::BORDER_MEDIUMDASHDOT,
0x0B => StyleBorder::BORDER_DASHDOTDOT,
0x0C => StyleBorder::BORDER_MEDIUMDASHDOTDOT,
0x0D => StyleBorder::BORDER_SLANTDASHDOT,
];
public static function lookup(int $index): string
{
if (isset(self::$borderStyleMap[$index])) {
return self::$borderStyleMap[$index];
}
return StyleBorder::BORDER_NONE;
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Style;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
class CellAlignment
{
/**
* @var array<int, string>
*/
protected static array $horizontalAlignmentMap = [
0 => Alignment::HORIZONTAL_GENERAL,
1 => Alignment::HORIZONTAL_LEFT,
2 => Alignment::HORIZONTAL_CENTER,
3 => Alignment::HORIZONTAL_RIGHT,
4 => Alignment::HORIZONTAL_FILL,
5 => Alignment::HORIZONTAL_JUSTIFY,
6 => Alignment::HORIZONTAL_CENTER_CONTINUOUS,
];
/**
* @var array<int, string>
*/
protected static array $verticalAlignmentMap = [
0 => Alignment::VERTICAL_TOP,
1 => Alignment::VERTICAL_CENTER,
2 => Alignment::VERTICAL_BOTTOM,
3 => Alignment::VERTICAL_JUSTIFY,
];
public static function horizontal(Alignment $alignment, int $horizontal): void
{
if (array_key_exists($horizontal, self::$horizontalAlignmentMap)) {
$alignment->setHorizontal(self::$horizontalAlignmentMap[$horizontal]);
}
}
public static function vertical(Alignment $alignment, int $vertical): void
{
if (array_key_exists($vertical, self::$verticalAlignmentMap)) {
$alignment->setVertical(self::$verticalAlignmentMap[$vertical]);
}
}
public static function wrap(Alignment $alignment, int $wrap): void
{
$alignment->setWrapText((bool) $wrap);
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Style;
use PhpOffice\PhpSpreadsheet\Style\Font;
class CellFont
{
public static function escapement(Font $font, int $escapement): void
{
switch ($escapement) {
case 0x0001:
$font->setSuperscript(true);
break;
case 0x0002:
$font->setSubscript(true);
break;
}
}
/**
* @var array<int, string>
*/
protected static array $underlineMap = [
0x01 => Font::UNDERLINE_SINGLE,
0x02 => Font::UNDERLINE_DOUBLE,
0x21 => Font::UNDERLINE_SINGLEACCOUNTING,
0x22 => Font::UNDERLINE_DOUBLEACCOUNTING,
];
public static function underline(Font $font, int $underline): void
{
if (array_key_exists($underline, self::$underlineMap)) {
$font->setUnderline(self::$underlineMap[$underline]);
}
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Style;
use PhpOffice\PhpSpreadsheet\Style\Fill;
class FillPattern
{
/**
* @var array<int, string>
*/
protected static array $fillPatternMap = [
0x00 => Fill::FILL_NONE,
0x01 => Fill::FILL_SOLID,
0x02 => Fill::FILL_PATTERN_MEDIUMGRAY,
0x03 => Fill::FILL_PATTERN_DARKGRAY,
0x04 => Fill::FILL_PATTERN_LIGHTGRAY,
0x05 => Fill::FILL_PATTERN_DARKHORIZONTAL,
0x06 => Fill::FILL_PATTERN_DARKVERTICAL,
0x07 => Fill::FILL_PATTERN_DARKDOWN,
0x08 => Fill::FILL_PATTERN_DARKUP,
0x09 => Fill::FILL_PATTERN_DARKGRID,
0x0A => Fill::FILL_PATTERN_DARKTRELLIS,
0x0B => Fill::FILL_PATTERN_LIGHTHORIZONTAL,
0x0C => Fill::FILL_PATTERN_LIGHTVERTICAL,
0x0D => Fill::FILL_PATTERN_LIGHTDOWN,
0x0E => Fill::FILL_PATTERN_LIGHTUP,
0x0F => Fill::FILL_PATTERN_LIGHTGRID,
0x10 => Fill::FILL_PATTERN_LIGHTTRELLIS,
0x11 => Fill::FILL_PATTERN_GRAY125,
0x12 => Fill::FILL_PATTERN_GRAY0625,
];
/**
* Get fill pattern from index
* OpenOffice documentation: 2.5.12.
*/
public static function lookup(int $index): string
{
if (isset(self::$fillPatternMap[$index])) {
return self::$fillPatternMap[$index];
}
return Fill::FILL_NONE;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,161 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column;
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
class AutoFilter
{
private Table|Worksheet $parent;
private SimpleXMLElement $worksheetXml;
public function __construct(Table|Worksheet $parent, SimpleXMLElement $worksheetXml)
{
$this->parent = $parent;
$this->worksheetXml = $worksheetXml;
}
public function load(): void
{
// Remove all "$" in the auto filter range
$attrs = $this->worksheetXml->autoFilter->attributes() ?? [];
$autoFilterRange = (string) preg_replace('/\$/', '', $attrs['ref'] ?? '');
if (str_contains($autoFilterRange, ':')) {
$this->readAutoFilter($autoFilterRange);
}
}
private function readAutoFilter(string $autoFilterRange): void
{
$autoFilter = $this->parent->getAutoFilter();
$autoFilter->setRange($autoFilterRange);
foreach ($this->worksheetXml->autoFilter->filterColumn as $filterColumn) {
$attributes = $filterColumn->attributes() ?? [];
$column = $autoFilter->getColumnByOffset((int) ($attributes['colId'] ?? 0));
// Check for standard filters
if ($filterColumn->filters) {
$column->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER);
$filters = Xlsx::testSimpleXml($filterColumn->filters->attributes());
if ((isset($filters['blank'])) && ((int) $filters['blank'] == 1)) {
// Operator is undefined, but always treated as EQUAL
$column->createRule()->setRule('', '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER);
}
// Standard filters are always an OR join, so no join rule needs to be set
// Entries can be either filter elements
foreach ($filterColumn->filters->filter as $filterRule) {
// Operator is undefined, but always treated as EQUAL
$attr2 = $filterRule->attributes() ?? ['val' => ''];
$column->createRule()->setRule('', (string) $attr2['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER);
}
// Or Date Group elements
$this->readDateRangeAutoFilter($filterColumn->filters, $column);
}
// Check for custom filters
$this->readCustomAutoFilter($filterColumn, $column);
// Check for dynamic filters
$this->readDynamicAutoFilter($filterColumn, $column);
// Check for dynamic filters
$this->readTopTenAutoFilter($filterColumn, $column);
}
$autoFilter->setEvaluated(true);
}
private function readDateRangeAutoFilter(SimpleXMLElement $filters, Column $column): void
{
foreach ($filters->dateGroupItem as $dateGroupItemx) {
// Operator is undefined, but always treated as EQUAL
$dateGroupItem = $dateGroupItemx->attributes();
if ($dateGroupItem !== null) {
$column->createRule()->setRule(
'',
[
'year' => (string) $dateGroupItem['year'],
'month' => (string) $dateGroupItem['month'],
'day' => (string) $dateGroupItem['day'],
'hour' => (string) $dateGroupItem['hour'],
'minute' => (string) $dateGroupItem['minute'],
'second' => (string) $dateGroupItem['second'],
],
(string) $dateGroupItem['dateTimeGrouping']
)->setRuleType(Rule::AUTOFILTER_RULETYPE_DATEGROUP);
}
}
}
private function readCustomAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void
{
if (isset($filterColumn, $filterColumn->customFilters)) {
$column->setFilterType(Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER);
$customFilters = $filterColumn->customFilters;
$attributes = $customFilters->attributes();
// Custom filters can an AND or an OR join;
// and there should only ever be one or two entries
if ((isset($attributes['and'])) && ((string) $attributes['and'] === '1')) {
$column->setJoin(Column::AUTOFILTER_COLUMN_JOIN_AND);
}
foreach ($customFilters->customFilter as $filterRule) {
$attr2 = $filterRule->attributes() ?? ['operator' => '', 'val' => ''];
$column->createRule()->setRule(
(string) $attr2['operator'],
(string) $attr2['val']
)->setRuleType(Rule::AUTOFILTER_RULETYPE_CUSTOMFILTER);
}
}
}
private function readDynamicAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void
{
if (isset($filterColumn, $filterColumn->dynamicFilter)) {
$column->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER);
// We should only ever have one dynamic filter
foreach ($filterColumn->dynamicFilter as $filterRule) {
// Operator is undefined, but always treated as EQUAL
$attr2 = $filterRule->attributes() ?? [];
$column->createRule()->setRule(
'',
(string) ($attr2['val'] ?? ''),
(string) ($attr2['type'] ?? '')
)->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER);
if (isset($attr2['val'])) {
$column->setAttribute('val', (string) $attr2['val']);
}
if (isset($attr2['maxVal'])) {
$column->setAttribute('maxVal', (string) $attr2['maxVal']);
}
}
}
}
private function readTopTenAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void
{
if (isset($filterColumn, $filterColumn->top10)) {
$column->setFilterType(Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER);
// We should only ever have one top10 filter
foreach ($filterColumn->top10 as $filterRule) {
$attr2 = $filterRule->attributes() ?? [];
$column->createRule()->setRule(
(
((isset($attr2['percent'])) && ((string) $attr2['percent'] === '1'))
? Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT
: Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_BY_VALUE
),
(string) ($attr2['val'] ?? ''),
(
((isset($attr2['top'])) && ((string) $attr2['top'] === '1'))
? Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP
: Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM
)
)->setRuleType(Rule::AUTOFILTER_RULETYPE_TOPTENFILTER);
}
}
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use Stringable;
class BaseParserClass
{
protected static function boolean(mixed $value): bool
{
if (is_object($value)) {
$value = ($value instanceof Stringable) ? ((string) $value) : 'true';
}
if (is_numeric($value)) {
return (bool) $value;
}
return $value === 'true' || $value === 'TRUE';
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,219 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Reader\DefaultReadFilter;
use PhpOffice\PhpSpreadsheet\Reader\IReadFilter;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
class ColumnAndRowAttributes extends BaseParserClass
{
private Worksheet $worksheet;
private ?SimpleXMLElement $worksheetXml;
public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null)
{
$this->worksheet = $workSheet;
$this->worksheetXml = $worksheetXml;
}
/**
* Set Worksheet column attributes by attributes array passed.
*
* @param string $columnAddress A, B, ... DX, ...
* @param array $columnAttributes array of attributes (indexes are attribute name, values are value)
* 'xfIndex', 'visible', 'collapsed', 'outlineLevel', 'width', ... ?
*/
private function setColumnAttributes(string $columnAddress, array $columnAttributes): void
{
if (isset($columnAttributes['xfIndex'])) {
$this->worksheet->getColumnDimension($columnAddress)->setXfIndex($columnAttributes['xfIndex']);
}
if (isset($columnAttributes['visible'])) {
$this->worksheet->getColumnDimension($columnAddress)->setVisible($columnAttributes['visible']);
}
if (isset($columnAttributes['collapsed'])) {
$this->worksheet->getColumnDimension($columnAddress)->setCollapsed($columnAttributes['collapsed']);
}
if (isset($columnAttributes['outlineLevel'])) {
$this->worksheet->getColumnDimension($columnAddress)->setOutlineLevel($columnAttributes['outlineLevel']);
}
if (isset($columnAttributes['width'])) {
$this->worksheet->getColumnDimension($columnAddress)->setWidth($columnAttributes['width']);
}
}
/**
* Set Worksheet row attributes by attributes array passed.
*
* @param int $rowNumber 1, 2, 3, ... 99, ...
* @param array $rowAttributes array of attributes (indexes are attribute name, values are value)
* 'xfIndex', 'visible', 'collapsed', 'outlineLevel', 'rowHeight', ... ?
*/
private function setRowAttributes(int $rowNumber, array $rowAttributes): void
{
if (isset($rowAttributes['xfIndex'])) {
$this->worksheet->getRowDimension($rowNumber)->setXfIndex($rowAttributes['xfIndex']);
}
if (isset($rowAttributes['visible'])) {
$this->worksheet->getRowDimension($rowNumber)->setVisible($rowAttributes['visible']);
}
if (isset($rowAttributes['collapsed'])) {
$this->worksheet->getRowDimension($rowNumber)->setCollapsed($rowAttributes['collapsed']);
}
if (isset($rowAttributes['outlineLevel'])) {
$this->worksheet->getRowDimension($rowNumber)->setOutlineLevel($rowAttributes['outlineLevel']);
}
if (isset($rowAttributes['rowHeight'])) {
$this->worksheet->getRowDimension($rowNumber)->setRowHeight($rowAttributes['rowHeight']);
}
}
public function load(?IReadFilter $readFilter = null, bool $readDataOnly = false): void
{
if ($this->worksheetXml === null) {
return;
}
$columnsAttributes = [];
$rowsAttributes = [];
if (isset($this->worksheetXml->cols)) {
$columnsAttributes = $this->readColumnAttributes($this->worksheetXml->cols, $readDataOnly);
}
if ($this->worksheetXml->sheetData && $this->worksheetXml->sheetData->row) {
$rowsAttributes = $this->readRowAttributes($this->worksheetXml->sheetData->row, $readDataOnly);
}
if ($readFilter !== null && $readFilter::class === DefaultReadFilter::class) {
$readFilter = null;
}
// set columns/rows attributes
$columnsAttributesAreSet = [];
foreach ($columnsAttributes as $columnCoordinate => $columnAttributes) {
if (
$readFilter === null
|| !$this->isFilteredColumn($readFilter, $columnCoordinate, $rowsAttributes)
) {
if (!isset($columnsAttributesAreSet[$columnCoordinate])) {
$this->setColumnAttributes($columnCoordinate, $columnAttributes);
$columnsAttributesAreSet[$columnCoordinate] = true;
}
}
}
$rowsAttributesAreSet = [];
foreach ($rowsAttributes as $rowCoordinate => $rowAttributes) {
if (
$readFilter === null
|| !$this->isFilteredRow($readFilter, $rowCoordinate, $columnsAttributes)
) {
if (!isset($rowsAttributesAreSet[$rowCoordinate])) {
$this->setRowAttributes($rowCoordinate, $rowAttributes);
$rowsAttributesAreSet[$rowCoordinate] = true;
}
}
}
}
private function isFilteredColumn(IReadFilter $readFilter, string $columnCoordinate, array $rowsAttributes): bool
{
foreach ($rowsAttributes as $rowCoordinate => $rowAttributes) {
if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) {
return true;
}
}
return false;
}
private function readColumnAttributes(SimpleXMLElement $worksheetCols, bool $readDataOnly): array
{
$columnAttributes = [];
foreach ($worksheetCols->col as $columnx) {
$column = $columnx->attributes();
if ($column !== null) {
$startColumn = Coordinate::stringFromColumnIndex((int) $column['min']);
$endColumn = Coordinate::stringFromColumnIndex((int) $column['max']);
++$endColumn;
for ($columnAddress = $startColumn; $columnAddress !== $endColumn; ++$columnAddress) {
$columnAttributes[$columnAddress] = $this->readColumnRangeAttributes($column, $readDataOnly);
if ((int) ($column['max']) == 16384) {
break;
}
}
}
}
return $columnAttributes;
}
private function readColumnRangeAttributes(?SimpleXMLElement $column, bool $readDataOnly): array
{
$columnAttributes = [];
if ($column !== null) {
if (isset($column['style']) && !$readDataOnly) {
$columnAttributes['xfIndex'] = (int) $column['style'];
}
if (isset($column['hidden']) && self::boolean($column['hidden'])) {
$columnAttributes['visible'] = false;
}
if (isset($column['collapsed']) && self::boolean($column['collapsed'])) {
$columnAttributes['collapsed'] = true;
}
if (isset($column['outlineLevel']) && ((int) $column['outlineLevel']) > 0) {
$columnAttributes['outlineLevel'] = (int) $column['outlineLevel'];
}
if (isset($column['width'])) {
$columnAttributes['width'] = (float) $column['width'];
}
}
return $columnAttributes;
}
private function isFilteredRow(IReadFilter $readFilter, int $rowCoordinate, array $columnsAttributes): bool
{
foreach ($columnsAttributes as $columnCoordinate => $columnAttributes) {
if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) {
return true;
}
}
return false;
}
private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDataOnly): array
{
$rowAttributes = [];
foreach ($worksheetRow as $rowx) {
$row = $rowx->attributes();
if ($row !== null) {
if (isset($row['ht']) && !$readDataOnly) {
$rowAttributes[(int) $row['r']]['rowHeight'] = (float) $row['ht'];
}
if (isset($row['hidden']) && self::boolean($row['hidden'])) {
$rowAttributes[(int) $row['r']]['visible'] = false;
}
if (isset($row['collapsed']) && self::boolean($row['collapsed'])) {
$rowAttributes[(int) $row['r']]['collapsed'] = true;
}
if (isset($row['outlineLevel']) && (int) $row['outlineLevel'] > 0) {
$rowAttributes[(int) $row['r']]['outlineLevel'] = (int) $row['outlineLevel'];
}
if (isset($row['s']) && !$readDataOnly) {
$rowAttributes[(int) $row['r']]['xfIndex'] = (int) $row['s'];
}
}
}
return $rowAttributes;
}
}

View file

@ -0,0 +1,331 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles as StyleReader;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueObject;
use PhpOffice\PhpSpreadsheet\Style\Style as Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
use stdClass;
class ConditionalStyles
{
private Worksheet $worksheet;
private SimpleXMLElement $worksheetXml;
private array $ns;
private array $dxfs;
private StyleReader $styleReader;
public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs, StyleReader $styleReader)
{
$this->worksheet = $workSheet;
$this->worksheetXml = $worksheetXml;
$this->dxfs = $dxfs;
$this->styleReader = $styleReader;
}
public function load(): void
{
$selectedCells = $this->worksheet->getSelectedCells();
$this->setConditionalStyles(
$this->worksheet,
$this->readConditionalStyles($this->worksheetXml),
$this->worksheetXml->extLst
);
$this->worksheet->setSelectedCells($selectedCells);
}
public function loadFromExt(): void
{
$selectedCells = $this->worksheet->getSelectedCells();
$this->ns = $this->worksheetXml->getNamespaces(true);
$this->setConditionalsFromExt(
$this->readConditionalsFromExt($this->worksheetXml->extLst)
);
$this->worksheet->setSelectedCells($selectedCells);
}
private function setConditionalsFromExt(array $conditionals): void
{
foreach ($conditionals as $conditionalRange => $cfRules) {
ksort($cfRules);
// Priority is used as the key for sorting; but may not start at 0,
// so we use array_values to reset the index after sorting.
$this->worksheet->getStyle($conditionalRange)
->setConditionalStyles(array_values($cfRules));
}
}
private function readConditionalsFromExt(SimpleXMLElement $extLst): array
{
$conditionals = [];
if (!isset($extLst->ext)) {
return $conditionals;
}
foreach ($extLst->ext as $extlstcond) {
$extAttrs = $extlstcond->attributes() ?? [];
$extUri = (string) ($extAttrs['uri'] ?? '');
if ($extUri !== '{78C0D931-6437-407d-A8EE-F0AAD7539E65}') {
continue;
}
$conditionalFormattingRuleXml = $extlstcond->children($this->ns['x14']);
if (!$conditionalFormattingRuleXml->conditionalFormattings) {
return [];
}
foreach ($conditionalFormattingRuleXml->children($this->ns['x14']) as $extFormattingXml) {
$extFormattingRangeXml = $extFormattingXml->children($this->ns['xm']);
if (!$extFormattingRangeXml->sqref) {
continue;
}
$sqref = (string) $extFormattingRangeXml->sqref;
$extCfRuleXml = $extFormattingXml->cfRule;
$attributes = $extCfRuleXml->attributes();
if (!$attributes) {
continue;
}
$conditionType = (string) $attributes->type;
if (
!Conditional::isValidConditionType($conditionType)
|| $conditionType === Conditional::CONDITION_DATABAR
) {
continue;
}
$priority = (int) $attributes->priority;
$conditional = $this->readConditionalRuleFromExt($extCfRuleXml, $attributes);
$cfStyle = $this->readStyleFromExt($extCfRuleXml);
$conditional->setStyle($cfStyle);
$conditionals[$sqref][$priority] = $conditional;
}
}
return $conditionals;
}
private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleXMLElement $attributes): Conditional
{
$conditionType = (string) $attributes->type;
$operatorType = (string) $attributes->operator;
$operands = [];
foreach ($cfRuleXml->children($this->ns['xm']) as $cfRuleOperandsXml) {
$operands[] = (string) $cfRuleOperandsXml;
}
$conditional = new Conditional();
$conditional->setConditionType($conditionType);
$conditional->setOperatorType($operatorType);
if (
$conditionType === Conditional::CONDITION_CONTAINSTEXT
|| $conditionType === Conditional::CONDITION_NOTCONTAINSTEXT
|| $conditionType === Conditional::CONDITION_BEGINSWITH
|| $conditionType === Conditional::CONDITION_ENDSWITH
|| $conditionType === Conditional::CONDITION_TIMEPERIOD
) {
$conditional->setText(array_pop($operands) ?? '');
}
$conditional->setConditions($operands);
return $conditional;
}
private function readStyleFromExt(SimpleXMLElement $extCfRuleXml): Style
{
$cfStyle = new Style(false, true);
if ($extCfRuleXml->dxf) {
$styleXML = $extCfRuleXml->dxf->children();
if ($styleXML->borders) {
$this->styleReader->readBorderStyle($cfStyle->getBorders(), $styleXML->borders);
}
if ($styleXML->fill) {
$this->styleReader->readFillStyle($cfStyle->getFill(), $styleXML->fill);
}
}
return $cfStyle;
}
private function readConditionalStyles(SimpleXMLElement $xmlSheet): array
{
$conditionals = [];
foreach ($xmlSheet->conditionalFormatting as $conditional) {
foreach ($conditional->cfRule as $cfRule) {
if (Conditional::isValidConditionType((string) $cfRule['type']) && (!isset($cfRule['dxfId']) || isset($this->dxfs[(int) ($cfRule['dxfId'])]))) {
$conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule;
} elseif ((string) $cfRule['type'] == Conditional::CONDITION_DATABAR) {
$conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule;
}
}
}
return $conditionals;
}
private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void
{
foreach ($conditionals as $cellRangeReference => $cfRules) {
ksort($cfRules);
$conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst);
// Extract all cell references in $cellRangeReference
$cellBlocks = explode(' ', str_replace('$', '', strtoupper($cellRangeReference)));
foreach ($cellBlocks as $cellBlock) {
$worksheet->getStyle($cellBlock)->setConditionalStyles($conditionalStyles);
}
}
}
private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array
{
$conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst);
$conditionalStyles = [];
/** @var SimpleXMLElement $cfRule */
foreach ($cfRules as $cfRule) {
$objConditional = new Conditional();
$objConditional->setConditionType((string) $cfRule['type']);
$objConditional->setOperatorType((string) $cfRule['operator']);
$objConditional->setNoFormatSet(!isset($cfRule['dxfId']));
if ((string) $cfRule['text'] != '') {
$objConditional->setText((string) $cfRule['text']);
} elseif ((string) $cfRule['timePeriod'] != '') {
$objConditional->setText((string) $cfRule['timePeriod']);
}
if (isset($cfRule['stopIfTrue']) && (int) $cfRule['stopIfTrue'] === 1) {
$objConditional->setStopIfTrue(true);
}
if (count($cfRule->formula) >= 1) {
foreach ($cfRule->formula as $formulax) {
$formula = (string) $formulax;
if ($formula === 'TRUE') {
$objConditional->addCondition(true);
} elseif ($formula === 'FALSE') {
$objConditional->addCondition(false);
} else {
$objConditional->addCondition($formula);
}
}
} else {
$objConditional->addCondition('');
}
if (isset($cfRule->dataBar)) {
$objConditional->setDataBar(
$this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions)
);
} elseif (isset($cfRule->colorScale)) {
$objConditional->setColorScale(
$this->readColorScale($cfRule)
);
} elseif (isset($cfRule['dxfId'])) {
$objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]);
}
$conditionalStyles[] = $objConditional;
}
return $conditionalStyles;
}
private function readDataBarOfConditionalRule(SimpleXMLElement $cfRule, array $conditionalFormattingRuleExtensions): ConditionalDataBar
{
$dataBar = new ConditionalDataBar();
//dataBar attribute
if (isset($cfRule->dataBar['showValue'])) {
$dataBar->setShowValue((bool) $cfRule->dataBar['showValue']);
}
//dataBar children
//conditionalFormatValueObjects
$cfvoXml = $cfRule->dataBar->cfvo;
$cfvoIndex = 0;
foreach ((count($cfvoXml) > 1 ? $cfvoXml : [$cfvoXml]) as $cfvo) {
if ($cfvoIndex === 0) {
$dataBar->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $cfvo['type'], (string) $cfvo['val']));
}
if ($cfvoIndex === 1) {
$dataBar->setMaximumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $cfvo['type'], (string) $cfvo['val']));
}
++$cfvoIndex;
}
//color
if (isset($cfRule->dataBar->color)) {
$dataBar->setColor($this->styleReader->readColor($cfRule->dataBar->color));
}
//extLst
$this->readDataBarExtLstOfConditionalRule($dataBar, $cfRule, $conditionalFormattingRuleExtensions);
return $dataBar;
}
private function readColorScale(simpleXMLElement|stdClass $cfRule): ConditionalColorScale
{
$colorScale = new ConditionalColorScale();
$types = [];
foreach ($cfRule->colorScale->cfvo as $cfvoXml) {
$attr = $cfvoXml->attributes() ?? [];
$type = (string) ($attr['type'] ?? '');
$types[] = $type;
$val = $attr['val'] ?? null;
if ($type === 'min') {
$colorScale->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject($type, $val));
} elseif ($type === 'percentile') {
$colorScale->setMidpointConditionalFormatValueObject(new ConditionalFormatValueObject($type, $val));
} elseif ($type === 'max') {
$colorScale->setMaximumConditionalFormatValueObject(new ConditionalFormatValueObject($type, $val));
}
}
$idx = 0;
foreach ($cfRule->colorScale->color as $color) {
$type = $types[$idx];
$rgb = $this->styleReader->readColor($color);
if ($type === 'min') {
$colorScale->setMinimumColor(new Color($rgb));
} elseif ($type === 'percentile') {
$colorScale->setMidpointColor(new Color($rgb));
} elseif ($type === 'max') {
$colorScale->setMaximumColor(new Color($rgb));
}
++$idx;
}
return $colorScale;
}
private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, SimpleXMLElement $cfRule, array $conditionalFormattingRuleExtensions): void
{
if (isset($cfRule->extLst)) {
$ns = $cfRule->extLst->getNamespaces(true);
foreach ((count($cfRule->extLst) > 0 ? $cfRule->extLst->ext : [$cfRule->extLst->ext]) as $ext) {
$extId = (string) $ext->children($ns['x14'])->id;
if (isset($conditionalFormattingRuleExtensions[$extId]) && (string) $ext['uri'] === '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}') {
$dataBar->setConditionalFormattingRuleExt($conditionalFormattingRuleExtensions[$extId]);
}
}
}
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
class DataValidations
{
private Worksheet $worksheet;
private SimpleXMLElement $worksheetXml;
public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml)
{
$this->worksheet = $workSheet;
$this->worksheetXml = $worksheetXml;
}
public function load(): void
{
foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) {
// Uppercase coordinate
$range = strtoupper((string) $dataValidation['sqref']);
$rangeSet = explode(' ', $range);
foreach ($rangeSet as $range) {
if (preg_match('/^[A-Z]{1,3}\\d{1,7}/', $range, $matches) === 1) {
// Ensure left/top row of range exists, thereby
// adjusting high row/column.
$this->worksheet->getCell($matches[0]);
}
}
}
foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) {
// Uppercase coordinate
$range = strtoupper((string) $dataValidation['sqref']);
$rangeSet = explode(' ', $range);
foreach ($rangeSet as $range) {
$stRange = $this->worksheet->shrinkRangeToFit($range);
// Extract all cell references in $range
foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $reference) {
// Create validation
$docValidation = $this->worksheet->getCell($reference)->getDataValidation();
$docValidation->setType((string) $dataValidation['type']);
$docValidation->setErrorStyle((string) $dataValidation['errorStyle']);
$docValidation->setOperator((string) $dataValidation['operator']);
$docValidation->setAllowBlank(filter_var($dataValidation['allowBlank'], FILTER_VALIDATE_BOOLEAN));
// showDropDown is inverted (works as hideDropDown if true)
$docValidation->setShowDropDown(!filter_var($dataValidation['showDropDown'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowInputMessage(filter_var($dataValidation['showInputMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setShowErrorMessage(filter_var($dataValidation['showErrorMessage'], FILTER_VALIDATE_BOOLEAN));
$docValidation->setErrorTitle((string) $dataValidation['errorTitle']);
$docValidation->setError((string) $dataValidation['error']);
$docValidation->setPromptTitle((string) $dataValidation['promptTitle']);
$docValidation->setPrompt((string) $dataValidation['prompt']);
$docValidation->setFormula1((string) $dataValidation->formula1);
$docValidation->setFormula2((string) $dataValidation->formula2);
$docValidation->setSqref($range);
}
}
}
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
class Hyperlinks
{
private Worksheet $worksheet;
private array $hyperlinks = [];
public function __construct(Worksheet $workSheet)
{
$this->worksheet = $workSheet;
}
public function readHyperlinks(SimpleXMLElement $relsWorksheet): void
{
foreach ($relsWorksheet->children(Namespaces::RELATIONSHIPS)->Relationship as $elementx) {
$element = Xlsx::getAttributes($elementx);
if ($element->Type == Namespaces::HYPERLINK) {
$this->hyperlinks[(string) $element->Id] = (string) $element->Target;
}
}
}
public function setHyperlinks(SimpleXMLElement $worksheetXml): void
{
foreach ($worksheetXml->children(Namespaces::MAIN)->hyperlink as $hyperlink) {
if ($hyperlink !== null) {
$this->setHyperlink($hyperlink, $this->worksheet);
}
}
}
private function setHyperlink(SimpleXMLElement $hyperlink, Worksheet $worksheet): void
{
// Link url
$linkRel = Xlsx::getAttributes($hyperlink, Namespaces::SCHEMA_OFFICE_DOCUMENT);
$attributes = Xlsx::getAttributes($hyperlink);
foreach (Coordinate::extractAllCellReferencesInRange($attributes->ref) as $cellReference) {
$cell = $worksheet->getCell($cellReference);
if (isset($linkRel['id'])) {
$hyperlinkUrl = $this->hyperlinks[(string) $linkRel['id']] ?? null;
if (isset($attributes['location'])) {
$hyperlinkUrl .= '#' . (string) $attributes['location'];
}
$cell->getHyperlink()->setUrl($hyperlinkUrl);
} elseif (isset($attributes['location'])) {
$cell->getHyperlink()->setUrl('sheet://' . (string) $attributes['location']);
}
// Tooltip
if (isset($attributes['tooltip'])) {
$cell->getHyperlink()->setTooltip((string) $attributes['tooltip']);
}
}
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
class Namespaces
{
const SCHEMAS = 'http://schemas.openxmlformats.org';
const RELATIONSHIPS = 'http://schemas.openxmlformats.org/package/2006/relationships';
// This one used in Reader\Xlsx
const CORE_PROPERTIES = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties';
// This one used in Reader\Xlsx\Properties
const CORE_PROPERTIES2 = 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties';
const THUMBNAIL = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail';
const THEME = 'http://schemas.openxmlformats.org/package/2006/relationships/theme';
const THEME2 = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme';
const COMPATIBILITY = 'http://schemas.openxmlformats.org/markup-compatibility/2006';
const MAIN = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
const RELATIONSHIPS_DRAWING = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing';
const DRAWINGML = 'http://schemas.openxmlformats.org/drawingml/2006/main';
const CHART = 'http://schemas.openxmlformats.org/drawingml/2006/chart';
const CHART_ALTERNATE = 'http://schemas.microsoft.com/office/drawing/2007/8/2/chart';
const RELATIONSHIPS_CHART = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart';
const SPREADSHEET_DRAWING = 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing';
const SCHEMA_OFFICE_DOCUMENT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
const COMMENTS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments';
const RELATIONSHIPS_CUSTOM_PROPERTIES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties';
const RELATIONSHIPS_EXTENDED_PROPERTIES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties';
const RELATIONSHIPS_CTRLPROP = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/ctrlProp';
const CUSTOM_PROPERTIES = 'http://schemas.openxmlformats.org/officeDocument/2006/custom-properties';
const EXTENDED_PROPERTIES = 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties';
const PROPERTIES_VTYPES = 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes';
const HYPERLINK = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink';
const OFFICE_DOCUMENT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument';
const SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
const STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles';
const IMAGE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image';
const VML = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing';
const WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
const CHARTSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet';
const SCHEMA_MICROSOFT = 'http://schemas.microsoft.com/office/2006/relationships';
const EXTENSIBILITY = 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility';
const VBA = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject';
const VBA_SIGNATURE = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject';
const DATA_VALIDATIONS1 = 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main';
const DATA_VALIDATIONS2 = 'http://schemas.microsoft.com/office/excel/2006/main';
const CONTENT_TYPES = 'http://schemas.openxmlformats.org/package/2006/content-types';
const RELATIONSHIPS_PRINTER_SETTINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings';
const RELATIONSHIPS_TABLE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table';
const SPREADSHEETML_AC = 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac';
const DC_ELEMENTS = 'http://purl.org/dc/elements/1.1/';
const DC_TERMS = 'http://purl.org/dc/terms/';
const DC_DCMITYPE = 'http://purl.org/dc/dcmitype/';
const SCHEMA_INSTANCE = 'http://www.w3.org/2001/XMLSchema-instance';
const URN_EXCEL = 'urn:schemas-microsoft-com:office:excel';
const URN_MSOFFICE = 'urn:schemas-microsoft-com:office:office';
const URN_VML = 'urn:schemas-microsoft-com:vml';
const SCHEMA_PURL = 'http://purl.oclc.org/ooxml';
const PURL_OFFICE_DOCUMENT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument';
const PURL_RELATIONSHIPS = 'http://purl.oclc.org/ooxml/officeDocument/relationships';
const PURL_MAIN = 'http://purl.oclc.org/ooxml/spreadsheetml/main';
const PURL_DRAWING = 'http://purl.oclc.org/ooxml/drawingml/main';
const PURL_CHART = 'http://purl.oclc.org/ooxml/drawingml/chart';
const PURL_WORKSHEET = 'http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet';
}

View file

@ -0,0 +1,170 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
class PageSetup extends BaseParserClass
{
private Worksheet $worksheet;
private ?SimpleXMLElement $worksheetXml;
public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null)
{
$this->worksheet = $workSheet;
$this->worksheetXml = $worksheetXml;
}
public function load(array $unparsedLoadedData): array
{
$worksheetXml = $this->worksheetXml;
if ($worksheetXml === null) {
return $unparsedLoadedData;
}
$this->margins($worksheetXml, $this->worksheet);
$unparsedLoadedData = $this->pageSetup($worksheetXml, $this->worksheet, $unparsedLoadedData);
$this->headerFooter($worksheetXml, $this->worksheet);
$this->pageBreaks($worksheetXml, $this->worksheet);
return $unparsedLoadedData;
}
private function margins(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void
{
if ($xmlSheet->pageMargins) {
$docPageMargins = $worksheet->getPageMargins();
$docPageMargins->setLeft((float) ($xmlSheet->pageMargins['left']));
$docPageMargins->setRight((float) ($xmlSheet->pageMargins['right']));
$docPageMargins->setTop((float) ($xmlSheet->pageMargins['top']));
$docPageMargins->setBottom((float) ($xmlSheet->pageMargins['bottom']));
$docPageMargins->setHeader((float) ($xmlSheet->pageMargins['header']));
$docPageMargins->setFooter((float) ($xmlSheet->pageMargins['footer']));
}
}
private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData): array
{
if ($xmlSheet->pageSetup) {
$docPageSetup = $worksheet->getPageSetup();
if (isset($xmlSheet->pageSetup['orientation'])) {
$docPageSetup->setOrientation((string) $xmlSheet->pageSetup['orientation']);
}
if (isset($xmlSheet->pageSetup['paperSize'])) {
$docPageSetup->setPaperSize((int) ($xmlSheet->pageSetup['paperSize']));
}
if (isset($xmlSheet->pageSetup['scale'])) {
$docPageSetup->setScale((int) ($xmlSheet->pageSetup['scale']), false);
}
if (isset($xmlSheet->pageSetup['fitToHeight']) && (int) ($xmlSheet->pageSetup['fitToHeight']) >= 0) {
$docPageSetup->setFitToHeight((int) ($xmlSheet->pageSetup['fitToHeight']), false);
}
if (isset($xmlSheet->pageSetup['fitToWidth']) && (int) ($xmlSheet->pageSetup['fitToWidth']) >= 0) {
$docPageSetup->setFitToWidth((int) ($xmlSheet->pageSetup['fitToWidth']), false);
}
if (
isset($xmlSheet->pageSetup['firstPageNumber'], $xmlSheet->pageSetup['useFirstPageNumber'])
&& self::boolean((string) $xmlSheet->pageSetup['useFirstPageNumber'])
) {
$docPageSetup->setFirstPageNumber((int) ($xmlSheet->pageSetup['firstPageNumber']));
}
if (isset($xmlSheet->pageSetup['pageOrder'])) {
$docPageSetup->setPageOrder((string) $xmlSheet->pageSetup['pageOrder']);
}
$relAttributes = $xmlSheet->pageSetup->attributes(Namespaces::SCHEMA_OFFICE_DOCUMENT);
if (isset($relAttributes['id'])) {
$relid = (string) $relAttributes['id'];
if (!str_ends_with($relid, 'ps')) {
$relid .= 'ps';
}
$unparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId'] = $relid;
}
}
return $unparsedLoadedData;
}
private function headerFooter(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void
{
if ($xmlSheet->headerFooter) {
$docHeaderFooter = $worksheet->getHeaderFooter();
if (
isset($xmlSheet->headerFooter['differentOddEven'])
&& self::boolean((string) $xmlSheet->headerFooter['differentOddEven'])
) {
$docHeaderFooter->setDifferentOddEven(true);
} else {
$docHeaderFooter->setDifferentOddEven(false);
}
if (
isset($xmlSheet->headerFooter['differentFirst'])
&& self::boolean((string) $xmlSheet->headerFooter['differentFirst'])
) {
$docHeaderFooter->setDifferentFirst(true);
} else {
$docHeaderFooter->setDifferentFirst(false);
}
if (
isset($xmlSheet->headerFooter['scaleWithDoc'])
&& !self::boolean((string) $xmlSheet->headerFooter['scaleWithDoc'])
) {
$docHeaderFooter->setScaleWithDocument(false);
} else {
$docHeaderFooter->setScaleWithDocument(true);
}
if (
isset($xmlSheet->headerFooter['alignWithMargins'])
&& !self::boolean((string) $xmlSheet->headerFooter['alignWithMargins'])
) {
$docHeaderFooter->setAlignWithMargins(false);
} else {
$docHeaderFooter->setAlignWithMargins(true);
}
$docHeaderFooter->setOddHeader((string) $xmlSheet->headerFooter->oddHeader);
$docHeaderFooter->setOddFooter((string) $xmlSheet->headerFooter->oddFooter);
$docHeaderFooter->setEvenHeader((string) $xmlSheet->headerFooter->evenHeader);
$docHeaderFooter->setEvenFooter((string) $xmlSheet->headerFooter->evenFooter);
$docHeaderFooter->setFirstHeader((string) $xmlSheet->headerFooter->firstHeader);
$docHeaderFooter->setFirstFooter((string) $xmlSheet->headerFooter->firstFooter);
}
}
private function pageBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void
{
if ($xmlSheet->rowBreaks && $xmlSheet->rowBreaks->brk) {
$this->rowBreaks($xmlSheet, $worksheet);
}
if ($xmlSheet->colBreaks && $xmlSheet->colBreaks->brk) {
$this->columnBreaks($xmlSheet, $worksheet);
}
}
private function rowBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void
{
foreach ($xmlSheet->rowBreaks->brk as $brk) {
$rowBreakMax = isset($brk['max']) ? ((int) $brk['max']) : -1;
if ($brk['man']) {
$worksheet->setBreak("A{$brk['id']}", Worksheet::BREAK_ROW, $rowBreakMax);
}
}
}
private function columnBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void
{
foreach ($xmlSheet->colBreaks->brk as $brk) {
if ($brk['man']) {
$worksheet->setBreak(
Coordinate::stringFromColumnIndex(((int) $brk['id']) + 1) . '1',
Worksheet::BREAK_COLUMN
);
}
}
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Document\Properties as DocumentProperties;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\Settings;
use SimpleXMLElement;
class Properties
{
private XmlScanner $securityScanner;
private DocumentProperties $docProps;
public function __construct(XmlScanner $securityScanner, DocumentProperties $docProps)
{
$this->securityScanner = $securityScanner;
$this->docProps = $docProps;
}
private function extractPropertyData(string $propertyData): ?SimpleXMLElement
{
// okay to omit namespace because everything will be processed by xpath
$obj = simplexml_load_string(
$this->securityScanner->scan($propertyData),
'SimpleXMLElement',
Settings::getLibXmlLoaderOptions()
);
return $obj === false ? null : $obj;
}
public function readCoreProperties(string $propertyData): void
{
$xmlCore = $this->extractPropertyData($propertyData);
if (is_object($xmlCore)) {
$xmlCore->registerXPathNamespace('dc', Namespaces::DC_ELEMENTS);
$xmlCore->registerXPathNamespace('dcterms', Namespaces::DC_TERMS);
$xmlCore->registerXPathNamespace('cp', Namespaces::CORE_PROPERTIES2);
$this->docProps->setCreator($this->getArrayItem($xmlCore->xpath('dc:creator')));
$this->docProps->setLastModifiedBy($this->getArrayItem($xmlCore->xpath('cp:lastModifiedBy')));
$this->docProps->setCreated($this->getArrayItem($xmlCore->xpath('dcterms:created'))); //! respect xsi:type
$this->docProps->setModified($this->getArrayItem($xmlCore->xpath('dcterms:modified'))); //! respect xsi:type
$this->docProps->setTitle($this->getArrayItem($xmlCore->xpath('dc:title')));
$this->docProps->setDescription($this->getArrayItem($xmlCore->xpath('dc:description')));
$this->docProps->setSubject($this->getArrayItem($xmlCore->xpath('dc:subject')));
$this->docProps->setKeywords($this->getArrayItem($xmlCore->xpath('cp:keywords')));
$this->docProps->setCategory($this->getArrayItem($xmlCore->xpath('cp:category')));
}
}
public function readExtendedProperties(string $propertyData): void
{
$xmlCore = $this->extractPropertyData($propertyData);
if (is_object($xmlCore)) {
if (isset($xmlCore->Company)) {
$this->docProps->setCompany((string) $xmlCore->Company);
}
if (isset($xmlCore->Manager)) {
$this->docProps->setManager((string) $xmlCore->Manager);
}
if (isset($xmlCore->HyperlinkBase)) {
$this->docProps->setHyperlinkBase((string) $xmlCore->HyperlinkBase);
}
}
}
public function readCustomProperties(string $propertyData): void
{
$xmlCore = $this->extractPropertyData($propertyData);
if (is_object($xmlCore)) {
foreach ($xmlCore as $xmlProperty) {
/** @var SimpleXMLElement $xmlProperty */
$cellDataOfficeAttributes = $xmlProperty->attributes();
if (isset($cellDataOfficeAttributes['name'])) {
$propertyName = (string) $cellDataOfficeAttributes['name'];
$cellDataOfficeChildren = $xmlProperty->children('http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes');
$attributeType = $cellDataOfficeChildren->getName();
$attributeValue = (string) $cellDataOfficeChildren->{$attributeType};
$attributeValue = DocumentProperties::convertProperty($attributeValue, $attributeType);
$attributeType = DocumentProperties::convertPropertyType($attributeType);
$this->docProps->setCustomProperty($propertyName, $attributeValue, $attributeType);
}
}
}
}
private function getArrayItem(null|array|false $array): string
{
return is_array($array) ? (string) ($array[0] ?? '') : '';
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
class SharedFormula
{
private string $master;
private string $formula;
public function __construct(string $master, string $formula)
{
$this->master = $master;
$this->formula = $formula;
}
public function master(): string
{
return $this->master;
}
public function formula(): string
{
return $this->formula;
}
}

View file

@ -0,0 +1,138 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
class SheetViewOptions extends BaseParserClass
{
private Worksheet $worksheet;
private ?SimpleXMLElement $worksheetXml;
public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null)
{
$this->worksheet = $workSheet;
$this->worksheetXml = $worksheetXml;
}
public function load(bool $readDataOnly, Styles $styleReader): void
{
if ($this->worksheetXml === null) {
return;
}
if (isset($this->worksheetXml->sheetPr)) {
$sheetPr = $this->worksheetXml->sheetPr;
$this->tabColor($sheetPr, $styleReader);
$this->codeName($sheetPr);
$this->outlines($sheetPr);
$this->pageSetup($sheetPr);
}
if (isset($this->worksheetXml->sheetFormatPr)) {
$this->sheetFormat($this->worksheetXml->sheetFormatPr);
}
if (!$readDataOnly && isset($this->worksheetXml->printOptions)) {
$this->printOptions($this->worksheetXml->printOptions);
}
}
private function tabColor(SimpleXMLElement $sheetPr, Styles $styleReader): void
{
if (isset($sheetPr->tabColor)) {
$this->worksheet->getTabColor()->setARGB($styleReader->readColor($sheetPr->tabColor));
}
}
private function codeName(SimpleXMLElement $sheetPrx): void
{
$sheetPr = $sheetPrx->attributes() ?? [];
if (isset($sheetPr['codeName'])) {
$this->worksheet->setCodeName((string) $sheetPr['codeName'], false);
}
}
private function outlines(SimpleXMLElement $sheetPr): void
{
if (isset($sheetPr->outlinePr)) {
$attr = $sheetPr->outlinePr->attributes() ?? [];
if (
isset($attr['summaryRight'])
&& !self::boolean((string) $attr['summaryRight'])
) {
$this->worksheet->setShowSummaryRight(false);
} else {
$this->worksheet->setShowSummaryRight(true);
}
if (
isset($attr['summaryBelow'])
&& !self::boolean((string) $attr['summaryBelow'])
) {
$this->worksheet->setShowSummaryBelow(false);
} else {
$this->worksheet->setShowSummaryBelow(true);
}
}
}
private function pageSetup(SimpleXMLElement $sheetPr): void
{
if (isset($sheetPr->pageSetUpPr)) {
$attr = $sheetPr->pageSetUpPr->attributes() ?? [];
if (
isset($attr['fitToPage'])
&& !self::boolean((string) $attr['fitToPage'])
) {
$this->worksheet->getPageSetup()->setFitToPage(false);
} else {
$this->worksheet->getPageSetup()->setFitToPage(true);
}
}
}
private function sheetFormat(SimpleXMLElement $sheetFormatPrx): void
{
$sheetFormatPr = $sheetFormatPrx->attributes() ?? [];
if (
isset($sheetFormatPr['customHeight'])
&& self::boolean((string) $sheetFormatPr['customHeight'])
&& isset($sheetFormatPr['defaultRowHeight'])
) {
$this->worksheet->getDefaultRowDimension()
->setRowHeight((float) $sheetFormatPr['defaultRowHeight']);
}
if (isset($sheetFormatPr['defaultColWidth'])) {
$this->worksheet->getDefaultColumnDimension()
->setWidth((float) $sheetFormatPr['defaultColWidth']);
}
if (
isset($sheetFormatPr['zeroHeight'])
&& ((string) $sheetFormatPr['zeroHeight'] === '1')
) {
$this->worksheet->getDefaultRowDimension()->setZeroHeight(true);
}
}
private function printOptions(SimpleXMLElement $printOptionsx): void
{
$printOptions = $printOptionsx->attributes() ?? [];
if (isset($printOptions['gridLinesSet']) && self::boolean((string) $printOptions['gridLinesSet'])) {
$this->worksheet->setShowGridlines(true);
}
if (isset($printOptions['gridLines']) && self::boolean((string) $printOptions['gridLines'])) {
$this->worksheet->setPrintGridlines(true);
}
if (isset($printOptions['horizontalCentered']) && self::boolean((string) $printOptions['horizontalCentered'])) {
$this->worksheet->getPageSetup()->setHorizontalCentered(true);
}
if (isset($printOptions['verticalCentered']) && self::boolean((string) $printOptions['verticalCentered'])) {
$this->worksheet->getPageSetup()->setVerticalCentered(true);
}
}
}

View file

@ -0,0 +1,185 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\Pane;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
class SheetViews extends BaseParserClass
{
private SimpleXMLElement $sheetViewXml;
private SimpleXMLElement $sheetViewAttributes;
private Worksheet $worksheet;
private string $activePane = '';
public function __construct(SimpleXMLElement $sheetViewXml, Worksheet $workSheet)
{
$this->sheetViewXml = $sheetViewXml;
$this->sheetViewAttributes = Xlsx::testSimpleXml($sheetViewXml->attributes());
$this->worksheet = $workSheet;
}
public function load(): void
{
$this->topLeft();
$this->zoomScale();
$this->view();
$this->gridLines();
$this->headers();
$this->direction();
$this->showZeros();
$usesPanes = false;
if (isset($this->sheetViewXml->pane)) {
$this->pane();
$usesPanes = true;
}
if (isset($this->sheetViewXml->selection)) {
foreach ($this->sheetViewXml->selection as $selection) {
$this->selection($selection, $usesPanes);
}
}
}
private function zoomScale(): void
{
if (isset($this->sheetViewAttributes->zoomScale)) {
$zoomScale = (int) ($this->sheetViewAttributes->zoomScale);
if ($zoomScale <= 0) {
// setZoomScale will throw an Exception if the scale is less than or equals 0
// that is OK when manually creating documents, but we should be able to read all documents
$zoomScale = 100;
}
$this->worksheet->getSheetView()->setZoomScale($zoomScale);
}
if (isset($this->sheetViewAttributes->zoomScaleNormal)) {
$zoomScaleNormal = (int) ($this->sheetViewAttributes->zoomScaleNormal);
if ($zoomScaleNormal <= 0) {
// setZoomScaleNormal will throw an Exception if the scale is less than or equals 0
// that is OK when manually creating documents, but we should be able to read all documents
$zoomScaleNormal = 100;
}
$this->worksheet->getSheetView()->setZoomScaleNormal($zoomScaleNormal);
}
}
private function view(): void
{
if (isset($this->sheetViewAttributes->view)) {
$this->worksheet->getSheetView()->setView((string) $this->sheetViewAttributes->view);
}
}
private function topLeft(): void
{
if (isset($this->sheetViewAttributes->topLeftCell)) {
$this->worksheet->setTopLeftCell($this->sheetViewAttributes->topLeftCell);
}
}
private function gridLines(): void
{
if (isset($this->sheetViewAttributes->showGridLines)) {
$this->worksheet->setShowGridLines(
self::boolean((string) $this->sheetViewAttributes->showGridLines)
);
}
}
private function headers(): void
{
if (isset($this->sheetViewAttributes->showRowColHeaders)) {
$this->worksheet->setShowRowColHeaders(
self::boolean((string) $this->sheetViewAttributes->showRowColHeaders)
);
}
}
private function direction(): void
{
if (isset($this->sheetViewAttributes->rightToLeft)) {
$this->worksheet->setRightToLeft(
self::boolean((string) $this->sheetViewAttributes->rightToLeft)
);
}
}
private function showZeros(): void
{
if (isset($this->sheetViewAttributes->showZeros)) {
$this->worksheet->getSheetView()->setShowZeros(
self::boolean((string) $this->sheetViewAttributes->showZeros)
);
}
}
private function pane(): void
{
$xSplit = 0;
$ySplit = 0;
$topLeftCell = null;
$paneAttributes = $this->sheetViewXml->pane->attributes();
if (isset($paneAttributes->xSplit)) {
$xSplit = (int) ($paneAttributes->xSplit);
$this->worksheet->setXSplit($xSplit);
}
if (isset($paneAttributes->ySplit)) {
$ySplit = (int) ($paneAttributes->ySplit);
$this->worksheet->setYSplit($ySplit);
}
$paneState = isset($paneAttributes->state) ? ((string) $paneAttributes->state) : '';
$this->worksheet->setPaneState($paneState);
if (isset($paneAttributes->topLeftCell)) {
$topLeftCell = (string) $paneAttributes->topLeftCell;
$this->worksheet->setPaneTopLeftCell($topLeftCell);
if ($paneState === Worksheet::PANE_FROZEN) {
$this->worksheet->setTopLeftCell($topLeftCell);
}
}
$activePane = isset($paneAttributes->activePane) ? ((string) $paneAttributes->activePane) : 'topLeft';
$this->worksheet->setActivePane($activePane);
$this->activePane = $activePane;
if ($paneState === Worksheet::PANE_FROZEN || $paneState === Worksheet::PANE_FROZENSPLIT) {
$this->worksheet->freezePane(
Coordinate::stringFromColumnIndex($xSplit + 1) . ($ySplit + 1),
$topLeftCell,
$paneState === Worksheet::PANE_FROZENSPLIT
);
}
}
private function selection(?SimpleXMLElement $selection, bool $usesPanes): void
{
$attributes = ($selection === null) ? null : $selection->attributes();
if ($attributes !== null) {
$position = (string) $attributes->pane;
if ($usesPanes && $position === '') {
$position = 'topLeft';
}
$activeCell = (string) $attributes->activeCell;
$sqref = (string) $attributes->sqref;
$sqref = explode(' ', $sqref);
$sqref = $sqref[0];
if ($position === '') {
$this->worksheet->setSelectedCells($sqref);
} else {
$pane = new Pane($position, $sqref, $activeCell);
$this->worksheet->setPane($position, $pane);
if ($position === $this->activePane && $sqref !== '') {
$this->worksheet->setSelectedCells($sqref);
}
}
}
}
}

View file

@ -0,0 +1,436 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Borders;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Style\Protection;
use PhpOffice\PhpSpreadsheet\Style\Style;
use SimpleXMLElement;
use stdClass;
class Styles extends BaseParserClass
{
/**
* Theme instance.
*
* @var ?Theme
*/
private ?Theme $theme = null;
private array $workbookPalette = [];
private array $styles = [];
private array $cellStyles = [];
private SimpleXMLElement $styleXml;
private string $namespace = '';
public function setNamespace(string $namespace): void
{
$this->namespace = $namespace;
}
public function setWorkbookPalette(array $palette): void
{
$this->workbookPalette = $palette;
}
private function getStyleAttributes(SimpleXMLElement $value): SimpleXMLElement
{
$attr = $value->attributes('');
if ($attr === null || count($attr) === 0) {
$attr = $value->attributes($this->namespace);
}
return Xlsx::testSimpleXml($attr);
}
public function setStyleXml(SimpleXmlElement $styleXml): void
{
$this->styleXml = $styleXml;
}
public function setTheme(Theme $theme): void
{
$this->theme = $theme;
}
public function setStyleBaseData(?Theme $theme = null, array $styles = [], array $cellStyles = []): void
{
$this->theme = $theme;
$this->styles = $styles;
$this->cellStyles = $cellStyles;
}
public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void
{
if (isset($fontStyleXml->name)) {
$attr = $this->getStyleAttributes($fontStyleXml->name);
if (isset($attr['val'])) {
$fontStyle->setName((string) $attr['val']);
}
}
if (isset($fontStyleXml->sz)) {
$attr = $this->getStyleAttributes($fontStyleXml->sz);
if (isset($attr['val'])) {
$fontStyle->setSize((float) $attr['val']);
}
}
if (isset($fontStyleXml->b)) {
$attr = $this->getStyleAttributes($fontStyleXml->b);
$fontStyle->setBold(!isset($attr['val']) || self::boolean((string) $attr['val']));
}
if (isset($fontStyleXml->i)) {
$attr = $this->getStyleAttributes($fontStyleXml->i);
$fontStyle->setItalic(!isset($attr['val']) || self::boolean((string) $attr['val']));
}
if (isset($fontStyleXml->strike)) {
$attr = $this->getStyleAttributes($fontStyleXml->strike);
$fontStyle->setStrikethrough(!isset($attr['val']) || self::boolean((string) $attr['val']));
}
$fontStyle->getColor()->setARGB($this->readColor($fontStyleXml->color));
if (isset($fontStyleXml->u)) {
$attr = $this->getStyleAttributes($fontStyleXml->u);
if (!isset($attr['val'])) {
$fontStyle->setUnderline(Font::UNDERLINE_SINGLE);
} else {
$fontStyle->setUnderline((string) $attr['val']);
}
}
if (isset($fontStyleXml->vertAlign)) {
$attr = $this->getStyleAttributes($fontStyleXml->vertAlign);
if (isset($attr['val'])) {
$verticalAlign = strtolower((string) $attr['val']);
if ($verticalAlign === 'superscript') {
$fontStyle->setSuperscript(true);
} elseif ($verticalAlign === 'subscript') {
$fontStyle->setSubscript(true);
}
}
}
if (isset($fontStyleXml->scheme)) {
$attr = $this->getStyleAttributes($fontStyleXml->scheme);
$fontStyle->setScheme((string) $attr['val']);
}
}
private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void
{
if ((string) $numfmtStyleXml['formatCode'] !== '') {
$numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmtStyleXml['formatCode']));
return;
}
$numfmt = $this->getStyleAttributes($numfmtStyleXml);
if (isset($numfmt['formatCode'])) {
$numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmt['formatCode']));
}
}
public function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyleXml): void
{
if ($fillStyleXml->gradientFill) {
/** @var SimpleXMLElement $gradientFill */
$gradientFill = $fillStyleXml->gradientFill[0];
$attr = $this->getStyleAttributes($gradientFill);
if (!empty($attr['type'])) {
$fillStyle->setFillType((string) $attr['type']);
}
$fillStyle->setRotation((float) ($attr['degree']));
$gradientFill->registerXPathNamespace('sml', Namespaces::MAIN);
$fillStyle->getStartColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color));
$fillStyle->getEndColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color));
} elseif ($fillStyleXml->patternFill) {
$defaultFillStyle = Fill::FILL_NONE;
if ($fillStyleXml->patternFill->fgColor) {
$fillStyle->getStartColor()->setARGB($this->readColor($fillStyleXml->patternFill->fgColor, true));
$defaultFillStyle = Fill::FILL_SOLID;
}
if ($fillStyleXml->patternFill->bgColor) {
$fillStyle->getEndColor()->setARGB($this->readColor($fillStyleXml->patternFill->bgColor, true));
$defaultFillStyle = Fill::FILL_SOLID;
}
$type = '';
if ((string) $fillStyleXml->patternFill['patternType'] !== '') {
$type = (string) $fillStyleXml->patternFill['patternType'];
} else {
$attr = $this->getStyleAttributes($fillStyleXml->patternFill);
$type = (string) $attr['patternType'];
}
$patternType = ($type === '') ? $defaultFillStyle : $type;
$fillStyle->setFillType($patternType);
}
}
public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderStyleXml): void
{
$diagonalUp = $this->getAttribute($borderStyleXml, 'diagonalUp');
$diagonalUp = self::boolean($diagonalUp);
$diagonalDown = $this->getAttribute($borderStyleXml, 'diagonalDown');
$diagonalDown = self::boolean($diagonalDown);
if ($diagonalUp === false) {
if ($diagonalDown === false) {
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_NONE);
} else {
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_DOWN);
}
} elseif ($diagonalDown === false) {
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_UP);
} else {
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_BOTH);
}
if (isset($borderStyleXml->left)) {
$this->readBorder($borderStyle->getLeft(), $borderStyleXml->left);
}
if (isset($borderStyleXml->right)) {
$this->readBorder($borderStyle->getRight(), $borderStyleXml->right);
}
if (isset($borderStyleXml->top)) {
$this->readBorder($borderStyle->getTop(), $borderStyleXml->top);
}
if (isset($borderStyleXml->bottom)) {
$this->readBorder($borderStyle->getBottom(), $borderStyleXml->bottom);
}
if (isset($borderStyleXml->diagonal)) {
$this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal);
}
}
private function getAttribute(SimpleXMLElement $xml, string $attribute): string
{
$style = '';
if ((string) $xml[$attribute] !== '') {
$style = (string) $xml[$attribute];
} else {
$attr = $this->getStyleAttributes($xml);
if (isset($attr[$attribute])) {
$style = (string) $attr[$attribute];
}
}
return $style;
}
private function readBorder(Border $border, SimpleXMLElement $borderXml): void
{
$style = $this->getAttribute($borderXml, 'style');
if ($style !== '') {
$border->setBorderStyle((string) $style);
} else {
$border->setBorderStyle(Border::BORDER_NONE);
}
if (isset($borderXml->color)) {
$border->getColor()->setARGB($this->readColor($borderXml->color));
}
}
public function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void
{
$horizontal = (string) $this->getAttribute($alignmentXml, 'horizontal');
if ($horizontal !== '') {
$alignment->setHorizontal($horizontal);
}
$vertical = (string) $this->getAttribute($alignmentXml, 'vertical');
if ($vertical !== '') {
$alignment->setVertical($vertical);
}
$textRotation = (int) $this->getAttribute($alignmentXml, 'textRotation');
if ($textRotation > 90) {
$textRotation = 90 - $textRotation;
}
$alignment->setTextRotation($textRotation);
$wrapText = $this->getAttribute($alignmentXml, 'wrapText');
$alignment->setWrapText(self::boolean((string) $wrapText));
$shrinkToFit = $this->getAttribute($alignmentXml, 'shrinkToFit');
$alignment->setShrinkToFit(self::boolean((string) $shrinkToFit));
$indent = (int) $this->getAttribute($alignmentXml, 'indent');
$alignment->setIndent(max($indent, 0));
$readingOrder = (int) $this->getAttribute($alignmentXml, 'readingOrder');
$alignment->setReadOrder(max($readingOrder, 0));
}
private static function formatGeneral(string $formatString): string
{
if ($formatString === 'GENERAL') {
$formatString = NumberFormat::FORMAT_GENERAL;
}
return $formatString;
}
/**
* Read style.
*/
public function readStyle(Style $docStyle, SimpleXMLElement|stdClass $style): void
{
if ($style instanceof SimpleXMLElement) {
$this->readNumberFormat($docStyle->getNumberFormat(), $style->numFmt);
} else {
$docStyle->getNumberFormat()->setFormatCode(self::formatGeneral((string) $style->numFmt));
}
if (isset($style->font)) {
$this->readFontStyle($docStyle->getFont(), $style->font);
}
if (isset($style->fill)) {
$this->readFillStyle($docStyle->getFill(), $style->fill);
}
if (isset($style->border)) {
$this->readBorderStyle($docStyle->getBorders(), $style->border);
}
if (isset($style->alignment)) {
$this->readAlignmentStyle($docStyle->getAlignment(), $style->alignment);
}
// protection
if (isset($style->protection)) {
$this->readProtectionLocked($docStyle, $style->protection);
$this->readProtectionHidden($docStyle, $style->protection);
}
// top-level style settings
if (isset($style->quotePrefix)) {
$docStyle->setQuotePrefix((bool) $style->quotePrefix);
}
}
/**
* Read protection locked attribute.
*/
public function readProtectionLocked(Style $docStyle, SimpleXMLElement $style): void
{
$locked = '';
if ((string) $style['locked'] !== '') {
$locked = (string) $style['locked'];
} else {
$attr = $this->getStyleAttributes($style);
if (isset($attr['locked'])) {
$locked = (string) $attr['locked'];
}
}
if ($locked !== '') {
if (self::boolean($locked)) {
$docStyle->getProtection()->setLocked(Protection::PROTECTION_PROTECTED);
} else {
$docStyle->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED);
}
}
}
/**
* Read protection hidden attribute.
*/
public function readProtectionHidden(Style $docStyle, SimpleXMLElement $style): void
{
$hidden = '';
if ((string) $style['hidden'] !== '') {
$hidden = (string) $style['hidden'];
} else {
$attr = $this->getStyleAttributes($style);
if (isset($attr['hidden'])) {
$hidden = (string) $attr['hidden'];
}
}
if ($hidden !== '') {
if (self::boolean((string) $hidden)) {
$docStyle->getProtection()->setHidden(Protection::PROTECTION_PROTECTED);
} else {
$docStyle->getProtection()->setHidden(Protection::PROTECTION_UNPROTECTED);
}
}
}
public function readColor(SimpleXMLElement $color, bool $background = false): string
{
$attr = $this->getStyleAttributes($color);
if (isset($attr['rgb'])) {
return (string) $attr['rgb'];
}
if (isset($attr['indexed'])) {
$indexedColor = (int) $attr['indexed'];
if ($indexedColor >= count($this->workbookPalette)) {
return Color::indexedColor($indexedColor - 7, $background)->getARGB() ?? '';
}
return Color::indexedColor($indexedColor, $background, $this->workbookPalette)->getARGB() ?? '';
}
if (isset($attr['theme'])) {
if ($this->theme !== null) {
$returnColour = $this->theme->getColourByIndex((int) $attr['theme']);
if (isset($attr['tint'])) {
$tintAdjust = (float) $attr['tint'];
$returnColour = Color::changeBrightness($returnColour ?? '', $tintAdjust);
}
return 'FF' . $returnColour;
}
}
return ($background) ? 'FFFFFFFF' : 'FF000000';
}
public function dxfs(bool $readDataOnly = false): array
{
$dxfs = [];
if (!$readDataOnly && $this->styleXml) {
// Conditional Styles
if ($this->styleXml->dxfs) {
foreach ($this->styleXml->dxfs->dxf as $dxf) {
$style = new Style(false, true);
$this->readStyle($style, $dxf);
$dxfs[] = $style;
}
}
// Cell Styles
if ($this->styleXml->cellStyles) {
foreach ($this->styleXml->cellStyles->cellStyle as $cellStylex) {
$cellStyle = Xlsx::getAttributes($cellStylex);
if ((int) ($cellStyle['builtinId']) == 0) {
if (isset($this->cellStyles[(int) ($cellStyle['xfId'])])) {
// Set default style
$style = new Style();
$this->readStyle($style, $this->cellStyles[(int) ($cellStyle['xfId'])]);
// normal style, currently not using it for anything
}
}
}
}
}
return $dxfs;
}
public function styles(): array
{
return $this->styles;
}
/**
* Get array item.
*
* @param mixed $array (usually array, in theory can be false)
*/
private static function getArrayItem(mixed $array, int $key = 0): ?SimpleXMLElement
{
return is_array($array) ? ($array[$key] ?? null) : null;
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
class TableReader
{
private Worksheet $worksheet;
private SimpleXMLElement $tableXml;
/** @var array|SimpleXMLElement */
private $tableAttributes;
public function __construct(Worksheet $workSheet, SimpleXMLElement $tableXml)
{
$this->worksheet = $workSheet;
$this->tableXml = $tableXml;
}
/**
* Loads Table into the Worksheet.
*/
public function load(): void
{
$this->tableAttributes = $this->tableXml->attributes() ?? [];
// Remove all "$" in the table range
$tableRange = (string) preg_replace('/\$/', '', $this->tableAttributes['ref'] ?? '');
if (str_contains($tableRange, ':')) {
$this->readTable($tableRange);
}
}
/**
* Read Table from xml.
*/
private function readTable(string $tableRange): void
{
$table = new Table($tableRange);
$table->setName((string) ($this->tableAttributes['displayName'] ?? ''));
$table->setShowHeaderRow(((string) ($this->tableAttributes['headerRowCount'] ?? '')) !== '0');
$table->setShowTotalsRow(((string) ($this->tableAttributes['totalsRowCount'] ?? '')) === '1');
$this->readTableAutoFilter($table, $this->tableXml->autoFilter);
$this->readTableColumns($table, $this->tableXml->tableColumns);
$this->readTableStyle($table, $this->tableXml->tableStyleInfo);
(new AutoFilter($table, $this->tableXml))->load();
$this->worksheet->addTable($table);
}
/**
* Reads TableAutoFilter from xml.
*/
private function readTableAutoFilter(Table $table, SimpleXMLElement $autoFilterXml): void
{
if ($autoFilterXml->filterColumn === null) {
$table->setAllowFilter(false);
return;
}
foreach ($autoFilterXml->filterColumn as $filterColumn) {
$attributes = $filterColumn->attributes() ?? ['colId' => 0, 'hiddenButton' => 0];
$column = $table->getColumnByOffset((int) $attributes['colId']);
$column->setShowFilterButton(((string) $attributes['hiddenButton']) !== '1');
}
}
/**
* Reads TableColumns from xml.
*/
private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXml): void
{
$offset = 0;
foreach ($tableColumnsXml->tableColumn as $tableColumn) {
$attributes = $tableColumn->attributes() ?? ['totalsRowLabel' => 0, 'totalsRowFunction' => 0];
$column = $table->getColumnByOffset($offset++);
if ($table->getShowTotalsRow()) {
if ($attributes['totalsRowLabel']) {
$column->setTotalsRowLabel((string) $attributes['totalsRowLabel']);
}
if ($attributes['totalsRowFunction']) {
$column->setTotalsRowFunction((string) $attributes['totalsRowFunction']);
}
}
if ($tableColumn->calculatedColumnFormula) {
$column->setColumnFormula((string) $tableColumn->calculatedColumnFormula);
}
}
}
/**
* Reads TableStyle from xml.
*/
private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml): void
{
$tableStyle = new TableStyle();
$attributes = $tableStyleInfoXml->attributes();
if ($attributes !== null) {
$tableStyle->setTheme((string) $attributes['name']);
$tableStyle->setShowRowStripes((string) $attributes['showRowStripes'] === '1');
$tableStyle->setShowColumnStripes((string) $attributes['showColumnStripes'] === '1');
$tableStyle->setShowFirstColumn((string) $attributes['showFirstColumn'] === '1');
$tableStyle->setShowLastColumn((string) $attributes['showLastColumn'] === '1');
}
$table->setStyle($tableStyle);
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
class Theme
{
/**
* Theme Name.
*/
private string $themeName;
/**
* Colour Scheme Name.
*/
private string $colourSchemeName;
/**
* Colour Map.
*
* @var string[]
*/
private array $colourMap;
/**
* Create a new Theme.
*
* @param string[] $colourMap
*/
public function __construct(string $themeName, string $colourSchemeName, array $colourMap)
{
// Initialise values
$this->themeName = $themeName;
$this->colourSchemeName = $colourSchemeName;
$this->colourMap = $colourMap;
}
/**
* Not called by Reader, never accessible any other time.
*
* @codeCoverageIgnore
*/
public function getThemeName(): string
{
return $this->themeName;
}
/**
* Not called by Reader, never accessible any other time.
*
* @codeCoverageIgnore
*/
public function getColourSchemeName(): string
{
return $this->colourSchemeName;
}
/**
* Get colour Map Value by Position.
*/
public function getColourByIndex(int $index): ?string
{
return $this->colourMap[$index] ?? null;
}
}

View file

@ -0,0 +1,141 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use SimpleXMLElement;
class WorkbookView
{
private Spreadsheet $spreadsheet;
public function __construct(Spreadsheet $spreadsheet)
{
$this->spreadsheet = $spreadsheet;
}
public function viewSettings(SimpleXMLElement $xmlWorkbook, mixed $mainNS, array $mapSheetId, bool $readDataOnly): void
{
// Default active sheet index to the first loaded worksheet from the file
$this->spreadsheet->setActiveSheetIndex(0);
$workbookView = $xmlWorkbook->children($mainNS)->bookViews->workbookView;
if ($readDataOnly !== true && !empty($workbookView)) {
$workbookViewAttributes = self::testSimpleXml(self::getAttributes($workbookView));
// active sheet index
$activeTab = (int) $workbookViewAttributes->activeTab; // refers to old sheet index
// keep active sheet index if sheet is still loaded, else first sheet is set as the active worksheet
if (isset($mapSheetId[$activeTab]) && $mapSheetId[$activeTab] !== null) {
$this->spreadsheet->setActiveSheetIndex($mapSheetId[$activeTab]);
}
$this->horizontalScroll($workbookViewAttributes);
$this->verticalScroll($workbookViewAttributes);
$this->sheetTabs($workbookViewAttributes);
$this->minimized($workbookViewAttributes);
$this->autoFilterDateGrouping($workbookViewAttributes);
$this->firstSheet($workbookViewAttributes);
$this->visibility($workbookViewAttributes);
$this->tabRatio($workbookViewAttributes);
}
}
public static function testSimpleXml(mixed $value): SimpleXMLElement
{
return ($value instanceof SimpleXMLElement)
? $value
: new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><root></root>');
}
public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement
{
return self::testSimpleXml($value === null ? $value : $value->attributes($ns));
}
/**
* Convert an 'xsd:boolean' XML value to a PHP boolean value.
* A valid 'xsd:boolean' XML value can be one of the following
* four values: 'true', 'false', '1', '0'. It is case sensitive.
*
* Note that just doing '(bool) $xsdBoolean' is not safe,
* since '(bool) "false"' returns true.
*
* @see https://www.w3.org/TR/xmlschema11-2/#boolean
*
* @param string $xsdBoolean An XML string value of type 'xsd:boolean'
*
* @return bool Boolean value
*/
private function castXsdBooleanToBool(string $xsdBoolean): bool
{
if ($xsdBoolean === 'false') {
return false;
}
return (bool) $xsdBoolean;
}
private function horizontalScroll(SimpleXMLElement $workbookViewAttributes): void
{
if (isset($workbookViewAttributes->showHorizontalScroll)) {
$showHorizontalScroll = (string) $workbookViewAttributes->showHorizontalScroll;
$this->spreadsheet->setShowHorizontalScroll($this->castXsdBooleanToBool($showHorizontalScroll));
}
}
private function verticalScroll(SimpleXMLElement $workbookViewAttributes): void
{
if (isset($workbookViewAttributes->showVerticalScroll)) {
$showVerticalScroll = (string) $workbookViewAttributes->showVerticalScroll;
$this->spreadsheet->setShowVerticalScroll($this->castXsdBooleanToBool($showVerticalScroll));
}
}
private function sheetTabs(SimpleXMLElement $workbookViewAttributes): void
{
if (isset($workbookViewAttributes->showSheetTabs)) {
$showSheetTabs = (string) $workbookViewAttributes->showSheetTabs;
$this->spreadsheet->setShowSheetTabs($this->castXsdBooleanToBool($showSheetTabs));
}
}
private function minimized(SimpleXMLElement $workbookViewAttributes): void
{
if (isset($workbookViewAttributes->minimized)) {
$minimized = (string) $workbookViewAttributes->minimized;
$this->spreadsheet->setMinimized($this->castXsdBooleanToBool($minimized));
}
}
private function autoFilterDateGrouping(SimpleXMLElement $workbookViewAttributes): void
{
if (isset($workbookViewAttributes->autoFilterDateGrouping)) {
$autoFilterDateGrouping = (string) $workbookViewAttributes->autoFilterDateGrouping;
$this->spreadsheet->setAutoFilterDateGrouping($this->castXsdBooleanToBool($autoFilterDateGrouping));
}
}
private function firstSheet(SimpleXMLElement $workbookViewAttributes): void
{
if (isset($workbookViewAttributes->firstSheet)) {
$firstSheet = (string) $workbookViewAttributes->firstSheet;
$this->spreadsheet->setFirstSheetIndex((int) $firstSheet);
}
}
private function visibility(SimpleXMLElement $workbookViewAttributes): void
{
if (isset($workbookViewAttributes->visibility)) {
$visibility = (string) $workbookViewAttributes->visibility;
$this->spreadsheet->setVisibility($visibility);
}
}
private function tabRatio(SimpleXMLElement $workbookViewAttributes): void
{
if (isset($workbookViewAttributes->tabRatio)) {
$tabRatio = (string) $workbookViewAttributes->tabRatio;
$this->spreadsheet->setTabRatio((int) $tabRatio);
}
}
}

View file

@ -0,0 +1,653 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader;
use DateTime;
use DateTimeZone;
use PhpOffice\PhpSpreadsheet\Cell\AddressHelper;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\DefinedName;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Reader\Xml\PageSettings;
use PhpOffice\PhpSpreadsheet\Reader\Xml\Properties;
use PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
/**
* Reader for SpreadsheetML, the XML schema for Microsoft Office Excel 2003.
*/
class Xml extends BaseReader
{
public const NAMESPACES_SS = 'urn:schemas-microsoft-com:office:spreadsheet';
/**
* Formats.
*/
protected array $styles = [];
/**
* Create a new Excel2003XML Reader instance.
*/
public function __construct()
{
parent::__construct();
$this->securityScanner = XmlScanner::getInstance($this);
}
private string $fileContents = '';
public static function xmlMappings(): array
{
return array_merge(
Style\Fill::FILL_MAPPINGS,
Style\Border::BORDER_MAPPINGS
);
}
/**
* Can the current IReader read the file?
*/
public function canRead(string $filename): bool
{
// Office xmlns:o="urn:schemas-microsoft-com:office:office"
// Excel xmlns:x="urn:schemas-microsoft-com:office:excel"
// XML Spreadsheet xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
// Spreadsheet component xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet"
// XML schema xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
// XML data type xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
// MS-persist recordset xmlns:rs="urn:schemas-microsoft-com:rowset"
// Rowset xmlns:z="#RowsetSchema"
//
$signature = [
'<?xml version="1.0"',
'xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet',
];
// Open file
$data = file_get_contents($filename) ?: '';
// Why?
//$data = str_replace("'", '"', $data); // fix headers with single quote
$valid = true;
foreach ($signature as $match) {
// every part of the signature must be present
if (!str_contains($data, $match)) {
$valid = false;
break;
}
}
// Retrieve charset encoding
if (preg_match('/<?xml.*encoding=[\'"](.*?)[\'"].*?>/m', $data, $matches)) {
$charSet = strtoupper($matches[1]);
if (preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet) === 1) {
$data = StringHelper::convertEncoding($data, 'UTF-8', $charSet);
$data = (string) preg_replace('/(<?xml.*encoding=[\'"]).*?([\'"].*?>)/um', '$1' . 'UTF-8' . '$2', $data, 1);
}
}
$this->fileContents = $data;
return $valid;
}
/**
* Check if the file is a valid SimpleXML.
*
* @return false|SimpleXMLElement
*/
public function trySimpleXMLLoadString(string $filename): SimpleXMLElement|bool
{
try {
$xml = simplexml_load_string(
$this->getSecurityScannerOrThrow()->scan($this->fileContents ?: file_get_contents($filename)),
'SimpleXMLElement',
Settings::getLibXmlLoaderOptions()
);
} catch (\Exception $e) {
throw new Exception('Cannot load invalid XML file: ' . $filename, 0, $e);
}
$this->fileContents = '';
return $xml;
}
/**
* Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
*/
public function listWorksheetNames(string $filename): array
{
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an Invalid Spreadsheet file.');
}
$worksheetNames = [];
$xml = $this->trySimpleXMLLoadString($filename);
if ($xml === false) {
throw new Exception("Problem reading {$filename}");
}
$xml_ss = $xml->children(self::NAMESPACES_SS);
foreach ($xml_ss->Worksheet as $worksheet) {
$worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
$worksheetNames[] = (string) $worksheet_ss['Name'];
}
return $worksheetNames;
}
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*/
public function listWorksheetInfo(string $filename): array
{
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an Invalid Spreadsheet file.');
}
$worksheetInfo = [];
$xml = $this->trySimpleXMLLoadString($filename);
if ($xml === false) {
throw new Exception("Problem reading {$filename}");
}
$worksheetID = 1;
$xml_ss = $xml->children(self::NAMESPACES_SS);
foreach ($xml_ss->Worksheet as $worksheet) {
$worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
$tmpInfo = [];
$tmpInfo['worksheetName'] = '';
$tmpInfo['lastColumnLetter'] = 'A';
$tmpInfo['lastColumnIndex'] = 0;
$tmpInfo['totalRows'] = 0;
$tmpInfo['totalColumns'] = 0;
$tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}";
if (isset($worksheet_ss['Name'])) {
$tmpInfo['worksheetName'] = (string) $worksheet_ss['Name'];
}
if (isset($worksheet->Table->Row)) {
$rowIndex = 0;
foreach ($worksheet->Table->Row as $rowData) {
$columnIndex = 0;
$rowHasData = false;
foreach ($rowData->Cell as $cell) {
if (isset($cell->Data)) {
$tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
$rowHasData = true;
}
++$columnIndex;
}
++$rowIndex;
if ($rowHasData) {
$tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
}
}
}
$tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
$tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
$worksheetInfo[] = $tmpInfo;
++$worksheetID;
}
return $worksheetInfo;
}
/**
* Loads Spreadsheet from string.
*/
public function loadSpreadsheetFromString(string $contents): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet->removeSheetByIndex(0);
// Load into this instance
return $this->loadIntoExisting($contents, $spreadsheet, true);
}
/**
* Loads Spreadsheet from file.
*/
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet->removeSheetByIndex(0);
// Load into this instance
return $this->loadIntoExisting($filename, $spreadsheet);
}
/**
* Loads from file or contents into Spreadsheet instance.
*
* @param string $filename file name if useContents is false else file contents
*/
public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, bool $useContents = false): Spreadsheet
{
if ($useContents) {
$this->fileContents = $filename;
} else {
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an Invalid Spreadsheet file.');
}
}
$xml = $this->trySimpleXMLLoadString($filename);
if ($xml === false) {
throw new Exception("Problem reading {$filename}");
}
$namespaces = $xml->getNamespaces(true);
(new Properties($spreadsheet))->readProperties($xml, $namespaces);
$this->styles = (new Style())->parseStyles($xml, $namespaces);
if (isset($this->styles['Default'])) {
$spreadsheet->getCellXfCollection()[0]->applyFromArray($this->styles['Default']);
}
$worksheetID = 0;
$xml_ss = $xml->children(self::NAMESPACES_SS);
/** @var null|SimpleXMLElement $worksheetx */
foreach ($xml_ss->Worksheet as $worksheetx) {
$worksheet = $worksheetx ?? new SimpleXMLElement('<xml></xml>');
$worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
if (
isset($this->loadSheetsOnly, $worksheet_ss['Name'])
&& (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly))
) {
continue;
}
// Create new Worksheet
$spreadsheet->createSheet();
$spreadsheet->setActiveSheetIndex($worksheetID);
$worksheetName = '';
if (isset($worksheet_ss['Name'])) {
$worksheetName = (string) $worksheet_ss['Name'];
// 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
$spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
}
if (isset($worksheet_ss['Protected'])) {
$protection = (string) $worksheet_ss['Protected'] === '1';
$spreadsheet->getActiveSheet()->getProtection()->setSheet($protection);
}
// locally scoped defined names
if (isset($worksheet->Names[0])) {
foreach ($worksheet->Names[0] as $definedName) {
$definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS);
$name = (string) $definedName_ss['Name'];
$definedValue = (string) $definedName_ss['RefersTo'];
$convertedValue = AddressHelper::convertFormulaToA1($definedValue);
if ($convertedValue[0] === '=') {
$convertedValue = substr($convertedValue, 1);
}
$spreadsheet->addDefinedName(DefinedName::createInstance($name, $spreadsheet->getActiveSheet(), $convertedValue, true));
}
}
$columnID = 'A';
if (isset($worksheet->Table->Column)) {
foreach ($worksheet->Table->Column as $columnData) {
$columnData_ss = self::getAttributes($columnData, self::NAMESPACES_SS);
$colspan = 0;
if (isset($columnData_ss['Span'])) {
$spanAttr = (string) $columnData_ss['Span'];
if (is_numeric($spanAttr)) {
$colspan = max(0, (int) $spanAttr);
}
}
if (isset($columnData_ss['Index'])) {
$columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']);
}
$columnWidth = null;
if (isset($columnData_ss['Width'])) {
$columnWidth = $columnData_ss['Width'];
}
$columnVisible = null;
if (isset($columnData_ss['Hidden'])) {
$columnVisible = ((string) $columnData_ss['Hidden']) !== '1';
}
while ($colspan >= 0) {
if (isset($columnWidth)) {
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4);
}
if (isset($columnVisible)) {
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setVisible($columnVisible);
}
++$columnID;
--$colspan;
}
}
}
$rowID = 1;
if (isset($worksheet->Table->Row)) {
$additionalMergedCells = 0;
foreach ($worksheet->Table->Row as $rowData) {
$rowHasData = false;
$row_ss = self::getAttributes($rowData, self::NAMESPACES_SS);
if (isset($row_ss['Index'])) {
$rowID = (int) $row_ss['Index'];
}
if (isset($row_ss['Hidden'])) {
$rowVisible = ((string) $row_ss['Hidden']) !== '1';
$spreadsheet->getActiveSheet()->getRowDimension($rowID)->setVisible($rowVisible);
}
$columnID = 'A';
foreach ($rowData->Cell as $cell) {
$cell_ss = self::getAttributes($cell, self::NAMESPACES_SS);
if (isset($cell_ss['Index'])) {
$columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']);
}
$cellRange = $columnID . $rowID;
if ($this->getReadFilter() !== null) {
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
++$columnID;
continue;
}
}
if (isset($cell_ss['HRef'])) {
$spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl((string) $cell_ss['HRef']);
}
if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) {
$columnTo = $columnID;
if (isset($cell_ss['MergeAcross'])) {
$additionalMergedCells += (int) $cell_ss['MergeAcross'];
$columnTo = Coordinate::stringFromColumnIndex((int) (Coordinate::columnIndexFromString($columnID) + $cell_ss['MergeAcross']));
}
$rowTo = $rowID;
if (isset($cell_ss['MergeDown'])) {
$rowTo = $rowTo + $cell_ss['MergeDown'];
}
$cellRange .= ':' . $columnTo . $rowTo;
$spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE);
}
$hasCalculatedValue = false;
$cellDataFormula = '';
if (isset($cell_ss['Formula'])) {
$cellDataFormula = $cell_ss['Formula'];
$hasCalculatedValue = true;
}
if (isset($cell->Data)) {
$cellData = $cell->Data;
$cellValue = (string) $cellData;
$type = DataType::TYPE_NULL;
$cellData_ss = self::getAttributes($cellData, self::NAMESPACES_SS);
if (isset($cellData_ss['Type'])) {
$cellDataType = $cellData_ss['Type'];
switch ($cellDataType) {
/*
const TYPE_STRING = 's';
const TYPE_FORMULA = 'f';
const TYPE_NUMERIC = 'n';
const TYPE_BOOL = 'b';
const TYPE_NULL = 'null';
const TYPE_INLINE = 'inlineStr';
const TYPE_ERROR = 'e';
*/
case 'String':
$type = DataType::TYPE_STRING;
break;
case 'Number':
$type = DataType::TYPE_NUMERIC;
$cellValue = (float) $cellValue;
if (floor($cellValue) == $cellValue) {
$cellValue = (int) $cellValue;
}
break;
case 'Boolean':
$type = DataType::TYPE_BOOL;
$cellValue = ($cellValue != 0);
break;
case 'DateTime':
$type = DataType::TYPE_NUMERIC;
$dateTime = new DateTime($cellValue, new DateTimeZone('UTC'));
$cellValue = Date::PHPToExcel($dateTime);
break;
case 'Error':
$type = DataType::TYPE_ERROR;
$hasCalculatedValue = false;
break;
}
}
$originalType = $type;
if ($hasCalculatedValue) {
$type = DataType::TYPE_FORMULA;
$columnNumber = Coordinate::columnIndexFromString($columnID);
$cellDataFormula = AddressHelper::convertFormulaToA1($cellDataFormula, $rowID, $columnNumber);
}
$spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $cellValue), $type);
if ($hasCalculatedValue) {
$spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue, $originalType === DataType::TYPE_NUMERIC);
}
$rowHasData = true;
}
if (isset($cell->Comment)) {
$this->parseCellComment($cell->Comment, $spreadsheet, $columnID, $rowID);
}
if (isset($cell_ss['StyleID'])) {
$style = (string) $cell_ss['StyleID'];
if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) {
//if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) {
// $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null);
//}
$spreadsheet->getActiveSheet()->getStyle($cellRange)
->applyFromArray($this->styles[$style]);
}
}
++$columnID;
while ($additionalMergedCells > 0) {
++$columnID;
--$additionalMergedCells;
}
}
if ($rowHasData) {
if (isset($row_ss['Height'])) {
$rowHeight = $row_ss['Height'];
$spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight((float) $rowHeight);
}
}
++$rowID;
}
}
$dataValidations = new Xml\DataValidations();
$dataValidations->loadDataValidations($worksheet, $spreadsheet);
$xmlX = $worksheet->children(Namespaces::URN_EXCEL);
if (isset($xmlX->WorksheetOptions)) {
if (isset($xmlX->WorksheetOptions->FreezePanes)) {
$freezeRow = $freezeColumn = 1;
if (isset($xmlX->WorksheetOptions->SplitHorizontal)) {
$freezeRow = (int) $xmlX->WorksheetOptions->SplitHorizontal + 1;
}
if (isset($xmlX->WorksheetOptions->SplitVertical)) {
$freezeColumn = (int) $xmlX->WorksheetOptions->SplitVertical + 1;
}
$leftTopRow = (string) $xmlX->WorksheetOptions->TopRowBottomPane;
$leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnRightPane;
if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) {
$leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1);
$spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, $leftTopCoordinate, !isset($xmlX->WorksheetOptions->FrozenNoSplit));
} else {
$spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, null, !isset($xmlX->WorksheetOptions->FrozenNoSplit));
}
} elseif (isset($xmlX->WorksheetOptions->SplitVertical) || isset($xmlX->WorksheetOptions->SplitHorizontal)) {
if (isset($xmlX->WorksheetOptions->SplitHorizontal)) {
$ySplit = (int) $xmlX->WorksheetOptions->SplitHorizontal;
$spreadsheet->getActiveSheet()->setYSplit($ySplit);
}
if (isset($xmlX->WorksheetOptions->SplitVertical)) {
$xSplit = (int) $xmlX->WorksheetOptions->SplitVertical;
$spreadsheet->getActiveSheet()->setXSplit($xSplit);
}
if (isset($xmlX->WorksheetOptions->LeftColumnVisible) || isset($xmlX->WorksheetOptions->TopRowVisible)) {
$leftTopColumn = $leftTopRow = 1;
if (isset($xmlX->WorksheetOptions->LeftColumnVisible)) {
$leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnVisible;
}
if (isset($xmlX->WorksheetOptions->TopRowVisible)) {
$leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowVisible;
}
$leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow";
$spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate);
}
$leftTopColumn = $leftTopRow = 1;
if (isset($xmlX->WorksheetOptions->LeftColumnRightPane)) {
$leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnRightPane;
}
if (isset($xmlX->WorksheetOptions->TopRowBottomPane)) {
$leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowBottomPane;
}
$leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow";
$spreadsheet->getActiveSheet()->setPaneTopLeftCell($leftTopCoordinate);
}
(new PageSettings($xmlX))->loadPageSettings($spreadsheet);
if (isset($xmlX->WorksheetOptions->TopRowVisible, $xmlX->WorksheetOptions->LeftColumnVisible)) {
$leftTopRow = (string) $xmlX->WorksheetOptions->TopRowVisible;
$leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnVisible;
if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) {
$leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1);
$spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate);
}
}
$rangeCalculated = false;
if (isset($xmlX->WorksheetOptions->Panes->Pane->RangeSelection)) {
if (1 === preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $xmlX->WorksheetOptions->Panes->Pane->RangeSelection, $selectionMatches)) {
$selectedCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
. $selectionMatches[1]
. ':'
. Coordinate::stringFromColumnIndex((int) $selectionMatches[4])
. $selectionMatches[3];
$spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
$rangeCalculated = true;
}
}
if (!$rangeCalculated) {
if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveRow)) {
$activeRow = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveRow;
} else {
$activeRow = 0;
}
if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveCol)) {
$activeColumn = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveCol;
} else {
$activeColumn = 0;
}
if (is_numeric($activeRow) && is_numeric($activeColumn)) {
$selectedCell = Coordinate::stringFromColumnIndex((int) $activeColumn + 1) . (string) ($activeRow + 1);
$spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
}
}
}
++$worksheetID;
}
// Globally scoped defined names
$activeSheetIndex = 0;
if (isset($xml->ExcelWorkbook->ActiveSheet)) {
$activeSheetIndex = (int) (string) $xml->ExcelWorkbook->ActiveSheet;
}
$activeWorksheet = $spreadsheet->setActiveSheetIndex($activeSheetIndex);
if (isset($xml->Names[0])) {
foreach ($xml->Names[0] as $definedName) {
$definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS);
$name = (string) $definedName_ss['Name'];
$definedValue = (string) $definedName_ss['RefersTo'];
$convertedValue = AddressHelper::convertFormulaToA1($definedValue);
if ($convertedValue[0] === '=') {
$convertedValue = substr($convertedValue, 1);
}
$spreadsheet->addDefinedName(DefinedName::createInstance($name, $activeWorksheet, $convertedValue));
}
}
// Return
return $spreadsheet;
}
protected function parseCellComment(
SimpleXMLElement $comment,
Spreadsheet $spreadsheet,
string $columnID,
int $rowID
): void {
$commentAttributes = $comment->attributes(self::NAMESPACES_SS);
$author = 'unknown';
if (isset($commentAttributes->Author)) {
$author = (string) $commentAttributes->Author;
}
$node = $comment->Data->asXML();
$annotation = strip_tags((string) $node);
$spreadsheet->getActiveSheet()->getComment($columnID . $rowID)
->setAuthor($author)
->setText($this->parseRichText($annotation));
}
protected function parseRichText(string $annotation): RichText
{
$value = new RichText();
$value->createText($annotation);
return $value;
}
private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement
{
return ($simple === null)
? new SimpleXMLElement('<xml></xml>')
: ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>'));
}
}

View file

@ -0,0 +1,177 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml;
use PhpOffice\PhpSpreadsheet\Cell\AddressHelper;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use SimpleXMLElement;
class DataValidations
{
private const OPERATOR_MAPPINGS = [
'between' => DataValidation::OPERATOR_BETWEEN,
'equal' => DataValidation::OPERATOR_EQUAL,
'greater' => DataValidation::OPERATOR_GREATERTHAN,
'greaterorequal' => DataValidation::OPERATOR_GREATERTHANOREQUAL,
'less' => DataValidation::OPERATOR_LESSTHAN,
'lessorequal' => DataValidation::OPERATOR_LESSTHANOREQUAL,
'notbetween' => DataValidation::OPERATOR_NOTBETWEEN,
'notequal' => DataValidation::OPERATOR_NOTEQUAL,
];
private const TYPE_MAPPINGS = [
'textlength' => DataValidation::TYPE_TEXTLENGTH,
];
private int $thisRow = 0;
private int $thisColumn = 0;
private function replaceR1C1(array $matches): string
{
return AddressHelper::convertToA1($matches[0], $this->thisRow, $this->thisColumn, false);
}
public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $spreadsheet): void
{
$xmlX = $worksheet->children(Namespaces::URN_EXCEL);
$sheet = $spreadsheet->getActiveSheet();
/** @var callable $pregCallback */
$pregCallback = [$this, 'replaceR1C1'];
foreach ($xmlX->DataValidation as $dataValidation) {
$cells = [];
$validation = new DataValidation();
// set defaults
$validation->setShowDropDown(true);
$validation->setShowInputMessage(true);
$validation->setShowErrorMessage(true);
$validation->setShowDropDown(true);
$this->thisRow = 1;
$this->thisColumn = 1;
foreach ($dataValidation as $tagName => $tagValue) {
$tagValue = (string) $tagValue;
$tagValueLower = strtolower($tagValue);
switch ($tagName) {
case 'Range':
foreach (explode(',', $tagValue) as $range) {
$cell = '';
if (preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) {
// range
$firstCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
. $selectionMatches[1];
$cell = $firstCell
. ':'
. Coordinate::stringFromColumnIndex((int) $selectionMatches[4])
. $selectionMatches[3];
$this->thisRow = (int) $selectionMatches[1];
$this->thisColumn = (int) $selectionMatches[2];
$sheet->getCell($firstCell);
} elseif (preg_match('/^R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) {
// cell
$cell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
. $selectionMatches[1];
$sheet->getCell($cell);
$this->thisRow = (int) $selectionMatches[1];
$this->thisColumn = (int) $selectionMatches[2];
} elseif (preg_match('/^C(\d+)$/', (string) $range, $selectionMatches) === 1) {
// column
$firstCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[1])
. '1';
$cell = $firstCell
. ':'
. Coordinate::stringFromColumnIndex((int) $selectionMatches[1])
. ((string) AddressRange::MAX_ROW);
$this->thisColumn = (int) $selectionMatches[1];
$sheet->getCell($firstCell);
} elseif (preg_match('/^R(\d+)$/', (string) $range, $selectionMatches)) {
// row
$firstCell = 'A'
. $selectionMatches[1];
$cell = $firstCell
. ':'
. AddressRange::MAX_COLUMN
. $selectionMatches[1];
$this->thisRow = (int) $selectionMatches[1];
$sheet->getCell($firstCell);
}
$validation->setSqref($cell);
$stRange = $sheet->shrinkRangeToFit($cell);
$cells = array_merge($cells, Coordinate::extractAllCellReferencesInRange($stRange));
}
break;
case 'Type':
$validation->setType(self::TYPE_MAPPINGS[$tagValueLower] ?? $tagValueLower);
break;
case 'Qualifier':
$validation->setOperator(self::OPERATOR_MAPPINGS[$tagValueLower] ?? $tagValueLower);
break;
case 'InputTitle':
$validation->setPromptTitle($tagValue);
break;
case 'InputMessage':
$validation->setPrompt($tagValue);
break;
case 'InputHide':
$validation->setShowInputMessage(false);
break;
case 'ErrorStyle':
$validation->setErrorStyle($tagValueLower);
break;
case 'ErrorTitle':
$validation->setErrorTitle($tagValue);
break;
case 'ErrorMessage':
$validation->setError($tagValue);
break;
case 'ErrorHide':
$validation->setShowErrorMessage(false);
break;
case 'ComboHide':
$validation->setShowDropDown(false);
break;
case 'UseBlank':
$validation->setAllowBlank(true);
break;
case 'CellRangeList':
// FIXME missing FIXME
break;
case 'Min':
case 'Value':
$tagValue = (string) preg_replace_callback(AddressHelper::R1C1_COORDINATE_REGEX, $pregCallback, $tagValue);
$validation->setFormula1($tagValue);
break;
case 'Max':
$tagValue = (string) preg_replace_callback(AddressHelper::R1C1_COORDINATE_REGEX, $pregCallback, $tagValue);
$validation->setFormula2($tagValue);
break;
}
}
foreach ($cells as $cell) {
$sheet->getCell($cell)->setDataValidation(clone $validation);
}
}
}
}

View file

@ -0,0 +1,130 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use SimpleXMLElement;
use stdClass;
class PageSettings
{
private stdClass $printSettings;
public function __construct(SimpleXMLElement $xmlX)
{
$printSettings = $this->pageSetup($xmlX, $this->getPrintDefaults());
$this->printSettings = $this->printSetup($xmlX, $printSettings);
}
public function loadPageSettings(Spreadsheet $spreadsheet): void
{
$spreadsheet->getActiveSheet()->getPageSetup()
->setPaperSize($this->printSettings->paperSize)
->setOrientation($this->printSettings->orientation)
->setScale($this->printSettings->scale)
->setVerticalCentered($this->printSettings->verticalCentered)
->setHorizontalCentered($this->printSettings->horizontalCentered)
->setPageOrder($this->printSettings->printOrder);
$spreadsheet->getActiveSheet()->getPageMargins()
->setTop($this->printSettings->topMargin)
->setHeader($this->printSettings->headerMargin)
->setLeft($this->printSettings->leftMargin)
->setRight($this->printSettings->rightMargin)
->setBottom($this->printSettings->bottomMargin)
->setFooter($this->printSettings->footerMargin);
}
private function getPrintDefaults(): stdClass
{
return (object) [
'paperSize' => 9,
'orientation' => PageSetup::ORIENTATION_DEFAULT,
'scale' => 100,
'horizontalCentered' => false,
'verticalCentered' => false,
'printOrder' => PageSetup::PAGEORDER_DOWN_THEN_OVER,
'topMargin' => 0.75,
'headerMargin' => 0.3,
'leftMargin' => 0.7,
'rightMargin' => 0.7,
'bottomMargin' => 0.75,
'footerMargin' => 0.3,
];
}
private function pageSetup(SimpleXMLElement $xmlX, stdClass $printDefaults): stdClass
{
if (isset($xmlX->WorksheetOptions->PageSetup)) {
foreach ($xmlX->WorksheetOptions->PageSetup as $pageSetupData) {
foreach ($pageSetupData as $pageSetupKey => $pageSetupValue) {
$pageSetupAttributes = $pageSetupValue->attributes(Namespaces::URN_EXCEL);
if ($pageSetupAttributes !== null) {
switch ($pageSetupKey) {
case 'Layout':
$this->setLayout($printDefaults, $pageSetupAttributes);
break;
case 'Header':
$printDefaults->headerMargin = (float) $pageSetupAttributes->Margin ?: 1.0;
break;
case 'Footer':
$printDefaults->footerMargin = (float) $pageSetupAttributes->Margin ?: 1.0;
break;
case 'PageMargins':
$this->setMargins($printDefaults, $pageSetupAttributes);
break;
}
}
}
}
}
return $printDefaults;
}
private function printSetup(SimpleXMLElement $xmlX, stdClass $printDefaults): stdClass
{
if (isset($xmlX->WorksheetOptions->Print)) {
foreach ($xmlX->WorksheetOptions->Print as $printData) {
foreach ($printData as $printKey => $printValue) {
switch ($printKey) {
case 'LeftToRight':
$printDefaults->printOrder = PageSetup::PAGEORDER_OVER_THEN_DOWN;
break;
case 'PaperSizeIndex':
$printDefaults->paperSize = (int) $printValue ?: 9;
break;
case 'Scale':
$printDefaults->scale = (int) $printValue ?: 100;
break;
}
}
}
}
return $printDefaults;
}
private function setLayout(stdClass $printDefaults, SimpleXMLElement $pageSetupAttributes): void
{
$printDefaults->orientation = (string) strtolower($pageSetupAttributes->Orientation ?? '') ?: PageSetup::ORIENTATION_PORTRAIT;
$printDefaults->horizontalCentered = (bool) $pageSetupAttributes->CenterHorizontal ?: false;
$printDefaults->verticalCentered = (bool) $pageSetupAttributes->CenterVertical ?: false;
}
private function setMargins(stdClass $printDefaults, SimpleXMLElement $pageSetupAttributes): void
{
$printDefaults->leftMargin = (float) $pageSetupAttributes->Left ?: 1.0;
$printDefaults->rightMargin = (float) $pageSetupAttributes->Right ?: 1.0;
$printDefaults->topMargin = (float) $pageSetupAttributes->Top ?: 1.0;
$printDefaults->bottomMargin = (float) $pageSetupAttributes->Bottom ?: 1.0;
}
}

View file

@ -0,0 +1,155 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml;
use PhpOffice\PhpSpreadsheet\Document\Properties as DocumentProperties;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use SimpleXMLElement;
class Properties
{
protected Spreadsheet $spreadsheet;
public function __construct(Spreadsheet $spreadsheet)
{
$this->spreadsheet = $spreadsheet;
}
public function readProperties(SimpleXMLElement $xml, array $namespaces): void
{
$this->readStandardProperties($xml);
$this->readCustomProperties($xml, $namespaces);
}
protected function readStandardProperties(SimpleXMLElement $xml): void
{
if (isset($xml->DocumentProperties[0])) {
$docProps = $this->spreadsheet->getProperties();
foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) {
$propertyValue = (string) $propertyValue;
$this->processStandardProperty($docProps, $propertyName, $propertyValue);
}
}
}
protected function readCustomProperties(SimpleXMLElement $xml, array $namespaces): void
{
if (isset($xml->CustomDocumentProperties) && is_iterable($xml->CustomDocumentProperties[0])) {
$docProps = $this->spreadsheet->getProperties();
foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) {
$propertyAttributes = self::getAttributes($propertyValue, $namespaces['dt']);
$propertyName = (string) preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName);
$this->processCustomProperty($docProps, $propertyName, $propertyValue, $propertyAttributes);
}
}
}
protected function processStandardProperty(
DocumentProperties $docProps,
string $propertyName,
string $stringValue
): void {
switch ($propertyName) {
case 'Title':
$docProps->setTitle($stringValue);
break;
case 'Subject':
$docProps->setSubject($stringValue);
break;
case 'Author':
$docProps->setCreator($stringValue);
break;
case 'Created':
$docProps->setCreated($stringValue);
break;
case 'LastAuthor':
$docProps->setLastModifiedBy($stringValue);
break;
case 'LastSaved':
$docProps->setModified($stringValue);
break;
case 'Company':
$docProps->setCompany($stringValue);
break;
case 'Category':
$docProps->setCategory($stringValue);
break;
case 'Manager':
$docProps->setManager($stringValue);
break;
case 'HyperlinkBase':
$docProps->setHyperlinkBase($stringValue);
break;
case 'Keywords':
$docProps->setKeywords($stringValue);
break;
case 'Description':
$docProps->setDescription($stringValue);
break;
}
}
protected function processCustomProperty(
DocumentProperties $docProps,
string $propertyName,
?SimpleXMLElement $propertyValue,
SimpleXMLElement $propertyAttributes
): void {
switch ((string) $propertyAttributes) {
case 'boolean':
$propertyType = DocumentProperties::PROPERTY_TYPE_BOOLEAN;
$propertyValue = (bool) (string) $propertyValue;
break;
case 'integer':
$propertyType = DocumentProperties::PROPERTY_TYPE_INTEGER;
$propertyValue = (int) $propertyValue;
break;
case 'float':
$propertyType = DocumentProperties::PROPERTY_TYPE_FLOAT;
$propertyValue = (float) $propertyValue;
break;
case 'dateTime.tz':
case 'dateTime.iso8601tz':
$propertyType = DocumentProperties::PROPERTY_TYPE_DATE;
$propertyValue = trim((string) $propertyValue);
break;
default:
$propertyType = DocumentProperties::PROPERTY_TYPE_STRING;
$propertyValue = trim((string) $propertyValue);
break;
}
$docProps->setCustomProperty($propertyName, $propertyValue, $propertyType);
}
protected function hex2str(array $hex): string
{
return mb_chr((int) hexdec($hex[1]), 'UTF-8');
}
private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement
{
return ($simple === null) ? new SimpleXMLElement('<xml></xml>') : ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>'));
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml;
use PhpOffice\PhpSpreadsheet\Style\Protection;
use SimpleXMLElement;
class Style
{
/**
* Formats.
*/
protected array $styles = [];
public function parseStyles(SimpleXMLElement $xml, array $namespaces): array
{
if (!isset($xml->Styles) || !is_iterable($xml->Styles[0])) {
return [];
}
$alignmentStyleParser = new Style\Alignment();
$borderStyleParser = new Style\Border();
$fontStyleParser = new Style\Font();
$fillStyleParser = new Style\Fill();
$numberFormatStyleParser = new Style\NumberFormat();
foreach ($xml->Styles[0] as $style) {
$style_ss = self::getAttributes($style, $namespaces['ss']);
$styleID = (string) $style_ss['ID'];
$this->styles[$styleID] = $this->styles['Default'] ?? [];
$alignment = $border = $font = $fill = $numberFormat = $protection = [];
foreach ($style as $styleType => $styleDatax) {
$styleData = self::getSxml($styleDatax);
$styleAttributes = $styleData->attributes($namespaces['ss']);
switch ($styleType) {
case 'Alignment':
if ($styleAttributes) {
$alignment = $alignmentStyleParser->parseStyle($styleAttributes);
}
break;
case 'Borders':
$border = $borderStyleParser->parseStyle($styleData, $namespaces);
break;
case 'Font':
if ($styleAttributes) {
$font = $fontStyleParser->parseStyle($styleAttributes);
}
break;
case 'Interior':
if ($styleAttributes) {
$fill = $fillStyleParser->parseStyle($styleAttributes);
}
break;
case 'NumberFormat':
if ($styleAttributes) {
$numberFormat = $numberFormatStyleParser->parseStyle($styleAttributes);
}
break;
case 'Protection':
$locked = $hidden = null;
$styleAttributesP = $styleData->attributes($namespaces['x']);
if (isset($styleAttributes['Protected'])) {
$locked = ((bool) (string) $styleAttributes['Protected']) ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED;
}
if (isset($styleAttributesP['HideFormula'])) {
$hidden = ((bool) (string) $styleAttributesP['HideFormula']) ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED;
}
if ($locked !== null || $hidden !== null) {
$protection['protection'] = [];
if ($locked !== null) {
$protection['protection']['locked'] = $locked;
}
if ($hidden !== null) {
$protection['protection']['hidden'] = $hidden;
}
}
break;
}
}
$this->styles[$styleID] = array_merge($alignment, $border, $font, $fill, $numberFormat, $protection);
}
return $this->styles;
}
private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement
{
return ($simple === null) ? new SimpleXMLElement('<xml></xml>') : ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>'));
}
private static function getSxml(?SimpleXMLElement $simple): SimpleXMLElement
{
return ($simple !== null) ? $simple : new SimpleXMLElement('<xml></xml>');
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
use PhpOffice\PhpSpreadsheet\Style\Alignment as AlignmentStyles;
use SimpleXMLElement;
class Alignment extends StyleBase
{
protected const VERTICAL_ALIGNMENT_STYLES = [
AlignmentStyles::VERTICAL_BOTTOM,
AlignmentStyles::VERTICAL_TOP,
AlignmentStyles::VERTICAL_CENTER,
AlignmentStyles::VERTICAL_JUSTIFY,
];
protected const HORIZONTAL_ALIGNMENT_STYLES = [
AlignmentStyles::HORIZONTAL_GENERAL,
AlignmentStyles::HORIZONTAL_LEFT,
AlignmentStyles::HORIZONTAL_RIGHT,
AlignmentStyles::HORIZONTAL_CENTER,
AlignmentStyles::HORIZONTAL_CENTER_CONTINUOUS,
AlignmentStyles::HORIZONTAL_JUSTIFY,
];
public function parseStyle(SimpleXMLElement $styleAttributes): array
{
$style = [];
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
$styleAttributeValue = (string) $styleAttributeValue;
switch ($styleAttributeKey) {
case 'Vertical':
if (self::identifyFixedStyleValue(self::VERTICAL_ALIGNMENT_STYLES, $styleAttributeValue)) {
$style['alignment']['vertical'] = $styleAttributeValue;
}
break;
case 'Horizontal':
if (self::identifyFixedStyleValue(self::HORIZONTAL_ALIGNMENT_STYLES, $styleAttributeValue)) {
$style['alignment']['horizontal'] = $styleAttributeValue;
}
break;
case 'WrapText':
$style['alignment']['wrapText'] = true;
break;
case 'Rotate':
$style['alignment']['textRotation'] = $styleAttributeValue;
break;
}
}
return $style;
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
use PhpOffice\PhpSpreadsheet\Style\Border as BorderStyle;
use PhpOffice\PhpSpreadsheet\Style\Borders;
use SimpleXMLElement;
class Border extends StyleBase
{
protected const BORDER_POSITIONS = [
'top',
'left',
'bottom',
'right',
];
/**
* @var array
*/
public const BORDER_MAPPINGS = [
'borderStyle' => [
'1continuous' => BorderStyle::BORDER_THIN,
'1dash' => BorderStyle::BORDER_DASHED,
'1dashdot' => BorderStyle::BORDER_DASHDOT,
'1dashdotdot' => BorderStyle::BORDER_DASHDOTDOT,
'1dot' => BorderStyle::BORDER_DOTTED,
'1double' => BorderStyle::BORDER_DOUBLE,
'2continuous' => BorderStyle::BORDER_MEDIUM,
'2dash' => BorderStyle::BORDER_MEDIUMDASHED,
'2dashdot' => BorderStyle::BORDER_MEDIUMDASHDOT,
'2dashdotdot' => BorderStyle::BORDER_MEDIUMDASHDOTDOT,
'2dot' => BorderStyle::BORDER_DOTTED,
'2double' => BorderStyle::BORDER_DOUBLE,
'3continuous' => BorderStyle::BORDER_THICK,
'3dash' => BorderStyle::BORDER_MEDIUMDASHED,
'3dashdot' => BorderStyle::BORDER_MEDIUMDASHDOT,
'3dashdotdot' => BorderStyle::BORDER_MEDIUMDASHDOTDOT,
'3dot' => BorderStyle::BORDER_DOTTED,
'3double' => BorderStyle::BORDER_DOUBLE,
],
];
public function parseStyle(SimpleXMLElement $styleData, array $namespaces): array
{
$style = [];
$diagonalDirection = '';
$borderPosition = '';
foreach ($styleData->Border as $borderStyle) {
$borderAttributes = self::getAttributes($borderStyle, $namespaces['ss']);
$thisBorder = [];
$styleType = (string) $borderAttributes->Weight;
$styleType .= strtolower((string) $borderAttributes->LineStyle);
$thisBorder['borderStyle'] = self::BORDER_MAPPINGS['borderStyle'][$styleType] ?? BorderStyle::BORDER_NONE;
foreach ($borderAttributes as $borderStyleKey => $borderStyleValuex) {
$borderStyleValue = (string) $borderStyleValuex;
switch ($borderStyleKey) {
case 'Position':
[$borderPosition, $diagonalDirection]
= $this->parsePosition($borderStyleValue, $diagonalDirection);
break;
case 'Color':
$borderColour = substr($borderStyleValue, 1);
$thisBorder['color']['rgb'] = $borderColour;
break;
}
}
if ($borderPosition) {
$style['borders'][$borderPosition] = $thisBorder;
} elseif ($diagonalDirection) {
$style['borders']['diagonalDirection'] = $diagonalDirection;
$style['borders']['diagonal'] = $thisBorder;
}
}
return $style;
}
protected function parsePosition(string $borderStyleValue, string $diagonalDirection): array
{
$borderStyleValue = strtolower($borderStyleValue);
if (in_array($borderStyleValue, self::BORDER_POSITIONS)) {
$borderPosition = $borderStyleValue;
} elseif ($borderStyleValue === 'diagonalleft') {
$diagonalDirection = $diagonalDirection ? Borders::DIAGONAL_BOTH : Borders::DIAGONAL_DOWN;
} elseif ($borderStyleValue === 'diagonalright') {
$diagonalDirection = $diagonalDirection ? Borders::DIAGONAL_BOTH : Borders::DIAGONAL_UP;
}
return [$borderPosition ?? null, $diagonalDirection];
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
use PhpOffice\PhpSpreadsheet\Style\Fill as FillStyles;
use SimpleXMLElement;
class Fill extends StyleBase
{
/**
* @var array
*/
public const FILL_MAPPINGS = [
'fillType' => [
'solid' => FillStyles::FILL_SOLID,
'gray75' => FillStyles::FILL_PATTERN_DARKGRAY,
'gray50' => FillStyles::FILL_PATTERN_MEDIUMGRAY,
'gray25' => FillStyles::FILL_PATTERN_LIGHTGRAY,
'gray125' => FillStyles::FILL_PATTERN_GRAY125,
'gray0625' => FillStyles::FILL_PATTERN_GRAY0625,
'horzstripe' => FillStyles::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe
'vertstripe' => FillStyles::FILL_PATTERN_DARKVERTICAL, // vertical stripe
'reversediagstripe' => FillStyles::FILL_PATTERN_DARKUP, // reverse diagonal stripe
'diagstripe' => FillStyles::FILL_PATTERN_DARKDOWN, // diagonal stripe
'diagcross' => FillStyles::FILL_PATTERN_DARKGRID, // diagoanl crosshatch
'thickdiagcross' => FillStyles::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch
'thinhorzstripe' => FillStyles::FILL_PATTERN_LIGHTHORIZONTAL,
'thinvertstripe' => FillStyles::FILL_PATTERN_LIGHTVERTICAL,
'thinreversediagstripe' => FillStyles::FILL_PATTERN_LIGHTUP,
'thindiagstripe' => FillStyles::FILL_PATTERN_LIGHTDOWN,
'thinhorzcross' => FillStyles::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch
'thindiagcross' => FillStyles::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch
],
];
public function parseStyle(SimpleXMLElement $styleAttributes): array
{
$style = [];
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValuex) {
$styleAttributeValue = (string) $styleAttributeValuex;
switch ($styleAttributeKey) {
case 'Color':
$style['fill']['endColor']['rgb'] = substr($styleAttributeValue, 1);
$style['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1);
break;
case 'PatternColor':
$style['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1);
break;
case 'Pattern':
$lcStyleAttributeValue = strtolower((string) $styleAttributeValue);
$style['fill']['fillType']
= self::FILL_MAPPINGS['fillType'][$lcStyleAttributeValue] ?? FillStyles::FILL_NONE;
break;
}
}
return $style;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
use PhpOffice\PhpSpreadsheet\Style\Font as FontUnderline;
use SimpleXMLElement;
class Font extends StyleBase
{
protected const UNDERLINE_STYLES = [
FontUnderline::UNDERLINE_NONE,
FontUnderline::UNDERLINE_DOUBLE,
FontUnderline::UNDERLINE_DOUBLEACCOUNTING,
FontUnderline::UNDERLINE_SINGLE,
FontUnderline::UNDERLINE_SINGLEACCOUNTING,
];
protected function parseUnderline(array $style, string $styleAttributeValue): array
{
if (self::identifyFixedStyleValue(self::UNDERLINE_STYLES, $styleAttributeValue)) {
$style['font']['underline'] = $styleAttributeValue;
}
return $style;
}
protected function parseVerticalAlign(array $style, string $styleAttributeValue): array
{
if ($styleAttributeValue == 'Superscript') {
$style['font']['superscript'] = true;
}
if ($styleAttributeValue == 'Subscript') {
$style['font']['subscript'] = true;
}
return $style;
}
public function parseStyle(SimpleXMLElement $styleAttributes): array
{
$style = [];
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
$styleAttributeValue = (string) $styleAttributeValue;
switch ($styleAttributeKey) {
case 'FontName':
$style['font']['name'] = $styleAttributeValue;
break;
case 'Size':
$style['font']['size'] = $styleAttributeValue;
break;
case 'Color':
$style['font']['color']['rgb'] = substr($styleAttributeValue, 1);
break;
case 'Bold':
$style['font']['bold'] = $styleAttributeValue === '1';
break;
case 'Italic':
$style['font']['italic'] = $styleAttributeValue === '1';
break;
case 'Underline':
$style = $this->parseUnderline($style, $styleAttributeValue);
break;
case 'VerticalAlign':
$style = $this->parseVerticalAlign($style, $styleAttributeValue);
break;
}
}
return $style;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
use SimpleXMLElement;
class NumberFormat extends StyleBase
{
public function parseStyle(SimpleXMLElement $styleAttributes): array
{
$style = [];
$fromFormats = ['\-', '\ '];
$toFormats = ['-', ' '];
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
$styleAttributeValue = str_replace($fromFormats, $toFormats, $styleAttributeValue);
switch ($styleAttributeValue) {
case 'Short Date':
$styleAttributeValue = 'dd/mm/yyyy';
break;
}
if ($styleAttributeValue > '') {
$style['numberFormat']['formatCode'] = $styleAttributeValue;
}
}
return $style;
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
use SimpleXMLElement;
abstract class StyleBase
{
protected static function identifyFixedStyleValue(array $styleList, string &$styleAttributeValue): bool
{
$returnValue = false;
$styleAttributeValue = strtolower($styleAttributeValue);
foreach ($styleList as $style) {
if ($styleAttributeValue == strtolower($style)) {
$styleAttributeValue = $style;
$returnValue = true;
break;
}
}
return $returnValue;
}
protected static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement
{
return ($simple === null) ? new SimpleXMLElement('<xml></xml>') : ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>'));
}
}