First steps in the implementation of AutoFilters for ODS Reader and Writer (#2053)
* First steps in the implementation of AutoFilters for ODS Reader and Writer, starting with reading a basic AutoFilter range (ignoring row visibility, filter types and active filters for the moment). And also some additional refactoring to extract the DefinedNames Reader into its own dedicated class as a part of overall code improvement... on the principle of "when working on a class, always try to leave the library codebase in a better state than you found it" * Provide a basic Ods Writer implementation for AutoFilters * AutoFilter Reader Test * AutoFilter Writer Test * Update Change Log
This commit is contained in:
parent
defef9a4ff
commit
83e55cffcc
|
|
@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
|
||||
### Added
|
||||
|
||||
- Implemented basic AutoFiltering for Ods Reader and Writer [PR #2053](https://github.com/PHPOffice/PhpSpreadsheet/pull/2053)
|
||||
- Improved support for Row and Column ranges in formulae [Issue #1755](https://github.com/PHPOffice/PhpSpreadsheet/issues/1755) [PR #2028](https://github.com/PHPOffice/PhpSpreadsheet/pull/2028)
|
||||
- Implemented URLENCODE() Web Function
|
||||
- Implemented the CHITEST(), CHISQ.DIST() and CHISQ.INV() and equivalent Statistical functions, for both left- and right-tailed distributions.
|
||||
- Support for ActiveSheet and SelectedCells in the ODS Reader and Writer. [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908)
|
||||
|
||||
|
|
|
|||
|
|
@ -1313,13 +1313,13 @@
|
|||
<td style="text-align: center; color: orange;">●</td>
|
||||
<td style="text-align: center; color: orange;">●</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: center; color: orange;">●</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: center; color: orange;">●</td>
|
||||
<td style="text-align: center; color: orange;">●</td>
|
||||
<td></td>
|
||||
<td style="text-align: center; color: orange;">●</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
|
|
|||
|
|
@ -2812,12 +2812,7 @@ parameters:
|
|||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#"
|
||||
count: 7
|
||||
path: src/PhpSpreadsheet/Reader/Ods.php
|
||||
|
||||
-
|
||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\:\\:convertToExcelAddressValue\\(\\) should return string but returns string\\|null\\.$#"
|
||||
count: 1
|
||||
count: 3
|
||||
path: src/PhpSpreadsheet/Reader/Ods.php
|
||||
|
||||
-
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ use DOMNode;
|
|||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\DefinedName;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
|
||||
|
|
@ -22,7 +23,6 @@ use PhpOffice\PhpSpreadsheet\Shared\Date;
|
|||
use PhpOffice\PhpSpreadsheet\Shared\File;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Throwable;
|
||||
use XMLReader;
|
||||
use ZipArchive;
|
||||
|
|
@ -304,8 +304,10 @@ class Ods extends BaseReader
|
|||
|
||||
$pageSettings->readStyleCrossReferences($dom);
|
||||
|
||||
// Content
|
||||
$autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
|
||||
$definedNameReader = new DefinedNames($spreadsheet, $tableNs);
|
||||
|
||||
// Content
|
||||
$spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body')
|
||||
->item(0)
|
||||
->getElementsByTagNameNS($officeNs, 'spreadsheet');
|
||||
|
|
@ -642,8 +644,8 @@ class Ods extends BaseReader
|
|||
++$worksheetID;
|
||||
}
|
||||
|
||||
$this->readDefinedRanges($spreadsheet, $workbookData, $tableNs);
|
||||
$this->readDefinedExpressions($spreadsheet, $workbookData, $tableNs);
|
||||
$autoFilterReader->read($workbookData);
|
||||
$definedNameReader->read($workbookData);
|
||||
}
|
||||
$spreadsheet->setActiveSheetIndex(0);
|
||||
|
||||
|
|
@ -771,26 +773,6 @@ class Ods extends BaseReader
|
|||
return $value;
|
||||
}
|
||||
|
||||
private function convertToExcelAddressValue(string $openOfficeAddress): string
|
||||
{
|
||||
$excelAddress = $openOfficeAddress;
|
||||
|
||||
// Cell range 3-d reference
|
||||
// As we don't support 3-d ranges, we're just going to take a quick and dirty approach
|
||||
// and assume that the second worksheet reference is the same as the first
|
||||
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress);
|
||||
// Cell range reference in another sheet
|
||||
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress);
|
||||
// Cell reference in another sheet
|
||||
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress);
|
||||
// Cell range reference
|
||||
$excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress);
|
||||
// Simple cell reference
|
||||
$excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress);
|
||||
|
||||
return $excelAddress;
|
||||
}
|
||||
|
||||
private function convertToExcelFormulaValue(string $openOfficeFormula): string
|
||||
{
|
||||
$temp = explode('"', $openOfficeFormula);
|
||||
|
|
@ -816,53 +798,4 @@ class Ods extends BaseReader
|
|||
|
||||
return $excelFormula;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read any Named Ranges that are defined in this spreadsheet.
|
||||
*/
|
||||
private function readDefinedRanges(Spreadsheet $spreadsheet, DOMElement $workbookData, string $tableNs): void
|
||||
{
|
||||
$namedRanges = $workbookData->getElementsByTagNameNS($tableNs, 'named-range');
|
||||
foreach ($namedRanges as $definedNameElement) {
|
||||
$definedName = $definedNameElement->getAttributeNS($tableNs, 'name');
|
||||
$baseAddress = $definedNameElement->getAttributeNS($tableNs, 'base-cell-address');
|
||||
$range = $definedNameElement->getAttributeNS($tableNs, 'cell-range-address');
|
||||
|
||||
$baseAddress = $this->convertToExcelAddressValue($baseAddress);
|
||||
$range = $this->convertToExcelAddressValue($range);
|
||||
|
||||
$this->addDefinedName($spreadsheet, $baseAddress, $definedName, $range);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read any Named Formulae that are defined in this spreadsheet.
|
||||
*/
|
||||
private function readDefinedExpressions(Spreadsheet $spreadsheet, DOMElement $workbookData, string $tableNs): void
|
||||
{
|
||||
$namedExpressions = $workbookData->getElementsByTagNameNS($tableNs, 'named-expression');
|
||||
foreach ($namedExpressions as $definedNameElement) {
|
||||
$definedName = $definedNameElement->getAttributeNS($tableNs, 'name');
|
||||
$baseAddress = $definedNameElement->getAttributeNS($tableNs, 'base-cell-address');
|
||||
$expression = $definedNameElement->getAttributeNS($tableNs, 'expression');
|
||||
|
||||
$baseAddress = $this->convertToExcelAddressValue($baseAddress);
|
||||
$expression = $this->convertToExcelFormulaValue($expression);
|
||||
|
||||
$this->addDefinedName($spreadsheet, $baseAddress, $definedName, $expression);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assess scope and store the Defined Name.
|
||||
*/
|
||||
private function addDefinedName(Spreadsheet $spreadsheet, string $baseAddress, string $definedName, string $value): void
|
||||
{
|
||||
[$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true);
|
||||
$worksheet = $spreadsheet->getSheetByName($sheetReference);
|
||||
// Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet
|
||||
if ($worksheet !== null) {
|
||||
$spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
|
||||
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
|
||||
class AutoFilter extends BaseReader
|
||||
{
|
||||
public function read(DOMElement $workbookData): void
|
||||
{
|
||||
$this->readAutoFilters($workbookData);
|
||||
}
|
||||
|
||||
protected function readAutoFilters(DOMElement $workbookData): void
|
||||
{
|
||||
$databases = $workbookData->getElementsByTagNameNS($this->tableNs, 'database-ranges');
|
||||
|
||||
foreach ($databases as $autofilters) {
|
||||
foreach ($autofilters->childNodes as $autofilter) {
|
||||
$autofilterRange = $this->getAttributeValue($autofilter, 'target-range-address');
|
||||
if ($autofilterRange !== null) {
|
||||
$baseAddress = $this->convertToExcelAddressValue($autofilterRange);
|
||||
$this->spreadsheet->getActiveSheet()->setAutoFilter($baseAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getAttributeValue(?DOMNode $node, string $attributeName): ?string
|
||||
{
|
||||
if ($node !== null && $node->attributes !== null) {
|
||||
$attribute = $node->attributes->getNamedItemNS(
|
||||
$this->tableNs,
|
||||
$attributeName
|
||||
);
|
||||
|
||||
if ($attribute !== null) {
|
||||
return $attribute->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
|
||||
|
||||
use DOMElement;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
abstract class BaseReader
|
||||
{
|
||||
/**
|
||||
* @var Spreadsheet
|
||||
*/
|
||||
protected $spreadsheet;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $tableNs;
|
||||
|
||||
public function __construct(Spreadsheet $spreadsheet, string $tableNs)
|
||||
{
|
||||
$this->spreadsheet = $spreadsheet;
|
||||
$this->tableNs = $tableNs;
|
||||
}
|
||||
|
||||
abstract public function read(DOMElement $workbookData): void;
|
||||
|
||||
protected function convertToExcelAddressValue(string $openOfficeAddress): string
|
||||
{
|
||||
$excelAddress = $openOfficeAddress;
|
||||
|
||||
// Cell range 3-d reference
|
||||
// As we don't support 3-d ranges, we're just going to take a quick and dirty approach
|
||||
// and assume that the second worksheet reference is the same as the first
|
||||
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress);
|
||||
// Cell range reference in another sheet
|
||||
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress ?? '');
|
||||
// Cell reference in another sheet
|
||||
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress ?? '');
|
||||
// Cell range reference
|
||||
$excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress ?? '');
|
||||
// Simple cell reference
|
||||
$excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress ?? '');
|
||||
|
||||
return $excelAddress ?? '';
|
||||
}
|
||||
|
||||
protected function convertToExcelFormulaValue(string $openOfficeFormula): string
|
||||
{
|
||||
$temp = explode('"', $openOfficeFormula);
|
||||
$tKey = false;
|
||||
foreach ($temp as &$value) {
|
||||
// @var string $value
|
||||
// Only replace in alternate array entries (i.e. non-quoted blocks)
|
||||
if ($tKey = !$tKey) {
|
||||
// Cell range reference in another sheet
|
||||
$value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value);
|
||||
// Cell reference in another sheet
|
||||
$value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? '');
|
||||
// Cell range reference
|
||||
$value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? '');
|
||||
// Simple cell reference
|
||||
$value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? '');
|
||||
|
||||
$value = Calculation::translateSeparator(';', ',', $value ?? '', $inBraces);
|
||||
}
|
||||
}
|
||||
|
||||
// Then rebuild the formula string
|
||||
$excelFormula = implode('"', $temp);
|
||||
|
||||
return $excelFormula;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
|
||||
|
||||
use DOMElement;
|
||||
use PhpOffice\PhpSpreadsheet\DefinedName;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class DefinedNames extends BaseReader
|
||||
{
|
||||
public function read(DOMElement $workbookData): void
|
||||
{
|
||||
$this->readDefinedRanges($workbookData);
|
||||
$this->readDefinedExpressions($workbookData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read any Named Ranges that are defined in this spreadsheet.
|
||||
*/
|
||||
protected function readDefinedRanges(DOMElement $workbookData): void
|
||||
{
|
||||
$namedRanges = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-range');
|
||||
foreach ($namedRanges as $definedNameElement) {
|
||||
$definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name');
|
||||
$baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address');
|
||||
$range = $definedNameElement->getAttributeNS($this->tableNs, 'cell-range-address');
|
||||
|
||||
$baseAddress = $this->convertToExcelAddressValue($baseAddress);
|
||||
$range = $this->convertToExcelAddressValue($range);
|
||||
|
||||
$this->addDefinedName($baseAddress, $definedName, $range);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read any Named Formulae that are defined in this spreadsheet.
|
||||
*/
|
||||
protected function readDefinedExpressions(DOMElement $workbookData): void
|
||||
{
|
||||
$namedExpressions = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-expression');
|
||||
foreach ($namedExpressions as $definedNameElement) {
|
||||
$definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name');
|
||||
$baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address');
|
||||
$expression = $definedNameElement->getAttributeNS($this->tableNs, 'expression');
|
||||
|
||||
$baseAddress = $this->convertToExcelAddressValue($baseAddress);
|
||||
$expression = $this->convertToExcelFormulaValue($expression);
|
||||
|
||||
$this->addDefinedName($baseAddress, $definedName, $expression);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assess scope and store the Defined Name.
|
||||
*/
|
||||
private function addDefinedName(string $baseAddress, string $definedName, string $value): void
|
||||
{
|
||||
[$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true);
|
||||
$worksheet = $this->spreadsheet->getSheetByName($sheetReference);
|
||||
// Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet
|
||||
if ($worksheet !== null) {
|
||||
$this->spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class AutoFilters
|
||||
{
|
||||
/**
|
||||
* @var XMLWriter
|
||||
*/
|
||||
private $objWriter;
|
||||
|
||||
/**
|
||||
* @var Spreadsheet
|
||||
*/
|
||||
private $spreadsheet;
|
||||
|
||||
public function __construct(XMLWriter $objWriter, Spreadsheet $spreadsheet)
|
||||
{
|
||||
$this->objWriter = $objWriter;
|
||||
$this->spreadsheet = $spreadsheet;
|
||||
}
|
||||
|
||||
public function write(): void
|
||||
{
|
||||
$wrapperWritten = false;
|
||||
$sheetCount = $this->spreadsheet->getSheetCount();
|
||||
for ($i = 0; $i < $sheetCount; ++$i) {
|
||||
$worksheet = $this->spreadsheet->getSheet($i);
|
||||
$autofilter = $worksheet->getAutoFilter();
|
||||
if ($autofilter !== null && !empty($autofilter->getRange())) {
|
||||
if ($wrapperWritten === false) {
|
||||
$this->objWriter->startElement('table:database-ranges');
|
||||
$wrapperWritten = true;
|
||||
}
|
||||
$this->objWriter->startElement('table:database-range');
|
||||
$this->objWriter->writeAttribute('table:orientation', 'column');
|
||||
$this->objWriter->writeAttribute('table:display-filter-buttons', 'true');
|
||||
$this->objWriter->writeAttribute(
|
||||
'table:target-range-address',
|
||||
$this->formatRange($worksheet, $autofilter)
|
||||
);
|
||||
$this->objWriter->endElement();
|
||||
}
|
||||
}
|
||||
|
||||
if ($wrapperWritten === true) {
|
||||
$this->objWriter->endElement();
|
||||
}
|
||||
}
|
||||
|
||||
protected function formatRange(Worksheet $worksheet, Autofilter $autofilter): string
|
||||
{
|
||||
$title = $worksheet->getTitle();
|
||||
$range = $autofilter->getRange();
|
||||
|
||||
return "'{$title}'.{$range}";
|
||||
}
|
||||
}
|
||||
|
|
@ -101,6 +101,7 @@ class Content extends WriterPart
|
|||
|
||||
$this->writeSheets($objWriter);
|
||||
|
||||
(new AutoFilters($objWriter, $this->getParentWriter()->getSpreadsheet()))->write();
|
||||
// Defined names (ranges and formulae)
|
||||
(new NamedExpressions($objWriter, $this->getParentWriter()->getSpreadsheet(), $this->formulaConvertor))->write();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Ods;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AutoFilterTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var Spreadsheet
|
||||
*/
|
||||
private $spreadsheet;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$filename = 'tests/data/Reader/Ods/AutoFilter.ods';
|
||||
$reader = new Ods();
|
||||
$this->spreadsheet = $reader->load($filename);
|
||||
}
|
||||
|
||||
public function testAutoFilterRange(): void
|
||||
{
|
||||
$worksheet = $this->spreadsheet->getActiveSheet();
|
||||
|
||||
$autoFilterRange = $worksheet->getAutoFilter()->getRange();
|
||||
|
||||
self::assertSame('A1:C9', $autoFilterRange);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Writer\Ods;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
|
||||
|
||||
class AutoFilterTest extends AbstractFunctional
|
||||
{
|
||||
public function testAutoFilterWriter(): void
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$dataSet = [
|
||||
['Year', 'Quarter', 'Sales'],
|
||||
[2020, 'Q1', 100],
|
||||
[2020, 'Q2', 120],
|
||||
[2020, 'Q3', 140],
|
||||
[2020, 'Q4', 160],
|
||||
[2021, 'Q1', 180],
|
||||
[2021, 'Q2', 75],
|
||||
[2021, 'Q3', 0],
|
||||
[2021, 'Q4', 0],
|
||||
];
|
||||
$worksheet->fromArray($dataSet, null, 'A1');
|
||||
$worksheet->getAutoFilter()->setRange('A1:C9');
|
||||
|
||||
$reloaded = $this->writeAndReload($spreadsheet, 'Ods');
|
||||
|
||||
self::assertSame('A1:C9', $reloaded->getActiveSheet()->getAutoFilter()->getRange());
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Loading…
Reference in New Issue