Skip to content

Commit

Permalink
Recognize 'Hidden' Attribute and Other Unsupported Options in Xml Spr…
Browse files Browse the repository at this point in the history
…eadsheet (#3567)

* Recognize 'Hidden' Attribute in Xml Spreadsheet

Fix #3566; PhpSpreadsheet should now handle Hidden attribute for rows and columns in Xml spreadsheets. Also added the ability to load Xml spreadsheet from string rather than file, as can be done for Csv and Html.

* Add Support for Top Left Cell

Another missing piece.

* Font Bold/Italic

Was always setting to true rather than checking value for 0/1.

* Active Sheet Index

Had always been set to hard-coded 0. Read it from XML if available.

* Selected Cells

Get Selected Cells from XML when available.

* Worksheet Protection and Style Protection

Add support for those to Xml Reader.

* Column Spans

Not really an essential part of Excel, used in Xml to reduce file size.
  • Loading branch information
oleibman authored May 18, 2023
1 parent f8121e7 commit 482fe6d
Show file tree
Hide file tree
Showing 10 changed files with 706 additions and 15 deletions.
108 changes: 97 additions & 11 deletions src/PhpSpreadsheet/Reader/Xml.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,19 @@ public function listWorksheetInfo($filename)
return $worksheetInfo;
}

/**
* Loads Spreadsheet from string.
*/
public function loadSpreadsheetFromString(string $contents): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet->removeSheetByIndex(0);

// Load into this instance
return $this->loadIntoExisting($contents, $spreadsheet, true);
}

/**
* Loads Spreadsheet from file.
*/
Expand All @@ -245,17 +258,19 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
}

/**
* Loads from file into Spreadsheet instance.
*
* @param string $filename
* Loads from file or contents into Spreadsheet instance.
*
* @return Spreadsheet
* @param string $filename file name if useContents is false else file contents
*/
public function loadIntoExisting($filename, Spreadsheet $spreadsheet)
public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, bool $useContents = false): Spreadsheet
{
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an Invalid Spreadsheet file.');
if ($useContents) {
$this->fileContents = $filename;
} else {
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an Invalid Spreadsheet file.');
}
}

$xml = $this->trySimpleXMLLoadString($filename);
Expand All @@ -268,6 +283,9 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet)
(new Properties($spreadsheet))->readProperties($xml, $namespaces);

$this->styles = (new Style())->parseStyles($xml, $namespaces);
if (isset($this->styles['Default'])) {
$spreadsheet->getCellXfCollection()[0]->applyFromArray($this->styles['Default']);
}

$worksheetID = 0;
$xml_ss = $xml->children($namespaces['ss']);
Expand Down Expand Up @@ -295,6 +313,10 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet)
// the worksheet name in line with the formula, not the reverse
$spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
}
if (isset($worksheet_ss['Protected'])) {
$protection = (string) $worksheet_ss['Protected'] === '1';
$spreadsheet->getActiveSheet()->getProtection()->setSheet($protection);
}

// locally scoped defined names
if (isset($worksheet->Names[0])) {
Expand All @@ -314,14 +336,34 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet)
if (isset($worksheet->Table->Column)) {
foreach ($worksheet->Table->Column as $columnData) {
$columnData_ss = self::getAttributes($columnData, $namespaces['ss']);
$colspan = 0;
if (isset($columnData_ss['Span'])) {
$spanAttr = (string) $columnData_ss['Span'];
if (is_numeric($spanAttr)) {
$colspan = max(0, (int) $spanAttr);
}
}
if (isset($columnData_ss['Index'])) {
$columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']);
}
$columnWidth = null;
if (isset($columnData_ss['Width'])) {
$columnWidth = $columnData_ss['Width'];
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4);
}
++$columnID;
$columnVisible = null;
if (isset($columnData_ss['Hidden'])) {
$columnVisible = ((string) $columnData_ss['Hidden']) !== '1';
}
while ($colspan >= 0) {
if (isset($columnWidth)) {
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4);
}
if (isset($columnVisible)) {
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setVisible($columnVisible);
}
++$columnID;
--$colspan;
}
}
}

Expand All @@ -334,6 +376,10 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet)
if (isset($row_ss['Index'])) {
$rowID = (int) $row_ss['Index'];
}
if (isset($row_ss['Hidden'])) {
$rowVisible = ((string) $row_ss['Hidden']) !== '1';
$spreadsheet->getActiveSheet()->getRowDimension($rowID)->setVisible($rowVisible);
}

$columnID = 'A';
foreach ($rowData->Cell as $cell) {
Expand Down Expand Up @@ -471,14 +517,54 @@ public function loadIntoExisting($filename, Spreadsheet $spreadsheet)
$xmlX = $worksheet->children($namespaces['x']);
if (isset($xmlX->WorksheetOptions)) {
(new PageSettings($xmlX, $namespaces))->loadPageSettings($spreadsheet);
if (isset($xmlX->WorksheetOptions->TopRowVisible, $xmlX->WorksheetOptions->LeftColumnVisible)) {
$leftTopRow = (string) $xmlX->WorksheetOptions->TopRowVisible;
$leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnVisible;
if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) {
$leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1);
$spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate);
}
}
$rangeCalculated = false;
if (isset($xmlX->WorksheetOptions->Panes->Pane->RangeSelection)) {
if (1 === preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $xmlX->WorksheetOptions->Panes->Pane->RangeSelection, $selectionMatches)) {
$selectedCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
. $selectionMatches[1]
. ':'
. Coordinate::stringFromColumnIndex((int) $selectionMatches[4])
. $selectionMatches[3];
$spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
$rangeCalculated = true;
}
}
if (!$rangeCalculated) {
if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveRow)) {
$activeRow = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveRow;
} else {
$activeRow = 0;
}
if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveCol)) {
$activeColumn = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveCol;
} else {
$activeColumn = 0;
}
if (is_numeric($activeRow) && is_numeric($activeColumn)) {
$selectedCell = Coordinate::stringFromColumnIndex((int) $activeColumn + 1) . (string) ($activeRow + 1);
$spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
}
}
}
}
}
++$worksheetID;
}

// Globally scoped defined names
$activeWorksheet = $spreadsheet->setActiveSheetIndex(0);
$activeSheetIndex = 0;
if (isset($xml->ExcelWorkbook->ActiveSheet)) {
$activeSheetIndex = (int) (string) $xml->ExcelWorkbook->ActiveSheet;
}
$activeWorksheet = $spreadsheet->setActiveSheetIndex($activeSheetIndex);
if (isset($xml->Names[0])) {
foreach ($xml->Names[0] as $definedName) {
$definedName_ss = self::getAttributes($definedName, $namespaces['ss']);
Expand Down
25 changes: 23 additions & 2 deletions src/PhpSpreadsheet/Reader/Xml/Style.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PhpOffice\PhpSpreadsheet\Reader\Xml;

use PhpOffice\PhpSpreadsheet\Style\Protection;
use SimpleXMLElement;

class Style
Expand Down Expand Up @@ -30,7 +31,7 @@ public function parseStyles(SimpleXMLElement $xml, array $namespaces): array
$styleID = (string) $style_ss['ID'];
$this->styles[$styleID] = $this->styles['Default'] ?? [];

$alignment = $border = $font = $fill = $numberFormat = [];
$alignment = $border = $font = $fill = $numberFormat = $protection = [];

foreach ($style as $styleType => $styleDatax) {
$styleData = self::getSxml($styleDatax);
Expand Down Expand Up @@ -64,11 +65,31 @@ public function parseStyles(SimpleXMLElement $xml, array $namespaces): array
$numberFormat = $numberFormatStyleParser->parseStyle($styleAttributes);
}

break;
case 'Protection':
$locked = $hidden = null;
$styleAttributesP = $styleData->attributes($namespaces['x']);
if (isset($styleAttributes['Protected'])) {
$locked = ((bool) (string) $styleAttributes['Protected']) ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED;
}
if (isset($styleAttributesP['HideFormula'])) {
$hidden = ((bool) (string) $styleAttributesP['HideFormula']) ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED;
}
if ($locked !== null || $hidden !== null) {
$protection['protection'] = [];
if ($locked !== null) {
$protection['protection']['locked'] = $locked;
}
if ($hidden !== null) {
$protection['protection']['hidden'] = $hidden;
}
}

break;
}
}

$this->styles[$styleID] = array_merge($alignment, $border, $font, $fill, $numberFormat);
$this->styles[$styleID] = array_merge($alignment, $border, $font, $fill, $numberFormat, $protection);
}

return $this->styles;
Expand Down
4 changes: 2 additions & 2 deletions src/PhpSpreadsheet/Reader/Xml/Style/Font.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ public function parseStyle(SimpleXMLElement $styleAttributes): array

break;
case 'Bold':
$style['font']['bold'] = true;
$style['font']['bold'] = $styleAttributeValue === '1';

break;
case 'Italic':
$style['font']['italic'] = true;
$style['font']['italic'] = $styleAttributeValue === '1';

break;
case 'Underline':
Expand Down
116 changes: 116 additions & 0 deletions tests/PhpSpreadsheetTests/Reader/Xml/XmlActiveSheetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace PhpOffice\PhpSpreadsheetTests\Reader\Xml;

use PhpOffice\PhpSpreadsheet\Reader\Xml;
use PHPUnit\Framework\TestCase;

class XmlActiveSheetTest extends TestCase
{
public function testActiveSheet(): void
{
$xmldata = <<< 'EOT'
<?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:html="http://www.w3.org/TR/REC-html40">
<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
<Version>16.00</Version>
</DocumentProperties>
<OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office">
<AllowPNG/>
</OfficeDocumentSettings>
<ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
<WindowHeight>6820</WindowHeight>
<WindowWidth>19200</WindowWidth>
<WindowTopX>32767</WindowTopX>
<WindowTopY>32767</WindowTopY>
<ActiveSheet>1</ActiveSheet>
<ProtectStructure>False</ProtectStructure>
<ProtectWindows>False</ProtectWindows>
</ExcelWorkbook>
<Styles>
<Style ss:ID="Default" ss:Name="Normal">
<Alignment ss:Vertical="Bottom"/>
<Borders/>
<Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>
<Interior/>
<NumberFormat/>
<Protection/>
</Style>
</Styles>
<Worksheet ss:Name="sheet 1">
<Table ss:ExpandedColumnCount="1" ss:ExpandedRowCount="1" x:FullColumns="1"
x:FullRows="1" ss:DefaultRowHeight="14.5">
<Row>
<Cell><Data ss:Type="String">Sheet 1</Data></Cell>
</Row>
</Table>
<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
<ProtectObjects>False</ProtectObjects>
<ProtectScenarios>False</ProtectScenarios>
</WorksheetOptions>
</Worksheet>
<Worksheet ss:Name="sheet 2">
<Table ss:ExpandedColumnCount="1" ss:ExpandedRowCount="1" x:FullColumns="1"
x:FullRows="1" ss:DefaultRowHeight="14.5">
<Row>
<Cell><Data ss:Type="String">Sheet 2</Data></Cell>
</Row>
</Table>
<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
<Selected/>
<Panes>
<Pane>
<Number>3</Number>
<ActiveRow>2</ActiveRow>
<ActiveCol>2</ActiveCol>
</Pane>
</Panes>
<ProtectObjects>False</ProtectObjects>
<ProtectScenarios>False</ProtectScenarios>
</WorksheetOptions>
</Worksheet>
<Worksheet ss:Name="sheet 3">
<Table ss:ExpandedColumnCount="1" ss:ExpandedRowCount="1" x:FullColumns="1"
x:FullRows="1" ss:DefaultRowHeight="14.5">
<Row>
<Cell><Data ss:Type="String">Sheet 3</Data></Cell>
</Row>
</Table>
<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
<PageSetup>
<Header x:Margin="0.3"/>
<Footer x:Margin="0.3"/>
<PageMargins x:Bottom="0.75" x:Left="0.7" x:Right="0.7" x:Top="0.75"/>
</PageSetup>
<Panes>
<Pane>
<Number>3</Number>
<ActiveRow>3</ActiveRow>
<ActiveCol>2</ActiveCol>
<RangeSelection>R4C3:R4C4</RangeSelection>
</Pane>
</Panes>
<ProtectObjects>False</ProtectObjects>
<ProtectScenarios>False</ProtectScenarios>
</WorksheetOptions>
</Worksheet>
</Workbook>
EOT;
$reader = new Xml();
$spreadsheet = $reader->loadSpreadsheetFromString($xmldata);
self::assertEquals(3, $spreadsheet->getSheetCount());

$sheet = $spreadsheet->getActiveSheet();
self::assertSame('sheet 2', $sheet->getTitle());
self::assertSame('C3', $sheet->getSelectedCells());
$sheet = $spreadsheet->getSheetByNameOrThrow('sheet 3');
self::assertSame('C4:D4', $sheet->getSelectedCells());

$spreadsheet->disconnectWorksheets();
}
}
Loading

0 comments on commit 482fe6d

Please sign in to comment.