Merge branch 'MDL-84161-main' of https://github.com/meirzamoodle/moodle
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
],
|
||||
"homepage": "https://github.com/openspout/openspout",
|
||||
"require": {
|
||||
"php": "~8.1.0 || ~8.2.0 || ~8.3.0",
|
||||
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-filter": "*",
|
||||
@@ -38,13 +38,13 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-zlib": "*",
|
||||
"friendsofphp/php-cs-fixer": "^3.46.0",
|
||||
"infection/infection": "^0.27.9",
|
||||
"phpbench/phpbench": "^1.2.15",
|
||||
"phpstan/phpstan": "^1.10.55",
|
||||
"phpstan/phpstan-phpunit": "^1.3.15",
|
||||
"phpstan/phpstan-strict-rules": "^1.5.2",
|
||||
"phpunit/phpunit": "^10.5.5"
|
||||
"friendsofphp/php-cs-fixer": "^3.68.3",
|
||||
"infection/infection": "^0.29.10",
|
||||
"phpbench/phpbench": "^1.4.0",
|
||||
"phpstan/phpstan": "^2.1.2",
|
||||
"phpstan/phpstan-phpunit": "^2.0.4",
|
||||
"phpstan/phpstan-strict-rules": "^2",
|
||||
"phpunit/phpunit": "^11.5.4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)",
|
||||
|
||||
@@ -6,5 +6,5 @@ Downloaded from: https://github.com/openspout/openspout
|
||||
Import procedure:
|
||||
|
||||
* Copy all the files from the folder 'src' directory.
|
||||
* Copy the LICENSE & README.md files from the project root.
|
||||
* Copy the LICENSE, README.md and composer.json files from the root of the project.
|
||||
* Update lib/thirdpartylibs.xml with the latest version.
|
||||
|
||||
@@ -14,7 +14,7 @@ final class FormulaCell extends Cell
|
||||
public function __construct(
|
||||
private readonly string $value,
|
||||
?Style $style,
|
||||
private readonly null|DateInterval|DateTimeImmutable|float|int|string $computedValue = null,
|
||||
private readonly null|bool|DateInterval|DateTimeImmutable|float|int|string $computedValue = null,
|
||||
) {
|
||||
parent::__construct($style);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ final class FormulaCell extends Cell
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getComputedValue(): null|DateInterval|DateTimeImmutable|float|int|string
|
||||
public function getComputedValue(): null|bool|DateInterval|DateTimeImmutable|float|int|string
|
||||
{
|
||||
return $this->computedValue;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,19 @@ final class Row
|
||||
return new self($cells, $rowStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, null|bool|DateInterval|DateTimeInterface|float|int|string> $cellValues
|
||||
* @param array<array-key, Style> $columnStyles
|
||||
*/
|
||||
public static function fromValuesWithStyles(array $cellValues = [], ?Style $rowStyle = null, array $columnStyles = []): self
|
||||
{
|
||||
$cells = array_map(static function (null|bool|DateInterval|DateTimeInterface|float|int|string $cellValue, int|string $key) use ($columnStyles): Cell {
|
||||
return Cell::fromValue($cellValue, $columnStyles[$key] ?? null);
|
||||
}, $cellValues, array_keys($cellValues));
|
||||
|
||||
return new self($cells, $rowStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Cell[] $cells
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,7 @@ final class Border
|
||||
public const WIDTH_THICK = 'thick';
|
||||
|
||||
/** @var array<string, BorderPart> */
|
||||
private array $parts;
|
||||
private array $parts = [];
|
||||
|
||||
public function __construct(BorderPart ...$borderParts)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ use OpenSpout\Writer\Exception\Border\InvalidNameException;
|
||||
use OpenSpout\Writer\Exception\Border\InvalidStyleException;
|
||||
use OpenSpout\Writer\Exception\Border\InvalidWidthException;
|
||||
|
||||
final class BorderPart
|
||||
final readonly class BorderPart
|
||||
{
|
||||
public const allowedStyles = [
|
||||
Border::STYLE_NONE,
|
||||
@@ -31,10 +31,10 @@ final class BorderPart
|
||||
Border::WIDTH_THICK,
|
||||
];
|
||||
|
||||
private readonly string $style;
|
||||
private readonly string $name;
|
||||
private readonly string $color;
|
||||
private readonly string $width;
|
||||
private string $style;
|
||||
private string $name;
|
||||
private string $color;
|
||||
private string $width;
|
||||
|
||||
/**
|
||||
* @param string $name @see BorderPart::allowedNames
|
||||
|
||||
@@ -90,6 +90,12 @@ final class Style
|
||||
/** @var bool Whether the wrap text property was set */
|
||||
private bool $hasSetWrapText = false;
|
||||
|
||||
/** @var int Text rotation */
|
||||
private int $textRotation = 0;
|
||||
|
||||
/** @var bool Whether the text rotation property was set */
|
||||
private bool $hasSetTextRotation = false;
|
||||
|
||||
/** @var bool Whether the cell should shrink to fit to content */
|
||||
private bool $shouldShrinkToFit = false;
|
||||
|
||||
@@ -383,6 +389,28 @@ final class Style
|
||||
return $this->hasSetWrapText;
|
||||
}
|
||||
|
||||
public function textRotation(): int
|
||||
{
|
||||
return $this->textRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $rotation Rotate text
|
||||
*/
|
||||
public function setTextRotation(int $rotation): self
|
||||
{
|
||||
$this->textRotation = $rotation;
|
||||
$this->hasSetTextRotation = true;
|
||||
$this->isEmpty = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasSetTextRotation(): bool
|
||||
{
|
||||
return $this->hasSetTextRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether specific font properties should be applied
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@ use OpenSpout\Common\Exception\EncodingConversionException;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EncodingHelper
|
||||
final readonly class EncodingHelper
|
||||
{
|
||||
/**
|
||||
* Definition of the encodings that can have a BOM.
|
||||
@@ -33,9 +33,9 @@ final class EncodingHelper
|
||||
/** @var array<string, string> Map representing the encodings supporting BOMs (key) and their associated BOM (value) */
|
||||
private array $supportedEncodingsWithBom;
|
||||
|
||||
private readonly bool $canUseIconv;
|
||||
private bool $canUseIconv;
|
||||
|
||||
private readonly bool $canUseMbString;
|
||||
private bool $canUseMbString;
|
||||
|
||||
public function __construct(bool $canUseIconv, bool $canUseMbString)
|
||||
{
|
||||
|
||||
@@ -107,7 +107,7 @@ final class XLSX implements EscaperInterface
|
||||
$character = \chr($charValue);
|
||||
if (1 === preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
|
||||
$charHexValue = dechex($charValue);
|
||||
$escapedChar = '_x'.sprintf('%04s', strtoupper($charHexValue)).'_';
|
||||
$escapedChar = '_x'.\sprintf('%04s', strtoupper($charHexValue)).'_';
|
||||
$controlCharactersEscapingMap[$escapedChar] = $character;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ use RecursiveIteratorIterator;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class FileSystemHelper implements FileSystemHelperInterface
|
||||
final readonly class FileSystemHelper implements FileSystemHelperInterface
|
||||
{
|
||||
/** @var string Real path of the base folder where all the I/O can occur */
|
||||
private readonly string $baseFolderRealPath;
|
||||
private string $baseFolderRealPath;
|
||||
|
||||
/**
|
||||
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
|
||||
|
||||
@@ -7,10 +7,10 @@ namespace OpenSpout\Common\Helper;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class StringHelper
|
||||
final readonly class StringHelper
|
||||
{
|
||||
/** @var bool Whether the mbstring extension is loaded */
|
||||
private readonly bool $hasMbstringSupport;
|
||||
private bool $hasMbstringSupport;
|
||||
|
||||
public function __construct(bool $hasMbstringSupport)
|
||||
{
|
||||
|
||||
@@ -134,7 +134,7 @@ final class RowIterator implements RowIteratorInterface
|
||||
|
||||
if (false !== $rowData) {
|
||||
// array_map will replace NULL values by empty strings
|
||||
$rowDataBufferAsArray = array_map('\\strval', $rowData);
|
||||
$rowDataBufferAsArray = array_map('\strval', $rowData);
|
||||
$this->rowBuffer = new Row(array_map(static function ($cellValue) {
|
||||
return Cell::fromValue($cellValue);
|
||||
}, $rowDataBufferAsArray), null);
|
||||
|
||||
@@ -9,10 +9,10 @@ use OpenSpout\Reader\SheetInterface;
|
||||
/**
|
||||
* @implements SheetInterface<RowIterator>
|
||||
*/
|
||||
final class Sheet implements SheetInterface
|
||||
final readonly class Sheet implements SheetInterface
|
||||
{
|
||||
/** @var RowIterator To iterate over the CSV's rows */
|
||||
private readonly RowIterator $rowIterator;
|
||||
private RowIterator $rowIterator;
|
||||
|
||||
/**
|
||||
* @param RowIterator $rowIterator Corresponding row iterator
|
||||
|
||||
@@ -7,15 +7,15 @@ namespace OpenSpout\Reader\Common;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ColumnWidth
|
||||
final readonly class ColumnWidth
|
||||
{
|
||||
/**
|
||||
* @param positive-int $start
|
||||
* @param positive-int $end
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $start,
|
||||
public readonly int $end,
|
||||
public readonly float $width,
|
||||
public int $start,
|
||||
public int $end,
|
||||
public float $width,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use OpenSpout\Reader\Exception\InvalidValueException;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class CellValueFormatter
|
||||
final readonly class CellValueFormatter
|
||||
{
|
||||
/**
|
||||
* Definition of all possible cell types.
|
||||
@@ -61,10 +61,10 @@ final class CellValueFormatter
|
||||
];
|
||||
|
||||
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
|
||||
private readonly bool $shouldFormatDates;
|
||||
private bool $shouldFormatDates;
|
||||
|
||||
/** @var ODS Used to unescape XML data */
|
||||
private readonly ODS $escaper;
|
||||
private ODS $escaper;
|
||||
|
||||
/**
|
||||
* @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings
|
||||
|
||||
@@ -37,8 +37,8 @@ final class RowIterator implements RowIteratorInterface
|
||||
/** @var XMLProcessor Helper Object to process XML nodes */
|
||||
private readonly XMLProcessor $xmlProcessor;
|
||||
|
||||
/** @var Helper\CellValueFormatter Helper to format cell values */
|
||||
private readonly Helper\CellValueFormatter $cellValueFormatter;
|
||||
/** @var CellValueFormatter Helper to format cell values */
|
||||
private readonly CellValueFormatter $cellValueFormatter;
|
||||
|
||||
/** @var bool Whether the iterator has already been rewound once */
|
||||
private bool $hasAlreadyBeenRewound = false;
|
||||
|
||||
@@ -9,22 +9,22 @@ use OpenSpout\Reader\SheetWithVisibilityInterface;
|
||||
/**
|
||||
* @implements SheetWithVisibilityInterface<RowIterator>
|
||||
*/
|
||||
final class Sheet implements SheetWithVisibilityInterface
|
||||
final readonly class Sheet implements SheetWithVisibilityInterface
|
||||
{
|
||||
/** @var RowIterator To iterate over sheet's rows */
|
||||
private readonly RowIterator $rowIterator;
|
||||
private RowIterator $rowIterator;
|
||||
|
||||
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||
private readonly int $index;
|
||||
private int $index;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
private readonly string $name;
|
||||
private string $name;
|
||||
|
||||
/** @var bool Whether the sheet was the active one */
|
||||
private readonly bool $isActive;
|
||||
private bool $isActive;
|
||||
|
||||
/** @var bool Whether the sheet is visible */
|
||||
private readonly bool $isVisible;
|
||||
private bool $isVisible;
|
||||
|
||||
/**
|
||||
* @param RowIterator $rowIterator The corresponding row iterator
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Reader;
|
||||
|
||||
/**
|
||||
* @template T of RowIteratorInterface
|
||||
*
|
||||
* @extends SheetInterface<T>
|
||||
*/
|
||||
interface SheetWithMergeCellsInterface extends SheetInterface
|
||||
{
|
||||
/**
|
||||
* @return list<string> Merge cells list ["C7:E7", "A9:D10"]
|
||||
*/
|
||||
public function getMergeCells(): array;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Reader\Wrapper;
|
||||
|
||||
use OpenSpout\Common\Exception\IOException;
|
||||
use OpenSpout\Reader\Exception\XMLProcessingException;
|
||||
use ZipArchive;
|
||||
|
||||
@@ -52,7 +53,12 @@ final class XMLReader extends \XMLReader
|
||||
// The file path should not start with a '/', otherwise it won't be found
|
||||
$fileInsideZipPathWithoutLeadingSlash = ltrim($fileInsideZipPath, '/');
|
||||
|
||||
return self::ZIP_WRAPPER.realpath($zipFilePath).'#'.$fileInsideZipPathWithoutLeadingSlash;
|
||||
$realpath = realpath($zipFilePath);
|
||||
if (false === $realpath) {
|
||||
throw new IOException("Could not open {$zipFilePath} for reading! File does not exist.");
|
||||
}
|
||||
|
||||
return self::ZIP_WRAPPER.$realpath.'#'.$fileInsideZipPathWithoutLeadingSlash;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ use OpenSpout\Reader\XLSX\Manager\StyleManagerInterface;
|
||||
/**
|
||||
* This class provides helper functions to format cell values.
|
||||
*/
|
||||
final class CellValueFormatter
|
||||
final readonly class CellValueFormatter
|
||||
{
|
||||
/**
|
||||
* Definition of all possible cell types.
|
||||
@@ -49,19 +49,19 @@ final class CellValueFormatter
|
||||
public const NUM_SECONDS_IN_ONE_DAY = 86400;
|
||||
|
||||
/** @var SharedStringsManager Manages shared strings */
|
||||
private readonly SharedStringsManager $sharedStringsManager;
|
||||
private SharedStringsManager $sharedStringsManager;
|
||||
|
||||
/** @var StyleManagerInterface Manages styles */
|
||||
private readonly StyleManagerInterface $styleManager;
|
||||
private StyleManagerInterface $styleManager;
|
||||
|
||||
/** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */
|
||||
private readonly bool $shouldFormatDates;
|
||||
private bool $shouldFormatDates;
|
||||
|
||||
/** @var bool Whether date/time values should use a calendar starting in 1904 instead of 1900 */
|
||||
private readonly bool $shouldUse1904Dates;
|
||||
private bool $shouldUse1904Dates;
|
||||
|
||||
/** @var XLSX Used to unescape XML data */
|
||||
private readonly XLSX $escaper;
|
||||
private XLSX $escaper;
|
||||
|
||||
/**
|
||||
* @param SharedStringsManager $sharedStringsManager Manages shared strings
|
||||
@@ -96,28 +96,22 @@ final class CellValueFormatter
|
||||
}
|
||||
$vNodeValue = $this->getVNodeValue($node);
|
||||
|
||||
if (self::CELL_TYPE_NUMERIC === $cellType) {
|
||||
$fNodeValue = $node->getElementsByTagName(self::XML_NODE_FORMULA)->item(0)?->nodeValue;
|
||||
if (null !== $fNodeValue) {
|
||||
$computedValue = $this->formatNumericCellValue($vNodeValue, (int) $node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID));
|
||||
$fNodeValue = $node->getElementsByTagName(self::XML_NODE_FORMULA)->item(0)?->nodeValue;
|
||||
if (null !== $fNodeValue) {
|
||||
$computedValue = $this->formatRawValueForCellType($cellType, $node, $vNodeValue);
|
||||
|
||||
return new Cell\FormulaCell('='.$fNodeValue, null, $computedValue);
|
||||
}
|
||||
return new Cell\FormulaCell(
|
||||
'='.$fNodeValue,
|
||||
null,
|
||||
$computedValue instanceof Cell\ErrorCell ? null : $computedValue
|
||||
);
|
||||
}
|
||||
|
||||
if ('' === $vNodeValue && self::CELL_TYPE_INLINE_STRING !== $cellType) {
|
||||
return Cell::fromValue($vNodeValue);
|
||||
}
|
||||
|
||||
$rawValue = match ($cellType) {
|
||||
self::CELL_TYPE_INLINE_STRING => $this->formatInlineStringCellValue($node),
|
||||
self::CELL_TYPE_SHARED_STRING => $this->formatSharedStringCellValue($vNodeValue),
|
||||
self::CELL_TYPE_STR => $this->formatStrCellValue($vNodeValue),
|
||||
self::CELL_TYPE_BOOLEAN => $this->formatBooleanCellValue($vNodeValue),
|
||||
self::CELL_TYPE_NUMERIC => $this->formatNumericCellValue($vNodeValue, (int) $node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID)),
|
||||
self::CELL_TYPE_DATE => $this->formatDateCellValue($vNodeValue),
|
||||
default => new Cell\ErrorCell($vNodeValue, null),
|
||||
};
|
||||
$rawValue = $this->formatRawValueForCellType($cellType, $node, $vNodeValue);
|
||||
|
||||
if ($rawValue instanceof Cell) {
|
||||
return $rawValue;
|
||||
@@ -328,4 +322,23 @@ final class CellValueFormatter
|
||||
|
||||
return $cellValue;
|
||||
}
|
||||
|
||||
private function formatRawValueForCellType(
|
||||
string $cellType,
|
||||
DOMElement $node,
|
||||
string $vNodeValue
|
||||
): bool|Cell\ErrorCell|DateInterval|DateTimeImmutable|float|int|string {
|
||||
return match ($cellType) {
|
||||
self::CELL_TYPE_INLINE_STRING => $this->formatInlineStringCellValue($node),
|
||||
self::CELL_TYPE_SHARED_STRING => $this->formatSharedStringCellValue($vNodeValue),
|
||||
self::CELL_TYPE_STR => $this->formatStrCellValue($vNodeValue),
|
||||
self::CELL_TYPE_BOOLEAN => $this->formatBooleanCellValue($vNodeValue),
|
||||
self::CELL_TYPE_NUMERIC => $this->formatNumericCellValue(
|
||||
$vNodeValue,
|
||||
(int) $node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID)
|
||||
),
|
||||
self::CELL_TYPE_DATE => $this->formatDateCellValue($vNodeValue),
|
||||
default => new Cell\ErrorCell($vNodeValue, null),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +61,16 @@ final class DateIntervalFormatHelper
|
||||
return 1 === preg_match('/^(\[hh?](:mm(:ss)?)?|\[mm?](:ss)?|\[ss?])$/', $excelFormat);
|
||||
}
|
||||
|
||||
public static function toPHPDateIntervalFormat(string $excelDateFormat, ?string &$startUnit = null): string
|
||||
public static function toPHPDateIntervalFormat(string $excelDateFormat, string &$startUnit): string
|
||||
{
|
||||
$startUnit = null;
|
||||
$startUnitStarted = false;
|
||||
$phpFormatParts = [];
|
||||
$formatParts = explode(':', str_replace(['[', ']'], '', $excelDateFormat));
|
||||
foreach ($formatParts as $formatPart) {
|
||||
$startUnit ??= $formatPart;
|
||||
if (false === $startUnitStarted) {
|
||||
$startUnit = $formatPart;
|
||||
$startUnitStarted = true;
|
||||
}
|
||||
$phpFormatParts[] = self::dateIntervalFormats[$formatPart];
|
||||
}
|
||||
|
||||
@@ -77,6 +80,7 @@ final class DateIntervalFormatHelper
|
||||
|
||||
public static function formatDateInterval(DateInterval $dateInterval, string $excelDateFormat): string
|
||||
{
|
||||
$startUnit = '';
|
||||
$phpFormat = self::toPHPDateIntervalFormat($excelDateFormat, $startUnit);
|
||||
|
||||
// We have to move the hours to minutes or hours+minutes to seconds if the format in Excel did the same:
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ namespace OpenSpout\Reader\XLSX\Manager\SharedStringsCaching;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class CachingStrategyFactory implements CachingStrategyFactoryInterface
|
||||
final readonly class CachingStrategyFactory implements CachingStrategyFactoryInterface
|
||||
{
|
||||
/**
|
||||
* The memory amount needed to store a string was obtained empirically from this data:.
|
||||
@@ -50,7 +50,7 @@ final class CachingStrategyFactory implements CachingStrategyFactoryInterface
|
||||
*/
|
||||
public const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000;
|
||||
|
||||
private readonly MemoryLimit $memoryLimit;
|
||||
private MemoryLimit $memoryLimit;
|
||||
|
||||
public function __construct(MemoryLimit $memoryLimit)
|
||||
{
|
||||
|
||||
@@ -42,7 +42,10 @@ final class FileBasedStrategy implements CachingStrategyInterface
|
||||
*
|
||||
* @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
|
||||
*/
|
||||
private string $inMemoryTempFilePath = '';
|
||||
private string $readMemoryTempFilePath = '';
|
||||
|
||||
/** @var string Path of the temporary file whose contents is currently being written to */
|
||||
private string $writeMemoryTempFilePath = '';
|
||||
|
||||
/**
|
||||
* @see CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE
|
||||
@@ -73,13 +76,14 @@ final class FileBasedStrategy implements CachingStrategyInterface
|
||||
{
|
||||
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
|
||||
|
||||
if (!file_exists($tempFilePath)) {
|
||||
if ($this->writeMemoryTempFilePath !== $tempFilePath) {
|
||||
if (null !== $this->tempFilePointer) {
|
||||
fclose($this->tempFilePointer);
|
||||
}
|
||||
$resource = fopen($tempFilePath, 'w');
|
||||
\assert(false !== $resource);
|
||||
$this->tempFilePointer = $resource;
|
||||
$this->writeMemoryTempFilePath = $tempFilePath;
|
||||
}
|
||||
|
||||
// The shared string retrieval logic expects each cell data to be on one line only
|
||||
@@ -97,6 +101,7 @@ final class FileBasedStrategy implements CachingStrategyInterface
|
||||
{
|
||||
// close pointer to the last temp file that was written
|
||||
if (null !== $this->tempFilePointer) {
|
||||
$this->writeMemoryTempFilePath = '';
|
||||
fclose($this->tempFilePointer);
|
||||
}
|
||||
}
|
||||
@@ -115,17 +120,13 @@ final class FileBasedStrategy implements CachingStrategyInterface
|
||||
$tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
|
||||
$indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile;
|
||||
|
||||
if (!file_exists($tempFilePath)) {
|
||||
throw new SharedStringNotFoundException("Shared string temp file not found: {$tempFilePath} ; for index: {$sharedStringIndex}");
|
||||
}
|
||||
|
||||
if ($this->inMemoryTempFilePath !== $tempFilePath) {
|
||||
$tempFilePath = realpath($tempFilePath);
|
||||
\assert(false !== $tempFilePath);
|
||||
$contents = file_get_contents($tempFilePath);
|
||||
\assert(false !== $contents);
|
||||
if ($this->readMemoryTempFilePath !== $tempFilePath) {
|
||||
$contents = @file_get_contents($tempFilePath);
|
||||
if (false === $contents) {
|
||||
throw new SharedStringNotFoundException("Shared string temp file could not be read: {$tempFilePath} ; for index: {$sharedStringIndex}");
|
||||
}
|
||||
$this->inMemoryTempFileContents = explode(PHP_EOL, $contents);
|
||||
$this->inMemoryTempFilePath = $tempFilePath;
|
||||
$this->readMemoryTempFilePath = $tempFilePath;
|
||||
}
|
||||
|
||||
$sharedString = null;
|
||||
|
||||
@@ -7,9 +7,9 @@ namespace OpenSpout\Reader\XLSX\Manager\SharedStringsCaching;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class MemoryLimit
|
||||
final readonly class MemoryLimit
|
||||
{
|
||||
private readonly string $memoryLimit;
|
||||
private string $memoryLimit;
|
||||
|
||||
public function __construct(string $memoryLimit)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ use OpenSpout\Reader\XLSX\Options;
|
||||
use OpenSpout\Reader\XLSX\RowIterator;
|
||||
use OpenSpout\Reader\XLSX\Sheet;
|
||||
use OpenSpout\Reader\XLSX\SheetHeaderReader;
|
||||
use OpenSpout\Reader\XLSX\SheetMergeCellsReader;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -49,6 +50,7 @@ final class SheetManager
|
||||
* State value to represent a hidden sheet.
|
||||
*/
|
||||
public const SHEET_STATE_HIDDEN = 'hidden';
|
||||
public const SHEET_STATE_VERY_HIDDEN = 'veryHidden';
|
||||
|
||||
/** @var string Path of the XLSX file being read */
|
||||
private readonly string $filePath;
|
||||
@@ -178,7 +180,7 @@ final class SheetManager
|
||||
\assert(null !== $sheetId);
|
||||
|
||||
$sheetState = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_STATE);
|
||||
$isSheetVisible = (self::SHEET_STATE_HIDDEN !== $sheetState);
|
||||
$isSheetVisible = (self::SHEET_STATE_HIDDEN !== $sheetState && self::SHEET_STATE_VERY_HIDDEN !== $sheetState);
|
||||
|
||||
$escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
|
||||
\assert(null !== $escapedSheetName);
|
||||
@@ -186,13 +188,24 @@ final class SheetManager
|
||||
|
||||
$sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
|
||||
|
||||
$mergeCells = [];
|
||||
if ($this->options->SHOULD_LOAD_MERGE_CELLS) {
|
||||
$mergeCells = (new SheetMergeCellsReader(
|
||||
$this->filePath,
|
||||
$sheetDataXMLFilePath,
|
||||
$xmlReader = new XMLReader(),
|
||||
new XMLProcessor($xmlReader)
|
||||
))->getMergeCells();
|
||||
}
|
||||
|
||||
return new Sheet(
|
||||
$this->createRowIterator($this->filePath, $sheetDataXMLFilePath, $this->options, $this->sharedStringsManager),
|
||||
$this->createSheetHeaderReader($this->filePath, $sheetDataXMLFilePath),
|
||||
$sheetIndexZeroBased,
|
||||
$sheetName,
|
||||
$isSheetActive,
|
||||
$isSheetVisible
|
||||
$isSheetVisible,
|
||||
$mergeCells
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,8 +253,6 @@ final class SheetManager
|
||||
Options $options,
|
||||
SharedStringsManager $sharedStringsManager
|
||||
): RowIterator {
|
||||
$xmlReader = new XMLReader();
|
||||
|
||||
$workbookRelationshipsManager = new WorkbookRelationshipsManager($filePath);
|
||||
$styleManager = new StyleManager(
|
||||
$filePath,
|
||||
@@ -262,7 +273,7 @@ final class SheetManager
|
||||
$filePath,
|
||||
$sheetDataXMLFilePath,
|
||||
$options->SHOULD_PRESERVE_EMPTY_ROWS,
|
||||
$xmlReader,
|
||||
$xmlReader = new XMLReader(),
|
||||
new XMLProcessor($xmlReader),
|
||||
$cellValueFormatter,
|
||||
new RowManager()
|
||||
|
||||
@@ -67,8 +67,7 @@ class StyleManager implements StyleManagerInterface
|
||||
private array $numFmtIdToIsDateFormatCache = [];
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param ?string $stylesXMLFilePath
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
*/
|
||||
public function __construct(string $filePath, ?string $stylesXMLFilePath)
|
||||
{
|
||||
@@ -98,7 +97,16 @@ class StyleManager implements StyleManagerInterface
|
||||
|
||||
public function getNumberFormatCode(int $styleId): string
|
||||
{
|
||||
if (null === $this->stylesXMLFilePath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$stylesAttributes = $this->getStylesAttributes();
|
||||
|
||||
if (!isset($stylesAttributes[$styleId])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$styleAttributes = $stylesAttributes[$styleId];
|
||||
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
|
||||
\assert(\is_int($numFmtId));
|
||||
|
||||
@@ -13,4 +13,5 @@ final class Options
|
||||
public bool $SHOULD_FORMAT_DATES = false;
|
||||
public bool $SHOULD_PRESERVE_EMPTY_ROWS = false;
|
||||
public bool $SHOULD_USE_1904_DATES = false;
|
||||
public bool $SHOULD_LOAD_MERGE_CELLS = false;
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ final class RowIterator implements RowIteratorInterface
|
||||
/** @var XMLProcessor Helper Object to process XML nodes */
|
||||
private readonly XMLProcessor $xmlProcessor;
|
||||
|
||||
/** @var Helper\CellValueFormatter Helper to format cell values */
|
||||
private readonly Helper\CellValueFormatter $cellValueFormatter;
|
||||
/** @var CellValueFormatter Helper to format cell values */
|
||||
private readonly CellValueFormatter $cellValueFormatter;
|
||||
|
||||
/** @var RowManager Manages rows */
|
||||
private readonly RowManager $rowManager;
|
||||
|
||||
@@ -5,46 +5,60 @@ declare(strict_types=1);
|
||||
namespace OpenSpout\Reader\XLSX;
|
||||
|
||||
use OpenSpout\Reader\Common\ColumnWidth;
|
||||
use OpenSpout\Reader\SheetWithMergeCellsInterface;
|
||||
use OpenSpout\Reader\SheetWithVisibilityInterface;
|
||||
|
||||
/**
|
||||
* @implements SheetWithVisibilityInterface<RowIterator>
|
||||
* @implements SheetWithMergeCellsInterface<RowIterator>
|
||||
*/
|
||||
final class Sheet implements SheetWithVisibilityInterface
|
||||
final readonly class Sheet implements SheetWithVisibilityInterface, SheetWithMergeCellsInterface
|
||||
{
|
||||
/** @var RowIterator To iterate over sheet's rows */
|
||||
private readonly RowIterator $rowIterator;
|
||||
private RowIterator $rowIterator;
|
||||
|
||||
/** @var SheetHeaderReader To read the header of the sheet, containing for instance the col widths */
|
||||
private readonly SheetHeaderReader $headerReader;
|
||||
private SheetHeaderReader $headerReader;
|
||||
|
||||
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||
private readonly int $index;
|
||||
private int $index;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
private readonly string $name;
|
||||
private string $name;
|
||||
|
||||
/** @var bool Whether the sheet was the active one */
|
||||
private readonly bool $isActive;
|
||||
private bool $isActive;
|
||||
|
||||
/** @var bool Whether the sheet is visible */
|
||||
private readonly bool $isVisible;
|
||||
private bool $isVisible;
|
||||
|
||||
/** @var list<string> Merge cells list ["C7:E7", "A9:D10"] */
|
||||
private array $mergeCells;
|
||||
|
||||
/**
|
||||
* @param RowIterator $rowIterator The corresponding row iterator
|
||||
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||
* @param string $sheetName Name of the sheet
|
||||
* @param bool $isSheetActive Whether the sheet was defined as active
|
||||
* @param bool $isSheetVisible Whether the sheet is visible
|
||||
* @param RowIterator $rowIterator The corresponding row iterator
|
||||
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||
* @param string $sheetName Name of the sheet
|
||||
* @param bool $isSheetActive Whether the sheet was defined as active
|
||||
* @param bool $isSheetVisible Whether the sheet is visible
|
||||
* @param list<string> $mergeCells Merge cells list ["C7:E7", "A9:D10"]
|
||||
*/
|
||||
public function __construct(RowIterator $rowIterator, SheetHeaderReader $headerReader, int $sheetIndex, string $sheetName, bool $isSheetActive, bool $isSheetVisible)
|
||||
{
|
||||
public function __construct(
|
||||
RowIterator $rowIterator,
|
||||
SheetHeaderReader $headerReader,
|
||||
int $sheetIndex,
|
||||
string $sheetName,
|
||||
bool $isSheetActive,
|
||||
bool $isSheetVisible,
|
||||
array $mergeCells
|
||||
) {
|
||||
$this->rowIterator = $rowIterator;
|
||||
$this->headerReader = $headerReader;
|
||||
$this->index = $sheetIndex;
|
||||
$this->name = $sheetName;
|
||||
$this->isActive = $isSheetActive;
|
||||
$this->isVisible = $isSheetVisible;
|
||||
$this->mergeCells = $mergeCells;
|
||||
}
|
||||
|
||||
public function getRowIterator(): RowIterator
|
||||
@@ -91,4 +105,12 @@ final class Sheet implements SheetWithVisibilityInterface
|
||||
{
|
||||
return $this->isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> Merge cells list ["C7:E7", "A9:D10"]
|
||||
*/
|
||||
public function getMergeCells(): array
|
||||
{
|
||||
return $this->mergeCells;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Reader\XLSX;
|
||||
|
||||
use OpenSpout\Common\Exception\IOException;
|
||||
use OpenSpout\Reader\Common\XMLProcessor;
|
||||
use OpenSpout\Reader\Wrapper\XMLReader;
|
||||
|
||||
use function ltrim;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class SheetMergeCellsReader
|
||||
{
|
||||
public const XML_NODE_MERGE_CELL = 'mergeCell';
|
||||
public const XML_ATTRIBUTE_REF = 'ref';
|
||||
|
||||
/** @var list<string> Merged cells list */
|
||||
private array $mergeCells = [];
|
||||
|
||||
/**
|
||||
* @param string $filePath Path of the XLSX file being read
|
||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||
* @param XMLProcessor $xmlProcessor Helper to process XML files
|
||||
*/
|
||||
public function __construct(
|
||||
string $filePath,
|
||||
string $sheetDataXMLFilePath,
|
||||
XMLReader $xmlReader,
|
||||
XMLProcessor $xmlProcessor
|
||||
) {
|
||||
$sheetDataXMLFilePath = ltrim($sheetDataXMLFilePath, '/');
|
||||
|
||||
// Register all callbacks to process different nodes when reading the XML file
|
||||
$xmlProcessor->registerCallback(self::XML_NODE_MERGE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processMergeCellsStartingNode']);
|
||||
$xmlReader->close();
|
||||
|
||||
if (false === $xmlReader->openFileInZip($filePath, $sheetDataXMLFilePath)) {
|
||||
throw new IOException("Could not open \"{$sheetDataXMLFilePath}\".");
|
||||
}
|
||||
|
||||
// Now read the entire header of the sheet, until we reach the <sheetData> element
|
||||
$xmlProcessor->readUntilStopped();
|
||||
$xmlReader->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getMergeCells(): array
|
||||
{
|
||||
return $this->mergeCells;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XMLReader $xmlReader XMLReader object, positioned on a "<mergeCells>" starting node
|
||||
*
|
||||
* @return int A return code that indicates what action should the processor take next
|
||||
*/
|
||||
private function processMergeCellsStartingNode(XMLReader $xmlReader): int
|
||||
{
|
||||
$this->mergeCells[] = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF);
|
||||
|
||||
return XMLProcessor::PROCESSING_CONTINUE;
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,6 @@ abstract class AbstractWriter implements WriterInterface
|
||||
/** @var resource Pointer to the file/stream we will write to */
|
||||
protected $filePointer;
|
||||
|
||||
/** @var string document creator */
|
||||
protected string $creator = 'OpenSpout';
|
||||
|
||||
/** @var string Content-Type value for the header - to be defined by child class */
|
||||
protected static string $headerContentType;
|
||||
|
||||
@@ -122,11 +119,6 @@ abstract class AbstractWriter implements WriterInterface
|
||||
}
|
||||
}
|
||||
|
||||
final public function setCreator(string $creator): void
|
||||
{
|
||||
$this->creator = $creator;
|
||||
}
|
||||
|
||||
final public function getWrittenRowCount(): int
|
||||
{
|
||||
return $this->writtenRowCount;
|
||||
|
||||
@@ -4,10 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Writer;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
final class AutoFilter
|
||||
final readonly class AutoFilter
|
||||
{
|
||||
/**
|
||||
* @param 0|positive-int $fromColumnIndex
|
||||
@@ -16,9 +13,9 @@ final class AutoFilter
|
||||
* @param positive-int $toRow
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $fromColumnIndex,
|
||||
public readonly int $fromRow,
|
||||
public readonly int $toColumnIndex,
|
||||
public readonly int $toRow
|
||||
public int $fromColumnIndex,
|
||||
public int $fromRow,
|
||||
public int $toColumnIndex,
|
||||
public int $toRow
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Writer\CSV;
|
||||
|
||||
use Exception;
|
||||
use OpenSpout\Common\Entity\Cell;
|
||||
use OpenSpout\Common\Entity\Row;
|
||||
use OpenSpout\Common\Exception\IOException;
|
||||
@@ -29,6 +30,11 @@ final class Writer extends AbstractWriter
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function setCreator(string $creator): void
|
||||
{
|
||||
throw new Exception('Method unsopported for CSV documents');
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the CSV streamer and makes it ready to accept data.
|
||||
*/
|
||||
|
||||
@@ -7,15 +7,15 @@ namespace OpenSpout\Writer\Common;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ColumnWidth
|
||||
final readonly class ColumnWidth
|
||||
{
|
||||
/**
|
||||
* @param positive-int $start
|
||||
* @param positive-int $end
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $start,
|
||||
public readonly int $end,
|
||||
public readonly float $width,
|
||||
public int $start,
|
||||
public int $end,
|
||||
public float $width,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use OpenSpout\Writer\Common\ColumnWidth;
|
||||
use OpenSpout\Writer\Common\Manager\SheetManager;
|
||||
use OpenSpout\Writer\Exception\InvalidSheetNameException;
|
||||
use OpenSpout\Writer\XLSX\Entity\SheetView;
|
||||
use OpenSpout\Writer\XLSX\Options\SheetProtection;
|
||||
|
||||
/**
|
||||
* External representation of a worksheet.
|
||||
@@ -45,6 +46,8 @@ final class Sheet
|
||||
/** @var string rows to repeat at top */
|
||||
private ?string $printTitleRows = null;
|
||||
|
||||
private ?SheetProtection $sheetProtection = null;
|
||||
|
||||
/**
|
||||
* @param 0|positive-int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||
* @param string $associatedWorkbookId ID of the sheet's associated workbook
|
||||
@@ -219,4 +222,19 @@ final class Sheet
|
||||
{
|
||||
$this->printTitleRows = $printTitleRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSheetProtection(SheetProtection $sheetProtection): self
|
||||
{
|
||||
$this->sheetProtection = $sheetProtection;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSheetProtection(): ?SheetProtection
|
||||
{
|
||||
return $this->sheetProtection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ use OpenSpout\Common\Entity\Style\Style;
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class RegisteredStyle
|
||||
final readonly class RegisteredStyle
|
||||
{
|
||||
private readonly Style $style;
|
||||
private Style $style;
|
||||
|
||||
private readonly bool $isMatchingRowStyle;
|
||||
private bool $isMatchingRowStyle;
|
||||
|
||||
public function __construct(Style $style, bool $isMatchingRowStyle)
|
||||
{
|
||||
|
||||
@@ -60,7 +60,7 @@ final class SheetManager
|
||||
}
|
||||
|
||||
if ($this->doesContainInvalidCharacters($name)) {
|
||||
$failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]';
|
||||
$failedRequirements[] = 'It should not contain these characters: \ / ? * : [ or ]';
|
||||
}
|
||||
|
||||
if ($this->doesStartOrEndWithSingleQuote($name)) {
|
||||
|
||||
@@ -9,10 +9,10 @@ use OpenSpout\Common\Entity\Style\Style;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class PossiblyUpdatedStyle
|
||||
final readonly class PossiblyUpdatedStyle
|
||||
{
|
||||
private readonly Style $style;
|
||||
private readonly bool $isUpdated;
|
||||
private Style $style;
|
||||
private bool $isUpdated;
|
||||
|
||||
public function __construct(Style $style, bool $isUpdated)
|
||||
{
|
||||
|
||||
@@ -75,6 +75,9 @@ final class StyleMerger
|
||||
if (!$style->hasSetWrapText() && $baseStyle->hasSetWrapText()) {
|
||||
$styleToUpdate->setShouldWrapText($baseStyle->shouldWrapText());
|
||||
}
|
||||
if (!$style->hasSetTextRotation() && $baseStyle->hasSetTextRotation()) {
|
||||
$styleToUpdate->setTextRotation($baseStyle->textRotation());
|
||||
}
|
||||
if (!$style->hasSetShrinkToFit() && $baseStyle->shouldShrinkToFit()) {
|
||||
$styleToUpdate->setShouldShrinkToFit();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ final class InvalidNameException extends WriterException
|
||||
{
|
||||
$msg = '%s is not a valid name identifier for a border. Valid identifiers are: %s.';
|
||||
|
||||
parent::__construct(sprintf($msg, $name, implode(',', BorderPart::allowedNames)));
|
||||
parent::__construct(\sprintf($msg, $name, implode(',', BorderPart::allowedNames)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ final class InvalidStyleException extends WriterException
|
||||
{
|
||||
$msg = '%s is not a valid style identifier for a border. Valid identifiers are: %s.';
|
||||
|
||||
parent::__construct(sprintf($msg, $name, implode(',', BorderPart::allowedStyles)));
|
||||
parent::__construct(\sprintf($msg, $name, implode(',', BorderPart::allowedStyles)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ final class InvalidWidthException extends WriterException
|
||||
{
|
||||
$msg = '%s is not a valid width identifier for a border. Valid identifiers are: %s.';
|
||||
|
||||
parent::__construct(sprintf($msg, $name, implode(',', BorderPart::allowedWidths)));
|
||||
parent::__construct(\sprintf($msg, $name, implode(',', BorderPart::allowedWidths)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,14 +47,14 @@ final class BorderHelper
|
||||
$definition = 'fo:border-%s="%s"';
|
||||
|
||||
if (Border::STYLE_NONE === $borderPart->getStyle()) {
|
||||
$borderPartDefinition = sprintf($definition, $borderPart->getName(), 'none');
|
||||
$borderPartDefinition = \sprintf($definition, $borderPart->getName(), 'none');
|
||||
} else {
|
||||
$attributes = [
|
||||
self::widthMap[$borderPart->getWidth()],
|
||||
self::styleMap[$borderPart->getStyle()],
|
||||
'#'.$borderPart->getColor(),
|
||||
];
|
||||
$borderPartDefinition = sprintf($definition, $borderPart->getName(), implode(' ', $attributes));
|
||||
$borderPartDefinition = \sprintf($definition, $borderPart->getName(), implode(' ', $attributes));
|
||||
}
|
||||
|
||||
return $borderPartDefinition;
|
||||
|
||||
@@ -28,11 +28,11 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
public const MIMETYPE_FILE_NAME = 'mimetype';
|
||||
public const STYLES_XML_FILE_NAME = 'styles.xml';
|
||||
|
||||
private readonly string $baseFolderRealPath;
|
||||
private string $baseFolderRealPath;
|
||||
|
||||
/** @var string document creator */
|
||||
private readonly string $creator;
|
||||
private readonly CommonFileSystemHelper $baseFileSystemHelper;
|
||||
private string $creator;
|
||||
private CommonFileSystemHelper $baseFileSystemHelper;
|
||||
|
||||
/** @var string Path to the root folder inside the temp folder where the files to create the ODS will be stored */
|
||||
private string $rootFolder;
|
||||
|
||||
@@ -336,7 +336,7 @@ final class StyleManager extends CommonStyleManager
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
return \sprintf(
|
||||
' fo:text-align="%s" ',
|
||||
$this->transformCellAlignment($style->getCellAlignment())
|
||||
);
|
||||
@@ -351,7 +351,7 @@ final class StyleManager extends CommonStyleManager
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
return \sprintf(
|
||||
' fo:vertical-align="%s" ',
|
||||
$this->transformCellVerticalAlignment($style->getCellVerticalAlignment())
|
||||
);
|
||||
@@ -423,7 +423,7 @@ final class StyleManager extends CommonStyleManager
|
||||
return BorderHelper::serializeBorderPart($borderPart);
|
||||
}, $border->getParts());
|
||||
|
||||
return sprintf(' %s ', implode(' ', $borders));
|
||||
return \sprintf(' %s ', implode(' ', $borders));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,6 +431,6 @@ final class StyleManager extends CommonStyleManager
|
||||
*/
|
||||
private function getBackgroundColorXMLContent(string $bgColor): string
|
||||
{
|
||||
return sprintf(' fo:background-color="#%s" ', $bgColor);
|
||||
return \sprintf(' fo:background-color="#%s" ', $bgColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,16 @@ use OpenSpout\Writer\ODS\Manager\Style\StyleManager;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class WorksheetManager implements WorksheetManagerInterface
|
||||
final readonly class WorksheetManager implements WorksheetManagerInterface
|
||||
{
|
||||
/** @var ODSEscaper Strings escaper */
|
||||
private readonly ODSEscaper $stringsEscaper;
|
||||
private ODSEscaper $stringsEscaper;
|
||||
|
||||
/** @var StyleManager Manages styles */
|
||||
private readonly StyleManager $styleManager;
|
||||
private StyleManager $styleManager;
|
||||
|
||||
/** @var StyleMerger Helper to merge styles together */
|
||||
private readonly StyleMerger $styleMerger;
|
||||
private StyleMerger $styleMerger;
|
||||
|
||||
/**
|
||||
* WorksheetManager constructor.
|
||||
@@ -88,7 +88,7 @@ final class WorksheetManager implements WorksheetManagerInterface
|
||||
$databaseRange = '';
|
||||
|
||||
if (null !== $autofilter = $externalSheet->getAutoFilter()) {
|
||||
$rangeAddress = sprintf(
|
||||
$rangeAddress = \sprintf(
|
||||
'\'%s\'.%s%s:\'%s\'.%s%s',
|
||||
$escapedSheetName,
|
||||
CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex),
|
||||
|
||||
@@ -19,6 +19,9 @@ final class Writer extends AbstractWriterMultiSheets
|
||||
{
|
||||
/** @var string Content-Type value for the header */
|
||||
protected static string $headerContentType = 'application/vnd.oasis.opendocument.spreadsheet';
|
||||
|
||||
/** @var string document creator */
|
||||
protected string $creator = 'OpenSpout';
|
||||
private readonly Options $options;
|
||||
|
||||
public function __construct(?Options $options = null)
|
||||
@@ -31,6 +34,11 @@ final class Writer extends AbstractWriterMultiSheets
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function setCreator(string $creator): void
|
||||
{
|
||||
$this->creator = $creator;
|
||||
}
|
||||
|
||||
protected function createWorkbookManager(): WorkbookManager
|
||||
{
|
||||
$workbook = new Workbook();
|
||||
|
||||
@@ -40,6 +40,13 @@ interface WriterInterface
|
||||
*/
|
||||
public function addRow(Row $row): void;
|
||||
|
||||
/**
|
||||
* Set document creator.
|
||||
*
|
||||
* @param string $creator document creator
|
||||
*/
|
||||
public function setCreator(string $creator): void;
|
||||
|
||||
/**
|
||||
* Appends the rows to the end of the stream.
|
||||
*
|
||||
@@ -51,13 +58,6 @@ interface WriterInterface
|
||||
*/
|
||||
public function addRows(array $rows): void;
|
||||
|
||||
/**
|
||||
* Set document creator.
|
||||
*
|
||||
* @param string $creator document creator
|
||||
*/
|
||||
public function setCreator(string $creator): void;
|
||||
|
||||
/**
|
||||
* @return 0|positive-int
|
||||
*/
|
||||
|
||||
@@ -48,8 +48,8 @@ final class BorderHelper
|
||||
|
||||
$borderStyle = self::getBorderStyle($borderPart);
|
||||
|
||||
$colorEl = sprintf('<color rgb="%s"/>', $borderPart->getColor());
|
||||
$partEl = sprintf(
|
||||
$colorEl = \sprintf('<color rgb="%s"/>', $borderPart->getColor());
|
||||
$partEl = \sprintf(
|
||||
'<%s style="%s">%s</%s>',
|
||||
$borderPart->getName(),
|
||||
$borderStyle,
|
||||
|
||||
@@ -16,6 +16,7 @@ use OpenSpout\Writer\Common\Helper\ZipHelper;
|
||||
use OpenSpout\Writer\XLSX\Manager\Style\StyleManager;
|
||||
use OpenSpout\Writer\XLSX\MergeCell;
|
||||
use OpenSpout\Writer\XLSX\Options;
|
||||
use OpenSpout\Writer\XLSX\Properties;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -31,6 +32,7 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
public const RELS_FILE_NAME = '.rels';
|
||||
public const APP_XML_FILE_NAME = 'app.xml';
|
||||
public const CORE_XML_FILE_NAME = 'core.xml';
|
||||
public const CUSTOM_XML_FILE_NAME = 'custom.xml';
|
||||
public const CONTENT_TYPES_XML_FILE_NAME = '[Content_Types].xml';
|
||||
public const WORKBOOK_XML_FILE_NAME = 'workbook.xml';
|
||||
public const WORKBOOK_RELS_XML_FILE_NAME = 'workbook.xml.rels';
|
||||
@@ -47,8 +49,8 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
/** @var ZipHelper Helper to perform tasks with Zip archive */
|
||||
private readonly ZipHelper $zipHelper;
|
||||
|
||||
/** @var string document creator */
|
||||
private readonly string $creator;
|
||||
/** @var Properties document properties */
|
||||
private readonly Properties $properties;
|
||||
|
||||
/** @var XLSX Used to escape XML data */
|
||||
private readonly XLSX $escaper;
|
||||
@@ -75,18 +77,18 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
private string $sheetsContentTempFolder;
|
||||
|
||||
/**
|
||||
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
|
||||
* @param ZipHelper $zipHelper Helper to perform tasks with Zip archive
|
||||
* @param XLSX $escaper Used to escape XML data
|
||||
* @param string $creator document creator
|
||||
* @param string $baseFolderPath The path of the base folder where all the I/O can occur
|
||||
* @param ZipHelper $zipHelper Helper to perform tasks with Zip archive
|
||||
* @param XLSX $escaper Used to escape XML data
|
||||
* @param Properties $properties document properies
|
||||
*/
|
||||
public function __construct(string $baseFolderPath, ZipHelper $zipHelper, XLSX $escaper, string $creator)
|
||||
public function __construct(string $baseFolderPath, ZipHelper $zipHelper, XLSX $escaper, Properties $properties)
|
||||
{
|
||||
$this->baseFileSystemHelper = new CommonFileSystemHelper($baseFolderPath);
|
||||
$this->baseFolderRealPath = $this->baseFileSystemHelper->getBaseFolderRealPath();
|
||||
$this->zipHelper = $zipHelper;
|
||||
$this->escaper = $escaper;
|
||||
$this->creator = $creator;
|
||||
$this->properties = $properties;
|
||||
}
|
||||
|
||||
public function createFolder(string $parentFolderPath, string $folderName): string
|
||||
@@ -168,10 +170,19 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
}
|
||||
|
||||
$contentTypesXmlFileContents .= <<<'EOD'
|
||||
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" PartName="/xl/styles.xml"/>
|
||||
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/>
|
||||
<Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/>
|
||||
<Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/>
|
||||
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" PartName="/xl/styles.xml"/>
|
||||
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/>
|
||||
<Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/>
|
||||
<Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/>
|
||||
EOD;
|
||||
|
||||
if ([] !== $this->properties->customProperties) {
|
||||
$contentTypesXmlFileContents .= <<<'EOD'
|
||||
<Override ContentType="application/vnd.openxmlformats-officedocument.custom-properties+xml" PartName="/docProps/custom.xml" />
|
||||
EOD;
|
||||
}
|
||||
|
||||
$contentTypesXmlFileContents .= <<<'EOD'
|
||||
</Types>
|
||||
EOD;
|
||||
|
||||
@@ -185,11 +196,18 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
*
|
||||
* @param Worksheet[] $worksheets
|
||||
*/
|
||||
public function createWorkbookFile(array $worksheets): self
|
||||
public function createWorkbookFile(Options $options, array $worksheets): self
|
||||
{
|
||||
$workbookXmlFileContents = <<<'EOD'
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
EOD;
|
||||
|
||||
if (null !== $options->getWorkbookProtection()) {
|
||||
$workbookXmlFileContents .= $options->getWorkbookProtection()->getXml();
|
||||
}
|
||||
|
||||
$workbookXmlFileContents .= <<<'EOD'
|
||||
<sheets>
|
||||
EOD;
|
||||
|
||||
@@ -212,7 +230,7 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
$sheet = $worksheet->getExternalSheet();
|
||||
if (null !== $autofilter = $sheet->getAutoFilter()) {
|
||||
$worksheetName = $sheet->getName();
|
||||
$name = sprintf(
|
||||
$name = \sprintf(
|
||||
'\'%s\'!$%s$%s:$%s$%s',
|
||||
$this->escaper->escape($worksheetName),
|
||||
CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex),
|
||||
@@ -321,24 +339,23 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
fwrite($worksheetFilePointer, self::SHEET_XML_FILE_HEADER);
|
||||
|
||||
// AutoFilter tags
|
||||
$range = '';
|
||||
if (null !== $autofilter = $sheet->getAutoFilter()) {
|
||||
$range = sprintf(
|
||||
'%s%s:%s%s',
|
||||
CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex),
|
||||
$autofilter->fromRow,
|
||||
CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex),
|
||||
$autofilter->toRow
|
||||
);
|
||||
if (isset($pageSetup) && $pageSetup->fitToPage) {
|
||||
fwrite($worksheetFilePointer, '<sheetPr filterMode="false"><pageSetUpPr fitToPage="true"/></sheetPr>');
|
||||
} else {
|
||||
fwrite($worksheetFilePointer, '<sheetPr filterMode="false"><pageSetUpPr fitToPage="false"/></sheetPr>');
|
||||
}
|
||||
fwrite($worksheetFilePointer, sprintf('<dimension ref="%s"/>', $range));
|
||||
} elseif (isset($pageSetup) && $pageSetup->fitToPage) {
|
||||
fwrite($worksheetFilePointer, '<sheetPr><pageSetUpPr fitToPage="true"/></sheetPr>');
|
||||
}
|
||||
$sheetRange = \sprintf(
|
||||
'%s%s:%s%s',
|
||||
CellHelper::getColumnLettersFromColumnIndex(0),
|
||||
1,
|
||||
CellHelper::getColumnLettersFromColumnIndex($worksheet->getMaxNumColumns() - 1),
|
||||
$worksheet->getLastWrittenRowIndex()
|
||||
);
|
||||
fwrite($worksheetFilePointer, \sprintf('<dimension ref="%s"/>', $sheetRange));
|
||||
if (null !== ($sheetView = $sheet->getSheetView())) {
|
||||
fwrite($worksheetFilePointer, '<sheetViews>'.$sheetView->getXml().'</sheetViews>');
|
||||
}
|
||||
@@ -350,9 +367,20 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
$this->copyFileContentsToTarget($worksheetFilePath, $worksheetFilePointer);
|
||||
fwrite($worksheetFilePointer, '</sheetData>');
|
||||
|
||||
if (null !== $sheet->getSheetProtection()) {
|
||||
fwrite($worksheetFilePointer, $sheet->getSheetProtection()->getXml());
|
||||
}
|
||||
|
||||
// AutoFilter tag
|
||||
if ('' !== $range) {
|
||||
fwrite($worksheetFilePointer, sprintf('<autoFilter ref="%s"/>', $range));
|
||||
if (null !== $autofilter) {
|
||||
$autoFilterRange = \sprintf(
|
||||
'%s%s:%s%s',
|
||||
CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex),
|
||||
$autofilter->fromRow,
|
||||
CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex),
|
||||
$autofilter->toRow
|
||||
);
|
||||
fwrite($worksheetFilePointer, \sprintf('<autoFilter ref="%s"/>', $autoFilterRange));
|
||||
}
|
||||
|
||||
// create nodes for merge cells
|
||||
@@ -365,7 +393,7 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
foreach ($mergeCells as $mergeCell) {
|
||||
$topLeft = CellHelper::getColumnLettersFromColumnIndex($mergeCell->topLeftColumn).$mergeCell->topLeftRow;
|
||||
$bottomRight = CellHelper::getColumnLettersFromColumnIndex($mergeCell->bottomRightColumn).$mergeCell->bottomRightRow;
|
||||
$mergeCellString .= sprintf(
|
||||
$mergeCellString .= \sprintf(
|
||||
'<mergeCell ref="%s:%s"/>',
|
||||
$topLeft,
|
||||
$bottomRight
|
||||
@@ -585,13 +613,21 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
*/
|
||||
private function createRelsFile(): self
|
||||
{
|
||||
$relsFileContents = <<<'EOD'
|
||||
$relationshipsXmlContents = <<<'EOD'
|
||||
<Relationship Id="rIdWorkbook" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||||
<Relationship Id="rIdCore" Type="http://schemas.openxmlformats.org/officedocument/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
|
||||
<Relationship Id="rIdApp" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
|
||||
EOD;
|
||||
|
||||
if ([] !== $this->properties->customProperties) {
|
||||
$relationshipsXmlContents .= <<<'EOD'
|
||||
<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties" Target="docProps/custom.xml"/>
|
||||
EOD;
|
||||
}
|
||||
|
||||
$relsFileContents = <<<EOD
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rIdWorkbook" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||||
<Relationship Id="rIdCore" Type="http://schemas.openxmlformats.org/officedocument/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
|
||||
<Relationship Id="rIdApp" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
|
||||
</Relationships>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{$relationshipsXmlContents}</Relationships>
|
||||
EOD;
|
||||
|
||||
$this->createFileWithContents($this->relsFolder, self::RELS_FILE_NAME, $relsFileContents);
|
||||
@@ -611,6 +647,10 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
$this->createAppXmlFile();
|
||||
$this->createCoreXmlFile();
|
||||
|
||||
if ([] !== $this->properties->customProperties) {
|
||||
$this->createCustomXmlFile();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -624,7 +664,7 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
$appXmlFileContents = <<<EOD
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
|
||||
<Application>{$this->creator}</Application>
|
||||
<Application>{$this->properties->application}</Application>
|
||||
<TotalTime>0</TotalTime>
|
||||
</Properties>
|
||||
EOD;
|
||||
@@ -645,6 +685,14 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
$coreXmlFileContents = <<<EOD
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<dc:title>{$this->properties->title}</dc:title>
|
||||
<dc:subject>{$this->properties->subject}</dc:subject>
|
||||
<dc:creator>{$this->properties->creator}</dc:creator>
|
||||
<cp:lastModifiedBy>{$this->properties->lastModifiedBy}</cp:lastModifiedBy>
|
||||
<cp:keywords>{$this->properties->keywords}</cp:keywords>
|
||||
<dc:description>{$this->properties->description}</dc:description>
|
||||
<cp:category>{$this->properties->category}</cp:category>
|
||||
<dc:language>{$this->properties->language}</dc:language>
|
||||
<dcterms:created xsi:type="dcterms:W3CDTF">{$createdDate}</dcterms:created>
|
||||
<dcterms:modified xsi:type="dcterms:W3CDTF">{$createdDate}</dcterms:modified>
|
||||
<cp:revision>0</cp:revision>
|
||||
@@ -656,6 +704,35 @@ final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the "custom.xml" file under the "docProps" folder.
|
||||
*
|
||||
* @throws IOException If unable to create the file
|
||||
*/
|
||||
private function createCustomXmlFile(): self
|
||||
{
|
||||
/** The pid must increment for each property, starting with 2 */
|
||||
$pid = 2;
|
||||
$propertiesXmlContents = '';
|
||||
|
||||
foreach ($this->properties->customProperties as $name => $value) {
|
||||
$propertiesXmlContents .= <<<EOD
|
||||
<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="{$pid}" name="{$name}"><vt:lpwstr>{$value}</vt:lpwstr></property>
|
||||
EOD;
|
||||
|
||||
++$pid;
|
||||
}
|
||||
|
||||
$customXmlFileContents = <<<EOD
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">{$propertiesXmlContents}</Properties>
|
||||
EOD;
|
||||
|
||||
$this->createFileWithContents($this->docPropsFolder, self::CUSTOM_XML_FILE_NAME, $customXmlFileContents);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the "xl" folder under the root folder as well as its subfolders.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Writer\XLSX\Helper;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class PasswordHashHelper
|
||||
{
|
||||
public static function make(string $password): string
|
||||
{
|
||||
$verifier = 0;
|
||||
$pwlen = \strlen($password);
|
||||
$passwordArray = pack('c', $pwlen).$password;
|
||||
|
||||
for ($i = $pwlen; $i >= 0; --$i) {
|
||||
$intermediate1 = (($verifier & 0x4000) === 0) ? 0 : 1;
|
||||
$intermediate2 = 2 * $verifier;
|
||||
$intermediate2 &= 0x7FFF;
|
||||
$intermediate3 = $intermediate1 | $intermediate2;
|
||||
$verifier = $intermediate3 ^ \ord($passwordArray[$i]);
|
||||
}
|
||||
|
||||
$verifier ^= 0xCE4B;
|
||||
|
||||
return strtoupper(dechex($verifier));
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ final class SharedStringsManager
|
||||
|
||||
// Adding 1 to take into account the space between the last xml attribute and "count"
|
||||
fseek($this->sharedStringsFilePointer, $firstPartHeaderLength + 1);
|
||||
fwrite($this->sharedStringsFilePointer, sprintf("%-{$defaultStringsCountPartLength}s", 'count="'.$this->numSharedStrings.'" uniqueCount="'.$this->numSharedStrings.'"'));
|
||||
fwrite($this->sharedStringsFilePointer, \sprintf("%-{$defaultStringsCountPartLength}s", 'count="'.$this->numSharedStrings.'" uniqueCount="'.$this->numSharedStrings.'"'));
|
||||
|
||||
fclose($this->sharedStringsFilePointer);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace OpenSpout\Writer\XLSX\Manager\Style;
|
||||
use OpenSpout\Common\Entity\Style\BorderPart;
|
||||
use OpenSpout\Common\Entity\Style\Color;
|
||||
use OpenSpout\Common\Entity\Style\Style;
|
||||
use OpenSpout\Common\Helper\Escaper\XLSX as XLSXEscaper;
|
||||
use OpenSpout\Writer\Common\Manager\Style\AbstractStyleManager as CommonStyleManager;
|
||||
use OpenSpout\Writer\XLSX\Helper\BorderHelper;
|
||||
|
||||
@@ -17,9 +18,13 @@ use OpenSpout\Writer\XLSX\Helper\BorderHelper;
|
||||
*/
|
||||
final class StyleManager extends CommonStyleManager
|
||||
{
|
||||
public function __construct(StyleRegistry $styleRegistry)
|
||||
/** @var XLSXEscaper Strings escaper */
|
||||
private readonly XLSXEscaper $stringsEscaper;
|
||||
|
||||
public function __construct(StyleRegistry $styleRegistry, XLSXEscaper $stringsEscaper)
|
||||
{
|
||||
parent::__construct($styleRegistry);
|
||||
$this->stringsEscaper = $stringsEscaper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +96,7 @@ final class StyleManager extends CommonStyleManager
|
||||
/** @var Style $style */
|
||||
$style = $this->styleRegistry->getStyleFromStyleId($styleId);
|
||||
$format = $style->getFormat();
|
||||
$tags[] = '<numFmt numFmtId="'.$numFmtId.'" formatCode="'.$format.'"/>';
|
||||
$tags[] = '<numFmt numFmtId="'.$numFmtId.'" formatCode="'.$this->stringsEscaper->escape($format).'"/>';
|
||||
}
|
||||
$content = '<numFmts count="'.\count($tags).'">';
|
||||
$content .= implode('', $tags);
|
||||
@@ -147,7 +152,7 @@ final class StyleManager extends CommonStyleManager
|
||||
|
||||
// Excel reserves two default fills
|
||||
$fillsCount = \count($registeredFills) + 2;
|
||||
$content = sprintf('<fills count="%d">', $fillsCount);
|
||||
$content = \sprintf('<fills count="%d">', $fillsCount);
|
||||
|
||||
$content .= '<fill><patternFill patternType="none"/></fill>';
|
||||
$content .= '<fill><patternFill patternType="gray125"/></fill>';
|
||||
@@ -158,7 +163,7 @@ final class StyleManager extends CommonStyleManager
|
||||
$style = $this->styleRegistry->getStyleFromStyleId($styleId);
|
||||
|
||||
$backgroundColor = $style->getBackgroundColor();
|
||||
$content .= sprintf(
|
||||
$content .= \sprintf(
|
||||
'<fill><patternFill patternType="solid"><fgColor rgb="%s"/></patternFill></fill>',
|
||||
$backgroundColor
|
||||
);
|
||||
@@ -236,16 +241,16 @@ final class StyleManager extends CommonStyleManager
|
||||
$content .= ' applyFont="1"';
|
||||
}
|
||||
|
||||
$content .= sprintf(' applyBorder="%d"', (bool) $style->getBorder());
|
||||
$content .= \sprintf(' applyBorder="%d"', (bool) $style->getBorder());
|
||||
|
||||
if ($style->shouldApplyCellAlignment() || $style->shouldApplyCellVerticalAlignment() || $style->hasSetWrapText() || $style->shouldShrinkToFit()) {
|
||||
if ($style->shouldApplyCellAlignment() || $style->shouldApplyCellVerticalAlignment() || $style->hasSetWrapText() || $style->shouldShrinkToFit() || $style->hasSetTextRotation()) {
|
||||
$content .= ' applyAlignment="1">';
|
||||
$content .= '<alignment';
|
||||
if ($style->shouldApplyCellAlignment()) {
|
||||
$content .= sprintf(' horizontal="%s"', $style->getCellAlignment());
|
||||
$content .= \sprintf(' horizontal="%s"', $style->getCellAlignment());
|
||||
}
|
||||
if ($style->shouldApplyCellVerticalAlignment()) {
|
||||
$content .= sprintf(' vertical="%s"', $style->getCellVerticalAlignment());
|
||||
$content .= \sprintf(' vertical="%s"', $style->getCellVerticalAlignment());
|
||||
}
|
||||
if ($style->hasSetWrapText()) {
|
||||
$content .= ' wrapText="'.($style->shouldWrapText() ? '1' : '0').'"';
|
||||
@@ -253,6 +258,9 @@ final class StyleManager extends CommonStyleManager
|
||||
if ($style->shouldShrinkToFit()) {
|
||||
$content .= ' shrinkToFit="true"';
|
||||
}
|
||||
if ($style->hasSetTextRotation()) {
|
||||
$content .= \sprintf(' textRotation="%s"', $style->textRotation());
|
||||
}
|
||||
|
||||
$content .= '/>';
|
||||
$content .= '</xf>';
|
||||
|
||||
@@ -75,7 +75,7 @@ final class WorkbookManager extends AbstractWorkbookManager
|
||||
->createContentFiles($this->options, $worksheets)
|
||||
->deleteWorksheetTempFolder()
|
||||
->createContentTypesFile($worksheets)
|
||||
->createWorkbookFile($worksheets)
|
||||
->createWorkbookFile($this->options, $worksheets)
|
||||
->createWorkbookRelsFile($worksheets)
|
||||
->createWorksheetRelsFiles($worksheets)
|
||||
->createStylesFile($this->styleManager)
|
||||
|
||||
@@ -24,7 +24,7 @@ use OpenSpout\Writer\XLSX\Options;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class WorksheetManager implements WorksheetManagerInterface
|
||||
final readonly class WorksheetManager implements WorksheetManagerInterface
|
||||
{
|
||||
/**
|
||||
* Maximum number of characters a cell can contain.
|
||||
@@ -36,24 +36,24 @@ final class WorksheetManager implements WorksheetManagerInterface
|
||||
public const MAX_CHARACTERS_PER_CELL = 32767;
|
||||
|
||||
/** @var CommentsManager Manages comments */
|
||||
private readonly CommentsManager $commentsManager;
|
||||
private CommentsManager $commentsManager;
|
||||
|
||||
private readonly Options $options;
|
||||
private Options $options;
|
||||
|
||||
/** @var StyleManager Manages styles */
|
||||
private readonly StyleManager $styleManager;
|
||||
private StyleManager $styleManager;
|
||||
|
||||
/** @var StyleMerger Helper to merge styles together */
|
||||
private readonly StyleMerger $styleMerger;
|
||||
private StyleMerger $styleMerger;
|
||||
|
||||
/** @var SharedStringsManager Helper to write shared strings */
|
||||
private readonly SharedStringsManager $sharedStringsManager;
|
||||
private SharedStringsManager $sharedStringsManager;
|
||||
|
||||
/** @var XLSXEscaper Strings escaper */
|
||||
private readonly XLSXEscaper $stringsEscaper;
|
||||
private XLSXEscaper $stringsEscaper;
|
||||
|
||||
/** @var StringHelper String helper */
|
||||
private readonly StringHelper $stringHelper;
|
||||
private StringHelper $stringHelper;
|
||||
|
||||
/**
|
||||
* WorksheetManager constructor.
|
||||
@@ -198,14 +198,14 @@ final class WorksheetManager implements WorksheetManagerInterface
|
||||
} elseif ($cell instanceof Cell\NumericCell) {
|
||||
$cellXML .= '><v>'.$cell->getValue().'</v></c>';
|
||||
} elseif ($cell instanceof Cell\FormulaCell) {
|
||||
$cellXML .= '><f>'.substr($cell->getValue(), 1).'</f></c>';
|
||||
$cellXML .= '><f>'.$this->stringsEscaper->escape(substr($cell->getValue(), 1)).'</f></c>';
|
||||
} elseif ($cell instanceof Cell\DateTimeCell) {
|
||||
$cellXML .= '><v>'.DateHelper::toExcel($cell->getValue()).'</v></c>';
|
||||
} elseif ($cell instanceof Cell\DateIntervalCell) {
|
||||
$cellXML .= '><v>'.DateIntervalHelper::toExcel($cell->getValue()).'</v></c>';
|
||||
} elseif ($cell instanceof Cell\ErrorCell) {
|
||||
// only writes the error value if it's a string
|
||||
$cellXML .= ' t="e"><v>'.$cell->getRawValue().'</v></c>';
|
||||
$cellXML .= ' t="e"><v>'.$this->stringsEscaper->escape($cell->getRawValue()).'</v></c>';
|
||||
} elseif ($cell instanceof Cell\EmptyCell) {
|
||||
if ($this->styleManager->shouldApplyStyleOnEmptyCell($styleId)) {
|
||||
$cellXML .= '/>';
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace OpenSpout\Writer\XLSX;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class MergeCell
|
||||
final readonly class MergeCell
|
||||
{
|
||||
/**
|
||||
* @param 0|positive-int $sheetIndex
|
||||
@@ -17,10 +17,10 @@ final class MergeCell
|
||||
* @param positive-int $bottomRightRow
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $sheetIndex,
|
||||
public readonly int $topLeftColumn,
|
||||
public readonly int $topLeftRow,
|
||||
public readonly int $bottomRightColumn,
|
||||
public readonly int $bottomRightRow,
|
||||
public int $sheetIndex,
|
||||
public int $topLeftColumn,
|
||||
public int $topLeftRow,
|
||||
public int $bottomRightColumn,
|
||||
public int $bottomRightRow,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use OpenSpout\Writer\Common\AbstractOptions;
|
||||
use OpenSpout\Writer\XLSX\Options\HeaderFooter;
|
||||
use OpenSpout\Writer\XLSX\Options\PageMargin;
|
||||
use OpenSpout\Writer\XLSX\Options\PageSetup;
|
||||
use OpenSpout\Writer\XLSX\Options\WorkbookProtection;
|
||||
|
||||
final class Options extends AbstractOptions
|
||||
{
|
||||
@@ -26,6 +27,10 @@ final class Options extends AbstractOptions
|
||||
|
||||
private ?HeaderFooter $headerFooter = null;
|
||||
|
||||
private ?WorkbookProtection $workbookProtection = null;
|
||||
|
||||
private Properties $properties;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
@@ -35,6 +40,8 @@ final class Options extends AbstractOptions
|
||||
$defaultRowStyle->setFontName(self::DEFAULT_FONT_NAME);
|
||||
|
||||
$this->DEFAULT_ROW_STYLE = $defaultRowStyle;
|
||||
|
||||
$this->properties = new Properties();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,4 +109,24 @@ final class Options extends AbstractOptions
|
||||
{
|
||||
return $this->headerFooter;
|
||||
}
|
||||
|
||||
public function getWorkbookProtection(): ?WorkbookProtection
|
||||
{
|
||||
return $this->workbookProtection;
|
||||
}
|
||||
|
||||
public function setWorkbookProtection(WorkbookProtection $workbookProtection): void
|
||||
{
|
||||
$this->workbookProtection = $workbookProtection;
|
||||
}
|
||||
|
||||
public function getProperties(): Properties
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
public function setProperties(Properties $properties): void
|
||||
{
|
||||
$this->properties = $properties;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Writer\XLSX\Options;
|
||||
|
||||
final class HeaderFooter
|
||||
final readonly class HeaderFooter
|
||||
{
|
||||
public function __construct(
|
||||
public readonly ?string $oddHeader = null,
|
||||
public readonly ?string $oddFooter = null,
|
||||
public readonly ?string $evenHeader = null,
|
||||
public readonly ?string $evenFooter = null,
|
||||
public readonly bool $differentOddEven = false,
|
||||
public ?string $oddHeader = null,
|
||||
public ?string $oddFooter = null,
|
||||
public ?string $evenHeader = null,
|
||||
public ?string $evenFooter = null,
|
||||
public bool $differentOddEven = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Writer\XLSX\Options;
|
||||
|
||||
final class PageMargin
|
||||
final readonly class PageMargin
|
||||
{
|
||||
public function __construct(
|
||||
public readonly float $top = 0.75,
|
||||
public readonly float $right = 0.7,
|
||||
public readonly float $bottom = 0.75,
|
||||
public readonly float $left = 0.7,
|
||||
public readonly float $header = 0.3,
|
||||
public readonly float $footer = 0.3
|
||||
public float $top = 0.75,
|
||||
public float $right = 0.7,
|
||||
public float $bottom = 0.75,
|
||||
public float $left = 0.7,
|
||||
public float $header = 0.3,
|
||||
public float $footer = 0.3
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Writer\XLSX\Options;
|
||||
|
||||
final class PageSetup
|
||||
final readonly class PageSetup
|
||||
{
|
||||
public readonly bool $fitToPage;
|
||||
public bool $fitToPage;
|
||||
|
||||
public function __construct(
|
||||
public readonly ?PageOrientation $pageOrientation,
|
||||
public readonly ?PaperSize $paperSize,
|
||||
public readonly ?int $fitToHeight = null,
|
||||
public readonly ?int $fitToWidth = null,
|
||||
public ?PageOrientation $pageOrientation,
|
||||
public ?PaperSize $paperSize,
|
||||
public ?int $fitToHeight = null,
|
||||
public ?int $fitToWidth = null,
|
||||
) {
|
||||
$this->fitToPage = null !== $fitToHeight || null !== $fitToWidth;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Writer\XLSX\Options;
|
||||
|
||||
use OpenSpout\Writer\XLSX\Helper\PasswordHashHelper;
|
||||
|
||||
final readonly class SheetProtection
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $password = null,
|
||||
public bool $lockSheet = false,
|
||||
public bool $lockColumnInsert = false,
|
||||
public bool $lockColumnDelete = false,
|
||||
public bool $lockColumnFormatting = false,
|
||||
public bool $lockRowInsert = false,
|
||||
public bool $lockRowDelete = false,
|
||||
public bool $lockRowFormatting = false,
|
||||
public bool $lockAutoFilter = false,
|
||||
public bool $lockSort = false,
|
||||
public bool $lockCellFormatting = false,
|
||||
public bool $lockLockedCellSelection = false,
|
||||
public bool $lockUnlockedCellsSelection = false,
|
||||
public bool $lockObjects = false,
|
||||
public bool $lockHyperlinkInsert = false,
|
||||
public bool $lockPivotTables = false,
|
||||
public bool $lockScenarios = false,
|
||||
) {}
|
||||
|
||||
public function getXml(): string
|
||||
{
|
||||
return '<sheetProtection'.$this->getSheetViewAttributes().'/>';
|
||||
}
|
||||
|
||||
private function getSheetViewAttributes(): string
|
||||
{
|
||||
return $this->generateAttributes([
|
||||
'password' => null !== $this->password ? PasswordHashHelper::make($this->password) : '',
|
||||
'sheet' => $this->lockSheet,
|
||||
'objects' => $this->lockObjects,
|
||||
'scenarios' => $this->lockScenarios,
|
||||
'formatCells' => $this->lockCellFormatting,
|
||||
'formatColumns' => $this->lockColumnFormatting,
|
||||
'formatRows' => $this->lockRowFormatting,
|
||||
'insertColumns' => $this->lockColumnInsert,
|
||||
'insertRows' => $this->lockRowInsert,
|
||||
'deleteColumns' => $this->lockColumnDelete,
|
||||
'deleteRows' => $this->lockRowDelete,
|
||||
'selectLockedCells' => $this->lockLockedCellSelection,
|
||||
'selectUnlockedCells' => $this->lockUnlockedCellsSelection,
|
||||
'autoFilter' => $this->lockAutoFilter,
|
||||
'sort' => $this->lockSort,
|
||||
'hyperlink' => $this->lockHyperlinkInsert,
|
||||
'pivotTables' => $this->lockPivotTables,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool|string> $data with key containing the attribute name and value containing the attribute value
|
||||
*/
|
||||
private function generateAttributes(array $data): string
|
||||
{
|
||||
// Create attribute for each key
|
||||
$attributes = array_map(static function (string $key, bool|string $value): string {
|
||||
if (\is_bool($value)) {
|
||||
$value = $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
return $key.'="'.$value.'"';
|
||||
}, array_keys($data), $data);
|
||||
|
||||
// Append all attributes
|
||||
return ' '.implode(' ', $attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Writer\XLSX\Options;
|
||||
|
||||
use OpenSpout\Writer\XLSX\Helper\PasswordHashHelper;
|
||||
|
||||
final readonly class WorkbookProtection
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $password = null,
|
||||
public bool $lockStructure = false,
|
||||
public bool $lockWindows = false,
|
||||
public bool $lockRevisions = false,
|
||||
) {}
|
||||
|
||||
public function getXml(): string
|
||||
{
|
||||
return '<workbookProtection'.$this->getSheetViewAttributes().'/>';
|
||||
}
|
||||
|
||||
private function getSheetViewAttributes(): string
|
||||
{
|
||||
return $this->generateAttributes([
|
||||
'workbookPassword' => null !== $this->password ? PasswordHashHelper::make($this->password) : '',
|
||||
'lockStructure' => $this->lockStructure,
|
||||
'lockWindows' => $this->lockWindows,
|
||||
'lockRevisions' => $this->lockRevisions,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool|string> $data with key containing the attribute name and value containing the attribute value
|
||||
*/
|
||||
private function generateAttributes(array $data): string
|
||||
{
|
||||
// Create attribute for each key
|
||||
$attributes = array_map(static function (string $key, bool|string $value): string {
|
||||
if (\is_bool($value)) {
|
||||
$value = $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
return $key.'="'.$value.'"';
|
||||
}, array_keys($data), $data);
|
||||
|
||||
// Append all attributes
|
||||
return ' '.implode(' ', $attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OpenSpout\Writer\XLSX;
|
||||
|
||||
final readonly class Properties
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $title = 'Untitled Spreadsheet',
|
||||
public ?string $subject = null,
|
||||
public ?string $application = 'OpenSpout',
|
||||
public ?string $creator = 'OpenSpout',
|
||||
public ?string $lastModifiedBy = 'OpenSpout',
|
||||
public ?string $keywords = null,
|
||||
public ?string $description = null,
|
||||
public ?string $category = null,
|
||||
public ?string $language = null,
|
||||
/** @var array<string, string> $customProperties */
|
||||
public array $customProperties = [],
|
||||
) {}
|
||||
}
|
||||
@@ -35,6 +35,23 @@ final class Writer extends AbstractWriterMultiSheets
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function setCreator(string $creator): void
|
||||
{
|
||||
$props = $this->options->getProperties();
|
||||
$this->options->setProperties(new Properties(
|
||||
$props->title,
|
||||
$props->subject,
|
||||
$props->application,
|
||||
$creator,
|
||||
$props->lastModifiedBy,
|
||||
$props->keywords,
|
||||
$props->description,
|
||||
$props->category,
|
||||
$props->language,
|
||||
$props->customProperties
|
||||
));
|
||||
}
|
||||
|
||||
protected function createWorkbookManager(): WorkbookManager
|
||||
{
|
||||
$workbook = new Workbook();
|
||||
@@ -43,7 +60,7 @@ final class Writer extends AbstractWriterMultiSheets
|
||||
$this->options->getTempFolder(),
|
||||
new ZipHelper(),
|
||||
new XLSX(),
|
||||
$this->creator
|
||||
$this->options->getProperties()
|
||||
);
|
||||
$fileSystemHelper->createBaseFilesAndFolders();
|
||||
|
||||
@@ -51,7 +68,12 @@ final class Writer extends AbstractWriterMultiSheets
|
||||
$sharedStringsManager = new SharedStringsManager($xlFolder, new XLSX());
|
||||
|
||||
$styleMerger = new StyleMerger();
|
||||
$styleManager = new StyleManager(new StyleRegistry($this->options->DEFAULT_ROW_STYLE));
|
||||
$escaper = new XLSX();
|
||||
|
||||
$styleManager = new StyleManager(
|
||||
new StyleRegistry($this->options->DEFAULT_ROW_STYLE),
|
||||
$escaper
|
||||
);
|
||||
|
||||
$commentsManager = new CommentsManager($xlFolder, new XLSX());
|
||||
|
||||
@@ -61,7 +83,7 @@ final class Writer extends AbstractWriterMultiSheets
|
||||
$styleMerger,
|
||||
$commentsManager,
|
||||
$sharedStringsManager,
|
||||
new XLSX(),
|
||||
$escaper,
|
||||
StringHelper::factory()
|
||||
);
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ All rights reserved.</copyright>
|
||||
<location>openspout</location>
|
||||
<name>OpenSpout</name>
|
||||
<description>Library to read and write spreadsheet files (CSV, XLSX and ODS).</description>
|
||||
<version>4.23.0</version>
|
||||
<version>4.28.5</version>
|
||||
<license>MIT</license>
|
||||
<repository>https://github.com/openspout/openspout</repository>
|
||||
<copyrights>
|
||||
|
||||
Reference in New Issue
Block a user