Merge 0a56c40b715f9d3b05b63e661344fe3c989734a7 into 84596668410bea89d21aa9867b91e1550e359329

This commit is contained in:
xwiz 2022-05-26 15:55:27 +02:00 committed by GitHub
commit 93c5bd21af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 348 additions and 10 deletions

View File

@ -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

View File

@ -23,6 +23,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 @@ class Worksheet
$this->externalSheet = $externalSheet;
$this->maxNumColumns = 0;
$this->lastWrittenRowIndex = 0;
$this->columnWidths = [];
$this->widthCalcuationStyle = 0;
}
/**
@ -78,6 +96,79 @@ class Worksheet
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 @@ class Worksheet
$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
*/

View File

@ -0,0 +1,48 @@
<?php
namespace Box\Spout\Writer\Common\Helper;
class AppendHelper {
/**
* Instead of seeking and re-writing from position, a better hack might be to write dummy empty data
* Enough to take care of any length, then carefully overwrite
*
*/
/**
* This function will truncate from specified position
* Write data to be inserted and re-append the truncated data
*
* @param $fp Pointer to file only
* @param $pos Position to insert
* @param $content Content to insert
*/
public static function insertToFile($fp, $pos, $content)
{
fseek($fp, $pos);
$trailer = stream_get_contents($fp);
ftruncate($fp, $pos);
fseek($fp, $pos);
fwrite($fp, $content);
fwrite($fp, $trailer);
return $fp;
}
/**
* This function overwrite data in pointer from specified position
*
* @param $fp Pointer to file only
* @param $pos Position to insert
* @param $content Content to insert
*/
public static function overwriteToFile($fp, $pos, $content)
{
$cur = ftell($fp);
fseek($fp, $pos);
fwrite($fp, $content);
fseek($fp, $cur);
return $fp;
}
}

View File

@ -48,7 +48,7 @@ class ManagerFactory implements ManagerFactoryInterface
$styleMerger = $this->createStyleMerger();
$styleManager = $this->createStyleManager($optionsManager);
$worksheetManager = $this->createWorksheetManager($styleManager, $styleMerger);
$worksheetManager = $this->createWorksheetManager($optionsManager, $styleManager, $styleMerger);
return new WorkbookManager(
$workbook,
@ -63,16 +63,17 @@ class ManagerFactory implements ManagerFactoryInterface
}
/**
* @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);
}
/**

View File

@ -202,7 +202,7 @@ EOD;
EOD;
$contentXmlFileContents .= $styleManager->getContentXmlFontFaceSectionContent();
$contentXmlFileContents .= $styleManager->getContentXmlAutomaticStylesSectionContent($worksheets);
$contentXmlFileContents .= $styleManager->getContentXmlAutomaticStylesSectionContent($worksheetManager, $worksheets);
$contentXmlFileContents .= '<office:body><office:spreadsheet>';

View File

@ -33,6 +33,8 @@ class OptionsManager extends OptionsManagerAbstract
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 @@ class OptionsManager extends OptionsManagerAbstract
$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);
}
}

View File

@ -151,13 +151,16 @@ EOD;
/**
* Returns the contents of the "<office:automatic-styles>" section, inside "content.xml" file.
*
* @param WorksheetManager $manager
* @param Worksheet[] $worksheets
* @return string
*/
public function getContentXmlAutomaticStylesSectionContent($worksheets)
public function getContentXmlAutomaticStylesSectionContent($manager, $worksheets)
{
$content = '<office:automatic-styles>';
$content .= $manager->getWidthStylesContent($worksheets[0]);
foreach ($this->styleRegistry->getRegisteredStyles() as $style) {
$content .= $this->getStyleSectionContent($style);
}

View File

@ -9,6 +9,9 @@ use Box\Spout\Common\Exception\InvalidArgumentException;
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 @@ class WorksheetManager implements WorksheetManagerInterface
*/
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 @@ class WorksheetManager implements WorksheetManagerInterface
$tableStyleName = 'ta' . ($externalSheet->getIndex() + 1);
$tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
$tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co1" table:number-columns-repeated="' . $worksheet->getMaxNumColumns() . '"/>';
if ($worksheet->getWidthCalculation() != Worksheet::W_NONE) {
foreach ($worksheet->getColumnWidths() as $i => $w){
$colNo = $i + 1;
$tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co'.$colNo.'"/>';
}
} else {
$tableElement .= '<table:table-column table:default-cell-style-name="ce1" table:style-name="co1" table:number-columns-repeated="' . $worksheet->getMaxNumColumns() . '"/>';
}
return $tableElement;
}
@ -125,6 +155,10 @@ class WorksheetManager implements WorksheetManagerInterface
/** @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 @@ class WorksheetManager implements WorksheetManagerInterface
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 .= '<style:style style:name="co'.$colNo.
'" style:family="table-column"><style:table-column-properties fo:break-before="auto" style:column-width="'.
$win.
'in"/></style:style>';
}
return $style;
}
return "";
}
/**
* Closes the worksheet
*

View File

@ -27,6 +27,9 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract
/** @var WorkbookManagerInterface|null */
private $workbookManager;
/** @var int Width calculation style */
protected $widthCalcuationStyle;
/**
* @param OptionsManagerInterface $optionsManager
* @param GlobalFunctionsHelper $globalFunctionsHelper
@ -146,6 +149,38 @@ abstract class WriterMultiSheetsAbstract extends WriterAbstract
}
}
/**
* 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}
*/

View File

@ -37,6 +37,8 @@ class OptionsManager extends OptionsManagerAbstract
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 @@ class OptionsManager extends OptionsManagerAbstract
$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);
}
}

View File

@ -9,6 +9,7 @@ use Box\Spout\Common\Exception\InvalidArgumentException;
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'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
EOD;
/** @var bool Whether inline or shared strings should be used */
@ -59,6 +61,15 @@ EOD;
/** @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 @@ EOD;
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 @@ EOD;
*/
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, '<sheetData>');
}
@ -159,6 +186,12 @@ EOD;
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 @@ EOD;
}
\fwrite($worksheetFilePointer, '</sheetData>');
if ($worksheet->getWidthCalculation() != Worksheet::W_NONE) {
$colNode ='<cols>';
$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 .= '<col hidden="false" collapsed="false" min="'.$colAffect.'" max="'.$colAffect.'" width="'.$width.'" customWidth="true"/>';
}
$colNode .= '</cols>';
if ($worksheet->getWidthCalculation() == Worksheet::W_FULL_ALT) {
$worksheetFilePointer = AppendHelper::overwriteToFile($worksheetFilePointer, $this->headWritePosition, $colNode);
} else {
$worksheetFilePointer = AppendHelper::insertToFile($worksheetFilePointer, $this->headWritePosition, $colNode);
}
}
\fwrite($worksheetFilePointer, '</worksheet>');
\fclose($worksheetFilePointer);
}