Compare commits

...
18 Commits
Author SHA1 Message Date
Adrien Loison 9bdb027d31 Update documentation with number format 2021-05-14 23:18:09 +02:00
Adrien Loison 76017f0949 Skipped cells are in wrong order
This only happens when no sheet's dimension is specified.
When filling empty cells with empty strings, we push these new cells with the correct cell index but they are added at the end of the cells array (normal PHP behavior). This means that we were going from `{[0] => 'A', [2] => 'C'}` to `{[0] => 'A', [2] => 'C', [1] => ''}`. We therefore need to sort the array to get the values in the correct order ( `{[0] => 'A', [1] => '', [2] => 'C'}`).
2021-05-14 23:01:36 +02:00
Adrien Loison fde8a495ca Inline strings can contain multiple value nodes
We were working under the assumption that XLSX's inline strings only had a single value node (`<t>`). This is incorrect.
To get the actual value of an inline string node, we need to concatenate the value of all its child nodes.
2021-05-14 22:36:34 +02:00
Adrien Loison 69eeeff478 Remove var_dump 2021-05-14 15:17:30 +02:00
Adrien Loison 2ff515c306 Support for strict OOXML
There are 2 types of OOXML format: transitional and strict. Transitional is what's mostly used but some softwares still allow XLSX to be generated using the strict OOXML format.
In this format, namespaces of the XML files are different: `http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings` is replaced by `http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings` for instance. To support both formats, Spout needs to be able to look for both.
2021-05-13 12:16:20 +02:00
Toby Allen 0837d49c2b Remove unneeded comma 2021-05-10 10:04:14 +02:00
Toby Allen 110876e32c Some small grammar changes 2021-05-10 10:04:14 +02:00
Adrien Loison 8c1f0cc447 Floats must not be stored as locale dependent
Floats are currently stored formatted per the locale setting. This leads to different values being written whether the locale uses "." or "," for the decimal point for instance. This poses a problem as floats must be stored using "." as the decimal point to be valid.
This commit ensures that the floats are stored correctly by forcing the formatting of the value.
2021-05-05 20:43:02 +02:00
Antoine Lamirault eb84ec9364 Rename ManagedStyle to PossiblyUpdatedStyle and add documentation 2021-03-30 19:42:21 +02:00
Antoine Lamirault 8a17d6c71f Remove rowStyle reference and replace it by new RegisteredStyle class 2021-03-30 19:42:21 +02:00
Antoine Lamirault c6f596c776 New code review fixs 2021-03-30 19:42:21 +02:00
Antoine Lamirault 11d91e1740 Code review changes 2021-03-30 19:42:21 +02:00
Antoine Lamirault 197fb9987a Register style can be skipped when already registered 2021-03-30 19:42:21 +02:00
Antoine Lamirault a58b340835 Empty style on cell 2021-03-30 19:42:21 +02:00
Antoine Lamirault 57b6e87a65 Begin optimize xlsx write 2021-03-30 19:42:21 +02:00
yiranzai 91f756be0b remove custom headers 2021-03-18 20:05:10 +01:00
yiranzai 03e1ce438a Fixed Code Style 2021-03-18 20:05:10 +01:00
yiranzai df9d96366f Fixed WriterAbstract::openToBrowser meet RFC6266 2021-03-18 20:05:10 +01:00
39 changed files with 476 additions and 121 deletions

No files matched your search

+3 -3
View File
@@ -8,9 +8,9 @@
[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout)
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB).
Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB).
Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
Join the community and come discuss Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Documentation
@@ -43,7 +43,7 @@ For information, the performance tests take about 10 minutes to run (processing
## Support
You can ask questions, submit new features ideas or discuss about Spout in the chat room:<br>
You can ask questions, submit new features ideas or discuss Spout in the chat room:<br>
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Copyright and License
+2 -1
View File
@@ -14,7 +14,8 @@
"require": {
"php": ">=7.2.0",
"ext-zip": "*",
"ext-xmlreader" : "*"
"ext-xmlreader": "*",
"ext-dom": "*"
},
"require-dev": {
"phpunit/phpunit": "^8",
+13 -12
View File
@@ -116,18 +116,19 @@ $reader->setShouldPreserveEmptyRows(true);
For fonts and alignments, {{ site.spout_html }} does not support all the possible formatting options yet. But you can find the most important ones:
| Category | Property | API
|:----------|:---------------|:--------------------------------------
| Font | Bold | `StyleBuilder::setFontBold()`
| | Italic | `StyleBuilder::setFontItalic()`
| | Underline | `StyleBuilder::setFontUnderline()`
| | Strikethrough | `StyleBuilder::setFontStrikethrough()`
| | Font name | `StyleBuilder::setFontName('Arial')`
| | Font size | `StyleBuilder::setFontSize(14)`
| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
| Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)`
| | Wrap text | `StyleBuilder::setShouldWrapText(true)`
| Category | Property | API
|:---------------------|:---------------|:--------------------------------------
| Font | Bold | `StyleBuilder::setFontBold()`
| | Italic | `StyleBuilder::setFontItalic()`
| | Underline | `StyleBuilder::setFontUnderline()`
| | Strikethrough | `StyleBuilder::setFontStrikethrough()`
| | Font name | `StyleBuilder::setFontName('Arial')`
| | Font size | `StyleBuilder::setFontSize(14)`
| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
| Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)`
| | Wrap text | `StyleBuilder::setShouldWrapText(true)`
| Format _(XLSX only)_ | Number format | `StyleBuilder::setFormat('0.000')`
| | Date format | `StyleBuilder::setFormat('m/d/yy h:mm')`
### Styling rows
+1 -1
View File
@@ -65,7 +65,7 @@ class Cell
protected $style;
/**
* @param $value mixed
* @param mixed|null $value
* @param Style|null $style
*/
public function __construct($value, Style $style = null)
+43
View File
@@ -84,6 +84,12 @@ class Style
/** @var bool */
private $hasSetFormat = false;
/** @var bool */
private $isRegistered = false;
/** @var bool */
private $isEmpty = true;
/**
* @return int|null
*/
@@ -119,6 +125,7 @@ class Style
{
$this->shouldApplyBorder = true;
$this->border = $border;
$this->isEmpty = false;
return $this;
}
@@ -147,6 +154,7 @@ class Style
$this->fontBold = true;
$this->hasSetFontBold = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@@ -175,6 +183,7 @@ class Style
$this->fontItalic = true;
$this->hasSetFontItalic = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@@ -203,6 +212,7 @@ class Style
$this->fontUnderline = true;
$this->hasSetFontUnderline = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@@ -231,6 +241,7 @@ class Style
$this->fontStrikethrough = true;
$this->hasSetFontStrikethrough = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@@ -260,6 +271,7 @@ class Style
$this->fontSize = $fontSize;
$this->hasSetFontSize = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@@ -291,6 +303,7 @@ class Style
$this->fontColor = $fontColor;
$this->hasSetFontColor = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@@ -320,6 +333,7 @@ class Style
$this->fontName = $fontName;
$this->hasSetFontName = true;
$this->shouldApplyFont = true;
$this->isEmpty = false;
return $this;
}
@@ -350,6 +364,7 @@ class Style
$this->cellAlignment = $cellAlignment;
$this->hasSetCellAlignment = true;
$this->shouldApplyCellAlignment = true;
$this->isEmpty = false;
return $this;
}
@@ -386,6 +401,7 @@ class Style
{
$this->shouldWrapText = $shouldWrap;
$this->hasSetWrapText = true;
$this->isEmpty = false;
return $this;
}
@@ -415,6 +431,7 @@ class Style
{
$this->hasSetBackgroundColor = true;
$this->backgroundColor = $color;
$this->isEmpty = false;
return $this;
}
@@ -444,6 +461,7 @@ class Style
{
$this->hasSetFormat = true;
$this->format = $format;
$this->isEmpty = false;
return $this;
}
@@ -463,4 +481,29 @@ class Style
{
return $this->hasSetFormat;
}
/**
* @return bool
*/
public function isRegistered() : bool
{
return $this->isRegistered;
}
public function markAsRegistered(?int $id) : void
{
$this->setId($id);
$this->isRegistered = true;
}
public function unmarkAsRegistered() : void
{
$this->setId(0);
$this->isRegistered = false;
}
public function isEmpty() : bool
{
return $this->isEmpty;
}
}
+34
View File
@@ -13,12 +13,20 @@ class StringHelper
/** @var bool Whether the mbstring extension is loaded */
protected $hasMbstringSupport;
/** @var bool Whether the code is running with PHP7 or older versions */
private $isRunningPhp7OrOlder;
/** @var array Locale info, used for number formatting */
private $localeInfo;
/**
*
*/
public function __construct()
{
$this->hasMbstringSupport = \extension_loaded('mbstring');
$this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0;
$this->localeInfo = \localeconv();
}
/**
@@ -68,4 +76,30 @@ class StringHelper
return ($position !== false) ? $position : -1;
}
/**
* Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format.
*
* Formatting of float values is locale dependent in PHP < 8.
* Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34).
* However, float values must be formatted with no thousands separator and a "." as decimal point
* to work properly. This method can be used to convert the value to the correct format before storing it.
*
* @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8.
*
* @param int|float $numericValue
* @return string
*/
public function formatNumericValue($numericValue)
{
if ($this->isRunningPhp7OrOlder && is_float($numericValue)) {
return str_replace(
[$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']],
['', '.'],
$numericValue
);
}
return $numericValue;
}
}
@@ -56,12 +56,25 @@ class RowManager
$rowCells = $row->getCells();
$maxCellIndex = $numCells;
// If the row has empty cells, calling "setCellAtIndex" will add the cell
// but in the wrong place (the new cell is added at the end of the array).
// Therefore, we need to sort the array using keys to have proper order.
// @see https://github.com/box/spout/issues/740
$needsSorting = false;
for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) {
if (!isset($rowCells[$cellIndex])) {
$row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex);
$needsSorting = true;
}
}
if ($needsSorting) {
$rowCells = $row->getCells();
ksort($rowCells);
$row->setCells($rowCells);
}
return $row;
}
}
@@ -31,14 +31,6 @@ class CellValueFormatter
/** Constants used for date formatting */
const NUM_SECONDS_IN_ONE_DAY = 86400;
const NUM_SECONDS_IN_ONE_HOUR = 3600;
const NUM_SECONDS_IN_ONE_MINUTE = 60;
/**
* February 29th, 1900 is NOT a leap year but Excel thinks it is...
* @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel
*/
const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60;
/** @var SharedStringsManager Manages shared strings */
protected $sharedStringsManager;
@@ -130,10 +122,15 @@ class CellValueFormatter
*/
protected function formatInlineStringCellValue($node)
{
// inline strings are formatted this way:
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
$cellValue = $this->escaper->unescape($tNode->nodeValue);
// inline strings are formatted this way (they can contain any number of <t> nodes):
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t><t>[INLINE_STRING_2]</t></is></c>
$tNodes = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE);
$cellValue = '';
for ($i = 0; $i < $tNodes->count(); $i++) {
$tNode = $tNodes->item($i);
$cellValue .= $this->escaper->unescape($tNode->nodeValue);
}
return $cellValue;
}
@@ -16,9 +16,6 @@ use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyInterface;
*/
class SharedStringsManager
{
/** Main namespace for the sharedStrings.xml file */
const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
/** Definition of XML nodes names used to parse data */
const XML_NODE_SST = 'sst';
const XML_NODE_SI = 'si';
@@ -17,10 +17,11 @@ class WorkbookRelationshipsManager
/** Path of workbook relationships XML file inside the XLSX file */
const WORKBOOK_RELS_XML_FILE_PATH = 'xl/_rels/workbook.xml.rels';
/** Relationships types */
/** Relationships types - For Transitional and Strict OOXML */
const RELATIONSHIP_TYPE_SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
const RELATIONSHIP_TYPE_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles';
const RELATIONSHIP_TYPE_WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
const RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings';
const RELATIONSHIP_TYPE_STYLES_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/styles';
/** Nodes and attributes used to find relevant information in the workbook relationships XML file */
const XML_NODE_RELATIONSHIP = 'Relationship';
@@ -52,7 +53,8 @@ class WorkbookRelationshipsManager
public function getSharedStringsXMLFilePath()
{
$workbookRelationships = $this->getWorkbookRelationships();
$sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS];
$sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]
?? $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT];
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
$doesContainBasePath = (\strpos($sharedStringsXMLFilePath, self::BASE_PATH) !== false);
@@ -71,7 +73,8 @@ class WorkbookRelationshipsManager
{
$workbookRelationships = $this->getWorkbookRelationships();
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]);
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS])
|| isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT]);
}
/**
@@ -81,7 +84,8 @@ class WorkbookRelationshipsManager
{
$workbookRelationships = $this->getWorkbookRelationships();
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]);
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES])
|| isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT]);
}
/**
@@ -90,7 +94,8 @@ class WorkbookRelationshipsManager
public function getStylesXMLFilePath()
{
$workbookRelationships = $this->getWorkbookRelationships();
$stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES];
$stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]
?? $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT];
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
$doesContainBasePath = (\strpos($stylesXMLFilePath, self::BASE_PATH) !== false);
@@ -0,0 +1,38 @@
<?php
namespace Box\Spout\Writer\Common\Manager;
use Box\Spout\Common\Entity\Style\Style;
/**
* Class RegisteredStyle
* Allow to know if this style must replace actual row style.
*/
class RegisteredStyle
{
/**
* @var Style
*/
private $style;
/**
* @var bool
*/
private $isMatchingRowStyle;
public function __construct(Style $style, bool $isMatchingRowStyle)
{
$this->style = $style;
$this->isMatchingRowStyle = $isMatchingRowStyle;
}
public function getStyle() : Style
{
return $this->style;
}
public function isMatchingRowStyle() : bool
{
return $this->isMatchingRowStyle;
}
}
@@ -0,0 +1,32 @@
<?php
namespace Box\Spout\Writer\Common\Manager\Style;
use Box\Spout\Common\Entity\Style\Style;
/**
* Class PossiblyUpdatedStyle
* Indicates if style is updated.
* It allow to know if style registration must be done.
*/
class PossiblyUpdatedStyle
{
private $style;
private $isUpdated;
public function __construct(Style $style, bool $isUpdated)
{
$this->style = $style;
$this->isUpdated = $isUpdated;
}
public function getStyle() : Style
{
return $this->style;
}
public function isUpdated() : bool
{
return $this->isUpdated;
}
}
@@ -50,13 +50,11 @@ class StyleManager implements StyleManagerInterface
* Typically, set "wrap text" if a cell contains a new line.
*
* @param Cell $cell
* @return Style
* @return PossiblyUpdatedStyle The eventually updated style
*/
public function applyExtraStylesIfNeeded(Cell $cell)
public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle
{
$updatedStyle = $this->applyWrapTextIfCellContainsNewLine($cell);
return $updatedStyle;
return $this->applyWrapTextIfCellContainsNewLine($cell);
}
/**
@@ -69,21 +67,19 @@ class StyleManager implements StyleManagerInterface
* on the Windows version of Excel...
*
* @param Cell $cell The cell the style should be applied to
* @return \Box\Spout\Common\Entity\Style\Style The eventually updated style
* @return PossiblyUpdatedStyle The eventually updated style
*/
protected function applyWrapTextIfCellContainsNewLine(Cell $cell)
protected function applyWrapTextIfCellContainsNewLine(Cell $cell) : PossiblyUpdatedStyle
{
$cellStyle = $cell->getStyle();
// if the "wrap text" option is already set, no-op
if ($cellStyle->hasSetWrapText()) {
return $cellStyle;
}
if ($cell->isString() && \strpos($cell->getValue(), "\n") !== false) {
if (!$cellStyle->hasSetWrapText() && $cell->isString() && \strpos($cell->getValue(), "\n") !== false) {
$cellStyle->setShouldWrapText();
return new PossiblyUpdatedStyle($cellStyle, true);
}
return $cellStyle;
return new PossiblyUpdatedStyle($cellStyle, false);
}
}
@@ -24,7 +24,7 @@ interface StyleManagerInterface
* Typically, set "wrap text" if a cell contains a new line.
*
* @param Cell $cell
* @return Style The updated style
* @return PossiblyUpdatedStyle The eventually updated style
*/
public function applyExtraStylesIfNeeded(Cell $cell);
public function applyExtraStylesIfNeeded(Cell $cell) : PossiblyUpdatedStyle;
}
@@ -36,9 +36,9 @@ class StyleRegistry
{
$serializedStyle = $this->serialize($style);
if (!$this->hasStyleAlreadyBeenRegistered($style)) {
if (!$this->hasSerializedStyleAlreadyBeenRegistered($serializedStyle)) {
$nextStyleId = \count($this->serializedStyleToStyleIdMappingTable);
$style->setId($nextStyleId);
$style->markAsRegistered($nextStyleId);
$this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId;
$this->styleIdToStyleMappingTable[$nextStyleId] = $style;
@@ -48,15 +48,13 @@ class StyleRegistry
}
/**
* Returns whether the given style has already been registered.
* Returns whether the serialized style has already been registered.
*
* @param Style $style
* @param string $serializedStyle The serialized style
* @return bool
*/
protected function hasStyleAlreadyBeenRegistered(Style $style)
protected function hasSerializedStyleAlreadyBeenRegistered(string $serializedStyle)
{
$serializedStyle = $this->serialize($style);
// Using isset here because it is way faster than array_key_exists...
return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]);
}
@@ -101,13 +99,13 @@ class StyleRegistry
*/
public function serialize(Style $style)
{
// In order to be able to properly compare style, set static ID value
// In order to be able to properly compare style, set static ID value and reset registration
$currentId = $style->getId();
$style->setId(0);
$style->unmarkAsRegistered();
$serializedStyle = \serialize($style);
$style->setId($currentId);
$style->markAsRegistered($currentId);
return $serializedStyle;
}
@@ -22,6 +22,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
*/
public function registerStyle(Style $style)
{
if ($style->isRegistered()) {
return $style;
}
$registeredStyle = parent::registerStyle($style);
$this->usedFontsSet[$style->getFontName()] = true;
@@ -10,6 +10,7 @@ use Box\Spout\Common\Exception\IOException;
use Box\Spout\Common\Helper\Escaper\ODS as ODSEscaper;
use Box\Spout\Common\Helper\StringHelper;
use Box\Spout\Writer\Common\Entity\Worksheet;
use Box\Spout\Writer\Common\Manager\RegisteredStyle;
use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
use Box\Spout\Writer\ODS\Manager\Style\StyleManager;
@@ -93,7 +94,7 @@ class WorksheetManager implements WorksheetManagerInterface
$escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());
$tableStyleName = 'ta' . ($externalSheet->getIndex() + 1);
$tableElement = '<table:table table:style-name="' . $tableStyleName . '" table:name="' . $escapedSheetName . '">';
$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() . '"/>';
return $tableElement;
@@ -104,8 +105,8 @@ class WorksheetManager implements WorksheetManagerInterface
*
* @param Worksheet $worksheet The worksheet to add the row to
* @param Row $row The row to be added
* @throws IOException If the data cannot be written
* @throws InvalidArgumentException If a cell value's type is not supported
* @throws IOException If the data cannot be written
* @return void
*/
public function addRow(Worksheet $worksheet, Row $row)
@@ -125,7 +126,13 @@ class WorksheetManager implements WorksheetManagerInterface
$nextCell = isset($cells[$nextCellIndex]) ? $cells[$nextCellIndex] : null;
if ($nextCell === null || $cell->getValue() !== $nextCell->getValue()) {
$data .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $currentCellIndex, $nextCellIndex);
$registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
$cellStyle = $registeredStyle->getStyle();
if ($registeredStyle->isMatchingRowStyle()) {
$rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
}
$data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex);
$currentCellIndex = $nextCellIndex;
}
@@ -146,24 +153,46 @@ class WorksheetManager implements WorksheetManagerInterface
/**
* Applies styles to the given style, merging the cell's style with its row's style
* Then builds and returns xml for the cell.
*
* @param Cell $cell
* @param Style $rowStyle
* @param int $currentCellIndex
* @param int $nextCellIndex
* @throws InvalidArgumentException If a cell value's type is not supported
* @return string
* @return RegisteredStyle
*/
private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $currentCellIndex, $nextCellIndex)
private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
{
// Apply row and extra styles
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
$isMatchingRowStyle = false;
if ($cell->getStyle()->isEmpty()) {
$cell->setStyle($rowStyle);
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
$styleIndex = $registeredStyle->getId() + 1; // 1-based
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
if ($possiblyUpdatedStyle->isUpdated()) {
$registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
} else {
$registeredStyle = $this->styleManager->registerStyle($rowStyle);
$isMatchingRowStyle = true;
}
} else {
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
if ($possiblyUpdatedStyle->isUpdated()) {
$newCellStyle = $possiblyUpdatedStyle->getStyle();
} else {
$newCellStyle = $mergedCellAndRowStyle;
}
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
}
return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
}
private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex) : string
{
$styleIndex = $style->getId() + 1; // 1-based
$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
@@ -202,8 +231,9 @@ class WorksheetManager implements WorksheetManagerInterface
$data .= '<text:p>' . $cell->getValue() . '</text:p>';
$data .= '</table:table-cell>';
} elseif ($cell->isNumeric()) {
$data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cell->getValue() . '">';
$data .= '<text:p>' . $cell->getValue() . '</text:p>';
$cellValue = $this->stringHelper->formatNumericValue($cell->getValue());
$data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">';
$data .= '<text:p>' . $cellValue . '</text:p>';
$data .= '</table:table-cell>';
} elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
// only writes the error value if it's a string
+19 -2
View File
@@ -123,9 +123,26 @@ abstract class WriterAbstract implements WriterInterface
// @see https://github.com/box/spout/issues/241
$this->globalFunctionsHelper->ob_end_clean();
// Set headers
/*
* Set headers
*
* For newer browsers such as Firefox, Chrome, Opera, Safari, etc., they all support and use `filename*`
* specified by the new standard, even if they do not automatically decode filename; it does not matter;
* and for older versions of Internet Explorer, they are not recognized `filename*`, will automatically
* ignore it and use the old `filename` (the only minor flaw is that there must be an English suffix name).
* In this way, the multi-browser multi-language compatibility problem is perfectly solved, which does not
* require UA judgment and is more in line with the standard.
*
* @see https://github.com/box/spout/issues/745
* @see https://tools.ietf.org/html/rfc6266
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
*/
$this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType);
$this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"');
$this->globalFunctionsHelper->header(
'Content-Disposition: attachment; ' .
'filename="' . rawurldecode($this->outputFilePath) . '"; ' .
'filename*=UTF-8\'\'' . rawurldecode($this->outputFilePath)
);
/*
* When forcing the download of a file over SSL,IE8 and lower browsers fail
@@ -119,6 +119,10 @@ class StyleRegistry extends \Box\Spout\Writer\Common\Manager\Style\StyleRegistry
*/
public function registerStyle(Style $style)
{
if ($style->isRegistered()) {
return $style;
}
$registeredStyle = parent::registerStyle($style);
$this->registerFill($registeredStyle);
$this->registerFormat($registeredStyle);
@@ -14,6 +14,7 @@ use Box\Spout\Writer\Common\Creator\InternalEntityFactory;
use Box\Spout\Writer\Common\Entity\Options;
use Box\Spout\Writer\Common\Entity\Worksheet;
use Box\Spout\Writer\Common\Helper\CellHelper;
use Box\Spout\Writer\Common\Manager\RegisteredStyle;
use Box\Spout\Writer\Common\Manager\RowManager;
use Box\Spout\Writer\Common\Manager\Style\StyleMerger;
use Box\Spout\Writer\Common\Manager\WorksheetManagerInterface;
@@ -160,7 +161,12 @@ EOD;
$rowXML = '<row r="' . $rowIndexOneBased . '" spans="1:' . $numCells . '">';
foreach ($row->getCells() as $columnIndexZeroBased => $cell) {
$rowXML .= $this->applyStyleAndGetCellXML($cell, $rowStyle, $rowIndexOneBased, $columnIndexZeroBased);
$registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);
$cellStyle = $registeredStyle->getStyle();
if ($registeredStyle->isMatchingRowStyle()) {
$rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)
}
$rowXML .= $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $cellStyle->getId());
}
$rowXML .= '</row>';
@@ -173,26 +179,43 @@ EOD;
/**
* Applies styles to the given style, merging the cell's style with its row's style
* Then builds and returns xml for the cell.
*
* @param Cell $cell
* @param Style $rowStyle
* @param int $rowIndexOneBased
* @param int $columnIndexZeroBased
*
* @throws InvalidArgumentException If the given value cannot be processed
* @return string
* @return RegisteredStyle
*/
private function applyStyleAndGetCellXML(Cell $cell, Style $rowStyle, $rowIndexOneBased, $columnIndexZeroBased)
private function applyStyleAndRegister(Cell $cell, Style $rowStyle) : RegisteredStyle
{
// Apply row and extra styles
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$newCellStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
$isMatchingRowStyle = false;
if ($cell->getStyle()->isEmpty()) {
$cell->setStyle($rowStyle);
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
return $this->getCellXML($rowIndexOneBased, $columnIndexZeroBased, $cell, $registeredStyle->getId());
if ($possiblyUpdatedStyle->isUpdated()) {
$registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());
} else {
$registeredStyle = $this->styleManager->registerStyle($rowStyle);
$isMatchingRowStyle = true;
}
} else {
$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);
$cell->setStyle($mergedCellAndRowStyle);
$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);
if ($possiblyUpdatedStyle->isUpdated()) {
$newCellStyle = $possiblyUpdatedStyle->getStyle();
} else {
$newCellStyle = $mergedCellAndRowStyle;
}
$registeredStyle = $this->styleManager->registerStyle($newCellStyle);
}
return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);
}
/**
@@ -217,7 +240,7 @@ EOD;
} elseif ($cell->isBoolean()) {
$cellXML .= ' t="b"><v>' . (int) ($cell->getValue()) . '</v></c>';
} elseif ($cell->isNumeric()) {
$cellXML .= '><v>' . $cell->getValue() . '</v></c>';
$cellXML .= '><v>' . $this->stringHelper->formatNumericValue($cell->getValue()) . '</v></c>';
} elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
// only writes the error value if it's a string
$cellXML .= ' t="e"><v>' . $cell->getValueEvenIfError() . '</v></c>';
@@ -14,7 +14,7 @@ class OptionsManagerTest extends TestCase
*/
protected $optionsManager;
protected function setUp(): void
protected function setUp() : void
{
$this->optionsManager = new class() extends OptionsManagerAbstract {
protected function getSupportedOptions()
@@ -186,18 +186,22 @@ class CellValueFormatterTest extends TestCase
public function testFormatInlineStringCellValue($value, $expectedFormattedValue)
{
$nodeListMock = $this->createMock(\DOMNodeList::class);
$nodeListMock
->expects($this->atLeastOnce())
->method('count')
->willReturn(1);
$nodeListMock
->expects($this->atLeastOnce())
->method('item')
->with(0)
->will($this->returnValue((object) ['nodeValue' => $value]));
->willReturn((object) ['nodeValue' => $value]);
$nodeMock = $this->createMock(\DOMElement::class);
$nodeMock
->expects($this->atLeastOnce())
->method('getElementsByTagName')
->with(CellValueFormatter::XML_NODE_INLINE_STRING_VALUE)
->will($this->returnValue($nodeListMock));
->willReturn($nodeListMock);
$formatter = new CellValueFormatter(null, null, false, false, new Escaper\XLSX());
$formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatInlineStringCellValue', $nodeMock);
@@ -25,7 +25,7 @@ class SharedStringsManagerTest extends TestCase
/**
* @return void
*/
public function setUp(): void
public function setUp() : void
{
$this->sharedStringsManager = null;
}
@@ -33,7 +33,7 @@ class SharedStringsManagerTest extends TestCase
/**
* @return void
*/
public function tearDown(): void
public function tearDown() : void
{
if ($this->sharedStringsManager !== null) {
$this->sharedStringsManager->cleanup();
+42 -1
View File
@@ -72,6 +72,19 @@ class ReaderTest extends TestCase
}
}
/**
* @return void
*/
public function testReadShouldSupportInlineStringsWithMultipleValueNodes()
{
$allRows = $this->getAllRowsForFile('sheet_with_multiple_value_nodes_in_inline_strings.xlsx');
$expectedRows = [
['VALUE 1 VALUE 2 VALUE 3 VALUE 4', 's1 - B1'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
@@ -679,13 +692,29 @@ class ReaderTest extends TestCase
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells.xlsx');
$expectedRows = [
['A', 'B', 'C'],
['A', '', 'C'],
['0', '', ''],
['1', '1', ''],
];
$this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length');
}
/**
* https://github.com/box/spout/issues/184
* @return void
*/
public function testReadShouldCreateOutputEmptyCellPreservedWhenNoDimensionsSpecified()
{
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells_without_dimensions.xlsx');
$expectedRows = [
['A', '', 'C'],
['0'],
['1', '1'],
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* https://github.com/box/spout/issues/195
* @return void
@@ -703,6 +732,18 @@ class ReaderTest extends TestCase
$this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed');
}
/**
* https://github.com/box/spout/issues/726
* @return void
*/
public function testReaderShouldSupportStrictOOXML()
{
$allRows = $this->getAllRowsForFile('sheet_with_strict_ooxml.xlsx');
$this->assertEquals('UNIQUE_ACCOUNT_IDENTIFIER', $allRows[0][0]);
$this->assertEquals('A2Z34NJA7N2ESJ', $allRows[1][0]);
}
/**
* @param string $fileName
* @param bool $shouldFormatDates
+1 -1
View File
@@ -61,7 +61,7 @@ class WriterPerfTest extends TestCase
*/
private function getNumWrittenRows($resourcePath)
{
$lineCountResult = `wc -l $resourcePath`;
$lineCountResult = shell_exec("wc -l $resourcePath");
return (int) $lineCountResult;
}
@@ -18,7 +18,7 @@ class SheetTest extends TestCase
/**
* @return void
*/
public function setUp(): void
public function setUp() : void
{
$this->sheetManager = new SheetManager(new StringHelper());
}
@@ -14,7 +14,7 @@ class StyleManagerTest extends TestCase
/**
* @return StyleManager
*/
private function getStyleManager()
private function getStyleManager() : StyleManager
{
$style = (new StyleBuilder())->build();
$styleRegistry = new StyleRegistry($style);
@@ -22,31 +22,37 @@ class StyleManagerTest extends TestCase
return new StyleManager($styleRegistry);
}
/**
* @return void
*/
public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine()
public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine() : void
{
$style = (new StyleBuilder())->build();
$this->assertFalse($style->shouldWrapText());
$styleManager = $this->getStyleManager();
$updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style));
$possiblyUpdatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style));
$this->assertTrue($updatedStyle->shouldWrapText());
$this->assertTrue($possiblyUpdatedStyle->isUpdated());
$this->assertTrue($possiblyUpdatedStyle->getStyle()->shouldWrapText());
}
/**
* @return void
*/
public function testApplyExtraStylesIfNeededShouldDoNothingIfWrapTextAlreadyApplied()
public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextNotNeeded() : void
{
$style = (new StyleBuilder())->build();
$this->assertFalse($style->shouldWrapText());
$styleManager = $this->getStyleManager();
$possiblyUpdatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell('oneline', $style));
$this->assertFalse($possiblyUpdatedStyle->isUpdated());
}
public function testApplyExtraStylesIfNeededShouldReturnNullIfWrapTextAlreadyApplied() : void
{
$style = (new StyleBuilder())->setShouldWrapText()->build();
$this->assertTrue($style->shouldWrapText());
$styleManager = $this->getStyleManager();
$updatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style));
$possiblyUpdatedStyle = $styleManager->applyExtraStylesIfNeeded(new Cell("multi\nlines", $style));
$this->assertTrue($updatedStyle->shouldWrapText());
$this->assertFalse($possiblyUpdatedStyle->isUpdated());
}
}
@@ -18,7 +18,7 @@ class StyleMergerTest extends TestCase
/**
* @return void
*/
public function setUp(): void
public function setUp() : void
{
$this->styleMerger = new StyleMerger();
}
@@ -20,7 +20,7 @@ class StyleRegistryTest extends TestCase
/**
* @return void
*/
public function setUp(): void
public function setUp() : void
{
$this->defaultStyle = (new StyleBuilder())->build();
$this->styleRegistry = new StyleRegistry($this->defaultStyle);
+1 -1
View File
@@ -88,7 +88,7 @@ class WriterPerfTest extends TestCase
copy($pathToContentXmlFile, $tmpFile);
// Get the last 200 characters
$lastCharacters = `tail -c 200 $tmpFile`;
$lastCharacters = shell_exec("tail -c 200 $tmpFile");
// remove the temporary file
unlink($tmpFile);
+35
View File
@@ -279,6 +279,41 @@ class WriterTest extends TestCase
$this->assertValueWasWrittenToSheet($fileName, 1, 10.2);
}
/**
* @return void
*/
public function testAddRowShouldSupportFloatValuesInDifferentLocale()
{
$previousLocale = \setlocale(LC_ALL, 0);
try {
// Pick a supported locale whose decimal point is a comma.
// Installed locales differ from one system to another, so we can't pick
// a given locale.
$supportedLocales = explode("\n", shell_exec('locale -a'));
foreach ($supportedLocales as $supportedLocale) {
\setlocale(LC_ALL, $supportedLocale);
if (\localeconv()['decimal_point'] === ',') {
break;
}
}
$this->assertEquals(',', \localeconv()['decimal_point']);
$fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx';
$dataRows = $this->createRowsFromValues([
[1234.5],
]);
$this->writeToODSFile($dataRows, $fileName);
$this->assertValueWasNotWrittenToSheet($fileName, 1, "1234,5");
$this->assertValueWasWrittenToSheet($fileName, 1, "1234.5");
} finally {
// reset locale
\setlocale(LC_ALL, $previousLocale);
}
}
/**
* @return array
*/
@@ -30,7 +30,7 @@ class WriterWithStyleTest extends TestCase
/**
* @return void
*/
public function setUp(): void
public function setUp() : void
{
$this->defaultStyle = (new StyleBuilder())->build();
}
+1 -1
View File
@@ -135,7 +135,7 @@ class WriterPerfTest extends TestCase
copy($filePath, $tmpFile);
// Get the last 200 characters
$lastCharacters = `tail -c $numCharacters $tmpFile`;
$lastCharacters = shell_exec("tail -c $numCharacters $tmpFile");
// remove the temporary file
unlink($tmpFile);
+36
View File
@@ -393,6 +393,42 @@ class WriterTest extends TestCase
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 't="e"><v>#DIV/0</v>');
}
/**
* @return void
*/
public function testAddRowShouldSupportFloatValuesInDifferentLocale()
{
$previousLocale = \setlocale(LC_ALL, 0);
$valueToWrite = 1234.5; // needs to be defined before changing the locale as PHP8 would expect 1234,5
try {
// Pick a supported locale whose decimal point is a comma.
// Installed locales differ from one system to another, so we can't pick
// a given locale.
$supportedLocales = explode("\n", shell_exec('locale -a'));
foreach ($supportedLocales as $supportedLocale) {
\setlocale(LC_ALL, $supportedLocale);
if (\localeconv()['decimal_point'] === ',') {
break;
}
}
$this->assertEquals(',', \localeconv()['decimal_point']);
$fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx';
$dataRows = $this->createRowsFromValues([
[$valueToWrite],
]);
$this->writeToXLSXFile($dataRows, $fileName, $shouldUseInlineStrings = false);
$this->assertInlineDataWasNotWrittenToSheet($fileName, 1, "1234,5");
$this->assertInlineDataWasWrittenToSheet($fileName, 1, "1234.5");
} finally {
// reset locale
\setlocale(LC_ALL, $previousLocale);
}
}
/**
* @return void
*/
@@ -32,7 +32,7 @@ class WriterWithStyleTest extends TestCase
/**
* @return void
*/
public function setUp(): void
public function setUp() : void
{
$this->defaultStyle = (new StyleBuilder())->build();
}
Binary file not shown.
Binary file not shown.