diff --git a/.travis.yml b/.travis.yml index 365ee595f..e9c312b3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,4 +58,4 @@ after_script: if [[ "$WITH_COVERAGE" == "true" ]]; then wget https://scrutinizer-ci.com/ocular.phar php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover - fi + fi diff --git a/src/Spout/Common/Entity/Row.php b/src/Spout/Common/Entity/Row.php index 0cc49c730..3ef52b2e5 100644 --- a/src/Spout/Common/Entity/Row.php +++ b/src/Spout/Common/Entity/Row.php @@ -6,6 +6,8 @@ class Row { + public const DEFAULT_HEIGHT = 15; + /** * The cells in this row * @var Cell[] @@ -18,6 +20,18 @@ class Row */ protected $style; + /** + * Row height + * @var float + */ + protected $height = self::DEFAULT_HEIGHT; + + /** + * Whether the height property was set + * @var bool + */ + protected $hasSetHeight = false; + /** * Row constructor. * @param Cell[] $cells @@ -126,4 +140,34 @@ public function toArray() return $cell->getValue(); }, $this->cells); } + + /** + * Set row height + * @param float $height + * @return Row + */ + public function setHeight($height) + { + $this->height = $height; + $this->hasSetHeight = (bool)$height; + + return $this; + } + + /** + * Returns row height + * @return float + */ + public function getHeight() + { + return $this->height; + } + + /** + * @return bool + */ + public function hasSetHeight(): bool + { + return $this->hasSetHeight; + } } diff --git a/src/Spout/Common/Entity/Style/CellVerticalAlignment.php b/src/Spout/Common/Entity/Style/CellVerticalAlignment.php new file mode 100644 index 000000000..27a4dd14f --- /dev/null +++ b/src/Spout/Common/Entity/Style/CellVerticalAlignment.php @@ -0,0 +1,34 @@ + 1, + self::CENTER => 1, + self::BOTTOM => 1, + self::JUSTIFY => 1, + self::DISTRIBUTED => 1, + ]; + + /** + * @param string $cellVerticalAlignment + * + * @return bool Whether the given cell vertical alignment is valid + */ + public static function isValid($cellVerticalAlignment) + { + return isset(self::$VALID_VERTICAL_ALIGNMENTS[$cellVerticalAlignment]); + } +} diff --git a/src/Spout/Common/Entity/Style/Style.php b/src/Spout/Common/Entity/Style/Style.php index 2bacc7afa..bdbdec6fc 100644 --- a/src/Spout/Common/Entity/Style/Style.php +++ b/src/Spout/Common/Entity/Style/Style.php @@ -61,11 +61,23 @@ class Style /** @var bool Whether the cell alignment property was set */ private $hasSetCellAlignment = false; + /** @var bool Whether specific cell alignment should be applied */ + private $shouldApplyCellVerticalAlignment = false; + /** @var string Cell alignment */ + private $cellVerticalAlignment; + /** @var bool Whether the cell alignment property was set */ + private $hasSetCellVerticalAlignment = false; + /** @var bool Whether the text should wrap in the cell (useful for long or multi-lines text) */ private $shouldWrapText = false; /** @var bool Whether the wrap text property was set */ private $hasSetWrapText = false; + /** @var bool Whether the cell should shrink to fit to content */ + private $shouldShrinkToFit = false; + /** @var bool Whether the shouldShrinkToFit text property was set */ + private $hasSetShrinkToFit = false; + /** @var Border */ private $border; @@ -385,6 +397,45 @@ public function shouldApplyCellAlignment() return $this->shouldApplyCellAlignment; } + /** + * @return string + */ + public function getCellVerticalAlignment() + { + return $this->cellVerticalAlignment; + } + + /** + * @param string $cellVerticalAlignment The cell vertical alignment + * + * @return Style + */ + public function setCellVerticalAlignment($cellVerticalAlignment) + { + $this->cellVerticalAlignment = $cellVerticalAlignment; + $this->hasSetCellVerticalAlignment = true; + $this->shouldApplyCellVerticalAlignment = true; + $this->isEmpty = false; + + return $this; + } + + /** + * @return bool + */ + public function hasSetCellVerticalAlignment() + { + return $this->hasSetCellVerticalAlignment; + } + + /** + * @return bool Whether specific cell vertical alignment should be applied + */ + public function shouldApplyCellVerticalAlignment() + { + return $this->shouldApplyCellVerticalAlignment; + } + /** * @return bool */ @@ -482,6 +533,36 @@ public function shouldApplyFormat() return $this->hasSetFormat; } + /** + * Sets should shrink to fit + * @param bool $shrinkToFit + * @return Style + */ + public function setShouldShrinkToFit($shrinkToFit = true) + { + $this->hasSetShrinkToFit = true; + $this->shouldShrinkToFit = $shrinkToFit; + $this->isEmpty = false; + + return $this; + } + + /** + * @return bool Whether format should be applied + */ + public function shouldShrinkToFit() + { + return $this->shouldShrinkToFit; + } + + /** + * @return bool + */ + public function hasSetShrinkToFit() + { + return $this->hasSetShrinkToFit; + } + /** * @return bool */ diff --git a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php index bc2d406f3..60058de84 100644 --- a/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php +++ b/src/Spout/Writer/Common/Creator/Style/StyleBuilder.php @@ -4,6 +4,7 @@ use Box\Spout\Common\Entity\Style\Border; use Box\Spout\Common\Entity\Style\CellAlignment; +use Box\Spout\Common\Entity\Style\CellVerticalAlignment; use Box\Spout\Common\Entity\Style\Style; use Box\Spout\Common\Exception\InvalidArgumentException; @@ -143,6 +144,25 @@ public function setCellAlignment($cellAlignment) return $this; } + /** + * Sets the cell vertical alignment. + * + * @param string $cellVerticalAlignment The cell vertical alignment + * + * @throws InvalidArgumentException If the given cell vertical alignment is not valid + * @return StyleBuilder + */ + public function setCellVerticalAlignment($cellVerticalAlignment) + { + if (!CellVerticalAlignment::isValid($cellVerticalAlignment)) { + throw new InvalidArgumentException('Invalid cell vertical alignment value'); + } + + $this->style->setCellVerticalAlignment($cellVerticalAlignment); + + return $this; + } + /** * Set a border * @@ -183,6 +203,19 @@ public function setFormat($format) return $this; } + /** + * Set should shrink to fit + * @param bool $shrinkToFit + * @return StyleBuilder + * @api + */ + public function setShouldShrinkToFit($shrinkToFit = true) + { + $this->style->setShouldShrinkToFit($shrinkToFit); + + return $this; + } + /** * Returns the configured style. The style is cached and can be reused. * diff --git a/src/Spout/Writer/Common/Entity/Sheet.php b/src/Spout/Writer/Common/Entity/Sheet.php index aabdd91fb..107ef7608 100644 --- a/src/Spout/Writer/Common/Entity/Sheet.php +++ b/src/Spout/Writer/Common/Entity/Sheet.php @@ -2,6 +2,7 @@ namespace Box\Spout\Writer\Common\Entity; +use Box\Spout\Writer\Common\Helper\CellHelper; use Box\Spout\Writer\Common\Manager\SheetManager; /** @@ -27,6 +28,9 @@ class Sheet /** @var SheetManager Sheet manager */ private $sheetManager; + /** @var array merge cell */ + private array $mergeRanges = []; + /** * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) * @param string $associatedWorkbookId ID of the sheet's associated workbook @@ -108,4 +112,48 @@ public function setIsVisible($isVisible) return $this; } + + /** + * @return array + */ + public function getMergeRanges() + { + return $this->mergeRanges; + } + + /** + * @param array $mergeRanges + * @return array + */ + public function setMergeRanges(array $mergeRanges) + { + return $this->mergeRanges = $mergeRanges; + } + + /** + * @param $mergeRanges + * @return array + */ + public function addMergeRanges(string ...$mergeRanges) + { + return $this->mergeRanges = array_merge($this->mergeRanges, $mergeRanges); + } + + /** + * @param int $fromColumnIndexZeroBased + * @param int $fromRowIndexOneBased + * @param int $toColumnIndexZeroBased + * @param int $toRowIndexOneBased + * @return array + */ + public function addMergeRangeByIndexes( + int $fromColumnIndexZeroBased, + int $fromRowIndexOneBased, + int $toColumnIndexZeroBased, + int $toRowIndexOneBased + ) { + $fromLetter = CellHelper::getColumnLettersFromColumnIndex($fromColumnIndexZeroBased); + $toLetter = CellHelper::getColumnLettersFromColumnIndex($toColumnIndexZeroBased); + return $this->addMergeRanges("{$fromLetter}{$fromRowIndexOneBased}:{$toLetter}{$toRowIndexOneBased}"); + } } diff --git a/src/Spout/Writer/Common/Manager/ManagesCellSize.php b/src/Spout/Writer/Common/Manager/ManagesCellSize.php index 8a517f0d9..2ec958cb1 100644 --- a/src/Spout/Writer/Common/Manager/ManagesCellSize.php +++ b/src/Spout/Writer/Common/Manager/ManagesCellSize.php @@ -29,6 +29,14 @@ public function setDefaultRowHeight($height) $this->defaultRowHeight = $height; } + /** + * Clear old column widths when we want to start new sheet + */ + public function clearColumnWidths() + { + $this->columnWidths = []; + } + /** * @param float $width * @param array $columns One or more columns with this width diff --git a/src/Spout/Writer/Common/Manager/Style/StyleMerger.php b/src/Spout/Writer/Common/Manager/Style/StyleMerger.php index 806c8d555..b94f06ed9 100644 --- a/src/Spout/Writer/Common/Manager/Style/StyleMerger.php +++ b/src/Spout/Writer/Common/Manager/Style/StyleMerger.php @@ -85,9 +85,15 @@ private function mergeCellProperties(Style $styleToUpdate, Style $style, Style $ if (!$style->hasSetWrapText() && $baseStyle->shouldWrapText()) { $styleToUpdate->setShouldWrapText(); } + if (!$style->hasSetShrinkToFit() && $baseStyle->shouldShrinkToFit()) { + $styleToUpdate->setShouldShrinkToFit(); + } if (!$style->hasSetCellAlignment() && $baseStyle->shouldApplyCellAlignment()) { $styleToUpdate->setCellAlignment($baseStyle->getCellAlignment()); } + if (!$style->hasSetCellVerticalAlignment() && $baseStyle->shouldApplyCellVerticalAlignment()) { + $styleToUpdate->setCellVerticalAlignment($baseStyle->getCellVerticalAlignment()); + } if (!$style->getBorder() && $baseStyle->shouldApplyBorder()) { $styleToUpdate->setBorder($baseStyle->getBorder()); } diff --git a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php index 604de65ce..a96d33747 100644 --- a/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php +++ b/src/Spout/Writer/Common/Manager/WorkbookManagerAbstract.php @@ -303,6 +303,14 @@ public function setDefaultRowHeight(float $height) $this->worksheetManager->setDefaultRowHeight($height); } + /** + * Clear old column widths when we want to start new sheet + */ + public function clearColumnWidths() + { + $this->worksheetManager->clearColumnWidths(); + } + /** * @param float $width * @param array $columns One or more columns with this width diff --git a/src/Spout/Writer/Common/Manager/WorksheetManagerInterface.php b/src/Spout/Writer/Common/Manager/WorksheetManagerInterface.php index bb6758a7f..0cc7ec53e 100644 --- a/src/Spout/Writer/Common/Manager/WorksheetManagerInterface.php +++ b/src/Spout/Writer/Common/Manager/WorksheetManagerInterface.php @@ -21,6 +21,11 @@ public function setDefaultColumnWidth($width); */ public function setDefaultRowHeight($height); + /** + * Clear old column widths when we want to start new sheet + */ + public function clearColumnWidths(); + /** * @param float $width * @param array $columns One or more columns with this width diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index 64f6e6e60..124a30b27 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -282,6 +282,14 @@ public function setDefaultRowHeight($height) $this->styleManager->setDefaultRowHeight($height); } + /** + * Clear old column widths when we want to start new sheet + */ + public function clearColumnWidths() + { + $this->styleManager->clearColumnWidths(); + } + /** * @param float $width * @param array $columns One or more columns with this width diff --git a/src/Spout/Writer/WriterMultiSheetsAbstract.php b/src/Spout/Writer/WriterMultiSheetsAbstract.php index 0161877e0..cf5873c13 100644 --- a/src/Spout/Writer/WriterMultiSheetsAbstract.php +++ b/src/Spout/Writer/WriterMultiSheetsAbstract.php @@ -166,6 +166,15 @@ public function setDefaultRowHeight(float $height) ); } + /** + * Clear old column widths when we want to start new sheet + */ + public function clearColumnWidths() + { + $this->throwIfWorkbookIsNotAvailable(); + $this->workbookManager->clearColumnWidths(); + } + /** * @param float|null $width * @param array $columns One or more columns with this width diff --git a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php index f0ca9d9e6..89040840f 100644 --- a/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/XLSX/Manager/Style/StyleManager.php @@ -249,15 +249,26 @@ protected function getCellXfsSectionContent() $content .= \sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0); - if ($style->shouldApplyCellAlignment() || $style->shouldWrapText()) { + if ( + $style->shouldApplyCellAlignment() + || $style->shouldApplyCellVerticalAlignment() + || $style->shouldWrapText() + || $style->shouldShrinkToFit() + ) { $content .= ' applyAlignment="1">'; $content .= 'shouldApplyCellAlignment()) { $content .= \sprintf(' horizontal="%s"', $style->getCellAlignment()); } + if ($style->shouldApplyCellVerticalAlignment()) { + $content .= \sprintf(' vertical="%s"', $style->getCellVerticalAlignment()); + } if ($style->shouldWrapText()) { $content .= ' wrapText="1"'; } + if ($style->shouldShrinkToFit()) { + $content .= ' shrinkToFit="1"'; + } $content .= '/>'; $content .= ''; } else { diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 6452cccb8..2d79781f2 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -181,9 +181,11 @@ private function addNonEmptyRow(Worksheet $worksheet, Row $row) $rowStyle = $row->getStyle(); $rowIndexOneBased = $worksheet->getLastWrittenRowIndex() + 1; $numCells = $row->getNumCells(); + $hasSetRowHeight = $row->hasSetHeight(); - $hasCustomHeight = $this->defaultRowHeight > 0 ? '1' : '0'; - $rowXML = ""; + $customHeight = $hasSetRowHeight || $this->defaultRowHeight > 0 ? 'customHeight="1"' : ''; + $rowHeight = $hasSetRowHeight ? "ht=\"{$row->getHeight()}\"" : ''; + $rowXML = ""; foreach ($row->getCells() as $columnIndexZeroBased => $cell) { $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); @@ -356,6 +358,17 @@ public function close(Worksheet $worksheet) } $this->ensureSheetDataStated($worksheet); \fwrite($worksheetFilePointer, ''); + // do something to merging cells + $mergeRanges = $worksheet->getExternalSheet()->getMergeRanges(); + if (!empty($mergeRanges)) { + $startLine = ''; + $rangeLine = ''; + foreach ($mergeRanges as $key => $range) { + $rangeLine .= ''; + } + $endLine = ''; + \fwrite($worksheetFilePointer, $startLine . $rangeLine . $endLine); + } \fwrite($worksheetFilePointer, ''); \fclose($worksheetFilePointer); } diff --git a/tests/Spout/Writer/XLSX/WriterTest.php b/tests/Spout/Writer/XLSX/WriterTest.php index 26b379257..ca8830bb6 100644 --- a/tests/Spout/Writer/XLSX/WriterTest.php +++ b/tests/Spout/Writer/XLSX/WriterTest.php @@ -7,6 +7,7 @@ use Box\Spout\Common\Exception\InvalidArgumentException; use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Exception\SpoutException; +use Box\Spout\Reader\Wrapper\XMLReader; use Box\Spout\TestUsingResource; use Box\Spout\Writer\Common\Creator\WriterEntityFactory; use Box\Spout\Writer\Exception\WriterAlreadyOpenedException; @@ -563,6 +564,48 @@ public function testAddRowShouldEscapeControlCharacters() $this->assertInlineDataWasWrittenToSheet($fileName, 1, 'control _x0015_ character'); } + /** + * @return void + */ + public function testAddRowShouldSupportRowHeights() + { + $fileName = 'test_add_row_should_support_row_heights.xlsx'; + $dataRows = $this->createRowsFromValues([ + ['First row with default height'], + ['Second row with custom height'], + ]); + + $dataRows[1]->setHeight('23'); + + $this->writeToXLSXFile($dataRows, $fileName); + $firstRow = $this->getXmlRowFromXmlFile($fileName, 1, 1); + $secondRow = $this->getXmlRowFromXmlFile($fileName, 1, 2); + $this->assertEquals('15', $firstRow->getAttribute('ht'), '1st row does not have default height.'); + $this->assertEquals('23', $secondRow->getAttribute('ht'), '2nd row does not have custom height.'); + } + + /** + * @return void + */ + public function testGeneratedFileShouldBeValidForEmptySheets() + { + $fileName = 'test_empty_sheet.xlsx'; + $this->createGeneratedFolderIfNeeded($fileName); + $resourcePath = $this->getGeneratedResourcePath($fileName); + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->openToFile($resourcePath); + + $writer->addNewSheetAndMakeItCurrent(); + $writer->close(); + + $xmlReader = $this->getXmlReaderForSheetFromXmlFile($fileName, 1); + $xmlReader->setParserProperty(XMLReader::VALIDATE, true); + $this->assertTrue($xmlReader->isValid(), 'worksheet xml is not valid'); + $xmlReader->setParserProperty(XMLReader::VALIDATE, false); + $xmlReader->readUntilNodeFound('sheetData'); + $this->assertEquals('sheetData', $xmlReader->getCurrentNodeName(), 'worksheet xml does not have sheetData'); + } + /** * @return void */ @@ -677,4 +720,42 @@ private function assertSharedStringWasWritten($fileName, $sharedString, $message $this->assertStringContainsString($sharedString, $xmlContents, $message); } + + /** + * @param $fileName + * @param $sheetIndex - 1 based + * @return XMLReader + */ + private function getXmlReaderForSheetFromXmlFile($fileName, $sheetIndex) + { + $resourcePath = $this->getGeneratedResourcePath($fileName); + + $xmlReader = new XMLReader(); + $xmlReader->openFileInZip($resourcePath, 'xl/worksheets/sheet' . $sheetIndex . '.xml'); + + return $xmlReader; + } + + /** + * @param $fileName + * @param $sheetIndex - 1 based + * @param $rowIndex - 1 based + * @throws \Box\Spout\Reader\Exception\XMLProcessingException + * @return \DOMNode|null + */ + private function getXmlRowFromXmlFile($fileName, $sheetIndex, $rowIndex) + { + $xmlReader = $this->getXmlReaderForSheetFromXmlFile($fileName, $sheetIndex); + $xmlReader->readUntilNodeFound('sheetData'); + + for ($i = 0; $i < $rowIndex; $i++) { + $xmlReader->readUntilNodeFound('row'); + } + + $row = $xmlReader->expand(); + + $xmlReader->close(); + + return $row; + } } diff --git a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php index 2571df416..c7af7be76 100644 --- a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php +++ b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php @@ -306,6 +306,24 @@ public function testAddRowShouldApplyCellAlignment() $this->assertFirstChildHasAttributeEquals(CellAlignment::RIGHT, $xfElement, 'alignment', 'horizontal'); } + /** + * @return void + */ + public function testAddRowShouldApplyShrinkToFit() + { + $fileName = 'test_add_row_should_apply_shrink_to_fit.xlsx'; + + $shrinkToFitStyle = (new StyleBuilder())->setShouldShrinkToFit()->build(); + $dataRows = $this->createStyledRowsFromValues([['xlsx--11']], $shrinkToFitStyle); + + $this->writeToXLSXFile($dataRows, $fileName); + + $cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); + $xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1); + $this->assertEquals(1, $xfElement->getAttribute('applyAlignment')); + $this->assertFirstChildHasAttributeEquals('true', $xfElement, 'alignment', 'shrinkToFit'); + } + /** * @return void */