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);