diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 664306fd75..de63f72f59 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1215,26 +1215,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Chart/DataSeries.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:refresh\\(\\) has parameter \\$flatten with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:\\$dataSource \\(string\\) does not accept string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:\\$dataTypeValues has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\:\\:\\$fillColor \\(array\\\\|string\\) does not accept array\\\\|string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/DataSeriesValues.php - - message: "#^Parameter \\#1 \\$angle of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setShadowAngle\\(\\) expects int, int\\|null given\\.$#" count: 1 @@ -2560,56 +2540,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php - - - message: "#^Cannot call method getFont\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\Run\\|null\\.$#" - count: 12 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setSize\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Cannot call method setUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 3 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeries\\(\\) has no return type specified\\.$#" count: 1 @@ -2635,11 +2565,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has parameter \\$marker with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:chartDataSeriesValueSet\\(\\) has parameter \\$namespacesChartMeta with no type specified\\.$#" count: 1 @@ -2715,21 +2640,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readColor\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readColor\\(\\) has parameter \\$background with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Chart\\:\\:readColor\\(\\) has parameter \\$color with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Chart.php - - message: "#^Parameter \\#1 \\$position of class PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend constructor expects string, bool\\|float\\|int\\|string\\|null given\\.$#" count: 1 @@ -4997,7 +4907,7 @@ parameters: - message: "#^Cannot call method getBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5007,12 +4917,12 @@ parameters: - message: "#^Cannot call method getItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - message: "#^Cannot call method getName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5022,7 +4932,7 @@ parameters: - message: "#^Cannot call method getStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - @@ -5037,7 +4947,7 @@ parameters: - message: "#^Cannot call method getUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - diff --git a/samples/templates/32readwriteScatterChart6.xlsx b/samples/templates/32readwriteScatterChart6.xlsx new file mode 100644 index 0000000000..ddfa304831 Binary files /dev/null and b/samples/templates/32readwriteScatterChart6.xlsx differ diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 745f010600..a4f8f61def 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -12,7 +12,7 @@ class DataSeriesValues const DATASERIES_TYPE_STRING = 'String'; const DATASERIES_TYPE_NUMBER = 'Number'; - private static $dataTypeValues = [ + private const DATA_TYPE_VALUES = [ self::DATASERIES_TYPE_STRING, self::DATASERIES_TYPE_NUMBER, ]; @@ -27,7 +27,7 @@ class DataSeriesValues /** * Series Data Source. * - * @var string + * @var ?string */ private $dataSource; @@ -69,7 +69,7 @@ class DataSeriesValues /** * Fill color (can be array with colors if dataseries have custom colors). * - * @var string|string[] + * @var null|string|string[] */ private $fillColor; @@ -80,6 +80,9 @@ class DataSeriesValues */ private $lineWidth = 12700; + /** @var bool */ + private $scatterLines = true; + /** * Create a new DataSeriesValues object. * @@ -90,8 +93,9 @@ class DataSeriesValues * @param mixed $dataValues * @param null|mixed $marker * @param null|string|string[] $fillColor + * @param string $pointSize */ - public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null) + public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null, $pointSize = '3') { $this->setDataType($dataType); $this->dataSource = $dataSource; @@ -100,6 +104,9 @@ public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSourc $this->dataValues = $dataValues; $this->pointMarker = $marker; $this->fillColor = $fillColor; + if (is_numeric($pointSize)) { + $this->pointSize = (int) $pointSize; + } } /** @@ -126,7 +133,7 @@ public function getDataType() */ public function setDataType($dataType) { - if (!in_array($dataType, self::$dataTypeValues)) { + if (!in_array($dataType, self::DATA_TYPE_VALUES)) { throw new Exception('Invalid datatype for chart data series values'); } $this->dataType = $dataType; @@ -137,7 +144,7 @@ public function setDataType($dataType) /** * Get Series Data Source (formula). * - * @return string + * @return ?string */ public function getDataSource() { @@ -147,7 +154,7 @@ public function getDataSource() /** * Set Series Data Source (formula). * - * @param string $dataSource + * @param ?string $dataSource * * @return $this */ @@ -239,7 +246,7 @@ public function getPointCount() /** * Get fill color. * - * @return string|string[] HEX color or array with HEX colors + * @return null|string|string[] HEX color or array with HEX colors */ public function getFillColor() { @@ -249,7 +256,7 @@ public function getFillColor() /** * Set fill color for series. * - * @param string|string[] $color HEX color or array with HEX colors + * @param null|string|string[] $color HEX color or array with HEX colors * * @return DataSeriesValues */ @@ -260,7 +267,7 @@ public function setFillColor($color) $this->validateColor($colorValue); } } else { - $this->validateColor($color); + $this->validateColor("$color"); } $this->fillColor = $color; @@ -379,7 +386,7 @@ public function setDataValues($dataValues) return $this; } - public function refresh(Worksheet $worksheet, $flatten = true): void + public function refresh(Worksheet $worksheet, bool $flatten = true): void { if ($this->dataSource !== null) { $calcEngine = Calculation::getInstance($worksheet->getParent()); @@ -421,4 +428,16 @@ public function refresh(Worksheet $worksheet, $flatten = true): void $this->pointCount = count($this->dataValues); } } + + public function getScatterLines(): bool + { + return $this->scatterLines; + } + + public function setScatterLines(bool $scatterLines): self + { + $this->scatterLines = $scatterLines; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 680335b737..4e3cd02d59 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -25,7 +25,7 @@ class Chart private static function getAttribute(SimpleXMLElement $component, $name, $format) { $attributes = $component->attributes(); - if (isset($attributes[$name])) { + if (@isset($attributes[$name])) { if ($format == 'string') { return (string) $attributes[$name]; } elseif ($format == 'integer') { @@ -42,15 +42,6 @@ private static function getAttribute(SimpleXMLElement $component, $name, $format return null; } - private static function readColor($color, $background = false) - { - if (isset($color['rgb'])) { - return (string) $color['rgb']; - } elseif (isset($color['indexed'])) { - return Color::indexedColor($color['indexed'] - 7, $background)->getARGB(); - } - } - /** * @param string $chartName * @@ -293,6 +284,10 @@ private static function chartDataSeries($chartDetail, $namespacesChartMeta, $plo case 'ser': $marker = null; $seriesIndex = ''; + $srgbClr = null; + $lineWidth = null; + $pointSize = null; + $noFill = false; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -307,9 +302,25 @@ private static function chartDataSeries($chartDetail, $namespacesChartMeta, $plo case 'tx': $seriesLabel[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + break; + case 'spPr': + $ln = $seriesDetail->children($namespacesChartMeta['a'])->ln; + $lineWidth = self::getAttribute($ln, 'w', 'string'); + if (is_countable($ln->noFill) && count($ln->noFill) === 1) { + $noFill = true; + } + break; case 'marker': $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string'); + $pointSize = self::getAttribute($seriesDetail->size, 'val', 'string'); + $pointSize = is_numeric($pointSize) ? ((int) $pointSize) : null; + if (count($seriesDetail->spPr) === 1) { + $ln = $seriesDetail->spPr->children($namespacesChartMeta['a']); + if (count($ln->solidFill) === 1) { + $srgbClr = self::getattribute($ln->solidFill->srgbClr, 'val', 'string'); + } + } break; case 'smooth': @@ -321,30 +332,52 @@ private static function chartDataSeries($chartDetail, $namespacesChartMeta, $plo break; case 'val': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); break; case 'xVal': - $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); break; case 'yVal': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, "$marker", "$srgbClr", "$pointSize"); break; } } + if ($noFill) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setScatterLines(false); + } + } + if (is_numeric($lineWidth)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setLineWidth((int) $lineWidth); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setLineWidth((int) $lineWidth); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setLineWidth((int) $lineWidth); + } + } } } return new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); } - private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker = null) + private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, ?string $marker = null, ?string $srgbClr = null, ?string $pointSize = null) { if (isset($seriesDetail->strRef)) { $seriesSource = (string) $seriesDetail->strRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->strRef->strCache)) { $seriesData = self::chartDataSeriesValues($seriesDetail->strRef->strCache->children($namespacesChartMeta['c']), 's'); @@ -356,7 +389,7 @@ private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartM return $seriesValues; } elseif (isset($seriesDetail->numRef)) { $seriesSource = (string) $seriesDetail->numRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->numRef->numCache)) { $seriesData = self::chartDataSeriesValues($seriesDetail->numRef->numCache->children($namespacesChartMeta['c'])); $seriesValues @@ -367,7 +400,7 @@ private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartM return $seriesValues; } elseif (isset($seriesDetail->multiLvlStrRef)) { $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) { $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($namespacesChartMeta['c']), 's'); @@ -379,7 +412,7 @@ private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartM return $seriesValues; } elseif (isset($seriesDetail->multiLvlNumRef)) { $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker, $srgbClr, "$pointSize"); if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) { $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($namespacesChartMeta['c']), 's'); @@ -474,62 +507,138 @@ private static function parseRichText(SimpleXMLElement $titleDetailPart) { $value = new RichText(); $objText = null; + $defaultFontSize = null; + $defaultBold = null; + $defaultItalic = null; + $defaultUnderscore = null; + $defaultStrikethrough = null; + $defaultBaseline = null; + $defaultFontName = null; + $defaultColor = null; + if (isset($titleDetailPart->pPr->defRPr)) { + /** @var ?int */ + $defaultFontSize = self::getAttribute($titleDetailPart->pPr->defRPr, 'sz', 'integer'); + /** @var ?bool */ + $defaultBold = self::getAttribute($titleDetailPart->pPr->defRPr, 'b', 'boolean'); + /** @var ?bool */ + $defaultItalic = self::getAttribute($titleDetailPart->pPr->defRPr, 'i', 'boolean'); + /** @var ?string */ + $defaultUnderscore = self::getAttribute($titleDetailPart->pPr->defRPr, 'u', 'string'); + /** @var ?string */ + $defaultStrikethrough = self::getAttribute($titleDetailPart->pPr->defRPr, 'strike', 'string'); + /** @var ?int */ + $defaultBaseline = self::getAttribute($titleDetailPart->pPr->defRPr, 'baseline', 'integer'); + if (isset($titleDetailPart->pPr->defRPr->latin)) { + /** @var ?string */ + $defaultFontName = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->solidFill->srgbClr)) { + /** @var ?string */ + $defaultColor = self::getAttribute($titleDetailPart->pPr->defRPr->solidFill->srgbClr, 'val', 'string'); + } + } foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { if (isset($titleDetailElement->t)) { $objText = $value->createTextRun((string) $titleDetailElement->t); } + if ($objText === null || $objText->getFont() === null) { + continue; + } + $fontSize = null; + $bold = null; + $italic = null; + $underscore = null; + $strikethrough = null; + $baseline = null; + $fontName = null; + $fontColor = null; if (isset($titleDetailElement->rPr)) { + // not used now, not sure it ever was, grandfathering if (isset($titleDetailElement->rPr->rFont['val'])) { - $objText->getFont()->setName((string) $titleDetailElement->rPr->rFont['val']); + $fontName = (string) $titleDetailElement->rPr->rFont['val']; } - - $fontSize = (self::getAttribute($titleDetailElement->rPr, 'sz', 'integer')); - if (is_int($fontSize)) { - $objText->getFont()->setSize(floor($fontSize / 100)); + if (isset($titleDetailElement->rPr->latin)) { + /** @var ?string */ + $fontName = self::getAttribute($titleDetailElement->rPr->latin, 'typeface', 'string'); } - - $fontColor = (self::getAttribute($titleDetailElement->rPr, 'color', 'string')); - if ($fontColor !== null) { - $objText->getFont()->setColor(new Color(self::readColor($fontColor))); + /** @var ?int */ + $fontSize = self::getAttribute($titleDetailElement->rPr, 'sz', 'integer'); + + // not used now, not sure it ever was, grandfathering + /** @var ?string */ + $fontColor = self::getAttribute($titleDetailElement->rPr, 'color', 'string'); + if (isset($titleDetailElement->rPr->solidFill->srgbClr)) { + /** @var ?string */ + $fontColor = self::getAttribute($titleDetailElement->rPr->solidFill->srgbClr, 'val', 'string'); } + /** @var ?bool */ $bold = self::getAttribute($titleDetailElement->rPr, 'b', 'boolean'); - if ($bold !== null) { - $objText->getFont()->setBold($bold); - } + /** @var ?bool */ $italic = self::getAttribute($titleDetailElement->rPr, 'i', 'boolean'); - if ($italic !== null) { - $objText->getFont()->setItalic($italic); - } + /** @var ?int */ $baseline = self::getAttribute($titleDetailElement->rPr, 'baseline', 'integer'); - if ($baseline !== null) { - if ($baseline > 0) { - $objText->getFont()->setSuperscript(true); - } elseif ($baseline < 0) { - $objText->getFont()->setSubscript(true); - } + + /** @var ?string */ + $underscore = self::getAttribute($titleDetailElement->rPr, 'u', 'string'); + + /** @var ?string */ + $strikethrough = self::getAttribute($titleDetailElement->rPr, 's', 'string'); + } + + $fontName = $fontName ?? $defaultFontName; + if ($fontName !== null) { + $objText->getFont()->setName($fontName); + } + + $fontSize = $fontSize ?? $defaultFontSize; + if (is_int($fontSize)) { + $objText->getFont()->setSize(floor($fontSize / 100)); + } + + $fontColor = $fontColor ?? $defaultColor; + if ($fontColor !== null) { + $objText->getFont()->setColor(new Color($fontColor)); + } + + $bold = $bold ?? $defaultBold; + if ($bold !== null) { + $objText->getFont()->setBold($bold); + } + + $italic = $italic ?? $defaultItalic; + if ($italic !== null) { + $objText->getFont()->setItalic($italic); + } + + $baseline = $baseline ?? $defaultBaseline; + if ($baseline !== null) { + if ($baseline > 0) { + $objText->getFont()->setSuperscript(true); + } elseif ($baseline < 0) { + $objText->getFont()->setSubscript(true); } + } - $underscore = (self::getAttribute($titleDetailElement->rPr, 'u', 'string')); - if ($underscore !== null) { - if ($underscore == 'sng') { - $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); - } elseif ($underscore == 'dbl') { - $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); - } else { - $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); - } + $underscore = $underscore ?? $defaultUnderscore; + if ($underscore !== null) { + if ($underscore == 'sng') { + $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); + } elseif ($underscore == 'dbl') { + $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); + } else { + $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); } + } - $strikethrough = (self::getAttribute($titleDetailElement->rPr, 's', 'string')); - if ($strikethrough !== null) { - if ($strikethrough == 'noStrike') { - $objText->getFont()->setStrikethrough(false); - } else { - $objText->getFont()->setStrikethrough(true); - } + $strikethrough = $strikethrough ?? $defaultStrikethrough; + if ($strikethrough !== null) { + if ($strikethrough == 'noStrike') { + $objText->getFont()->setStrikethrough(false); + } else { + $objText->getFont()->setStrikethrough(true); } } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index ba7a65450b..f18d9216ce 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -306,7 +306,7 @@ private function writePlotArea(XMLWriter $objWriter, PlotArea $plotArea, ?Title if ($chartType === DataSeries::TYPE_BUBBLECHART) { $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id1, $id2, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); } else { - $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis); + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis, ($chartType === DataSeries::TYPE_SCATTERCHART) ? 'c:valAx' : 'c:catAx'); } $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); @@ -367,9 +367,9 @@ private function writeDataLabels(XMLWriter $objWriter, ?Layout $chartLayout = nu * @param string $id2 * @param bool $isMultiLevelSeries */ - private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void + private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis, string $element = 'c:catAx'): void { - $objWriter->startElement('c:catAx'); + $objWriter->startElement($element); if ($id1 > 0) { $objWriter->startElement('c:axId'); @@ -1016,7 +1016,7 @@ private function writePlotSeriesValuesElement(XMLWriter $objWriter, $val = 3, $f * @param bool $valIsMultiLevelSeries Is value set a multi-series set * @param string $plotGroupingType Type of grouping for multi-series values */ - private function writePlotGroup(?DataSeries $plotGroup, $groupType, XMLWriter $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void + private function writePlotGroup(?DataSeries $plotGroup, string $groupType, XMLWriter $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void { if ($plotGroup === null) { return; @@ -1104,7 +1104,7 @@ private function writePlotGroup(?DataSeries $plotGroup, $groupType, XMLWriter $o } // Formatting for the points - if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART)) { + if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()))) { $plotLineWidth = 12700; if ($plotSeriesValues) { $plotLineWidth = $plotSeriesValues->getLineWidth(); @@ -1113,7 +1113,7 @@ private function writePlotGroup(?DataSeries $plotGroup, $groupType, XMLWriter $o $objWriter->startElement('c:spPr'); $objWriter->startElement('a:ln'); $objWriter->writeAttribute('w', $plotLineWidth); - if ($groupType == DataSeries::TYPE_STOCKCHART) { + if ($groupType == DataSeries::TYPE_STOCKCHART || $groupType === DataSeries::TYPE_SCATTERCHART) { $objWriter->startElement('a:noFill'); $objWriter->endElement(); } elseif ($plotLabel) { @@ -1142,6 +1142,16 @@ private function writePlotGroup(?DataSeries $plotGroup, $groupType, XMLWriter $o $objWriter->startElement('c:size'); $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointSize()); $objWriter->endElement(); + $fillColor = $plotSeriesValues->getFillColor(); + if (is_string($fillColor) && $fillColor !== '') { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $fillColor); + $objWriter->endElement(); // srgbClr + $objWriter->endElement(); // solidFill + $objWriter->endElement(); // spPr + } } $objWriter->endElement(); @@ -1192,6 +1202,11 @@ private function writePlotGroup(?DataSeries $plotGroup, $groupType, XMLWriter $o $this->writePlotSeriesValues($plotSeriesValues, $objWriter, $groupType, 'num'); $objWriter->endElement(); + if ($groupType === DataSeries::TYPE_SCATTERCHART && $plotGroup->getPlotStyle() === 'smoothMarker') { + $objWriter->startElement('c:smooth'); + $objWriter->writeAttribute('val', '1'); + $objWriter->endElement(); + } } if ($groupType === DataSeries::TYPE_BUBBLECHART) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index a64e0d6872..6808f33eed 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -215,36 +215,48 @@ public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $ foreach ($elements as $element) { // r $objWriter->startElement($prefix . 'r'); + if ($element->getFont() !== null) { + // rPr + $objWriter->startElement($prefix . 'rPr'); + $size = $element->getFont()->getSize(); + if (is_numeric($size)) { + $objWriter->writeAttribute('sz', (string) (int) ($size * 100)); + } - // rPr - $objWriter->startElement($prefix . 'rPr'); - - // Bold - $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); - // Italic - $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); - // Underline - $underlineType = $element->getFont()->getUnderline(); - switch ($underlineType) { - case 'single': - $underlineType = 'sng'; - - break; - case 'double': - $underlineType = 'dbl'; - - break; - } - $objWriter->writeAttribute('u', $underlineType); - // Strikethrough - $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike')); + // Bold + $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); + // Italic + $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); + // Underline + $underlineType = $element->getFont()->getUnderline(); + switch ($underlineType) { + case 'single': + $underlineType = 'sng'; - // rFont - $objWriter->startElement($prefix . 'latin'); - $objWriter->writeAttribute('typeface', $element->getFont()->getName()); - $objWriter->endElement(); + break; + case 'double': + $underlineType = 'dbl'; - $objWriter->endElement(); + break; + } + $objWriter->writeAttribute('u', $underlineType); + // Strikethrough + $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike')); + + // Color + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . 'srgbClr'); + $objWriter->writeAttribute('val', $element->getFont()->getColor()->getRGB()); + $objWriter->endElement(); // srgbClr + $objWriter->endElement(); // solidFill + + // fontName + $objWriter->startElement($prefix . 'latin'); + $objWriter->writeAttribute('typeface', $element->getFont()->getName()); + $objWriter->endElement(); + + $objWriter->endElement(); + } // t $objWriter->startElement($prefix . 't'); diff --git a/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php b/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php index da82153285..60351d71b1 100644 --- a/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php +++ b/tests/PhpSpreadsheetTests/Functional/AbstractFunctional.php @@ -19,10 +19,13 @@ abstract class AbstractFunctional extends TestCase * * @return Spreadsheet */ - protected function writeAndReload(Spreadsheet $spreadsheet, $format, ?callable $readerCustomizer = null) + protected function writeAndReload(Spreadsheet $spreadsheet, $format, ?callable $readerCustomizer = null, ?callable $writerCustomizer = null) { $filename = File::temporaryFilename(); $writer = IOFactory::createWriter($spreadsheet, $format); + if ($writerCustomizer) { + $writerCustomizer($writer); + } $writer->save($filename); $reader = IOFactory::createReader($format); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php new file mode 100644 index 0000000000..dc7fbc0b28 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32ScatterTest.php @@ -0,0 +1,261 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testScatter1(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Scatter - No Join and Markers', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getName()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(7, $values->getPointSize()); + self::assertSame('FFFF00', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter6(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart6.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Scatter - Rich Text Title No Join and Markers', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(3, $elements); + + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getName()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $run = $elements[1]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Courier New', $font->getName()); + self::assertEquals(10, $font->getSize()); + self::assertFalse($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('single', $font->getUnderline()); + self::assertSame('00B0F0', $font->getColor()->getRGB()); + + $run = $elements[2]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getName()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertFalse($values->getScatterLines()); + self::assertSame(28575, $values->getLineWidth()); + self::assertSame(7, $values->getPointSize()); + self::assertSame('FFFF00', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter3(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart3.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Charts', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $title = $chart->getTitle(); + $captionArray = $title->getCaption(); + self::assertIsArray($captionArray); + self::assertCount(1, $captionArray); + $caption = $captionArray[0]; + self::assertInstanceOf(RichText::class, $caption); + self::assertSame('Scatter - Join Straight Lines and Markers', $caption->getPlainText()); + $elements = $caption->getRichTextElements(); + self::assertCount(1, $elements); + $run = $elements[0]; + self::assertInstanceOf(Run::class, $run); + $font = $run->getFont(); + self::assertInstanceOf(Font::class, $font); + self::assertSame('Calibri', $font->getName()); + self::assertEquals(12, $font->getSize()); + self::assertTrue($font->getBold()); + self::assertFalse($font->getItalic()); + self::assertFalse($font->getSuperscript()); + self::assertFalse($font->getSubscript()); + self::assertFalse($font->getStrikethrough()); + self::assertSame('none', $font->getUnderline()); + self::assertSame('000000', $font->getColor()->getRGB()); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertTrue($values->getScatterLines()); + self::assertSame(12700, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[1]; + self::assertTrue($values->getScatterLines()); + self::assertSame(12700, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + $values = $plotValues[2]; + self::assertTrue($values->getScatterLines()); + self::assertSame(12700, $values->getLineWidth()); + self::assertSame(3, $values->getPointSize()); + self::assertSame('', $values->getFillColor()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php new file mode 100644 index 0000000000..75a3946ce7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32XmlTest.php @@ -0,0 +1,89 @@ +outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + /** + * @dataProvider providerScatterCharts + */ + public function testBezierCount(int $expectedCount, string $inputFile): void + { + $file = self::DIRECTORY . $inputFile; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart2.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(1, substr_count($data, '')); + self::assertSame($expectedCount, substr_count($data, '')); + } + } + + public function providerScatterCharts(): array + { + return [ + 'no line' => [0, '32readwriteScatterChart1.xlsx'], + 'smooth line (Bezier)' => [3, '32readwriteScatterChart2.xlsx'], + 'straight line' => [0, '32readwriteScatterChart3.xlsx'], + ]; + } + + public function testAreaPercentageNoCat(): void + { + $file = self::DIRECTORY . '32readwriteAreaPercentageChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart1.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertSame(0, substr_count($data, '')); + } + } +}