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