feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles

- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD)
  * DEV: Clés TEST Pierre (mode test)
  * REC: Clés TEST Client (mode test)
  * PROD: Clés LIVE Client (mode live)
- Ajout de la gestion des bases de données immeubles/bâtiments
  * Configuration buildings_database pour DEV/REC/PROD
  * Service BuildingService pour enrichissement des adresses
- Optimisations pages et améliorations ergonomie
- Mises à jour des dépendances Composer
- Nettoyage des fichiers obsolètes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-11-09 18:26:27 +01:00
parent 21657a3820
commit 2f5946a184
812 changed files with 142105 additions and 25992 deletions

View File

@@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Reader;
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
@@ -19,7 +20,7 @@ abstract class BaseReader implements IReader
/**
* Read empty cells?
* Identifies whether the Reader should read data values for cells all cells, or should ignore cells containing
* Identifies whether the Reader should read data values for all cells, or should ignore cells containing
* null value or empty string.
*/
protected bool $readEmptyCells = true;
@@ -46,6 +47,19 @@ abstract class BaseReader implements IReader
*/
protected bool $ignoreRowsWithNoCells = false;
/**
* Allow external images. Use with caution.
* Improper specification of these within a spreadsheet
* can subject the caller to security exploits.
*/
protected bool $allowExternalImages = false;
/**
* Create a blank sheet if none are read,
* possibly due to a typo when using LoadSheetsOnly.
*/
protected bool $createBlankSheetIfNoneRead = false;
/**
* IReadFilter instance.
*/
@@ -56,6 +70,8 @@ abstract class BaseReader implements IReader
protected ?XmlScanner $securityScanner = null;
protected ?IValueBinder $valueBinder = null;
public function __construct()
{
$this->readFilter = new DefaultReadFilter();
@@ -109,11 +125,13 @@ abstract class BaseReader implements IReader
return $this;
}
/** @return null|string[] */
public function getLoadSheetsOnly(): ?array
{
return $this->loadSheetsOnly;
}
/** @param null|string|string[] $sheetList */
public function setLoadSheetsOnly(string|array|null $sheetList): self
{
if ($sheetList === null) {
@@ -144,6 +162,34 @@ abstract class BaseReader implements IReader
return $this;
}
/**
* Allow external images. Use with caution.
* Improper specification of these within a spreadsheet
* can subject the caller to security exploits.
*/
public function setAllowExternalImages(bool $allowExternalImages): self
{
$this->allowExternalImages = $allowExternalImages;
return $this;
}
public function getAllowExternalImages(): bool
{
return $this->allowExternalImages;
}
/**
* Create a blank sheet if none are read,
* possibly due to a typo when using LoadSheetsOnly.
*/
public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self
{
$this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead;
return $this;
}
public function getSecurityScanner(): ?XmlScanner
{
return $this->securityScanner;
@@ -166,12 +212,21 @@ abstract class BaseReader implements IReader
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) {
if (((bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) {
$this->setReadEmptyCells(false);
}
if (((bool) ($flags & self::IGNORE_ROWS_WITH_NO_CELLS)) === true) {
$this->setIgnoreRowsWithNoCells(true);
}
if (((bool) ($flags & self::ALLOW_EXTERNAL_IMAGES)) === true) {
$this->setAllowExternalImages(true);
}
if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) {
$this->setAllowExternalImages(false);
}
if (((bool) ($flags & self::CREATE_BLANK_SHEET_IF_NONE_READ)) === true) {
$this->setCreateBlankSheetIfNoneRead(true);
}
}
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
@@ -218,6 +273,8 @@ abstract class BaseReader implements IReader
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
* @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
*/
public function listWorksheetInfo(string $filename): array
{
@@ -229,17 +286,34 @@ abstract class BaseReader implements IReader
* 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.
*
* @return string[]
*/
public function listWorksheetNames(string $filename): array
{
$returnArray = [];
$info = $this->listWorksheetInfo($filename);
foreach ($info as $infoArray) {
if (isset($infoArray['worksheetName'])) {
$returnArray[] = $infoArray['worksheetName'];
}
$returnArray[] = $infoArray['worksheetName'];
}
return $returnArray;
}
public function getValueBinder(): ?IValueBinder
{
return $this->valueBinder;
}
public function setValueBinder(?IValueBinder $valueBinder): self
{
$this->valueBinder = $valueBinder;
return $this;
}
protected function newSpreadsheet(): Spreadsheet
{
return new Spreadsheet();
}
}

View File

@@ -69,14 +69,6 @@ class Csv extends BaseReader
*/
private ?string $escapeCharacter = null;
/**
* The character that will be supplied to fgetcsv
* when escapeCharacter is null.
* It is anticipated that it will conditionally be set
* to null-string for Php9 and above.
*/
private static string $defaultEscapeCharacter = PHP_VERSION_ID < 90000 ? '\\' : '';
/**
* Callback for setting defaults in construction.
*
@@ -84,10 +76,13 @@ class Csv extends BaseReader
*/
private static $constructorCallback;
/** Changed from true to false in release 4.0.0 */
public const DEFAULT_TEST_AUTODETECT = false;
/**
* Attempt autodetect line endings (deprecated after PHP8.1)?
*/
private bool $testAutodetect = true;
private bool $testAutodetect = self::DEFAULT_TEST_AUTODETECT;
protected bool $castFormattedNumberToNumeric = false;
@@ -193,11 +188,12 @@ class Csv extends BaseReader
*/
protected function inferSeparator(): void
{
if ($this->delimiter !== null) {
$temp = $this->delimiter;
if ($temp !== null) {
return;
}
$inferenceEngine = new Delimiter($this->fileHandle, $this->escapeCharacter ?? self::$defaultEscapeCharacter, $this->enclosure);
$inferenceEngine = new Delimiter($this->fileHandle, $this->getEscapeCharacter(), $this->enclosure);
// If number of lines is 0, nothing to infer : fall back to the default
if ($inferenceEngine->linesCounted() === 0) {
@@ -219,6 +215,8 @@ class Csv extends BaseReader
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
* @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
*/
public function listWorksheetInfo(string $filename): array
{
@@ -231,12 +229,15 @@ class Csv extends BaseReader
$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;
$worksheetInfo = [
[
'worksheetName' => 'Worksheet',
'lastColumnLetter' => 'A',
'lastColumnIndex' => 0,
'totalRows' => 0,
'totalColumns' => 0,
],
];
$delimiter = $this->delimiter ?? '';
// Loop through each line of the file in turn
@@ -249,6 +250,7 @@ class Csv extends BaseReader
$worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
$worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
$worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
// Close file
fclose($fileHandle);
@@ -261,8 +263,8 @@ class Csv extends BaseReader
*/
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet = $this->newSpreadsheet();
$spreadsheet->setValueBinder($this->valueBinder);
// Load into this instance
return $this->loadIntoExisting($filename, $spreadsheet);
@@ -273,8 +275,8 @@ class Csv extends BaseReader
*/
public function loadSpreadsheetFromString(string $contents): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet = $this->newSpreadsheet();
$spreadsheet->setValueBinder($this->valueBinder);
// Load into this instance
return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true);
@@ -317,10 +319,10 @@ class Csv extends BaseReader
return $this;
}
private function setAutoDetect(?string $value): ?string
private function setAutoDetect(?string $value, int $version = PHP_VERSION_ID): ?string
{
$retVal = null;
if ($value !== null && $this->testAutodetect && PHP_VERSION_ID < 90000) {
if ($value !== null && $this->testAutodetect && $version < 90000) {
$retVal2 = @ini_set('auto_detect_line_endings', $value);
if (is_string($retVal2)) {
$retVal = $retVal2;
@@ -383,6 +385,7 @@ class Csv extends BaseReader
private function loadStringOrFile2(string $filename, Spreadsheet $spreadsheet, bool $dataUri): void
{
// Open file
if ($dataUri) {
$this->openDataUri($filename);
@@ -412,7 +415,7 @@ class Csv extends BaseReader
// Loop through each line of the file in turn
$delimiter = $this->delimiter ?? '';
$rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
$valueBinder = Cell::getValueBinder();
$valueBinder = $this->valueBinder ?? Cell::getValueBinder();
$preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion();
$this->getTrue = Calculation::getTRUE();
$this->getFalse = Calculation::getFALSE();
@@ -446,7 +449,7 @@ class Csv extends BaseReader
// Set cell value
$sheet->getCell($columnLetter . $outRow)->setValue($rowDatum);
}
++$columnLetter;
StringHelper::stringIncrement($columnLetter);
}
$rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
++$currentRow;
@@ -559,9 +562,9 @@ class Csv extends BaseReader
* Not yet ready to mark deprecated in order to give users
* a migration path.
*/
public function setEscapeCharacter(string $escapeCharacter): self
public function setEscapeCharacter(string $escapeCharacter, int $version = PHP_VERSION_ID): self
{
if (PHP_VERSION_ID >= 90000 && $escapeCharacter !== '') {
if ($version >= 90000 && $escapeCharacter !== '') {
throw new ReaderException('Escape character must be null string for Php9+');
}
@@ -570,9 +573,9 @@ class Csv extends BaseReader
return $this;
}
public function getEscapeCharacter(): string
public function getEscapeCharacter(int $version = PHP_VERSION_ID): string
{
return $this->escapeCharacter ?? self::$defaultEscapeCharacter;
return $this->escapeCharacter ?? self::getDefaultEscapeCharacter($version);
}
/**
@@ -602,6 +605,7 @@ class Csv extends BaseReader
'text/csv',
'text/plain',
'inode/x-empty',
'application/x-empty', // has now replaced previous
'text/html',
];
@@ -621,7 +625,7 @@ class Csv extends BaseReader
private static function guessEncodingNoBom(string $filename): string
{
$encoding = '';
$contents = file_get_contents($filename);
$contents = (string) 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');
@@ -698,10 +702,11 @@ class Csv extends BaseReader
?int $length = null,
string $separator = ',',
string $enclosure = '"',
?string $escape = null
?string $escape = null,
int $version = PHP_VERSION_ID
): array|false {
$escape = $escape ?? self::$defaultEscapeCharacter;
if (PHP_VERSION_ID >= 80400 && $escape !== '') {
$escape = $escape ?? self::getDefaultEscapeCharacter();
if ($version >= 80400 && $escape !== '') {
return @fgetcsv($stream, $length, $separator, $enclosure, $escape);
}
@@ -713,10 +718,11 @@ class Csv extends BaseReader
string $inputEncoding = 'UTF-8',
?string $delimiter = null,
string $enclosure = '"',
string $escapeCharacter = '\\'
string $escapeCharacter = '\\',
int $version = PHP_VERSION_ID
): bool {
if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 90000) {
throw new ReaderException('Function valid only for Php7.4 or Php8'); // @codeCoverageIgnore
if ($version < 70400 || $version >= 90000) {
throw new ReaderException('Function valid only for Php7.4 or Php8');
}
$reader1 = new self();
$reader1->setInputEncoding($inputEncoding)
@@ -742,4 +748,15 @@ class Csv extends BaseReader
return $array1 !== $array2;
}
/**
* The character that will be supplied to fgetcsv
* when escapeCharacter is null.
* It is anticipated that it will conditionally be set
* to null-string for Php9 and above.
*/
private static function getDefaultEscapeCharacter(int $version = PHP_VERSION_ID): string
{
return $version < 90000 ? '\\' : '';
}
}

View File

@@ -13,6 +13,7 @@ class Delimiter
protected string $enclosure;
/** @var array<string, int[]> */
protected array $counts = [];
protected int $numberLines = 0;
@@ -53,16 +54,15 @@ class Delimiter
}
}
/** @param array<string, int> $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);
$splitString = mb_str_split($line, 1, 'UTF-8');
$distribution = array_count_values($splitString);
$countLine = array_intersect_key($distribution, $delimiterKeys);
foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
$this->counts[$delimiter][] = $countLine[$delimiter] ?? 0;
}
foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
$this->counts[$delimiter][] = $countLine[$delimiter] ?? 0;
}
}
@@ -71,7 +71,7 @@ class Delimiter
// Calculate the mean square deviations for each delimiter
// (ignoring delimiters that haven't been found consistently)
$meanSquareDeviations = [];
$middleIdx = floor(($this->numberLines - 1) / 2);
$middleIdx = (int) floor(($this->numberLines - 1) / 2);
foreach (self::POTENTIAL_DELIMETERS as $delimiter) {
$series = $this->counts[$delimiter];

View File

@@ -33,8 +33,13 @@ class Gnumeric extends BaseReader
const NAMESPACE_OOO = 'http://openoffice.org/2004/office';
const GNM_SHEET_VISIBILITY_VISIBLE = 'GNM_SHEET_VISIBILITY_VISIBLE';
const GNM_SHEET_VISIBILITY_HIDDEN = 'GNM_SHEET_VISIBILITY_HIDDEN';
/**
* Shared Expressions.
*
* @var array<array{column: int, row: int, formula:string}>
*/
private array $expressions = [];
@@ -45,6 +50,7 @@ class Gnumeric extends BaseReader
private ReferenceHelper $referenceHelper;
/** @var array{'dataType': string[]} */
public static array $mappings = [
'dataType' => [
'10' => DataType::TYPE_NULL,
@@ -93,6 +99,8 @@ class Gnumeric extends BaseReader
/**
* Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
*
* @return string[]
*/
public function listWorksheetNames(string $filename): array
{
@@ -122,6 +130,8 @@ class Gnumeric extends BaseReader
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
* @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
*/
public function listWorksheetInfo(string $filename): array
{
@@ -144,7 +154,12 @@ class Gnumeric extends BaseReader
'lastColumnIndex' => 0,
'totalRows' => 0,
'totalColumns' => 0,
'sheetState' => Worksheet::SHEETSTATE_VISIBLE,
];
$visibility = $xml->getAttribute('Visibility');
if ((string) $visibility === self::GNM_SHEET_VISIBILITY_HIDDEN) {
$tmpInfo['sheetState'] = Worksheet::SHEETSTATE_HIDDEN;
}
while ($xml->read()) {
if (self::matchXml($xml, 'Name')) {
@@ -193,6 +208,7 @@ class Gnumeric extends BaseReader
return $data;
}
/** @return mixed[] */
public static function gnumericMappings(): array
{
return array_merge(self::$mappings, Styles::$mappings);
@@ -223,8 +239,8 @@ class Gnumeric extends BaseReader
*/
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet = $this->newSpreadsheet();
$spreadsheet->setValueBinder($this->valueBinder);
$spreadsheet->removeSheetByIndex(0);
// Load into this instance
@@ -253,6 +269,7 @@ class Gnumeric extends BaseReader
(new Properties($this->spreadsheet))->readProperties($xml, $gnmXML);
$worksheetID = 0;
$sheetCreated = false;
foreach ($gnmXML->Sheets->Sheet as $sheetOrNull) {
$sheet = self::testSimpleXml($sheetOrNull);
$worksheetName = (string) $sheet->Name;
@@ -264,14 +281,15 @@ class Gnumeric extends BaseReader
// Create new Worksheet
$this->spreadsheet->createSheet();
$sheetCreated = true;
$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') {
$visibility = $sheet->attributes()['Visibility'] ?? self::GNM_SHEET_VISIBILITY_VISIBLE;
if ((string) $visibility !== self::GNM_SHEET_VISIBILITY_VISIBLE) {
$this->spreadsheet->getActiveSheet()->setSheetState(Worksheet::SHEETSTATE_HIDDEN);
}
@@ -293,10 +311,8 @@ class Gnumeric extends BaseReader
$column = Coordinate::stringFromColumnIndex($column + 1);
// Read cell?
if ($this->getReadFilter() !== null) {
if (!$this->getReadFilter()->readCell($column, $row, $worksheetName)) {
continue;
}
if (!$this->getReadFilter()->readCell($column, $row, $worksheetName)) {
continue;
}
$this->loadCell($cell, $worksheetName, $cellAttributes, $column, $row);
@@ -315,6 +331,9 @@ class Gnumeric extends BaseReader
$this->setSelectedCells($sheet);
++$worksheetID;
}
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
$this->spreadsheet->createSheet();
}
$this->processDefinedNames($gnmXML);
@@ -371,11 +390,9 @@ class Gnumeric extends BaseReader
{
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']);
}
$attributes = $autofilter->attributes();
if (isset($attributes['Area'])) {
$this->spreadsheet->getActiveSheet()->setAutoFilter((string) $attributes['Area']);
}
}
}
@@ -383,20 +400,20 @@ class Gnumeric extends BaseReader
private function setColumnWidth(int $whichColumn, float $defaultWidth): void
{
$columnDimension = $this->spreadsheet->getActiveSheet()
->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1));
if ($columnDimension !== null) {
$columnDimension->setWidth($defaultWidth);
}
$this->spreadsheet->getActiveSheet()
->getColumnDimension(
Coordinate::stringFromColumnIndex($whichColumn + 1)
)
->setWidth($defaultWidth);
}
private function setColumnInvisible(int $whichColumn): void
{
$columnDimension = $this->spreadsheet->getActiveSheet()
->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1));
if ($columnDimension !== null) {
$columnDimension->setVisible(false);
}
$this->spreadsheet->getActiveSheet()
->getColumnDimension(
Coordinate::stringFromColumnIndex($whichColumn + 1)
)
->setVisible(false);
}
private function processColumnLoop(int $whichColumn, int $maxCol, ?SimpleXMLElement $columnOverride, float $defaultWidth): int
@@ -444,18 +461,18 @@ class Gnumeric extends BaseReader
private function setRowHeight(int $whichRow, float $defaultHeight): void
{
$rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow);
if ($rowDimension !== null) {
$rowDimension->setRowHeight($defaultHeight);
}
$this->spreadsheet
->getActiveSheet()
->getRowDimension($whichRow)
->setRowHeight($defaultHeight);
}
private function setRowInvisible(int $whichRow): void
{
$rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow);
if ($rowDimension !== null) {
$rowDimension->setVisible(false);
}
$this->spreadsheet
->getActiveSheet()
->getRowDimension($whichRow)
->setVisible(false);
}
private function processRowLoop(int $whichRow, int $maxRow, ?SimpleXMLElement $rowOverride, float $defaultHeight): int
@@ -516,8 +533,8 @@ class Gnumeric extends BaseReader
continue;
}
[$worksheetName] = Worksheet::extractSheetTitle($value, true);
$worksheetName = trim($worksheetName, "'");
$value = str_replace("\\'", "''", $value);
[$worksheetName] = Worksheet::extractSheetTitle($value, true, true);
$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) {
@@ -544,15 +561,21 @@ class Gnumeric extends BaseReader
): void {
$ValueType = $cellAttributes->ValueType;
$ExprID = (string) $cellAttributes->ExprID;
$rows = (int) ($cellAttributes->Rows ?? 0);
$cols = (int) ($cellAttributes->Cols ?? 0);
$type = DataType::TYPE_FORMULA;
$isArrayFormula = ($rows > 0 && $cols > 0);
$arrayFormulaRange = $isArrayFormula ? $this->getArrayFormulaRange($column, $row, $cols, $rows) : null;
if ($ExprID > '') {
if (((string) $cell) > '') {
// Formula
$this->expressions[$ExprID] = [
'column' => $cellAttributes->Col,
'row' => $cellAttributes->Row,
'column' => (int) $cellAttributes->Col,
'row' => (int) $cellAttributes->Row,
'formula' => (string) $cell,
];
} else {
// Shared Formula
$expression = $this->expressions[$ExprID];
$cell = $this->referenceHelper->updateFormulaReferences(
@@ -564,21 +587,39 @@ class Gnumeric extends BaseReader
);
}
$type = DataType::TYPE_FORMULA;
} else {
} elseif ($isArrayFormula === false) {
$vtype = (string) $ValueType;
if (array_key_exists($vtype, self::$mappings['dataType'])) {
$type = self::$mappings['dataType'][$vtype];
}
if ($vtype === '20') { // Boolean
if ($vtype === '20') { // Boolean
$cell = $cell == 'TRUE';
}
}
$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type);
if ($arrayFormulaRange === null) {
$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setFormulaAttributes(null);
} else {
$this->spreadsheet->getActiveSheet()->getCell($column . $row)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayFormulaRange]);
}
if (isset($cellAttributes->ValueFormat)) {
$this->spreadsheet->getActiveSheet()->getCell($column . $row)
->getStyle()->getNumberFormat()
->setFormatCode((string) $cellAttributes->ValueFormat);
}
}
private function getArrayFormulaRange(string $column, int $row, int $cols, int $rows): string
{
$arrayFormulaRange = $column . $row;
$arrayFormulaRange .= ':'
. Coordinate::stringFromColumnIndex(
Coordinate::columnIndexFromString($column)
+ $cols - 1
)
. (string) ($row + $rows - 1);
return $arrayFormulaRange;
}
}

View File

@@ -70,6 +70,11 @@ class PageSetup
return $this;
}
/**
* @param float[] $marginSet
*
* @return float[]
*/
private function buildMarginSet(SimpleXMLElement $sheet, array $marginSet): array
{
foreach ($sheet->PrintInformation->Margins->children(Gnumeric::NAMESPACE_GNM) as $key => $margin) {
@@ -83,6 +88,7 @@ class PageSetup
return $marginSet;
}
/** @param float[] $marginSet */
private function adjustMargins(array $marginSet): void
{
foreach ($marginSet as $key => $marginSize) {

View File

@@ -91,32 +91,30 @@ class Properties
{
$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);
$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 'initial-creator':
$docProps->setCreator($propertyValue);
$docProps->setLastModifiedBy($propertyValue);
break;
case 'creation-date':
$creationDate = $propertyValue;
$docProps->setCreated($creationDate);
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;
case 'user-defined':
if ($attributes) {
[, $attrName] = explode(':', (string) $attributes['name']);
$this->userDefinedProperties($attrName, $propertyValue);
}
break;
}
break;
}
}
}

View File

@@ -18,6 +18,7 @@ class Styles
protected bool $readDataOnly;
/** @var array<string, string[]> */
public static array $mappings = [
'borderStyle' => [
'0' => Border::BORDER_NONE,
@@ -100,6 +101,7 @@ class Styles
$styleAttributes = $style->Style->attributes();
/** @var mixed[][] */
$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
@@ -112,11 +114,16 @@ class Styles
$styleArray['numberFormat']['formatCode'] = $formatCode;
$styleArray = $this->readStyle($styleArray, $styleAttributes, $style);
}
$this->spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($styleArray);
/** @var mixed[][] $styleArray */
$this->spreadsheet
->getActiveSheet()
->getStyle($cellRange)
->applyFromArray($styleArray);
}
}
}
/** @param mixed[][] $styleArray */
private function addBorderDiagonal(SimpleXMLElement $srssb, array &$styleArray): void
{
if (isset($srssb->Diagonal, $srssb->{'Rev-Diagonal'})) {
@@ -131,11 +138,14 @@ class Styles
}
}
/** @param mixed[][] $styleArray */
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());
/** @var SimpleXMLElement */
$temp = $srssb->$ucDirection;
$styleArray['borders'][$direction] = self::parseBorderAttributes($temp->attributes());
}
}
@@ -150,13 +160,15 @@ class Styles
return $rotation;
}
/** @param mixed[][] $styleArray */
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];
$styleArray[$key] = self::$mappings[$key][$value]; //* @phpstan-ignore-line
}
}
/** @param mixed[][] $styleArray */
private static function addStyle2(array &$styleArray, string $key1, string $key, string $value): void
{
if (array_key_exists($value, self::$mappings[$key])) {
@@ -164,8 +176,10 @@ class Styles
}
}
/** @return mixed[][] */
private static function parseBorderAttributes(?SimpleXMLElement $borderAttributes): array
{
/** @var mixed[][] */
$styleArray = [];
if ($borderAttributes !== null) {
if (isset($borderAttributes['Color'])) {
@@ -174,6 +188,7 @@ class Styles
self::addStyle($styleArray, 'borderStyle', (string) $borderAttributes['Style']);
}
/** @var mixed[][] $styleArray */
return $styleArray;
}
@@ -188,9 +203,11 @@ class Styles
return $gnmR . $gnmG . $gnmB;
}
/** @param mixed[][] $styleArray */
private function addColors(array &$styleArray, SimpleXMLElement $styleAttributes): void
{
$RGB = self::parseGnumericColour((string) $styleAttributes['Fore']);
/** @var mixed[][][] $styleArray */
$styleArray['font']['color']['rgb'] = $RGB;
$RGB = self::parseGnumericColour((string) $styleAttributes['Back']);
$shade = (string) $styleAttributes['Shade'];
@@ -221,6 +238,11 @@ class Styles
return $cellRange;
}
/**
* @param mixed[][] $styleArray
*
* @return mixed[]
*/
private function readStyle(array $styleArray, SimpleXMLElement $styleAttributes, SimpleXMLElement $style): array
{
self::addStyle2($styleArray, 'alignment', 'horizontal', (string) $styleAttributes['HAlign']);

View File

@@ -7,6 +7,7 @@ use DOMDocument;
use DOMElement;
use DOMNode;
use DOMText;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Comment;
@@ -15,6 +16,7 @@ use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension;
use PhpOffice\PhpSpreadsheet\Helper\Html as HelperHtml;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Color;
@@ -34,7 +36,7 @@ class Html extends BaseReader
private const STARTS_WITH_BOM = '/^(?:\xfe\xff|\xff\xfe|\xEF\xBB\xBF)/';
private const DECLARES_CHARSET = '/\\bcharset=/i';
private const DECLARES_CHARSET = '/\bcharset=/i';
/**
* Input encoding.
@@ -49,7 +51,7 @@ class Html extends BaseReader
/**
* Formats.
*/
protected array $formats = [
protected const FORMATS = [
'h1' => [
'font' => [
'bold' => true,
@@ -126,6 +128,7 @@ class Html extends BaseReader
], // Italic
];
/** @var array<string, bool> */
protected array $rowspan = [];
/**
@@ -173,6 +176,7 @@ class Html extends BaseReader
// Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3
$filename = $meta['uri']; //@phpstan-ignore-line
clearstatcache(true, $filename);
$size = (int) filesize($filename);
if ($size === 0) {
return '';
@@ -208,19 +212,24 @@ class Html extends BaseReader
*/
public function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet = $this->newSpreadsheet();
$spreadsheet->setValueBinder($this->valueBinder);
// Load into this instance
return $this->loadIntoExisting($filename, $spreadsheet);
}
// Data Array used for testing only, should write to Spreadsheet object on completion of tests
/**
* Data Array used for testing only, should write to
* Spreadsheet object on completion of tests.
*
* @var mixed[][]
*/
protected array $dataArray = [];
protected int $tableLevel = 0;
/** @var string[] */
protected array $nestedColumn = ['A'];
protected function setTableStartColumn(string $column): string
@@ -243,11 +252,15 @@ class Html extends BaseReader
{
--$this->tableLevel;
return array_pop($this->nestedColumn);
return array_pop($this->nestedColumn) ?? '';
}
/**
* Flush cell.
*
* @param string[] $attributeArray
*
* @param-out string $cellContent In one case, it can be bool
*/
protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContent, array $attributeArray): void
{
@@ -269,6 +282,13 @@ class Html extends BaseReader
->setQuotePrefix(true);
}
}
if ($datatype === DataType::TYPE_BOOL) {
// This is the case where we can set cellContent to bool rather than string
$cellContent = self::convertBoolean($cellContent); //* @phpstan-ignore-line
if (!is_bool($cellContent)) {
$attributeArray['data-type'] = DataType::TYPE_STRING;
}
}
//catching the Exception and ignoring the invalid data types
try {
@@ -284,16 +304,41 @@ class Html extends BaseReader
} else {
// We have a Rich Text run
// TODO
$this->dataArray[$row][$column] = 'RICH TEXT: ' . $cellContent;
$this->dataArray[$row][$column] = 'RICH TEXT: ' . StringHelper::convertToString($cellContent);
}
$cellContent = (string) '';
}
/** @var array<int, array<int, string>> */
private static array $falseTrueArray = [];
private static function convertBoolean(?string $cellContent): bool|string
{
if ($cellContent === '1') {
return true;
}
if ($cellContent === '0' || $cellContent === '' || $cellContent === null) {
return false;
}
if (empty(self::$falseTrueArray)) {
$calc = Calculation::getInstance();
self::$falseTrueArray = $calc->getFalseTrueArray();
}
if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[1], true)) {
return true;
}
if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[0], true)) {
return false;
}
return $cellContent;
}
private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void
{
$attributeArray = [];
/** @var DOMAttr $attribute */
foreach ($child->attributes as $attribute) {
foreach (($child->attributes ?? []) as $attribute) {
$attributeArray[$attribute->name] = $attribute->value;
}
@@ -308,6 +353,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'title') {
@@ -315,6 +361,7 @@ class Html extends BaseReader
try {
$sheet->setTitle($cellContent, true, true);
$sheet->getParent()?->getProperties()?->setTitle($cellContent);
} catch (SpreadsheetException) {
// leave default title if too long or illegal chars
}
@@ -326,6 +373,7 @@ class Html extends BaseReader
private const SPAN_ETC = ['span', 'div', 'font', 'i', 'em', 'strong', 'b'];
/** @param string[] $attributeArray */
private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if (in_array((string) $child->nodeName, self::SPAN_ETC, true)) {
@@ -338,7 +386,7 @@ class Html extends BaseReader
}
if (isset($attributeArray['style'])) {
$alignStyle = $attributeArray['style'];
if (preg_match('/\\btext-align:\\s*(left|right|center|justify)\\b/', $alignStyle, $matches) === 1) {
if (preg_match('/\btext-align:\s*(left|right|center|justify)\b/', (string) $alignStyle, $matches) === 1) {
$sheet->getComment($column . $row)->setAlignment($matches[1]);
}
}
@@ -346,28 +394,28 @@ class Html extends BaseReader
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
}
if (isset($this->formats[$child->nodeName])) {
$sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
if (isset(self::FORMATS[$child->nodeName])) {
$sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
}
} else {
$this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray);
}
}
/** @param string[] $attributeArray */
private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'hr') {
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
++$row;
if (isset($this->formats[$child->nodeName])) {
$sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
}
$sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
++$row;
}
// fall through to br
$this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray);
}
/** @param string[] $attributeArray */
private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'br' || $child->nodeName === 'hr') {
@@ -385,6 +433,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'a') {
@@ -392,9 +441,7 @@ class Html extends BaseReader
switch ($attributeName) {
case 'href':
$sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue);
if (isset($this->formats[$child->nodeName])) {
$sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
}
$sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
break;
case 'class':
@@ -413,6 +460,7 @@ class Html extends BaseReader
private const H1_ETC = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'p'];
/** @param string[] $attributeArray */
private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if (in_array((string) $child->nodeName, self::H1_ETC, true)) {
@@ -429,8 +477,8 @@ class Html extends BaseReader
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
if (isset($this->formats[$child->nodeName])) {
$sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
if (isset(self::FORMATS[$child->nodeName])) {
$sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
}
++$row;
@@ -441,6 +489,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'li') {
@@ -462,6 +511,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'img') {
@@ -473,9 +523,18 @@ class Html extends BaseReader
private string $currentColumn = 'A';
/** @param string[] $attributeArray */
private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'table') {
if (isset($attributeArray['class'])) {
$classes = explode(' ', $attributeArray['class']);
$sheet->setShowGridlines(in_array('gridlines', $classes, true));
$sheet->setPrintGridlines(in_array('gridlinesp', $classes, true));
}
if ('rtl' === ($attributeArray['dir'] ?? '')) {
$sheet->setRightToLeft(true);
}
$this->currentColumn = 'A';
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
$column = $this->setTableStartColumn($column);
@@ -485,7 +544,7 @@ class Html extends BaseReader
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
$column = $this->releaseTableStartColumn();
if ($this->tableLevel > 1) {
++$column;
StringHelper::stringIncrement($column);
} else {
++$row;
}
@@ -494,18 +553,19 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'col') {
$this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray);
++$this->currentColumn;
StringHelper::stringIncrement($this->currentColumn);
} elseif ($child->nodeName === 'tr') {
$column = $this->getTableStartColumn();
$cellContent = '';
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
if (isset($attributeArray['height'])) {
$sheet->getRowDimension($row)->setRowHeight($attributeArray['height']);
$sheet->getRowDimension($row)->setRowHeight((float) $attributeArray['height']);
}
++$row;
@@ -514,6 +574,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName !== 'td' && $child->nodeName !== 'th') {
@@ -523,6 +584,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void
{
if (isset($attributeArray['bgcolor'])) {
@@ -537,6 +599,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void
{
if (isset($attributeArray['width'])) {
@@ -544,6 +607,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void
{
if (isset($attributeArray['height'])) {
@@ -551,6 +615,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
{
if (isset($attributeArray['align'])) {
@@ -558,6 +623,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
{
if (isset($attributeArray['valign'])) {
@@ -565,6 +631,7 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void
{
if (isset($attributeArray['data-format'])) {
@@ -572,16 +639,19 @@ class Html extends BaseReader
}
}
/** @param string[] $attributeArray */
private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
while (isset($this->rowspan[$column . $row])) {
++$column;
$temp = (string) $column;
$column = StringHelper::stringIncrement($temp);
}
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
// apply inline style
$this->applyInlineStyle($sheet, $row, $column, $attributeArray);
/** @var string $cellContent */
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
$this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
@@ -595,7 +665,7 @@ class Html extends BaseReader
//create merging rowspan and colspan
$columnTo = $column;
for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
++$columnTo;
StringHelper::stringIncrement($columnTo);
}
$range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
@@ -614,13 +684,13 @@ class Html extends BaseReader
//create merging colspan
$columnTo = $column;
for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
++$columnTo;
StringHelper::stringIncrement($columnTo);
}
$sheet->mergeCells($column . $row . ':' . $columnTo . $row);
$column = $columnTo;
}
++$column;
StringHelper::stringIncrement($column);
}
protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void
@@ -631,10 +701,8 @@ class Html extends BaseReader
if ($domText === "\u{a0}") {
$domText = '';
}
if (is_string($cellContent)) {
// simply append the text if the cell content is a plain text string
$cellContent .= $domText;
}
// simply append the text if the cell content is a plain text string
$cellContent .= $domText;
// but if we have a rich text run instead, we need to append it correctly
// TODO
} elseif ($child instanceof DOMElement) {
@@ -747,6 +815,7 @@ class Html extends BaseReader
}
}
/** @param string[] $matches */
private static function replaceNonAscii(array $matches): string
{
return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
@@ -785,7 +854,8 @@ class Html extends BaseReader
if ($loaded === false) {
throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
}
$spreadsheet = $spreadsheet ?? new Spreadsheet();
$spreadsheet = $spreadsheet ?? $this->newSpreadsheet();
$spreadsheet->setValueBinder($this->valueBinder);
self::loadProperties($dom, $spreadsheet);
return $this->loadDocument($dom, $spreadsheet);
@@ -845,6 +915,8 @@ class Html extends BaseReader
*
* TODO :
* - Implement to other propertie, such as border
*
* @param string[] $attributeArray
*/
private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, array $attributeArray): void
{
@@ -857,7 +929,7 @@ class Html extends BaseReader
} elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
$columnTo = $column;
for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
++$columnTo;
StringHelper::stringIncrement($columnTo);
}
$range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
$cellStyle = $sheet->getStyle($range);
@@ -867,7 +939,7 @@ class Html extends BaseReader
} elseif (isset($attributeArray['colspan'])) {
$columnTo = $column;
for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
++$columnTo;
StringHelper::stringIncrement($columnTo);
}
$range = $column . $row . ':' . $columnTo . $row;
$cellStyle = $sheet->getStyle($range);
@@ -879,7 +951,7 @@ class Html extends BaseReader
$styles = explode(';', $attributeArray['style']);
foreach ($styles as $st) {
$value = explode(':', $st);
$styleName = isset($value[0]) ? trim($value[0]) : null;
$styleName = trim($value[0]);
$styleValue = isset($value[1]) ? trim($value[1]) : null;
$styleValueString = (string) $styleValue;
@@ -1033,19 +1105,24 @@ class Html extends BaseReader
return HelperHtml::colourNameLookup($value);
}
/** @param string[] $attributes */
private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void
{
if (!isset($attributes['src'])) {
return;
}
$styleArray = self::getStyleArray($attributes);
$src = urldecode($attributes['src']);
$width = isset($attributes['width']) ? (float) $attributes['width'] : null;
$height = isset($attributes['height']) ? (float) $attributes['height'] : null;
$src = $attributes['src'];
if (substr($src, 0, 5) !== 'data:') {
$src = urldecode($src);
}
$width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
$height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null);
$name = $attributes['alt'] ?? null;
$drawing = new Drawing();
$drawing->setPath($src, false);
$drawing->setPath($src, false, allowExternal: $this->allowExternalImages);
if ($drawing->getPath() === '') {
return;
}
@@ -1059,11 +1136,15 @@ class Html extends BaseReader
$drawing->setName($name);
}
/** @var null|scalar $width */
/** @var null|scalar $height */
if ($width) {
$drawing->setWidth((int) $width);
}
if ($height) {
if ($height) {
$drawing->setWidthAndHeight((int) $width, (int) $height);
} else {
$drawing->setWidth((int) $width);
}
} elseif ($height) {
$drawing->setHeight((int) $height);
}
@@ -1074,6 +1155,49 @@ class Html extends BaseReader
$sheet->getRowDimension($row)->setRowHeight(
$drawing->getHeight() * 0.9
);
if (isset($styleArray['opacity'])) {
$opacity = $styleArray['opacity'];
if (is_numeric($opacity)) {
$drawing->setOpacity((int) ($opacity * 100000));
}
}
}
/**
* @param string[] $attributes
*
* @return mixed[]
*/
private static function getStyleArray(array $attributes): array
{
$styleArray = [];
if (isset($attributes['style'])) {
$styles = explode(';', $attributes['style']);
foreach ($styles as $style) {
$value = explode(':', $style);
if (count($value) === 2) {
$arrayKey = trim($value[0]);
$arrayValue = trim($value[1]);
if ($arrayKey === 'width') {
if (substr($arrayValue, -2) === 'px') {
$arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
} else {
$arrayValue = (new CssDimension($arrayValue))->width();
}
} elseif ($arrayKey === 'height') {
if (substr($arrayValue, -2) === 'px') {
$arrayValue = substr($arrayValue, 0, -2);
} else {
$arrayValue = (new CssDimension($arrayValue))->height();
}
}
$styleArray[$arrayKey] = $arrayValue;
}
}
}
return $styleArray;
}
private const BORDER_MAPPINGS = [
@@ -1093,6 +1217,7 @@ class Html extends BaseReader
'thick' => Border::BORDER_THICK,
];
/** @return array<string, string> */
public static function getBorderMappings(): array
{
return self::BORDER_MAPPINGS;
@@ -1135,11 +1260,13 @@ class Html extends BaseReader
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
* @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
*/
public function listWorksheetInfo(string $filename): array
{
$info = [];
$spreadsheet = new Spreadsheet();
$spreadsheet = $this->newSpreadsheet();
$this->loadIntoExisting($filename, $spreadsheet);
foreach ($spreadsheet->getAllSheets() as $sheet) {
$newEntry = ['worksheetName' => $sheet->getTitle()];
@@ -1147,6 +1274,7 @@ class Html extends BaseReader
$newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1;
$newEntry['totalRows'] = $sheet->getHighestDataRow();
$newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1;
$newEntry['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
$info[] = $newEntry;
}
$spreadsheet->disconnectWorksheets();

View File

@@ -6,15 +6,43 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
interface IReader
{
/**
* Flag used to load the charts.
*
* This flag is supported only for some formats.
*/
public const LOAD_WITH_CHARTS = 1;
/**
* Flag used to read data only, not style or structure information.
*/
public const READ_DATA_ONLY = 2;
public const SKIP_EMPTY_CELLS = 4;
/**
* Flag used to ignore empty cells when reading.
*
* The ignored cells will not be instantiated.
*/
public const IGNORE_EMPTY_CELLS = 4;
/**
* Flag used to ignore rows without cells.
*
* This flag is supported only for some formats.
* This can heavily improve performance for some files.
*/
public const IGNORE_ROWS_WITH_NO_CELLS = 8;
/**
* Allow external images. Use with caution.
* Improper specification of these within a spreadsheet
* can subject the caller to security exploits.
*/
public const ALLOW_EXTERNAL_IMAGES = 16;
public const DONT_ALLOW_EXTERNAL_IMAGES = 32;
public const CREATE_BLANK_SHEET_IF_NONE_READ = 64;
public function __construct();
/**
@@ -78,13 +106,15 @@ interface IReader
* 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.
*
* @return null|string[]
*/
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,
* @param null|string|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
*
@@ -112,6 +142,21 @@ interface IReader
*/
public function setReadFilter(IReadFilter $readFilter): self;
/**
* Allow external images. Use with caution.
* Improper specification of these within a spreadsheet
* can subject the caller to security exploits.
*/
public function setAllowExternalImages(bool $allowExternalImages): self;
public function getAllowExternalImages(): bool;
/**
* Create a blank sheet if none are read,
* possibly due to a typo when using LoadSheetsOnly.
*/
public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self;
/**
* Loads PhpSpreadsheet from file.
*
@@ -119,8 +164,12 @@ interface IReader
* @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,
* self::IGNORE_EMPTY_CELLS Don't read empty cells (cells that contain a null value,
* empty string, or a string containing only whitespace characters)
* self::IGNORE_ROWS_WITH_NO_CELLS Don't load any rows that contain no cells.
* self::ALLOW_EXTERNAL_IMAGES Attempt to fetch images stored outside the spreadsheet.
* self::DONT_ALLOW_EXTERNAL_IMAGES Don't attempt to fetch images stored outside the spreadsheet.
* self::CREATE_BLANK_SHEET_IF_NONE_READ If no sheets are read, create a blank one.
*/
public function load(string $filename, int $flags = 0): Spreadsheet;
}

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,7 @@ class DefinedNames extends BaseLoader
*/
private function addDefinedName(string $baseAddress, string $definedName, string $value): void
{
[$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true);
[$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true, 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) {

View File

@@ -2,24 +2,40 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use Composer\Pcre\Preg;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
class FormulaTranslator
{
private static function replaceQuotedPeriod(string $value): string
{
$value2 = '';
$quoted = false;
foreach (mb_str_split($value, 1, 'UTF-8') as $char) {
if ($char === "'") {
$quoted = !$quoted;
} elseif ($char === '.' && $quoted) {
$char = "\u{fffe}";
}
$value2 .= $char;
}
return $value2;
}
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(
$excelAddress = Preg::replace(
[
'/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu',
'/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', // Cell range reference in another sheet
'/\$?([^\.]+)\.([^\.]+)/miu', // Cell reference in another sheet
'/\.([^\.]+):\.([^\.]+)/miu', // Cell range reference
'/\.([^\.]+)/miu', // Simple cell reference
'/\x{FFFE}/miu', // restore quoted periods
],
[
'$1!$2:$4',
@@ -27,8 +43,9 @@ class FormulaTranslator
'$1!$2',
'$1:$2',
'$1',
'.',
],
$excelAddress
self::replaceQuotedPeriod($openOfficeAddress)
);
return $excelAddress;
@@ -46,20 +63,22 @@ class FormulaTranslator
// so that conversion isn't done in string values
$tKey = $tKey === false;
if ($tKey) {
$value = (string) preg_replace(
$value = Preg::replace(
[
'/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference in another sheet
'/\[\$?([^\.]+)\.([^\.]+)\]/miu', // Cell reference in another sheet
'/\[\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference
'/\[\.([^\.]+)\]/miu', // Simple cell reference
'/\x{FFFE}/miu', // restore quoted periods
],
[
'$1!$2:$3',
'$1!$2',
'$1:$2',
'$1',
'.',
],
$value
self::replaceQuotedPeriod($value)
);
// Convert references to defined names/formulae
$value = str_replace('$$', '', $value);
@@ -85,7 +104,18 @@ class FormulaTranslator
Calculation::FORMULA_CLOSE_MATRIX_BRACE
);
$value = (string) preg_replace('/COM\.MICROSOFT\./ui', '', $value);
$value = Preg::replace(
[
'/\b(?<!com[.]microsoft[.])'
. '(floor|ceiling)\s*[(]/ui',
'/COM\.MICROSOFT\./ui',
],
[
'$1.ODS(',
'',
],
$value
);
}
}

View File

@@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use DOMDocument;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use stdClass;
class PageSettings
{
@@ -21,6 +22,7 @@ class PageSettings
*/
private array $tableStylesCrossReference = [];
/** @var mixed[] */
private array $pageLayoutStyles = [];
/**
@@ -151,12 +153,15 @@ class PageSettings
if (!array_key_exists($printSettingsIndex, $this->pageLayoutStyles)) {
return;
}
/** @var (object{orientation: string, scale: int|string, printOrder: ?string,
* horizontalCentered: bool, verticalCentered: bool, marginLeft: float, marginRight: float, marginTop: float,
* marginBottom: float, marginHeader: float, marginFooter: float}&stdClass) */
$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, '%'))
->setScale((int) trim((string) $printSettings->scale, '%'))
->setHorizontalCentered($printSettings->horizontalCentered)
->setVerticalCentered($printSettings->verticalCentered);

View File

@@ -15,10 +15,11 @@ class Properties
$this->spreadsheet = $spreadsheet;
}
/** @param array{meta?: string, office?: string, dc?: string} $namespacesMeta */
public function load(SimpleXMLElement $xml, array $namespacesMeta): void
{
$docProps = $this->spreadsheet->getProperties();
$officeProperty = $xml->children($namespacesMeta['office']);
$officeProperty = $xml->children($namespacesMeta['office'] ?? '');
foreach ($officeProperty as $officePropertyData) {
if (isset($namespacesMeta['dc'])) {
$officePropertiesDC = $officePropertyData->children($namespacesMeta['dc']);
@@ -27,7 +28,7 @@ class Properties
$officePropertyMeta = null;
if (isset($namespacesMeta['dc'])) {
$officePropertyMeta = $officePropertyData->children($namespacesMeta['meta']);
$officePropertyMeta = $officePropertyData->children($namespacesMeta['meta'] ?? '');
}
$officePropertyMeta = $officePropertyMeta ?? [];
foreach ($officePropertyMeta as $propertyName => $propertyValue) {
@@ -66,13 +67,14 @@ class Properties
}
}
/** @param array{meta?: string, office?: mixed, dc?: mixed} $namespacesMeta */
private function setMetaProperties(
array $namespacesMeta,
SimpleXMLElement $propertyValue,
string $propertyName,
DocumentProperties $docProps
): void {
$propertyValueAttributes = $propertyValue->attributes($namespacesMeta['meta']);
$propertyValueAttributes = $propertyValue->attributes($namespacesMeta['meta'] ?? '');
$propertyValue = (string) $propertyValue;
switch ($propertyName) {
case 'initial-creator':
@@ -101,14 +103,17 @@ class Properties
}
}
/** @param iterable<string> $propertyValueAttributes */
private function setUserDefinedProperty(iterable $propertyValueAttributes, string $propertyValue, DocumentProperties $docProps): void
{
$propertyValueName = '';
$propertyValueType = DocumentProperties::PROPERTY_TYPE_STRING;
foreach ($propertyValueAttributes as $key => $value) {
if ($key == 'name') {
/** @var scalar $value */
$propertyValueName = (string) $value;
} elseif ($key == 'value-type') {
/** @var string $value */
switch ($value) {
case 'date':
$propertyValue = DocumentProperties::convertProperty($propertyValue, 'date');

View File

@@ -6,8 +6,8 @@ use PhpOffice\PhpSpreadsheet\Reader;
class XmlScanner
{
private const ENCODING_PATTERN = '/encoding\\s*=\\s*(["\'])(.+?)\\1/s';
private const ENCODING_UTF7 = '/encoding\\s*=\\s*(["\'])UTF-7\\1/si';
private const ENCODING_PATTERN = '/encoding\s*=\s*(["\'])(.+?)\1/s';
private const ENCODING_UTF7 = '/encoding\s*=\s*(["\'])UTF-7\1/si';
private string $pattern;
@@ -41,7 +41,7 @@ class XmlScanner
$charset = $this->findCharSet($xml);
$foundUtf7 = $charset === 'UTF-7';
if ($charset !== 'UTF-8') {
$testStart = '/^.{0,4}\\s*<?xml/s';
$testStart = '/^.{0,4}\s*<?xml/s';
$startWithXml1 = preg_match($testStart, $xml);
$xml = self::forceString(mb_convert_encoding($xml, 'UTF-8', $charset));
if ($startWithXml1 === 1 && preg_match($testStart, $xml) !== 1) {
@@ -87,7 +87,7 @@ class XmlScanner
public function scan($xml): string
{
// Don't rely purely on libxml_disable_entity_loader()
$pattern = '/\\0*' . implode('\\0*', str_split($this->pattern)) . '\\0*/';
$pattern = '/\0*' . implode('\0*', mb_str_split($this->pattern, 1, 'UTF-8')) . '\0*/';
$xml = "$xml";
if (preg_match($pattern, $xml)) {
@@ -95,7 +95,6 @@ class XmlScanner
}
$xml = $this->toUtf8($xml);
if (preg_match($pattern, $xml)) {
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
}
@@ -103,6 +102,7 @@ class XmlScanner
if ($this->callback !== null) {
$xml = call_user_func($this->callback, $xml);
}
/** @var string $xml */
return $xml;
}

View File

@@ -21,6 +21,8 @@ class Slk extends BaseReader
/**
* Formats.
*
* @var mixed[]
*/
private array $formats = [];
@@ -31,6 +33,8 @@ class Slk extends BaseReader
/**
* Fonts.
*
* @var mixed[]
*/
private array $fonts = [];
@@ -84,6 +88,8 @@ class Slk extends BaseReader
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
* @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
*/
public function listWorksheetInfo(string $filename): array
{
@@ -92,8 +98,7 @@ class Slk extends BaseReader
$fileHandle = $this->fileHandle;
rewind($fileHandle);
$worksheetInfo = [];
$worksheetInfo[0]['worksheetName'] = basename($filename, '.slk');
$worksheetInfo = [['worksheetName' => basename($filename, '.slk')]];
// loop through one row (line) at a time in the file
$rowIndex = 0;
@@ -131,6 +136,7 @@ class Slk extends BaseReader
$worksheetInfo[0]['totalRows'] = $rowIndex;
$worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
$worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
$worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
// Close file
fclose($fileHandle);
@@ -143,8 +149,8 @@ class Slk extends BaseReader
*/
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet = $this->newSpreadsheet();
$spreadsheet->setValueBinder($this->valueBinder);
// Load into this instance
return $this->loadIntoExisting($filename, $spreadsheet);
@@ -215,6 +221,7 @@ class Slk extends BaseReader
$hasCalculatedValue = true;
}
/** @param mixed[] $rowData */
private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void
{
// Read cell value data
@@ -224,6 +231,7 @@ class Slk extends BaseReader
$sharedColumn = $sharedRow = -1;
$sharedFormula = false;
foreach ($rowData as $rowDatum) {
/** @var string $rowDatum */
switch ($rowDatum[0]) {
case 'X':
$column = substr($rowDatum, 1);
@@ -299,6 +307,7 @@ class Slk extends BaseReader
}
}
/** @param mixed[] $rowData */
private function processFRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void
{
// Read cell formatting
@@ -307,6 +316,7 @@ class Slk extends BaseReader
$fontStyle = '';
$styleData = [];
foreach ($rowData as $rowDatum) {
/** @var string $rowDatum */
switch ($rowDatum[0]) {
case 'C':
case 'X':
@@ -332,6 +342,7 @@ class Slk extends BaseReader
break;
}
}
/** @var string $formatStyle */
$this->addFormats($spreadsheet, $formatStyle, $row, $column);
$this->addFonts($spreadsheet, $fontStyle, $row, $column);
$this->addStyle($spreadsheet, $styleData, $row, $column);
@@ -347,6 +358,7 @@ class Slk extends BaseReader
'T' => 'top',
];
/** @param mixed[][] $styleData */
private function styleSettings(string $rowDatum, array &$styleData, string &$fontStyle): void
{
$styleSettings = substr($rowDatum, 1);
@@ -356,11 +368,11 @@ class Slk extends BaseReader
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;
$styleData['borders'][self::STYLE_SETTINGS_BORDER[$char]]['borderStyle'] = Border::BORDER_THIN; //* @phpstan-ignore-line
} elseif ($char == 'S') {
$styleData['fill']['fillType'] = Fill::FILL_PATTERN_GRAY125;
} elseif ($char == 'M') {
if (preg_match('/M([1-9]\\d*)/', $styleSettings, $matches)) {
if (preg_match('/M([1-9]\d*)/', $styleSettings, $matches)) {
$fontStyle = $matches[1];
}
}
@@ -371,7 +383,7 @@ class Slk extends BaseReader
{
if ($formatStyle && $column > '' && $row > '') {
$columnLetter = Coordinate::stringFromColumnIndex((int) $column);
if (isset($this->formats[$formatStyle])) {
if (isset($this->formats[$formatStyle]) && is_array($this->formats[$formatStyle])) {
$spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->formats[$formatStyle]);
}
}
@@ -381,12 +393,13 @@ class Slk extends BaseReader
{
if ($fontStyle && $column > '' && $row > '') {
$columnLetter = Coordinate::stringFromColumnIndex((int) $column);
if (isset($this->fonts[$fontStyle])) {
if (isset($this->fonts[$fontStyle]) && is_array($this->fonts[$fontStyle])) {
$spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->fonts[$fontStyle]);
}
}
}
/** @param mixed[] $styleData */
private function addStyle(Spreadsheet &$spreadsheet, array $styleData, string $row, string $column): void
{
if ((!empty($styleData)) && $column > '' && $row > '') {
@@ -406,12 +419,18 @@ class Slk extends BaseReader
$endCol = Coordinate::stringFromColumnIndex((int) $endCol);
$spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth);
do {
$spreadsheet->getActiveSheet()->getColumnDimension((string) ++$startCol)->setWidth((float) $columnWidth);
/** @var string $startCol */
$spreadsheet->getActiveSheet()
->getColumnDimension(
StringHelper::stringIncrement($startCol)
)
->setWidth((float) $columnWidth);
} while ($startCol !== $endCol);
}
}
}
/** @param string[] $rowData */
private function processPRecord(array $rowData, Spreadsheet &$spreadsheet): void
{
// Read shared styles
@@ -434,6 +453,7 @@ class Slk extends BaseReader
break;
case 'L':
/** @var mixed[][][] $formatArray */
$this->processPColors($rowDatum, $formatArray);
break;
@@ -446,14 +466,16 @@ class Slk extends BaseReader
$this->processPFinal($spreadsheet, $formatArray);
}
/** @param mixed[][][] $formatArray */
private function processPColors(string $rowDatum, array &$formatArray): void
{
if (preg_match('/L([1-9]\\d*)/', $rowDatum, $matches)) {
if (preg_match('/L([1-9]\d*)/', $rowDatum, $matches)) {
$fontColor = ((int) $matches[1]) % 8;
$formatArray['font']['color']['argb'] = self::COLOR_ARRAY[$fontColor];
}
}
/** @param mixed[][] $formatArray */
private function processPFontStyles(string $rowDatum, array &$formatArray): void
{
$styleSettings = substr($rowDatum, 1);
@@ -465,6 +487,7 @@ class Slk extends BaseReader
}
}
/** @param mixed[] $formatArray */
private function processPFinal(Spreadsheet &$spreadsheet, array $formatArray): void
{
if (array_key_exists('numberFormat', $formatArray)) {
@@ -530,6 +553,7 @@ class Slk extends BaseReader
return $spreadsheet;
}
/** @param string[] $rowData */
private function columnRowFromRowData(array $rowData, string &$column, string &$row): void
{
foreach ($rowData as $rowDatum) {

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,9 @@ class Color
* Read color.
*
* @param int $color Indexed color
* @param array $palette Color palette
* @param string[][] $palette Color palette
*
* @return array RGB color value, example: ['rgb' => 'FF0000']
* @return string[] RGB color value, example: ['rgb' => 'FF0000']
*/
public static function map(int $color, array $palette, int $version): array
{
@@ -24,12 +24,6 @@ class Color
return $palette[$color - 8];
}
// default color table
if ($version == Xls::XLS_BIFF8) {
return Color\BIFF8::lookup($color);
}
// BIFF5
return Color\BIFF5::lookup($color);
return ($version === Xls::XLS_BIFF8) ? Color\BIFF8::lookup($color) : Color\BIFF5::lookup($color);
}
}

View File

@@ -65,6 +65,8 @@ class BIFF5
/**
* Map color array from BIFF5 built-in color index.
*
* @return array{rgb: string}
*/
public static function lookup(int $color): array
{

View File

@@ -65,6 +65,8 @@ class BIFF8
/**
* Map color array from BIFF8 built-in color index.
*
* @return array{rgb: string}
*/
public static function lookup(int $color): array
{

View File

@@ -21,6 +21,8 @@ class BuiltIn
* Map built-in color to RGB value.
*
* @param int $color Indexed color
*
* @return array{rgb: string}
*/
public static function lookup(int $color): array
{

View File

@@ -2,9 +2,14 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Style;
class ConditionalFormatting
class ConditionalFormatting extends Xls
{
/**
* @var array<int, string>
@@ -31,19 +36,310 @@ class ConditionalFormatting
public static function type(int $type): ?string
{
if (isset(self::$types[$type])) {
return self::$types[$type];
}
return null;
return self::$types[$type] ?? null;
}
public static function operator(int $operator): ?string
{
if (isset(self::$operators[$operator])) {
return self::$operators[$operator];
return self::$operators[$operator] ?? null;
}
/**
* Parse conditional formatting blocks.
*
* @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE
*
* @return mixed[]
*/
protected function readCFHeader2(Xls $xls): array
{
$length = self::getUInt2d($xls->data, $xls->pos + 2);
$recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length);
// move stream pointer forward to next record
$xls->pos += 4 + $length;
if ($xls->readDataOnly) {
return [];
}
return null;
// offset: 0; size: 2; Rule Count
// $ruleCount = self::getUInt2d($recordData, 0);
// offset: var; size: var; cell range address list with
$cellRangeAddressList = ($xls->version == self::XLS_BIFF8)
? Biff8::readBIFF8CellRangeAddressList(substr($recordData, 12))
: Biff5::readBIFF5CellRangeAddressList(substr($recordData, 12));
$cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
return $cellRangeAddresses;
}
/** @param string[] $cellRangeAddresses */
protected function readCFRule2(array $cellRangeAddresses, Xls $xls): void
{
$length = self::getUInt2d($xls->data, $xls->pos + 2);
$recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length);
// move stream pointer forward to next record
$xls->pos += 4 + $length;
if ($xls->readDataOnly) {
return;
}
// offset: 0; size: 2; Options
$cfRule = self::getUInt2d($recordData, 0);
// bit: 8-15; mask: 0x00FF; type
$type = (0x00FF & $cfRule) >> 0;
$type = self::type($type);
// bit: 0-7; mask: 0xFF00; type
$operator = (0xFF00 & $cfRule) >> 8;
$operator = self::operator($operator);
if ($type === null || $operator === null) {
return;
}
// offset: 2; size: 2; Size1
$size1 = self::getUInt2d($recordData, 2);
// offset: 4; size: 2; Size2
$size2 = self::getUInt2d($recordData, 4);
// offset: 6; size: 4; Options
$options = self::getInt4d($recordData, 6);
$style = new Style(false, true); // non-supervisor, conditional
$noFormatSet = true;
//$xls->getCFStyleOptions($options, $style);
$hasFontRecord = (bool) ((0x04000000 & $options) >> 26);
$hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27);
$hasBorderRecord = (bool) ((0x10000000 & $options) >> 28);
$hasFillRecord = (bool) ((0x20000000 & $options) >> 29);
$hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30);
// note unexpected values for following 4
$hasBorderLeft = !(bool) (0x00000400 & $options);
$hasBorderRight = !(bool) (0x00000800 & $options);
$hasBorderTop = !(bool) (0x00001000 & $options);
$hasBorderBottom = !(bool) (0x00002000 & $options);
$offset = 12;
if ($hasFontRecord === true) {
$fontStyle = substr($recordData, $offset, 118);
$this->getCFFontStyle($fontStyle, $style, $xls);
$offset += 118;
$noFormatSet = false;
}
if ($hasAlignmentRecord === true) {
//$alignmentStyle = substr($recordData, $offset, 8);
//$this->getCFAlignmentStyle($alignmentStyle, $style, $xls);
$offset += 8;
}
if ($hasBorderRecord === true) {
$borderStyle = substr($recordData, $offset, 8);
$this->getCFBorderStyle($borderStyle, $style, $hasBorderLeft, $hasBorderRight, $hasBorderTop, $hasBorderBottom, $xls);
$offset += 8;
$noFormatSet = false;
}
if ($hasFillRecord === true) {
$fillStyle = substr($recordData, $offset, 4);
$this->getCFFillStyle($fillStyle, $style, $xls);
$offset += 4;
$noFormatSet = false;
}
if ($hasProtectionRecord === true) {
//$protectionStyle = substr($recordData, $offset, 4);
//$this->getCFProtectionStyle($protectionStyle, $style, $xls);
$offset += 2;
}
$formula1 = $formula2 = null;
if ($size1 > 0) {
$formula1 = $this->readCFFormula($recordData, $offset, $size1, $xls);
if ($formula1 === null) {
return;
}
$offset += $size1;
}
if ($size2 > 0) {
$formula2 = $this->readCFFormula($recordData, $offset, $size2, $xls);
if ($formula2 === null) {
return;
}
$offset += $size2;
}
$this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style, $noFormatSet, $xls);
}
/*private function getCFStyleOptions(int $options, Style $style, Xls $xls): void
{
}*/
private function getCFFontStyle(string $options, Style $style, Xls $xls): void
{
$fontSize = self::getInt4d($options, 64);
if ($fontSize !== -1) {
$style->getFont()->setSize($fontSize / 20); // Convert twips to points
}
$options68 = self::getInt4d($options, 68);
$options88 = self::getInt4d($options, 88);
if (($options88 & 2) === 0) {
$bold = self::getUInt2d($options, 72); // 400 = normal, 700 = bold
if ($bold !== 0) {
$style->getFont()->setBold($bold >= 550);
}
if (($options68 & 2) !== 0) {
$style->getFont()->setItalic(true);
}
}
if (($options88 & 0x80) === 0) {
if (($options68 & 0x80) !== 0) {
$style->getFont()->setStrikethrough(true);
}
}
$color = self::getInt4d($options, 80);
if ($color !== -1) {
$style->getFont()
->getColor()
->setRGB(Color::map($color, $xls->palette, $xls->version)['rgb']);
}
}
/*private function getCFAlignmentStyle(string $options, Style $style, Xls $xls): void
{
}*/
private function getCFBorderStyle(string $options, Style $style, bool $hasBorderLeft, bool $hasBorderRight, bool $hasBorderTop, bool $hasBorderBottom, Xls $xls): void
{
/** @var false|int[] */
$valueArray = unpack('V', $options);
$value = is_array($valueArray) ? $valueArray[1] : 0;
$left = $value & 15;
$right = ($value >> 4) & 15;
$top = ($value >> 8) & 15;
$bottom = ($value >> 12) & 15;
$leftc = ($value >> 16) & 0x7F;
$rightc = ($value >> 23) & 0x7F;
/** @var false|int[] */
$valueArray = unpack('V', substr($options, 4));
$value = is_array($valueArray) ? $valueArray[1] : 0;
$topc = $value & 0x7F;
$bottomc = ($value & 0x3F80) >> 7;
if ($hasBorderLeft) {
$style->getBorders()->getLeft()
->setBorderStyle(self::BORDER_STYLE_MAP[$left]);
$style->getBorders()->getLeft()->getColor()
->setRGB(Color::map($leftc, $xls->palette, $xls->version)['rgb']);
}
if ($hasBorderRight) {
$style->getBorders()->getRight()
->setBorderStyle(self::BORDER_STYLE_MAP[$right]);
$style->getBorders()->getRight()->getColor()
->setRGB(Color::map($rightc, $xls->palette, $xls->version)['rgb']);
}
if ($hasBorderTop) {
$style->getBorders()->getTop()
->setBorderStyle(self::BORDER_STYLE_MAP[$top]);
$style->getBorders()->getTop()->getColor()
->setRGB(Color::map($topc, $xls->palette, $xls->version)['rgb']);
}
if ($hasBorderBottom) {
$style->getBorders()->getBottom()
->setBorderStyle(self::BORDER_STYLE_MAP[$bottom]);
$style->getBorders()->getBottom()->getColor()
->setRGB(Color::map($bottomc, $xls->palette, $xls->version)['rgb']);
}
}
private function getCFFillStyle(string $options, Style $style, Xls $xls): void
{
$fillPattern = self::getUInt2d($options, 0);
// bit: 10-15; mask: 0xFC00; type
$fillPattern = (0xFC00 & $fillPattern) >> 10;
$fillPattern = FillPattern::lookup($fillPattern);
$fillPattern = $fillPattern === Fill::FILL_NONE ? Fill::FILL_SOLID : $fillPattern;
if ($fillPattern !== Fill::FILL_NONE) {
$style->getFill()->setFillType($fillPattern);
$fillColors = self::getUInt2d($options, 2);
// bit: 0-6; mask: 0x007F; type
$color1 = (0x007F & $fillColors) >> 0;
// bit: 7-13; mask: 0x3F80; type
$color2 = (0x3F80 & $fillColors) >> 7;
if ($fillPattern === Fill::FILL_SOLID) {
$style->getFill()->getStartColor()->setRGB(Color::map($color2, $xls->palette, $xls->version)['rgb']);
} else {
$style->getFill()->getStartColor()->setRGB(Color::map($color1, $xls->palette, $xls->version)['rgb']);
$style->getFill()->getEndColor()->setRGB(Color::map($color2, $xls->palette, $xls->version)['rgb']);
}
}
}
/*private function getCFProtectionStyle(string $options, Style $style, Xls $xls): void
{
}*/
private function readCFFormula(string $recordData, int $offset, int $size, Xls $xls): float|int|string|null
{
try {
$formula = substr($recordData, $offset, $size);
$formula = pack('v', $size) . $formula; // prepend the length
$formula = $xls->getFormulaFromStructure($formula);
if (is_numeric($formula)) {
return (str_contains($formula, '.')) ? (float) $formula : (int) $formula;
}
return $formula;
} catch (PhpSpreadsheetException) {
return null;
}
}
/** @param string[] $cellRanges */
private function setCFRules(array $cellRanges, string $type, string $operator, null|float|int|string $formula1, null|float|int|string $formula2, Style $style, bool $noFormatSet, Xls $xls): void
{
foreach ($cellRanges as $cellRange) {
$conditional = new Conditional();
$conditional->setNoFormatSet($noFormatSet);
$conditional->setConditionType($type);
$conditional->setOperatorType($operator);
$conditional->setStopIfTrue(true);
if ($formula1 !== null) {
$conditional->addCondition($formula1);
}
if ($formula2 !== null) {
$conditional->addCondition($formula2);
}
$conditional->setStyle($style);
$conditionalStyles = $xls->phpSheet
->getStyle($cellRange)
->getConditionalStyles();
$conditionalStyles[] = $conditional;
$xls->phpSheet
->getStyle($cellRange)
->setConditionalStyles($conditionalStyles);
}
}
}

View File

@@ -2,9 +2,13 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet as XlsWorksheet;
class DataValidationHelper
class DataValidationHelper extends Xls
{
/**
* @var array<int, string>
@@ -45,28 +49,171 @@ class DataValidationHelper
public static function type(int $type): ?string
{
if (isset(self::$types[$type])) {
return self::$types[$type];
}
return null;
return self::$types[$type] ?? null;
}
public static function errorStyle(int $errorStyle): ?string
{
if (isset(self::$errorStyles[$errorStyle])) {
return self::$errorStyles[$errorStyle];
}
return null;
return self::$errorStyles[$errorStyle] ?? null;
}
public static function operator(int $operator): ?string
{
if (isset(self::$operators[$operator])) {
return self::$operators[$operator];
return self::$operators[$operator] ?? null;
}
/**
* Read DATAVALIDATION record.
*/
protected function readDataValidation2(Xls $xls): void
{
$length = self::getUInt2d($xls->data, $xls->pos + 2);
$recordData = $xls->readRecordData($xls->data, $xls->pos + 4, $length);
// move stream pointer forward to next record
$xls->pos += 4 + $length;
if ($xls->readDataOnly) {
return;
}
return null;
// offset: 0; size: 4; Options
$options = self::getInt4d($recordData, 0);
// bit: 0-3; mask: 0x0000000F; type
$type = (0x0000000F & $options) >> 0;
$type = self::type($type);
// bit: 4-6; mask: 0x00000070; error type
$errorStyle = (0x00000070 & $options) >> 4;
$errorStyle = self::errorStyle($errorStyle);
// bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list)
// I have only seen cases where this is 1
//$explicitFormula = (0x00000080 & $options) >> 7;
// bit: 8; mask: 0x00000100; 1= empty cells allowed
$allowBlank = (0x00000100 & $options) >> 8;
// bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity
$suppressDropDown = (0x00000200 & $options) >> 9;
// bit: 18; mask: 0x00040000; 1= show prompt box if cell selected
$showInputMessage = (0x00040000 & $options) >> 18;
// bit: 19; mask: 0x00080000; 1= show error box if invalid values entered
$showErrorMessage = (0x00080000 & $options) >> 19;
// bit: 20-23; mask: 0x00F00000; condition operator
$operator = (0x00F00000 & $options) >> 20;
$operator = self::operator($operator);
if ($type === null || $errorStyle === null || $operator === null) {
return;
}
// offset: 4; size: var; title of the prompt box
$offset = 4;
$string = self::readUnicodeStringLong(substr($recordData, $offset));
$promptTitle = $string['value'] !== chr(0) ? $string['value'] : '';
$offset += $string['size'];
// offset: var; size: var; title of the error box
$string = self::readUnicodeStringLong(substr($recordData, $offset));
$errorTitle = $string['value'] !== chr(0) ? $string['value'] : '';
$offset += $string['size'];
// offset: var; size: var; text of the prompt box
$string = self::readUnicodeStringLong(substr($recordData, $offset));
$prompt = $string['value'] !== chr(0) ? $string['value'] : '';
$offset += $string['size'];
// offset: var; size: var; text of the error box
$string = self::readUnicodeStringLong(substr($recordData, $offset));
$error = $string['value'] !== chr(0) ? $string['value'] : '';
$offset += $string['size'];
// offset: var; size: 2; size of the formula data for the first condition
$sz1 = self::getUInt2d($recordData, $offset);
$offset += 2;
// offset: var; size: 2; not used
$offset += 2;
// offset: var; size: $sz1; formula data for first condition (without size field)
$formula1 = substr($recordData, $offset, $sz1);
$formula1 = pack('v', $sz1) . $formula1; // prepend the length
try {
$formula1 = $xls->getFormulaFromStructure($formula1);
// in list type validity, null characters are used as item separators
if ($type == DataValidation::TYPE_LIST) {
$formula1 = str_replace(chr(0), ',', $formula1);
}
} catch (PhpSpreadsheetException $e) {
return;
}
$offset += $sz1;
// offset: var; size: 2; size of the formula data for the first condition
$sz2 = self::getUInt2d($recordData, $offset);
$offset += 2;
// offset: var; size: 2; not used
$offset += 2;
// offset: var; size: $sz2; formula data for second condition (without size field)
$formula2 = substr($recordData, $offset, $sz2);
$formula2 = pack('v', $sz2) . $formula2; // prepend the length
try {
$formula2 = $xls->getFormulaFromStructure($formula2);
} catch (PhpSpreadsheetException) {
return;
}
$offset += $sz2;
// offset: var; size: var; cell range address list with
$cellRangeAddressList = Biff8::readBIFF8CellRangeAddressList(substr($recordData, $offset));
/** @var string[] */
$cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
$maxRow = (string) AddressRange::MAX_ROW;
$maxCol = AddressRange::MAX_COLUMN;
$maxXlsRow = (string) XlsWorksheet::MAX_XLS_ROW;
$maxXlsColumnString = (string) XlsWorksheet::MAX_XLS_COLUMN_STRING;
foreach ($cellRangeAddresses as $cellRange) {
$cellRange = preg_replace(
[
"/([a-z]+)1:([a-z]+)$maxXlsRow/i",
"/([a-z]+\\d+):([a-z]+)$maxXlsRow/i",
"/A(\\d+):$maxXlsColumnString(\\d+)/i",
"/([a-z]+\\d+):$maxXlsColumnString(\\d+)/i",
],
[
'$1:$2',
'$1:${2}' . $maxRow,
'$1:$2',
'$1:' . $maxCol . '$2',
],
$cellRange
) ?? $cellRange;
$objValidation = new DataValidation();
$objValidation->setType($type);
$objValidation->setErrorStyle($errorStyle);
$objValidation->setAllowBlank((bool) $allowBlank);
$objValidation->setShowInputMessage((bool) $showInputMessage);
$objValidation->setShowErrorMessage((bool) $showErrorMessage);
$objValidation->setShowDropDown(!$suppressDropDown);
$objValidation->setOperator($operator);
$objValidation->setErrorTitle($errorTitle);
$objValidation->setError($error);
$objValidation->setPromptTitle($promptTitle);
$objValidation->setPrompt($prompt);
$objValidation->setFormula1($formula1);
$objValidation->setFormula2($formula2);
$xls->phpSheet->setDataValidation($cellRange, $objValidation);
}
}
}

View File

@@ -2,6 +2,8 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
class MD5
{
private int $a;
@@ -58,8 +60,9 @@ class MD5
*/
public function add(string $data): void
{
// @phpstan-ignore-next-line
$words = array_values(unpack('V16', $data));
$unpacked = unpack('V16', $data) ?: throw new ReaderException('unable to unpack data');
/** @var int[] */
$words = array_values($unpacked);
$A = $this->a;
$B = $this->b;
@@ -173,7 +176,9 @@ class MD5
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;
/** @var int */
$temp = call_user_func($func, $B, $C, $D);
$A = (int) ($A + $temp + $M + $t) & self::$allOneBits;
$A = self::rotate($A, $s);
$A = (int) ($B + $A) & self::$allOneBits;
}

View File

@@ -28,10 +28,6 @@ class Border
public static function lookup(int $index): string
{
if (isset(self::$borderStyleMap[$index])) {
return self::$borderStyleMap[$index];
}
return StyleBorder::BORDER_NONE;
return self::$borderStyleMap[$index] ?? StyleBorder::BORDER_NONE;
}
}

View File

@@ -37,10 +37,6 @@ class FillPattern
*/
public static function lookup(int $index): string
{
if (isset(self::$fillPatternMap[$index])) {
return self::$fillPatternMap[$index];
}
return Fill::FILL_NONE;
return self::$fillPatternMap[$index] ?? Fill::FILL_NONE;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -51,6 +51,7 @@ class AutoFilter
// Entries can be either filter elements
foreach ($filterColumn->filters->filter as $filterRule) {
// Operator is undefined, but always treated as EQUAL
/** @var SimpleXMLElement */
$attr2 = $filterRule->attributes() ?? ['val' => ''];
$column->createRule()->setRule('', (string) $attr2['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER);
}
@@ -103,6 +104,7 @@ class AutoFilter
$column->setJoin(Column::AUTOFILTER_COLUMN_JOIN_AND);
}
foreach ($customFilters->customFilter as $filterRule) {
/** @var SimpleXMLElement */
$attr2 = $filterRule->attributes() ?? ['operator' => '', 'val' => ''];
$column->createRule()->setRule(
(string) $attr2['operator'],

View File

@@ -95,6 +95,7 @@ class Chart
$gapWidth = null;
$useUpBars = null;
$useDownBars = null;
$noBorder = false;
foreach ($chartElementsC as $chartElementKey => $chartElement) {
switch ($chartElementKey) {
case 'spPr':
@@ -108,6 +109,9 @@ class Chart
if (isset($children->ln)) {
$chartBorderLines = new GridLines();
$this->readLineStyle($chartElementsC, $chartBorderLines);
if (isset($children->ln->noFill)) {
$noBorder = true;
}
}
break;
@@ -470,6 +474,7 @@ class Chart
if ($chartBorderLines !== null) {
$chart->setBorderLines($chartBorderLines);
}
$chart->setNoBorder($noBorder);
$chart->setRoundedCorners($roundedCorners);
if (is_bool($autoTitleDeleted)) {
$chart->setAutoTitleDeleted($autoTitleDeleted);
@@ -853,6 +858,7 @@ class Chart
$seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize");
if (isset($seriesDetail->strRef->strCache)) {
/** @var array{formatCode: string, dataValues: mixed[]} */
$seriesData = $this->chartDataSeriesValues($seriesDetail->strRef->strCache->children($this->cNamespace), 's');
$seriesValues
->setFormatCode($seriesData['formatCode'])
@@ -864,6 +870,7 @@ class Chart
$seriesSource = (string) $seriesDetail->numRef->f;
$seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize");
if (isset($seriesDetail->numRef->numCache)) {
/** @var array{formatCode: string, dataValues: mixed[]} */
$seriesData = $this->chartDataSeriesValues($seriesDetail->numRef->numCache->children($this->cNamespace));
$seriesValues
->setFormatCode($seriesData['formatCode'])
@@ -876,6 +883,7 @@ class Chart
$seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize");
if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) {
/** @var array{formatCode: string, dataValues: mixed[]} */
$seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($this->cNamespace), 's');
$seriesValues
->setFormatCode($seriesData['formatCode'])
@@ -888,6 +896,7 @@ class Chart
$seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize");
if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) {
/** @var array{formatCode: string, dataValues: mixed[]} */
$seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($this->cNamespace), 's');
$seriesValues
->setFormatCode($seriesData['formatCode'])
@@ -910,6 +919,7 @@ class Chart
return null;
}
/** @return mixed[] */
private function chartDataSeriesValues(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array
{
$seriesVal = [];
@@ -948,6 +958,7 @@ class Chart
];
}
/** @return mixed[] */
private function chartDataSeriesValuesMultiLevel(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array
{
$seriesVal = [];
@@ -1187,31 +1198,45 @@ class Chart
}
$fontArray = [];
$fontArray['size'] = self::getAttributeInteger($titleDetailPart->pPr->defRPr, 'sz');
$fontArray['bold'] = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'b');
$fontArray['italic'] = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'i');
if ($fontArray['size'] !== null && $fontArray['size'] >= 100) {
$fontArray['size'] /= 100.0;
}
if ($fontArray['size'] !== null) {
$fontArray['size'] = (int) ($fontArray['size']);
}
$fontArray['bold'] = (bool) self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'b');
$fontArray['italic'] = (bool) self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'i');
$fontArray['underscore'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'u');
$fontArray['strikethrough'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'strike');
$fontArray['cap'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'cap');
$strikethrough = self::getAttributeString($titleDetailPart->pPr->defRPr, 'strike');
if ($strikethrough !== null) {
if ($strikethrough == 'noStrike') {
$fontArray['strikethrough'] = false;
} else {
$fontArray['strikethrough'] = true;
}
}
$fontArray['cap'] = (string) self::getAttributeString($titleDetailPart->pPr->defRPr, 'cap');
if (isset($titleDetailPart->pPr->defRPr->latin)) {
$fontArray['latin'] = self::getAttributeString($titleDetailPart->pPr->defRPr->latin, 'typeface');
$fontArray['latin'] = (string) self::getAttributeString($titleDetailPart->pPr->defRPr->latin, 'typeface');
}
if (isset($titleDetailPart->pPr->defRPr->ea)) {
$fontArray['eastAsian'] = self::getAttributeString($titleDetailPart->pPr->defRPr->ea, 'typeface');
$fontArray['eastAsian'] = (string) self::getAttributeString($titleDetailPart->pPr->defRPr->ea, 'typeface');
}
if (isset($titleDetailPart->pPr->defRPr->cs)) {
$fontArray['complexScript'] = self::getAttributeString($titleDetailPart->pPr->defRPr->cs, 'typeface');
$fontArray['complexScript'] = (string) self::getAttributeString($titleDetailPart->pPr->defRPr->cs, 'typeface');
}
if (isset($titleDetailPart->pPr->defRPr->solidFill)) {
$fontArray['chartColor'] = new ChartColor($this->readColor($titleDetailPart->pPr->defRPr->solidFill));
}
$font = new Font();
$font->setSize(null, true);
//$font->setSize(null, true);
$font->applyFromArray($fontArray);
return $font;
}
/** @return mixed[] */
private function readChartAttributes(?SimpleXMLElement $chartDetail): array
{
$plotAttributes = [];
@@ -1269,9 +1294,11 @@ class Chart
return $plotAttributes;
}
/** @param array<mixed> $plotAttributes */
private function setChartAttributes(Layout $plotArea, array $plotAttributes): void
{
foreach ($plotAttributes as $plotAttributeKey => $plotAttributeValue) {
/** @var ?bool $plotAttributeValue */
switch ($plotAttributeKey) {
case 'showLegendKey':
$plotArea->setShowLegendKey($plotAttributeValue);
@@ -1300,6 +1327,11 @@ class Chart
case 'showLeaderLines':
$plotArea->setShowLeaderLines($plotAttributeValue);
break;
case 'labelFont':
/** @var ?Font $plotAttributeValue */
$plotArea->setLabelFont($plotAttributeValue);
break;
}
}
@@ -1385,6 +1417,7 @@ class Chart
'innerShdw',
];
/** @return array{type: ?string, value: ?string, alpha: ?int, brightness: ?int} */
private function readColor(SimpleXMLElement $colorXml): array
{
$result = [

View File

@@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Reader\DefaultReadFilter;
use PhpOffice\PhpSpreadsheet\Reader\IReadFilter;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
@@ -24,8 +25,7 @@ class ColumnAndRowAttributes extends BaseParserClass
* 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', ... ?
* @param array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float, width?: int} $columnAttributes array of attributes (indexes are attribute name, values are value)
*/
private function setColumnAttributes(string $columnAddress, array $columnAttributes): void
{
@@ -50,7 +50,7 @@ class ColumnAndRowAttributes extends BaseParserClass
* 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)
* @param array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float} $rowAttributes array of attributes (indexes are attribute name, values are value)
* 'xfIndex', 'visible', 'collapsed', 'outlineLevel', 'rowHeight', ... ?
*/
private function setRowAttributes(int $rowNumber, array $rowAttributes): void
@@ -77,6 +77,9 @@ class ColumnAndRowAttributes extends BaseParserClass
if ($this->worksheetXml === null) {
return;
}
if ($readFilter !== null && $readFilter::class === DefaultReadFilter::class) {
$readFilter = null;
}
$columnsAttributes = [];
$rowsAttributes = [];
@@ -85,11 +88,7 @@ class ColumnAndRowAttributes extends BaseParserClass
}
if ($this->worksheetXml->sheetData && $this->worksheetXml->sheetData->row) {
$rowsAttributes = $this->readRowAttributes($this->worksheetXml->sheetData->row, $readDataOnly, $ignoreRowsWithNoCells);
}
if ($readFilter !== null && $readFilter::class === DefaultReadFilter::class) {
$readFilter = null;
$rowsAttributes = $this->readRowAttributes($this->worksheetXml->sheetData->row, $readDataOnly, $ignoreRowsWithNoCells, $readFilter !== null);
}
// set columns/rows attributes
@@ -100,6 +99,7 @@ class ColumnAndRowAttributes extends BaseParserClass
|| !$this->isFilteredColumn($readFilter, $columnCoordinate, $rowsAttributes)
) {
if (!isset($columnsAttributesAreSet[$columnCoordinate])) {
/** @var array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float, width?: int} $columnAttributes */
$this->setColumnAttributes($columnCoordinate, $columnAttributes);
$columnsAttributesAreSet[$columnCoordinate] = true;
}
@@ -113,6 +113,7 @@ class ColumnAndRowAttributes extends BaseParserClass
|| !$this->isFilteredRow($readFilter, $rowCoordinate, $columnsAttributes)
) {
if (!isset($rowsAttributesAreSet[$rowCoordinate])) {
/** @var array{xfIndex?: int, visible?: bool, collapsed?: bool, collapsed?: bool, outlineLevel?: int, rowHeight?: float} $rowAttributes */
$this->setRowAttributes($rowCoordinate, $rowAttributes);
$rowsAttributesAreSet[$rowCoordinate] = true;
}
@@ -120,17 +121,19 @@ class ColumnAndRowAttributes extends BaseParserClass
}
}
/** @param mixed[] $rowsAttributes */
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;
if ($readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) {
return false;
}
}
return false;
return true;
}
/** @return mixed[] */
private function readColumnAttributes(SimpleXMLElement $worksheetCols, bool $readDataOnly): array
{
$columnAttributes = [];
@@ -140,8 +143,8 @@ class ColumnAndRowAttributes extends BaseParserClass
if ($column !== null) {
$startColumn = Coordinate::stringFromColumnIndex((int) $column['min']);
$endColumn = Coordinate::stringFromColumnIndex((int) $column['max']);
++$endColumn;
for ($columnAddress = $startColumn; $columnAddress !== $endColumn; ++$columnAddress) {
StringHelper::stringIncrement($endColumn);
for ($columnAddress = $startColumn; $columnAddress !== $endColumn; StringHelper::stringIncrement($columnAddress)) {
$columnAttributes[$columnAddress] = $this->readColumnRangeAttributes($column, $readDataOnly);
if ((int) ($column['max']) == 16384) {
@@ -154,6 +157,7 @@ class ColumnAndRowAttributes extends BaseParserClass
return $columnAttributes;
}
/** @return mixed[] */
private function readColumnRangeAttributes(?SimpleXMLElement $column, bool $readDataOnly): array
{
$columnAttributes = [];
@@ -178,6 +182,7 @@ class ColumnAndRowAttributes extends BaseParserClass
return $columnAttributes;
}
/** @param mixed[] $columnsAttributes */
private function isFilteredRow(IReadFilter $readFilter, int $rowCoordinate, array $columnsAttributes): bool
{
foreach ($columnsAttributes as $columnCoordinate => $columnAttributes) {
@@ -189,27 +194,32 @@ class ColumnAndRowAttributes extends BaseParserClass
return false;
}
private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDataOnly, bool $ignoreRowsWithNoCells): array
/** @return mixed[] */
private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDataOnly, bool $ignoreRowsWithNoCells, bool $readFilterIsNotNull): array
{
$rowAttributes = [];
foreach ($worksheetRow as $rowx) {
$row = $rowx->attributes();
if ($row !== null && (!$ignoreRowsWithNoCells || isset($rowx->c))) {
$rowIndex = (int) $row['r'];
if (isset($row['ht']) && !$readDataOnly) {
$rowAttributes[(int) $row['r']]['rowHeight'] = (float) $row['ht'];
$rowAttributes[$rowIndex]['rowHeight'] = (float) $row['ht'];
}
if (isset($row['hidden']) && self::boolean($row['hidden'])) {
$rowAttributes[(int) $row['r']]['visible'] = false;
$rowAttributes[$rowIndex]['visible'] = false;
}
if (isset($row['collapsed']) && self::boolean($row['collapsed'])) {
$rowAttributes[(int) $row['r']]['collapsed'] = true;
$rowAttributes[$rowIndex]['collapsed'] = true;
}
if (isset($row['outlineLevel']) && (int) $row['outlineLevel'] > 0) {
$rowAttributes[(int) $row['r']]['outlineLevel'] = (int) $row['outlineLevel'];
$rowAttributes[$rowIndex]['outlineLevel'] = (int) $row['outlineLevel'];
}
if (isset($row['s']) && !$readDataOnly) {
$rowAttributes[(int) $row['r']]['xfIndex'] = (int) $row['s'];
$rowAttributes[$rowIndex]['xfIndex'] = (int) $row['s'];
}
if ($readFilterIsNotNull && empty($rowAttributes[$rowIndex])) {
$rowAttributes[$rowIndex]['exists'] = true;
}
}
}

View File

@@ -9,6 +9,8 @@ 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\ConditionalFormatting\ConditionalIconSet;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\IconSetValues;
use PhpOffice\PhpSpreadsheet\Style\Style as Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
@@ -20,12 +22,15 @@ class ConditionalStyles
private SimpleXMLElement $worksheetXml;
/** @var string[] */
private array $ns;
/** @var Style[] */
private array $dxfs;
private StyleReader $styleReader;
/** @param Style[] $dxfs */
public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs, StyleReader $styleReader)
{
$this->worksheet = $workSheet;
@@ -59,6 +64,7 @@ class ConditionalStyles
$this->worksheet->setSelectedCells($selectedCells);
}
/** @param Conditional[][] $conditionals */
private function setConditionalsFromExt(array $conditionals): void
{
foreach ($conditionals as $conditionalRange => $cfRules) {
@@ -70,6 +76,7 @@ class ConditionalStyles
}
}
/** @return array<string, array<int, Conditional>> */
private function readConditionalsFromExt(SimpleXMLElement $extLst): array
{
$conditionals = [];
@@ -125,6 +132,7 @@ class ConditionalStyles
{
$conditionType = (string) $attributes->type;
$operatorType = (string) $attributes->operator;
$priority = (int) (string) $attributes->priority;
$operands = [];
foreach ($cfRuleXml->children($this->ns['xm']) as $cfRuleOperandsXml) {
@@ -134,6 +142,7 @@ class ConditionalStyles
$conditional = new Conditional();
$conditional->setConditionType($conditionType);
$conditional->setOperatorType($operatorType);
$conditional->setPriority($priority);
if (
$conditionType === Conditional::CONDITION_CONTAINSTEXT
|| $conditionType === Conditional::CONDITION_NOTCONTAINSTEXT
@@ -165,6 +174,7 @@ class ConditionalStyles
return $cfStyle;
}
/** @return mixed[] */
private function readConditionalStyles(SimpleXMLElement $xmlSheet): array
{
$conditionals = [];
@@ -181,22 +191,37 @@ class ConditionalStyles
return $conditionals;
}
/** @param mixed[] $conditionals */
private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void
{
foreach ($conditionals as $cellRangeReference => $cfRules) {
ksort($cfRules);
/** @var mixed[] $cfRules */
ksort($cfRules); // no longer needed for Xlsx, but helps Xls
$conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst);
// Extract all cell references in $cellRangeReference
// N.B. In Excel UI, intersection is space and union is comma.
// But in Xml, intersection is comma and union is space.
$cellRangeReference = str_replace(['$', ' ', ',', '^'], ['', '^', ' ', ','], strtoupper($cellRangeReference));
foreach ($conditionalStyles as $cs) {
$scale = $cs->getColorScale();
if ($scale !== null) {
$scale->setSqRef($cellRangeReference, $worksheet);
}
}
$worksheet->getStyle($cellRangeReference)->setConditionalStyles($conditionalStyles);
}
}
/**
* @param mixed[] $cfRules
*
* @return Conditional[]
*/
private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array
{
/** @var ConditionalFormattingRuleExtension[] */
$conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst);
$conditionalStyles = [];
@@ -205,6 +230,7 @@ class ConditionalStyles
$objConditional = new Conditional();
$objConditional->setConditionType((string) $cfRule['type']);
$objConditional->setOperatorType((string) $cfRule['operator']);
$objConditional->setPriority((int) (string) $cfRule['priority']);
$objConditional->setNoFormatSet(!isset($cfRule['dxfId']));
if ((string) $cfRule['text'] != '') {
@@ -220,6 +246,7 @@ class ConditionalStyles
if (count($cfRule->formula) >= 1) {
foreach ($cfRule->formula as $formulax) {
$formula = (string) $formulax;
$formula = str_replace(['_xlfn.', '_xlws.'], '', $formula);
if ($formula === 'TRUE') {
$objConditional->addCondition(true);
} elseif ($formula === 'FALSE') {
@@ -240,6 +267,8 @@ class ConditionalStyles
$objConditional->setColorScale(
$this->readColorScale($cfRule)
);
} elseif (isset($cfRule->iconSet)) {
$objConditional->setIconSet($this->readIconSet($cfRule));
} elseif (isset($cfRule['dxfId'])) {
$objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]);
}
@@ -250,6 +279,7 @@ class ConditionalStyles
return $conditionalStyles;
}
/** @param ConditionalFormattingRuleExtension[] $conditionalFormattingRuleExtensions */
private function readDataBarOfConditionalRule(SimpleXMLElement $cfRule, array $conditionalFormattingRuleExtensions): ConditionalDataBar
{
$dataBar = new ConditionalDataBar();
@@ -263,6 +293,7 @@ class ConditionalStyles
$cfvoXml = $cfRule->dataBar->cfvo;
$cfvoIndex = 0;
foreach ((count($cfvoXml) > 1 ? $cfvoXml : [$cfvoXml]) as $cfvo) { //* @phpstan-ignore-line
/** @var SimpleXMLElement $cfvo */
if ($cfvoIndex === 0) {
$dataBar->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $cfvo['type'], (string) $cfvo['val']));
}
@@ -285,6 +316,7 @@ class ConditionalStyles
private function readColorScale(SimpleXMLElement|stdClass $cfRule): ConditionalColorScale
{
$colorScale = new ConditionalColorScale();
/** @var SimpleXMLElement $cfRule */
$count = count($cfRule->colorScale->cfvo);
$idx = 0;
foreach ($cfRule->colorScale->cfvo as $cfvoXml) {
@@ -321,11 +353,47 @@ class ConditionalStyles
return $colorScale;
}
private function readIconSet(SimpleXMLElement $cfRule): ConditionalIconSet
{
$iconSet = new ConditionalIconSet();
if (isset($cfRule->iconSet['iconSet'])) {
$iconSet->setIconSetType(IconSetValues::from($cfRule->iconSet['iconSet']));
}
if (isset($cfRule->iconSet['reverse'])) {
$iconSet->setReverse('1' === (string) $cfRule->iconSet['reverse']);
}
if (isset($cfRule->iconSet['showValue'])) {
$iconSet->setShowValue('1' === (string) $cfRule->iconSet['showValue']);
}
if (isset($cfRule->iconSet['custom'])) {
$iconSet->setCustom('1' === (string) $cfRule->iconSet['custom']);
}
$cfvos = [];
foreach ($cfRule->iconSet->cfvo as $cfvoXml) {
$type = (string) $cfvoXml['type'];
$value = (string) ($cfvoXml['val'] ?? '');
$cfvo = new ConditionalFormatValueObject($type, $value);
if (isset($cfvoXml['gte'])) {
$cfvo->setGreaterThanOrEqual('1' === (string) $cfvoXml['gte']);
}
$cfvos[] = $cfvo;
}
$iconSet->setCfvos($cfvos);
// TODO: The cfIcon element is not implemented yet.
return $iconSet;
}
/** @param ConditionalFormattingRuleExtension[] $conditionalFormattingRuleExtensions */
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) { //* @phpstan-ignore-line
/** @var SimpleXMLElement $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

@@ -3,6 +3,8 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
@@ -25,7 +27,7 @@ class DataValidations
$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) {
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]);
@@ -35,31 +37,22 @@ class DataValidations
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);
}
}
$docValidation = new DataValidation();
$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(Xlsx::replacePrefixes((string) $dataValidation->formula1));
$docValidation->setFormula2(Xlsx::replacePrefixes((string) $dataValidation->formula2));
$this->worksheet->setDataValidation($range, $docValidation);
}
}
}

View File

@@ -11,6 +11,7 @@ class Hyperlinks
{
private Worksheet $worksheet;
/** @var string[] */
private array $hyperlinks = [];
public function __construct(Worksheet $workSheet)
@@ -31,9 +32,7 @@ class Hyperlinks
public function setHyperlinks(SimpleXMLElement $worksheetXml): void
{
foreach ($worksheetXml->children(Namespaces::MAIN)->hyperlink as $hyperlink) {
if ($hyperlink !== null) {
$this->setHyperlink($hyperlink, $this->worksheet);
}
$this->setHyperlink($hyperlink, $this->worksheet);
}
}
@@ -46,7 +45,7 @@ class Hyperlinks
foreach (Coordinate::extractAllCellReferencesInRange($attributes->ref) as $cellReference) {
$cell = $worksheet->getCell($cellReference);
if (isset($linkRel['id'])) {
$hyperlinkUrl = $this->hyperlinks[(string) $linkRel['id']] ?? null;
$hyperlinkUrl = $this->hyperlinks[(string) $linkRel['id']] ?? '';
if (isset($attributes['location'])) {
$hyperlinkUrl .= '#' . (string) $attributes['location'];
}

View File

@@ -82,6 +82,8 @@ class Namespaces
const CONTENT_TYPES = 'http://schemas.openxmlformats.org/package/2006/content-types';
const RELATIONSHIPS_METADATA = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata';
const RELATIONSHIPS_PRINTER_SETTINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings';
const RELATIONSHIPS_TABLE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table';
@@ -115,4 +117,8 @@ class Namespaces
const PURL_CHART = 'http://purl.oclc.org/ooxml/drawingml/chart';
const PURL_WORKSHEET = 'http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet';
const DYNAMIC_ARRAY = 'http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray';
const DYNAMIC_ARRAY_RICHDATA = 'http://schemas.microsoft.com/office/spreadsheetml/2017/richdata';
}

View File

@@ -18,6 +18,11 @@ class PageSetup extends BaseParserClass
$this->worksheetXml = $worksheetXml;
}
/**
* @param mixed[] $unparsedLoadedData
*
* @return mixed[]
*/
public function load(array $unparsedLoadedData): array
{
$worksheetXml = $this->worksheetXml;
@@ -46,6 +51,11 @@ class PageSetup extends BaseParserClass
}
}
/**
* @param mixed[] $unparsedLoadedData
*
* @return mixed[]
*/
private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData): array
{
if ($xmlSheet->pageSetup) {
@@ -82,6 +92,7 @@ class PageSetup extends BaseParserClass
if (!str_ends_with($relid, 'ps')) {
$relid .= 'ps';
}
/** @var mixed[][][] $unparsedLoadedData */
$unparsedLoadedData['sheets'][$worksheet->getCodeName()]['pageSetupRelId'] = $relid;
}
}
@@ -149,7 +160,7 @@ class PageSetup extends BaseParserClass
private function rowBreaks(SimpleXMLElement $xmlSheet, Worksheet $worksheet): void
{
foreach ($xmlSheet->rowBreaks->brk as $brk) {
$rowBreakMax = isset($brk['max']) ? ((int) $brk['max']) : -1;
$rowBreakMax = /*isset($brk['max']) ? ((int) $brk['max']) :*/ -1;
if ($brk['man']) {
$worksheet->setBreak("A{$brk['id']}", Worksheet::BREAK_ROW, $rowBreakMax);
}

View File

@@ -79,7 +79,9 @@ class Properties
$cellDataOfficeChildren = $xmlProperty->children('http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes');
$attributeType = $cellDataOfficeChildren->getName();
$attributeValue = (string) $cellDataOfficeChildren->{$attributeType};
/** @var SimpleXMLElement */
$attributeValue = $cellDataOfficeChildren->{$attributeType};
$attributeValue = (string) $attributeValue;
$attributeValue = DocumentProperties::convertProperty($attributeValue, $attributeType);
$attributeType = DocumentProperties::convertPropertyType($attributeType);
$this->docProps->setCustomProperty($propertyName, $attributeValue, $attributeType);
@@ -88,6 +90,7 @@ class Properties
}
}
/** @param null|false|scalar[] $array */
private function getArrayItem(null|array|false $array): string
{
return is_array($array) ? (string) ($array[0] ?? '') : '';

View File

@@ -12,6 +12,7 @@ use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Style\Protection;
use PhpOffice\PhpSpreadsheet\Style\Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableDxfsStyle;
use SimpleXMLElement;
use stdClass;
@@ -22,21 +23,34 @@ class Styles extends BaseParserClass
*/
private ?Theme $theme = null;
/** @var string[] */
private array $workbookPalette = [];
/** @var mixed[] */
private array $styles = [];
/** @var array<SimpleXMLElement|stdClass> */
private array $cellStyles = [];
private SimpleXMLElement $styleXml;
private string $namespace = '';
/** @var array<string, int> */
private array $fontCharsets = [];
/** @return array<string, int> */
public function getFontCharsets(): array
{
return $this->fontCharsets;
}
public function setNamespace(string $namespace): void
{
$this->namespace = $namespace;
}
/** @param string[] $palette */
public function setWorkbookPalette(array $palette): void
{
$this->workbookPalette = $palette;
@@ -62,6 +76,10 @@ class Styles extends BaseParserClass
$this->theme = $theme;
}
/**
* @param mixed[] $styles
* @param array<SimpleXMLElement|stdClass> $cellStyles
*/
public function setStyleBaseData(?Theme $theme = null, array $styles = [], array $cellStyles = []): void
{
$this->theme = $theme;
@@ -76,6 +94,13 @@ class Styles extends BaseParserClass
if (isset($attr['val'])) {
$fontStyle->setName((string) $attr['val']);
}
if (isset($fontStyleXml->charset)) {
$charsetAttr = $this->getStyleAttributes($fontStyleXml->charset);
if (isset($charsetAttr['val'])) {
$charsetVal = (int) $charsetAttr['val'];
$this->fontCharsets[$fontStyle->getName()] = $charsetVal;
}
}
}
if (isset($fontStyleXml->sz)) {
$attr = $this->getStyleAttributes($fontStyleXml->sz);
@@ -95,7 +120,14 @@ class Styles extends BaseParserClass
$attr = $this->getStyleAttributes($fontStyleXml->strike);
$fontStyle->setStrikethrough(!isset($attr['val']) || self::boolean((string) $attr['val']));
}
$fontStyle->getColor()->setARGB($this->readColor($fontStyleXml->color));
$fontStyle->getColor()
->setARGB(
$this->readColor($fontStyleXml->color)
);
$theme = $this->readColorTheme($fontStyleXml->color);
if ($theme >= 0) {
$fontStyle->getColor()->setTheme($theme);
}
if (isset($fontStyleXml->u)) {
$attr = $this->getStyleAttributes($fontStyleXml->u);
@@ -120,6 +152,12 @@ class Styles extends BaseParserClass
$attr = $this->getStyleAttributes($fontStyleXml->scheme);
$fontStyle->setScheme((string) $attr['val']);
}
if (isset($fontStyleXml->auto)) {
$attr = $this->getStyleAttributes($fontStyleXml->auto);
if (isset($attr['val'])) {
$fontStyle->setAutoColor(self::boolean((string) $attr['val']));
}
}
}
private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void
@@ -149,14 +187,22 @@ class Styles extends BaseParserClass
$fillStyle->getStartColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color)); //* @phpstan-ignore-line
$fillStyle->getEndColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color)); //* @phpstan-ignore-line
} elseif ($fillStyleXml->patternFill) {
$defaultFillStyle = Fill::FILL_NONE;
$defaultFillStyle = ($fillStyle->getFillType() !== null) ? Fill::FILL_NONE : '';
$fgFound = false;
$bgFound = false;
if ($fillStyleXml->patternFill->fgColor) {
$fillStyle->getStartColor()->setARGB($this->readColor($fillStyleXml->patternFill->fgColor, true));
$defaultFillStyle = Fill::FILL_SOLID;
if ($fillStyle->getFillType() !== null) {
$defaultFillStyle = Fill::FILL_SOLID;
}
$fgFound = true;
}
if ($fillStyleXml->patternFill->bgColor) {
$fillStyle->getEndColor()->setARGB($this->readColor($fillStyleXml->patternFill->bgColor, true));
$defaultFillStyle = Fill::FILL_SOLID;
if ($fillStyle->getFillType() !== null) {
$defaultFillStyle = Fill::FILL_SOLID;
}
$bgFound = true;
}
$type = '';
@@ -169,6 +215,22 @@ class Styles extends BaseParserClass
$patternType = ($type === '') ? $defaultFillStyle : $type;
$fillStyle->setFillType($patternType);
if (
!$fgFound // no foreground color specified
&& !in_array($patternType, [Fill::FILL_NONE, Fill::FILL_SOLID], true) // these patterns aren't relevant
&& $fillStyle->getStartColor()->getARGB() // not conditional
) {
$fillStyle->getStartColor()
->setARGB('', true);
}
if (
!$bgFound // no background color specified
&& !in_array($patternType, [Fill::FILL_NONE, Fill::FILL_SOLID], true) // these patterns aren't relevant
&& $fillStyle->getEndColor()->getARGB() // not conditional
) {
$fillStyle->getEndColor()
->setARGB('', true);
}
}
}
@@ -241,6 +303,12 @@ class Styles extends BaseParserClass
if ($horizontal !== '') {
$alignment->setHorizontal($horizontal);
}
$justifyLastLine = (string) $this->getAttribute($alignmentXml, 'justifyLastLine');
if ($justifyLastLine !== '') {
$alignment->setJustifyLastLine(
self::boolean($justifyLastLine)
);
}
$vertical = (string) $this->getAttribute($alignmentXml, 'vertical');
if ($vertical !== '') {
$alignment->setVertical($vertical);
@@ -279,9 +347,12 @@ class Styles extends BaseParserClass
if ($style instanceof SimpleXMLElement) {
$this->readNumberFormat($docStyle->getNumberFormat(), $style->numFmt);
} else {
$docStyle->getNumberFormat()->setFormatCode(self::formatGeneral((string) $style->numFmt));
/** @var SimpleXMLElement */
$temp = $style->numFmt;
$docStyle->getNumberFormat()->setFormatCode(self::formatGeneral((string) $temp));
}
/** @var SimpleXMLElement $style */
if (isset($style->font)) {
$this->readFontStyle($docStyle->getFont(), $style->font);
}
@@ -356,6 +427,17 @@ class Styles extends BaseParserClass
}
}
public function readColorTheme(SimpleXMLElement $color): int
{
$attr = $this->getStyleAttributes($color);
$retVal = -1;
if (isset($attr['theme']) && is_numeric((string) $attr['theme']) && !isset($attr['tint'])) {
$retVal = (int) $attr['theme'];
}
return $retVal;
}
public function readColor(SimpleXMLElement $color, bool $background = false): string
{
$attr = $this->getStyleAttributes($color);
@@ -385,6 +467,7 @@ class Styles extends BaseParserClass
return ($background) ? 'FFFFFFFF' : 'FF000000';
}
/** @return Style[] */
public function dxfs(bool $readDataOnly = false): array
{
$dxfs = [];
@@ -417,6 +500,47 @@ class Styles extends BaseParserClass
return $dxfs;
}
/** @return TableDxfsStyle[] */
public function tableStyles(bool $readDataOnly = false): array
{
$tableStyles = [];
if (!$readDataOnly && $this->styleXml) {
// Conditional Styles
if ($this->styleXml->tableStyles) {
foreach ($this->styleXml->tableStyles->tableStyle as $s) {
$attrs = Xlsx::getAttributes($s);
if (isset($attrs['name'][0])) {
$style = new TableDxfsStyle((string) ($attrs['name'][0]));
foreach ($s->tableStyleElement as $e) {
$a = Xlsx::getAttributes($e);
if (isset($a['dxfId'][0], $a['type'][0])) {
switch ($a['type'][0]) {
case 'headerRow':
$style->setHeaderRow((int) ($a['dxfId'][0]));
break;
case 'firstRowStripe':
$style->setFirstRowStripe((int) ($a['dxfId'][0]));
break;
case 'secondRowStripe':
$style->setSecondRowStripe((int) ($a['dxfId'][0]));
break;
default:
}
}
}
$tableStyles[] = $style;
}
}
}
}
return $tableStyles;
}
/** @return mixed[] */
public function styles(): array
{
return $this->styles;
@@ -425,10 +549,10 @@ class Styles extends BaseParserClass
/**
* Get array item.
*
* @param mixed $array (usually array, in theory can be false)
* @param false|mixed[] $array (usually array, in theory can be false)
*/
private static function getArrayItem(mixed $array): ?SimpleXMLElement
{
return is_array($array) ? ($array[0] ?? null) : null;
return is_array($array) ? ($array[0] ?? null) : null; // @phpstan-ignore-line
}
}

View File

@@ -2,7 +2,9 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableDxfsStyle;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
@@ -13,7 +15,7 @@ class TableReader
private SimpleXMLElement $tableXml;
/** @var array|SimpleXMLElement */
/** @var mixed[]|SimpleXMLElement */
private $tableAttributes;
public function __construct(Worksheet $workSheet, SimpleXMLElement $tableXml)
@@ -24,30 +26,38 @@ class TableReader
/**
* Loads Table into the Worksheet.
*
* @param TableDxfsStyle[] $tableStyles
* @param Style[] $dxfs
*/
public function load(): void
public function load(array $tableStyles, array $dxfs): 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);
$this->readTable($tableRange, $tableStyles, $dxfs);
}
}
/**
* Read Table from xml.
*
* @param TableDxfsStyle[] $tableStyles
* @param Style[] $dxfs
*/
private function readTable(string $tableRange): void
private function readTable(string $tableRange, array $tableStyles, array $dxfs): 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');
/** @var string[] */
$attributes = $this->tableAttributes;
$table->setName((string) ($attributes['displayName'] ?? ''));
$table->setShowHeaderRow(((string) ($attributes['headerRowCount'] ?? '')) !== '0');
$table->setShowTotalsRow(((string) ($attributes['totalsRowCount'] ?? '')) === '1');
$this->readTableAutoFilter($table, $this->tableXml->autoFilter);
$this->readTableColumns($table, $this->tableXml->tableColumns);
$this->readTableStyle($table, $this->tableXml->tableStyleInfo);
$this->readTableStyle($table, $this->tableXml->tableStyleInfo, $tableStyles, $dxfs);
(new AutoFilter($table, $this->tableXml))->load();
$this->worksheet->addTable($table);
@@ -65,6 +75,7 @@ class TableReader
}
foreach ($autoFilterXml->filterColumn as $filterColumn) {
/** @var SimpleXMLElement */
$attributes = $filterColumn->attributes() ?? ['colId' => 0, 'hiddenButton' => 0];
$column = $table->getColumnByOffset((int) $attributes['colId']);
$column->setShowFilterButton(((string) $attributes['hiddenButton']) !== '1');
@@ -78,6 +89,7 @@ class TableReader
{
$offset = 0;
foreach ($tableColumnsXml->tableColumn as $tableColumn) {
/** @var SimpleXMLElement */
$attributes = $tableColumn->attributes() ?? ['totalsRowLabel' => 0, 'totalsRowFunction' => 0];
$column = $table->getColumnByOffset($offset++);
@@ -99,8 +111,11 @@ class TableReader
/**
* Reads TableStyle from xml.
*
* @param TableDxfsStyle[] $tableStyles
* @param Style[] $dxfs
*/
private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml): void
private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml, array $tableStyles, array $dxfs): void
{
$tableStyle = new TableStyle();
$attributes = $tableStyleInfoXml->attributes();
@@ -110,6 +125,12 @@ class TableReader
$tableStyle->setShowColumnStripes((string) $attributes['showColumnStripes'] === '1');
$tableStyle->setShowFirstColumn((string) $attributes['showFirstColumn'] === '1');
$tableStyle->setShowLastColumn((string) $attributes['showLastColumn'] === '1');
foreach ($tableStyles as $style) {
if ($style->getName() === (string) $attributes['name']) {
$tableStyle->setTableDxfsStyle($style, $dxfs);
}
}
}
$table->setStyle($tableStyle);
}

View File

@@ -14,6 +14,7 @@ class WorkbookView
$this->spreadsheet = $spreadsheet;
}
/** @param array<int, ?int> $mapSheetId */
public function viewSettings(SimpleXMLElement $xmlWorkbook, string $mainNS, array $mapSheetId, bool $readDataOnly): void
{
// Default active sheet index to the first loaded worksheet from the file
@@ -25,7 +26,7 @@ class 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) {
if (isset($mapSheetId[$activeTab])) {
$this->spreadsheet->setActiveSheetIndex($mapSheetId[$activeTab]);
}

View File

@@ -17,6 +17,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
@@ -32,6 +33,8 @@ class Xml extends BaseReader
/**
* Formats.
*
* @var mixed[]
*/
protected array $styles = [];
@@ -42,12 +45,25 @@ class Xml extends BaseReader
{
parent::__construct();
$this->securityScanner = XmlScanner::getInstance($this);
/** @var callable */
$unentity = [self::class, 'unentity'];
$this->securityScanner->setAdditionalCallback($unentity);
}
public static function unentity(string $contents): string
{
$contents = preg_replace('/&(amp|lt|gt|quot|apos);/', "\u{fffe}\u{feff}\$1;", trim($contents)) ?? $contents;
$contents = html_entity_decode($contents, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
$contents = str_replace("\u{fffe}\u{feff}", '&', $contents);
return $contents;
}
private string $fileContents = '';
private string $xmlFailMessage = '';
/** @return mixed[] */
public static function xmlMappings(): array
{
return array_merge(
@@ -92,25 +108,12 @@ class Xml extends BaseReader
break;
}
}
$this->fileContents = $data;
return $valid;
}
/**
* Check if the file is a valid SimpleXML.
*
* @return false|SimpleXMLElement
*
* @deprecated 2.0.1 Should never have had public visibility
*
* @codeCoverageIgnore
*/
public function trySimpleXMLLoadString(string $filename, string $fileOrString = 'file'): SimpleXMLElement|bool
{
return $this->trySimpleXMLLoadStringPrivate($filename, $fileOrString);
}
/** @return false|SimpleXMLElement */
private function trySimpleXMLLoadStringPrivate(string $filename, string $fileOrString = 'file'): SimpleXMLElement|bool
{
@@ -132,7 +135,8 @@ class Xml extends BaseReader
}
if ($continue) {
$xml = @simplexml_load_string(
$this->getSecurityScannerOrThrow()->scan($data)
$this->getSecurityScannerOrThrow()
->scan($data)
);
}
} catch (Throwable $e) {
@@ -145,6 +149,8 @@ class Xml extends BaseReader
/**
* Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
*
* @return string[]
*/
public function listWorksheetNames(string $filename): array
{
@@ -171,6 +177,8 @@ class Xml extends BaseReader
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
* @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
*/
public function listWorksheetInfo(string $filename): array
{
@@ -229,6 +237,7 @@ class Xml extends BaseReader
$tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
$tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
$tmpInfo['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
$worksheetInfo[] = $tmpInfo;
++$worksheetID;
@@ -242,8 +251,8 @@ class Xml extends BaseReader
*/
public function loadSpreadsheetFromString(string $contents): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet = $this->newSpreadsheet();
$spreadsheet->setValueBinder($this->valueBinder);
$spreadsheet->removeSheetByIndex(0);
// Load into this instance
@@ -255,8 +264,8 @@ class Xml extends BaseReader
*/
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet = $this->newSpreadsheet();
$spreadsheet->setValueBinder($this->valueBinder);
$spreadsheet->removeSheetByIndex(0);
// Load into this instance
@@ -291,13 +300,14 @@ class Xml extends BaseReader
(new Properties($spreadsheet))->readProperties($xml, $namespaces);
$this->styles = (new Style())->parseStyles($xml, $namespaces);
if (isset($this->styles['Default'])) {
if (isset($this->styles['Default']) && is_array($this->styles['Default'])) {
$spreadsheet->getCellXfCollection()[0]->applyFromArray($this->styles['Default']);
}
$worksheetID = 0;
$xml_ss = $xml->children(self::NAMESPACES_SS);
$sheetCreated = false;
/** @var null|SimpleXMLElement $worksheetx */
foreach ($xml_ss->Worksheet as $worksheetx) {
$worksheet = $worksheetx ?? new SimpleXMLElement('<xml></xml>');
@@ -312,6 +322,7 @@ class Xml extends BaseReader
// Create new Worksheet
$spreadsheet->createSheet();
$sheetCreated = true;
$spreadsheet->setActiveSheetIndex($worksheetID);
$worksheetName = '';
if (isset($worksheet_ss['Name'])) {
@@ -363,13 +374,14 @@ class Xml extends BaseReader
$columnVisible = ((string) $columnData_ss['Hidden']) !== '1';
}
while ($colspan >= 0) {
/** @var string $columnID */
if (isset($columnWidth)) {
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4);
}
if (isset($columnVisible)) {
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setVisible($columnVisible);
}
++$columnID;
StringHelper::stringIncrement($columnID);
--$colspan;
}
}
@@ -391,18 +403,21 @@ class Xml extends BaseReader
$columnID = 'A';
foreach ($rowData->Cell as $cell) {
$arrayRef = '';
$cell_ss = self::getAttributes($cell, self::NAMESPACES_SS);
if (isset($cell_ss['Index'])) {
$columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']);
}
$cellRange = $columnID . $rowID;
if (isset($cell_ss['ArrayRange'])) {
$arrayRange = (string) $cell_ss['ArrayRange'];
$arrayRef = AddressHelper::convertFormulaToA1($arrayRange, $rowID, Coordinate::columnIndexFromString($columnID));
}
if ($this->getReadFilter() !== null) {
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
++$columnID;
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
StringHelper::stringIncrement($columnID);
continue;
}
continue;
}
if (isset($cell_ss['HRef'])) {
@@ -428,6 +443,9 @@ class Xml extends BaseReader
if (isset($cell_ss['Formula'])) {
$cellDataFormula = $cell_ss['Formula'];
$hasCalculatedValue = true;
if ($arrayRef !== '') {
$spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayRef]);
}
}
if (isset($cell->Data)) {
$cellData = $cell->Data;
@@ -505,17 +523,14 @@ class Xml extends BaseReader
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);
//}
if ((isset($this->styles[$style])) && is_array($this->styles[$style]) && (!empty($this->styles[$style]))) {
$spreadsheet->getActiveSheet()->getStyle($cellRange)
->applyFromArray($this->styles[$style]);
}
}
++$columnID;
StringHelper::stringIncrement($columnID);
while ($additionalMergedCells > 0) {
++$columnID;
StringHelper::stringIncrement($columnID);
--$additionalMergedCells;
}
}
@@ -655,6 +670,9 @@ class Xml extends BaseReader
}
++$worksheetID;
}
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
$spreadsheet->createSheet();
}
// Globally scoped defined names
$activeSheetIndex = 0;

View File

@@ -3,7 +3,6 @@
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;
@@ -31,6 +30,7 @@ class DataValidations
private int $thisColumn = 0;
/** @param string[] $matches */
private function replaceR1C1(array $matches): string
{
return AddressHelper::convertToA1($matches[0], $this->thisRow, $this->thisColumn, false);
@@ -43,7 +43,8 @@ class DataValidations
/** @var callable $pregCallback */
$pregCallback = [$this, 'replaceR1C1'];
foreach ($xmlX->DataValidation as $dataValidation) {
$cells = [];
$combinedCells = '';
$separator = '';
$validation = new DataValidation();
// set defaults
@@ -72,6 +73,8 @@ class DataValidations
$this->thisRow = (int) $selectionMatches[1];
$this->thisColumn = (int) $selectionMatches[2];
$sheet->getCell($firstCell);
$combinedCells .= "$separator$cell";
$separator = ' ';
} elseif (preg_match('/^R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) {
// cell
$cell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
@@ -79,31 +82,31 @@ class DataValidations
$sheet->getCell($cell);
$this->thisRow = (int) $selectionMatches[1];
$this->thisColumn = (int) $selectionMatches[2];
} elseif (preg_match('/^C(\d+)$/', (string) $range, $selectionMatches) === 1) {
$combinedCells .= "$separator$cell";
$separator = ' ';
} elseif (preg_match('/^C(\d+)(: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];
$firstCol = $selectionMatches[1];
$firstColString = Coordinate::stringFromColumnIndex((int) $firstCol);
$lastCol = $selectionMatches[3] ?? $firstCol;
$lastColString = Coordinate::stringFromColumnIndex((int) $lastCol);
$firstCell = "{$firstColString}1";
$cell = "$firstColString:$lastColString";
$this->thisColumn = (int) $firstCol;
$sheet->getCell($firstCell);
} elseif (preg_match('/^R(\d+)$/', (string) $range, $selectionMatches)) {
$combinedCells .= "$separator$cell";
$separator = ' ';
} elseif (preg_match('/^R(\d+)(:R(]\d+))?$/', (string) $range, $selectionMatches)) {
// row
$firstCell = 'A'
. $selectionMatches[1];
$cell = $firstCell
. ':'
. AddressRange::MAX_COLUMN
. $selectionMatches[1];
$this->thisRow = (int) $selectionMatches[1];
$firstRow = $selectionMatches[1];
$lastRow = $selectionMatches[3] ?? $firstRow;
$firstCell = "A$firstRow";
$cell = "$firstRow:$lastRow";
$this->thisRow = (int) $firstRow;
$sheet->getCell($firstCell);
$combinedCells .= "$separator$cell";
$separator = ' ';
}
$validation->setSqref($cell);
$stRange = $sheet->shrinkRangeToFit($cell);
$cells = array_merge($cells, Coordinate::extractAllCellReferencesInRange($stRange));
}
break;
@@ -169,9 +172,7 @@ class DataValidations
}
}
foreach ($cells as $cell) {
$sheet->getCell($cell)->setDataValidation(clone $validation);
}
$sheet->setDataValidation($combinedCells, $validation);
}
}
}

View File

@@ -10,12 +10,16 @@ use stdClass;
class PageSettings
{
/** @var (object{orientation: string, scale: ?int, printOrder: ?string,
* paperSize: int,
* horizontalCentered: bool, verticalCentered: bool, leftMargin: float, rightMargin: float, topMargin: float,
* bottomMargin: float, headerMargin: float, footerMargin: float}&stdClass) */
private stdClass $printSettings;
public function __construct(SimpleXMLElement $xmlX)
{
$printSettings = $this->pageSetup($xmlX, $this->getPrintDefaults());
$this->printSettings = $this->printSetup($xmlX, $printSettings);
$this->printSettings = $this->printSetup($xmlX, $printSettings); //* @phpstan-ignore-line
}
public function loadPageSettings(Spreadsheet $spreadsheet): void

View File

@@ -15,6 +15,7 @@ class Properties
$this->spreadsheet = $spreadsheet;
}
/** @param string[] $namespaces */
public function readProperties(SimpleXMLElement $xml, array $namespaces): void
{
$this->readStandardProperties($xml);
@@ -34,6 +35,7 @@ class Properties
}
}
/** @param string[] $namespaces */
protected function readCustomProperties(SimpleXMLElement $xml, array $namespaces): void
{
if (isset($xml->CustomDocumentProperties) && is_iterable($xml->CustomDocumentProperties[0])) {
@@ -143,6 +145,7 @@ class Properties
$docProps->setCustomProperty($propertyName, $propertyValue, $propertyType);
}
/** @param string[] $hex */
protected function hex2str(array $hex): string
{
return mb_chr((int) hexdec($hex[1]), 'UTF-8');

View File

@@ -9,14 +9,21 @@ class Style
{
/**
* Formats.
*
* @var mixed[]
*/
protected array $styles = [];
/**
* @param string[] $namespaces
*
* @return mixed[]
*/
public function parseStyles(SimpleXMLElement $xml, array $namespaces): array
{
$children = $xml->children('urn:schemas-microsoft-com:office:spreadsheet');
$stylesXml = $children->Styles[0];
if (!isset($stylesXml) || !is_iterable($stylesXml)) {
if (!isset($stylesXml)) {
return [];
}
@@ -27,6 +34,7 @@ class Style
$numberFormatStyleParser = new Style\NumberFormat();
foreach ($stylesXml as $style) {
/** @var SimpleXMLElement $style */
$style_ss = self::getAttributes($style, $namespaces['ss']);
$styleID = (string) $style_ss['ID'];
$this->styles[$styleID] = $this->styles['Default'] ?? [];

View File

@@ -23,6 +23,7 @@ class Alignment extends StyleBase
AlignmentStyles::HORIZONTAL_JUSTIFY,
];
/** @return mixed[] */
public function parseStyle(SimpleXMLElement $styleAttributes): array
{
$style = [];
@@ -49,6 +50,10 @@ class Alignment extends StyleBase
case 'Rotate':
$style['alignment']['textRotation'] = $styleAttributeValue;
break;
case 'Indent':
$style['alignment']['indent'] = $styleAttributeValue;
break;
}
}

View File

@@ -15,9 +15,6 @@ class Border extends StyleBase
'right',
];
/**
* @var array
*/
public const BORDER_MAPPINGS = [
'borderStyle' => [
'continuous' => BorderStyle::BORDER_HAIR,
@@ -53,6 +50,11 @@ class Border extends StyleBase
],
];
/**
* @param string[] $namespaces
*
* @return mixed[]
*/
public function parseStyle(SimpleXMLElement $styleData, array $namespaces): array
{
$style = [];
@@ -70,6 +72,7 @@ class Border extends StyleBase
$borderStyleValue = (string) $borderStyleValuex;
switch ($borderStyleKey) {
case 'Position':
/** @var string $diagonalDirection */
[$borderPosition, $diagonalDirection]
= $this->parsePosition($borderStyleValue, $diagonalDirection);
@@ -93,6 +96,7 @@ class Border extends StyleBase
return $style;
}
/** @return mixed[] */
protected function parsePosition(string $borderStyleValue, string $diagonalDirection): array
{
$borderStyleValue = strtolower($borderStyleValue);

View File

@@ -7,9 +7,6 @@ use SimpleXMLElement;
class Fill extends StyleBase
{
/**
* @var array
*/
public const FILL_MAPPINGS = [
'fillType' => [
'solid' => FillStyles::FILL_SOLID,
@@ -33,6 +30,7 @@ class Fill extends StyleBase
],
];
/** @return mixed[] */
public function parseStyle(SimpleXMLElement $styleAttributes): array
{
$style = [];

View File

@@ -15,6 +15,11 @@ class Font extends StyleBase
FontUnderline::UNDERLINE_SINGLEACCOUNTING,
];
/**
* @param mixed[][] $style
*
* @return mixed[][]
*/
protected function parseUnderline(array $style, string $styleAttributeValue): array
{
if (self::identifyFixedStyleValue(self::UNDERLINE_STYLES, $styleAttributeValue)) {
@@ -24,6 +29,11 @@ class Font extends StyleBase
return $style;
}
/**
* @param mixed[][] $style
*
* @return mixed[][]
*/
protected function parseVerticalAlign(array $style, string $styleAttributeValue): array
{
if ($styleAttributeValue == 'Superscript') {
@@ -36,6 +46,7 @@ class Font extends StyleBase
return $style;
}
/** @return mixed[] */
public function parseStyle(SimpleXMLElement $styleAttributes): array
{
$style = [];
@@ -52,6 +63,7 @@ class Font extends StyleBase
break;
case 'Color':
/** @var string[][][] $style */
$style['font']['color']['rgb'] = substr($styleAttributeValue, 1);
break;

View File

@@ -6,6 +6,7 @@ use SimpleXMLElement;
class NumberFormat extends StyleBase
{
/** @return mixed[] */
public function parseStyle(SimpleXMLElement $styleAttributes): array
{
$style = [];

View File

@@ -6,6 +6,7 @@ use SimpleXMLElement;
abstract class StyleBase
{
/** @param string[] $styleList */
protected static function identifyFixedStyleValue(array $styleList, string &$styleAttributeValue): bool
{
$returnValue = false;