From 7d0f2ad706a5f28fa4bf7426de4bcda8d76f6d32 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Tue, 16 Jun 2026 20:08:46 -0700
Subject: [PATCH] Avoid Hard-Coding Some Chart Xml Attributes
Writer/Xlsx/Chart hard-codes a lot of output values. While nobody has reported a problem involving these, it makes sense to me to copy those values over from what Xlsx/Reader/Chart read, rather than hard-coding them. This involves adding some new properties to Chart and Layout, all, naturally, initialized to the values that we have been hard-coding.
- Chart\date1904
- Chart\lang
- Chart\pageMargins
- Chart\pageSetup
- Layout\bodyPr
With one exception, I have not investigated any of the new properties in depth. That may come in time. The one I did look at is `date1904`. Chances are that it should match the equivalent spreadsheet setting, but it won't matter if there are no dates on your chart, and it often won't matter even if you do. It may well matter if you are using a `date axis`. While looking into that, it became apparent that some Shared Date conversions need to be a bit more flexible, specifying an optional `calendar` parameter rather than relying on the Spreadsheet `calendar` (which is not accessible while processing the chart) or the Shared\Date `calendar` (which is accessible).
---
.../33_Chart_create_line_dateaxis.php | 18 ++-
src/PhpSpreadsheet/Cell/Cell.php | 20 ++-
src/PhpSpreadsheet/Chart/Chart.php | 119 +++++++++++++++++-
src/PhpSpreadsheet/Chart/Layout.php | 80 +++++++++++-
src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 62 ++++++---
src/PhpSpreadsheet/Shared/Date.php | 56 +++++----
src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 57 +++++----
.../PhpSpreadsheetTests/Chart/CopyXmlTest.php | 42 +++++++
.../Chart/Issue2931Test.php | 4 +-
tests/PhpSpreadsheetTests/Shared/DateTest.php | 34 +++++
10 files changed, 415 insertions(+), 77 deletions(-)
create mode 100644 tests/PhpSpreadsheetTests/Chart/CopyXmlTest.php
diff --git a/samples/Chart33a/33_Chart_create_line_dateaxis.php b/samples/Chart33a/33_Chart_create_line_dateaxis.php
index 86415c4aa..7cdefbba7 100644
--- a/samples/Chart33a/33_Chart_create_line_dateaxis.php
+++ b/samples/Chart33a/33_Chart_create_line_dateaxis.php
@@ -10,6 +10,10 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet;
require __DIR__ . '/../Header.php';
/** @var \PhpOffice\PhpSpreadsheet\Helper\Sample $helper */
$spreadsheet = new Spreadsheet();
+// following stmt can be commented in/out to test both calendars
+$spreadsheet->setExcelCalendar(SharedDate::CALENDAR_MAC_1904);
+$calendar = $spreadsheet->getExcelCalendar();
+$use1904 = $calendar === SharedDate::CALENDAR_MAC_1904;
$dataSheet = $spreadsheet->getActiveSheet();
$dataSheet->setTitle('Data');
// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date
@@ -185,6 +189,9 @@ $chart = new Chart(
$xAxis, // xAxis
$yAxis, // yAxis
);
+if ($use1904) {
+ $chart->setDate1904(true);
+}
// Set the position of the chart in the chart sheet
$chart->setTopLeftPosition('A1');
@@ -275,7 +282,7 @@ $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601);
$xAxis->setAxisType('dateAx'); // dateAx available ONLY for LINECHART, not SCATTERCHART
// measure the time span in Quarters, of data.
-$dateMinMax = dateRange(8, $spreadsheet); // array 'min'=>earliest date of first Q, 'max'=>latest date of final Q
+$dateMinMax = dateRange(8, $spreadsheet, $calendar); // array 'min'=>earliest date of first Q, 'max'=>latest date of final Q
// change xAxis tick marks to match Qtr boundaries
$nQtrs = sprintf('%3.2f', (($dateMinMax['max'] - $dateMinMax['min']) / 30.5) / 4);
@@ -321,6 +328,9 @@ $chart = new Chart(
$xAxis, // xAxis
$yAxis, // yAxis
);
+if ($use1904) {
+ $chart->setDate1904(true);
+}
// Set the position of the chart in the chart sheet below the first chart
$chart->setTopLeftPosition('A13');
@@ -339,7 +349,7 @@ $helper->write($spreadsheet, __FILE__, ['Xlsx'], true, resetActiveSheet: false);
$spreadsheet->disconnectWorksheets();
/** @return array{'min': float|int, 'max': float|int} */
-function dateRange(int $nrows, Spreadsheet $wrkbk): array
+function dateRange(int $nrows, Spreadsheet $wrkbk, int $calendar): array
{
$dataSheet = $wrkbk->getSheetByNameOrThrow('Data');
@@ -357,7 +367,7 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array
$qtr = intdiv($startMonth, 3) + (($startMonth % 3 > 0) ? 1 : 0);
$qtrStartMonth = sprintf('%02d', 1 + (($qtr - 1) * 3));
$qtrStartStr = "$startYr-$qtrStartMonth-01";
- $ExcelQtrStartDateVal = SharedDate::convertIsoDate($qtrStartStr);
+ $ExcelQtrStartDateVal = SharedDate::convertIsoDate($qtrStartStr, $calendar);
// end the xaxis at the end of the quarter of the last date
/** @var string */
@@ -377,7 +387,7 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array
}
$lastDOM = $lastDOMDate->format('t');
$qtrEndStr = "$lastYr-$qtrEndMonth-$lastDOM";
- $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr);
+ $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr, $calendar);
$minMaxDates = ['min' => $ExcelQtrStartDateVal, 'max' => $ExcelQtrEndDateVal];
diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php
index ca13e824f..5fdccfc8d 100644
--- a/src/PhpSpreadsheet/Cell/Cell.php
+++ b/src/PhpSpreadsheet/Cell/Cell.php
@@ -191,12 +191,22 @@ class Cell implements Stringable
public function getFormattedValue(): string
{
$currentCalendar = SharedDate::getExcelCalendar();
- SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
- $formattedValue = (string) NumberFormat::toFormattedString(
- $this->getCalculatedValueString(),
- (string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
+ SharedDate::setExcelCalendar(
+ $this->getWorksheet()
+ ->getParent()
+ ?->getExcelCalendar()
);
- SharedDate::setExcelCalendar($currentCalendar);
+
+ try {
+ $formattedValue = NumberFormat::toFormattedString(
+ $this->getCalculatedValueString(),
+ (string) $this->getStyle()
+ ->getNumberFormat()
+ ->getFormatCode(true)
+ );
+ } finally {
+ SharedDate::setExcelCalendar($currentCalendar);
+ }
return $formattedValue;
}
diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php
index e750edc21..6288ea1d7 100644
--- a/src/PhpSpreadsheet/Chart/Chart.php
+++ b/src/PhpSpreadsheet/Chart/Chart.php
@@ -110,6 +110,37 @@ class Chart
private bool $roundedCorners = false;
+ private bool $date1904 = false;
+
+ private string $lang = 'en-GB';
+
+ /** @var array{
+ * b?: numeric-string,
+ * l?: numeric-string,
+ * r?: numeric-string,
+ * t?: numeric-string,
+ * header?: numeric-string,
+ * footer?: numeric-string,
+ * }
+ */
+ private array $pageMargins = [
+ 'b' => '0.75',
+ 'l' => '0.7',
+ 'r' => '0.7',
+ 't' => '0.75',
+ 'header' => '0.3',
+ 'footer' => '0.3',
+ ];
+
+ /** @var array{
+ * paperSize?: string,
+ * orientation?: string,
+ * }
+ */
+ private array $pageSetup = [
+ 'orientation' => 'portrait',
+ ];
+
private GridLines $borderLines;
private ChartColor $fillColor;
@@ -680,9 +711,11 @@ class Chart
return $this->autoTitleDeleted;
}
- public function setAutoTitleDeleted(bool $autoTitleDeleted): self
+ public function setAutoTitleDeleted(?bool $autoTitleDeleted): self
{
- $this->autoTitleDeleted = $autoTitleDeleted;
+ if (is_bool($autoTitleDeleted)) {
+ $this->autoTitleDeleted = $autoTitleDeleted;
+ }
return $this;
}
@@ -725,6 +758,34 @@ class Chart
return $this;
}
+ public function getDate1904(): bool
+ {
+ return $this->date1904;
+ }
+
+ public function setDate1904(?bool $date1904): self
+ {
+ if ($date1904 !== null) {
+ $this->date1904 = $date1904;
+ }
+
+ return $this;
+ }
+
+ public function getLang(): string
+ {
+ return $this->lang;
+ }
+
+ public function setLang(?string $lang): self
+ {
+ if ($lang !== null && $lang !== '') {
+ $this->lang = $lang;
+ }
+
+ return $this;
+ }
+
public function getBorderLines(): GridLines
{
return $this->borderLines;
@@ -782,4 +843,58 @@ class Chart
$this->borderLines = clone $this->borderLines;
$this->fillColor = clone $this->fillColor;
}
+
+ /** @return array{
+ * b?: numeric-string,
+ * l?: numeric-string,
+ * r?: numeric-string,
+ * t?: numeric-string,
+ * header?: numeric-string,
+ * footer?: numeric-string,
+ * }
+ */
+ public function getPageMargins(): array
+ {
+ return $this->pageMargins;
+ }
+
+ /** @param mixed $pageMargins expecting array matching $this->pageMargins */
+ public function setPageMargins(mixed $pageMargins): self
+ {
+ if (is_array($pageMargins)) {
+ foreach (['b', 'l', 'r', 't', 'header', 'footer'] as $key) {
+ $value = $pageMargins[$key] ?? null;
+ if (is_string($value) && is_numeric($value)) {
+ $this->pageMargins[$key] = "$value";
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /** @return array{
+ * paperSize?: string,
+ * orientation?: string,
+ * }
+ */
+ public function getPageSetup(): array
+ {
+ return $this->pageSetup;
+ }
+
+ /** @param mixed $pageSetup expecting array matching $this->pageSetup */
+ public function setPageSetup(mixed $pageSetup): self
+ {
+ if (is_array($pageSetup)) {
+ foreach (['paperSize', 'orientation'] as $key) {
+ $value = $pageSetup[$key] ?? null;
+ if (is_string($value)) {
+ $this->pageSetup[$key] = "$value";
+ }
+ }
+ }
+
+ return $this;
+ }
}
diff --git a/src/PhpSpreadsheet/Chart/Layout.php b/src/PhpSpreadsheet/Chart/Layout.php
index 95e113d5e..6b90b7b58 100644
--- a/src/PhpSpreadsheet/Chart/Layout.php
+++ b/src/PhpSpreadsheet/Chart/Layout.php
@@ -99,6 +99,26 @@ class Layout
private ?Properties $labelEffects = null;
+ /** @var array{
+ * vertOverflow?: string,
+ * horzOverflow?: string,
+ * wrap?: string,
+ * lIns?: numeric-string,
+ * tIns?: numeric-string,
+ * rIns?: numeric-string,
+ * bIns?: numeric-string,
+ * anchor?: string,
+ * }
+ */
+ private array $bodyPr = [
+ 'wrap' => 'square',
+ 'lIns' => '38100',
+ 'tIns' => '19050',
+ 'rIns' => '38100',
+ 'bIns' => '19050',
+ 'anchor' => 'ctr',
+ ];
+
/**
* Create a new Layout.
*
@@ -106,7 +126,22 @@ class Layout
*/
public function __construct(array $layout = [])
{
- /** @var array{layoutTarget?: string, xMode?: string, yMode?: string, x?: float, y?: float, w?:float, h?:float, dLblPos?: string, labelFont?: ?mixed, labelFontColor?: ?mixed, labelEffects?: ?mixed, numFmtCode?: string} $layout */
+ /** @var array{
+ * layoutTarget?: string,
+ * xMode?: string,
+ * yMode?: string,
+ * x?: float,
+ * y?: float,
+ * w?:float,
+ * h?:float,
+ * dLblPos?: string,
+ * labelFont?: ?mixed,
+ * labelFontColor?: ?mixed,
+ * labelEffects?: ?mixed,
+ * numFmtCode?: string,
+ * bodyPr?: mixed,
+ * } $layout
+ */
if (isset($layout['layoutTarget'])) {
$this->layoutTarget = $layout['layoutTarget'];
}
@@ -156,6 +191,10 @@ class Layout
if ($labelEffects instanceof Properties) {
$this->labelEffects = $labelEffects;
}
+ $bodyPr = $layout['bodyPr'] ?? null;
+ if (is_array($bodyPr)) {
+ $this->setBodyPr($bodyPr);
+ }
}
/** @param mixed[] $layout */
@@ -523,6 +562,45 @@ class Layout
return $this;
}
+ /** @return array{
+ * vertOverflow?: string,
+ * horzOverflow?: string,
+ * wrap?: string,
+ * lIns?: numeric-string,
+ * tIns?: numeric-string,
+ * rIns?: numeric-string,
+ * bIns?: numeric-string,
+ * anchor?: string,
+ * }
+ */
+ public function getBodyPr(): array
+ {
+ return $this->bodyPr;
+ }
+
+ /**
+ * @param mixed $bodyPr expect array matching $this->bodyPr
+ */
+ public function setBodyPr(mixed $bodyPr): self
+ {
+ if (is_array($bodyPr)) {
+ foreach (['vertOverflow', 'horzOverflow', 'wrap', 'anchor'] as $key) {
+ $value = $bodyPr[$key] ?? null;
+ if (is_string($value)) {
+ $this->bodyPr[$key] = "$value";
+ }
+ }
+ foreach (['lIns', 'tIns', 'rIns', 'bIns'] as $key) {
+ $value = $bodyPr[$key] ?? null;
+ if (is_string($value) && is_numeric($value)) {
+ $this->bodyPr[$key] = "$value";
+ }
+ }
+ }
+
+ return $this;
+ }
+
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php
index 2b6dfb687..d00a7aed0 100644
--- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php
+++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php
@@ -92,13 +92,26 @@ class Chart
$chartFillColor = null;
$gradientArray = [];
$gradientLin = null;
- $roundedCorners = false;
+ $roundedCorners = null;
+ $date1904 = null;
+ $lang = null;
$gapWidth = null;
$useUpBars = null;
$useDownBars = null;
$noBorder = false;
+ $pageMargins = [];
+ $pageSetup = [];
foreach ($chartElementsC as $chartElementKey => $chartElement) {
switch ($chartElementKey) {
+ case 'printSettings':
+ if (isset($chartElement->pageMargins)) {
+ $pageMargins = current((array) $chartElement->pageMargins->attributes());
+ }
+ if (isset($chartElement->pageSetup)) {
+ $pageSetup = current((array) $chartElement->pageSetup->attributes());
+ }
+
+ break;
case 'spPr':
$children = $chartElementsC->spPr->children($this->aNamespace);
if (isset($children->noFill)) {
@@ -117,16 +130,22 @@ class Chart
break;
case 'roundedCorners':
- /** @var bool $roundedCorners */
$roundedCorners = self::getAttributeBoolean($chartElementsC->roundedCorners, 'val');
+ break;
+ case 'date1904':
+ $date1904 = self::getAttributeBoolean($chartElementsC->date1904, 'val');
+
+ break;
+ case 'lang':
+ $lang = self::getAttributeString($chartElementsC->lang, 'val');
+
break;
case 'chart':
foreach ($chartElement as $chartDetailsKey => $chartDetails) {
$chartDetails = Xlsx::testSimpleXml($chartDetails);
switch ($chartDetailsKey) {
case 'autoTitleDeleted':
- /** @var bool $autoTitleDeleted */
$autoTitleDeleted = self::getAttributeBoolean($chartElementsC->chart->autoTitleDeleted, 'val');
break;
@@ -482,23 +501,18 @@ class Chart
if ($chartBorderLines !== null) {
$chart->setBorderLines($chartBorderLines);
}
- $chart->setNoBorder($noBorder);
- $chart->setRoundedCorners($roundedCorners);
- if (is_bool($autoTitleDeleted)) {
- $chart->setAutoTitleDeleted($autoTitleDeleted);
- }
- if (is_int($rotX)) {
- $chart->setRotX($rotX);
- }
- if (is_int($rotY)) {
- $chart->setRotY($rotY);
- }
- if (is_int($rAngAx)) {
- $chart->setRAngAx($rAngAx);
- }
- if (is_int($perspective)) {
- $chart->setPerspective($perspective);
- }
+ $chart
+ ->setNoBorder($noBorder)
+ ->setRoundedCorners($roundedCorners)
+ ->setDate1904($date1904)
+ ->setLang($lang)
+ ->setPageMargins($pageMargins)
+ ->setPageSetup($pageSetup)
+ ->setAutoTitleDeleted($autoTitleDeleted)
+ ->setRotX($rotX)
+ ->setRotY($rotY)
+ ->setRAngAx($rAngAx)
+ ->setPerspective($perspective);
return $chart;
}
@@ -1299,6 +1313,9 @@ class Chart
$plotAttributes['labelEffects'] = $labelEffects;
}
}
+ if (isset($txpr->bodyPr)) {
+ $plotAttributes['bodyPr'] = current((array) $txpr->bodyPr->attributes());
+ }
}
}
@@ -1366,6 +1383,11 @@ class Chart
/** @var ?Font $plotAttributeValue */
$plotArea->setLabelFont($plotAttributeValue);
+ break;
+ case 'bodyPr':
+ /** @var mixed $plotAttributeValue */
+ $plotArea->setBodyPr($plotAttributeValue);
+
break;
}
}
diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php
index 2e2df9ada..3746cd46b 100644
--- a/src/PhpSpreadsheet/Shared/Date.php
+++ b/src/PhpSpreadsheet/Shared/Date.php
@@ -160,7 +160,7 @@ class Date
* serialized timestamp.
* See https://en.wikipedia.org/wiki/ISO_8601 for details of the ISO-8601 standard format.
*/
- public static function convertIsoDate(mixed $value): float|int
+ public static function convertIsoDate(mixed $value, ?int $calendar = null): float|int
{
if (!is_string($value)) {
throw new Exception('Non-string value supplied for Iso Date conversion');
@@ -173,7 +173,7 @@ class Date
throw new Exception("Invalid string $value supplied for datatype Date");
}
- $newValue = self::dateTimeToExcel($date);
+ $newValue = self::dateTimeToExcel($date, $calendar);
if (preg_match('/^\s*\d?\d:\d\d(:\d\d([.]\d+)?)?\s*(am|pm)?\s*$/i', $value) == 1) {
$newValue = fmod($newValue, 1.0);
@@ -194,16 +194,17 @@ class Date
*
* @return DateTime PHP date/time object
*/
- public static function excelToDateTimeObject(float|int $excelTimestamp, null|DateTimeZone|string $timeZone = null): DateTime
+ public static function excelToDateTimeObject(float|int $excelTimestamp, null|DateTimeZone|string $timeZone = null, ?int $calendar = null): DateTime
{
+ $calendar ??= self::$excelCalendar;
$timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone);
if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
- if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) {
+ if ($excelTimestamp < 1 && $calendar === self::CALENDAR_WINDOWS_1900) {
// Unix timestamp base date
$baseDate = new DateTime('1970-01-01', $timeZone);
} else {
// MS Excel calendar base dates
- if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
+ if ($calendar == self::CALENDAR_WINDOWS_1900) {
// Allow adjustment for 1900 Leap Year in MS Excel
$baseDate = ($excelTimestamp < 60) ? new DateTime('1899-12-31', $timeZone) : new DateTime('1899-12-30', $timeZone);
} else {
@@ -252,9 +253,9 @@ class Date
*
* @return int Unix timetamp for this date/time
*/
- public static function excelToTimestamp($excelTimestamp, $timeZone = null): int
+ public static function excelToTimestamp($excelTimestamp, $timeZone = null, ?int $calendar = null): int
{
- $dto = self::excelToDateTimeObject($excelTimestamp, $timeZone);
+ $dto = self::excelToDateTimeObject($excelTimestamp, $timeZone, $calendar);
self::roundMicroseconds($dto);
return (int) $dto->format('U');
@@ -269,14 +270,16 @@ class Date
* @return false|float Excel date/time value
* or boolean FALSE on failure
*/
- public static function PHPToExcel(mixed $dateValue)
+ public static function PHPToExcel(mixed $dateValue, ?int $calendar = null)
{
if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) {
- return self::dateTimeToExcel($dateValue);
- } elseif (is_numeric($dateValue)) {
- return self::timestampToExcel($dateValue);
- } elseif (is_string($dateValue)) {
- return self::stringToExcel($dateValue);
+ return self::dateTimeToExcel($dateValue, $calendar);
+ }
+ if (is_numeric($dateValue)) {
+ return self::timestampToExcel($dateValue, $calendar);
+ }
+ if (is_string($dateValue)) {
+ return self::stringToExcel($dateValue, $calendar);
}
return false;
@@ -289,7 +292,7 @@ class Date
*
* @return float MS Excel serialized date/time value
*/
- public static function dateTimeToExcel(DateTimeInterface $dateValue): float
+ public static function dateTimeToExcel(DateTimeInterface $dateValue, ?int $calendar = null): float
{
$seconds = (float) sprintf('%d.%06d', $dateValue->format('s'), $dateValue->format('u'));
@@ -299,7 +302,8 @@ class Date
(int) $dateValue->format('d'),
(int) $dateValue->format('H'),
(int) $dateValue->format('i'),
- $seconds
+ $seconds,
+ $calendar
);
}
@@ -312,13 +316,13 @@ class Date
*
* @return false|float MS Excel serialized date/time value
*/
- public static function timestampToExcel($unixTimestamp): bool|float
+ public static function timestampToExcel($unixTimestamp, ?int $calendar = null): bool|float
{
if (!is_numeric($unixTimestamp)) {
return false;
}
- return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp));
+ return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp), $calendar);
}
/**
@@ -326,9 +330,10 @@ class Date
*
* @return float Excel date/time value
*/
- public static function formattedPHPToExcel(int $year, int $month, int $day, int $hours = 0, int $minutes = 0, float|int $seconds = 0): float
+ public static function formattedPHPToExcel(int $year, int $month, int $day, int $hours = 0, int $minutes = 0, float|int $seconds = 0, ?int $calendar = null): float
{
- if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
+ $calendar ??= self::$excelCalendar;
+ if ($calendar === self::CALENDAR_WINDOWS_1900) {
//
// Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel
// This affects every date following 28th February 1900
@@ -474,7 +479,7 @@ class Date
*
* @return false|float Excel date/time serial value
*/
- public static function stringToExcel(string $dateValue): bool|float
+ public static function stringToExcel(string $dateValue, ?int $calendar = null): bool|float
{
if (strlen($dateValue) < 2) {
return false;
@@ -483,7 +488,16 @@ class Date
return false;
}
- $dateValueNew = DateTimeExcel\DateValue::fromString($dateValue);
+ $hold = self::$excelCalendar;
+
+ try {
+ if ($calendar !== null) {
+ self::$excelCalendar = $calendar;
+ }
+ $dateValueNew = DateTimeExcel\DateValue::fromString($dateValue);
+ } finally {
+ self::$excelCalendar = $hold;
+ }
if (!is_float($dateValueNew)) {
return false;
diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php
index a6fc7d07d..d16b42354 100644
--- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php
+++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php
@@ -53,10 +53,10 @@ class Chart extends WriterPart
$objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
$objWriter->startElement('c:date1904');
- $objWriter->writeAttribute('val', '0');
+ $objWriter->writeAttribute('val', $chart->getDate1904() ? '1' : '0');
$objWriter->endElement();
$objWriter->startElement('c:lang');
- $objWriter->writeAttribute('val', 'en-GB');
+ $objWriter->writeAttribute('val', $chart->getLang());
$objWriter->endElement();
$objWriter->startElement('c:roundedCorners');
$objWriter->writeAttribute('val', $chart->getRoundedCorners() ? '1' : '0');
@@ -72,7 +72,6 @@ class Chart extends WriterPart
$objWriter->writeAttribute('val', (string) (int) $chart->getAutoTitleDeleted());
$objWriter->endElement();
- $objWriter->startElement('c:view3D');
$surface2D = false;
$plotArea = $chart->getPlotArea();
if ($plotArea !== null) {
@@ -85,11 +84,14 @@ class Chart extends WriterPart
}
}
}
+ $this->view3DStarted = false;
$this->writeView3D($objWriter, $chart->getRotX(), 'c:rotX', $surface2D, 90);
$this->writeView3D($objWriter, $chart->getRotY(), 'c:rotY', $surface2D);
$this->writeView3D($objWriter, $chart->getRAngAx(), 'c:rAngAx', $surface2D);
$this->writeView3D($objWriter, $chart->getPerspective(), 'c:perspective', $surface2D);
- $objWriter->endElement(); // view3D
+ if ($this->view3DStarted) {
+ $objWriter->endElement(); // view3D
+ }
$this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY());
@@ -123,7 +125,7 @@ class Chart extends WriterPart
$this->writeEffects($objWriter, $borderLines);
$objWriter->endElement(); // c:spPr
- $this->writePrintSettings($objWriter);
+ $this->writePrintSettings($objWriter, $chart);
$objWriter->endElement(); // c:chartSpace
@@ -131,12 +133,18 @@ class Chart extends WriterPart
return $objWriter->getData();
}
+ private bool $view3DStarted = false;
+
private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void
{
if ($value === null && $surface2D) {
$value = $default;
}
if ($value !== null) {
+ if (!$this->view3DStarted) {
+ $objWriter->startElement('c:view3D');
+ $this->view3DStarted = true;
+ }
$objWriter->startElement($tag);
$objWriter->writeAttribute('val', "$value");
$objWriter->endElement();
@@ -549,12 +557,12 @@ class Chart extends WriterPart
$objWriter->startElement('c:txPr');
$objWriter->startElement('a:bodyPr');
- $objWriter->writeAttribute('wrap', 'square');
- $objWriter->writeAttribute('lIns', '38100');
- $objWriter->writeAttribute('tIns', '19050');
- $objWriter->writeAttribute('rIns', '38100');
- $objWriter->writeAttribute('bIns', '19050');
- $objWriter->writeAttribute('anchor', 'ctr');
+ $bodyPr = $chartLayout->getBodyPr();
+ foreach (['vertOverflow', 'horzOverflow', 'wrap', 'lIns', 'tIns', 'rIns', 'bIns', 'anchor'] as $key) {
+ if (isset($bodyPr[$key])) {
+ $objWriter->writeAttribute($key, $bodyPr[$key]);
+ }
+ }
$objWriter->startElement('a:spAutoFit');
$objWriter->endElement(); // a:spAutoFit
$objWriter->endElement(); // a:bodyPr
@@ -1716,27 +1724,32 @@ class Chart extends WriterPart
/**
* Write Printer Settings.
*/
- private function writePrintSettings(XMLWriter $objWriter): void
+ private function writePrintSettings(XMLWriter $objWriter, SpreadsheetChart $chart): void
{
$objWriter->startElement('c:printSettings');
$objWriter->startElement('c:headerFooter');
$objWriter->endElement();
+ $pageMargins = $chart->getPageMargins();
$objWriter->startElement('c:pageMargins');
- $objWriter->writeAttribute('footer', '0.3');
- $objWriter->writeAttribute('header', '0.3');
- $objWriter->writeAttribute('r', '0.7');
- $objWriter->writeAttribute('l', '0.7');
- $objWriter->writeAttribute('t', '0.75');
- $objWriter->writeAttribute('b', '0.75');
- $objWriter->endElement();
+ foreach (['b', 'l', 'r', 't', 'header', 'footer'] as $key) {
+ if (array_key_exists($key, $pageMargins)) {
+ $objWriter->writeAttribute($key, $pageMargins[$key]);
+ }
+ }
+ $objWriter->endElement(); // c:pageMargins
+ $pageSetup = $chart->getPageSetup();
$objWriter->startElement('c:pageSetup');
- $objWriter->writeAttribute('orientation', 'portrait');
- $objWriter->endElement();
+ foreach (['paperSize', 'orientation'] as $key) {
+ if (array_key_exists($key, $pageSetup)) {
+ $objWriter->writeAttribute($key, $pageSetup[$key]);
+ }
+ }
+ $objWriter->endElement(); // c:pageSetup
- $objWriter->endElement();
+ $objWriter->endElement(); // c:printSettings
}
private function writeEffects(XMLWriter $objWriter, Properties $yAxis): void
diff --git a/tests/PhpSpreadsheetTests/Chart/CopyXmlTest.php b/tests/PhpSpreadsheetTests/Chart/CopyXmlTest.php
new file mode 100644
index 000000000..6921cd27f
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Chart/CopyXmlTest.php
@@ -0,0 +1,42 @@
+setIncludeCharts(true);
+ $spreadsheet = $reader->load($infile);
+ $sheet = $spreadsheet->getSheetByNameOrThrow('Charts');
+ $charts = $sheet->getChartCollection();
+ self::assertCount(1, $charts);
+ $chart = $charts[0] ?? null;
+ self::assertInstanceOf(Chart::class, $chart);
+
+ $writer = new XlsxWriter($spreadsheet);
+ $writer->setIncludeCharts(true);
+ $writer = new XlsxWriter($spreadsheet);
+ $writer->setIncludeCharts(true);
+ $writerChart = new XlsxWriter\Chart($writer);
+ $data = $writerChart->writeChart($chart);
+ //echo $data;
+ self::assertStringContainsString('', $data, 'From input even though same as default');
+ self::assertStringContainsString('', $data, 'From input, different from default');
+ self::assertStringNotContainsString('view3D', $data, 'No empty view3D tag');
+ self::assertStringContainsString('', $data, 'A couple of extra attributes');
+ self::assertStringContainsString('', $data, 'Some different values plus re-shuffling');
+ self::assertStringContainsString('', $data, 'An extra attribute');
+
+ $spreadsheet->disconnectWorksheets();
+ }
+}
diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php
index 2e1bd9d81..c2357e902 100644
--- a/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php
+++ b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php
@@ -81,7 +81,7 @@ class Issue2931Test extends TestCase
'',
];
$expectedXml3D = [
- '',
+ 'c:view3D', // empty view3d no longer generated
];
$expectedXmlNoX = [
'c:grouping',
@@ -102,7 +102,7 @@ class Issue2931Test extends TestCase
$data = $writerChart->writeChart($chart);
// confirm that file contains expected tags
foreach ($expectedXml3D as $expected) {
- self::assertSame(1, substr_count($data, $expected), $expected);
+ self::assertSame(0, substr_count($data, $expected), $expected);
}
foreach ($expectedXmlNoX as $expected) {
self::assertSame(0, substr_count($data, $expected), $expected);
diff --git a/tests/PhpSpreadsheetTests/Shared/DateTest.php b/tests/PhpSpreadsheetTests/Shared/DateTest.php
index 6f90aed29..1f6556c57 100644
--- a/tests/PhpSpreadsheetTests/Shared/DateTest.php
+++ b/tests/PhpSpreadsheetTests/Shared/DateTest.php
@@ -100,6 +100,17 @@ class DateTest extends TestCase
self::assertEqualsWithDelta($expectedResult, $result, 1E-5);
}
+ #[DataProvider('providerDateTimeDateTimeToExcel')]
+ public function testDateTime2DateTimeToExcel(float|int $expectedResult, DateTimeInterface $dateTimeObject): void
+ {
+ // Show new parameter will override static value
+ Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
+
+ $result = Date::dateTimeToExcel($dateTimeObject, Date::CALENDAR_WINDOWS_1900);
+ self::assertEqualsWithDelta($expectedResult, $result, 1E-5);
+ self::assertSame(Date::CALENDAR_MAC_1904, Date::getExcelCalendar());
+ }
+
public static function providerDateTimeDateTimeToExcel(): array
{
return require 'tests/data/Shared/Date/DateTimeToExcel.php';
@@ -134,6 +145,18 @@ class DateTest extends TestCase
self::assertEquals($expectedResult, $result);
}
+ #[DataProvider('providerDateTimeExcelToTimestamp1904')]
+ public function testDateTime2ExcelToTimestamp1904(float|int $expectedResult, float|int $excelDateTimeValue): void
+ {
+ if ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN) {
+ self::markTestSkipped('Test invalid on 32-bit system.');
+ }
+
+ $result = Date::excelToTimestamp($excelDateTimeValue, calendar: Date::CALENDAR_MAC_1904);
+ self::assertEquals($expectedResult, $result);
+ self::assertSame($this->excelCalendar, Date::getExcelCalendar());
+ }
+
public static function providerDateTimeExcelToTimestamp1904(): array
{
return require 'tests/data/Shared/Date/ExcelToTimestamp1904.php';
@@ -148,6 +171,14 @@ class DateTest extends TestCase
self::assertEqualsWithDelta($expectedResult, $result, 1E-5);
}
+ #[DataProvider('providerDateTimeTimestampToExcel1904')]
+ public function testDateTime2TimestampToExcel1904(mixed $expectedResult, float|int|string $unixTimestamp): void
+ {
+ $result = Date::timestampToExcel($unixTimestamp, Date::CALENDAR_MAC_1904);
+ self::assertEqualsWithDelta($expectedResult, $result, 1E-5);
+ self::assertSame($this->excelCalendar, Date::getExcelCalendar());
+ }
+
public static function providerDateTimeTimestampToExcel1904(): array
{
return require 'tests/data/Shared/Date/TimestampToExcel1904.php';
@@ -204,6 +235,9 @@ class DateTest extends TestCase
self::assertNotFalse($timestamp2);
self::assertEqualsWithDelta(45803.60277777778, $timestamp1, 1.0E-10);
self::assertSame($timestamp1, $timestamp2);
+ $timestamp3 = Date::stringToExcel('26.05.2025 14:28:00.00', Date::CALENDAR_MAC_1904);
+ self::assertEqualsWithDelta(45803.60277777778, 1462 + $timestamp3, 1.0E-10);
+ self::assertSame($this->excelCalendar, Date::getExcelCalendar());
$date = Date::PHPToExcel('2020-01-01');
self::assertEquals(43831.0, $date);