diff --git a/src/Spout/Writer/Common/Entity/Options.php b/src/Spout/Writer/Common/Entity/Options.php index fbfa70930..94dffa672 100644 --- a/src/Spout/Writer/Common/Entity/Options.php +++ b/src/Spout/Writer/Common/Entity/Options.php @@ -16,6 +16,8 @@ abstract class Options // Multisheets options public const TEMP_FOLDER = 'tempFolder'; public const DEFAULT_ROW_STYLE = 'defaultRowStyle'; + public const ROWWIDTH_CALC_STYLE = 'rowCalcMethod'; + public const ROWWIDTH_FIXED = 'rowFixedWith'; public const SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY = 'shouldCreateNewSheetsAutomatically'; // XLSX specific options diff --git a/src/Spout/Writer/Common/Entity/Worksheet.php b/src/Spout/Writer/Common/Entity/Worksheet.php index 1e496f012..b1a5d0715 100644 --- a/src/Spout/Writer/Common/Entity/Worksheet.php +++ b/src/Spout/Writer/Common/Entity/Worksheet.php @@ -22,6 +22,22 @@ class Worksheet /** @var int Index of the last written row */ private $lastWrittenRowIndex; + + /** @var array Array of the column widths */ + protected $columnWidths; + + /** @var int Width calculation style */ + protected $widthCalcuationStyle; + + /** @var int Fixed sheet width for fixed width calculation style */ + protected $fixedSheetWidth; + + public const W_FULL = 1; + public const W_FIXED = 2; + public const W_FULL_ALT = 3; + public const W_NONE = 0; + public const DEFAULT_COL_WIDTH = 30; + public const DEFAULT_FIXED_WIDTH = 320; /** * Worksheet constructor. @@ -36,6 +52,8 @@ public function __construct($worksheetFilePath, Sheet $externalSheet) $this->externalSheet = $externalSheet; $this->maxNumColumns = 0; $this->lastWrittenRowIndex = 0; + $this->columnWidths = []; + $this->widthCalcuationStyle = 0; } /** @@ -78,6 +96,79 @@ public function getMaxNumColumns() return $this->maxNumColumns; } + /** + * @return array + */ + public function getColumnWidths() + { + return $this->columnWidths; + } + + /** + * Gets the calculated max column width for the specified index + * @param int $zeroBasedIndex + * @return int + */ + public function getMaxColumnWidth($zeroBasedIndex) + { + if (isset($this->columnWidths[$zeroBasedIndex])) { + return $this->columnWidths[$zeroBasedIndex]; + } + + $this->columnWidths[$zeroBasedIndex] = self::DEFAULT_COL_WIDTH; + return $this->columnWidths[$zeroBasedIndex]; + } + + /** + * Sets the calculated max column width for the specified index + * @param int $zeroBasedIndex + * @param int $value Value to set to + * @return void + */ + public function setMaxColumnWidth($zeroBasedIndex, $value) + { + $curSize = $this->columnWidths[$zeroBasedIndex] ?? 0; + if ($curSize < $value) { + $this->columnWidths[$zeroBasedIndex] = $value; + } + } + + /** + * Automatically calculates and sets the max column width for the specified cell + * @param Cell $cell The cell + * @param Style $style Row/Cell style + * @param int $zeroBasedIndex of cell + * @return void + */ + public function autoSetWidth($cell, $style, $zeroBasedIndex) + { + $size = 1 + mb_strlen($cell->getValue());//ensure we have at least 1 space + $size *= $style->isFontBold() ? 1.2 : 1.0; + $this->setMaxColumnWidth($zeroBasedIndex, $size); + } + + /** + * Gets the fixed sheet width or returns the default if not available + * @return int + */ + public function getFixedSheetWidth() + { + if (!$this->fixedSheetWidth) { + return Worksheet::DEFAULT_FIXED_WIDTH; + } + return $this->fixedSheetWidth; + } + + /** + * Sets the fixed sheet width + * @param int $width + * @return void + */ + public function setFixedSheetWidth($width) + { + $this->fixedSheetWidth = $width; + } + /** * @param int $maxNumColumns */ @@ -86,6 +177,29 @@ public function setMaxNumColumns($maxNumColumns) $this->maxNumColumns = $maxNumColumns; } + /** + * Set the with calculation style for this sheet. + * 1=FullExpand,2=FixedWidth,0=None + * + * @return Worksheet Enable method chaining for easy set width + */ + public function setWidthCalculation($widthStyle) + { + $this->widthCalcuationStyle = $widthStyle; + return $this; + } + + /** + * Get the with calculation style for this sheet. + * 1=FullExpand,2=FixedWidth,0=None + * + * @return void + */ + public function getWidthCalculation() + { + return $this->widthCalcuationStyle; + } + /** * @return int */ diff --git a/src/Spout/Writer/Common/Helper/AppendHelper.php b/src/Spout/Writer/Common/Helper/AppendHelper.php new file mode 100644 index 000000000..473df9623 --- /dev/null +++ b/src/Spout/Writer/Common/Helper/AppendHelper.php @@ -0,0 +1,48 @@ +createStyleMerger(); $styleManager = $this->createStyleManager($optionsManager); - $worksheetManager = $this->createWorksheetManager($styleManager, $styleMerger); + $worksheetManager = $this->createWorksheetManager($optionsManager, $styleManager, $styleMerger); return new WorkbookManager( $workbook, @@ -63,16 +63,17 @@ public function createWorkbookManager(OptionsManagerInterface $optionsManager) } /** + * @param OptionsManagerInterface $optionsManager * @param StyleManager $styleManager * @param StyleMerger $styleMerger * @return WorksheetManager */ - private function createWorksheetManager(StyleManager $styleManager, StyleMerger $styleMerger) + private function createWorksheetManager(OptionsManagerInterface $optionsManager, StyleManager $styleManager, StyleMerger $styleMerger) { $stringsEscaper = $this->helperFactory->createStringsEscaper(); $stringsHelper = $this->helperFactory->createStringHelper(); - return new WorksheetManager($styleManager, $styleMerger, $stringsEscaper, $stringsHelper); + return new WorksheetManager($optionsManager, $styleManager, $styleMerger, $stringsEscaper, $stringsHelper); } /** diff --git a/src/Spout/Writer/ODS/Helper/FileSystemHelper.php b/src/Spout/Writer/ODS/Helper/FileSystemHelper.php index 0e593e606..e8735ddcf 100644 --- a/src/Spout/Writer/ODS/Helper/FileSystemHelper.php +++ b/src/Spout/Writer/ODS/Helper/FileSystemHelper.php @@ -202,7 +202,7 @@ public function createContentFile($worksheetManager, $styleManager, $worksheets) EOD; $contentXmlFileContents .= $styleManager->getContentXmlFontFaceSectionContent(); - $contentXmlFileContents .= $styleManager->getContentXmlAutomaticStylesSectionContent($worksheets); + $contentXmlFileContents .= $styleManager->getContentXmlAutomaticStylesSectionContent($worksheetManager, $worksheets); $contentXmlFileContents .= ''; diff --git a/src/Spout/Writer/ODS/Manager/OptionsManager.php b/src/Spout/Writer/ODS/Manager/OptionsManager.php index a6fb564ef..8ad486988 100644 --- a/src/Spout/Writer/ODS/Manager/OptionsManager.php +++ b/src/Spout/Writer/ODS/Manager/OptionsManager.php @@ -33,6 +33,8 @@ protected function getSupportedOptions() return [ Options::TEMP_FOLDER, Options::DEFAULT_ROW_STYLE, + Options::ROWWIDTH_CALC_STYLE, + Options::ROWWIDTH_FIXED, Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, ]; } @@ -45,5 +47,6 @@ protected function setDefaultOptions() $this->setOption(Options::TEMP_FOLDER, \sys_get_temp_dir()); $this->setOption(Options::DEFAULT_ROW_STYLE, $this->styleBuilder->build()); $this->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, true); + $this->setOption(Options::ROWWIDTH_CALC_STYLE, 0); } } diff --git a/src/Spout/Writer/ODS/Manager/Style/StyleManager.php b/src/Spout/Writer/ODS/Manager/Style/StyleManager.php index 34f75c78d..9430f247a 100644 --- a/src/Spout/Writer/ODS/Manager/Style/StyleManager.php +++ b/src/Spout/Writer/ODS/Manager/Style/StyleManager.php @@ -151,13 +151,16 @@ public function getContentXmlFontFaceSectionContent() /** * Returns the contents of the "" section, inside "content.xml" file. * + * @param WorksheetManager $manager * @param Worksheet[] $worksheets * @return string */ - public function getContentXmlAutomaticStylesSectionContent($worksheets) + public function getContentXmlAutomaticStylesSectionContent($manager, $worksheets) { $content = ''; + $content .= $manager->getWidthStylesContent($worksheets[0]); + foreach ($this->styleRegistry->getRegisteredStyles() as $style) { $content .= $this->getStyleSectionContent($style); } diff --git a/src/Spout/Writer/ODS/Manager/WorksheetManager.php b/src/Spout/Writer/ODS/Manager/WorksheetManager.php index e5d51c10b..8160c78e0 100644 --- a/src/Spout/Writer/ODS/Manager/WorksheetManager.php +++ b/src/Spout/Writer/ODS/Manager/WorksheetManager.php @@ -9,6 +9,9 @@ use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Helper\Escaper\ODS as ODSEscaper; use Box\Spout\Common\Helper\StringHelper; +use Box\Spout\Common\Manager\OptionsManagerInterface; +use Box\Spout\Writer\Common\Helper\AppendHelper; +use Box\Spout\Writer\Common\Entity\Options; use Box\Spout\Writer\Common\Entity\Worksheet; use Box\Spout\Writer\Common\Manager\RegisteredStyle; use Box\Spout\Writer\Common\Manager\Style\StyleMerger; @@ -33,20 +36,33 @@ class WorksheetManager implements WorksheetManagerInterface /** @var StyleMerger Helper to merge styles together */ private $styleMerger; + /** $int file pointer head position */ + private $headWritePosition; + + /** @var int Width calculation style */ + protected $widthCalcuationStyle; + + /** @var int Fixed Width */ + protected $fixedWidth; + /** * WorksheetManager constructor. * + * @param OptionsManagerInterface $optionsManager * @param StyleManager $styleManager * @param StyleMerger $styleMerger * @param ODSEscaper $stringsEscaper * @param StringHelper $stringHelper */ public function __construct( + OptionsManagerInterface $optionsManager, StyleManager $styleManager, StyleMerger $styleMerger, ODSEscaper $stringsEscaper, StringHelper $stringHelper ) { + $this->widthCalcuationStyle = $optionsManager->getOption(Options::ROWWIDTH_CALC_STYLE); + $this->fixedWidth = $optionsManager->getOption(Options::ROWWIDTH_FIXED); $this->styleManager = $styleManager; $this->styleMerger = $styleMerger; $this->stringsEscaper = $stringsEscaper; @@ -62,9 +78,15 @@ public function __construct( */ public function startSheet(Worksheet $worksheet) { - $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w'); + $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w+'); $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer); + $worksheet->setWidthCalculation($this->widthCalcuationStyle); + $worksheet->setFixedSheetWidth($this->fixedWidth); + if ($worksheet->getWidthCalculation() != Worksheet::W_NONE) { + $this->headWritePosition = ftell($sheetFilePointer); + } + $worksheet->setFilePointer($sheetFilePointer); } @@ -95,7 +117,15 @@ public function getTableElementStartAsString(Worksheet $worksheet) $tableStyleName = 'ta' . ($externalSheet->getIndex() + 1); $tableElement = ''; - $tableElement .= ''; + + if ($worksheet->getWidthCalculation() != Worksheet::W_NONE) { + foreach ($worksheet->getColumnWidths() as $i => $w){ + $colNo = $i + 1; + $tableElement .= ''; + } + } else { + $tableElement .= ''; + } return $tableElement; } @@ -125,6 +155,10 @@ public function addRow(Worksheet $worksheet, Row $row) /** @var Cell|null $nextCell */ $nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null; + if ($worksheet->getWidthCalculation() != Worksheet::W_NONE) { + $worksheet->autoSetWidth($cell, $rowStyle, $i); + } + if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) { $registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle); $cellStyle = $registeredStyle->getStyle(); @@ -249,6 +283,42 @@ private function getCellXML(Cell $cell, $styleIndex, $numTimesValueRepeated) return $data; } + /** + * Generate the related column widths style xml to be inserted in content.xml + * @param Worksheet $worksheet + * @return string + */ + public function getWidthStylesContent($worksheet) + { + if ($worksheet->getWidthCalculation() != Worksheet::W_NONE) { + //create the col styles + $style = ''; + $widths = $worksheet->getColumnWidths(); + //todo: this may not be adequate for multiple worksheets + + //re-calculate width for fixed sets + if ($worksheet->getWidthCalculation() == Worksheet::W_FIXED) { + $total = array_sum($widths); + foreach($widths as $i => $w) { + $wr = ($w / $total) * $worksheet->getFixedSheetWidth(); + $widths[$i] = $wr; + } + } + + foreach ($widths as $i => $width){ + //this is a rough equivalent based on pixel density, + $win = round($width / 9.6, 2);//convert to inches + $colNo = $i + 1; + $style .= ''; + } + return $style; + } + return ""; + } + /** * Closes the worksheet * diff --git a/src/Spout/Writer/WriterMultiSheetsAbstract.php b/src/Spout/Writer/WriterMultiSheetsAbstract.php index 18b40bd42..f5175577f 100644 --- a/src/Spout/Writer/WriterMultiSheetsAbstract.php +++ b/src/Spout/Writer/WriterMultiSheetsAbstract.php @@ -26,6 +26,9 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract /** @var WorkbookManagerInterface|null */ private $workbookManager; + + /** @var int Width calculation style */ + protected $widthCalcuationStyle; /** * @param OptionsManagerInterface $optionsManager @@ -146,6 +149,38 @@ protected function throwIfWorkbookIsNotAvailable() } } + /** + * Set default sheet width calculation option + * + * @param int $option The width calculation style + * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened + * @return Writer + */ + public function setWidthCalculation($option) + { + $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + + $this->optionsManager->setOption(Options::ROWWIDTH_CALC_STYLE, $option); + + return $this; + } + + /** + * Set fixed sheet width size option + * + * @param int $option The fixed width + * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened + * @return Writer + */ + public function setFixedWidth($option) + { + $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + + $this->optionsManager->setOption(Options::ROWWIDTH_FIXED, $option); + + return $this; + } + /** * {@inheritdoc} */ diff --git a/src/Spout/Writer/XLSX/Manager/OptionsManager.php b/src/Spout/Writer/XLSX/Manager/OptionsManager.php index 000767ffc..7be77be3b 100644 --- a/src/Spout/Writer/XLSX/Manager/OptionsManager.php +++ b/src/Spout/Writer/XLSX/Manager/OptionsManager.php @@ -37,6 +37,8 @@ protected function getSupportedOptions() return [ Options::TEMP_FOLDER, Options::DEFAULT_ROW_STYLE, + Options::ROWWIDTH_CALC_STYLE, + Options::ROWWIDTH_FIXED, Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, Options::SHOULD_USE_INLINE_STRINGS, ]; @@ -56,5 +58,6 @@ protected function setDefaultOptions() $this->setOption(Options::DEFAULT_ROW_STYLE, $defaultRowStyle); $this->setOption(Options::SHOULD_CREATE_NEW_SHEETS_AUTOMATICALLY, true); $this->setOption(Options::SHOULD_USE_INLINE_STRINGS, true); + $this->setOption(Options::ROWWIDTH_CALC_STYLE, 0); } } diff --git a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php index 602544295..537c2ce82 100644 --- a/src/Spout/Writer/XLSX/Manager/WorksheetManager.php +++ b/src/Spout/Writer/XLSX/Manager/WorksheetManager.php @@ -9,6 +9,7 @@ use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Helper\Escaper\XLSX as XLSXEscaper; use Box\Spout\Common\Helper\StringHelper; +use Box\Spout\Writer\Common\Helper\AppendHelper; use Box\Spout\Common\Manager\OptionsManagerInterface; use Box\Spout\Writer\Common\Entity\Options; use Box\Spout\Writer\Common\Entity\Worksheet; @@ -35,7 +36,8 @@ class WorksheetManager implements WorksheetManagerInterface public const SHEET_XML_FILE_HEADER = <<<'EOD' - + EOD; /** @var bool Whether inline or shared strings should be used */ @@ -59,6 +61,15 @@ class WorksheetManager implements WorksheetManagerInterface /** @var StringHelper String helper */ private $stringHelper; + /** $int file pointer head position */ + private $headWritePosition; + + /** @var int Width calculation style */ + protected $widthCalcuationStyle; + + /** @var int Fixed Width */ + protected $fixedWidth; + /** * WorksheetManager constructor. * @@ -80,6 +91,8 @@ public function __construct( StringHelper $stringHelper ) { $this->shouldUseInlineStrings = $optionsManager->getOption(Options::SHOULD_USE_INLINE_STRINGS); + $this->widthCalcuationStyle = $optionsManager->getOption(Options::ROWWIDTH_CALC_STYLE); + $this->fixedWidth = $optionsManager->getOption(Options::ROWWIDTH_FIXED); $this->rowManager = $rowManager; $this->styleManager = $styleManager; $this->styleMerger = $styleMerger; @@ -101,12 +114,26 @@ public function getSharedStringsManager() */ public function startSheet(Worksheet $worksheet) { - $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w'); + $sheetFilePointer = \fopen($worksheet->getFilePath(), 'w+'); $this->throwIfSheetFilePointerIsNotAvailable($sheetFilePointer); $worksheet->setFilePointer($sheetFilePointer); - + $worksheet->setWidthCalculation($this->widthCalcuationStyle); + $worksheet->setFixedSheetWidth($this->fixedWidth); + \fwrite($sheetFilePointer, self::SHEET_XML_FILE_HEADER); + if ($worksheet->getWidthCalculation() != Worksheet::W_NONE) { + $this->headWritePosition = ftell($sheetFilePointer); + } + //width calculation style 3 with empty spaces.. not suitable if column sizes more than 40 + if ($worksheet->getWidthCalculation() == Worksheet::W_FULL_ALT) { + //insert dummy nodes for up to 40 columns + for ($i = 0; $i < 40; $i++) { + $dummy = " "; + \fwrite($sheetFilePointer, $dummy); + } + } + \fwrite($sheetFilePointer, ''); } @@ -159,6 +186,12 @@ private function addNonEmptyRow(Worksheet $worksheet, Row $row) if ($registeredStyle->isMatchingRowStyle()) { $rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id) } + + if ($worksheet->getWidthCalculation() != Worksheet::W_NONE) { + //use row style to maintain a fair average based width computation for now + $worksheet->autoSetWidth($cell, $rowStyle, $columnIndexZeroBased); + } + $rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId()); } @@ -287,6 +320,32 @@ public function close(Worksheet $worksheet) } \fwrite($worksheetFilePointer, ''); + + if ($worksheet->getWidthCalculation() != Worksheet::W_NONE) { + $colNode =''; + $widths = $worksheet->getColumnWidths(); + + //re-calculate width for fixed sets + if ($worksheet->getWidthCalculation() == Worksheet::W_FIXED) { + $total = array_sum($widths); + foreach($widths as $i => $w) { + $wr = ($w / $total) * $worksheet->getFixedSheetWidth(); + $widths[$i] = $wr; + } + } + + foreach ($widths as $i => $width){ + $colAffect = $i + 1; + $colNode .= ''; + } + $colNode .= ''; + if ($worksheet->getWidthCalculation() == Worksheet::W_FULL_ALT) { + $worksheetFilePointer = AppendHelper::overwriteToFile($worksheetFilePointer, $this->headWritePosition, $colNode); + } else { + $worksheetFilePointer = AppendHelper::insertToFile($worksheetFilePointer, $this->headWritePosition, $colNode); + } + } + \fwrite($worksheetFilePointer, ''); \fclose($worksheetFilePointer); }