diff --git a/samples/Chart/32_Chart_read_write.php b/samples/Chart/32_Chart_read_write.php index a1f2f54681..42944c0ccb 100644 --- a/samples/Chart/32_Chart_read_write.php +++ b/samples/Chart/32_Chart_read_write.php @@ -42,7 +42,7 @@ foreach ($chartNames as $i => $chartName) { $chart = $worksheet->getChartByName($chartName); if ($chart->getTitle() !== null) { - $caption = '"' . implode(' ', $chart->getTitle()->getCaption()) . '"'; + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; } else { $caption = 'Untitled'; } diff --git a/samples/Chart/37_Chart_dynamic_title.php b/samples/Chart/37_Chart_dynamic_title.php new file mode 100644 index 0000000000..b8b801faae --- /dev/null +++ b/samples/Chart/37_Chart_dynamic_title.php @@ -0,0 +1,83 @@ +log('File ' . $inputFileNameShort . ' does not exist'); + + continue; + } + $reader = IOFactory::createReader($inputFileType); + $reader->setIncludeCharts(true); + $callStartTime = microtime(true); + $spreadsheet = $reader->load($inputFileName); + $helper->logRead($inputFileType, $inputFileName, $callStartTime); + + $helper->log('Iterate worksheets looking at the charts'); + foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { + $sheetName = $worksheet->getTitle(); + $worksheet->getCell('A1')->setValue('Changed Title'); + $helper->log('Worksheet: ' . $sheetName); + + $chartNames = $worksheet->getChartNames(); + if (empty($chartNames)) { + $helper->log(' There are no charts in this worksheet'); + } else { + natsort($chartNames); + foreach ($chartNames as $i => $chartName) { + $chart = $worksheet->getChartByName($chartName); + if ($chart->getTitle() !== null) { + $caption = '"' . $chart->getTitle()->getCaptionText($spreadsheet) . '"'; + } else { + $caption = 'Untitled'; + } + $helper->log(' ' . $chartName . ' - ' . $caption); + $indentation = str_repeat(' ', strlen($chartName) + 3); + $groupCount = $chart->getPlotArea()->getPlotGroupCount(); + if ($groupCount == 1) { + $chartType = $chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + $helper->log($indentation . ' ' . $chartType); + $helper->renderChart($chart, __FILE__, $spreadsheet); + } else { + $chartTypes = []; + for ($i = 0; $i < $groupCount; ++$i) { + $chartTypes[] = $chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + } + $chartTypes = array_unique($chartTypes); + if (count($chartTypes) == 1) { + $chartType = 'Multiple Plot ' . array_pop($chartTypes); + $helper->log($indentation . ' ' . $chartType); + $helper->renderChart($chart, __FILE__); + } elseif (count($chartTypes) == 0) { + $helper->log($indentation . ' *** Type not yet implemented'); + } else { + $helper->log($indentation . ' Combination Chart'); + $helper->renderChart($chart, __FILE__); + } + } + } + } + } + + $callStartTime = microtime(true); + $helper->write($spreadsheet, $inputFileName, ['Xlsx'], true); + + Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); + $callStartTime = microtime(true); + $helper->write($spreadsheet, $inputFileName, ['Html'], true); + + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); +} diff --git a/samples/templates/37dynamictitle.xlsx b/samples/templates/37dynamictitle.xlsx new file mode 100644 index 0000000000..6a5215e861 Binary files /dev/null and b/samples/templates/37dynamictitle.xlsx differ diff --git a/src/PhpSpreadsheet/Chart/Title.php b/src/PhpSpreadsheet/Chart/Title.php index d8e6e7497f..378987446e 100644 --- a/src/PhpSpreadsheet/Chart/Title.php +++ b/src/PhpSpreadsheet/Chart/Title.php @@ -3,9 +3,16 @@ namespace PhpOffice\PhpSpreadsheet\Chart; use PhpOffice\PhpSpreadsheet\RichText\RichText; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Font; class Title { + public const TITLE_CELL_REFERENCE + = '/^(.*)!' // beginning of string, everything up to ! is match[1] + . '[$]([A-Z]{1,3})' // absolute column string match[2] + . '[$](\d{1,7})$/i'; // absolute row string match[3] + /** * Title Caption. * @@ -25,6 +32,10 @@ class Title */ private ?Layout $layout; + private string $cellReference = ''; + + private ?Font $font = null; + /** * Create a new Title. * @@ -48,8 +59,14 @@ public function getCaption() return $this->caption; } - public function getCaptionText(): string + public function getCaptionText(?Spreadsheet $spreadsheet = null): string { + if ($spreadsheet !== null) { + $caption = $this->getCalculatedTitle($spreadsheet); + if ($caption !== null) { + return $caption; + } + } $caption = $this->caption; if (is_string($caption)) { return $caption; @@ -100,13 +117,50 @@ public function getOverlay() * * @param bool $overlay */ - public function setOverlay($overlay): void + public function setOverlay($overlay): static { $this->overlay = $overlay; + + return $this; } public function getLayout(): ?Layout { return $this->layout; } + + public function setCellReference(string $cellReference): self + { + $this->cellReference = $cellReference; + + return $this; + } + + public function getCellReference(): string + { + return $this->cellReference; + } + + public function getCalculatedTitle(?Spreadsheet $spreadsheet): ?string + { + preg_match(self::TITLE_CELL_REFERENCE, $this->cellReference, $matches); + if (count($matches) === 0 || $spreadsheet === null) { + return null; + } + $sheetName = preg_replace("/^'(.*)'$/", '$1', $matches[1]) ?? ''; + + return $spreadsheet->getSheetByName($sheetName)?->getCell($matches[2] . $matches[3])?->getFormattedValue(); + } + + public function getFont(): ?Font + { + return $this->font; + } + + public function setFont(?Font $font): self + { + $this->font = $font; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 3d53f1e053..cb23cf597c 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -198,7 +198,7 @@ public function log(string $message): void * * @codeCoverageIgnore */ - public function renderChart(Chart $chart, string $fileName): void + public function renderChart(Chart $chart, string $fileName, ?Spreadsheet $spreadsheet = null): void { if ($this->isCli() === true) { return; @@ -206,17 +206,32 @@ public function renderChart(Chart $chart, string $fileName): void Settings::setChartRenderer(MtJpGraphRenderer::class); $fileName = $this->getFilename($fileName, 'png'); + $title = $chart->getTitle(); + $caption = null; + if ($title !== null) { + $calculatedTitle = $title->getCalculatedTitle($spreadsheet); + if ($calculatedTitle !== null) { + $caption = $title->getCaption(); + $title->setCaption($calculatedTitle); + } + } try { $chart->render($fileName); $this->log('Rendered image: ' . $fileName); - $imageData = file_get_contents($fileName); + $imageData = @file_get_contents($fileName); if ($imageData !== false) { echo '
'; + } else { + $this->log('Unable to open chart' . PHP_EOL); } } catch (Throwable $e) { $this->log('Error rendering chart: ' . $e->getMessage() . PHP_EOL); } + if (isset($title, $caption)) { + $title->setCaption($caption); + } + Settings::unsetChartRenderer(); } public function titles(string $category, string $functionName, ?string $description = null): void diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 789d46ee78..32737f01a6 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -497,6 +497,8 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title $caption = []; $titleLayout = null; $titleOverlay = false; + $titleFormula = null; + $titleFont = null; foreach ($titleDetails as $titleDetailKey => $chartDetail) { $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($titleDetailKey) { @@ -517,6 +519,9 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title $caption[] = (string) $pt->v; } } + if (isset($chartDetail->strRef->f)) { + $titleFormula = (string) $chartDetail->strRef->f; + } } break; @@ -527,11 +532,24 @@ private function chartTitle(SimpleXMLElement $titleDetails): Title case 'layout': $titleLayout = $this->chartLayoutDetails($chartDetail); + break; + case 'txPr': + if (isset($chartDetail->children($this->aNamespace)->p)) { + $titleFont = $this->parseFont($chartDetail->children($this->aNamespace)->p); + } + break; } } + $title = new Title($caption, $titleLayout, (bool) $titleOverlay); + if (!empty($titleFormula)) { + $title->setCellReference($titleFormula); + } + if ($titleFont !== null) { + $title->setFont($titleFont); + } - return new Title($caption, $titleLayout, (bool) $titleOverlay); + return $title; } private function chartLayoutDetails(SimpleXMLElement $chartDetail): ?Layout @@ -1185,6 +1203,7 @@ private function parseFont(SimpleXMLElement $titleDetailPart): ?Font $fontArray['italic'] = self::getAttributeBoolean($titleDetailPart->pPr->defRPr, 'i'); $fontArray['underscore'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'u'); $fontArray['strikethrough'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'strike'); + $fontArray['cap'] = self::getAttributeString($titleDetailPart->pPr->defRPr, 'cap'); if (isset($titleDetailPart->pPr->defRPr->latin)) { $fontArray['latin'] = self::getAttributeString($titleDetailPart->pPr->defRPr->latin, 'typeface'); diff --git a/src/PhpSpreadsheet/Settings.php b/src/PhpSpreadsheet/Settings.php index 9f2ef0cd89..f7daa45750 100644 --- a/src/PhpSpreadsheet/Settings.php +++ b/src/PhpSpreadsheet/Settings.php @@ -78,6 +78,11 @@ public static function setChartRenderer(string $rendererClassName): void self::$chartRenderer = $rendererClassName; } + public static function unsetChartRenderer(): void + { + self::$chartRenderer = null; + } + /** * Return the Chart Rendering Library that PhpSpreadsheet is currently configured to use. * diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 09a24d10ab..ea26b8ce64 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -13,6 +13,13 @@ class Font extends Supervisor const UNDERLINE_SINGLE = 'single'; const UNDERLINE_SINGLEACCOUNTING = 'singleAccounting'; + const CAP_ALL = 'all'; + const CAP_SMALL = 'small'; + const CAP_NONE = 'none'; + private const VALID_CAPS = [self::CAP_ALL, self::CAP_SMALL, self::CAP_NONE]; + + protected ?string $cap = null; + /** * Font Name. * @@ -236,6 +243,9 @@ public function applyFromArray(array $styleArray): static if (isset($styleArray['scheme'])) { $this->setScheme($styleArray['scheme']); } + if (isset($styleArray['cap'])) { + $this->setCap($styleArray['cap']); + } } return $this; @@ -795,6 +805,7 @@ public function getHashCode() $this->hashChartColor($this->chartColor), $this->hashChartColor($this->underlineColor), (string) $this->baseLine, + (string) $this->cap, ] ) . __CLASS__ @@ -806,6 +817,7 @@ protected function exportArray1(): array $exportedArray = []; $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); $this->exportArray2($exportedArray, 'bold', $this->getBold()); + $this->exportArray2($exportedArray, 'cap', $this->getCap()); $this->exportArray2($exportedArray, 'chartColor', $this->getChartColor()); $this->exportArray2($exportedArray, 'color', $this->getColor()); $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); @@ -847,4 +859,23 @@ public function setScheme(string $scheme): self return $this; } + + /** + * Set capitalization attribute. If not one of the permitted + * values (all, small, or none), set it to null. + * This will be honored only for the font for chart titles. + * None is distinguished from null because null will inherit + * the current value, whereas 'none' will override it. + */ + public function setCap(string $cap): self + { + $this->cap = in_array($cap, self::VALID_CAPS, true) ? $cap : null; + + return $this; + } + + public function getCap(): ?string + { + return $this->cap; + } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index b4547637cf..5df0525f11 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -23,6 +23,7 @@ use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; @@ -150,6 +151,12 @@ class Html extends BaseWriter */ private $editHtmlCallback; + /** @var BaseDrawing[] */ + private $sheetDrawings; + + /** @var Chart[] */ + private $sheetCharts; + /** * Create a new HTML. */ @@ -479,11 +486,14 @@ public function generateSheetData(): string foreach ($sheets as $sheet) { // Write table header $html .= $this->generateTableHeader($sheet); + $this->sheetCharts = []; + $this->sheetDrawings = []; // Get worksheet dimension [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension()); [$minCol, $minRow] = Coordinate::indexesFromString($min); [$maxCol, $maxRow] = Coordinate::indexesFromString($max); + $this->extendRowsAndColumns($sheet, $maxCol, $maxRow); [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow); @@ -510,8 +520,6 @@ public function generateSheetData(): string $html .= $endTag; } - --$row; - $html .= $this->extendRowsForChartsAndImages($sheet, $row); // Write table footer $html .= $this->generateTableFooter(); @@ -563,78 +571,33 @@ public function generateNavigation(): string return $html; } - /** - * Extend Row if chart is placed after nominal end of row. - * This code should be exercised by sample: - * Chart/32_Chart_read_write_PDF.php. - * - * @param int $row Row to check for charts - */ - private function extendRowsForCharts(Worksheet $worksheet, int $row): array + private function extendRowsAndColumns(Worksheet $worksheet, int &$colMax, int &$rowMax): void { - $rowMax = $row; - $colMax = 'A'; - $anyfound = false; if ($this->includeCharts) { foreach ($worksheet->getChartCollection() as $chart) { if ($chart instanceof Chart) { - $anyfound = true; $chartCoordinates = $chart->getTopLeftPosition(); - $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']); - $chartCol = Coordinate::columnIndexFromString($chartTL[0]); + $this->sheetCharts[$chartCoordinates['cell']] = $chart; + $chartTL = Coordinate::indexesFromString($chartCoordinates['cell']); if ($chartTL[1] > $rowMax) { $rowMax = $chartTL[1]; } - if ($chartCol > Coordinate::columnIndexFromString($colMax)) { + if ($chartTL[0] > $colMax) { $colMax = $chartTL[0]; } } } } - - return [$rowMax, $colMax, $anyfound]; - } - - private function extendRowsForChartsAndImages(Worksheet $worksheet, int $row): string - { - [$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($worksheet, $row); - foreach ($worksheet->getDrawingCollection() as $drawing) { - $anyfound = true; - $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates()); - $imageCol = Coordinate::columnIndexFromString($imageTL[0]); + $imageTL = Coordinate::indexesFromString($drawing->getCoordinates()); + $this->sheetDrawings[$drawing->getCoordinates()] = $drawing; if ($imageTL[1] > $rowMax) { $rowMax = $imageTL[1]; } - if ($imageCol > Coordinate::columnIndexFromString($colMax)) { + if ($imageTL[0] > $colMax) { $colMax = $imageTL[0]; } } - - // Don't extend rows if not needed - if ($row === $rowMax || !$anyfound) { - return ''; - } - - $html = ''; - ++$colMax; - ++$row; - while ($row <= $rowMax) { - $html .= '