Skip to content

Commit 4df236a

Browse files
authored
Merge pull request #2830 from PHPOffice/ChartSheet
Read ChartSheets with the Xlsx Reader if includeCharts is enabled
2 parents db00b90 + b4ba57a commit 4df236a

File tree

9 files changed

+187
-109
lines changed

9 files changed

+187
-109
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1010
### Added
1111

1212
- Add point size option for scatter charts
13+
- Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830)
14+
15+
Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet.
1316

1417
### Changed
1518

1619
- Memory and speed improvements, particularly for the Cell Collection, and the Writers.
1720

18-
See [the Discussion](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance
21+
See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions
1922

2023
### Deprecated
2124

phpstan-baseline.neon

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,76 +1160,6 @@ parameters:
11601160
count: 1
11611161
path: src/PhpSpreadsheet/Chart/Chart.php
11621162

1163-
-
1164-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getBottomRightXOffset\\(\\) has no return type specified\\.$#"
1165-
count: 1
1166-
path: src/PhpSpreadsheet/Chart/Chart.php
1167-
1168-
-
1169-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getBottomRightYOffset\\(\\) has no return type specified\\.$#"
1170-
count: 1
1171-
path: src/PhpSpreadsheet/Chart/Chart.php
1172-
1173-
-
1174-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getTopLeftXOffset\\(\\) has no return type specified\\.$#"
1175-
count: 1
1176-
path: src/PhpSpreadsheet/Chart/Chart.php
1177-
1178-
-
1179-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:getTopLeftYOffset\\(\\) has no return type specified\\.$#"
1180-
count: 1
1181-
path: src/PhpSpreadsheet/Chart/Chart.php
1182-
1183-
-
1184-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightCell\\(\\) has no return type specified\\.$#"
1185-
count: 1
1186-
path: src/PhpSpreadsheet/Chart/Chart.php
1187-
1188-
-
1189-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightCell\\(\\) has parameter \\$cell with no type specified\\.$#"
1190-
count: 1
1191-
path: src/PhpSpreadsheet/Chart/Chart.php
1192-
1193-
-
1194-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightXOffset\\(\\) has no return type specified\\.$#"
1195-
count: 1
1196-
path: src/PhpSpreadsheet/Chart/Chart.php
1197-
1198-
-
1199-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightXOffset\\(\\) has parameter \\$xOffset with no type specified\\.$#"
1200-
count: 1
1201-
path: src/PhpSpreadsheet/Chart/Chart.php
1202-
1203-
-
1204-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightYOffset\\(\\) has no return type specified\\.$#"
1205-
count: 1
1206-
path: src/PhpSpreadsheet/Chart/Chart.php
1207-
1208-
-
1209-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setBottomRightYOffset\\(\\) has parameter \\$yOffset with no type specified\\.$#"
1210-
count: 1
1211-
path: src/PhpSpreadsheet/Chart/Chart.php
1212-
1213-
-
1214-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftXOffset\\(\\) has no return type specified\\.$#"
1215-
count: 1
1216-
path: src/PhpSpreadsheet/Chart/Chart.php
1217-
1218-
-
1219-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftXOffset\\(\\) has parameter \\$xOffset with no type specified\\.$#"
1220-
count: 1
1221-
path: src/PhpSpreadsheet/Chart/Chart.php
1222-
1223-
-
1224-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftYOffset\\(\\) has no return type specified\\.$#"
1225-
count: 1
1226-
path: src/PhpSpreadsheet/Chart/Chart.php
1227-
1228-
-
1229-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:setTopLeftYOffset\\(\\) has parameter \\$yOffset with no type specified\\.$#"
1230-
count: 1
1231-
path: src/PhpSpreadsheet/Chart/Chart.php
1232-
12331163
-
12341164
message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$legend \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\|null\\.$#"
12351165
count: 1

src/PhpSpreadsheet/Chart/Chart.php

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class Chart
124124
*
125125
* @var string
126126
*/
127-
private $bottomRightCellRef = 'A1';
127+
private $bottomRightCellRef = '';
128128

129129
/**
130130
* Bottom-Right X-Offset.
@@ -400,15 +400,15 @@ public function getMinorGridlines()
400400
/**
401401
* Set the Top Left position for the chart.
402402
*
403-
* @param string $cell
403+
* @param string $cellAddress
404404
* @param int $xOffset
405405
* @param int $yOffset
406406
*
407407
* @return $this
408408
*/
409-
public function setTopLeftPosition($cell, $xOffset = null, $yOffset = null)
409+
public function setTopLeftPosition($cellAddress, $xOffset = null, $yOffset = null)
410410
{
411-
$this->topLeftCellRef = $cell;
411+
$this->topLeftCellRef = $cellAddress;
412412
if ($xOffset !== null) {
413413
$this->setTopLeftXOffset($xOffset);
414414
}
@@ -446,13 +446,13 @@ public function getTopLeftCell()
446446
/**
447447
* Set the Top Left cell position for the chart.
448448
*
449-
* @param string $cell
449+
* @param string $cellAddress
450450
*
451451
* @return $this
452452
*/
453-
public function setTopLeftCell($cell)
453+
public function setTopLeftCell($cellAddress)
454454
{
455-
$this->topLeftCellRef = $cell;
455+
$this->topLeftCellRef = $cellAddress;
456456

457457
return $this;
458458
}
@@ -491,42 +491,52 @@ public function getTopLeftOffset()
491491
];
492492
}
493493

494+
/**
495+
* @param int $xOffset
496+
*
497+
* @return $this
498+
*/
494499
public function setTopLeftXOffset($xOffset)
495500
{
496501
$this->topLeftXOffset = $xOffset;
497502

498503
return $this;
499504
}
500505

501-
public function getTopLeftXOffset()
506+
public function getTopLeftXOffset(): int
502507
{
503508
return $this->topLeftXOffset;
504509
}
505510

511+
/**
512+
* @param int $yOffset
513+
*
514+
* @return $this
515+
*/
506516
public function setTopLeftYOffset($yOffset)
507517
{
508518
$this->topLeftYOffset = $yOffset;
509519

510520
return $this;
511521
}
512522

513-
public function getTopLeftYOffset()
523+
public function getTopLeftYOffset(): int
514524
{
515525
return $this->topLeftYOffset;
516526
}
517527

518528
/**
519529
* Set the Bottom Right position of the chart.
520530
*
521-
* @param string $cell
531+
* @param string $cellAddress
522532
* @param int $xOffset
523533
* @param int $yOffset
524534
*
525535
* @return $this
526536
*/
527-
public function setBottomRightPosition($cell, $xOffset = null, $yOffset = null)
537+
public function setBottomRightPosition($cellAddress = '', $xOffset = null, $yOffset = null)
528538
{
529-
$this->bottomRightCellRef = $cell;
539+
$this->bottomRightCellRef = $cellAddress;
530540
if ($xOffset !== null) {
531541
$this->setBottomRightXOffset($xOffset);
532542
}
@@ -551,19 +561,22 @@ public function getBottomRightPosition()
551561
];
552562
}
553563

554-
public function setBottomRightCell($cell)
564+
/**
565+
* Set the Bottom Right cell for the chart.
566+
*
567+
* @return $this
568+
*/
569+
public function setBottomRightCell(string $cellAddress = '')
555570
{
556-
$this->bottomRightCellRef = $cell;
571+
$this->bottomRightCellRef = $cellAddress;
557572

558573
return $this;
559574
}
560575

561576
/**
562577
* Get the cell address where the bottom right of the chart is fixed.
563-
*
564-
* @return string
565578
*/
566-
public function getBottomRightCell()
579+
public function getBottomRightCell(): string
567580
{
568581
return $this->bottomRightCellRef;
569582
}
@@ -602,26 +615,36 @@ public function getBottomRightOffset()
602615
];
603616
}
604617

618+
/**
619+
* @param int $xOffset
620+
*
621+
* @return $this
622+
*/
605623
public function setBottomRightXOffset($xOffset)
606624
{
607625
$this->bottomRightXOffset = $xOffset;
608626

609627
return $this;
610628
}
611629

612-
public function getBottomRightXOffset()
630+
public function getBottomRightXOffset(): int
613631
{
614632
return $this->bottomRightXOffset;
615633
}
616634

635+
/**
636+
* @param int $yOffset
637+
*
638+
* @return $this
639+
*/
617640
public function setBottomRightYOffset($yOffset)
618641
{
619642
$this->bottomRightYOffset = $yOffset;
620643

621644
return $this;
622645
}
623646

624-
public function getBottomRightYOffset()
647+
public function getBottomRightYOffset(): int
625648
{
626649
return $this->bottomRightYOffset;
627650
}

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,10 @@ public function listWorksheetInfo($filename)
226226
$worksheets = [];
227227
foreach ($relsWorkbook->Relationship as $elex) {
228228
$ele = self::getAttributes($elex);
229-
if ((string) $ele['Type'] === "$namespace/worksheet") {
229+
if (
230+
((string) $ele['Type'] === "$namespace/worksheet") ||
231+
((string) $ele['Type'] === "$namespace/chartsheet")
232+
) {
230233
$worksheets[(string) $ele['Id']] = $ele['Target'];
231234
}
232235
}
@@ -512,6 +515,12 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
512515
case Namespaces::PURL_WORKSHEET:
513516
$worksheets[(string) $ele['Id']] = $ele['Target'];
514517

518+
break;
519+
case Namespaces::CHARTSHEET:
520+
if ($this->includeCharts === true) {
521+
$worksheets[(string) $ele['Id']] = $ele['Target'];
522+
}
523+
515524
break;
516525
// a vbaProject ? (: some macros)
517526
case Namespaces::VBA:
@@ -690,6 +699,13 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
690699
continue;
691700
}
692701

702+
$sheetReferenceId = (string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id');
703+
if (isset($worksheets[$sheetReferenceId]) === false) {
704+
++$countSkippedSheets;
705+
$mapSheetId[$oldSheetId] = null;
706+
707+
continue;
708+
}
693709
// Map old sheet id in original workbook to new sheet id.
694710
// They will differ if loadSheetsOnly() is being used
695711
$mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets;
@@ -701,7 +717,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
701717
// and we're simply bringing the worksheet name in line with the formula, not the
702718
// reverse
703719
$docSheet->setTitle((string) $eleSheetAttr['name'], false, false);
704-
$fileWorksheet = (string) $worksheets[(string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id')];
720+
721+
$fileWorksheet = (string) $worksheets[$sheetReferenceId];
705722
$xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS);
706723
$xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS);
707724

@@ -1195,6 +1212,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
11951212
$drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
11961213
}
11971214
}
1215+
11981216
if ($xmlSheet->drawing && !$this->readDataOnly) {
11991217
$unparsedDrawings = [];
12001218
$fileDrawing = null;
@@ -1203,6 +1221,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
12031221
$fileDrawing = $drawings[$drawingRelId];
12041222
$drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels';
12051223
$relsDrawing = $this->loadZipNoNamespace($drawingFilename, $xmlNamespaceBase);
1224+
12061225
$images = [];
12071226
$hyperlinks = [];
12081227
if ($relsDrawing && $relsDrawing->Relationship) {
@@ -1223,6 +1242,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
12231242
}
12241243
}
12251244
}
1245+
12261246
$xmlDrawing = $this->loadZipNoNamespace($fileDrawing, '');
12271247
$xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING);
12281248

@@ -1401,6 +1421,27 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
14011421
}
14021422
}
14031423
}
1424+
if ($xmlDrawingChildren->absoluteAnchor) {
1425+
foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) {
1426+
if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) {
1427+
$graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1428+
/** @var SimpleXMLElement $chartRef */
1429+
$chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1430+
$thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1431+
$width = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cx')[0]);
1432+
$height = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cy')[0]);
1433+
1434+
$chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1435+
'fromCoordinate' => 'A1',
1436+
'fromOffsetX' => 0,
1437+
'fromOffsetY' => 0,
1438+
'width' => $width,
1439+
'height' => $height,
1440+
'worksheetTitle' => $docSheet->getTitle(),
1441+
];
1442+
}
1443+
}
1444+
}
14041445
if (empty($relsDrawing) && $xmlDrawing->count() == 0) {
14051446
// Save Drawing without rels and children as unparsed
14061447
$unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
@@ -1600,16 +1641,21 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
16001641
$chartEntryRef = ltrim((string) $contentType['PartName'], '/');
16011642
$chartElements = $this->loadZip($chartEntryRef);
16021643
$objChart = Chart::readChart($chartElements, basename($chartEntryRef, '.xml'));
1603-
16041644
if (isset($charts[$chartEntryRef])) {
16051645
$chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id'];
16061646
if (isset($chartDetails[$chartPositionRef])) {
16071647
$excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart);
16081648
$objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet']));
1609-
$objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
1649+
// For oneCellAnchor or absoluteAnchor positioned charts,
1650+
// toCoordinate is not in the data. Does it need to be calculated?
16101651
if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) {
1611-
// For oneCellAnchor positioned charts, toCoordinate is not in the data. Does it need to be calculated?
1652+
// twoCellAnchor
1653+
$objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
16121654
$objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']);
1655+
} else {
1656+
// oneCellAnchor or absoluteAnchor (e.g. Chart sheet)
1657+
$objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
1658+
$objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']);
16131659
}
16141660
}
16151661
}

src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ class Namespaces
5050

5151
const WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
5252

53+
const CHARTSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet';
54+
5355
const SCHEMA_MICROSOFT = 'http://schemas.microsoft.com/office/2006/relationships';
5456

5557
const EXTENSIBILITY = 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility';

0 commit comments

Comments
 (0)