diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index c3a74ede..e2e66a4a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -34,7 +34,7 @@ If this is an issue with reading a specific spreadsheet file, then it may be app - [ ] Writer - [ ] Styles - [ ] Data Validations -- [ ] Formula Calulations +- [ ] Formula Calculations - [ ] Charts - [ ] AutoFilter - [ ] Form Elements diff --git a/CHANGELOG.md b/CHANGELOG.md index 732badb1..206892ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,35 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) + +## 1.24.1 - 2022-07-18 + +### Added + +- Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) + +### Changed + - Nothing +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943) +- Additional Chart fixes + - Pie chart with part separated unwantedly [Issue #2506](https://github.com/PHPOffice/PhpSpreadsheet/issues/2506) [PR #2928](https://github.com/PHPOffice/PhpSpreadsheet/pull/2928) + - Chart styling is lost on simple load / save process [Issue #1797](https://github.com/PHPOffice/PhpSpreadsheet/issues/1797) [Issue #2077](https://github.com/PHPOffice/PhpSpreadsheet/issues/2077) [PR #2930](https://github.com/PHPOffice/PhpSpreadsheet/pull/2930) + - Can't create contour chart (surface 2d) [Issue #2931](https://github.com/PHPOffice/PhpSpreadsheet/issues/2931) [PR #2933](https://github.com/PHPOffice/PhpSpreadsheet/pull/2933) +- VLOOKUP Breaks When Array Contains Null Cells [Issue #2934](https://github.com/PHPOffice/PhpSpreadsheet/issues/2934) [PR #2939](https://github.com/PHPOffice/PhpSpreadsheet/pull/2939) + ## 1.24.0 - 2022-07-09 Note that this will be the last 1.x branch release before the 2.x release. We will maintain both branches in parallel for a time; but users are requested to update to version 2.0 once that is fully available. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5ef5522f..024f1fbd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2860,11 +2860,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Comparison operation \"\\<\\=\" between int\\ and 1000 is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Parameter \\#1 \\$worksheet of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getIndex\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet, PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null given\\.$#" count: 1 @@ -2875,21 +2870,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Result of \\|\\| is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet and null will always evaluate to false\\.$#" count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index ef6353bb..eed87119 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -124,6 +124,7 @@ $dataSeriesValues[2]->setScatterLines(false); // points not connected $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); $yAxis = new Axis(); $yAxis->setLineStyleProperties( diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php new file mode 100644 index 00000000..b4fc97b1 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -0,0 +1,192 @@ +getActiveSheet(); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] +); +$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$worksheet->getColumnDimension('A')->setAutoSize(true); +$worksheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 +]; +// Set the X-Axis Labels +// changed from STRING to NUMBER +// added 2 additional x-axis values associated with each of the 3 metrics +// added FORMATE_CODE_NUMBER +$xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), +]; + +// series 1 +// marker details +$dataSeriesValues[0] + ->setPointMarker('diamond') + ->setPointSize(5) + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - smooth line, connected +$dataSeriesValues[0] + ->setScatterLines(true) + ->setSmoothLine(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - straight line - no special effects, connected, straight line +$dataSeriesValues[1] // square fill + ->setPointMarker('square') + ->setPointSize(6) + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square border + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - markers, no line +$dataSeriesValues[2] // triangle fill + //->setPointMarker('triangle') // let Excel choose shape + ->setPointSize(7) + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected + + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +//$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); +$xAxis->setAxisOption('hidden', '1'); + +$yAxis = new Axis(); +$yAxis->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_SIMPLE, + Properties::LINE_STYLE_DASH_DASH_DOT, + Properties::LINE_STYLE_CAP_FLAT, + Properties::LINE_STYLE_JOIN_BEVEL +); +$yAxis->setLineColorProperties('ffc000', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$yAxis->setAxisOption('hidden', '1'); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +$plotArea->setNoFill(true); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Trend Chart'); +//$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null, //$yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); +$chart->setNoFill(true); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('P20'); +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$spreadsheet->disconnectWorksheets(); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/33_Chart_create_scatter4.php b/samples/Chart/33_Chart_create_scatter4.php new file mode 100644 index 00000000..d42933b6 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter4.php @@ -0,0 +1,130 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 +]; +// Set the X-Axis Labels +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), +]; + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); + +$pos1 = 0; // pos = 0% (extreme low side or lower left corner) +$brightness1 = 0; // 0% +$gsColor1 = new ChartColor(); +$gsColor1->setColorProperties('FF0000', 75, 'srgbClr', $brightness1); // red +$gradientStop1 = [$pos1, $gsColor1]; + +$pos2 = 0.5; // pos = 50% (middle) +$brightness2 = 0.5; // 50% +$gsColor2 = new ChartColor(); +$gsColor2->setColorProperties('FFFF00', 50, 'srgbClr', $brightness2); // yellow +$gradientStop2 = [$pos2, $gsColor2]; + +$pos3 = 1.0; // pos = 100% (extreme high side or upper right corner) +$brightness3 = 0.5; // 50% +$gsColor3 = new ChartColor(); +$gsColor3->setColorProperties('00B050', 50, 'srgbClr', $brightness3); // green +$gradientStop3 = [$pos3, $gsColor3]; + +$gradientFillStops = [ + $gradientStop1, + $gradientStop2, + $gradientStop3, +]; +$gradientFillAngle = 315.0; // 45deg above horiz + +$plotArea->setGradientFillProperties($gradientFillStops, $gradientFillAngle); + +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Chart'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('H20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteScatterChart10.xlsx b/samples/templates/32readwriteScatterChart10.xlsx new file mode 100644 index 00000000..7d5ac0d2 Binary files /dev/null and b/samples/templates/32readwriteScatterChart10.xlsx differ diff --git a/samples/templates/32readwriteScatterChart8.xlsx b/samples/templates/32readwriteScatterChart8.xlsx index fdd85b0e..1d4080d7 100644 Binary files a/samples/templates/32readwriteScatterChart8.xlsx and b/samples/templates/32readwriteScatterChart8.xlsx differ diff --git a/samples/templates/32readwriteScatterChart9.xlsx b/samples/templates/32readwriteScatterChart9.xlsx new file mode 100644 index 00000000..3a0b51b2 Binary files /dev/null and b/samples/templates/32readwriteScatterChart9.xlsx differ diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index ddd3e200..b3d6998c 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -573,24 +573,20 @@ class Functions return (array) $array; } - $arrayValues = []; - foreach ($array as $value) { + $flattened = []; + $stack = array_values($array); + + while ($stack) { + $value = array_shift($stack); + if (is_array($value)) { - foreach ($value as $val) { - if (is_array($val)) { - foreach ($val as $v) { - $arrayValues[] = $v; - } - } else { - $arrayValues[] = $val; - } - } + array_unshift($stack, ...array_values($value)); } else { - $arrayValues[] = $value; + $flattened[] = $value; } } - return $arrayValues; + return $flattened; } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index dda2c705..4b9f818f 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -47,7 +47,7 @@ class ErrorValue return false; } - return in_array($value, ExcelError::$errorCodes, true); + return in_array($value, ExcelError::ERROR_CODES, true); } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 5ca74a3e..06f38663 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -13,7 +13,7 @@ class ExcelError * * @var array */ - public static $errorCodes = [ + public const ERROR_CODES = [ 'null' => '#NULL!', // 1 'divisionbyzero' => '#DIV/0!', // 2 'value' => '#VALUE!', // 3 @@ -30,12 +30,23 @@ class ExcelError 'calculation' => '#CALC!', //14 ]; + /** + * List of error codes. Replaced by constant; + * previously it was public and updateable, allowing + * user to make inappropriate alterations. + * + * @deprecated 1.25.0 Use ERROR_CODES constant instead. + * + * @var array + */ + public static $errorCodes = self::ERROR_CODES; + /** * @param mixed $value */ public static function throwError($value): string { - return in_array($value, self::$errorCodes, true) ? $value : self::$errorCodes['value']; + return in_array($value, self::ERROR_CODES, true) ? $value : self::ERROR_CODES['value']; } /** @@ -52,7 +63,7 @@ class ExcelError } $i = 1; - foreach (self::$errorCodes as $errorCode) { + foreach (self::ERROR_CODES as $errorCode) { if ($value === $errorCode) { return $i; } @@ -71,7 +82,7 @@ class ExcelError */ public static function null(): string { - return self::$errorCodes['null']; + return self::ERROR_CODES['null']; } /** @@ -83,7 +94,7 @@ class ExcelError */ public static function NAN(): string { - return self::$errorCodes['num']; + return self::ERROR_CODES['num']; } /** @@ -95,7 +106,7 @@ class ExcelError */ public static function REF(): string { - return self::$errorCodes['reference']; + return self::ERROR_CODES['reference']; } /** @@ -111,7 +122,7 @@ class ExcelError */ public static function NA(): string { - return self::$errorCodes['na']; + return self::ERROR_CODES['na']; } /** @@ -123,7 +134,7 @@ class ExcelError */ public static function VALUE(): string { - return self::$errorCodes['value']; + return self::ERROR_CODES['value']; } /** @@ -135,7 +146,7 @@ class ExcelError */ public static function NAME(): string { - return self::$errorCodes['name']; + return self::ERROR_CODES['name']; } /** @@ -145,7 +156,7 @@ class ExcelError */ public static function DIV0(): string { - return self::$errorCodes['divisionbyzero']; + return self::ERROR_CODES['divisionbyzero']; } /** @@ -155,6 +166,6 @@ class ExcelError */ public static function CALC(): string { - return self::$errorCodes['calculation']; + return self::ERROR_CODES['calculation']; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index d67718ce..e2d27bde 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -66,7 +66,7 @@ class HLookup extends LookupBase */ private static function hLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray[$column] as $rowKey => $rowData) { diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php index 8e451fe4..a001540c 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -19,8 +19,16 @@ abstract class LookupBase protected static function validateIndexLookup(array $lookup_array, $index_number): int { - // index_number must be a number greater than or equal to 1 - if (!is_numeric($index_number) || $index_number < 1) { + // index_number must be a number greater than or equal to 1. + // Excel results are inconsistent when index is non-numeric. + // VLOOKUP(whatever, whatever, SQRT(-1)) yields NUM error, but + // VLOOKUP(whatever, whatever, cellref) yields REF error + // when cellref is '=SQRT(-1)'. So just try our best here. + // Similar results if string (literal yields VALUE, cellRef REF). + if (!is_numeric($index_number)) { + throw new Exception(ExcelError::throwError($index_number)); + } + if ($index_number < 1) { throw new Exception(ExcelError::VALUE()); } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index 53a7badc..edeb1aa8 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -68,8 +68,8 @@ class VLookup extends LookupBase { reset($a); $firstColumn = key($a); - $aLower = StringHelper::strToLower($a[$firstColumn]); - $bLower = StringHelper::strToLower($b[$firstColumn]); + $aLower = StringHelper::strToLower((string) $a[$firstColumn]); + $bLower = StringHelper::strToLower((string) $b[$firstColumn]); if ($aLower == $bLower) { return 0; @@ -84,7 +84,7 @@ class VLookup extends LookupBase */ private static function vLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray as $rowKey => $rowData) { diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 1f55cf03..bd7082ff 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -60,6 +60,8 @@ class Axis extends Properties 'axis_labels' => self::AXIS_LABELS_NEXT_TO, 'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO, 'horizontal_crosses_value' => null, + 'textRotation' => null, + 'hidden' => null, ]; /** @@ -136,7 +138,9 @@ class Axis extends Properties ?string $minimum = null, ?string $maximum = null, ?string $majorUnit = null, - ?string $minorUnit = null + ?string $minorUnit = null, + ?string $textRotation = null, + ?string $hidden = null ): void { $this->axisOptions['axis_labels'] = $axisLabels; $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); @@ -144,11 +148,12 @@ class Axis extends Properties $this->setAxisOption('orientation', $axisOrientation); $this->setAxisOption('major_tick_mark', $majorTmt); $this->setAxisOption('minor_tick_mark', $minorTmt); - $this->setAxisOption('minor_tick_mark', $minorTmt); $this->setAxisOption('minimum', $minimum); $this->setAxisOption('maximum', $maximum); $this->setAxisOption('major_unit', $majorUnit); $this->setAxisOption('minor_unit', $minorUnit); + $this->setAxisOption('textRotation', $textRotation); + $this->setAxisOption('hidden', $hidden); } /** diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 3f89e6fb..e850f502 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -144,6 +144,9 @@ class Chart /** @var bool */ private $autoTitleDeleted = false; + /** @var bool */ + private $noFill = false; + /** * Create a new Chart. * majorGridlines and minorGridlines are deprecated, moved to Axis. @@ -747,4 +750,16 @@ class Chart return $this; } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/ChartColor.php b/src/PhpSpreadsheet/Chart/ChartColor.php index 7f87e391..87f31020 100644 --- a/src/PhpSpreadsheet/Chart/ChartColor.php +++ b/src/PhpSpreadsheet/Chart/ChartColor.php @@ -24,15 +24,18 @@ class ChartColor /** @var ?int */ private $alpha; + /** @var ?int */ + private $brightness; + /** * @param string|string[] $value */ - public function __construct($value = '', ?int $alpha = null, ?string $type = null) + public function __construct($value = '', ?int $alpha = null, ?string $type = null, ?int $brightness = null) { if (is_array($value)) { $this->setColorPropertiesArray($value); } else { - $this->setColorProperties($value, $alpha, $type); + $this->setColorProperties($value, $alpha, $type, $brightness); } } @@ -72,10 +75,23 @@ class ChartColor return $this; } + public function getBrightness(): ?int + { + return $this->brightness; + } + + public function setBrightness(?int $brightness): self + { + $this->brightness = $brightness; + + return $this; + } + /** * @param null|float|int|string $alpha + * @param null|float|int|string $brightness */ - public function setColorProperties(?string $color, $alpha = null, ?string $type = null): self + public function setColorProperties(?string $color, $alpha = null, ?string $type = null, $brightness = null): self { if (empty($type) && !empty($color)) { if (substr($color, 0, 1) === '*') { @@ -99,6 +115,11 @@ class ChartColor } elseif (is_numeric($alpha)) { $this->setAlpha((int) $alpha); } + if ($brightness === null) { + $this->setBrightness(null); + } elseif (is_numeric($brightness)) { + $this->setBrightness((int) $brightness); + } return $this; } @@ -108,7 +129,8 @@ class ChartColor return $this->setColorProperties( $color['value'] ?? '', $color['alpha'] ?? null, - $color['type'] ?? null + $color['type'] ?? null, + $color['brightness'] ?? null ); } @@ -133,6 +155,8 @@ class ChartColor $retVal = $this->type; } elseif ($propertyName === 'alpha') { $retVal = $this->alpha; + } elseif ($propertyName === 'brightness') { + $retVal = $this->brightness; } return $retVal; diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index 548145e7..5d33e96d 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -94,7 +94,7 @@ class DataSeries private $plotCategory = []; /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index bc0e04d1..cb5fa742 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -536,7 +536,7 @@ class DataSeriesValues extends Properties } /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ diff --git a/src/PhpSpreadsheet/Chart/PlotArea.php b/src/PhpSpreadsheet/Chart/PlotArea.php index 4bd49ece..ccde4bb2 100644 --- a/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/src/PhpSpreadsheet/Chart/PlotArea.php @@ -6,6 +6,30 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PlotArea { + /** + * No fill in plot area (show Excel gridlines through chart). + * + * @var bool + */ + private $noFill = false; + + /** + * PlotArea Gradient Stop list. + * Each entry is a 2-element array. + * First is position in %. + * Second is ChartColor. + * + * @var array[] + */ + private $gradientFillStops = []; + + /** + * PlotArea Gradient Angle. + * + * @var ?float + */ + private $gradientFillAngle; + /** * PlotArea Layout. * @@ -101,4 +125,42 @@ class PlotArea $plotSeries->refresh($worksheet); } } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setGradientFillProperties(array $gradientFillStops, ?float $gradientFillAngle): self + { + $this->gradientFillStops = $gradientFillStops; + $this->gradientFillAngle = $gradientFillAngle; + + return $this; + } + + /** + * Get gradientFillAngle. + */ + public function getGradientFillAngle(): ?float + { + return $this->gradientFillAngle; + } + + /** + * Get gradientFillStops. + * + * @return array + */ + public function getGradientFillStops() + { + return $this->gradientFillStops; + } } diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 3d859e15..76f128e0 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -201,7 +201,7 @@ class Html extends BaseReader /** * Loads Spreadsheet from file. */ - protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + public function loadSpreadsheetFromFile(string $filename): Spreadsheet { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); @@ -651,7 +651,13 @@ class Html extends BaseReader // Reload the HTML file into the DOM object try { $convert = $this->securityScanner->scanFile($filename); - $loaded = $dom->loadHTML($convert); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } @@ -662,6 +668,11 @@ class Html extends BaseReader return $this->loadDocument($dom, $spreadsheet); } + private static function replaceNonAscii(array $matches): string + { + return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; + } + /** * Spreadsheet from content. * @@ -674,7 +685,13 @@ class Html extends BaseReader // Reload the HTML file into the DOM object try { $convert = $this->securityScanner->scan($content); - $loaded = $dom->loadHTML($convert); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index eb425646..12ee0ade 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -73,8 +73,18 @@ class Chart $xAxis = new Axis(); $yAxis = new Axis(); $autoTitleDeleted = null; + $chartNoFill = false; + $gradientArray = []; + $gradientLin = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { + case 'spPr': + $possibleNoFill = $chartElementsC->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $chartNoFill = true; + } + + break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { $chartDetailsC = $chartDetails->children($this->cNamespace); @@ -95,8 +105,29 @@ class Chart $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; $catAxRead = false; + $plotNoFill = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { switch ($chartDetailKey) { + case 'spPr': + $possibleNoFill = $chartDetails->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $plotNoFill = true; + } + if (isset($possibleNoFill->gradFill->gsLst)) { + foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { + /** @var float */ + $pos = self::getAttribute($gradient, 'pos', 'float'); + $gradientArray[] = [ + $pos / Properties::PERCENTAGE_MULTIPLIER, + new ChartColor($this->readColor($gradient)), + ]; + } + } + if (isset($possibleNoFill->gradFill->lin)) { + $gradientLin = Properties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); + } + + break; case 'layout': $plotAreaLayout = $this->chartLayoutDetails($chartDetail); @@ -288,6 +319,12 @@ class Chart } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); $this->setChartAttributes($plotAreaLayout, $plotAttributes); + if ($plotNoFill) { + $plotArea->setNoFill(true); + } + if (!empty($gradientArray)) { + $plotArea->setGradientFillProperties($gradientArray, $gradientLin); + } break; case 'plotVisOnly': @@ -330,6 +367,9 @@ class Chart } } $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); + if ($chartNoFill) { + $chart->setNoFill(true); + } if (is_bool($autoTitleDeleted)) { $chart->setAutoTitleDeleted($autoTitleDeleted); } @@ -1147,6 +1187,7 @@ class Chart 'type' => null, 'value' => null, 'alpha' => null, + 'brightness' => null, ]; foreach (ChartColor::EXCEL_COLOR_TYPES as $type) { if (isset($colorXml->$type)) { @@ -1159,6 +1200,13 @@ class Chart $result['alpha'] = ChartColor::alphaFromXml($alpha); } } + if (isset($colorXml->$type->lumMod)) { + /** @var string */ + $brightness = self::getAttribute($colorXml->$type->lumMod, 'val', 'string'); + if (is_numeric($brightness)) { + $result['brightness'] = ChartColor::alphaFromXml($brightness); + } + } break; } @@ -1236,6 +1284,9 @@ class Chart if (!isset($whichAxis)) { return; } + if (isset($chartDetail->delete)) { + $whichAxis->setAxisOption('hidden', (string) self::getAttribute($chartDetail->delete, 'val', 'string')); + } if (isset($chartDetail->numFmt)) { $whichAxis->setAxisNumberProperties( (string) self::getAttribute($chartDetail->numFmt, 'formatCode', 'string'), @@ -1279,5 +1330,15 @@ class Chart if (isset($chartDetail->minorUnit)) { $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string')); } + if (isset($chartDetail->txPr)) { + $children = $chartDetail->txPr->children($this->aNamespace); + if (isset($children->bodyPr)) { + /** @var string */ + $textRotation = self::getAttribute($children->bodyPr, 'rot', 'string'); + if (is_numeric($textRotation)) { + $whichAxis->setAxisOption('textRotation', (string) Properties::xmlToAngle($textRotation)); + } + } + } } } diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 33b4fe0c..4bb93987 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -3,10 +3,13 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; +use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; class Spreadsheet { @@ -1120,28 +1123,24 @@ class Spreadsheet */ public function copy() { - $copied = clone $this; + $filename = File::temporaryFilename(); + $writer = new XlsxWriter($this); + $writer->setIncludeCharts(true); + $writer->save($filename); - $worksheetCount = count($this->workSheetCollection); - for ($i = 0; $i < $worksheetCount; ++$i) { - $this->workSheetCollection[$i] = $this->workSheetCollection[$i]->copy(); - $this->workSheetCollection[$i]->rebindParent($this); - } + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $reloadedSpreadsheet = $reader->load($filename); + unlink($filename); - return $copied; + return $reloadedSpreadsheet; } - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ public function __clone() { - // @phpstan-ignore-next-line - foreach ($this as $key => $val) { - if (is_object($val) || (is_array($val))) { - $this->{$key} = unserialize(serialize($val)); - } - } + throw new Exception( + 'Do not use clone on spreadsheet. Use spreadsheet->copy() instead.' + ); } /** @@ -1562,7 +1561,7 @@ class Spreadsheet * Workbook window is hidden and cannot be shown in the * user interface. * - * @param string $visibility visibility status of the workbook + * @param null|string $visibility visibility status of the workbook */ public function setVisibility($visibility): void { @@ -1596,7 +1595,7 @@ class Spreadsheet */ public function setTabRatio($tabRatio): void { - if ($tabRatio >= 0 || $tabRatio <= 1000) { + if ($tabRatio >= 0 && $tabRatio <= 1000) { $this->tabRatio = (int) $tabRatio; } else { throw new Exception('Tab ratio must be between 0 and 1000.'); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 08935721..278b64e7 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -72,30 +72,22 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:view3D'); - $rotX = $chart->getRotX(); - if (is_int($rotX)) { - $objWriter->startElement('c:rotX'); - $objWriter->writeAttribute('val', "$rotX"); - $objWriter->endElement(); - } - $rotY = $chart->getRotY(); - if (is_int($rotY)) { - $objWriter->startElement('c:rotY'); - $objWriter->writeAttribute('val', "$rotY"); - $objWriter->endElement(); - } - $rAngAx = $chart->getRAngAx(); - if (is_int($rAngAx)) { - $objWriter->startElement('c:rAngAx'); - $objWriter->writeAttribute('val', "$rAngAx"); - $objWriter->endElement(); - } - $perspective = $chart->getPerspective(); - if (is_int($perspective)) { - $objWriter->startElement('c:perspective'); - $objWriter->writeAttribute('val', "$perspective"); - $objWriter->endElement(); + $surface2D = false; + $plotArea = $chart->getPlotArea(); + if ($plotArea !== null) { + $seriesArray = $plotArea->getPlotGroup(); + foreach ($seriesArray as $series) { + if ($series->getPlotType() === DataSeries::TYPE_SURFACECHART) { + $surface2D = true; + + break; + } + } } + $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 $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY()); @@ -114,16 +106,34 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:chart + if ($chart->getNoFill()) { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + $objWriter->endElement(); // c:spPr + } $this->writePrintSettings($objWriter); - $objWriter->endElement(); + $objWriter->endElement(); // c:chartSpace // Return return $objWriter->getData(); } + private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void + { + if ($value === null && $surface2D) { + $value = $default; + } + if ($value !== null) { + $objWriter->startElement($tag); + $objWriter->writeAttribute('val', "$value"); + $objWriter->endElement(); + } + } + /** * Write Chart Title. */ @@ -356,8 +366,35 @@ class Chart extends WriterPart $this->writeSerAxis($objWriter, $id2, $id3); } } + $stops = $plotArea->getGradientFillStops(); + if ($plotArea->getNoFill() || !empty($stops)) { + $objWriter->startElement('c:spPr'); + if ($plotArea->getNoFill()) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + } + if (!empty($stops)) { + $objWriter->startElement('a:gradFill'); + $objWriter->startElement('a:gsLst'); + foreach ($stops as $stop) { + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', (string) (Properties::PERCENTAGE_MULTIPLIER * (float) $stop[0])); + $this->writeColor($objWriter, $stop[1], false); + $objWriter->endElement(); // a:gs + } + $objWriter->endElement(); // a:gsLst + $angle = $plotArea->getGradientFillAngle(); + if ($angle !== null) { + $objWriter->startElement('a:lin'); + $objWriter->writeAttribute('ang', Properties::angleToXml($angle)); + $objWriter->endElement(); // a:lin + } + $objWriter->endElement(); // a:gradFill + } + $objWriter->endElement(); // c:spPr + } - $objWriter->endElement(); + $objWriter->endElement(); // c:plotArea } private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void @@ -488,7 +525,7 @@ class Chart extends WriterPart $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); @@ -569,6 +606,23 @@ class Chart extends WriterPart $objWriter->endElement(); } + $textRotation = $yAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } + $objWriter->startElement('c:spPr'); $this->writeColor($objWriter, $yAxis->getFillColorObject()); $this->writeEffects($objWriter, $yAxis); @@ -661,7 +715,7 @@ class Chart extends WriterPart $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); @@ -744,6 +798,23 @@ class Chart extends WriterPart $objWriter->endElement(); } + $textRotation = $xAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } + $objWriter->startElement('c:spPr'); $this->writeColor($objWriter, $xAxis->getFillColorObject()); $this->writeLineStyles($objWriter, $xAxis); @@ -913,8 +984,8 @@ class Chart extends WriterPart $objWriter->endElement(); } - if ($plotGroup->getPlotGrouping() !== null) { - $plotGroupingType = $plotGroup->getPlotGrouping(); + $plotGroupingType = $plotGroup->getPlotGrouping(); + if ($plotGroupingType !== null && $groupType !== DataSeries::TYPE_SURFACECHART && $groupType !== DataSeries::TYPE_SURFACECHART_3D) { $objWriter->startElement('c:grouping'); $objWriter->writeAttribute('val', $plotGroupingType); $objWriter->endElement(); @@ -1574,7 +1645,18 @@ class Chart extends WriterPart if (is_numeric($alpha)) { $objWriter->startElement('a:alpha'); $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); - $objWriter->endElement(); + $objWriter->endElement(); // a:alpha + } + $brightness = $chartColor->getBrightness(); + if (is_numeric($brightness)) { + $brightness = (int) $brightness; + $lumOff = 100 - $brightness; + $objWriter->startElement('a:lumMod'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($brightness)); + $objWriter->endElement(); // a:lumMod + $objWriter->startElement('a:lumOff'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($lumOff)); + $objWriter->endElement(); // a:lumOff } $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr if ($solidFill) { diff --git a/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php b/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php new file mode 100644 index 00000000..60c336cf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php @@ -0,0 +1,34 @@ + [ + 0 => [ + 32 => [ + 'B' => 'PHP', + ], + ], + ], + 1 => [ + 0 => [ + 32 => [ + 'C' => 'Spreadsheet', + ], + ], + ], + ]; + + $values = Functions::flattenArray($array); + + self::assertIsNotArray($values[0]); + self::assertIsNotArray($values[1]); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php index 4e05ea8a..5b7647be 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php @@ -3,26 +3,42 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class VLookupTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerVLOOKUP * * @param mixed $expectedResult + * @param mixed $value + * @param mixed $table + * @param mixed $index */ - public function testVLOOKUP($expectedResult, ...$args): void + public function testVLOOKUP($expectedResult, $value, $table, $index, ?bool $lookup = null): void { - $result = LookupRef::VLOOKUP(...$args); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + if (is_array($table)) { + $sheet->fromArray($table); + $dimension = $sheet->calculateWorksheetDimension(); + } else { + $sheet->getCell('A1')->setValue($table); + $dimension = 'A1'; + } + if ($lookup === null) { + $lastarg = ''; + } else { + $lastarg = $lookup ? ',TRUE' : ',FALSE'; + } + $sheet->getCell('Z98')->setValue($value); + $sheet->getCell('Z97')->setValue($index); + + $sheet->getCell('Z99')->setValue("=VLOOKUP(Z98,$dimension,Z97$lastarg)"); + $result = $sheet->getCell('Z99')->getCalculatedValue(); self::assertEquals($expectedResult, $result); + $spreadsheet->disconnectWorksheets(); } public function providerVLOOKUP(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php index 6c9a871d..31fb94fa 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData; +use PhpOffice\PhpSpreadsheet\Spreadsheet; + class ConcatenateTest extends AllSetupTeardown { /** @@ -30,4 +32,27 @@ class ConcatenateTest extends AllSetupTeardown { return require 'tests/data/Calculation/TextData/CONCATENATE.php'; } + + public function testConcatenateWithIndexMatch(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet1->setTitle('Formula'); + $sheet1->fromArray( + [ + ['Number', 'Formula'], + ['52101293', '=CONCAT(INDEX(Lookup!$B$2, MATCH($A2, Lookup!$A$2, 0)))'], + ] + ); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Lookup'); + $sheet2->fromArray( + [ + ['Lookup', 'Match'], + ['52101293', 'PHP'], + ] + ); + self::assertSame('PHP', $sheet1->getCell('B2')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php index b655afc4..fcde7ae3 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -391,6 +392,9 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); + $xAxis = $chart->getChartAxisX(); + self::assertEquals(45, $xAxis->getAxisOptionsProperty('textRotation')); + $plotArea = $chart->getPlotArea(); self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); @@ -444,4 +448,88 @@ class Charts32ScatterTest extends AbstractFunctional $reloadedSpreadsheet->disconnectWorksheets(); } + + public function testScatter9(): void + { + // gradient testing + $file = self::DIRECTORY . '32readwriteScatterChart9.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('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertFalse($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertFalse($plotArea->getNoFill()); + self::assertEquals(315.0, $plotArea->getGradientFillAngle()); + $stops = $plotArea->getGradientFillStops(); + self::assertCount(3, $stops); + self::assertEquals(0.43808, $stops[0][0]); + self::assertEquals(0, $stops[1][0]); + self::assertEquals(0.91, $stops[2][0]); + $color = $stops[0][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('CDDBEC', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(20, $color->getBrightness()); + $color = $stops[1][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('FFC000', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertNull($color->getBrightness()); + $color = $stops[2][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('00B050', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(4, $color->getBrightness()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter10(): void + { + // nofill for Chart and PlotArea, hidden Axis + $file = self::DIRECTORY . '32readwriteScatterChart10.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('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertTrue($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertTrue($plotArea->getNoFill()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php new file mode 100644 index 00000000..960eb04e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php @@ -0,0 +1,111 @@ +getActiveSheet(); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['5-6']), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['6-7']), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['7-8']), + ]; + + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [1, 2, 3, 4, 5, 6, 7, 8, 9]), + ]; + + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 6, 6, 6, 5.9, 6, 6]), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 6.5, 7, 7, 7, 7, 7]), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 7, 8, 8, 8, 8, 7.9]), + ]; + + $series = new DataSeries( + DataSeries::TYPE_SURFACECHART, + DataSeries::GROUPING_STANDARD, // grouping should not be written for surface chart + range(0, count($dataSeriesValues) - 1), + $dataSeriesLabels, + $xAxisTickValues, + $dataSeriesValues, + null, // plotDirection + false, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle + ); + + $plotArea = new PlotArea(null, [$series]); + $legend = new ChartLegend(ChartLegend::POSITION_BOTTOM, null, false); + + $title = new Title('График распредления температур в пределах кр'); + + $chart = new Chart( + 'chart2', + $title, + $legend, + $plotArea, + true, + DataSeries::EMPTY_AS_GAP, + ); + + $chart->setTopLeftPosition('$A$1'); + $chart->setBottomRightPosition('$P$20'); + + $sheet->addChart($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + + // rotX etc. should be generated for surfaceChart 2D + // even when unspecified. + $expectedXml2D = [ + '', + ]; + $expectedXml3D = [ + '', + ]; + $expectedXmlNoX = [ + 'c:grouping', + ]; + + // confirm that file contains expected tags + foreach ($expectedXml2D as $expected) { + self::assertSame(1, substr_count($data, $expected), $expected); + } + foreach ($expectedXmlNoX as $expected) { + self::assertSame(0, substr_count($data, $expected), $expected); + } + + $series->setPlotType(DataSeries::TYPE_SURFACECHART_3D); + $plotArea = new PlotArea(null, [$series]); + $chart->setPlotArea($plotArea); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + // confirm that file contains expected tags + foreach ($expectedXml3D as $expected) { + self::assertSame(1, substr_count($data, $expected), $expected); + } + foreach ($expectedXmlNoX as $expected) { + self::assertSame(0, substr_count($data, $expected), $expected); + } + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index 43eddc8a..82950880 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -85,6 +85,18 @@ class DefinedNameTest extends TestCase self::assertCount(1, $this->spreadsheet->getDefinedNames()); } + public function testRemoveGlobalDefinedName(): void + { + $this->spreadsheet->addDefinedName( + DefinedName::createInstance('Any', $this->spreadsheet->getActiveSheet(), '=A1') + ); + self::assertCount(1, $this->spreadsheet->getDefinedNames()); + + $this->spreadsheet->removeDefinedName('Any'); + self::assertCount(0, $this->spreadsheet->getDefinedNames()); + $this->spreadsheet->removeDefinedName('Other'); + } + public function testRemoveGlobalDefinedNameWhenDuplicateNames(): void { $this->spreadsheet->addDefinedName( diff --git a/tests/PhpSpreadsheetTests/NamedFormulaTest.php b/tests/PhpSpreadsheetTests/NamedFormulaTest.php index 4c4a6b11..02e9d818 100644 --- a/tests/PhpSpreadsheetTests/NamedFormulaTest.php +++ b/tests/PhpSpreadsheetTests/NamedFormulaTest.php @@ -133,4 +133,10 @@ class NamedFormulaTest extends TestCase $formula->getValue() ); } + + public function testRemoveNonExistentNamedFormula(): void + { + self::assertCount(0, $this->spreadsheet->getNamedFormulae()); + $this->spreadsheet->removeNamedFormula('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/NamedRangeTest.php b/tests/PhpSpreadsheetTests/NamedRangeTest.php index c72b7b73..402e7eba 100644 --- a/tests/PhpSpreadsheetTests/NamedRangeTest.php +++ b/tests/PhpSpreadsheetTests/NamedRangeTest.php @@ -133,4 +133,10 @@ class NamedRangeTest extends TestCase $range->getValue() ); } + + public function testRemoveNonExistentNamedRange(): void + { + self::assertCount(0, $this->spreadsheet->getNamedRanges()); + $this->spreadsheet->removeNamedRange('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php index cf4157e3..fe0117ca 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php @@ -13,7 +13,7 @@ class HtmlImageTest extends TestCase $html = ' - +
test imagetest image voilà
'; $filename = HtmlHelper::createHtml($html); @@ -24,7 +24,7 @@ class HtmlImageTest extends TestCase $drawing = $firstSheet->getDrawingCollection()[0]; self::assertEquals($imagePath, $drawing->getPath()); self::assertEquals('A1', $drawing->getCoordinates()); - self::assertEquals('test image', $drawing->getName()); + self::assertEquals('test image voilà', $drawing->getName()); self::assertEquals('100', $drawing->getWidth()); self::assertEquals('100', $drawing->getHeight()); } diff --git a/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php b/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php new file mode 100644 index 00000000..3a41805c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php @@ -0,0 +1,36 @@ +éàâèî'; + $reader = new Html(); + $spreadsheet = $reader->loadFromString($content); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('éàâèî', $sheet->getCell('A1')->getValue()); + } + + public function testLoadFromFile(): void + { + $file = 'tests/data/Reader/HTML/utf8chars.html'; + $reader = new Html(); + $spreadsheet = $reader->loadSpreadsheetFromFile($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Test Utf-8 characters voilà', $sheet->getTitle()); + self::assertSame('éàâèî', $sheet->getCell('A1')->getValue()); + self::assertSame('αβγδε', $sheet->getCell('B1')->getValue()); + self::assertSame('𐐁𐐂𐐃 & だけち', $sheet->getCell('A2')->getValue()); + self::assertSame('אבגדה', $sheet->getCell('B2')->getValue()); + self::assertSame('𪔀𪔁𪔂', $sheet->getCell('C2')->getValue()); + self::assertSame('᠐᠑᠒', $sheet->getCell('A3')->getValue()); + self::assertSame('അആ', $sheet->getCell('B3')->getValue()); + self::assertSame('กขฃ', $sheet->getCell('C3')->getValue()); + self::assertSame('✀✐✠', $sheet->getCell('D3')->getValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php index 4484b9e0..7a5829f7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php @@ -24,5 +24,7 @@ class Issue2301Test extends \PHPUnit\Framework\TestCase self::assertSame('Arial CE', $font->getName()); self::assertSame(9.0, $font->getSize()); self::assertSame('protected', $sheet->getCell('BT10')->getStyle()->getProtection()->getHidden()); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php index 197ad47f..ab304e7b 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php @@ -44,4 +44,34 @@ class RibbonTest extends AbstractFunctional self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); $reloadedSpreadsheet->disconnectWorksheets(); } + + /** + * Same as above but discard macros. + */ + public function testDiscardMacros(): void + { + $filename = 'tests/data/Reader/XLSX/ribbon.donotopen.zip'; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + self::assertTrue($spreadsheet->hasRibbon()); + $target = $spreadsheet->getRibbonXMLData('target'); + self::assertSame('customUI/customUI.xml', $target); + $data = $spreadsheet->getRibbonXMLData('data'); + self::assertIsString($data); + self::assertSame(1522, strlen($data)); + $vbaCode = (string) $spreadsheet->getMacrosCode(); + self::assertSame(13312, strlen($vbaCode)); + $spreadsheet->discardMacros(); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + self::assertTrue($reloadedSpreadsheet->hasRibbon()); + $ribbonData = $reloadedSpreadsheet->getRibbonXmlData(); + self::assertIsArray($ribbonData); + self::assertSame($target, $ribbonData['target'] ?? ''); + self::assertSame($data, $ribbonData['data'] ?? ''); + self::assertNull($reloadedSpreadsheet->getMacrosCode()); + self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); + $reloadedSpreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php new file mode 100644 index 00000000..584c53fe --- /dev/null +++ b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php @@ -0,0 +1,209 @@ +getProperties(); + $properties->setCreator('Anyone'); + $properties->setTitle('Description'); + $spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($properties, $spreadsheet2->getProperties()); + $properties2 = clone $properties; + $spreadsheet2->setProperties($properties2); + self::assertEquals($properties, $spreadsheet2->getProperties()); + $spreadsheet->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } + + public function testDocumentSecurity(): void + { + $spreadsheet = new Spreadsheet(); + $security = $spreadsheet->getSecurity(); + $security->setLockRevision(true); + $revisionsPassword = 'revpasswd'; + $security->setRevisionsPassword($revisionsPassword); + $spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($security, $spreadsheet2->getSecurity()); + $security2 = clone $security; + $spreadsheet2->setSecurity($security2); + self::assertEquals($security, $spreadsheet2->getSecurity()); + $spreadsheet->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } + + public function testCellXfCollection(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $collection = $spreadsheet->getCellXfCollection(); + self::assertCount(4, $collection); + $font1Style = $collection[1]; + self::assertTrue($spreadsheet->cellXfExists($font1Style)); + self::assertSame('font1', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + + $spreadsheet->removeCellXfByIndex(1); + self::assertFalse($spreadsheet->cellXfExists($font1Style)); + self::assertSame('font2', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveCellXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellXf index is out of bounds.'); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $spreadsheet->removeCellXfByIndex(5); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveDefaultStyle(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('No default style found for this workbook'); + // Removing default style probably should be disallowed. + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet->removeCellXfByIndex(0); + $style = $spreadsheet->getDefaultStyle(); + $spreadsheet->disconnectWorksheets(); + } + + public function testCellStyleXF(): void + { + $spreadsheet = new Spreadsheet(); + $collection = $spreadsheet->getCellStyleXfCollection(); + self::assertCount(1, $collection); + $styleXf = $collection[0]; + self::assertSame($styleXf, $spreadsheet->getCellStyleXfByIndex(0)); + $hash = $styleXf->getHashCode(); + self::assertSame($styleXf, $spreadsheet->getCellStyleXfByHashCode($hash)); + self::assertFalse($spreadsheet->getCellStyleXfByHashCode($hash . 'x')); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveCellStyleXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellStyleXf index is out of bounds.'); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet->removeCellStyleXfByIndex(5); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidFirstSheetIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('First sheet index must be a positive integer.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setFirstSheetIndex(-1); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidVisibility(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Invalid visibility value.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN); + self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $spreadsheet->getVisibility()); + $spreadsheet->setVisibility(null); + self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $spreadsheet->getVisibility()); + $spreadsheet->setVisibility('badvalue'); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidTabRatio(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Tab ratio must be between 0 and 1000.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setTabRatio(2000); + $spreadsheet->disconnectWorksheets(); + } + + public function testCopy(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $sheet->getCell('A1')->setValue('this is a1'); + $sheet->getCell('A2')->setValue('this is a2'); + $sheet->getCell('A3')->setValue('this is a3'); + $sheet->getCell('B1')->setValue('this is b1'); + $sheet->getCell('B2')->setValue('this is b2'); + $copied = $spreadsheet->copy(); + $copysheet = $copied->getActiveSheet(); + $copysheet->getStyle('A2')->getFont()->setName('font12'); + $copysheet->getCell('A2')->setValue('this was a2'); + + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $sheet->getCell('A1')->getValue()); + self::assertSame('this is a2', $sheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $sheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $sheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $sheet->getCell('B2')->getValue()); + + self::assertSame('font1', $copysheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font12', $copysheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $copysheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $copysheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $copysheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $copysheet->getCell('A1')->getValue()); + self::assertSame('this was a2', $copysheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $copysheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $copysheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $copysheet->getCell('B2')->getValue()); + + $spreadsheet->disconnectWorksheets(); + $copied->disconnectWorksheets(); + } + + public function testClone(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Do not use clone on spreadsheet. Use spreadsheet->copy() instead.'); + $spreadsheet = new Spreadsheet(); + $clone = clone $spreadsheet; + $spreadsheet->disconnectWorksheets(); + $clone->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php index 3c96403b..2c6d0da8 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php @@ -10,15 +10,24 @@ use PHPUnit\Framework\TestCase; class CellMatcherTest extends TestCase { /** - * @var Spreadsheet + * @var ?Spreadsheet */ protected $spreadsheet; - protected function setUp(): void + protected function loadSpreadsheet(): Spreadsheet { $filename = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx'; $reader = IOFactory::createReader('Xlsx'); - $this->spreadsheet = $reader->load($filename); + + return $reader->load($filename); + } + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } } /** @@ -26,16 +35,13 @@ class CellMatcherTest extends TestCase */ public function testBasicCellIsComparison(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "{$cellAddress} is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -82,16 +88,13 @@ class CellMatcherTest extends TestCase */ public function testRangeCellIsComparison(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -128,16 +131,13 @@ class CellMatcherTest extends TestCase */ public function testCellIsMultipleExpression(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -167,16 +167,13 @@ class CellMatcherTest extends TestCase */ public function testCellIsExpression(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -216,16 +213,13 @@ class CellMatcherTest extends TestCase */ public function testTextExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -329,16 +323,13 @@ class CellMatcherTest extends TestCase */ public function testBlankExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -365,16 +356,13 @@ class CellMatcherTest extends TestCase */ public function testErrorExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -400,16 +388,13 @@ class CellMatcherTest extends TestCase */ public function testDateOccurringExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -447,16 +432,13 @@ class CellMatcherTest extends TestCase */ public function testDuplicatesExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::AssertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -486,16 +468,13 @@ class CellMatcherTest extends TestCase */ public function testCrossWorksheetExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); diff --git a/tests/PhpSpreadsheetTests/Style/FontTest.php b/tests/PhpSpreadsheetTests/Style/FontTest.php index 02814afa..6cd4d950 100644 --- a/tests/PhpSpreadsheetTests/Style/FontTest.php +++ b/tests/PhpSpreadsheetTests/Style/FontTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Style; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Font; use PHPUnit\Framework\TestCase; class FontTest extends TestCase @@ -88,4 +89,20 @@ class FontTest extends TestCase self::assertEquals('Calibri', $font->getName(), 'Null string changed to default'); $spreadsheet->disconnectWorksheets(); } + + public function testUnderlineHash(): void + { + $font1 = new Font(); + $font2 = new Font(); + $font2aHash = $font2->getHashCode(); + self::assertSame($font1->getHashCode(), $font2aHash); + $font2->setUnderlineColor( + [ + 'type' => 'srgbClr', + 'value' => 'FF0000', + ] + ); + $font2bHash = $font2->getHashCode(); + self::assertNotEquals($font1->getHashCode(), $font2bHash); + } } diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index 078ed007..ee1b919e 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -186,4 +186,14 @@ return [ 3, true, ], + 'issue2934' => [ + 'Red', + 102, + [ + [null, 102], + [null, 'Red'], + ], + 2, + false, + ], ]; diff --git a/tests/data/Calculation/LookupRef/VLOOKUP.php b/tests/data/Calculation/LookupRef/VLOOKUP.php index 2162d49a..21146638 100644 --- a/tests/data/Calculation/LookupRef/VLOOKUP.php +++ b/tests/data/Calculation/LookupRef/VLOOKUP.php @@ -98,7 +98,7 @@ return [ ['10y1', 7.0], ['10y2', 10.0], ], - 'NaN', + -5, ], [ '#REF!', @@ -111,9 +111,9 @@ return [ '#REF!', '10y2', [ - 2.0, - 7.0, - 10.0, + [2.0], + [7.0], + [10.0], ], 2.0, ], @@ -163,4 +163,34 @@ return [ 3, null, ], + 'issue2934' => [ + 'Red', + 102, + [ + [null, null], + [102, 'Red'], + ], + 2, + false, + ], + 'string supplied as index' => [ + '#VALUE!', + 102, + [ + [null, null], + [102, 'Red'], + ], + 'xyz', + false, + ], + 'num error propagated' => [ + '#NUM!', + 102, + [ + [null, null], + [102, 'Red'], + ], + '=SQRT(-1)', + false, + ], ]; diff --git a/tests/data/Reader/HTML/utf8chars.html b/tests/data/Reader/HTML/utf8chars.html new file mode 100644 index 00000000..8d58c798 --- /dev/null +++ b/tests/data/Reader/HTML/utf8chars.html @@ -0,0 +1,28 @@ + + + + +Test Utf-8 characters voilà + + + + + + + + + + + + + + + + + + + + +
éàâèîαβγδε
𐐁𐐂𐐃 & だけちאבגדה𪔀𪔁𪔂
᠐᠑᠒അആกขฃ✀✐✠
+ +