Skip to content

Commit 10ec627

Browse files
committed
Improvements to Xml Reader
Fix PHPOffice#3999. Fix PHPOffice#4000. Fix PHPOffice#4002. Several bug reports and feature requests for Xml Reader arrived practically simultaneously. They are all small and hit the same code modules, so I have bundled them together in one PR. - `loadSpreadsheetFromString` might try to open a file with a falsy name (like '0'), which results in an exception with a misleading message (or a completely unexpected result if a file with that name exists). Code will still throw an exception, but the message will no longer be misleading, and no file I/O will be attempted. - function `trySimpleXmlLoadString` is deprecated. It should never have been implemented with public visibility, and the fact that it was made the fix above a little more difficult than it would otherwise have been. It is replaced with a private equivalent. - Style reader function `parseStyles` will now use a better namespace-aware method of reading its Xml data. Peculiarly, the Xml for the Style elements can either include or not a namespace prefix. This is probably because the global namespace and the styles namespace are the same. The existing prefix-based code does not recognize their equivalence, but the new namespace-based code does. Xml Reader continues to use prefix-based code in several other places. - Border line styles with Weight omitted or equal to 0 have been treated as no border, but they should be treated as 'hair' thickness. - Support for Zoom is added to Xml Reader. - In support of the above, new properties (and getters and setters) zoomScalePageLayoutView and zoomScaleSheetLayoutView are added to Worksheet/SheetView. (As far as I can tell, Excel does not support Sheet Layout View for Xml spreadsheets). - Support is added for those new properties in Xlsx Reader and Writer. - Xls Reader and Writer seem to work okay without changes. There is one test where Xls shows a different value for one of the properties than Xml or Xlsx, but the spreadsheet looks okay and I don't see any practical consequences of the difference. - PageBreak support is added to Xml Reader. - Code for writing out Column Page Breaks in Xlsx Writer was wrong (and, unsurprisingly, untested). A one-line change fixes it, and tests are added.
1 parent 35030fa commit 10ec627

File tree

11 files changed

+480
-29
lines changed

11 files changed

+480
-29
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
2222

2323
### Deprecated
2424

25-
- Nothing
25+
- Reader/Xml trySimpleXMLLoadString should not have had public visibility, and will be removed.
2626

2727
### Removed
2828

@@ -42,6 +42,9 @@ and this project adheres to [Semantic Versioning](https://semver.org).
4242
- Invalid Builtin Defined Name in Xls Reader [Issue #3935](https://github.com/PHPOffice/PhpSpreadsheet/issues/3935) [PR #3942](https://github.com/PHPOffice/PhpSpreadsheet/pull/3942)
4343
- Hidden Rows and Columns Tcpdf/Mpdf [PR #3945](https://github.com/PHPOffice/PhpSpreadsheet/pull/3945)
4444
- Protect Sheet But Allow Sort [Issue #3951](https://github.com/PHPOffice/PhpSpreadsheet/issues/3951) [PR #3956](https://github.com/PHPOffice/PhpSpreadsheet/pull/3956)
45+
- Default Value for Conditional::$text [PR #3946](https://github.com/PHPOffice/PhpSpreadsheet/pull/3946)
46+
- Table Filter Buttons [Issue #3988](https://github.com/PHPOffice/PhpSpreadsheet/issues/3988) [PR #3992](https://github.com/PHPOffice/PhpSpreadsheet/pull/3992)
47+
- Improvements to Xml Reader [Issue #3999](https://github.com/PHPOffice/PhpSpreadsheet/issues/3999) [Issue #4000](https://github.com/PHPOffice/PhpSpreadsheet/issues/4000) [Issue #4002](https://github.com/PHPOffice/PhpSpreadsheet/issues/4002) [PR #4003](https://github.com/PHPOffice/PhpSpreadsheet/pull/4003)
4548

4649
## 2.0.0 - 2024-01-04
4750

src/PhpSpreadsheet/Reader/Xlsx/SheetViews.php

+14
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@ private function zoomScale(): void
7070

7171
$this->worksheet->getSheetView()->setZoomScaleNormal($zoomScaleNormal);
7272
}
73+
74+
if (isset($this->sheetViewAttributes->zoomScalePageLayoutView)) {
75+
$zoomScaleNormal = (int) ($this->sheetViewAttributes->zoomScalePageLayoutView);
76+
if ($zoomScaleNormal > 0) {
77+
$this->worksheet->getSheetView()->setZoomScalePageLayoutView($zoomScaleNormal);
78+
}
79+
}
80+
81+
if (isset($this->sheetViewAttributes->zoomScaleSheetLayoutView)) {
82+
$zoomScaleNormal = (int) ($this->sheetViewAttributes->zoomScaleSheetLayoutView);
83+
if ($zoomScaleNormal > 0) {
84+
$this->worksheet->getSheetView()->setZoomScaleSheetLayoutView($zoomScaleNormal);
85+
}
86+
}
7387
}
7488

7589
private function view(): void

src/PhpSpreadsheet/Reader/Xml.php

+78-12
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
use PhpOffice\PhpSpreadsheet\Shared\File;
2020
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
2121
use PhpOffice\PhpSpreadsheet\Spreadsheet;
22+
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
2223
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
2324
use SimpleXMLElement;
25+
use Throwable;
2426

2527
/**
2628
* Reader for SpreadsheetML, the XML schema for Microsoft Office Excel 2003.
@@ -45,6 +47,8 @@ public function __construct()
4547

4648
private string $fileContents = '';
4749

50+
private string $xmlFailMessage = '';
51+
4852
public static function xmlMappings(): array
4953
{
5054
return array_merge(
@@ -106,17 +110,44 @@ public function canRead(string $filename): bool
106110
* Check if the file is a valid SimpleXML.
107111
*
108112
* @return false|SimpleXMLElement
113+
*
114+
* @deprecated 2.0.1 Should never have had public visibility
115+
*
116+
* @codeCoverageIgnore
109117
*/
110-
public function trySimpleXMLLoadString(string $filename): SimpleXMLElement|bool
118+
public function trySimpleXMLLoadString(string $filename, string $fileOrString = 'file'): SimpleXMLElement|bool
119+
{
120+
return $this->trySimpleXMLLoadStringPrivate($filename, $fileOrString);
121+
}
122+
123+
/** @return false|SimpleXMLElement */
124+
private function trySimpleXMLLoadStringPrivate(string $filename, string $fileOrString = 'file'): SimpleXMLElement|bool
111125
{
126+
$this->xmlFailMessage = "Cannot load invalid XML $fileOrString: " . $filename;
127+
$xml = false;
128+
112129
try {
113-
$xml = simplexml_load_string(
114-
$this->getSecurityScannerOrThrow()->scan($this->fileContents ?: file_get_contents($filename)),
115-
'SimpleXMLElement',
116-
Settings::getLibXmlLoaderOptions()
117-
);
118-
} catch (\Exception $e) {
119-
throw new Exception('Cannot load invalid XML file: ' . $filename, 0, $e);
130+
$data = $this->fileContents;
131+
$continue = true;
132+
if ($data === '' && $fileOrString === 'file') {
133+
if ($filename === '') {
134+
$this->xmlFailMessage = 'Cannot load empty path';
135+
$continue = false;
136+
} else {
137+
$datax = @file_get_contents($filename);
138+
$data = $datax ?: '';
139+
$continue = $datax !== false;
140+
}
141+
}
142+
if ($continue) {
143+
$xml = @simplexml_load_string(
144+
$this->getSecurityScannerOrThrow()->scan($data),
145+
'SimpleXMLElement',
146+
Settings::getLibXmlLoaderOptions()
147+
);
148+
}
149+
} catch (Throwable $e) {
150+
throw new Exception($this->xmlFailMessage, 0, $e);
120151
}
121152
$this->fileContents = '';
122153

@@ -135,7 +166,7 @@ public function listWorksheetNames(string $filename): array
135166

136167
$worksheetNames = [];
137168

138-
$xml = $this->trySimpleXMLLoadString($filename);
169+
$xml = $this->trySimpleXMLLoadStringPrivate($filename);
139170
if ($xml === false) {
140171
throw new Exception("Problem reading {$filename}");
141172
}
@@ -161,7 +192,7 @@ public function listWorksheetInfo(string $filename): array
161192

162193
$worksheetInfo = [];
163194

164-
$xml = $this->trySimpleXMLLoadString($filename);
195+
$xml = $this->trySimpleXMLLoadStringPrivate($filename);
165196
if ($xml === false) {
166197
throw new Exception("Problem reading {$filename}");
167198
}
@@ -252,16 +283,18 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
252283
{
253284
if ($useContents) {
254285
$this->fileContents = $filename;
286+
$fileOrString = 'string';
255287
} else {
256288
File::assertFile($filename);
257289
if (!$this->canRead($filename)) {
258290
throw new Exception($filename . ' is an Invalid Spreadsheet file.');
259291
}
292+
$fileOrString = 'file';
260293
}
261294

262-
$xml = $this->trySimpleXMLLoadString($filename);
295+
$xml = $this->trySimpleXMLLoadStringPrivate($filename, $fileOrString);
263296
if ($xml === false) {
264-
throw new Exception("Problem reading {$filename}");
297+
throw new Exception($this->xmlFailMessage);
265298
}
266299

267300
$namespaces = $xml->getNamespaces(true);
@@ -505,6 +538,25 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
505538
$dataValidations->loadDataValidations($worksheet, $spreadsheet);
506539
$xmlX = $worksheet->children(Namespaces::URN_EXCEL);
507540
if (isset($xmlX->WorksheetOptions)) {
541+
if (isset($xmlX->WorksheetOptions->ShowPageBreakZoom)) {
542+
$spreadsheet->getActiveSheet()->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW);
543+
}
544+
if (isset($xmlX->WorksheetOptions->Zoom)) {
545+
$zoomScaleNormal = (int) $xmlX->WorksheetOptions->Zoom;
546+
if ($zoomScaleNormal > 0) {
547+
$spreadsheet->getActiveSheet()->getSheetView()->setZoomScaleNormal($zoomScaleNormal);
548+
$spreadsheet->getActiveSheet()->getSheetView()->setZoomScale($zoomScaleNormal);
549+
}
550+
}
551+
if (isset($xmlX->WorksheetOptions->PageBreakZoom)) {
552+
$zoomScaleNormal = (int) $xmlX->WorksheetOptions->PageBreakZoom;
553+
if ($zoomScaleNormal > 0) {
554+
$spreadsheet->getActiveSheet()->getSheetView()->setZoomScaleSheetLayoutView($zoomScaleNormal);
555+
}
556+
}
557+
if (isset($xmlX->WorksheetOptions->ShowPageBreakZoom)) {
558+
$spreadsheet->getActiveSheet()->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW);
559+
}
508560
if (isset($xmlX->WorksheetOptions->FreezePanes)) {
509561
$freezeRow = $freezeColumn = 1;
510562
if (isset($xmlX->WorksheetOptions->SplitHorizontal)) {
@@ -590,6 +642,20 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
590642
}
591643
}
592644
}
645+
if (isset($xmlX->PageBreaks)) {
646+
if (isset($xmlX->PageBreaks->ColBreaks)) {
647+
foreach ($xmlX->PageBreaks->ColBreaks->ColBreak as $colBreak) {
648+
$colBreak = (string) $colBreak->Column;
649+
$spreadsheet->getActiveSheet()->setBreak([1 + (int) $colBreak, 1], Worksheet::BREAK_COLUMN);
650+
}
651+
}
652+
if (isset($xmlX->PageBreaks->RowBreaks)) {
653+
foreach ($xmlX->PageBreaks->RowBreaks->RowBreak as $rowBreak) {
654+
$rowBreak = (string) $rowBreak->Row;
655+
$spreadsheet->getActiveSheet()->setBreak([1, (int) $rowBreak], Worksheet::BREAK_ROW);
656+
}
657+
}
658+
}
593659
++$worksheetID;
594660
}
595661

src/PhpSpreadsheet/Reader/Xml/Style.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ class Style
1414

1515
public function parseStyles(SimpleXMLElement $xml, array $namespaces): array
1616
{
17-
if (!isset($xml->Styles) || !is_iterable($xml->Styles[0])) {
17+
$children = $xml->children('urn:schemas-microsoft-com:office:spreadsheet');
18+
$stylesXml = $children->Styles[0];
19+
if (!isset($stylesXml) || !is_iterable($stylesXml)) {
1820
return [];
1921
}
2022

@@ -24,7 +26,7 @@ public function parseStyles(SimpleXMLElement $xml, array $namespaces): array
2426
$fillStyleParser = new Style\Fill();
2527
$numberFormatStyleParser = new Style\NumberFormat();
2628

27-
foreach ($xml->Styles[0] as $style) {
29+
foreach ($stylesXml as $style) {
2830
$style_ss = self::getAttributes($style, $namespaces['ss']);
2931
$styleID = (string) $style_ss['ID'];
3032
$this->styles[$styleID] = $this->styles['Default'] ?? [];

src/PhpSpreadsheet/Reader/Xml/Style/Border.php

+12
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ class Border extends StyleBase
2020
*/
2121
public const BORDER_MAPPINGS = [
2222
'borderStyle' => [
23+
'continuous' => BorderStyle::BORDER_HAIR,
24+
'dash' => BorderStyle::BORDER_DASHED,
25+
'dashdot' => BorderStyle::BORDER_DASHDOT,
26+
'dashdotdot' => BorderStyle::BORDER_DASHDOTDOT,
27+
'dot' => BorderStyle::BORDER_DOTTED,
28+
'double' => BorderStyle::BORDER_DOUBLE,
29+
'0continuous' => BorderStyle::BORDER_HAIR,
30+
'0dash' => BorderStyle::BORDER_DASHED,
31+
'0dashdot' => BorderStyle::BORDER_DASHDOT,
32+
'0dashdotdot' => BorderStyle::BORDER_DASHDOTDOT,
33+
'0dot' => BorderStyle::BORDER_DOTTED,
34+
'0double' => BorderStyle::BORDER_DOUBLE,
2335
'1continuous' => BorderStyle::BORDER_THIN,
2436
'1dash' => BorderStyle::BORDER_DASHED,
2537
'1dashdot' => BorderStyle::BORDER_DASHDOT,

src/PhpSpreadsheet/Worksheet/SheetView.php

+46
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ class SheetView
3131
*/
3232
private ?int $zoomScaleNormal = 100;
3333

34+
/**
35+
* ZoomScalePageLayoutView.
36+
*
37+
* Valid values range from 10 to 400.
38+
*/
39+
private int $zoomScalePageLayoutView = 100;
40+
41+
/**
42+
* ZoomScaleSheetLayoutView.
43+
*
44+
* Valid values range from 10 to 400.
45+
*/
46+
private int $zoomScaleSheetLayoutView = 100;
47+
3448
/**
3549
* ShowZeros.
3650
*
@@ -105,6 +119,38 @@ public function setZoomScaleNormal(?int $zoomScaleNormal): static
105119
return $this;
106120
}
107121

122+
public function getZoomScalePageLayoutView(): int
123+
{
124+
return $this->zoomScalePageLayoutView;
125+
}
126+
127+
public function setZoomScalePageLayoutView(int $zoomScalePageLayoutView): static
128+
{
129+
if ($zoomScalePageLayoutView >= 1) {
130+
$this->zoomScalePageLayoutView = $zoomScalePageLayoutView;
131+
} else {
132+
throw new PhpSpreadsheetException('Scale must be greater than or equal to 1.');
133+
}
134+
135+
return $this;
136+
}
137+
138+
public function getZoomScaleSheetLayoutView(): int
139+
{
140+
return $this->zoomScaleSheetLayoutView;
141+
}
142+
143+
public function setZoomScaleSheetLayoutView(int $zoomScaleSheetLayoutView): static
144+
{
145+
if ($zoomScaleSheetLayoutView >= 1) {
146+
$this->zoomScaleSheetLayoutView = $zoomScaleSheetLayoutView;
147+
} else {
148+
throw new PhpSpreadsheetException('Scale must be greater than or equal to 1.');
149+
}
150+
151+
return $this;
152+
}
153+
108154
/**
109155
* Set ShowZeroes setting.
110156
*/

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

+15-5
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,21 @@ private function writeSheetViews(XMLWriter $objWriter, PhpspreadsheetWorksheet $
264264
$objWriter->writeAttribute('workbookViewId', '0');
265265

266266
// Zoom scales
267-
if ($worksheet->getSheetView()->getZoomScale() != 100) {
268-
$objWriter->writeAttribute('zoomScale', (string) $worksheet->getSheetView()->getZoomScale());
267+
$zoomScale = $worksheet->getSheetView()->getZoomScale();
268+
if ($zoomScale !== 100 && $zoomScale !== null) {
269+
$objWriter->writeAttribute('zoomScale', (string) $zoomScale);
269270
}
270-
if ($worksheet->getSheetView()->getZoomScaleNormal() != 100) {
271-
$objWriter->writeAttribute('zoomScaleNormal', (string) $worksheet->getSheetView()->getZoomScaleNormal());
271+
$zoomScale = $worksheet->getSheetView()->getZoomScaleNormal();
272+
if ($zoomScale !== 100 && $zoomScale !== null) {
273+
$objWriter->writeAttribute('zoomScaleNormal', (string) $zoomScale);
274+
}
275+
$zoomScale = $worksheet->getSheetView()->getZoomScalePageLayoutView();
276+
if ($zoomScale !== 100) {
277+
$objWriter->writeAttribute('zoomScalePageLayoutView', (string) $zoomScale);
278+
}
279+
$zoomScale = $worksheet->getSheetView()->getZoomScaleSheetLayoutView();
280+
if ($zoomScale !== 100) {
281+
$objWriter->writeAttribute('zoomScaleSheetLayoutView', (string) $zoomScale);
272282
}
273283

274284
// Show zeros (Excel also writes this attribute only if set to false)
@@ -1210,7 +1220,7 @@ private function writeBreaks(XMLWriter $objWriter, PhpspreadsheetWorksheet $work
12101220
$objWriter->writeAttribute('manualBreakCount', (string) count($aColumnBreaks));
12111221

12121222
foreach ($aColumnBreaks as $cell => $break) {
1213-
$coords = Coordinate::coordinateFromString($cell);
1223+
$coords = Coordinate::indexesFromString($cell);
12141224

12151225
$objWriter->startElement('brk');
12161226
$objWriter->writeAttribute('id', (string) ((int) $coords[0] - 1));

0 commit comments

Comments
 (0)