Merge branch 'MDL-79210-master' of https://github.com/meirzamoodle/moodle

This commit is contained in:
Ilya Tregubov 2023-09-12 10:50:11 +08:00
commit 73c8675623
No known key found for this signature in database
GPG Key ID: 0F58186F748E55C1
123 changed files with 3871 additions and 871 deletions

View File

@ -4,7 +4,7 @@ Last release package can be found in https://github.com/PHPOffice/PhpSpreadsheet
NOTICE:
* Before running composer command, make sure you have the composer version updated.
* Composer version 2.5.1 2022-12-22 15:33:54
* Composer version 2.5.5 2023-03-21 11:50:05
STEPS:
* Create a temporary folder outside your moodle installation

View File

@ -22,4 +22,4 @@ if (PHP_VERSION_ID < 50600) {
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitdb9a9022baf20d73f1edf8437bc648d7::getLoader();
return ComposerAutoloaderInitf14832faa9ea8f0ad137e596f5daa06a::getLoader();

View File

@ -429,7 +429,8 @@ class ClassLoader
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
(self::$includeFile)($file);
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
@ -560,7 +561,10 @@ class ClassLoader
return false;
}
private static function initializeIncludeClosure(): void
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
@ -574,8 +578,8 @@ class ClassLoader
* @param string $file
* @return void
*/
self::$includeFile = static function($file) {
self::$includeFile = \Closure::bind(static function($file) {
include $file;
};
}, null, null);
}
}

View File

@ -98,7 +98,7 @@ class InstalledVersions
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
@ -119,7 +119,7 @@ class InstalledVersions
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
@ -328,7 +328,9 @@ class InstalledVersions
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
@ -340,12 +342,17 @@ class InstalledVersions
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php';
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}

View File

@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitdb9a9022baf20d73f1edf8437bc648d7
class ComposerAutoloaderInitf14832faa9ea8f0ad137e596f5daa06a
{
private static $loader;
@ -24,12 +24,12 @@ class ComposerAutoloaderInitdb9a9022baf20d73f1edf8437bc648d7
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInitdb9a9022baf20d73f1edf8437bc648d7', 'loadClassLoader'), true, true);
spl_autoload_register(array('ComposerAutoloaderInitf14832faa9ea8f0ad137e596f5daa06a', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitdb9a9022baf20d73f1edf8437bc648d7', 'loadClassLoader'));
spl_autoload_unregister(array('ComposerAutoloaderInitf14832faa9ea8f0ad137e596f5daa06a', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitdb9a9022baf20d73f1edf8437bc648d7::getInitializer($loader));
call_user_func(\Composer\Autoload\ComposerStaticInitf14832faa9ea8f0ad137e596f5daa06a::getInitializer($loader));
$loader->register(true);

View File

@ -4,7 +4,7 @@
namespace Composer\Autoload;
class ComposerStaticInitdb9a9022baf20d73f1edf8437bc648d7
class ComposerStaticInitf14832faa9ea8f0ad137e596f5daa06a
{
public static $prefixLengthsPsr4 = array (
'P' =>
@ -59,9 +59,9 @@ class ComposerStaticInitdb9a9022baf20d73f1edf8437bc648d7
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitdb9a9022baf20d73f1edf8437bc648d7::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitdb9a9022baf20d73f1edf8437bc648d7::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitdb9a9022baf20d73f1edf8437bc648d7::$classMap;
$loader->prefixLengthsPsr4 = ComposerStaticInitf14832faa9ea8f0ad137e596f5daa06a::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitf14832faa9ea8f0ad137e596f5daa06a::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitf14832faa9ea8f0ad137e596f5daa06a::$classMap;
}, null, ClassLoader::class);
}

View File

@ -115,17 +115,17 @@
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.28.0",
"version_normalized": "1.28.0.0",
"version": "1.29.0",
"version_normalized": "1.29.0.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a"
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a",
"reference": "6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"shasum": ""
},
"require": {
@ -143,7 +143,7 @@
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^7.4 || ^8.0",
@ -155,12 +155,12 @@
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.2.4",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"phpunit/phpunit": "^8.5 || ^9.0 || ^10.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
@ -171,7 +171,7 @@
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"time": "2023-02-25T12:24:49+00:00",
"time": "2023-06-14T22:48:31+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@ -217,30 +217,30 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.28.0"
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.0"
},
"install-path": "../phpoffice/phpspreadsheet"
},
{
"name": "psr/http-client",
"version": "1.0.1",
"version_normalized": "1.0.1.0",
"version": "1.0.2",
"version_normalized": "1.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31",
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0"
"psr/http-message": "^1.0 || ^2.0"
},
"time": "2020-06-29T06:28:15+00:00",
"time": "2023-04-10T20:12:12+00:00",
"type": "library",
"extra": {
"branch-alias": {
@ -260,7 +260,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
@ -272,30 +272,30 @@
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client/tree/master"
"source": "https://github.com/php-fig/http-client/tree/1.0.2"
},
"install-path": "../psr/http-client"
},
{
"name": "psr/http-factory",
"version": "1.0.1",
"version_normalized": "1.0.1.0",
"version": "1.0.2",
"version_normalized": "1.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
"reference": "e616d01114759c4c489f93b099585439f795fe35"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
"reference": "e616d01114759c4c489f93b099585439f795fe35",
"shasum": ""
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0"
"psr/http-message": "^1.0 || ^2.0"
},
"time": "2019-04-30T12:38:16+00:00",
"time": "2023-04-10T20:10:41+00:00",
"type": "library",
"extra": {
"branch-alias": {
@ -315,7 +315,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interfaces for PSR-7 HTTP message factories",
@ -330,33 +330,33 @@
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory/tree/master"
"source": "https://github.com/php-fig/http-factory/tree/1.0.2"
},
"install-path": "../psr/http-factory"
},
{
"name": "psr/http-message",
"version": "1.0.1",
"version_normalized": "1.0.1.0",
"version": "2.0",
"version_normalized": "2.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": "^7.2 || ^8.0"
},
"time": "2016-08-06T14:39:51+00:00",
"time": "2023-04-04T09:54:51+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "2.0.x-dev"
}
},
"installation-source": "dist",
@ -372,7 +372,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
@ -386,7 +386,7 @@
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/master"
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"install-path": "../psr/http-message"
},

View File

@ -56,36 +56,36 @@
),
),
'phpoffice/phpspreadsheet' => array(
'pretty_version' => '1.28.0',
'version' => '1.28.0.0',
'reference' => '6e81cf39bbd93ebc3a4e8150444c41e8aa9b769a',
'pretty_version' => '1.29.0',
'version' => '1.29.0.0',
'reference' => 'fde2ccf55eaef7e86021ff1acce26479160a0fa0',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoffice/phpspreadsheet',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-client' => array(
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
'pretty_version' => '1.0.2',
'version' => '1.0.2.0',
'reference' => '0955afe48220520692d2d09f7ab7e0f93ffd6a31',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-client',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-factory' => array(
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
'pretty_version' => '1.0.2',
'version' => '1.0.2.0',
'reference' => 'e616d01114759c4c489f93b099585439f795fe35',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-factory',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-message' => array(
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
'pretty_version' => '2.0',
'version' => '2.0.0.0',
'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-message',
'aliases' => array(),

View File

@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com)
and this project adheres to [Semantic Versioning](https://semver.org).
## 1.29.0 - 2023-06-15
### Added
- Wizards for defining Number Format masks for Dates and Times, including Durations/Intervals. [PR #3458](https://github.com/PHPOffice/PhpSpreadsheet/pull/3458)
- Specify data type in html tags. [Issue #3444](https://github.com/PHPOffice/PhpSpreadsheet/issues/3444) [PR #3445](https://github.com/PHPOffice/PhpSpreadsheet/pull/3445)
- Provide option to ignore hidden rows/columns in `toArray()` methods. [PR #3494](https://github.com/PHPOffice/PhpSpreadsheet/pull/3494)
- Font/Effects/Theme support for Chart Data Labels and Axis. [PR #3476](https://github.com/PHPOffice/PhpSpreadsheet/pull/3476)
- Font Themes support. [PR #3486](https://github.com/PHPOffice/PhpSpreadsheet/pull/3486)
- Ability to Ignore Cell Errors in Excel. [Issue #1141](https://github.com/PHPOffice/PhpSpreadsheet/issues/1141) [PR #3508](https://github.com/PHPOffice/PhpSpreadsheet/pull/3508)
- Unzipped Gnumeric file [PR #3591](https://github.com/PHPOffice/PhpSpreadsheet/pull/3591)
### Changed
- Xlsx Color schemes read in will be written out (previously Excel 2007-2010 Color scheme was always written); manipulation of those schemes before write, including restoring prior behavior, is provided [PR #3476](https://github.com/PHPOffice/PhpSpreadsheet/pull/3476)
- Memory and speed optimisations for Read Filters with Xlsx Files and Shared Formulae. [PR #3474](https://github.com/PHPOffice/PhpSpreadsheet/pull/3474)
- Allow `CellRange` and `CellAddress` objects for the `range` argument in the `rangeToArray()` method. [PR #3494](https://github.com/PHPOffice/PhpSpreadsheet/pull/3494)
- Stock charts will now read and reproduce `upDownBars` and subsidiary tags; these were previously ignored on read and hard-coded on write. [PR #3515](https://github.com/PHPOffice/PhpSpreadsheet/pull/3515)
### Deprecated
- Nothing
### Removed
- Nothing
### Fixed
- Updates Cell formula absolute ranges/references, and Defined Name absolute ranges/references when inserting/deleting rows/columns. [Issue #3368](https://github.com/PHPOffice/PhpSpreadsheet/issues/3368) [PR #3402](https://github.com/PHPOffice/PhpSpreadsheet/pull/3402)
- EOMONTH() and EDATE() Functions should round date value before evaluation. [Issue #3436](https://github.com/PHPOffice/PhpSpreadsheet/issues/3436) [PR #3437](https://github.com/PHPOffice/PhpSpreadsheet/pull/3437)
- NETWORKDAYS function erroneously being converted to NETWORK_xlfn.DAYS in Xlsx Writer. [Issue #3461](https://github.com/PHPOffice/PhpSpreadsheet/issues/3461) [PR #3463](https://github.com/PHPOffice/PhpSpreadsheet/pull/3463)
- Getting a style for a CellAddress instance fails if the worksheet is set in the CellAddress instance. [Issue #3439](https://github.com/PHPOffice/PhpSpreadsheet/issues/3439) [PR #3469](https://github.com/PHPOffice/PhpSpreadsheet/pull/3469)
- Shared Formulae outside the filter range when reading with a filter are not always being identified. [Issue #3473](https://github.com/PHPOffice/PhpSpreadsheet/issues/3473) [PR #3474](https://github.com/PHPOffice/PhpSpreadsheet/pull/3474)
- Xls Reader Conditional Styles. [PR #3400](https://github.com/PHPOffice/PhpSpreadsheet/pull/3400)
- Allow use of # and 0 digit placeholders in fraction masks. [PR #3401](https://github.com/PHPOffice/PhpSpreadsheet/pull/3401)
- Modify Date/Time check in the NumberFormatter for decimal/fractional times. [PR #3413](https://github.com/PHPOffice/PhpSpreadsheet/pull/3413)
- Misplaced Xml Writing Chart Label FillColor. [Issue #3397](https://github.com/PHPOffice/PhpSpreadsheet/issues/3397) [PR #3404](https://github.com/PHPOffice/PhpSpreadsheet/pull/3404)
- TEXT function ignores Time in DateTimeStamp. [Issue #3409](https://github.com/PHPOffice/PhpSpreadsheet/issues/3409) [PR #3411](https://github.com/PHPOffice/PhpSpreadsheet/pull/3411)
- Xlsx Column Autosize Approximate for CJK. [Issue #3405](https://github.com/PHPOffice/PhpSpreadsheet/issues/3405) [PR #3416](https://github.com/PHPOffice/PhpSpreadsheet/pull/3416)
- Correct Xlsx Parsing of quotePrefix="0". [Issue #3435](https://github.com/PHPOffice/PhpSpreadsheet/issues/3435) [PR #3438](https://github.com/PHPOffice/PhpSpreadsheet/pull/3438)
- More Display Options for Chart Axis and Legend. [Issue #3414](https://github.com/PHPOffice/PhpSpreadsheet/issues/3414) [PR #3434](https://github.com/PHPOffice/PhpSpreadsheet/pull/3434)
- Apply strict type checking to Complex suffix. [PR #3452](https://github.com/PHPOffice/PhpSpreadsheet/pull/3452)
- Incorrect Font Color Read Xlsx Rich Text Indexed Color Custom Palette. [Issue #3464](https://github.com/PHPOffice/PhpSpreadsheet/issues/3464) [PR #3465](https://github.com/PHPOffice/PhpSpreadsheet/pull/3465)
- Xlsx Writer Honor Alignment in Default Font. [Issue #3443](https://github.com/PHPOffice/PhpSpreadsheet/issues/3443) [PR #3459](https://github.com/PHPOffice/PhpSpreadsheet/pull/3459)
- Support Border for Charts. [PR #3462](https://github.com/PHPOffice/PhpSpreadsheet/pull/3462)
- Error in "this row" structured reference calculation (cached result from first row when using a range) [Issue #3504](https://github.com/PHPOffice/PhpSpreadsheet/issues/3504) [PR #3505](https://github.com/PHPOffice/PhpSpreadsheet/pull/3505)
- Allow colour palette index references in Number Format masks [Issue #3511](https://github.com/PHPOffice/PhpSpreadsheet/issues/3511) [PR #3512](https://github.com/PHPOffice/PhpSpreadsheet/pull/3512)
- Xlsx Reader formula with quotePrefix [Issue #3495](https://github.com/PHPOffice/PhpSpreadsheet/issues/3495) [PR #3497](https://github.com/PHPOffice/PhpSpreadsheet/pull/3497)
- Handle REF error as part of range [Issue #3453](https://github.com/PHPOffice/PhpSpreadsheet/issues/3453) [PR #3467](https://github.com/PHPOffice/PhpSpreadsheet/pull/3467)
- Handle Absolute Pathnames in Rels File [Issue #3553](https://github.com/PHPOffice/PhpSpreadsheet/issues/3553) [PR #3554](https://github.com/PHPOffice/PhpSpreadsheet/pull/3554)
- Return Page Breaks in Order [Issue #3552](https://github.com/PHPOffice/PhpSpreadsheet/issues/3552) [PR #3555](https://github.com/PHPOffice/PhpSpreadsheet/pull/3555)
- Add position attribute for MemoryDrawing in Html [Issue #3529](https://github.com/PHPOffice/PhpSpreadsheet/issues/3529 [PR #3535](https://github.com/PHPOffice/PhpSpreadsheet/pull/3535)
- Allow Index_number as Array for VLOOKUP/HLOOKUP [Issue #3561](https://github.com/PHPOffice/PhpSpreadsheet/issues/3561 [PR #3570](https://github.com/PHPOffice/PhpSpreadsheet/pull/3570)
- Add Unsupported Options in Xml Spreadsheet [Issue #3566](https://github.com/PHPOffice/PhpSpreadsheet/issues/3566 [Issue #3568](https://github.com/PHPOffice/PhpSpreadsheet/issues/3568 [Issue #3569](https://github.com/PHPOffice/PhpSpreadsheet/issues/3569 [PR #3567](https://github.com/PHPOffice/PhpSpreadsheet/pull/3567)
- Changes to NUMBERVALUE, VALUE, DATEVALUE, TIMEVALUE [Issue #3574](https://github.com/PHPOffice/PhpSpreadsheet/issues/3574 [PR #3575](https://github.com/PHPOffice/PhpSpreadsheet/pull/3575)
- Redo calculation of color tinting [Issue #3550](https://github.com/PHPOffice/PhpSpreadsheet/issues/3550) [PR #3580](https://github.com/PHPOffice/PhpSpreadsheet/pull/3580)
- Accommodate Slash with preg_quote [PR #3582](https://github.com/PHPOffice/PhpSpreadsheet/pull/3582) [PR #3583](https://github.com/PHPOffice/PhpSpreadsheet/pull/3583) [PR #3584](https://github.com/PHPOffice/PhpSpreadsheet/pull/3584)
- HyperlinkBase Property and Html Handling of Properties [Issue #3573](https://github.com/PHPOffice/PhpSpreadsheet/issues/3573) [PR #3589](https://github.com/PHPOffice/PhpSpreadsheet/pull/3589)
- Improvements for Data Validation [Issue #3592](https://github.com/PHPOffice/PhpSpreadsheet/issues/3592) [Issue #3594](https://github.com/PHPOffice/PhpSpreadsheet/issues/3594) [PR #3605](https://github.com/PHPOffice/PhpSpreadsheet/pull/3605)
## 1.28.0 - 2023-02-25
### Added

View File

@ -2,19 +2,44 @@
If you would like to contribute, here are some notes and guidelines:
- All new development happens on feature/fix branches, and are then merged to the `master` branch once stable; so the `master` branch is always the most up-to-date, working code
- Tagged releases are made from the `master` branch
- If you are going to be submitting a pull request, please fork from `master`, and submit your pull request back as a fix/feature branch referencing the GitHub issue number
- Code style might be automatically fixed by `composer fix`
- All code changes must be validated by `composer check`
- All new development should be on feature/fix branches, which are then merged to the `master` branch once stable and approved; so the `master` branch is always the most up-to-date, working code
- If you are going to submit a pull request, please fork from `master`, and submit your pull request back as a fix/feature branch referencing the GitHub issue number
- The code must work with all PHP versions that we support (currently PHP 7.4 to PHP 8.2).
- You can call `composer versions` to test version compatibility.
- Code style should be maintained.
- `composer style` will identify any issues with Coding Style`.
- `composer fix` will fix most issues with Coding Style.
- All code changes must be validated by `composer check`.
- Please include Unit Tests to verify that a bug exists, and that this PR fixes it.
- Please include Unit Tests to show that a new Feature works as expected.
- Please don't "bundle" several changes into a single PR; submit a PR for each discrete change/fix.
- Remember to update documentation if necessary.
- [Helpful article about forking](https://help.github.com/articles/fork-a-repo/ "Forking a GitHub repository")
- [Helpful article about pull requests](https://help.github.com/articles/using-pull-requests/ "Pull Requests")
## Unit Tests
When writing Unit Tests, please
- Always try to write Unit Tests for both the happy and unhappy paths.
- Put all assertions in the Test itself, not in an abstract class that the Test extends (even if this means code duplication between tests).
- Include any necessary `setup()` and `tearDown()` in the Test itself.
- If you change any global settings (such as system locale, or Compatibility Mode for Excel Function tests), make sure that you reset to the default in the `tearDown()`.
- Use the `ExcelError` functions in assertions for Excel Error values in Excel Function implementations.
<br />Not only does it reduce the risk of typos; but at some point in the future, ExcelError values will be an object rather than a string, and we won't then need to update all the tests.
- Don't over-complicate test code by testing happy and unhappy paths in the same test.
This makes it easier to see exactly what is being tested when reviewing the PR. I want to be able to see it in the PR, not have to hunt in other unchanged classes to see what the test is doing.
## How to release
1. Complete CHANGELOG.md and commit
2. Create an annotated tag
1. `git tag -a 1.2.3`
2. Tag subject must be the version number, eg: `1.2.3`
3. Tag body must be a copy-paste of the changelog entries
3. Push tag with `git push --tags`, GitHub Actions will create a GitHub release automatically
3. Tag body must be a copy-paste of the changelog entries.
3. Push the tag with `git push --tags`, GitHub Actions will create a GitHub release automatically, and the release details will automatically be sent to packagist.
4. Github seems to remove markdown headings in the Release Notes, so you should edit to restore these.
> **Note:** Tagged releases are made from the `master` branch. Only in an emergency should a tagged release be made from the `release` branch. (i.e. cherry-picked hot-fixes.)

View File

@ -32,7 +32,7 @@ If you are building your installation on a development machine that is on a diff
```json
{
"require": {
"phpoffice/phpspreadsheet": "^1.23"
"phpoffice/phpspreadsheet": "^1.28"
},
"config": {
"platform": {
@ -74,16 +74,20 @@ or the appropriate PDF Writer wrapper for the library that you have chosen to in
For Chart export, we support following packages, which you will also need to install yourself using `composer require`
- [jpgraph/jpgraph](https://packagist.org/packages/jpgraph/jpgraph) (this package was abandoned at version 4.0.
You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/))
- [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) (fork with php 8.1 support)
- [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) - up to date fork with modern PHP versions support and some bugs fixed.
and then configure PhpSpreadsheet using:
```php
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph
// to use jpgraph/jpgraph
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class);
//or
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); // to use mitoteam/jpgraph
// to use mitoteam/jpgraph
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class);
```
One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts.
One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts; or to render a Chart to an Image format from within your code.
They are not necessary to define charts for writing to `Xlsx` files.
Other file formats don't support writing Charts.
## Documentation
@ -103,10 +107,15 @@ Posts already available to Patreon supporters:
- Looping the Loop
- Advice on Iterating through the rows and cells in a worksheet.
The next post (currently being written) will be:
And for Patrons at levels actively using PhpSpreadsheet:
- Behind the Mask
- A look at Number Format Masks.
The Next Article (currently Work in Progress):
- Formula for Success
- How to debug formulae that don't produce the expected result.
My aim is to post at least one article each month, taking a detailed look at some feature of MS Excel and how to use that feature in PhpSpreadsheet, or on how to perform different activities in PhpSpreadsheet.
Planned posts for the future include topics like:
@ -116,8 +125,9 @@ Planned posts for the future include topics like:
- Array Formulae
- Conditional Formatting
- Data Validation
- Formula Debugging
- Value Binders
- Images
- Charts
After a period of six months exclusive to Patreon supporters, articles will be incorporated into the public documentation for the library.

View File

@ -42,13 +42,19 @@
],
"scripts": {
"check": [
"phpcs src/ tests/ --report=checkstyle",
"phpcs --report-width=200 samples/ src/ tests/ --ignore=samples/Header.php --standard=PHPCompatibility --runtime-set testVersion 7.4- -n",
"php-cs-fixer fix --ansi --dry-run --diff",
"phpcs",
"phpunit --color=always",
"phpstan analyse --ansi"
"phpstan analyse --ansi --memory-limit=2048M"
],
"style": [
"phpcs src/ tests/ --report=checkstyle",
"php-cs-fixer fix --ansi --dry-run --diff"
],
"fix": [
"php-cs-fixer fix --ansi"
"phpcbf src/ tests/ --report=checkstyle",
"php-cs-fixer fix"
],
"versions": [
"phpcs --report-width=200 samples/ src/ tests/ --ignore=samples/Header.php --standard=PHPCompatibility --runtime-set testVersion 7.4- -n"
@ -70,7 +76,7 @@
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"psr/http-client": "^1.0",
@ -81,12 +87,12 @@
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.2.4",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"phpunit/phpunit": "^8.5 || ^9.0 || ^10.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" bootstrap="./tests/bootstrap.php" backupGlobals="true" colors="true" cacheResultFile="/tmp/.phpspreadsheet.phpunit.result.cache">
<coverage/>
<php>
<ini name="memory_limit" value="2048M"/>
</php>
<testsuite name="PhpSpreadsheet Unit Test Suite">
<directory>./tests/PhpSpreadsheetTests</directory>
</testsuite>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>

View File

@ -19,6 +19,7 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use ReflectionClassConstant;
use ReflectionMethod;
use ReflectionParameter;
use Throwable;
class Calculation
{
@ -3556,7 +3557,7 @@ class Calculation
}
}
throw new Exception($e->getMessage());
throw new Exception($e->getMessage(), $e->getCode(), $e);
}
if ((is_array($result)) && (self::$returnArrayAsType != self::RETURN_ARRAY_AS_ARRAY)) {
@ -4210,7 +4211,7 @@ class Calculation
try {
$this->branchPruner->closingBrace($d['value']);
} catch (Exception $e) {
return $this->raiseFormulaError($e->getMessage());
return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
}
$functionName = $matches[1]; // Get the function name
@ -4249,7 +4250,7 @@ class Calculation
} elseif ($expectedArgumentCount != '*') {
$isOperandOrFunction = preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch);
self::doNothing($isOperandOrFunction);
switch ($argMatch[2]) {
switch ($argMatch[2] ?? '') {
case '+':
if ($argumentCount < $argMatch[1]) {
$argumentCountError = true;
@ -4282,7 +4283,7 @@ class Calculation
try {
$this->branchPruner->argumentSeparator();
} catch (Exception $e) {
return $this->raiseFormulaError($e->getMessage());
return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
}
while (($o2 = $stack->pop()) && $o2['value'] !== '(') { // Pop off the stack back to the last (
@ -4364,9 +4365,13 @@ class Calculation
$rangeStartCellRef = $output[count($output) - 2]['value'] ?? '';
}
preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches);
if (array_key_exists(2, $rangeStartMatches)) {
if ($rangeStartMatches[2] > '') {
$val = $rangeStartMatches[2] . '!' . $val;
}
} else {
$val = Information\ExcelError::REF();
}
} else {
$rangeStartCellRef = $output[count($output) - 1]['value'] ?? '';
if ($rangeStartCellRef === ':') {
@ -4391,7 +4396,7 @@ class Calculation
try {
$structuredReference = Operands\StructuredReference::fromParser($formula, $index, $matches);
} catch (Exception $e) {
return $this->raiseFormulaError($e->getMessage());
return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
}
$val = $structuredReference->value();
@ -4434,6 +4439,8 @@ class Calculation
}
$val = $address;
}
} elseif ($val === Information\ExcelError::REF()) {
$stackItemReference = $val;
} else {
$startRowColRef = $output[count($output) - 1]['value'] ?? '';
[$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
@ -4731,7 +4738,7 @@ class Calculation
$cellRange = $token->parse($cell);
if (strpos($cellRange, ':') !== false) {
$this->debugLog->writeDebugLog('Evaluating Structured Reference %s as Cell Range %s', $token->value(), $cellRange);
$rangeValue = self::getInstance($cell->getWorksheet()->getParent())->_calculateFormulaValue("={$cellRange}", $token->value(), $cell);
$rangeValue = self::getInstance($cell->getWorksheet()->getParent())->_calculateFormulaValue("={$cellRange}", $cellRange, $cell);
$stack->push('Value', $rangeValue);
$this->debugLog->writeDebugLog('Evaluated Structured Reference %s as value %s', $token->value(), $this->showValue($rangeValue));
} else {
@ -4745,7 +4752,7 @@ class Calculation
$stack->push('Error', Information\ExcelError::REF(), null);
$this->debugLog->writeDebugLog('Evaluated Structured Reference %s as error value %s', $token->value(), Information\ExcelError::REF());
} else {
return $this->raiseFormulaError($e->getMessage());
return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
}
}
} elseif (!is_numeric($token) && !is_object($token) && isset(self::BINARY_OPERATORS[$token])) {
@ -4793,7 +4800,7 @@ class Calculation
}
}
}
if (strpos($operand1Data['reference'], '!') !== false) {
if (strpos($operand1Data['reference'] ?? '', '!') !== false) {
[$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
} else {
$sheet1 = ($pCellWorksheet !== null) ? $pCellWorksheet->getTitle() : '';
@ -4830,10 +4837,21 @@ class Calculation
$oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
$oCol = $oRow = [];
$breakNeeded = false;
foreach ($oData as $oDatum) {
try {
$oCR = Coordinate::coordinateFromString($oDatum);
$oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
$oRow[] = $oCR[1];
} catch (\Exception $e) {
$stack->push('Error', Information\ExcelError::REF(), null);
$breakNeeded = true;
break;
}
}
if ($breakNeeded) {
break;
}
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
if ($pCellParent !== null && $this->spreadsheet !== null) {
@ -4842,8 +4860,10 @@ class Calculation
return $this->raiseFormulaError('Unable to access Cell Reference');
}
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellValue));
$stack->push('Cell Reference', $cellValue, $cellRef);
} else {
$this->debugLog->writeDebugLog('Evaluation Result is a #REF! Error');
$stack->push('Error', Information\ExcelError::REF(), null);
}
@ -5434,13 +5454,13 @@ class Calculation
*
* @return false
*/
protected function raiseFormulaError(string $errorMessage)
protected function raiseFormulaError(string $errorMessage, int $code = 0, ?Throwable $exception = null)
{
$this->formulaError = $errorMessage;
$this->cyclicReferenceStack->clear();
$suppress = /** @scrutinizer ignore-deprecated */ $this->suppressFormulaErrors ?? $this->suppressFormulaErrorsNew;
if (!$suppress) {
throw new Exception($errorMessage);
throw new Exception($errorMessage, $code, $exception);
}
return false;

View File

@ -45,6 +45,11 @@ class DateValue
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
}
// try to parse as date iff there is at least one digit
if (is_string($dateValue) && preg_match('/\\d/', $dateValue) !== 1) {
return ExcelError::VALUE();
}
$dti = new DateTimeImmutable();
$baseYear = SharedDateHelper::getExcelCalendar();
$dateValue = trim($dateValue ?? '', '"');

View File

@ -45,6 +45,7 @@ class Month
} catch (Exception $e) {
return $e->getMessage();
}
$dateValue = floor($dateValue);
$adjustmentMonths = floor($adjustmentMonths);
// Execute function
@ -88,6 +89,7 @@ class Month
} catch (Exception $e) {
return $e->getMessage();
}
$dateValue = floor($dateValue);
$adjustmentMonths = floor($adjustmentMonths);
// Execute function

View File

@ -42,6 +42,11 @@ class TimeValue
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue);
}
// try to parse as time iff there is at least one digit
if (is_string($timeValue) && preg_match('/\\d/', $timeValue) !== 1) {
return ExcelError::VALUE();
}
$timeValue = trim($timeValue ?? '', '"');
$timeValue = str_replace(['/', '.'], '-', $timeValue);

View File

@ -48,9 +48,9 @@ class FormattedNumber
*/
public static function convertToNumberIfNumeric(string &$operand): bool
{
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator());
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
$value = preg_replace(['/(\d)' . $thousandsSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1$2', '$1$2'], trim($operand));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator());
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/');
$value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? '');
if (is_numeric($value)) {
@ -90,9 +90,9 @@ class FormattedNumber
*/
public static function convertToNumberIfPercent(string &$operand): bool
{
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator());
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
$value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', trim($operand));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator());
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/');
$value = preg_replace(['/(\d)' . $decimalSeparator . '(\d)/u', '/([+-])\s+(\d)/u'], ['$1.$2', '$1$2'], $value ?? '');
$match = [];
@ -116,26 +116,31 @@ class FormattedNumber
public static function convertToNumberIfCurrency(string &$operand): bool
{
$currencyRegexp = self::currencyMatcherRegexp();
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator());
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
$value = preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $operand);
$match = [];
if ($value !== null && preg_match($currencyRegexp, $value, $match, PREG_UNMATCHED_AS_NULL)) {
//Determine the sign
$sign = ($match['PrefixedSign'] ?? $match['PrefixedSign2'] ?? $match['PostfixedSign']) ?? '';
$decimalSeparator = StringHelper::getDecimalSeparator();
//Cast to a float
$operand = (float) ($sign . ($match['PostfixedValue'] ?? $match['PrefixedValue']));
$intermediate = (string) ($match['PostfixedValue'] ?? $match['PrefixedValue']);
$intermediate = str_replace($decimalSeparator, '.', $intermediate);
if (is_numeric($intermediate)) {
$operand = (float) ($sign . str_replace($decimalSeparator, '.', $intermediate));
return true;
}
}
return false;
}
public static function currencyMatcherRegexp(): string
{
$currencyCodes = sprintf(self::CURRENCY_CONVERSION_LIST, preg_quote(StringHelper::getCurrencyCode()));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator());
$currencyCodes = sprintf(self::CURRENCY_CONVERSION_LIST, preg_quote(StringHelper::getCurrencyCode(), '/'));
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator(), '/');
return '~^(?:(?: *(?<PrefixedSign>[-+])? *(?<PrefixedCurrency>[' . $currencyCodes . ']) *(?<PrefixedSign2>[-+])? *(?<PrefixedValue>[0-9]+[' . $decimalSeparator . ']?[0-9*]*(?:E[-+]?[0-9]*)?) *)|(?: *(?<PostfixedSign>[-+])? *(?<PostfixedValue>[0-9]+' . $decimalSeparator . '?[0-9]*(?:E[-+]?[0-9]*)?) *(?<PostfixedCurrency>[' . $currencyCodes . ']) *))$~ui';
}

View File

@ -190,8 +190,8 @@ final class StructuredReference implements Operand
{
if ($columnName !== '') {
$cellReference = $columnId . $cell->getRow();
$pattern1 = '/\[' . preg_quote($columnName) . '\]/miu';
$pattern2 = '/@' . preg_quote($columnName) . '/miu';
$pattern1 = '/\[' . preg_quote($columnName, '/') . '\]/miu';
$pattern2 = '/@' . preg_quote($columnName, '/') . '/miu';
if (preg_match($pattern1, $reference) === 1) {
$reference = preg_replace($pattern1, $cellReference, $reference);
} elseif (preg_match($pattern2, $reference) === 1) {
@ -328,7 +328,7 @@ final class StructuredReference implements Operand
$cellFrom = "{$columnId}{$startRow}";
$cellTo = "{$columnId}{$endRow}";
$cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
$pattern = '/\[' . preg_quote($columnName) . '\]/mui';
$pattern = '/\[' . preg_quote($columnName, '/') . '\]/mui';
if (preg_match($pattern, $reference) === 1) {
$columnsSelected = true;
$reference = preg_replace($pattern, $cellReference, $reference);

View File

@ -20,28 +20,6 @@ class Engineering
*/
public const EULER = 2.71828182845904523536;
/**
* parseComplex.
*
* Parses a complex number into its real and imaginary parts, and an I or J suffix
*
* @deprecated 1.12.0 No longer used by internal code. Please use the \Complex\Complex class instead
*
* @param string $complexNumber The complex number
*
* @return mixed[] Indexed on "real", "imaginary" and "suffix"
*/
public static function parseComplex($complexNumber)
{
$complex = new Complex($complexNumber);
return [
'real' => $complex->getReal(),
'imaginary' => $complex->getImaginary(),
'suffix' => $complex->getSuffix(),
];
}
/**
* BESSELI.
*

View File

@ -49,7 +49,7 @@ class Complex
return $e->getMessage();
}
if (($suffix == 'i') || ($suffix == 'j') || ($suffix == '')) {
if (($suffix === 'i') || ($suffix === 'j') || ($suffix === '')) {
$complex = new ComplexObject($realNumber, $imaginary, $suffix);
return (string) $complex;

View File

@ -40,7 +40,7 @@ class ConvertBinary extends ConvertBase
return $e->getMessage();
}
if (strlen($value) == 10) {
if (strlen($value) == 10 && $value[0] === '1') {
// Two's Complement
$value = substr($value, -9);
@ -91,7 +91,7 @@ class ConvertBinary extends ConvertBase
return $e->getMessage();
}
if (strlen($value) == 10) {
if (strlen($value) == 10 && $value[0] === '1') {
$high2 = substr($value, 0, 2);
$low8 = substr($value, 2);
$xarr = ['00' => '00000000', '01' => '00000001', '10' => 'FFFFFFFE', '11' => 'FFFFFFFF'];
@ -144,7 +144,7 @@ class ConvertBinary extends ConvertBase
return $e->getMessage();
}
if (strlen($value) == 10 && substr($value, 0, 1) === '1') { // Two's Complement
if (strlen($value) == 10 && $value[0] === '1') { // Two's Complement
return str_repeat('7', 6) . strtoupper(decoct((int) bindec("11$value")));
}
$octVal = (string) decoct((int) bindec($value));

View File

@ -27,7 +27,7 @@ class HLookup extends LookupBase
*/
public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true)
{
if (is_array($lookupValue)) {
if (is_array($lookupValue) || is_array($indexNumber)) {
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
}

View File

@ -26,7 +26,7 @@ class VLookup extends LookupBase
*/
public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true)
{
if (is_array($lookupValue)) {
if (is_array($lookupValue) || is_array($indexNumber)) {
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
}

View File

@ -66,8 +66,8 @@ class Sum
$returnValue += (int) $arg;
} elseif (ErrorValue::isError($arg)) {
return $arg;
// ignore non-numerics from cell, but fail as literals (except null)
} elseif ($arg !== null && !Functions::isCellValue($k)) {
// ignore non-numerics from cell, but fail as literals (except null)
return ExcelError::VALUE();
}
}

View File

@ -261,7 +261,7 @@ class Extract
$delimiter = Functions::flattenArray($delimiter);
$quotedDelimiters = array_map(
function ($delimiter) {
return preg_quote($delimiter ?? '');
return preg_quote($delimiter ?? '', '/');
},
$delimiter
);
@ -270,7 +270,7 @@ class Extract
return '(' . $delimiters . ')';
}
return '(' . preg_quote($delimiter ?? '') . ')';
return '(' . preg_quote($delimiter ?? '', '/') . ')';
}
private static function matchFlags(int $matchMode): string

View File

@ -129,7 +129,7 @@ class Format
$format = Helpers::extractString($format);
if (!is_numeric($value) && Date::isDateTimeFormatCode($format)) {
$value = DateTimeExcel\DateValue::fromString($value);
$value = DateTimeExcel\DateValue::fromString($value) + DateTimeExcel\TimeValue::fromString($value);
}
return (string) NumberFormat::toFormattedString($value, $format);
@ -140,7 +140,7 @@ class Format
*
* @return mixed
*/
private static function convertValue($value)
private static function convertValue($value, bool $spacesMeanZero = false)
{
$value = $value ?? 0;
if (is_bool($value)) {
@ -150,6 +150,12 @@ class Format
throw new CalcExp(ExcelError::VALUE());
}
}
if (is_string($value)) {
$value = trim($value);
if ($spacesMeanZero && $value === '') {
$value = 0;
}
}
return $value;
}
@ -181,6 +187,9 @@ class Format
'',
trim($value, " \t\n\r\0\x0B" . StringHelper::getCurrencyCode())
);
if ($numberValue === '') {
return ExcelError::VALUE();
}
if (is_numeric($numberValue)) {
return (float) $numberValue;
}
@ -277,7 +286,7 @@ class Format
}
try {
$value = self::convertValue($value);
$value = self::convertValue($value, true);
$decimalSeparator = self::getDecimalSeparator($decimalSeparator);
$groupSeparator = self::getGroupSeparator($groupSeparator);
} catch (CalcExp $e) {
@ -285,12 +294,12 @@ class Format
}
if (!is_numeric($value)) {
$decimalPositions = preg_match_all('/' . preg_quote($decimalSeparator) . '/', $value, $matches, PREG_OFFSET_CAPTURE);
$decimalPositions = preg_match_all('/' . preg_quote($decimalSeparator, '/') . '/', $value, $matches, PREG_OFFSET_CAPTURE);
if ($decimalPositions > 1) {
return ExcelError::VALUE();
}
$decimalOffset = array_pop($matches[0])[1]; // @phpstan-ignore-line
if (strpos($value, $groupSeparator, $decimalOffset) !== false) {
$decimalOffset = array_pop($matches[0])[1] ?? null;
if ($decimalOffset === null || strpos($value, $groupSeparator, $decimalOffset) !== false) {
return ExcelError::VALUE();
}

View File

@ -193,7 +193,7 @@ class Text
if (is_array($delimiter) && count($valueSet) > 1) {
$quotedDelimiters = array_map(
function ($delimiter) {
return preg_quote($delimiter ?? '');
return preg_quote($delimiter ?? '', '/');
},
$valueSet
);
@ -202,7 +202,7 @@ class Text
return '(' . $delimiters . ')';
}
return '(' . preg_quote(/** @scrutinizer ignore-type */ Functions::flattenSingleValue($delimiter)) . ')';
return '(' . preg_quote(/** @scrutinizer ignore-type */ Functions::flattenSingleValue($delimiter), '/') . ')';
}
private static function matchFlags(bool $matchMode): string

View File

@ -51,8 +51,9 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
return $this->setImproperFraction($matches, $cell);
}
$decimalSeparator = preg_quote(StringHelper::getDecimalSeparator());
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator());
$decimalSeparatorNoPreg = StringHelper::getDecimalSeparator();
$decimalSeparator = preg_quote($decimalSeparatorNoPreg, '/');
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
// Check for percentage
if (preg_match('/^\-?\d*' . $decimalSeparator . '?\d*\s?\%$/', preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value))) {
@ -64,7 +65,7 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
// Convert value to number
$sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null;
$currencyCode = $matches['PrefixedCurrency'] ?? $matches['PostfixedCurrency'];
$value = (float) ($sign . trim(str_replace([$decimalSeparator, $currencyCode, ' ', '-'], ['.', '', '', ''], preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value)))); // @phpstan-ignore-line
$value = (float) ($sign . trim(str_replace([$decimalSeparatorNoPreg, $currencyCode, ' ', '-'], ['.', '', '', ''], preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value)))); // @phpstan-ignore-line
return $this->setCurrency($value, $cell, $currencyCode); // @phpstan-ignore-line
}

View File

@ -71,6 +71,9 @@ class Cell
*/
private $formulaAttributes;
/** @var IgnoredErrors */
private $ignoredErrors;
/**
* Update the cell into the cell collection.
*
@ -119,6 +122,7 @@ class Cell
} elseif (self::getValueBinder()->bindValue($this, $value) === false) {
throw new Exception('Value could not be bound to cell.');
}
$this->ignoredErrors = new IgnoredErrors();
}
/**
@ -391,7 +395,9 @@ class Cell
}
throw new \PhpOffice\PhpSpreadsheet\Calculation\Exception(
$this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage()
$this->getWorksheet()->getTitle() . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(),
$ex->getCode(),
$ex
);
}
@ -794,4 +800,9 @@ class Cell
{
return (string) $this->getValue();
}
public function getIgnoredErrors(): IgnoredErrors
{
return $this->ignoredErrors;
}
}

View File

@ -20,7 +20,7 @@ class DataValidator
*/
public function isValid(Cell $cell)
{
if (!$cell->hasDataValidation()) {
if (!$cell->hasDataValidation() || $cell->getDataValidation()->getType() === DataValidation::TYPE_NONE) {
return true;
}
@ -31,13 +31,55 @@ class DataValidator
return false;
}
// TODO: write check on all cases
switch ($dataValidation->getType()) {
case DataValidation::TYPE_LIST:
return $this->isValueInList($cell);
$returnValue = false;
$type = $dataValidation->getType();
if ($type === DataValidation::TYPE_LIST) {
$returnValue = $this->isValueInList($cell);
} elseif ($type === DataValidation::TYPE_WHOLE) {
if (!is_numeric($cellValue) || fmod((float) $cellValue, 1) != 0) {
$returnValue = false;
} else {
$returnValue = $this->numericOperator($dataValidation, (int) $cellValue);
}
} elseif ($type === DataValidation::TYPE_DECIMAL || $type === DataValidation::TYPE_DATE || $type === DataValidation::TYPE_TIME) {
if (!is_numeric($cellValue)) {
$returnValue = false;
} else {
$returnValue = $this->numericOperator($dataValidation, (float) $cellValue);
}
} elseif ($type === DataValidation::TYPE_TEXTLENGTH) {
$returnValue = $this->numericOperator($dataValidation, mb_strlen((string) $cellValue));
}
return false;
return $returnValue;
}
/** @param float|int $cellValue */
private function numericOperator(DataValidation $dataValidation, $cellValue): bool
{
$operator = $dataValidation->getOperator();
$formula1 = $dataValidation->getFormula1();
$formula2 = $dataValidation->getFormula2();
$returnValue = false;
if ($operator === DataValidation::OPERATOR_BETWEEN) {
$returnValue = $cellValue >= $formula1 && $cellValue <= $formula2;
} elseif ($operator === DataValidation::OPERATOR_NOTBETWEEN) {
$returnValue = $cellValue < $formula1 || $cellValue > $formula2;
} elseif ($operator === DataValidation::OPERATOR_EQUAL) {
$returnValue = $cellValue == $formula1;
} elseif ($operator === DataValidation::OPERATOR_NOTEQUAL) {
$returnValue = $cellValue != $formula1;
} elseif ($operator === DataValidation::OPERATOR_LESSTHAN) {
$returnValue = $cellValue < $formula1;
} elseif ($operator === DataValidation::OPERATOR_LESSTHANOREQUAL) {
$returnValue = $cellValue <= $formula1;
} elseif ($operator === DataValidation::OPERATOR_GREATERTHAN) {
$returnValue = $cellValue > $formula1;
} elseif ($operator === DataValidation::OPERATOR_GREATERTHANOREQUAL) {
$returnValue = $cellValue >= $formula1;
}
return $returnValue;
}
/**

View File

@ -0,0 +1,66 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Cell;
class IgnoredErrors
{
/** @var bool */
private $numberStoredAsText = false;
/** @var bool */
private $formula = false;
/** @var bool */
private $twoDigitTextYear = false;
/** @var bool */
private $evalError = false;
public function setNumberStoredAsText(bool $value): self
{
$this->numberStoredAsText = $value;
return $this;
}
public function getNumberStoredAsText(): bool
{
return $this->numberStoredAsText;
}
public function setFormula(bool $value): self
{
$this->formula = $value;
return $this;
}
public function getFormula(): bool
{
return $this->formula;
}
public function setTwoDigitTextYear(bool $value): self
{
$this->twoDigitTextYear = $value;
return $this;
}
public function getTwoDigitTextYear(): bool
{
return $this->twoDigitTextYear;
}
public function setEvalError(bool $value): self
{
$this->evalError = $value;
return $this;
}
public function getEvalError(): bool
{
return $this->evalError;
}
}

View File

@ -118,7 +118,7 @@ class CellReferenceHelper
{
$newColumn = Coordinate::stringFromColumnIndex(min($newColumnIndex + $this->numberOfColumns, AddressRange::MAX_COLUMN_INT));
return $absoluteColumn . $newColumn;
return "{$absoluteColumn}{$newColumn}";
}
protected function updateRowReference(int $newRowIndex, string $absoluteRow): string
@ -126,6 +126,6 @@ class CellReferenceHelper
$newRow = $newRowIndex + $this->numberOfRows;
$newRow = ($newRow > AddressRange::MAX_ROW) ? AddressRange::MAX_ROW : $newRow;
return $absoluteRow . (string) $newRow;
return "{$absoluteRow}{$newRow}";
}
}

View File

@ -52,6 +52,9 @@ class Axis extends Properties
/** @var string */
private $axisType = '';
/** @var ?AxisText */
private $axisText;
/**
* Axis Options.
*
@ -88,6 +91,9 @@ class Axis extends Properties
Properties::FORMAT_CODE_DATE_ISO8601,
];
/** @var bool */
private $noFill = false;
/**
* Get Series Data Type.
*
@ -183,6 +189,14 @@ class Axis extends Properties
*/
public function getAxisOptionsProperty($property)
{
if ($property === 'textRotation') {
if ($this->axisText !== null) {
if ($this->axisText->getRotation() !== null) {
return (string) $this->axisText->getRotation();
}
}
}
return $this->axisOptions[$property];
}
@ -295,4 +309,28 @@ class Axis extends Properties
return $this;
}
public function getAxisText(): ?AxisText
{
return $this->axisText;
}
public function setAxisText(?AxisText $axisText): self
{
$this->axisText = $axisText;
return $this;
}
public function setNoFill(bool $noFill): self
{
$this->noFill = $noFill;
return $this;
}
public function getNoFill(): bool
{
return $this->noFill;
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\Style\Font;
class AxisText extends Properties
{
/** @var ?int */
private $rotation;
/** @var Font */
private $font;
public function __construct()
{
parent::__construct();
$this->font = new Font();
$this->font->setSize(null, true);
}
public function setRotation(?int $rotation): self
{
$this->rotation = $rotation;
return $this;
}
public function getRotation(): ?int
{
return $this->rotation;
}
public function getFillColorObject(): ChartColor
{
$fillColor = $this->font->getChartColor();
if ($fillColor === null) {
$fillColor = new ChartColor();
$this->font->setChartColorFromObject($fillColor);
}
return $fillColor;
}
public function getFont(): Font
{
return $this->font;
}
public function setFont(Font $font): self
{
$this->font = $font;
return $this;
}
}

View File

@ -150,6 +150,12 @@ class Chart
/** @var bool */
private $roundedCorners = false;
/** @var GridLines */
private $borderLines;
/** @var ChartColor */
private $fillColor;
/**
* Create a new Chart.
* majorGridlines and minorGridlines are deprecated, moved to Axis.
@ -176,6 +182,8 @@ class Chart
if ($minorGridlines !== null) {
$this->yAxis->setMinorGridlines($minorGridlines);
}
$this->fillColor = new ChartColor();
$this->borderLines = new GridLines();
}
/**
@ -786,4 +794,21 @@ class Chart
return $this;
}
public function getBorderLines(): GridLines
{
return $this->borderLines;
}
public function setBorderLines(GridLines $borderLines): self
{
$this->borderLines = $borderLines;
return $this;
}
public function getFillColor(): ChartColor
{
return $this->fillColor;
}
}

View File

@ -2,6 +2,8 @@
namespace PhpOffice\PhpSpreadsheet\Chart;
use PhpOffice\PhpSpreadsheet\Style\Font;
class Layout
{
/**
@ -127,8 +129,11 @@ class Layout
/** @var ?ChartColor */
private $labelBorderColor;
/** @var ?ChartColor */
private $labelFontColor;
/** @var ?Font */
private $labelFont;
/** @var Properties */
private $labelEffects;
/**
* Create a new Layout.
@ -172,7 +177,18 @@ class Layout
$this->initBoolean($layout, 'numFmtLinked');
$this->initColor($layout, 'labelFillColor');
$this->initColor($layout, 'labelBorderColor');
$this->initColor($layout, 'labelFontColor');
$labelFont = $layout['labelFont'] ?? null;
if ($labelFont instanceof Font) {
$this->labelFont = $labelFont;
}
$labelFontColor = $layout['labelFontColor'] ?? null;
if ($labelFontColor instanceof ChartColor) {
$this->setLabelFontColor($labelFontColor);
}
$labelEffects = $layout['labelEffects'] ?? null;
if ($labelEffects instanceof Properties) {
$this->labelEffects = $labelEffects;
}
}
private function initBoolean(array $layout, string $name): void
@ -493,14 +509,32 @@ class Layout
return $this;
}
public function getLabelFont(): ?Font
{
return $this->labelFont;
}
public function getLabelEffects(): ?Properties
{
return $this->labelEffects;
}
public function getLabelFontColor(): ?ChartColor
{
return $this->labelFontColor;
if ($this->labelFont === null) {
return null;
}
return $this->labelFont->getChartColor();
}
public function setLabelFontColor(?ChartColor $chartColor): self
{
$this->labelFontColor = $chartColor;
if ($this->labelFont === null) {
$this->labelFont = new Font();
$this->labelFont->setSize(null, true);
}
$this->labelFont->setChartColorFromObject($chartColor);
return $this;
}

View File

@ -48,6 +48,15 @@ class Legend
*/
private $layout;
/** @var GridLines */
private $borderLines;
/** @var ChartColor */
private $fillColor;
/** @var ?AxisText */
private $legendText;
/**
* Create a new Legend.
*
@ -60,6 +69,13 @@ class Legend
$this->setPosition($position);
$this->layout = $layout;
$this->setOverlay($overlay);
$this->borderLines = new GridLines();
$this->fillColor = new ChartColor();
}
public function getFillColor(): ChartColor
{
return $this->fillColor;
}
/**
@ -148,4 +164,28 @@ class Legend
{
return $this->layout;
}
public function getLegendText(): ?AxisText
{
return $this->legendText;
}
public function setLegendText(?AxisText $legendText): self
{
$this->legendText = $legendText;
return $this;
}
public function getBorderLines(): GridLines
{
return $this->borderLines;
}
public function setBorderLines(GridLines $borderLines): self
{
$this->borderLines = $borderLines;
return $this;
}
}

View File

@ -163,4 +163,49 @@ class PlotArea
{
return $this->gradientFillStops;
}
/** @var ?int */
private $gapWidth;
/** @var bool */
private $useUpBars = false;
/** @var bool */
private $useDownBars = false;
public function getGapWidth(): ?int
{
return $this->gapWidth;
}
public function setGapWidth(?int $gapWidth): self
{
$this->gapWidth = $gapWidth;
return $this;
}
public function getUseUpBars(): bool
{
return $this->useUpBars;
}
public function setUseUpBars(bool $useUpBars): self
{
$this->useUpBars = $useUpBars;
return $this;
}
public function getUseDownBars(): bool
{
return $this->useDownBars;
}
public function setUseDownBars(bool $useDownBars): self
{
$this->useDownBars = $useDownBars;
return $this;
}
}

View File

@ -434,12 +434,33 @@ abstract class JpGraphRendererBase implements IRenderer
// Loop through each data series in turn
for ($i = 0; $i < $seriesCount; ++$i) {
$dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues();
$plotCategoryByIndex = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i);
if ($plotCategoryByIndex === false) {
$plotCategoryByIndex = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0);
}
$dataValuesY = $plotCategoryByIndex->getDataValues();
$dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues();
$redoDataValuesY = true;
if ($bubble) {
if (!$bubbleSize) {
$bubbleSize = '10';
}
$redoDataValuesY = false;
foreach ($dataValuesY as $dataValueY) {
if (!is_int($dataValueY) && !is_float($dataValueY)) {
$redoDataValuesY = true;
break;
}
}
}
if ($redoDataValuesY) {
foreach ($dataValuesY as $k => $dataValueY) {
$dataValuesY[$k] = $k;
}
}
//var_dump($dataValuesY, $dataValuesX, $bubbleSize);
$seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY);
if ($scatterStyle == 'lineMarker') {
@ -483,7 +504,7 @@ abstract class JpGraphRendererBase implements IRenderer
$dataValues = [];
foreach ($dataValuesY as $k => $dataValueY) {
$dataValues[$k] = implode(' ', array_reverse($dataValueY));
$dataValues[$k] = is_array($dataValueY) ? implode(' ', array_reverse($dataValueY)) : $dataValueY;
}
$tmp = array_shift($dataValues);
$dataValues[] = $tmp;

View File

@ -3,12 +3,12 @@
namespace PhpOffice\PhpSpreadsheet\Chart\Renderer;
/**
* Jpgraph is not oficially maintained in Composer.
* Jpgraph is not officially maintained by Composer at packagist.org.
*
* This renderer implementation uses package
* https://packagist.org/packages/mitoteam/jpgraph
*
* This package is up to date for August 2022 and has PHP 8.1 support.
* This package is up to date for June 2023 and has PHP 8.2 support.
*/
class MtJpGraphRenderer extends JpGraphRendererBase
{
@ -29,7 +29,7 @@ class MtJpGraphRenderer extends JpGraphRendererBase
'regstat',
'scatter',
'stock',
]);
], true); // enable Extended mode
$loaded = true;
}

View File

@ -107,6 +107,8 @@ class Properties
*/
private $customProperties = [];
private string $hyperlinkBase = '';
/**
* Create a new Document Properties instance.
*/
@ -534,4 +536,16 @@ class Properties
{
return self::PROPERTY_TYPE_ARRAY[$propertyType] ?? self::PROPERTY_TYPE_UNKNOWN;
}
public function getHyperlinkBase(): string
{
return $this->hyperlinkBase;
}
public function setHyperlinkBase(string $hyperlinkBase): self
{
$this->hyperlinkBase = $hyperlinkBase;
return $this;
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Helper;
use PhpOffice\PhpSpreadsheet\Exception;
class Downloader
{
protected string $filepath;
protected string $filename;
protected string $filetype;
protected const CONTENT_TYPES = [
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xls' => 'application/vnd.ms-excel',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'csv' => 'text/csv',
'html' => 'text/html',
'pdf' => 'application/pdf',
];
public function __construct(string $folder, string $filename, ?string $filetype = null)
{
if ((is_dir($folder) === false) || (is_readable($folder) === false)) {
throw new Exception("Folder {$folder} is not accessable");
}
$filepath = "{$folder}/{$filename}";
$this->filepath = (string) realpath($filepath);
$this->filename = basename($filepath);
if ((file_exists($this->filepath) === false) || (is_readable($this->filepath) === false)) {
throw new Exception("{$this->filename} not found, or cannot be read");
}
$filetype ??= pathinfo($filename, PATHINFO_EXTENSION);
if (array_key_exists(strtolower($filetype), self::CONTENT_TYPES) === false) {
throw new Exception("Invalid filetype: {$filetype} cannot be downloaded");
}
$this->filetype = strtolower($filetype);
}
public function download(): void
{
$this->headers();
readfile($this->filepath);
}
public function headers(): void
{
ob_clean();
$this->contentType();
$this->contentDisposition();
$this->cacheHeaders();
$this->fileSize();
flush();
}
protected function contentType(): void
{
header('Content-Type: ' . self::CONTENT_TYPES[$this->filetype]);
}
protected function contentDisposition(): void
{
header('Content-Disposition: attachment;filename="' . $this->filename . '"');
}
protected function cacheHeaders(): void
{
header('Cache-Control: max-age=0');
// If you're serving to IE 9, then the following may be needed
header('Cache-Control: max-age=1');
// If you're serving to IE over SSL, then the following may be needed
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // Date in the past
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // always modified
header('Cache-Control: cache, must-revalidate'); // HTTP/1.1
header('Pragma: public'); // HTTP/1.0
}
protected function fileSize(): void
{
header('Content-Length: ' . filesize($this->filepath));
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Helper;
class Handler
{
/** @var string */
private static $invalidHex = 'Y';
// A bunch of methods to show that we continue
// to capture messages even using PhpUnit 10.
public static function suppressed(): bool
{
return @trigger_error('hello');
}
public static function deprecated(): string
{
return (string) hexdec(self::$invalidHex);
}
public static function notice(string $value): void
{
date_default_timezone_set($value);
}
public static function warning(): bool
{
return file_get_contents(__FILE__ . 'noexist') !== false;
}
public static function userDeprecated(): bool
{
return trigger_error('hello', E_USER_DEPRECATED);
}
public static function userNotice(): bool
{
return trigger_error('userNotice', E_USER_NOTICE);
}
public static function userWarning(): bool
{
return trigger_error('userWarning', E_USER_WARNING);
}
}

View File

@ -714,7 +714,7 @@ class Html
return self::COLOUR_MAP[$colorName] ?? '';
}
private function startFontTag(DOMElement $tag): void
protected function startFontTag(DOMElement $tag): void
{
$attrs = $tag->attributes;
if ($attrs !== null) {
@ -737,72 +737,72 @@ class Html
}
}
private function endFontTag(): void
protected function endFontTag(): void
{
$this->face = $this->size = $this->color = null;
}
private function startBoldTag(): void
protected function startBoldTag(): void
{
$this->bold = true;
}
private function endBoldTag(): void
protected function endBoldTag(): void
{
$this->bold = false;
}
private function startItalicTag(): void
protected function startItalicTag(): void
{
$this->italic = true;
}
private function endItalicTag(): void
protected function endItalicTag(): void
{
$this->italic = false;
}
private function startUnderlineTag(): void
protected function startUnderlineTag(): void
{
$this->underline = true;
}
private function endUnderlineTag(): void
protected function endUnderlineTag(): void
{
$this->underline = false;
}
private function startSubscriptTag(): void
protected function startSubscriptTag(): void
{
$this->subscript = true;
}
private function endSubscriptTag(): void
protected function endSubscriptTag(): void
{
$this->subscript = false;
}
private function startSuperscriptTag(): void
protected function startSuperscriptTag(): void
{
$this->superscript = true;
}
private function endSuperscriptTag(): void
protected function endSuperscriptTag(): void
{
$this->superscript = false;
}
private function startStrikethruTag(): void
protected function startStrikethruTag(): void
{
$this->strikethrough = true;
}
private function endStrikethruTag(): void
protected function endStrikethruTag(): void
{
$this->strikethrough = false;
}
private function breakTag(): void
protected function breakTag(): void
{
$this->stringData .= "\n";
}
@ -826,8 +826,9 @@ class Html
if (isset($callbacks[$callbackTag])) {
$elementHandler = $callbacks[$callbackTag];
if (method_exists($this, $elementHandler)) {
/** @phpstan-ignore-next-line */
call_user_func([$this, $elementHandler], $element);
/** @var callable */
$callable = [$this, $elementHandler];
call_user_func($callable, $element);
}
}
}

View File

@ -2,7 +2,9 @@
namespace PhpOffice\PhpSpreadsheet\Helper;
use PhpOffice\PhpSpreadsheet\Chart\Chart;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Writer\IWriter;
@ -12,6 +14,7 @@ use RecursiveRegexIterator;
use ReflectionClass;
use RegexIterator;
use RuntimeException;
use Throwable;
/**
* Helper class to be used in sample code.
@ -120,7 +123,7 @@ class Sample
* @param string $filename
* @param string[] $writers
*/
public function write(Spreadsheet $spreadsheet, $filename, array $writers = ['Xlsx', 'Xls']): void
public function write(Spreadsheet $spreadsheet, $filename, array $writers = ['Xlsx', 'Xls'], bool $withCharts = false, ?callable $writerCallback = null): void
{
// Set active sheet index to the first sheet, so Excel opens this as the first sheet
$spreadsheet->setActiveSheetIndex(0);
@ -129,9 +132,16 @@ class Sample
foreach ($writers as $writerType) {
$path = $this->getFilename($filename, mb_strtolower($writerType));
$writer = IOFactory::createWriter($spreadsheet, $writerType);
$writer->setIncludeCharts($withCharts);
if ($writerCallback !== null) {
$writerCallback($writer);
}
$callStartTime = microtime(true);
$writer->save($path);
$this->logWrite($writer, $path, /** @scrutinizer ignore-type */ $callStartTime);
if ($this->isCli() === false) {
echo '<a href="/download.php?type=' . pathinfo($path, PATHINFO_EXTENSION) . '&name=' . basename($path) . '">Download ' . basename($path) . '</a><br />';
}
}
$this->logEndingNotes();
@ -147,7 +157,7 @@ class Sample
*
* @return string
*/
private function getTemporaryFolder()
public function getTemporaryFolder()
{
$tempFolder = sys_get_temp_dir() . '/phpspreadsheet';
if (!$this->isDirOrMkdir($tempFolder)) {
@ -162,10 +172,8 @@ class Sample
*
* @param string $filename
* @param string $extension
*
* @return string
*/
public function getFilename($filename, $extension = 'xlsx')
public function getFilename($filename, $extension = 'xlsx'): string
{
$originalExtension = pathinfo($filename, PATHINFO_EXTENSION);
@ -195,7 +203,29 @@ class Sample
public function log(string $message): void
{
$eol = $this->isCli() ? PHP_EOL : '<br />';
echo date('H:i:s ') . $message . $eol;
echo($this->isCli() ? date('H:i:s ') : '') . $message . $eol;
}
public function renderChart(Chart $chart, string $fileName): void
{
if ($this->isCli() === true) {
return;
}
Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class);
$fileName = $this->getFilename($fileName, 'png');
try {
$chart->render($fileName);
$this->log('Rendered image: ' . $fileName);
$imageData = file_get_contents($fileName);
if ($imageData !== false) {
echo '<div><img src="data:image/gif;base64,' . base64_encode($imageData) . '" /></div>';
}
} catch (Throwable $e) {
$this->log('Error rendering chart: ' . $e->getMessage() . PHP_EOL);
}
}
public function titles(string $category, string $functionName, ?string $description = null): void
@ -246,7 +276,10 @@ class Sample
$callTime = $callEndTime - $callStartTime;
$reflection = new ReflectionClass($writer);
$format = $reflection->getShortName();
$message = "Write {$format} format to <code>{$path}</code> in " . sprintf('%.4f', $callTime) . ' seconds';
$message = ($this->isCli() === true)
? "Write {$format} format to {$path} in " . sprintf('%.4f', $callTime) . ' seconds'
: "Write {$format} format to <code>{$path}</code> in " . sprintf('%.4f', $callTime) . ' seconds';
$this->log($message);
}

View File

@ -48,17 +48,17 @@ class TextGrid
public function render(): string
{
$this->gridDisplay = $this->isCli ? '' : '<code>';
$this->gridDisplay = $this->isCli ? '' : '<pre>';
$maxRow = max($this->rows);
$maxRowLength = strlen((string) $maxRow) + 1;
$maxRowLength = mb_strlen((string) $maxRow) + 1;
$columnWidths = $this->getColumnWidths();
$this->renderColumnHeader($maxRowLength, $columnWidths);
$this->renderRows($maxRowLength, $columnWidths);
$this->renderFooter($maxRowLength, $columnWidths);
$this->gridDisplay .= $this->isCli ? '' : '</code>';
$this->gridDisplay .= $this->isCli ? '' : '</pre>';
return $this->gridDisplay;
}
@ -75,9 +75,9 @@ class TextGrid
private function renderCells(array $rowData, array $columnWidths): void
{
foreach ($rowData as $column => $cell) {
$cell = ($this->isCli) ? (string) $cell : htmlentities((string) $cell);
$displayCell = ($this->isCli) ? (string) $cell : htmlentities((string) $cell);
$this->gridDisplay .= '| ';
$this->gridDisplay .= str_pad($cell, $columnWidths[$column] + 1, ' ');
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - mb_strlen($cell ?? '') + 1);
}
}
@ -126,12 +126,12 @@ class TextGrid
foreach ($columnData as $columnValue) {
if (is_string($columnValue)) {
$columnWidth = max($columnWidth, strlen($columnValue));
$columnWidth = max($columnWidth, mb_strlen($columnValue));
} elseif (is_bool($columnValue)) {
$columnWidth = max($columnWidth, strlen($columnValue ? 'TRUE' : 'FALSE'));
$columnWidth = max($columnWidth, mb_strlen($columnValue ? 'TRUE' : 'FALSE'));
}
$columnWidth = max($columnWidth, strlen((string) $columnWidth));
$columnWidth = max($columnWidth, mb_strlen((string) $columnWidth));
}
return $columnWidth;

View File

@ -80,17 +80,15 @@ class Gnumeric extends BaseReader
*/
public function canRead(string $filename): bool
{
// Check if gzlib functions are available
if (File::testFileNoThrow($filename) && function_exists('gzread')) {
// Read signature data (first 3 bytes)
$fh = fopen($filename, 'rb');
if ($fh !== false) {
$data = fread($fh, 2);
fclose($fh);
$data = null;
if (File::testFileNoThrow($filename)) {
$data = $this->gzfileGetContents($filename);
if (strpos($data, self::NAMESPACE_GNM) === false) {
$data = '';
}
}
return isset($data) && $data === chr(0x1F) . chr(0x8B);
return !empty($data);
}
private static function matchXml(XMLReader $xml, string $expectedLocalName): bool
@ -110,9 +108,13 @@ class Gnumeric extends BaseReader
public function listWorksheetNames($filename)
{
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an invalid Gnumeric file.');
}
$xml = new XMLReader();
$xml->xml($this->getSecurityScannerOrThrow()->scanFile('compress.zlib://' . realpath($filename)), null, Settings::getLibXmlLoaderOptions());
$contents = $this->gzfileGetContents($filename);
$xml->xml($contents, null, Settings::getLibXmlLoaderOptions());
$xml->setParserProperty(2, true);
$worksheetNames = [];
@ -139,9 +141,13 @@ class Gnumeric extends BaseReader
public function listWorksheetInfo($filename)
{
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an invalid Gnumeric file.');
}
$xml = new XMLReader();
$xml->xml($this->getSecurityScannerOrThrow()->scanFile('compress.zlib://' . realpath($filename)), null, Settings::getLibXmlLoaderOptions());
$contents = $this->gzfileGetContents($filename);
$xml->xml($contents, null, Settings::getLibXmlLoaderOptions());
$xml->setParserProperty(2, true);
$worksheetInfo = [];
@ -185,13 +191,23 @@ class Gnumeric extends BaseReader
*/
private function gzfileGetContents($filename)
{
$file = @gzopen($filename, 'rb');
$data = '';
if ($file !== false) {
while (!gzeof($file)) {
$data .= gzread($file, 1024);
$contents = @file_get_contents($filename);
if ($contents !== false) {
if (substr($contents, 0, 2) === "\x1f\x8b") {
// Check if gzlib functions are available
if (function_exists('gzdecode')) {
$contents = @gzdecode($contents);
if ($contents !== false) {
$data = $contents;
}
gzclose($file);
}
} else {
$data = $contents;
}
}
if ($data !== '') {
$data = $this->getSecurityScannerOrThrow()->scan($data);
}
return $data;
@ -245,10 +261,13 @@ class Gnumeric extends BaseReader
{
$this->spreadsheet = $spreadsheet;
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an invalid Gnumeric file.');
}
$gFileData = $this->gzfileGetContents($filename);
$xml2 = simplexml_load_string($this->getSecurityScannerOrThrow()->scan($gFileData), 'SimpleXMLElement', Settings::getLibXmlLoaderOptions());
$xml2 = simplexml_load_string($gFileData, 'SimpleXMLElement', Settings::getLibXmlLoaderOptions());
$xml = self::testSimpleXml($xml2);
$gnmXML = $xml->children(self::NAMESPACE_GNM);

View File

@ -7,6 +7,8 @@ use DOMElement;
use DOMNode;
use DOMText;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Document\Properties;
use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
@ -283,15 +285,35 @@ class Html extends BaseReader
* @param int|string $row
* @param mixed $cellContent
*/
protected function flushCell(Worksheet $sheet, $column, $row, &$cellContent): void
protected function flushCell(Worksheet $sheet, $column, $row, &$cellContent, array $attributeArray): void
{
if (is_string($cellContent)) {
// Simple String content
if (trim($cellContent) > '') {
// Only actually write it if there's content in the string
// Write to worksheet to be done here...
// ... we return the cell so we can mess about with styles more easily
// ... we return the cell, so we can mess about with styles more easily
// Set cell value explicitly if there is data-type attribute
if (isset($attributeArray['data-type'])) {
$datatype = $attributeArray['data-type'];
if (in_array($datatype, [DataType::TYPE_STRING, DataType::TYPE_STRING2, DataType::TYPE_INLINE])) {
//Prevent to Excel treat string with beginning equal sign or convert big numbers to scientific number
if (substr($cellContent, 0, 1) === '=') {
$sheet->getCell($column . $row)
->getStyle()
->setQuotePrefix(true);
}
}
//catching the Exception and ignoring the invalid data types
try {
$sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']);
} catch (\PhpOffice\PhpSpreadsheet\Exception $exception) {
$sheet->setCellValue($column . $row, $cellContent);
}
} else {
$sheet->setCellValue($column . $row, $cellContent);
}
$this->dataArray[$row][$column] = $cellContent;
}
} else {
@ -305,7 +327,7 @@ class Html extends BaseReader
private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void
{
$attributeArray = [];
foreach (($child->attributes ?? []) as $attribute) {
foreach ($child->attributes as $attribute) {
$attributeArray[$attribute->name] = $attribute->value;
}
@ -355,7 +377,7 @@ class Html extends BaseReader
private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'hr') {
$this->flushCell($sheet, $column, $row, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
++$row;
if (isset($this->formats[$child->nodeName])) {
$sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
@ -375,7 +397,7 @@ class Html extends BaseReader
$sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
} else {
// Otherwise flush our existing content and move the row cursor on
$this->flushCell($sheet, $column, $row, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
++$row;
}
} else {
@ -421,11 +443,11 @@ class Html extends BaseReader
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
} else {
if ($cellContent > '') {
$this->flushCell($sheet, $column, $row, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
++$row;
}
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
if (isset($this->formats[$child->nodeName])) {
$sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
@ -448,11 +470,11 @@ class Html extends BaseReader
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
} else {
if ($cellContent > '') {
$this->flushCell($sheet, $column, $row, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
}
++$row;
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
$column = 'A';
}
} else {
@ -469,10 +491,13 @@ class Html extends BaseReader
}
}
private string $currentColumn = 'A';
private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'table') {
$this->flushCell($sheet, $column, $row, $cellContent);
$this->currentColumn = 'A';
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
$column = $this->setTableStartColumn($column);
if ($this->tableLevel > 1 && $row > 1) {
--$row;
@ -491,7 +516,10 @@ class Html extends BaseReader
private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
{
if ($child->nodeName === 'tr') {
if ($child->nodeName === 'col') {
$this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray);
++$this->currentColumn;
} elseif ($child->nodeName === 'tr') {
$column = $this->getTableStartColumn();
$cellContent = '';
$this->processDomElement($child, $sheet, $row, $column, $cellContent);
@ -574,7 +602,7 @@ class Html extends BaseReader
// apply inline style
$this->applyInlineStyle($sheet, $row, $column, $attributeArray);
$this->flushCell($sheet, $column, $row, $cellContent);
$this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
$this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
$this->processDomElementWidth($sheet, $column, $attributeArray);
@ -664,10 +692,94 @@ class Html extends BaseReader
if ($loaded === false) {
throw new Exception('Failed to load ' . $filename . ' as a DOM Document', 0, $e ?? null);
}
self::loadProperties($dom, $spreadsheet);
return $this->loadDocument($dom, $spreadsheet);
}
private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
{
$properties = $spreadsheet->getProperties();
foreach ($dom->getElementsByTagName('meta') as $meta) {
$metaContent = (string) $meta->getAttribute('content');
if ($metaContent !== '') {
$metaName = (string) $meta->getAttribute('name');
switch ($metaName) {
case 'author':
$properties->setCreator($metaContent);
break;
case 'category':
$properties->setCategory($metaContent);
break;
case 'company':
$properties->setCompany($metaContent);
break;
case 'created':
$properties->setCreated($metaContent);
break;
case 'description':
$properties->setDescription($metaContent);
break;
case 'keywords':
$properties->setKeywords($metaContent);
break;
case 'lastModifiedBy':
$properties->setLastModifiedBy($metaContent);
break;
case 'manager':
$properties->setManager($metaContent);
break;
case 'modified':
$properties->setModified($metaContent);
break;
case 'subject':
$properties->setSubject($metaContent);
break;
case 'title':
$properties->setTitle($metaContent);
break;
default:
if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) {
switch ($matches[1]) {
case 'bool':
$properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN);
break;
case 'float':
$properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT);
break;
case 'int':
$properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER);
break;
case 'date':
$properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE);
break;
default: // string
$properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING);
}
}
}
}
}
if (!empty($dom->baseURI)) {
$properties->setHyperlinkBase($dom->baseURI);
}
}
private static function replaceNonAscii(array $matches): string
{
return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
@ -698,8 +810,10 @@ class Html extends BaseReader
if ($loaded === false) {
throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
}
$spreadsheet = $spreadsheet ?? new Spreadsheet();
self::loadProperties($dom, $spreadsheet);
return $this->loadDocument($dom, $spreadsheet ?? new Spreadsheet());
return $this->loadDocument($dom, $spreadsheet);
}
/**
@ -769,7 +883,9 @@ class Html extends BaseReader
return;
}
if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
if ($row <= 0 || $column === '') {
$cellStyle = new Style();
} elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
$columnTo = $column;
for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
++$columnTo;
@ -901,16 +1017,20 @@ class Html extends BaseReader
break;
case 'width':
if ($column !== '') {
$sheet->getColumnDimension($column)->setWidth(
(new CssDimension($styleValue ?? ''))->width()
);
}
break;
case 'height':
if ($row > 0) {
$sheet->getRowDimension($row)->setRowHeight(
(new CssDimension($styleValue ?? ''))->height()
);
}
break;

View File

@ -8,6 +8,7 @@ use DOMElement;
use DOMNode;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension;
use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter;
use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames;
use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator;
@ -295,11 +296,29 @@ class Ods extends BaseReader
$tableNs = $dom->lookupNamespaceUri('table');
$textNs = $dom->lookupNamespaceUri('text');
$xlinkNs = $dom->lookupNamespaceUri('xlink');
$styleNs = $dom->lookupNamespaceUri('style');
$pageSettings->readStyleCrossReferences($dom);
$autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
$definedNameReader = new DefinedNames($spreadsheet, $tableNs);
$columnWidths = [];
$automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
$automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
foreach ($automaticStyles as $automaticStyle) {
$styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
$styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
if ($styleFamily === 'table-column') {
$tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties');
if ($tcprops !== null) {
$tcprop = $tcprops->item(0);
if ($tcprop !== null) {
$columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width');
$columnWidths[$styleName] = $columnWidth;
}
}
}
}
// Content
$item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0);
@ -340,6 +359,7 @@ class Ods extends BaseReader
// Go through every child of table element
$rowID = 1;
$tableColumnIndex = 1;
foreach ($worksheetDataSet->childNodes as $childNode) {
/** @var DOMElement $childNode */
@ -366,6 +386,26 @@ class Ods extends BaseReader
// $rowData = $cellData;
// break;
// }
break;
case 'table-column':
if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
} else {
$rowRepeats = 1;
}
$tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
if (isset($columnWidths[$tableStyleName])) {
$columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
$tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex);
for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) {
$spreadsheet->getActiveSheet()
->getColumnDimension($tableColumnString)
->setWidth($columnWidth->toUnit('cm'), 'cm');
++$tableColumnString;
}
}
$tableColumnIndex += $rowRepeats;
break;
case 'table-row':
if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {

View File

@ -151,7 +151,7 @@ class XmlScanner
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
}
if ($this->callback !== null && is_callable($this->callback)) {
if ($this->callback !== null) {
$xml = call_user_func($this->callback, $xml);
}

View File

@ -16,6 +16,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Hyperlinks;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\PageSetup;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SharedFormula;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles;
@ -61,6 +62,11 @@ class Xlsx extends BaseReader
/** @var Styles */
private $styleReader;
/**
* @var array
*/
private $sharedFormulae = [];
/**
* Create a new Xlsx Reader instance.
*/
@ -128,7 +134,7 @@ class Xlsx extends BaseReader
if ($replaceUnclosedBr) {
$contents = str_replace('<br>', '<br/>', $contents);
}
$rels = simplexml_load_string(
$rels = @simplexml_load_string(
$this->getSecurityScannerOrThrow()->scan($contents),
'SimpleXMLElement',
Settings::getLibXmlLoaderOptions(),
@ -246,6 +252,7 @@ class Xlsx extends BaseReader
$xmlWorkbook = $this->loadZip($relTarget, $mainNS);
if ($xmlWorkbook->sheets) {
$dir = dirname($relTarget);
/** @var SimpleXMLElement $eleSheet */
foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
$tmpInfo = [
@ -261,8 +268,8 @@ class Xlsx extends BaseReader
$xml = new XMLReader();
$xml->xml(
$this->getSecurityScannerOrThrow()->scanFile(
'zip://' . File::realpath($filename) . '#' . $fileWorksheetPath
$this->getSecurityScannerOrThrow()->scan(
$this->getFromZipArchive($this->zip, $fileWorksheetPath)
),
null,
Settings::getLibXmlLoaderOptions()
@ -324,13 +331,13 @@ class Xlsx extends BaseReader
* @param mixed $value
* @param mixed $calculatedValue
*/
private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void
private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void
{
if ($c === null) {
return;
}
$attr = $c->f->attributes();
$cellDataType = 'f';
$cellDataType = DataType::TYPE_FORMULA;
$value = "={$c->f}";
$calculatedValue = self::$castBaseType($c);
@ -338,17 +345,19 @@ class Xlsx extends BaseReader
if (isset($attr['t']) && strtolower((string) $attr['t']) == 'shared') {
$instance = (string) $attr['si'];
if (!isset($sharedFormulas[(string) $attr['si']])) {
$sharedFormulas[$instance] = ['master' => $r, 'formula' => $value];
} else {
$master = Coordinate::indexesFromString($sharedFormulas[$instance]['master']);
if (!isset($this->sharedFormulae[(string) $attr['si']])) {
$this->sharedFormulae[$instance] = new SharedFormula($r, $value);
} elseif ($updateSharedCells === true) {
// It's only worth the overhead of adjusting the shared formula for this cell if we're actually loading
// the cell, which may not be the case if we're using a read filter.
$master = Coordinate::indexesFromString($this->sharedFormulae[$instance]->master());
$current = Coordinate::indexesFromString($r);
$difference = [0, 0];
$difference[0] = $current[0] - $master[0];
$difference[1] = $current[1] - $master[1];
$value = $this->referenceHelper->updateFormulaReferences($sharedFormulas[$instance]['formula'], 'A1', $difference[0], $difference[1]);
$value = $this->referenceHelper->updateFormulaReferences($this->sharedFormulae[$instance]->formula(), 'A1', $difference[0], $difference[1]);
}
}
}
@ -395,12 +404,18 @@ class Xlsx extends BaseReader
// Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
// so we need to load case-insensitively from the zip file
// Apache POI fixes
$contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE);
// Apache POI fixes
if ($contents === false) {
$contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE);
}
// Has the file been saved with Windoze directory separators rather than unix?
if ($contents === false) {
$contents = $archive->getFromName(str_replace('/', '\\', $fileName), 0, ZipArchive::FL_NOCASE);
}
return ($contents === false) ? '' : $contents;
}
@ -447,6 +462,7 @@ class Xlsx extends BaseReader
$colourScheme = self::getAttributes($xmlTheme->themeElements->clrScheme);
$colourSchemeName = (string) $colourScheme['name'];
$excel->getTheme()->setThemeColorName($colourSchemeName);
$colourScheme = $xmlTheme->themeElements->clrScheme->children($drawingNS);
$themeColours = [];
@ -458,14 +474,46 @@ class Xlsx extends BaseReader
if (isset($xmlColour->sysClr)) {
$xmlColourData = self::getAttributes($xmlColour->sysClr);
$themeColours[$themePos] = (string) $xmlColourData['lastClr'];
$excel->getTheme()->setThemeColor($k, (string) $xmlColourData['lastClr']);
} elseif (isset($xmlColour->srgbClr)) {
$xmlColourData = self::getAttributes($xmlColour->srgbClr);
$themeColours[$themePos] = (string) $xmlColourData['val'];
$excel->getTheme()->setThemeColor($k, (string) $xmlColourData['val']);
}
}
$theme = new Theme($themeName, $colourSchemeName, $themeColours);
$this->styleReader->setTheme($theme);
$fontScheme = self::getAttributes($xmlTheme->themeElements->fontScheme);
$fontSchemeName = (string) $fontScheme['name'];
$excel->getTheme()->setThemeFontName($fontSchemeName);
$majorFonts = [];
$minorFonts = [];
$fontScheme = $xmlTheme->themeElements->fontScheme->children($drawingNS);
$majorLatin = self::getAttributes($fontScheme->majorFont->latin)['typeface'] ?? '';
$majorEastAsian = self::getAttributes($fontScheme->majorFont->ea)['typeface'] ?? '';
$majorComplexScript = self::getAttributes($fontScheme->majorFont->cs)['typeface'] ?? '';
$minorLatin = self::getAttributes($fontScheme->minorFont->latin)['typeface'] ?? '';
$minorEastAsian = self::getAttributes($fontScheme->minorFont->ea)['typeface'] ?? '';
$minorComplexScript = self::getAttributes($fontScheme->minorFont->cs)['typeface'] ?? '';
foreach ($fontScheme->majorFont->font as $xmlFont) {
$fontAttributes = self::getAttributes($xmlFont);
$script = (string) ($fontAttributes['script'] ?? '');
if (!empty($script)) {
$majorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
}
}
foreach ($fontScheme->minorFont->font as $xmlFont) {
$fontAttributes = self::getAttributes($xmlFont);
$script = (string) ($fontAttributes['script'] ?? '');
if (!empty($script)) {
$minorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
}
}
$excel->getTheme()->setMajorFontValues($majorLatin, $majorEastAsian, $majorComplexScript, $majorFonts);
$excel->getTheme()->setMinorFontValues($minorLatin, $minorEastAsian, $minorComplexScript, $minorFonts);
break;
}
}
@ -477,6 +525,10 @@ class Xlsx extends BaseReader
foreach ($rels->Relationship as $relx) {
$rel = self::getAttributes($relx);
$relTarget = (string) $rel['Target'];
// issue 3553
if ($relTarget[0] === '/') {
$relTarget = substr($relTarget, 1);
}
$relType = (string) $rel['Type'];
$mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
switch ($relType) {
@ -507,26 +559,6 @@ class Xlsx extends BaseReader
$relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', '');
$relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS);
$sharedStrings = [];
$relType = "rel:Relationship[@Type='"
//. Namespaces::SHARED_STRINGS
. "$xmlNamespaceBase/sharedStrings"
. "']";
$xpath = self::getArrayItem($relsWorkbook->xpath($relType));
if ($xpath) {
$xmlStrings = $this->loadZip("$dir/$xpath[Target]", $mainNS);
if (isset($xmlStrings->si)) {
foreach ($xmlStrings->si as $val) {
if (isset($val->t)) {
$sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t);
} elseif (isset($val->r)) {
$sharedStrings[] = $this->parseRichText($val);
}
}
}
}
$worksheets = [];
$macros = $customUI = null;
foreach ($relsWorkbook->Relationship as $elex) {
@ -618,7 +650,7 @@ class Xlsx extends BaseReader
$numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
}
}
$quotePrefix = (bool) ($xf['quotePrefix'] ?? false);
$quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
$style = (object) [
'numFmt' => $numFmt ?? NumberFormat::FORMAT_GENERAL,
@ -653,7 +685,7 @@ class Xlsx extends BaseReader
}
}
$quotePrefix = (bool) ($xf['quotePrefix'] ?? false);
$quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
$cellStyle = (object) [
'numFmt' => $numFmt,
@ -682,6 +714,27 @@ class Xlsx extends BaseReader
$dxfs = $this->styleReader->dxfs($this->readDataOnly);
$styles = $this->styleReader->styles();
// Read content after setting the styles
$sharedStrings = [];
$relType = "rel:Relationship[@Type='"
//. Namespaces::SHARED_STRINGS
. "$xmlNamespaceBase/sharedStrings"
. "']";
$xpath = self::getArrayItem($relsWorkbook->xpath($relType));
if ($xpath) {
$xmlStrings = $this->loadZip("$dir/$xpath[Target]", $mainNS);
if (isset($xmlStrings->si)) {
foreach ($xmlStrings->si as $val) {
if (isset($val->t)) {
$sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t);
} elseif (isset($val->r)) {
$sharedStrings[] = $this->parseRichText($val);
}
}
}
}
$xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS);
$xmlWorkbookNS = $this->loadZip($relTarget, $mainNS);
@ -743,7 +796,8 @@ class Xlsx extends BaseReader
$xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS);
$xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS);
$sharedFormulas = [];
// Shared Formula table is unique to each Worksheet, so we need to reset it here
$this->sharedFormulae = [];
if (isset($eleSheetAttr['state']) && (string) $eleSheetAttr['state'] != '') {
$docSheet->setSheetState((string) $eleSheetAttr['state']);
@ -789,8 +843,12 @@ class Xlsx extends BaseReader
$coordinates = Coordinate::coordinateFromString($r);
if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) {
if (isset($cAttr->f)) {
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError');
// Normally, just testing for the f attribute should identify this cell as containing a formula
// that we need to read, even though it is outside of the filter range, in case it is a shared formula.
// But in some cases, this attribute isn't set; so we need to delve a level deeper and look at
// whether or not the cell has a child formula element that is shared.
if (isset($cAttr->f) || (isset($c->f, $c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared')) {
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError', false);
}
++$rowIndex;
@ -822,7 +880,7 @@ class Xlsx extends BaseReader
}
} else {
// Formula
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToBoolean');
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean');
if (isset($c->f['t'])) {
$att = $c->f;
$docSheet->getCell($r)->setFormulaAttributes($att);
@ -832,7 +890,7 @@ class Xlsx extends BaseReader
break;
case 'inlineStr':
if (isset($c->f)) {
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError');
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
} else {
$value = $this->parseRichText($c->is);
}
@ -843,7 +901,7 @@ class Xlsx extends BaseReader
$value = self::castToError($c);
} else {
// Formula
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError');
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
}
break;
@ -852,7 +910,7 @@ class Xlsx extends BaseReader
$value = self::castToString($c);
} else {
// Formula
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToString');
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
if (isset($c->f['t'])) {
$attributes = $c->f['t'];
$docSheet->getCell($r)->setFormulaAttributes(['t' => (string) $attributes]);
@ -891,6 +949,10 @@ class Xlsx extends BaseReader
// no style index means 0, it seems
$cell->setXfIndex(isset($styles[(int) ($cAttr['s'])]) ?
(int) ($cAttr['s']) : 0);
// issue 3495
if ($cell->getDataType() === DataType::TYPE_FORMULA) {
$cell->getStyle()->setQuotePrefix(false);
}
}
}
++$rowIndex;
@ -898,6 +960,12 @@ class Xlsx extends BaseReader
++$cIndex;
}
}
if ($xmlSheetNS && $xmlSheetNS->ignoredErrors) {
foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredErrorx) {
$ignoredError = self::testSimpleXml($ignoredErrorx);
$this->processIgnoredErrors($ignoredError, $docSheet);
}
}
if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->sheetProtection) {
$protAttr = $xmlSheetNS->sheetProtection->attributes() ?? [];
@ -2205,4 +2273,48 @@ class Xlsx extends BaseReader
return $array;
}
private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet): void
{
$attributes = self::getAttributes($xml);
$sqref = (string) ($attributes['sqref'] ?? '');
$numberStoredAsText = (string) ($attributes['numberStoredAsText'] ?? '');
$formula = (string) ($attributes['formula'] ?? '');
$twoDigitTextYear = (string) ($attributes['twoDigitTextYear'] ?? '');
$evalError = (string) ($attributes['evalError'] ?? '');
if (!empty($sqref)) {
$explodedSqref = explode(' ', $sqref);
$pattern1 = '/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/';
foreach ($explodedSqref as $sqref1) {
if (preg_match($pattern1, $sqref1, $matches) === 1) {
$firstRow = $matches[2];
$firstCol = $matches[1];
if (array_key_exists(3, $matches)) {
$lastCol = $matches[4];
$lastRow = $matches[5];
} else {
$lastCol = $firstCol;
$lastRow = $firstRow;
}
++$lastCol;
for ($row = $firstRow; $row <= $lastRow; ++$row) {
for ($col = $firstCol; $col !== $lastCol; ++$col) {
if ($numberStoredAsText === '1') {
$sheet->getCell("$col$row")->getIgnoredErrors()->setNumberStoredAsText(true);
}
if ($formula === '1') {
$sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true);
}
if ($twoDigitTextYear === '1') {
$sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true);
}
if ($evalError === '1') {
$sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true);
}
}
}
}
}
}
}
}

View File

@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Chart\Axis;
use PhpOffice\PhpSpreadsheet\Chart\AxisText;
use PhpOffice\PhpSpreadsheet\Chart\ChartColor;
use PhpOffice\PhpSpreadsheet\Chart\DataSeries;
use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues;
@ -76,16 +77,28 @@ class Chart
$yAxis = new Axis();
$autoTitleDeleted = null;
$chartNoFill = false;
$chartBorderLines = null;
$chartFillColor = null;
$gradientArray = [];
$gradientLin = null;
$roundedCorners = false;
$gapWidth = null;
$useUpBars = null;
$useDownBars = null;
foreach ($chartElementsC as $chartElementKey => $chartElement) {
switch ($chartElementKey) {
case 'spPr':
$possibleNoFill = $chartElementsC->spPr->children($this->aNamespace);
if (isset($possibleNoFill->noFill)) {
$children = $chartElementsC->spPr->children($this->aNamespace);
if (isset($children->noFill)) {
$chartNoFill = true;
}
if (isset($children->solidFill)) {
$chartFillColor = $this->readColor($children->solidFill);
}
if (isset($children->ln)) {
$chartBorderLines = new GridLines();
$this->readLineStyle($chartElementsC, $chartBorderLines);
}
break;
case 'roundedCorners':
@ -157,6 +170,9 @@ class Chart
$axisColorArray = $this->readColor($sppr->solidFill);
$xAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']);
}
if (isset($chartDetail->spPr->ln->noFill)) {
$xAxis->setNoFill(true);
}
}
if (isset($chartDetail->majorGridlines)) {
$majorGridlines = new GridLines();
@ -227,6 +243,9 @@ class Chart
$axisColorArray = $this->readColor($sppr->solidFill);
$whichAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']);
}
if (isset($sppr->ln->noFill)) {
$whichAxis->setNoFill(true);
}
}
if ($whichAxis !== null && isset($chartDetail->majorGridlines)) {
$majorGridlines = new GridLines();
@ -316,6 +335,15 @@ class Chart
break;
case 'stockChart':
$plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey);
if (isset($chartDetail->upDownBars->gapWidth)) {
$gapWidth = self::getAttribute($chartDetail->upDownBars->gapWidth, 'val', 'integer');
}
if (isset($chartDetail->upDownBars->upBars)) {
$useUpBars = true;
}
if (isset($chartDetail->upDownBars->downBars)) {
$useDownBars = true;
}
$plotAttributes = $this->readChartAttributes($chartDetail);
break;
@ -332,6 +360,15 @@ class Chart
if (!empty($gradientArray)) {
$plotArea->setGradientFillProperties($gradientArray, $gradientLin);
}
if (is_int($gapWidth)) {
$plotArea->setGapWidth($gapWidth);
}
if ($useUpBars === true) {
$plotArea->setUseUpBars(true);
}
if ($useDownBars === true) {
$plotArea->setUseDownBars(true);
}
break;
case 'plotVisOnly':
@ -350,6 +387,10 @@ class Chart
$legendPos = 'r';
$legendLayout = null;
$legendOverlay = false;
$legendBorderLines = null;
$legendFillColor = null;
$legendText = null;
$addLegendText = false;
foreach ($chartDetails as $chartDetailKey => $chartDetail) {
$chartDetail = Xlsx::testSimpleXml($chartDetail);
switch ($chartDetailKey) {
@ -364,10 +405,45 @@ class Chart
case 'layout':
$legendLayout = $this->chartLayoutDetails($chartDetail);
break;
case 'spPr':
$children = $chartDetails->spPr->children($this->aNamespace);
if (isset($children->solidFill)) {
$legendFillColor = $this->readColor($children->solidFill);
}
if (isset($children->ln)) {
$legendBorderLines = new GridLines();
$this->readLineStyle($chartDetails, $legendBorderLines);
}
break;
case 'txPr':
$children = $chartDetails->txPr->children($this->aNamespace);
$addLegendText = false;
$legendText = new AxisText();
if (isset($children->p->pPr->defRPr->solidFill)) {
$colorArray = $this->readColor($children->p->pPr->defRPr->solidFill);
$legendText->getFillColorObject()->setColorPropertiesArray($colorArray);
$addLegendText = true;
}
if (isset($children->p->pPr->defRPr->effectLst)) {
$this->readEffects($children->p->pPr->defRPr, $legendText, false);
$addLegendText = true;
}
break;
}
}
$legend = new Legend("$legendPos", $legendLayout, (bool) $legendOverlay);
if ($legendFillColor !== null) {
$legend->getFillColor()->setColorPropertiesArray($legendFillColor);
}
if ($legendBorderLines !== null) {
$legend->setBorderLines($legendBorderLines);
}
if ($addLegendText) {
$legend->setLegendText($legendText);
}
break;
}
@ -378,6 +454,12 @@ class Chart
if ($chartNoFill) {
$chart->setNoFill(true);
}
if ($chartFillColor !== null) {
$chart->getFillColor()->setColorPropertiesArray($chartFillColor);
}
if ($chartBorderLines !== null) {
$chart->setBorderLines($chartBorderLines);
}
$chart->setRoundedCorners($roundedCorners);
if (is_bool($autoTitleDeleted)) {
$chart->setAutoTitleDeleted($autoTitleDeleted);
@ -1082,6 +1164,37 @@ class Chart
return $value;
}
private function parseFont(SimpleXMLElement $titleDetailPart): ?Font
{
if (!isset($titleDetailPart->pPr->defRPr)) {
return null;
}
$fontArray = [];
$fontArray['size'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'sz', 'integer');
$fontArray['bold'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'b', 'boolean');
$fontArray['italic'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'i', 'boolean');
$fontArray['underscore'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'u', 'string');
$fontArray['strikethrough'] = self::getAttribute($titleDetailPart->pPr->defRPr, 'strike', 'string');
if (isset($titleDetailPart->pPr->defRPr->latin)) {
$fontArray['latin'] = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string');
}
if (isset($titleDetailPart->pPr->defRPr->ea)) {
$fontArray['eastAsian'] = self::getAttribute($titleDetailPart->pPr->defRPr->ea, 'typeface', 'string');
}
if (isset($titleDetailPart->pPr->defRPr->cs)) {
$fontArray['complexScript'] = self::getAttribute($titleDetailPart->pPr->defRPr->cs, 'typeface', 'string');
}
if (isset($titleDetailPart->pPr->defRPr->solidFill)) {
$fontArray['chartColor'] = new ChartColor($this->readColor($titleDetailPart->pPr->defRPr->solidFill));
}
$font = new Font();
$font->setSize(null, true);
$font->applyFromArray($fontArray);
return $font;
}
/**
* @param ?SimpleXMLElement $chartDetail
*/
@ -1128,8 +1241,13 @@ class Chart
}
if (isset($chartDetail->dLbls->txPr)) {
$txpr = $chartDetail->dLbls->txPr->children($this->aNamespace);
if (isset($txpr->p->pPr->defRPr->solidFill)) {
$plotAttributes['labelFontColor'] = new ChartColor($this->readColor($txpr->p->pPr->defRPr->solidFill));
if (isset($txpr->p)) {
$plotAttributes['labelFont'] = $this->parseFont($txpr->p);
if (isset($txpr->p->pPr->defRPr->effectLst)) {
$labelEffects = new GridLines();
$this->readEffects($txpr->p->pPr->defRPr, $labelEffects, false);
$plotAttributes['labelEffects'] = $labelEffects;
}
}
}
}
@ -1176,13 +1294,19 @@ class Chart
}
}
private function readEffects(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void
private function readEffects(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject, bool $getSppr = true): void
{
if (!isset($chartObject, $chartDetail->spPr)) {
if (!isset($chartObject)) {
return;
}
if ($getSppr) {
if (!isset($chartDetail->spPr)) {
return;
}
$sppr = $chartDetail->spPr->children($this->aNamespace);
} else {
$sppr = $chartDetail;
}
if (isset($sppr->effectLst->glow)) {
$axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / ChartProperties::POINTS_WIDTH_MULTIPLIER;
if ($axisGlowSize != 0.0) {
@ -1412,12 +1536,29 @@ class Chart
}
if (isset($chartDetail->txPr)) {
$children = $chartDetail->txPr->children($this->aNamespace);
$addAxisText = false;
$axisText = new AxisText();
if (isset($children->bodyPr)) {
/** @var string */
$textRotation = self::getAttribute($children->bodyPr, 'rot', 'string');
if (is_numeric($textRotation)) {
$whichAxis->setAxisOption('textRotation', (string) ChartProperties::xmlToAngle($textRotation));
}
$axisText->setRotation((int) ChartProperties::xmlToAngle($textRotation));
$addAxisText = true;
}
}
if (isset($children->p->pPr->defRPr)) {
$font = $this->parseFont($children->p);
if ($font !== null) {
$axisText->setFont($font);
$addAxisText = true;
}
}
if (isset($children->p->pPr->defRPr->effectLst)) {
$this->readEffects($children->p->pPr->defRPr, $axisText, false);
$addAxisText = true;
}
if ($addAxisText) {
$whichAxis->setAxisText($axisText);
}
}
}

View File

@ -22,6 +22,18 @@ class DataValidations
public function load(): void
{
foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) {
// Uppercase coordinate
$range = strtoupper((string) $dataValidation['sqref']);
$rangeSet = explode(' ', $range);
foreach ($rangeSet as $range) {
if (preg_match('/^[A-Z]{1,3}\\d{1,7}/', $range, $matches) === 1) {
// Ensure left/top row of range exists, thereby
// adjusting high row/column.
$this->worksheet->getCell($matches[0]);
}
}
}
foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) {
// Uppercase coordinate
$range = strtoupper((string) $dataValidation['sqref']);

View File

@ -73,6 +73,9 @@ class Properties
if (isset($xmlCore->Manager)) {
$this->docProps->setManager((string) $xmlCore->Manager);
}
if (isset($xmlCore->HyperlinkBase)) {
$this->docProps->setHyperlinkBase((string) $xmlCore->HyperlinkBase);
}
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;
class SharedFormula
{
private string $master;
private string $formula;
public function __construct(string $master, string $formula)
{
$this->master = $master;
$this->formula = $formula;
}
public function master(): string
{
return $this->master;
}
public function formula(): string
{
return $this->formula;
}
}

View File

@ -136,6 +136,10 @@ class Styles extends BaseParserClass
}
}
}
if (isset($fontStyleXml->scheme)) {
$attr = $this->getStyleAttributes($fontStyleXml->scheme);
$fontStyle->setScheme((string) $attr['val']);
}
}
private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void
@ -253,10 +257,14 @@ class Styles extends BaseParserClass
public function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void
{
$horizontal = $this->getAttribute($alignmentXml, 'horizontal');
$horizontal = (string) $this->getAttribute($alignmentXml, 'horizontal');
if ($horizontal !== '') {
$alignment->setHorizontal($horizontal);
$vertical = $this->getAttribute($alignmentXml, 'vertical');
$alignment->setVertical((string) $vertical);
}
$vertical = (string) $this->getAttribute($alignmentXml, 'vertical');
if ($vertical !== '') {
$alignment->setVertical($vertical);
}
$textRotation = (int) $this->getAttribute($alignmentXml, 'textRotation');
if ($textRotation > 90) {

View File

@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\DefinedName;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Reader\Xml\PageSettings;
use PhpOffice\PhpSpreadsheet\Reader\Xml\Properties;
use PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
@ -26,6 +27,8 @@ use SimpleXMLElement;
*/
class Xml extends BaseReader
{
public const NAMESPACES_SS = 'urn:schemas-microsoft-com:office:spreadsheet';
/**
* Formats.
*
@ -146,11 +149,9 @@ class Xml extends BaseReader
throw new Exception("Problem reading {$filename}");
}
$namespaces = $xml->getNamespaces(true);
$xml_ss = $xml->children($namespaces['ss']);
$xml_ss = $xml->children(self::NAMESPACES_SS);
foreach ($xml_ss->Worksheet as $worksheet) {
$worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']);
$worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
$worksheetNames[] = (string) $worksheet_ss['Name'];
}
@ -178,12 +179,10 @@ class Xml extends BaseReader
throw new Exception("Problem reading {$filename}");
}
$namespaces = $xml->getNamespaces(true);
$worksheetID = 1;
$xml_ss = $xml->children($namespaces['ss']);
$xml_ss = $xml->children(self::NAMESPACES_SS);
foreach ($xml_ss->Worksheet as $worksheet) {
$worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']);
$worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
$tmpInfo = [];
$tmpInfo['worksheetName'] = '';
@ -231,6 +230,19 @@ class Xml extends BaseReader
return $worksheetInfo;
}
/**
* Loads Spreadsheet from string.
*/
public function loadSpreadsheetFromString(string $contents): Spreadsheet
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet->removeSheetByIndex(0);
// Load into this instance
return $this->loadIntoExisting($contents, $spreadsheet, true);
}
/**
* Loads Spreadsheet from file.
*/
@ -245,18 +257,20 @@ class Xml extends BaseReader
}
/**
* Loads from file into Spreadsheet instance.
* Loads from file or contents into Spreadsheet instance.
*
* @param string $filename
*
* @return Spreadsheet
* @param string $filename file name if useContents is false else file contents
*/
public function loadIntoExisting($filename, Spreadsheet $spreadsheet)
public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, bool $useContents = false): Spreadsheet
{
if ($useContents) {
$this->fileContents = $filename;
} else {
File::assertFile($filename);
if (!$this->canRead($filename)) {
throw new Exception($filename . ' is an Invalid Spreadsheet file.');
}
}
$xml = $this->trySimpleXMLLoadString($filename);
if ($xml === false) {
@ -268,14 +282,17 @@ class Xml extends BaseReader
(new Properties($spreadsheet))->readProperties($xml, $namespaces);
$this->styles = (new Style())->parseStyles($xml, $namespaces);
if (isset($this->styles['Default'])) {
$spreadsheet->getCellXfCollection()[0]->applyFromArray($this->styles['Default']);
}
$worksheetID = 0;
$xml_ss = $xml->children($namespaces['ss']);
$xml_ss = $xml->children(self::NAMESPACES_SS);
/** @var null|SimpleXMLElement $worksheetx */
foreach ($xml_ss->Worksheet as $worksheetx) {
$worksheet = $worksheetx ?? new SimpleXMLElement('<xml></xml>');
$worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']);
$worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
if (
isset($this->loadSheetsOnly, $worksheet_ss['Name']) &&
@ -295,11 +312,15 @@ class Xml extends BaseReader
// the worksheet name in line with the formula, not the reverse
$spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
}
if (isset($worksheet_ss['Protected'])) {
$protection = (string) $worksheet_ss['Protected'] === '1';
$spreadsheet->getActiveSheet()->getProtection()->setSheet($protection);
}
// locally scoped defined names
if (isset($worksheet->Names[0])) {
foreach ($worksheet->Names[0] as $definedName) {
$definedName_ss = self::getAttributes($definedName, $namespaces['ss']);
$definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS);
$name = (string) $definedName_ss['Name'];
$definedValue = (string) $definedName_ss['RefersTo'];
$convertedValue = AddressHelper::convertFormulaToA1($definedValue);
@ -313,15 +334,35 @@ class Xml extends BaseReader
$columnID = 'A';
if (isset($worksheet->Table->Column)) {
foreach ($worksheet->Table->Column as $columnData) {
$columnData_ss = self::getAttributes($columnData, $namespaces['ss']);
$columnData_ss = self::getAttributes($columnData, self::NAMESPACES_SS);
$colspan = 0;
if (isset($columnData_ss['Span'])) {
$spanAttr = (string) $columnData_ss['Span'];
if (is_numeric($spanAttr)) {
$colspan = max(0, (int) $spanAttr);
}
}
if (isset($columnData_ss['Index'])) {
$columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']);
}
$columnWidth = null;
if (isset($columnData_ss['Width'])) {
$columnWidth = $columnData_ss['Width'];
}
$columnVisible = null;
if (isset($columnData_ss['Hidden'])) {
$columnVisible = ((string) $columnData_ss['Hidden']) !== '1';
}
while ($colspan >= 0) {
if (isset($columnWidth)) {
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4);
}
if (isset($columnVisible)) {
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setVisible($columnVisible);
}
++$columnID;
--$colspan;
}
}
}
@ -330,14 +371,18 @@ class Xml extends BaseReader
$additionalMergedCells = 0;
foreach ($worksheet->Table->Row as $rowData) {
$rowHasData = false;
$row_ss = self::getAttributes($rowData, $namespaces['ss']);
$row_ss = self::getAttributes($rowData, self::NAMESPACES_SS);
if (isset($row_ss['Index'])) {
$rowID = (int) $row_ss['Index'];
}
if (isset($row_ss['Hidden'])) {
$rowVisible = ((string) $row_ss['Hidden']) !== '1';
$spreadsheet->getActiveSheet()->getRowDimension($rowID)->setVisible($rowVisible);
}
$columnID = 'A';
foreach ($rowData->Cell as $cell) {
$cell_ss = self::getAttributes($cell, $namespaces['ss']);
$cell_ss = self::getAttributes($cell, self::NAMESPACES_SS);
if (isset($cell_ss['Index'])) {
$columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']);
}
@ -379,7 +424,7 @@ class Xml extends BaseReader
$cellData = $cell->Data;
$cellValue = (string) $cellData;
$type = DataType::TYPE_NULL;
$cellData_ss = self::getAttributes($cellData, $namespaces['ss']);
$cellData_ss = self::getAttributes($cellData, self::NAMESPACES_SS);
if (isset($cellData_ss['Type'])) {
$cellDataType = $cellData_ss['Type'];
switch ($cellDataType) {
@ -437,7 +482,7 @@ class Xml extends BaseReader
}
if (isset($cell->Comment)) {
$this->parseCellComment($cell->Comment, $namespaces, $spreadsheet, $columnID, $rowID);
$this->parseCellComment($cell->Comment, $spreadsheet, $columnID, $rowID);
}
if (isset($cell_ss['StyleID'])) {
@ -466,11 +511,57 @@ class Xml extends BaseReader
++$rowID;
}
}
if (isset($namespaces['x'])) {
$xmlX = $worksheet->children($namespaces['x']);
$dataValidations = new Xml\DataValidations();
$dataValidations->loadDataValidations($worksheet, $spreadsheet);
$xmlX = $worksheet->children(Namespaces::URN_EXCEL);
if (isset($xmlX->WorksheetOptions)) {
(new PageSettings($xmlX, $namespaces))->loadPageSettings($spreadsheet);
if (isset($xmlX->WorksheetOptions->FreezePanes)) {
$freezeRow = $freezeColumn = 1;
if (isset($xmlX->WorksheetOptions->SplitHorizontal)) {
$freezeRow = (int) $xmlX->WorksheetOptions->SplitHorizontal + 1;
}
if (isset($xmlX->WorksheetOptions->SplitVertical)) {
$freezeColumn = (int) $xmlX->WorksheetOptions->SplitVertical + 1;
}
$spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow);
}
(new PageSettings($xmlX))->loadPageSettings($spreadsheet);
if (isset($xmlX->WorksheetOptions->TopRowVisible, $xmlX->WorksheetOptions->LeftColumnVisible)) {
$leftTopRow = (string) $xmlX->WorksheetOptions->TopRowVisible;
$leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnVisible;
if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) {
$leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1);
$spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate);
}
}
$rangeCalculated = false;
if (isset($xmlX->WorksheetOptions->Panes->Pane->RangeSelection)) {
if (1 === preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $xmlX->WorksheetOptions->Panes->Pane->RangeSelection, $selectionMatches)) {
$selectedCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
. $selectionMatches[1]
. ':'
. Coordinate::stringFromColumnIndex((int) $selectionMatches[4])
. $selectionMatches[3];
$spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
$rangeCalculated = true;
}
}
if (!$rangeCalculated) {
if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveRow)) {
$activeRow = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveRow;
} else {
$activeRow = 0;
}
if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveCol)) {
$activeColumn = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveCol;
} else {
$activeColumn = 0;
}
if (is_numeric($activeRow) && is_numeric($activeColumn)) {
$selectedCell = Coordinate::stringFromColumnIndex((int) $activeColumn + 1) . (string) ($activeRow + 1);
$spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
}
}
}
@ -478,10 +569,14 @@ class Xml extends BaseReader
}
// Globally scoped defined names
$activeWorksheet = $spreadsheet->setActiveSheetIndex(0);
$activeSheetIndex = 0;
if (isset($xml->ExcelWorkbook->ActiveSheet)) {
$activeSheetIndex = (int) (string) $xml->ExcelWorkbook->ActiveSheet;
}
$activeWorksheet = $spreadsheet->setActiveSheetIndex($activeSheetIndex);
if (isset($xml->Names[0])) {
foreach ($xml->Names[0] as $definedName) {
$definedName_ss = self::getAttributes($definedName, $namespaces['ss']);
$definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS);
$name = (string) $definedName_ss['Name'];
$definedValue = (string) $definedName_ss['RefersTo'];
$convertedValue = AddressHelper::convertFormulaToA1($definedValue);
@ -498,12 +593,11 @@ class Xml extends BaseReader
protected function parseCellComment(
SimpleXMLElement $comment,
array $namespaces,
Spreadsheet $spreadsheet,
string $columnID,
int $rowID
): void {
$commentAttributes = $comment->attributes($namespaces['ss']);
$commentAttributes = $comment->attributes(self::NAMESPACES_SS);
$author = 'unknown';
if (isset($commentAttributes->Author)) {
$author = (string) $commentAttributes->Author;

View File

@ -0,0 +1,177 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Xml;
use PhpOffice\PhpSpreadsheet\Cell\AddressHelper;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use SimpleXMLElement;
class DataValidations
{
private const OPERATOR_MAPPINGS = [
'between' => DataValidation::OPERATOR_BETWEEN,
'equal' => DataValidation::OPERATOR_EQUAL,
'greater' => DataValidation::OPERATOR_GREATERTHAN,
'greaterorequal' => DataValidation::OPERATOR_GREATERTHANOREQUAL,
'less' => DataValidation::OPERATOR_LESSTHAN,
'lessorequal' => DataValidation::OPERATOR_LESSTHANOREQUAL,
'notbetween' => DataValidation::OPERATOR_NOTBETWEEN,
'notequal' => DataValidation::OPERATOR_NOTEQUAL,
];
private const TYPE_MAPPINGS = [
'textlength' => DataValidation::TYPE_TEXTLENGTH,
];
private int $thisRow = 0;
private int $thisColumn = 0;
private function replaceR1C1(array $matches): string
{
return AddressHelper::convertToA1($matches[0], $this->thisRow, $this->thisColumn, false);
}
public function loadDataValidations(SimpleXMLElement $worksheet, Spreadsheet $spreadsheet): void
{
$xmlX = $worksheet->children(Namespaces::URN_EXCEL);
$sheet = $spreadsheet->getActiveSheet();
/** @var callable */
$pregCallback = [$this, 'replaceR1C1'];
foreach ($xmlX->DataValidation as $dataValidation) {
$cells = [];
$validation = new DataValidation();
// set defaults
$validation->setShowDropDown(true);
$validation->setShowInputMessage(true);
$validation->setShowErrorMessage(true);
$validation->setShowDropDown(true);
$this->thisRow = 1;
$this->thisColumn = 1;
foreach ($dataValidation as $tagName => $tagValue) {
$tagValue = (string) $tagValue;
$tagValueLower = strtolower($tagValue);
switch ($tagName) {
case 'Range':
foreach (explode(',', $tagValue) as $range) {
$cell = '';
if (preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) {
// range
$firstCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
. $selectionMatches[1];
$cell = $firstCell
. ':'
. Coordinate::stringFromColumnIndex((int) $selectionMatches[4])
. $selectionMatches[3];
$this->thisRow = (int) $selectionMatches[1];
$this->thisColumn = (int) $selectionMatches[2];
$sheet->getCell($firstCell);
} elseif (preg_match('/^R(\d+)C(\d+)$/', (string) $range, $selectionMatches) === 1) {
// cell
$cell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
. $selectionMatches[1];
$sheet->getCell($cell);
$this->thisRow = (int) $selectionMatches[1];
$this->thisColumn = (int) $selectionMatches[2];
} elseif (preg_match('/^C(\d+)$/', (string) $range, $selectionMatches) === 1) {
// column
$firstCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[1])
. '1';
$cell = $firstCell
. ':'
. Coordinate::stringFromColumnIndex((int) $selectionMatches[1])
. ((string) AddressRange::MAX_ROW);
$this->thisColumn = (int) $selectionMatches[1];
$sheet->getCell($firstCell);
} elseif (preg_match('/^R(\d+)$/', (string) $range, $selectionMatches)) {
// row
$firstCell = 'A'
. $selectionMatches[1];
$cell = $firstCell
. ':'
. AddressRange::MAX_COLUMN
. $selectionMatches[1];
$this->thisRow = (int) $selectionMatches[1];
$sheet->getCell($firstCell);
}
$validation->setSqref($cell);
$stRange = $sheet->shrinkRangeToFit($cell);
$cells = array_merge($cells, Coordinate::extractAllCellReferencesInRange($stRange));
}
break;
case 'Type':
$validation->setType(self::TYPE_MAPPINGS[$tagValueLower] ?? $tagValueLower);
break;
case 'Qualifier':
$validation->setOperator(self::OPERATOR_MAPPINGS[$tagValueLower] ?? $tagValueLower);
break;
case 'InputTitle':
$validation->setPromptTitle($tagValue);
break;
case 'InputMessage':
$validation->setPrompt($tagValue);
break;
case 'InputHide':
$validation->setShowInputMessage(false);
break;
case 'ErrorStyle':
$validation->setErrorStyle($tagValueLower);
break;
case 'ErrorTitle':
$validation->setErrorTitle($tagValue);
break;
case 'ErrorMessage':
$validation->setError($tagValue);
break;
case 'ErrorHide':
$validation->setShowErrorMessage(false);
break;
case 'ComboHide':
$validation->setShowDropDown(false);
break;
case 'UseBlank':
$validation->setAllowBlank(true);
break;
case 'CellRangeList':
// FIXME missing FIXME
break;
case 'Min':
case 'Value':
$tagValue = (string) preg_replace_callback(AddressHelper::R1C1_COORDINATE_REGEX, $pregCallback, $tagValue);
$validation->setFormula1($tagValue);
break;
case 'Max':
$tagValue = (string) preg_replace_callback(AddressHelper::R1C1_COORDINATE_REGEX, $pregCallback, $tagValue);
$validation->setFormula2($tagValue);
break;
}
}
foreach ($cells as $cell) {
$sheet->getCell($cell)->setDataValidation(clone $validation);
}
}
}
}

View File

@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xml;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use SimpleXMLElement;
@ -14,9 +15,9 @@ class PageSettings
*/
private $printSettings;
public function __construct(SimpleXMLElement $xmlX, array $namespaces)
public function __construct(SimpleXMLElement $xmlX)
{
$printSettings = $this->pageSetup($xmlX, $namespaces, $this->getPrintDefaults());
$printSettings = $this->pageSetup($xmlX, $this->getPrintDefaults());
$this->printSettings = $this->printSetup($xmlX, $printSettings);
}
@ -56,13 +57,13 @@ class PageSettings
];
}
private function pageSetup(SimpleXMLElement $xmlX, array $namespaces, stdClass $printDefaults): stdClass
private function pageSetup(SimpleXMLElement $xmlX, stdClass $printDefaults): stdClass
{
if (isset($xmlX->WorksheetOptions->PageSetup)) {
foreach ($xmlX->WorksheetOptions->PageSetup as $pageSetupData) {
foreach ($pageSetupData as $pageSetupKey => $pageSetupValue) {
/** @scrutinizer ignore-call */
$pageSetupAttributes = $pageSetupValue->attributes($namespaces['x']);
$pageSetupAttributes = $pageSetupValue->attributes(Namespaces::URN_EXCEL);
if ($pageSetupAttributes !== null) {
switch ($pageSetupKey) {
case 'Layout':

View File

@ -92,6 +92,10 @@ class Properties
case 'Manager':
$docProps->setManager($stringValue);
break;
case 'HyperlinkBase':
$docProps->setHyperlinkBase($stringValue);
break;
case 'Keywords':
$docProps->setKeywords($stringValue);
@ -110,17 +114,10 @@ class Properties
?SimpleXMLElement $propertyValue,
SimpleXMLElement $propertyAttributes
): void {
$propertyType = DocumentProperties::PROPERTY_TYPE_UNKNOWN;
switch ((string) $propertyAttributes) {
case 'string':
$propertyType = DocumentProperties::PROPERTY_TYPE_STRING;
$propertyValue = trim((string) $propertyValue);
break;
case 'boolean':
$propertyType = DocumentProperties::PROPERTY_TYPE_BOOLEAN;
$propertyValue = (bool) $propertyValue;
$propertyValue = (bool) (string) $propertyValue;
break;
case 'integer':
@ -134,9 +131,15 @@ class Properties
break;
case 'dateTime.tz':
case 'dateTime.iso8601tz':
$propertyType = DocumentProperties::PROPERTY_TYPE_DATE;
$propertyValue = trim((string) $propertyValue);
break;
default:
$propertyType = DocumentProperties::PROPERTY_TYPE_STRING;
$propertyValue = trim((string) $propertyValue);
break;
}

View File

@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Reader\Xml;
use PhpOffice\PhpSpreadsheet\Style\Protection;
use SimpleXMLElement;
class Style
@ -30,7 +31,7 @@ class Style
$styleID = (string) $style_ss['ID'];
$this->styles[$styleID] = $this->styles['Default'] ?? [];
$alignment = $border = $font = $fill = $numberFormat = [];
$alignment = $border = $font = $fill = $numberFormat = $protection = [];
foreach ($style as $styleType => $styleDatax) {
$styleData = self::getSxml($styleDatax);
@ -65,10 +66,30 @@ class Style
}
break;
case 'Protection':
$locked = $hidden = null;
$styleAttributesP = $styleData->attributes($namespaces['x']);
if (isset($styleAttributes['Protected'])) {
$locked = ((bool) (string) $styleAttributes['Protected']) ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED;
}
if (isset($styleAttributesP['HideFormula'])) {
$hidden = ((bool) (string) $styleAttributesP['HideFormula']) ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED;
}
if ($locked !== null || $hidden !== null) {
$protection['protection'] = [];
if ($locked !== null) {
$protection['protection']['locked'] = $locked;
}
if ($hidden !== null) {
$protection['protection']['hidden'] = $hidden;
}
}
$this->styles[$styleID] = array_merge($alignment, $border, $font, $fill, $numberFormat);
break;
}
}
$this->styles[$styleID] = array_merge($alignment, $border, $font, $fill, $numberFormat, $protection);
}
return $this->styles;

View File

@ -56,11 +56,11 @@ class Font extends StyleBase
break;
case 'Bold':
$style['font']['bold'] = true;
$style['font']['bold'] = $styleAttributeValue === '1';
break;
case 'Italic':
$style['font']['italic'] = true;
$style['font']['italic'] = $styleAttributeValue === '1';
break;
case 'Underline':

View File

@ -75,14 +75,11 @@ class ReferenceHelper
*
* @return int
*/
public static function columnReverseSort($a, $b)
public static function columnReverseSort(string $a, string $b)
{
return -strcasecmp(strlen($a) . $a, strlen($b) . $b);
}
/** @var int */
private static $scrutinizer0 = 0;
/**
* Compare two cell addresses
* Intended for use as a Callback function for sorting cell addresses by column and row.
@ -92,16 +89,16 @@ class ReferenceHelper
*
* @return int
*/
public static function cellSort($a, $b)
public static function cellSort(string $a, string $b)
{
$ac = $bc = '';
$ar = self::$scrutinizer0;
$br = 0;
/** @scrutinizer be-damned */
sscanf($a, '%[A-Z]%d', $ac, $ar);
/** @var int $ar */
/** @var string $ac */
/** @scrutinizer be-damned */
sscanf($b, '%[A-Z]%d', $bc, $br);
$ac = (string) $ac;
$bc = (string) $bc;
/** @var int $br */
/** @var string $bc */
if ($ar === $br) {
return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
}
@ -118,16 +115,16 @@ class ReferenceHelper
*
* @return int
*/
public static function cellReverseSort($a, $b)
public static function cellReverseSort(string $a, string $b)
{
$ac = $bc = '';
$ar = self::$scrutinizer0;
$br = 0;
/** @scrutinizer be-damned */
sscanf($a, '%[A-Z]%d', $ac, $ar);
/** @var int $ar */
/** @var string $ac */
/** @scrutinizer be-damned */
sscanf($b, '%[A-Z]%d', $bc, $br);
$ac = (string) $ac;
$bc = (string) $bc;
/** @var int $br */
/** @var string $bc */
if ($ar === $br) {
return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
}
@ -142,7 +139,7 @@ class ReferenceHelper
* @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
* @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
*/
protected function adjustPageBreaks(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void
protected function adjustPageBreaks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
{
$aBreaks = $worksheet->getBreaks();
($numberOfColumns > 0 || $numberOfRows > 0)
@ -171,7 +168,7 @@ class ReferenceHelper
*
* @param Worksheet $worksheet The worksheet that we're editing
*/
protected function adjustComments($worksheet): void
protected function adjustComments(Worksheet $worksheet): void
{
$aComments = $worksheet->getComments();
$aNewComments = []; // the new array of all comments
@ -195,7 +192,7 @@ class ReferenceHelper
* @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
* @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
*/
protected function adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows): void
protected function adjustHyperlinks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
{
$aHyperlinkCollection = $worksheet->getHyperlinkCollection();
($numberOfColumns > 0 || $numberOfRows > 0)
@ -220,7 +217,7 @@ class ReferenceHelper
* @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
* @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
*/
protected function adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows): void
protected function adjustConditionalFormatting(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
{
$aStyles = $worksheet->getConditionalStylesCollection();
($numberOfColumns > 0 || $numberOfRows > 0)
@ -259,7 +256,7 @@ class ReferenceHelper
* @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
* @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
*/
protected function adjustDataValidations(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void
protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
{
$aDataValidationCollection = $worksheet->getDataValidationCollection();
($numberOfColumns > 0 || $numberOfRows > 0)
@ -299,7 +296,7 @@ class ReferenceHelper
* @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
* @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
*/
protected function adjustProtectedCells(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void
protected function adjustProtectedCells(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
{
$aProtectedCells = $worksheet->getProtectedCells();
($numberOfColumns > 0 || $numberOfRows > 0)
@ -412,7 +409,7 @@ class ReferenceHelper
$cellCollection = $worksheet->getCellCollection();
$missingCoordinates = array_filter(
array_map(function ($row) use ($highestColumn) {
return $highestColumn . $row;
return "{$highestColumn}{$row}";
}, range(1, $highestRow)),
function ($coordinate) use ($cellCollection) {
return $cellCollection->has($coordinate) === false;
@ -453,9 +450,9 @@ class ReferenceHelper
if ($cell->getDataType() === DataType::TYPE_FORMULA) {
// Formula should be adjusted
$worksheet->getCell($newCoordinate)
->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()));
->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true));
} else {
// Formula should not be adjusted
// Cell value should not be adjusted
$worksheet->getCell($newCoordinate)->setValueExplicit($cell->getValue(), $cell->getDataType());
}
@ -466,7 +463,7 @@ class ReferenceHelper
but we do still need to adjust any formulae in those cells */
if ($cell->getDataType() === DataType::TYPE_FORMULA) {
// Formula should be adjusted
$cell->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()));
$cell->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true));
}
}
}
@ -609,7 +606,7 @@ class ReferenceHelper
// Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
$column = 100000;
$row = 10000000 + (int) trim($match[3], '$');
$cellIndex = $column . $row;
$cellIndex = "{$column}{$row}";
$newCellTokens[$cellIndex] = preg_quote($toString, '/');
$cellTokens[$cellIndex] = '/(?<!\d\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
@ -634,7 +631,7 @@ class ReferenceHelper
// Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
$column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000;
$row = 10000000;
$cellIndex = $column . $row;
$cellIndex = "{$column}{$row}";
$newCellTokens[$cellIndex] = preg_quote($toString, '/');
$cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?![A-Z])/i';
@ -660,7 +657,7 @@ class ReferenceHelper
// Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
$column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
$row = (int) trim($row, '$') + 10000000;
$cellIndex = $column . $row;
$cellIndex = "{$column}{$row}";
$newCellTokens[$cellIndex] = preg_quote($toString, '/');
$cellTokens[$cellIndex] = '/(?<![A-Z]\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
@ -917,11 +914,18 @@ class ReferenceHelper
$cellAddress = $definedName->getValue();
$asFormula = ($cellAddress[0] === '=');
if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) {
/**
* If we delete the entire range that is referenced by a Named Range, MS Excel sets the value to #REF!
* PhpSpreadsheet still only does a basic adjustment, so the Named Range will still reference Cells.
* Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
* TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
* them with a #REF!
*/
if ($asFormula === true) {
$formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle());
$formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true);
$definedName->setValue($formula);
} else {
$definedName->setValue($this->updateCellReference(ltrim($cellAddress, '=')));
$definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='), true));
}
}
}
@ -929,8 +933,15 @@ class ReferenceHelper
private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
{
if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) {
/**
* If we delete the entire range that is referenced by a Named Formula, MS Excel sets the value to #REF!
* PhpSpreadsheet still only does a basic adjustment, so the Named Formula will still reference Cells.
* Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
* TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
* them with a #REF!
*/
$formula = $definedName->getValue();
$formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle());
$formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true);
$definedName->setValue($formula);
}
}

View File

@ -47,7 +47,7 @@ class TextElement implements ITextElement
}
/**
* Get font.
* Get font. For this class, the return value is always null.
*
* @return null|\PhpOffice\PhpSpreadsheet\Style\Font
*/

View File

@ -155,7 +155,7 @@ class Settings
/**
* Sets the implementation of cache that should be used for cell collection.
*/
public static function setCache(CacheInterface $cache): void
public static function setCache(?CacheInterface $cache): void
{
self::$cache = $cache;
}

View File

@ -184,7 +184,7 @@ class Date
throw new Exception("Invalid string $value supplied for datatype Date");
}
if (preg_match('/^\\d\\d:\\d\\d:\\d\\d/', $value) == 1) {
if (preg_match('/^\\s*\\d?\\d:\\d\\d(:\\d\\d([.]\\d+)?)?\\s*(am|pm)?\\s*$/i', $value) == 1) {
$newValue = fmod($newValue, 1.0);
}

View File

@ -161,11 +161,15 @@ class File
if ($zipMember !== '') {
$zipfile = "zip://$filename#$zipMember";
if (!self::fileExists($zipfile)) {
// Has the file been saved with Windoze directory separators rather than unix?
$zipfile = "zip://$filename#" . str_replace('/', '\\', $zipMember);
if (!self::fileExists($zipfile)) {
throw new ReaderException("Could not find zip member $zipfile");
}
}
}
}
/**
* Same as assertFile, except return true/false and don't throw Exception.
@ -186,6 +190,14 @@ class File
return self::validateZipFirst4($filename);
}
return self::fileExists("zip://$filename#$zipMember");
$zipfile = "zip://$filename#$zipMember";
if (self::fileExists($zipfile)) {
return true;
}
// Has the file been saved with Windoze directory separators rather than unix?
$zipfile = "zip://$filename#" . str_replace('/', '\\', $zipMember);
return self::fileExists($zipfile);
}
}

View File

@ -380,6 +380,7 @@ class Font
$approximate = self::$autoSizeMethod === self::AUTOSIZE_METHOD_APPROX;
$columnWidth = 0;
if (!$approximate) {
try {
$columnWidthAdjust = ceil(
self::getTextWidthPixelsExact(
str_repeat('n', 1 * (($filterAdjustment ? 3 : 1) + ($indentAdjustment * 2))),
@ -388,7 +389,6 @@ class Font
) * 1.07
);
try {
// Width of text in pixels excl. padding
// and addition because Excel adds some padding, just use approx width of 'n' glyph
$columnWidth = self::getTextWidthPixelsExact($cellText, $font, $rotation) + $columnWidthAdjust;
@ -453,29 +453,26 @@ class Font
$fontName = $font->getName();
$fontSize = $font->getSize();
// Calculate column width in pixels. We assume fixed glyph width. Result varies with font name and size.
// Calculate column width in pixels.
// We assume fixed glyph width, but count double for "fullwidth" characters.
// Result varies with font name and size.
switch ($fontName) {
case 'Calibri':
// value 8.26 was found via interpolation by inspecting real Excel files with Calibri 11 font.
$columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText));
$columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size
break;
case 'Arial':
// value 8 was set because of experience in different exports at Arial 10 font.
$columnWidth = (int) (8 * StringHelper::countCharacters($columnText));
$columnWidth = (int) (8 * StringHelper::countCharactersDbcs($columnText));
$columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size
break;
case 'Verdana':
// value 8 was found via interpolation by inspecting real Excel files with Verdana 10 font.
$columnWidth = (int) (8 * StringHelper::countCharacters($columnText));
$columnWidth = (int) (8 * StringHelper::countCharactersDbcs($columnText));
$columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size
break;
default:
// just assume Calibri
$columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText));
// value 8.26 was found via interpolation by inspecting real Excel files with Calibri 11 font.
$columnWidth = (int) (8.26 * StringHelper::countCharactersDbcs($columnText));
$columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size
break;
@ -564,10 +561,13 @@ class Font
if (mb_strlen(self::$trueTypeFontPath) > 1 && mb_substr(self::$trueTypeFontPath, -1) !== '/' && mb_substr(self::$trueTypeFontPath, -1) !== '\\') {
$separator = DIRECTORY_SEPARATOR;
}
$fontFileAbsolute = preg_match('~^([A-Za-z]:)?[/\\\\]~', $fontFile) === 1;
if (!$fontFileAbsolute) {
$fontFile = self::$trueTypeFontPath . $separator . $fontFile;
}
// Check if file actually exists
if ($checkPath && !file_exists($fontFile)) {
if ($checkPath && !file_exists($fontFile) && !$fontFileAbsolute) {
$alternateName = $name;
if ($index !== 'x' && $fontArray[$name][$index] !== $fontArray[$name]['x']) {
// Bold but no italic:

View File

@ -451,6 +451,18 @@ class StringHelper
return mb_strlen($textValue, $encoding);
}
/**
* Get character count using mb_strwidth rather than mb_strlen.
*
* @param string $encoding Encoding
*
* @return int Character count
*/
public static function countCharactersDbcs(string $textValue, string $encoding = 'UTF-8'): int
{
return mb_strwidth($textValue, $encoding);
}
/**
* Get a substring of a UTF-8 encoded string.
*

View File

@ -105,7 +105,6 @@ class Trend
$className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendType . 'BestFit';
//* @phpstan-ignore-next-line
$bestFit[$trendMethod] = new $className($yValues, $xValues, $const);
//* @phpstan-ignore-next-line
$bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit();
}
if ($trendType != self::TREND_BEST_FIT_NO_POLY) {

View File

@ -203,6 +203,14 @@ class Spreadsheet implements JsonSerializable
*/
private $tabRatio = 600;
/** @var Theme */
private $theme;
public function getTheme(): Theme
{
return $this->theme;
}
/**
* The workbook has macros ?
*
@ -476,6 +484,7 @@ class Spreadsheet implements JsonSerializable
{
$this->uniqueID = uniqid('', true);
$this->calculationEngine = new Calculation($this);
$this->theme = new Theme();
// Initialise worksheet collection and add one worksheet
$this->workSheetCollection = [];
@ -1654,4 +1663,26 @@ class Spreadsheet implements JsonSerializable
{
throw new Exception('Spreadsheet objects cannot be json encoded');
}
public function resetThemeFonts(): void
{
$majorFontLatin = $this->theme->getMajorFontLatin();
$minorFontLatin = $this->theme->getMinorFontLatin();
foreach ($this->cellXfCollection as $cellStyleXf) {
$scheme = $cellStyleXf->getFont()->getScheme();
if ($scheme === 'major') {
$cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
} elseif ($scheme === 'minor') {
$cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
}
}
foreach ($this->cellStyleXfCollection as $cellStyleXf) {
$scheme = $cellStyleXf->getFont()->getScheme();
if ($scheme === 'major') {
$cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
} elseif ($scheme === 'minor') {
$cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
}
}
}
}

View File

@ -362,23 +362,8 @@ class Color extends Supervisor
$green = self::getGreen($hexColourValue, false);
/** @var int $blue */
$blue = self::getBlue($hexColourValue, false);
if ($adjustPercentage > 0) {
$red += (255 - $red) * $adjustPercentage;
$green += (255 - $green) * $adjustPercentage;
$blue += (255 - $blue) * $adjustPercentage;
} else {
$red += $red * $adjustPercentage;
$green += $green * $adjustPercentage;
$blue += $blue * $adjustPercentage;
}
$rgb = strtoupper(
str_pad(dechex((int) $red), 2, '0', 0) .
str_pad(dechex((int) $green), 2, '0', 0) .
str_pad(dechex((int) $blue), 2, '0', 0)
);
return (($rgba) ? 'FF' : '') . $rgb;
return (($rgba) ? 'FF' : '') . RgbTint::rgbAndTintToRgb($red, $green, $blue, $adjustPercentage);
}
/**

View File

@ -248,7 +248,7 @@ class Conditional implements IComparable
/**
* Set Conditions.
*
* @param bool|float|int|string|(bool|float|int|string)[] $conditions Condition
* @param (bool|float|int|string)[]|bool|float|int|string $conditions Condition
*
* @return $this
*/

View File

@ -107,6 +107,9 @@ class Font extends Supervisor
*/
public $colorIndex;
/** @var string */
protected $scheme = '';
/**
* Create a new Font.
*
@ -231,6 +234,12 @@ class Font extends Supervisor
if (isset($styleArray['size'])) {
$this->setSize($styleArray['size']);
}
if (isset($styleArray['chartColor'])) {
$this->chartColor = $styleArray['chartColor'];
}
if (isset($styleArray['scheme'])) {
$this->setScheme($styleArray['scheme']);
}
}
return $this;
@ -278,13 +287,11 @@ class Font extends Supervisor
}
/**
* Set Name.
* Set Name and turn off Scheme.
*
* @param string $fontname
*
* @return $this
*/
public function setName($fontname)
public function setName($fontname): self
{
if ($fontname == '') {
$fontname = 'Calibri';
@ -296,7 +303,7 @@ class Font extends Supervisor
$this->name = $fontname;
}
return $this;
return $this->setScheme('');
}
public function setLatin(string $fontname): self
@ -634,6 +641,13 @@ class Font extends Supervisor
return $this;
}
public function setChartColorFromObject(?ChartColor $chartColor): self
{
$this->chartColor = $chartColor;
return $this;
}
/**
* Get Underline.
*
@ -774,6 +788,7 @@ class Font extends Supervisor
$this->underline .
($this->strikethrough ? 't' : 'f') .
$this->color->getHashCode() .
$this->scheme .
implode(
'*',
[
@ -802,6 +817,7 @@ class Font extends Supervisor
$this->exportArray2($exportedArray, 'italic', $this->getItalic());
$this->exportArray2($exportedArray, 'latin', $this->getLatin());
$this->exportArray2($exportedArray, 'name', $this->getName());
$this->exportArray2($exportedArray, 'scheme', $this->getScheme());
$this->exportArray2($exportedArray, 'size', $this->getSize());
$this->exportArray2($exportedArray, 'strikethrough', $this->getStrikethrough());
$this->exportArray2($exportedArray, 'strikeType', $this->getStrikeType());
@ -812,4 +828,27 @@ class Font extends Supervisor
return $exportedArray;
}
public function getScheme(): string
{
if ($this->isSupervisor) {
return $this->getSharedComponent()->getScheme();
}
return $this->scheme;
}
public function setScheme(string $scheme): self
{
if ($scheme === '' || $scheme === 'major' || $scheme === 'minor') {
if ($this->isSupervisor) {
$styleArray = $this->getStyleArray(['scheme' => $scheme]);
$this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
} else {
$this->scheme = $scheme;
}
}
return $this;
}
}

View File

@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Reader\Xls\Color\BIFF8;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
@ -67,14 +68,19 @@ class Formatter
// 3 sections: [POSITIVE/TEXT] [NEGATIVE] [ZERO]
// 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT]
$sectionCount = count($sections);
$color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . ')\\]/mui';
// Colour could be a named colour, or a numeric index entry in the colour-palette
$color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . '|color\\s*(\\d+))\\]/mui';
$cond_regex = '/\\[(>|>=|<|<=|=|<>)([+-]?\\d+([.]\\d+)?)\\]/';
$colors = ['', '', '', '', ''];
$conditionOperations = ['', '', '', '', ''];
$conditionComparisonValues = [0, 0, 0, 0, 0];
for ($idx = 0; $idx < $sectionCount; ++$idx) {
if (preg_match($color_regex, $sections[$idx], $matches)) {
if (isset($matches[2])) {
$colors[$idx] = '#' . BIFF8::lookup((int) $matches[2] + 7)['rgb'];
} else {
$colors[$idx] = $matches[0];
}
$sections[$idx] = (string) preg_replace($color_regex, '', $sections[$idx]);
}
if (preg_match($cond_regex, $sections[$idx], $matches)) {
@ -170,10 +176,11 @@ class Formatter
$format = (string) preg_replace('/_.?/ui', ' ', $format);
// Let's begin inspecting the format and converting the value to a formatted string
// Check for date/time characters (not inside quotes)
if (
(preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format)) &&
(preg_match('/0(?![^\[]*\])/miu', $format) === 0)
// Check for date/time characters (not inside quotes)
(preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format))
// A date/time with a decimal time shouldn't have a digit placeholder before the decimal point
&& (preg_match('/[0\?#]\.(?![^\[]*\])/miu', $format) === 0)
) {
// datetime format
$value = DateFormatter::format($value, $format);
@ -194,8 +201,6 @@ class Formatter
$value = $writerInstance->$function($value, $colors);
}
$value = str_replace(chr(0x00), '.', $value);
return $value;
return str_replace(chr(0x00), '.', $value);
}
}

View File

@ -89,13 +89,13 @@ class Accounting extends Currency
(
$this->currencySymbolPosition === self::LEADING_SYMBOL &&
$this->currencySymbolSpacing === self::SYMBOL_WITH_SPACING
) ? ' ' : '',
) ? "\u{a0}" : '',
$this->thousandsSeparator ? '#,##' : null,
$this->decimals > 0 ? '.' . str_repeat('0', $this->decimals) : null,
(
$this->currencySymbolPosition === self::TRAILING_SYMBOL &&
$this->currencySymbolSpacing === self::SYMBOL_WITH_SPACING
) ? ' ' : '',
) ? "\u{a0}" : '',
$this->currencySymbolPosition === self::TRAILING_SYMBOL ? $this->formatCurrencyCode() : null
);
}

View File

@ -0,0 +1,125 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
class Date extends DateTimeWizard
{
/**
* Year (4 digits), e.g. 2023.
*/
public const YEAR_FULL = 'yyyy';
/**
* Year (last 2 digits), e.g. 23.
*/
public const YEAR_SHORT = 'yy';
public const MONTH_FIRST_LETTER = 'mmmmm';
/**
* Month name, long form, e.g. January.
*/
public const MONTH_NAME_FULL = 'mmmm';
/**
* Month name, short form, e.g. Jan.
*/
public const MONTH_NAME_SHORT = 'mmm';
/**
* Month number with a leading zero if required, e.g. 01.
*/
public const MONTH_NUMBER_LONG = 'mm';
/**
* Month number without a leading zero, e.g. 1.
*/
public const MONTH_NUMBER_SHORT = 'm';
/**
* Day of the week, full form, e.g. Tuesday.
*/
public const WEEKDAY_NAME_LONG = 'dddd';
/**
* Day of the week, short form, e.g. Tue.
*/
public const WEEKDAY_NAME_SHORT = 'ddd';
/**
* Day number with a leading zero, e.g. 03.
*/
public const DAY_NUMBER_LONG = 'dd';
/**
* Day number without a leading zero, e.g. 3.
*/
public const DAY_NUMBER_SHORT = 'd';
protected const DATE_BLOCKS = [
self::YEAR_FULL,
self::YEAR_SHORT,
self::MONTH_FIRST_LETTER,
self::MONTH_NAME_FULL,
self::MONTH_NAME_SHORT,
self::MONTH_NUMBER_LONG,
self::MONTH_NUMBER_SHORT,
self::WEEKDAY_NAME_LONG,
self::WEEKDAY_NAME_SHORT,
self::DAY_NUMBER_LONG,
self::DAY_NUMBER_SHORT,
];
public const SEPARATOR_DASH = '-';
public const SEPARATOR_DOT = '.';
public const SEPARATOR_SLASH = '/';
public const SEPARATOR_SPACE_NONBREAKING = "\u{a0}";
public const SEPARATOR_SPACE = ' ';
protected const DATE_DEFAULT = [
self::YEAR_FULL,
self::MONTH_NUMBER_LONG,
self::DAY_NUMBER_LONG,
];
/**
* @var string[]
*/
protected array $separators;
/**
* @var string[]
*/
protected array $formatBlocks;
/**
* @param null|string|string[] $separators
* If you want to use the same separator for all format blocks, then it can be passed as a string literal;
* if you wish to use different separators, then they should be passed as an array.
* If you want to use only a single format block, then pass a null as the separator argument
*/
public function __construct($separators = self::SEPARATOR_DASH, string ...$formatBlocks)
{
$separators ??= self::SEPARATOR_DASH;
$formatBlocks = (count($formatBlocks) === 0) ? self::DATE_DEFAULT : $formatBlocks;
$this->separators = $this->padSeparatorArray(
is_array($separators) ? $separators : [$separators],
count($formatBlocks) - 1
);
$this->formatBlocks = array_map([$this, 'mapFormatBlocks'], $formatBlocks);
}
private function mapFormatBlocks(string $value): string
{
// Any date masking codes are returned as lower case values
if (in_array(mb_strtolower($value), self::DATE_BLOCKS, true)) {
return mb_strtolower($value);
}
// Wrap any string literals in quotes, so that they're clearly defined as string literals
return $this->wrapLiteral($value);
}
public function format(): string
{
return implode('', array_map([$this, 'intersperse'], $this->formatBlocks, $this->separators));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
class DateTime extends DateTimeWizard
{
/**
* @var string[]
*/
protected array $separators;
/**
* @var array<DateTimeWizard|string>
*/
protected array $formatBlocks;
/**
* @param null|string|string[] $separators
* If you want to use only a single format block, then pass a null as the separator argument
* @param DateTimeWizard|string ...$formatBlocks
*/
public function __construct($separators, ...$formatBlocks)
{
$this->separators = $this->padSeparatorArray(
is_array($separators) ? $separators : [$separators],
count($formatBlocks) - 1
);
$this->formatBlocks = array_map([$this, 'mapFormatBlocks'], $formatBlocks);
}
/**
* @param DateTimeWizard|string $value
*/
private function mapFormatBlocks($value): string
{
// Any date masking codes are returned as lower case values
if (is_object($value)) {
// We can't explicitly test for Stringable until PHP >= 8.0
return $value;
}
// Wrap any string literals in quotes, so that they're clearly defined as string literals
return $this->wrapLiteral($value);
}
public function format(): string
{
return implode('', array_map([$this, 'intersperse'], $this->formatBlocks, $this->separators));
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
abstract class DateTimeWizard implements Wizard
{
protected const NO_ESCAPING_NEEDED = "$+-/():!^&'~{}<>= ";
protected function padSeparatorArray(array $separators, int $count): array
{
$lastSeparator = array_pop($separators);
return $separators + array_fill(0, $count, $lastSeparator);
}
protected function escapeSingleCharacter(string $value): string
{
if (strpos(self::NO_ESCAPING_NEEDED, $value) !== false) {
return $value;
}
return "\\{$value}";
}
protected function wrapLiteral(string $value): string
{
if (mb_strlen($value, 'UTF-8') === 1) {
return $this->escapeSingleCharacter($value);
}
// Wrap any other string literals in quotes, so that they're clearly defined as string literals
return '"' . str_replace('"', '""', $value) . '"';
}
protected function intersperse(string $formatBlock, ?string $separator): string
{
return "{$formatBlock}{$separator}";
}
public function __toString(): string
{
return $this->format();
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
class Duration extends DateTimeWizard
{
public const DAYS_DURATION = 'd';
/**
* Hours as a duration (can exceed 24), e.g. 29.
*/
public const HOURS_DURATION = '[h]';
/**
* Hours without a leading zero, e.g. 9.
*/
public const HOURS_SHORT = 'h';
/**
* Hours with a leading zero, e.g. 09.
*/
public const HOURS_LONG = 'hh';
/**
* Minutes as a duration (can exceed 60), e.g. 109.
*/
public const MINUTES_DURATION = '[m]';
/**
* Minutes without a leading zero, e.g. 5.
*/
public const MINUTES_SHORT = 'm';
/**
* Minutes with a leading zero, e.g. 05.
*/
public const MINUTES_LONG = 'mm';
/**
* Seconds as a duration (can exceed 60), e.g. 129.
*/
public const SECONDS_DURATION = '[s]';
/**
* Seconds without a leading zero, e.g. 2.
*/
public const SECONDS_SHORT = 's';
/**
* Seconds with a leading zero, e.g. 02.
*/
public const SECONDS_LONG = 'ss';
protected const DURATION_BLOCKS = [
self::DAYS_DURATION,
self::HOURS_DURATION,
self::HOURS_LONG,
self::HOURS_SHORT,
self::MINUTES_DURATION,
self::MINUTES_LONG,
self::MINUTES_SHORT,
self::SECONDS_DURATION,
self::SECONDS_LONG,
self::SECONDS_SHORT,
];
protected const DURATION_MASKS = [
self::DAYS_DURATION => self::DAYS_DURATION,
self::HOURS_DURATION => self::HOURS_SHORT,
self::MINUTES_DURATION => self::MINUTES_LONG,
self::SECONDS_DURATION => self::SECONDS_LONG,
];
protected const DURATION_DEFAULTS = [
self::HOURS_LONG => self::HOURS_DURATION,
self::HOURS_SHORT => self::HOURS_DURATION,
self::MINUTES_LONG => self::MINUTES_DURATION,
self::MINUTES_SHORT => self::MINUTES_DURATION,
self::SECONDS_LONG => self::SECONDS_DURATION,
self::SECONDS_SHORT => self::SECONDS_DURATION,
];
public const SEPARATOR_COLON = ':';
public const SEPARATOR_SPACE_NONBREAKING = "\u{a0}";
public const SEPARATOR_SPACE = ' ';
public const DURATION_DEFAULT = [
self::HOURS_DURATION,
self::MINUTES_LONG,
self::SECONDS_LONG,
];
/**
* @var string[]
*/
protected array $separators;
/**
* @var string[]
*/
protected array $formatBlocks;
protected bool $durationIsSet = false;
/**
* @param null|string|string[] $separators
* If you want to use the same separator for all format blocks, then it can be passed as a string literal;
* if you wish to use different separators, then they should be passed as an array.
* If you want to use only a single format block, then pass a null as the separator argument
*/
public function __construct($separators = self::SEPARATOR_COLON, string ...$formatBlocks)
{
$separators ??= self::SEPARATOR_COLON;
$formatBlocks = (count($formatBlocks) === 0) ? self::DURATION_DEFAULT : $formatBlocks;
$this->separators = $this->padSeparatorArray(
is_array($separators) ? $separators : [$separators],
count($formatBlocks) - 1
);
$this->formatBlocks = array_map([$this, 'mapFormatBlocks'], $formatBlocks);
if ($this->durationIsSet === false) {
// We need at least one duration mask, so if none has been set we change the first mask element
// to a duration.
$this->formatBlocks[0] = self::DURATION_DEFAULTS[mb_strtolower($this->formatBlocks[0])];
}
}
private function mapFormatBlocks(string $value): string
{
// Any duration masking codes are returned as lower case values
if (in_array(mb_strtolower($value), self::DURATION_BLOCKS, true)) {
if (array_key_exists(mb_strtolower($value), self::DURATION_MASKS)) {
if ($this->durationIsSet) {
// We should only have a single duration mask, the first defined in the mask set,
// so convert any additional duration masks to standard time masks.
$value = self::DURATION_MASKS[mb_strtolower($value)];
}
$this->durationIsSet = true;
}
return mb_strtolower($value);
}
// Wrap any string literals in quotes, so that they're clearly defined as string literals
return $this->wrapLiteral($value);
}
public function format(): string
{
return implode('', array_map([$this, 'intersperse'], $this->formatBlocks, $this->separators));
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;
class Time extends DateTimeWizard
{
/**
* Hours without a leading zero, e.g. 9.
*/
public const HOURS_SHORT = 'h';
/**
* Hours with a leading zero, e.g. 09.
*/
public const HOURS_LONG = 'hh';
/**
* Minutes without a leading zero, e.g. 5.
*/
public const MINUTES_SHORT = 'm';
/**
* Minutes with a leading zero, e.g. 05.
*/
public const MINUTES_LONG = 'mm';
/**
* Seconds without a leading zero, e.g. 2.
*/
public const SECONDS_SHORT = 's';
/**
* Seconds with a leading zero, e.g. 02.
*/
public const SECONDS_LONG = 'ss';
public const MORNING_AFTERNOON = 'AM/PM';
protected const TIME_BLOCKS = [
self::HOURS_LONG,
self::HOURS_SHORT,
self::MINUTES_LONG,
self::MINUTES_SHORT,
self::SECONDS_LONG,
self::SECONDS_SHORT,
self::MORNING_AFTERNOON,
];
public const SEPARATOR_COLON = ':';
public const SEPARATOR_SPACE_NONBREAKING = "\u{a0}";
public const SEPARATOR_SPACE = ' ';
protected const TIME_DEFAULT = [
self::HOURS_LONG,
self::MINUTES_LONG,
self::SECONDS_LONG,
];
/**
* @var string[]
*/
protected array $separators;
/**
* @var string[]
*/
protected array $formatBlocks;
/**
* @param null|string|string[] $separators
* If you want to use the same separator for all format blocks, then it can be passed as a string literal;
* if you wish to use different separators, then they should be passed as an array.
* If you want to use only a single format block, then pass a null as the separator argument
*/
public function __construct($separators = self::SEPARATOR_COLON, string ...$formatBlocks)
{
$separators ??= self::SEPARATOR_COLON;
$formatBlocks = (count($formatBlocks) === 0) ? self::TIME_DEFAULT : $formatBlocks;
$this->separators = $this->padSeparatorArray(
is_array($separators) ? $separators : [$separators],
count($formatBlocks) - 1
);
$this->formatBlocks = array_map([$this, 'mapFormatBlocks'], $formatBlocks);
}
private function mapFormatBlocks(string $value): string
{
// Any date masking codes are returned as lower case values
// except for AM/PM, which is set to uppercase
if (in_array(mb_strtolower($value), self::TIME_BLOCKS, true)) {
return mb_strtolower($value);
} elseif (mb_strtoupper($value) === self::MORNING_AFTERNOON) {
return mb_strtoupper($value);
}
// Wrap any string literals in quotes, so that they're clearly defined as string literals
return $this->wrapLiteral($value);
}
public function format(): string
{
return implode('', array_map([$this, 'intersperse'], $this->formatBlocks, $this->separators));
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style;
/**
* Class to handle tint applied to color.
* Code borrows heavily from some Python projects.
*
* @see https://docs.python.org/3/library/colorsys.html
* @see https://gist.github.com/Mike-Honey/b36e651e9a7f1d2e1d60ce1c63b9b633
*/
class RgbTint
{
private const ONE_THIRD = 1.0 / 3.0;
private const ONE_SIXTH = 1.0 / 6.0;
private const TWO_THIRD = 2.0 / 3.0;
private const RGBMAX = 255.0;
/**
* MS excel's tint function expects that HLS is base 240.
*
* @see https://social.msdn.microsoft.com/Forums/en-US/e9d8c136-6d62-4098-9b1b-dac786149f43/excel-color-tint-algorithm-incorrect?forum=os_binaryfile#d3c2ac95-52e0-476b-86f1-e2a697f24969
*/
private const HLSMAX = 240.0;
/**
* Convert red/green/blue to hue/luminance/saturation.
*
* @param float $red 0.0 through 1.0
* @param float $green 0.0 through 1.0
* @param float $blue 0.0 through 1.0
*
* @return float[]
*/
private static function rgbToHls(float $red, float $green, float $blue): array
{
$maxc = max($red, $green, $blue);
$minc = min($red, $green, $blue);
$luminance = ($minc + $maxc) / 2.0;
if ($minc === $maxc) {
return [0.0, $luminance, 0.0];
}
$maxMinusMin = $maxc - $minc;
if ($luminance <= 0.5) {
$s = $maxMinusMin / ($maxc + $minc);
} else {
$s = $maxMinusMin / (2.0 - $maxc - $minc);
}
$rc = ($maxc - $red) / $maxMinusMin;
$gc = ($maxc - $green) / $maxMinusMin;
$bc = ($maxc - $blue) / $maxMinusMin;
if ($red === $maxc) {
$h = $bc - $gc;
} elseif ($green === $maxc) {
$h = 2.0 + $rc - $bc;
} else {
$h = 4.0 + $gc - $rc;
}
$h = self::positiveDecimalPart($h / 6.0);
return [$h, $luminance, $s];
}
/** @var mixed */
private static $scrutinizerZeroPointZero = 0.0;
/**
* Convert hue/luminance/saturation to red/green/blue.
*
* @param float $hue 0.0 through 1.0
* @param float $luminance 0.0 through 1.0
* @param float $saturation 0.0 through 1.0
*
* @return float[]
*/
private static function hlsToRgb($hue, $luminance, $saturation): array
{
if ($saturation === self::$scrutinizerZeroPointZero) {
return [$luminance, $luminance, $luminance];
}
if ($luminance <= 0.5) {
$m2 = $luminance * (1.0 + $saturation);
} else {
$m2 = $luminance + $saturation - ($luminance * $saturation);
}
$m1 = 2.0 * $luminance - $m2;
return [
self::vFunction($m1, $m2, $hue + self::ONE_THIRD),
self::vFunction($m1, $m2, $hue),
self::vFunction($m1, $m2, $hue - self::ONE_THIRD),
];
}
private static function vFunction(float $m1, float $m2, float $hue): float
{
$hue = self::positiveDecimalPart($hue);
if ($hue < self::ONE_SIXTH) {
return $m1 + ($m2 - $m1) * $hue * 6.0;
}
if ($hue < 0.5) {
return $m2;
}
if ($hue < self::TWO_THIRD) {
return $m1 + ($m2 - $m1) * (self::TWO_THIRD - $hue) * 6.0;
}
return $m1;
}
private static function positiveDecimalPart(float $hue): float
{
$hue = fmod($hue, 1.0);
return ($hue >= 0.0) ? $hue : (1.0 + $hue);
}
/**
* Convert red/green/blue to HLSMAX-based hue/luminance/saturation.
*
* @return int[]
*/
private static function rgbToMsHls(int $red, int $green, int $blue): array
{
$red01 = $red / self::RGBMAX;
$green01 = $green / self::RGBMAX;
$blue01 = $blue / self::RGBMAX;
[$hue, $luminance, $saturation] = self::rgbToHls($red01, $green01, $blue01);
return [
(int) round($hue * self::HLSMAX),
(int) round($luminance * self::HLSMAX),
(int) round($saturation * self::HLSMAX),
];
}
/**
* Converts HLSMAX based HLS values to rgb values in the range (0,1).
*
* @return float[]
*/
private static function msHlsToRgb(int $hue, int $lightness, int $saturation): array
{
return self::hlsToRgb($hue / self::HLSMAX, $lightness / self::HLSMAX, $saturation / self::HLSMAX);
}
/**
* Tints HLSMAX based luminance.
*
* @see http://ciintelligence.blogspot.co.uk/2012/02/converting-excel-theme-color-and-tint.html
*/
private static function tintLuminance(float $tint, float $luminance): int
{
if ($tint < 0) {
return (int) round($luminance * (1.0 + $tint));
}
return (int) round($luminance * (1.0 - $tint) + (self::HLSMAX - self::HLSMAX * (1.0 - $tint)));
}
/**
* Return result of tinting supplied rgb as 6 hex digits.
*/
public static function rgbAndTintToRgb(int $red, int $green, int $blue, float $tint): string
{
[$hue, $luminance, $saturation] = self::rgbToMsHls($red, $green, $blue);
[$red, $green, $blue] = self::msHlsToRgb($hue, self::tintLuminance($tint, $luminance), $saturation);
return sprintf(
'%02X%02X%02X',
(int) round($red * self::RGBMAX),
(int) round($green * self::RGBMAX),
(int) round($blue * self::RGBMAX)
);
}
}

View File

@ -2,7 +2,10 @@
namespace PhpOffice\PhpSpreadsheet\Style;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
class Style extends Supervisor
@ -122,7 +125,7 @@ class Style extends Supervisor
public function getSharedComponent(): self
{
$activeSheet = $this->getActiveSheet();
$selectedCell = $this->getActiveCell(); // e.g. 'A1'
$selectedCell = Functions::trimSheetFromCellReference($this->getActiveCell()); // e.g. 'A1'
if ($activeSheet->cellExists($selectedCell)) {
$xfIndex = $activeSheet->getCell($selectedCell)->getXfIndex();
@ -203,8 +206,15 @@ class Style extends Supervisor
if ($this->isSupervisor) {
$pRange = $this->getSelectedCells();
// Uppercase coordinate
// Uppercase coordinate and strip any Worksheet reference from the selected range
$pRange = strtoupper($pRange);
if (strpos($pRange, '!') !== false) {
$pRangeWorksheet = StringHelper::strToUpper(trim(substr($pRange, 0, (int) strrpos($pRange, '!')), "'"));
if ($pRangeWorksheet !== '' && StringHelper::strToUpper($this->getActiveSheet()->getTitle()) !== $pRangeWorksheet) {
throw new Exception('Invalid Worksheet for specified Range');
}
$pRange = strtoupper(Functions::trimSheetFromCellReference($pRange));
}
// Is it a cell range or a single cell?
if (strpos($pRange, ':') === false) {

View File

@ -0,0 +1,269 @@
<?php
namespace PhpOffice\PhpSpreadsheet;
class Theme
{
/** @var string */
private $themeColorName = 'Office';
/** @var string */
private $themeFontName = 'Office';
public const COLOR_SCHEME_2013_PLUS_NAME = 'Office 2013+';
public const COLOR_SCHEME_2013_PLUS = [
'dk1' => '000000',
'lt1' => 'FFFFFF',
'dk2' => '44546A',
'lt2' => 'E7E6E6',
'accent1' => '4472C4',
'accent2' => 'ED7D31',
'accent3' => 'A5A5A5',
'accent4' => 'FFC000',
'accent5' => '5B9BD5',
'accent6' => '70AD47',
'hlink' => '0563C1',
'folHlink' => '954F72',
];
public const COLOR_SCHEME_2007_2010_NAME = 'Office 2007-2010';
public const COLOR_SCHEME_2007_2010 = [
'dk1' => '000000',
'lt1' => 'FFFFFF',
'dk2' => '1F497D',
'lt2' => 'EEECE1',
'accent1' => '4F81BD',
'accent2' => 'C0504D',
'accent3' => '9BBB59',
'accent4' => '8064A2',
'accent5' => '4BACC6',
'accent6' => 'F79646',
'hlink' => '0000FF',
'folHlink' => '800080',
];
/** @var string[] */
private $themeColors = self::COLOR_SCHEME_2007_2010;
/** @var string */
private $majorFontLatin = 'Cambria';
/** @var string */
private $majorFontEastAsian = '';
/** @var string */
private $majorFontComplexScript = '';
/** @var string */
private $minorFontLatin = 'Calibri';
/** @var string */
private $minorFontEastAsian = '';
/** @var string */
private $minorFontComplexScript = '';
/**
* Map of Major (header) fonts to write.
*
* @var string[]
*/
private $majorFontSubstitutions = self::FONTS_TIMES_SUBSTITUTIONS;
/**
* Map of Minor (body) fonts to write.
*
* @var string[]
*/
private $minorFontSubstitutions = self::FONTS_ARIAL_SUBSTITUTIONS;
public const FONTS_TIMES_SUBSTITUTIONS = [
'Jpan' => ' Pゴシック',
'Hang' => '맑은 고딕',
'Hans' => '宋体',
'Hant' => '新細明體',
'Arab' => 'Times New Roman',
'Hebr' => 'Times New Roman',
'Thai' => 'Tahoma',
'Ethi' => 'Nyala',
'Beng' => 'Vrinda',
'Gujr' => 'Shruti',
'Khmr' => 'MoolBoran',
'Knda' => 'Tunga',
'Guru' => 'Raavi',
'Cans' => 'Euphemia',
'Cher' => 'Plantagenet Cherokee',
'Yiii' => 'Microsoft Yi Baiti',
'Tibt' => 'Microsoft Himalaya',
'Thaa' => 'MV Boli',
'Deva' => 'Mangal',
'Telu' => 'Gautami',
'Taml' => 'Latha',
'Syrc' => 'Estrangelo Edessa',
'Orya' => 'Kalinga',
'Mlym' => 'Kartika',
'Laoo' => 'DokChampa',
'Sinh' => 'Iskoola Pota',
'Mong' => 'Mongolian Baiti',
'Viet' => 'Times New Roman',
'Uigh' => 'Microsoft Uighur',
'Geor' => 'Sylfaen',
];
public const FONTS_ARIAL_SUBSTITUTIONS = [
'Jpan' => ' Pゴシック',
'Hang' => '맑은 고딕',
'Hans' => '宋体',
'Hant' => '新細明體',
'Arab' => 'Arial',
'Hebr' => 'Arial',
'Thai' => 'Tahoma',
'Ethi' => 'Nyala',
'Beng' => 'Vrinda',
'Gujr' => 'Shruti',
'Khmr' => 'DaunPenh',
'Knda' => 'Tunga',
'Guru' => 'Raavi',
'Cans' => 'Euphemia',
'Cher' => 'Plantagenet Cherokee',
'Yiii' => 'Microsoft Yi Baiti',
'Tibt' => 'Microsoft Himalaya',
'Thaa' => 'MV Boli',
'Deva' => 'Mangal',
'Telu' => 'Gautami',
'Taml' => 'Latha',
'Syrc' => 'Estrangelo Edessa',
'Orya' => 'Kalinga',
'Mlym' => 'Kartika',
'Laoo' => 'DokChampa',
'Sinh' => 'Iskoola Pota',
'Mong' => 'Mongolian Baiti',
'Viet' => 'Arial',
'Uigh' => 'Microsoft Uighur',
'Geor' => 'Sylfaen',
];
public function getThemeColors(): array
{
return $this->themeColors;
}
public function setThemeColor(string $key, string $value): self
{
$this->themeColors[$key] = $value;
return $this;
}
public function getThemeColorName(): string
{
return $this->themeColorName;
}
public function setThemeColorName(string $name, ?array $themeColors = null): self
{
$this->themeColorName = $name;
if ($name === self::COLOR_SCHEME_2007_2010_NAME) {
$themeColors = $themeColors ?? self::COLOR_SCHEME_2007_2010;
} elseif ($name === self::COLOR_SCHEME_2013_PLUS_NAME) {
$themeColors = $themeColors ?? self::COLOR_SCHEME_2013_PLUS;
}
if ($themeColors !== null) {
$this->themeColors = $themeColors;
}
return $this;
}
public function getMajorFontLatin(): string
{
return $this->majorFontLatin;
}
public function getMajorFontEastAsian(): string
{
return $this->majorFontEastAsian;
}
public function getMajorFontComplexScript(): string
{
return $this->majorFontComplexScript;
}
public function getMajorFontSubstitutions(): array
{
return $this->majorFontSubstitutions;
}
/** @param null|array $substitutions */
public function setMajorFontValues(?string $latin, ?string $eastAsian, ?string $complexScript, $substitutions): self
{
if (!empty($latin)) {
$this->majorFontLatin = $latin;
}
if ($eastAsian !== null) {
$this->majorFontEastAsian = $eastAsian;
}
if ($complexScript !== null) {
$this->majorFontComplexScript = $complexScript;
}
if ($substitutions !== null) {
$this->majorFontSubstitutions = $substitutions;
}
return $this;
}
public function getMinorFontLatin(): string
{
return $this->minorFontLatin;
}
public function getMinorFontEastAsian(): string
{
return $this->minorFontEastAsian;
}
public function getMinorFontComplexScript(): string
{
return $this->minorFontComplexScript;
}
public function getMinorFontSubstitutions(): array
{
return $this->minorFontSubstitutions;
}
/** @param null|array $substitutions */
public function setMinorFontValues(?string $latin, ?string $eastAsian, ?string $complexScript, $substitutions): self
{
if (!empty($latin)) {
$this->minorFontLatin = $latin;
}
if ($eastAsian !== null) {
$this->minorFontEastAsian = $eastAsian;
}
if ($complexScript !== null) {
$this->minorFontComplexScript = $complexScript;
}
if ($substitutions !== null) {
$this->minorFontSubstitutions = $substitutions;
}
return $this;
}
public function getThemeFontName(): string
{
return $this->themeFontName;
}
public function setThemeFontName(?string $name): self
{
if (!empty($name)) {
$this->themeFontName = $name;
}
return $this;
}
}

View File

@ -321,7 +321,7 @@ class AutoFilter
*
* @return bool
*/
private static function filterTestInSimpleDataSet($cellValue, $dataSet)
protected static function filterTestInSimpleDataSet($cellValue, $dataSet)
{
$dataSetValues = $dataSet['filterValues'];
$blanks = $dataSet['blanks'];
@ -340,7 +340,7 @@ class AutoFilter
*
* @return bool
*/
private static function filterTestInDateGroupSet($cellValue, $dataSet)
protected static function filterTestInDateGroupSet($cellValue, $dataSet)
{
$dateSet = $dataSet['filterValues'];
$blanks = $dataSet['blanks'];
@ -384,7 +384,7 @@ class AutoFilter
*
* @return bool
*/
private static function filterTestInCustomDataSet($cellValue, $ruleSet)
protected static function filterTestInCustomDataSet($cellValue, $ruleSet)
{
/** @var array[] */
$dataSet = $ruleSet['filterRules'];
@ -509,7 +509,7 @@ class AutoFilter
*
* @return bool
*/
private static function filterTestInPeriodDateSet($cellValue, $monthSet)
protected static function filterTestInPeriodDateSet($cellValue, $monthSet)
{
// Blank cells are always ignored, so return a FALSE
if (($cellValue == '') || ($cellValue === null)) {

View File

@ -197,21 +197,6 @@ class PageMargins
return $this;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$vars = get_object_vars($this);
foreach ($vars as $key => $value) {
if (is_object($value)) {
$this->$key = clone $value;
} else {
$this->$key = $value;
}
}
}
public static function fromCentimeters(float $value): float
{
return $value / 2.54;

View File

@ -885,19 +885,4 @@ class PageSetup
return $this;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$vars = get_object_vars($this);
foreach ($vars as $key => $value) {
if (is_object($value)) {
$this->$key = clone $value;
} else {
$this->$key = $value;
}
}
}
}

View File

@ -175,19 +175,4 @@ class SheetView
return $this;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$vars = get_object_vars($this);
foreach ($vars as $key => $value) {
if (is_object($value)) {
$this->$key = clone $value;
} else {
$this->$key = $value;
}
}
}
}

View File

@ -180,7 +180,7 @@ class Table
private function updateStructuredReferencesInCells(Worksheet $worksheet, string $newName): void
{
$pattern = '/' . preg_quote($this->name) . '\[/mui';
$pattern = '/' . preg_quote($this->name, '/') . '\[/mui';
foreach ($worksheet->getCoordinates(false) as $coordinate) {
$cell = $worksheet->getCell($coordinate);
@ -196,7 +196,7 @@ class Table
private function updateStructuredReferencesInNamedFormulae(Spreadsheet $spreadsheet, string $newName): void
{
$pattern = '/' . preg_quote($this->name) . '\[/mui';
$pattern = '/' . preg_quote($this->name, '/') . '\[/mui';
foreach ($spreadsheet->getNamedFormulae() as $namedFormula) {
$formula = $namedFormula->getValue();

View File

@ -225,7 +225,7 @@ class Column
private static function updateStructuredReferencesInCells(Worksheet $worksheet, string $oldTitle, string $newTitle): void
{
$pattern = '/\[(@?)' . preg_quote($oldTitle) . '\]/mui';
$pattern = '/\[(@?)' . preg_quote($oldTitle, '/') . '\]/mui';
foreach ($worksheet->getCoordinates(false) as $coordinate) {
$cell = $worksheet->getCell($coordinate);
@ -241,7 +241,7 @@ class Column
private static function updateStructuredReferencesInNamedFormulae(Spreadsheet $spreadsheet, string $oldTitle, string $newTitle): void
{
$pattern = '/\[(@?)' . preg_quote($oldTitle) . '\]/mui';
$pattern = '/\[(@?)' . preg_quote($oldTitle, '/') . '\]/mui';
foreach ($spreadsheet->getNamedFormulae() as $namedFormula) {
$formula = $namedFormula->getValue();

View File

@ -53,6 +53,9 @@ class Validations
return self::validateCellRange($cellRange);
}
private const SETMAXROW = '${1}1:${2}' . AddressRange::MAX_ROW;
private const SETMAXCOL = 'A${1}:' . AddressRange::MAX_COLUMN . '${2}';
/**
* Validate a cell range.
*
@ -69,7 +72,7 @@ class Validations
// or Row ranges like '1:3' to 'A1:XFD3'
$addressRange = (string) preg_replace(
['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'],
['${1}1:${2}1048576', 'A${1}:XFD${2}'],
[self::SETMAXROW, self::SETMAXCOL],
$addressRange
);

View File

@ -543,9 +543,18 @@ class Worksheet implements IComparable
*/
public function getColumnDimensions()
{
/** @var callable */
$callable = [self::class, 'columnDimensionCompare'];
uasort($this->columnDimensions, $callable);
return $this->columnDimensions;
}
private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int
{
return $a->getColumnNumeric() - $b->getColumnNumeric();
}
/**
* Get default column dimension.
*
@ -1806,9 +1815,15 @@ class Worksheet implements IComparable
public function getBreaks()
{
$breaks = [];
/** @var callable */
$compareFunction = [self::class, 'compareRowBreaks'];
uksort($this->rowBreaks, $compareFunction);
foreach ($this->rowBreaks as $break) {
$breaks[$break->getCoordinate()] = self::BREAK_ROW;
}
/** @var callable */
$compareFunction = [self::class, 'compareColumnBreaks'];
uksort($this->columnBreaks, $compareFunction);
foreach ($this->columnBreaks as $break) {
$breaks[$break->getCoordinate()] = self::BREAK_COLUMN;
}
@ -1823,16 +1838,40 @@ class Worksheet implements IComparable
*/
public function getRowBreaks()
{
/** @var callable */
$compareFunction = [self::class, 'compareRowBreaks'];
uksort($this->rowBreaks, $compareFunction);
return $this->rowBreaks;
}
protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
{
$row1 = Coordinate::indexesFromString($coordinate1)[1];
$row2 = Coordinate::indexesFromString($coordinate2)[1];
return $row1 - $row2;
}
protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
{
$column1 = Coordinate::indexesFromString($coordinate1)[0];
$column2 = Coordinate::indexesFromString($coordinate2)[0];
return $column1 - $column2;
}
/**
* Get row breaks.
* Get column breaks.
*
* @return PageBreak[]
*/
public function getColumnBreaks()
{
/** @var callable */
$compareFunction = [self::class, 'compareColumnBreaks'];
uksort($this->columnBreaks, $compareFunction);
return $this->columnBreaks;
}
@ -2448,12 +2487,12 @@ class Worksheet implements IComparable
/**
* Insert a new row, updating all possible related data.
*
* @param int $before Insert before this one
* @param int $numberOfRows Number of rows to insert
* @param int $before Insert before this row number
* @param int $numberOfRows Number of new rows to insert
*
* @return $this
*/
public function insertNewRowBefore($before, $numberOfRows = 1)
public function insertNewRowBefore(int $before, int $numberOfRows = 1)
{
if ($before >= 1) {
$objReferenceHelper = ReferenceHelper::getInstance();
@ -2468,12 +2507,12 @@ class Worksheet implements IComparable
/**
* Insert a new column, updating all possible related data.
*
* @param string $before Insert before this one, eg: 'A'
* @param int $numberOfColumns Number of columns to insert
* @param string $before Insert before this column Name, eg: 'A'
* @param int $numberOfColumns Number of new columns to insert
*
* @return $this
*/
public function insertNewColumnBefore($before, $numberOfColumns = 1)
public function insertNewColumnBefore(string $before, int $numberOfColumns = 1)
{
if (!is_numeric($before)) {
$objReferenceHelper = ReferenceHelper::getInstance();
@ -2488,12 +2527,12 @@ class Worksheet implements IComparable
/**
* Insert a new column, updating all possible related data.
*
* @param int $beforeColumnIndex Insert before this one (numeric column coordinate of the cell)
* @param int $numberOfColumns Number of columns to insert
* @param int $beforeColumnIndex Insert before this column ID (numeric column coordinate of the cell)
* @param int $numberOfColumns Number of new columns to insert
*
* @return $this
*/
public function insertNewColumnBeforeByIndex($beforeColumnIndex, $numberOfColumns = 1)
public function insertNewColumnBeforeByIndex(int $beforeColumnIndex, int $numberOfColumns = 1)
{
if ($beforeColumnIndex >= 1) {
return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
@ -2505,12 +2544,12 @@ class Worksheet implements IComparable
/**
* Delete a row, updating all possible related data.
*
* @param int $row Remove starting with this one
* @param int $row Remove rows, starting with this row number
* @param int $numberOfRows Number of rows to remove
*
* @return $this
*/
public function removeRow($row, $numberOfRows = 1)
public function removeRow(int $row, int $numberOfRows = 1)
{
if ($row < 1) {
throw new Exception('Rows to be deleted should at least start from row 1.');
@ -2561,12 +2600,12 @@ class Worksheet implements IComparable
/**
* Remove a column, updating all possible related data.
*
* @param string $column Remove starting with this one, eg: 'A'
* @param string $column Remove columns starting with this column name, eg: 'A'
* @param int $numberOfColumns Number of columns to remove
*
* @return $this
*/
public function removeColumn($column, $numberOfColumns = 1)
public function removeColumn(string $column, int $numberOfColumns = 1)
{
if (is_numeric($column)) {
throw new Exception('Column references should not be numeric.');
@ -2623,12 +2662,12 @@ class Worksheet implements IComparable
/**
* Remove a column, updating all possible related data.
*
* @param int $columnIndex Remove starting with this one (numeric column coordinate of the cell)
* @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
* @param int $numColumns Number of columns to remove
*
* @return $this
*/
public function removeColumnByIndex($columnIndex, $numColumns = 1)
public function removeColumnByIndex(int $columnIndex, int $numColumns = 1)
{
if ($columnIndex >= 1) {
return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
@ -2988,21 +3027,58 @@ class Worksheet implements IComparable
return $this;
}
/**
* @param mixed $nullValue
*
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Calculation\Exception
*
* @return mixed
*/
protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, $nullValue)
{
$returnValue = $nullValue;
if ($cell->getValue() !== null) {
if ($cell->getValue() instanceof RichText) {
$returnValue = $cell->getValue()->getPlainText();
} else {
$returnValue = ($calculateFormulas) ? $cell->getCalculatedValue() : $cell->getValue();
}
if ($formatData) {
$style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
$returnValue = NumberFormat::toFormattedString(
$returnValue,
$style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL
);
}
}
return $returnValue;
}
/**
* Create array from a range of cells.
*
* @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
* @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
* @param bool $calculateFormulas Should formulas be calculated?
* @param bool $formatData Should formatting be applied to cell values?
* @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
* True - Return rows and columns indexed by their actual row and column IDs
*
* @return array
* @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
* True - Don't return values for rows/columns that are defined as hidden.
*/
public function rangeToArray($range, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
{
// Returnvalue
public function rangeToArray(
string $range,
$nullValue = null,
bool $calculateFormulas = true,
bool $formatData = true,
bool $returnCellRef = false,
bool $ignoreHidden = false
): array {
$range = Validations::validateCellOrCellRange($range);
$returnValue = [];
// Identify the range that we need to extract from the worksheet
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
@ -3015,42 +3091,23 @@ class Worksheet implements IComparable
// Loop through rows
$r = -1;
for ($row = $minRow; $row <= $maxRow; ++$row) {
$rRef = $returnCellRef ? $row : ++$r;
if (($ignoreHidden === true) && ($this->getRowDimension($row)->getVisible() === false)) {
continue;
}
$rowRef = $returnCellRef ? $row : ++$r;
$c = -1;
// Loop through columns in the current row
for ($col = $minCol; $col != $maxCol; ++$col) {
$cRef = $returnCellRef ? $col : ++$c;
for ($col = $minCol; $col !== $maxCol; ++$col) {
if (($ignoreHidden === true) && ($this->getColumnDimension($col)->getVisible() === false)) {
continue;
}
$columnRef = $returnCellRef ? $col : ++$c;
// Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen
// so we test and retrieve directly against cellCollection
$cell = $this->cellCollection->get($col . $row);
//if ($this->cellCollection->has($col . $row)) {
$cell = $this->cellCollection->get("{$col}{$row}");
$returnValue[$rowRef][$columnRef] = $nullValue;
if ($cell !== null) {
// Cell exists
if ($cell->getValue() !== null) {
if ($cell->getValue() instanceof RichText) {
$returnValue[$rRef][$cRef] = $cell->getValue()->getPlainText();
} else {
if ($calculateFormulas) {
$returnValue[$rRef][$cRef] = $cell->getCalculatedValue();
} else {
$returnValue[$rRef][$cRef] = $cell->getValue();
}
}
if ($formatData) {
$style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
$returnValue[$rRef][$cRef] = NumberFormat::toFormattedString(
$returnValue[$rRef][$cRef],
$style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL
);
}
} else {
// Cell holds a NULL
$returnValue[$rRef][$cRef] = $nullValue;
}
} else {
// Cell doesn't exist
$returnValue[$rRef][$cRef] = $nullValue;
$returnValue[$rowRef][$columnRef] = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue);
}
}
}
@ -3103,11 +3160,17 @@ class Worksheet implements IComparable
* @param bool $formatData Should formatting be applied to cell values?
* @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
* True - Return rows and columns indexed by their actual row and column IDs
*
* @return array
* @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
* True - Don't return values for rows/columns that are defined as hidden.
*/
public function namedRangeToArray(string $definedName, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
{
public function namedRangeToArray(
string $definedName,
$nullValue = null,
bool $calculateFormulas = true,
bool $formatData = true,
bool $returnCellRef = false,
bool $ignoreHidden = false
): array {
$retVal = [];
$namedRange = $this->validateNamedRange($definedName);
if ($namedRange !== null) {
@ -3115,7 +3178,7 @@ class Worksheet implements IComparable
$cellRange = str_replace('$', '', $cellRange);
$workSheet = $namedRange->getWorksheet();
if ($workSheet !== null) {
$retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
$retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden);
}
}
@ -3130,11 +3193,16 @@ class Worksheet implements IComparable
* @param bool $formatData Should formatting be applied to cell values?
* @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
* True - Return rows and columns indexed by their actual row and column IDs
*
* @return array
* @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
* True - Don't return values for rows/columns that are defined as hidden.
*/
public function toArray($nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
{
public function toArray(
$nullValue = null,
bool $calculateFormulas = true,
bool $formatData = true,
bool $returnCellRef = false,
bool $ignoreHidden = false
): array {
// Garbage collect...
$this->garbageCollect();
@ -3143,7 +3211,7 @@ class Worksheet implements IComparable
$maxRow = $this->getHighestRow();
// Return
return $this->rangeToArray('A1:' . $maxCol . $maxRow, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden);
}
/**

View File

@ -7,9 +7,11 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Chart\Chart;
use PhpOffice\PhpSpreadsheet\Document\Properties;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\RichText\Run;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
@ -342,13 +344,21 @@ class Html extends BaseWriter
private static function generateMeta(?string $val, string $desc): string
{
return $val
return ($val || $val === '0')
? (' <meta name="' . $desc . '" content="' . htmlspecialchars($val, Settings::htmlEntityFlags()) . '" />' . PHP_EOL)
: '';
}
public const BODY_LINE = ' <body>' . PHP_EOL;
private const CUSTOM_TO_META = [
Properties::PROPERTY_TYPE_BOOLEAN => 'bool',
Properties::PROPERTY_TYPE_DATE => 'date',
Properties::PROPERTY_TYPE_FLOAT => 'float',
Properties::PROPERTY_TYPE_INTEGER => 'int',
Properties::PROPERTY_TYPE_STRING => 'string',
];
/**
* Generate HTML header.
*
@ -374,6 +384,36 @@ class Html extends BaseWriter
$html .= self::generateMeta($properties->getCategory(), 'category');
$html .= self::generateMeta($properties->getCompany(), 'company');
$html .= self::generateMeta($properties->getManager(), 'manager');
$html .= self::generateMeta($properties->getLastModifiedBy(), 'lastModifiedBy');
$date = Date::dateTimeFromTimestamp((string) $properties->getCreated());
$date->setTimeZone(Date::getDefaultOrLocalTimeZone());
$html .= self::generateMeta($date->format(DATE_W3C), 'created');
$date = Date::dateTimeFromTimestamp((string) $properties->getModified());
$date->setTimeZone(Date::getDefaultOrLocalTimeZone());
$html .= self::generateMeta($date->format(DATE_W3C), 'modified');
$customProperties = $properties->getCustomProperties();
foreach ($customProperties as $customProperty) {
$propertyValue = $properties->getCustomPropertyValue($customProperty);
$propertyType = $properties->getCustomPropertyType($customProperty);
$propertyQualifier = self::CUSTOM_TO_META[$propertyType] ?? null;
if ($propertyQualifier !== null) {
if ($propertyType === Properties::PROPERTY_TYPE_BOOLEAN) {
$propertyValue = $propertyValue ? '1' : '0';
} elseif ($propertyType === Properties::PROPERTY_TYPE_DATE) {
$date = Date::dateTimeFromTimestamp((string) $propertyValue);
$date->setTimeZone(Date::getDefaultOrLocalTimeZone());
$propertyValue = $date->format(DATE_W3C);
} else {
$propertyValue = (string) $propertyValue;
}
$html .= self::generateMeta($propertyValue, "custom.$propertyQualifier.$customProperty");
}
}
if (!empty($properties->getHyperlinkBase())) {
$html .= ' <base href="' . $properties->getHyperlinkBase() . '" />' . PHP_EOL;
}
$html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true);
@ -693,7 +733,8 @@ class Html extends BaseWriter
// max-width: 100% ensures that image doesnt overflow containing cell
// width: X sets width of supplied image.
// As a result, images bigger than cell will be contained and images smaller will not get stretched
$html .= '<img alt="' . $filedesc . '" src="' . $dataUri . '" style="max-width:100%;width:' . $drawing->getWidth() . 'px;" />';
$html .= '<img alt="' . $filedesc . '" src="' . $dataUri . '" style="max-width:100%;width:' . $drawing->getWidth() . 'px;left: ' .
$drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px;position: absolute; z-index: 1;" />';
}
}
}

View File

@ -12,7 +12,6 @@ use PhpOffice\PhpSpreadsheet\Writer\Ods\Settings;
use PhpOffice\PhpSpreadsheet\Writer\Ods\Styles;
use PhpOffice\PhpSpreadsheet\Writer\Ods\Thumbnails;
use ZipStream\Exception\OverflowException;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
class Ods extends BaseWriter
@ -158,11 +157,7 @@ class Ods extends BaseWriter
}
// Create new ZIP stream
$options = new Archive();
$options->setEnableZip64(false);
$options->setOutputStream($this->fileHandle);
return new ZipStream(null, $options);
return ZipStream0::newZipStream($this->fileHandle);
}
/**

View File

@ -126,7 +126,16 @@ class Content extends WriterPart
$objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle());
$objWriter->writeAttribute('table:style-name', Style::TABLE_STYLE_PREFIX . (string) ($sheetIndex + 1));
$objWriter->writeElement('office:forms');
$lastColumn = 0;
foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) {
$thisColumn = $columnDimension->getColumnNumeric();
$emptyColumns = $thisColumn - $lastColumn - 1;
if ($emptyColumns > 0) {
$objWriter->startElement('table:table-column');
$objWriter->writeAttribute('table:number-columns-repeated', (string) $emptyColumns);
$objWriter->endElement();
}
$lastColumn = $thisColumn;
$objWriter->startElement('table:table-column');
$objWriter->writeAttribute(
'table:style-name',

View File

@ -31,7 +31,6 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Workbook;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet;
use ZipArchive;
use ZipStream\Exception\OverflowException;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
class Xlsx extends BaseWriter
@ -377,7 +376,7 @@ class Xlsx extends BaseWriter
}
// Add theme to ZIP file
$zipContent['xl/theme/theme1.xml'] = $this->getWriterPartTheme()->writeTheme();
$zipContent['xl/theme/theme1.xml'] = $this->getWriterPartTheme()->writeTheme($this->spreadSheet);
// Add string table to ZIP file
$zipContent['xl/sharedStrings.xml'] = $this->getWriterPartStringTable()->writeStringTable($this->stringTable);
@ -546,11 +545,7 @@ class Xlsx extends BaseWriter
$this->openFileHandle($filename);
$options = new Archive();
$options->setEnableZip64(false);
$options->setOutputStream($this->fileHandle);
$this->zip = new ZipStream(null, $options);
$this->zip = ZipStream0::newZipStream($this->fileHandle);
$this->addZipFiles($zipContent);

View File

@ -14,6 +14,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Title;
use PhpOffice\PhpSpreadsheet\Chart\TrendLine;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
class Chart extends WriterPart
@ -109,12 +110,20 @@ class Chart extends WriterPart
$objWriter->endElement();
$objWriter->endElement(); // c:chart
if ($chart->getNoFill()) {
$objWriter->startElement('c:spPr');
if ($chart->getNoFill()) {
$objWriter->startElement('a:noFill');
$objWriter->endElement(); // a:noFill
$objWriter->endElement(); // c:spPr
}
$fillColor = $chart->getFillColor();
if ($fillColor->isUsable()) {
$this->writeColor($objWriter, $fillColor);
}
$borderLines = $chart->getBorderLines();
$this->writeLineStyles($objWriter, $borderLines);
$this->writeEffects($objWriter, $borderLines);
$objWriter->endElement(); // c:spPr
$this->writePrintSettings($objWriter);
@ -201,6 +210,17 @@ class Chart extends WriterPart
$objWriter->writeAttribute('val', ($legend->getOverlay()) ? '1' : '0');
$objWriter->endElement();
$objWriter->startElement('c:spPr');
$fillColor = $legend->getFillColor();
if ($fillColor->isUsable()) {
$this->writeColor($objWriter, $fillColor);
}
$borderLines = $legend->getBorderLines();
$this->writeLineStyles($objWriter, $borderLines);
$this->writeEffects($objWriter, $borderLines);
$objWriter->endElement(); // c:spPr
$legendText = $legend->getLegendText();
$objWriter->startElement('c:txPr');
$objWriter->startElement('a:bodyPr');
$objWriter->endElement();
@ -213,17 +233,21 @@ class Chart extends WriterPart
$objWriter->writeAttribute('rtl', '0');
$objWriter->startElement('a:defRPr');
$objWriter->endElement();
$objWriter->endElement();
if ($legendText !== null) {
$this->writeColor($objWriter, $legendText->getFillColorObject());
$this->writeEffects($objWriter, $legendText);
}
$objWriter->endElement(); // a:defRpr
$objWriter->endElement(); // a:pPr
$objWriter->startElement('a:endParaRPr');
$objWriter->writeAttribute('lang', 'en-US');
$objWriter->endElement();
$objWriter->endElement(); // a:endParaRPr
$objWriter->endElement();
$objWriter->endElement();
$objWriter->endElement(); // a:p
$objWriter->endElement(); // c:txPr
$objWriter->endElement();
$objWriter->endElement(); // c:legend
}
/**
@ -307,19 +331,26 @@ class Chart extends WriterPart
$objWriter->startElement('c:hiLowLines');
$objWriter->endElement();
$gapWidth = $plotArea->getGapWidth();
$upBars = $plotArea->getUseUpBars();
$downBars = $plotArea->getUseDownBars();
if ($gapWidth !== null || $upBars || $downBars) {
$objWriter->startElement('c:upDownBars');
if ($gapWidth !== null) {
$objWriter->startElement('c:gapWidth');
$objWriter->writeAttribute('val', '300');
$objWriter->writeAttribute('val', "$gapWidth");
$objWriter->endElement();
}
if ($upBars) {
$objWriter->startElement('c:upBars');
$objWriter->endElement();
}
if ($downBars) {
$objWriter->startElement('c:downBars');
$objWriter->endElement();
$objWriter->endElement();
}
$objWriter->endElement(); // c:upDownBars
}
}
// Generate 3 unique numbers to use for axId values
@ -428,8 +459,8 @@ class Chart extends WriterPart
}
$objWriter->endElement(); // c:spPr
}
$fontColor = $chartLayout->getLabelFontColor();
if ($fontColor && $fontColor->isUsable()) {
$labelFont = $chartLayout->getLabelFont();
if ($labelFont !== null) {
$objWriter->startElement('c:txPr');
$objWriter->startElement('a:bodyPr');
@ -445,14 +476,7 @@ class Chart extends WriterPart
$objWriter->startElement('a:lstStyle');
$objWriter->endElement(); // a:lstStyle
$objWriter->startElement('a:p');
$objWriter->startElement('a:pPr');
$objWriter->startElement('a:defRPr');
$this->writeColor($objWriter, $fontColor);
$objWriter->endElement(); // a:defRPr
$objWriter->endElement(); // a:pPr
$objWriter->endElement(); // a:p
$this->writeLabelFont($objWriter, $labelFont, $chartLayout->getLabelEffects());
$objWriter->endElement(); // c:txPr
}
@ -608,25 +632,24 @@ class Chart extends WriterPart
}
$textRotation = $yAxis->getAxisOptionsProperty('textRotation');
if (is_numeric($textRotation)) {
$axisText = $yAxis->getAxisText();
if ($axisText !== null || is_numeric($textRotation)) {
$objWriter->startElement('c:txPr');
$objWriter->startElement('a:bodyPr');
if (is_numeric($textRotation)) {
$objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation));
}
$objWriter->endElement(); // a:bodyPr
$objWriter->startElement('a:lstStyle');
$objWriter->endElement(); // a:lstStyle
$objWriter->startElement('a:p');
$objWriter->startElement('a:pPr');
$objWriter->startElement('a:defRPr');
$objWriter->endElement(); // a:defRPr
$objWriter->endElement(); // a:pPr
$objWriter->endElement(); // a:p
$this->writeLabelFont($objWriter, ($axisText === null) ? null : $axisText->getFont(), $axisText);
$objWriter->endElement(); // c:txPr
}
$objWriter->startElement('c:spPr');
$this->writeColor($objWriter, $yAxis->getFillColorObject());
$this->writeLineStyles($objWriter, $yAxis);
$this->writeLineStyles($objWriter, $yAxis, $yAxis->getNoFill());
$this->writeEffects($objWriter, $yAxis);
$objWriter->endElement(); // spPr
@ -826,25 +849,26 @@ class Chart extends WriterPart
}
$textRotation = $xAxis->getAxisOptionsProperty('textRotation');
if (is_numeric($textRotation)) {
$axisText = $xAxis->getAxisText();
if ($axisText !== null || is_numeric($textRotation)) {
$objWriter->startElement('c:txPr');
$objWriter->startElement('a:bodyPr');
if (is_numeric($textRotation)) {
$objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation));
}
$objWriter->endElement(); // a:bodyPr
$objWriter->startElement('a:lstStyle');
$objWriter->endElement(); // a:lstStyle
$objWriter->startElement('a:p');
$objWriter->startElement('a:pPr');
$objWriter->startElement('a:defRPr');
$objWriter->endElement(); // a:defRPr
$objWriter->endElement(); // a:pPr
$objWriter->endElement(); // a:p
$this->writeLabelFont($objWriter, ($axisText === null) ? null : $axisText->getFont(), $axisText);
$objWriter->endElement(); // c:txPr
}
$objWriter->startElement('c:spPr');
$this->writeColor($objWriter, $xAxis->getFillColorObject());
$this->writeLineStyles($objWriter, $xAxis);
$this->writeLineStyles($objWriter, $xAxis, $xAxis->getNoFill());
$this->writeEffects($objWriter, $xAxis);
$objWriter->endElement(); //end spPr
@ -1055,14 +1079,6 @@ class Chart extends WriterPart
$labelFill = $plotLabel->getFillColorObject();
$labelFill = ($labelFill instanceof ChartColor) ? $labelFill : null;
}
if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) {
$fillColor = $plotLabel->getFillColorObject();
if ($fillColor !== null && !is_array($fillColor) && $fillColor->isUsable()) {
$objWriter->startElement('c:spPr');
$this->writeColor($objWriter, $fillColor);
$objWriter->endElement(); // c:spPr
}
}
// Values
$plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesIdx);
@ -1094,6 +1110,12 @@ class Chart extends WriterPart
$plotSeriesValues !== false
) {
$objWriter->startElement('c:spPr');
if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) {
$fillColor = $plotLabel->getFillColorObject();
if ($fillColor !== null && !is_array($fillColor) && $fillColor->isUsable()) {
$this->writeColor($objWriter, $fillColor);
}
}
$fillObject = $labelFill ?? $plotSeriesValues->getFillColorObject();
$callLineStyles = true;
if ($fillObject instanceof ChartColor && $fillObject->isUsable()) {
@ -1398,7 +1420,7 @@ class Chart extends WriterPart
$count = $plotSeriesValues->getPointCount();
$source = $plotSeriesValues->getDataSource();
$values = $plotSeriesValues->getDataValues();
if ($count > 1 || ($count === 1 && "=$source" !== (string) $values[0])) {
if ($count > 1 || ($count === 1 && array_key_exists(0, $values) && "=$source" !== (string) $values[0])) {
$objWriter->startElement('c:' . $dataType . 'Cache');
if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) {
@ -1770,4 +1792,51 @@ class Chart extends WriterPart
}
}
}
private function writeLabelFont(XMLWriter $objWriter, ?Font $labelFont, ?Properties $axisText): void
{
$objWriter->startElement('a:p');
$objWriter->startElement('a:pPr');
$objWriter->startElement('a:defRPr');
if ($labelFont !== null) {
$fontSize = $labelFont->getSize();
if (is_numeric($fontSize)) {
$fontSize *= (($fontSize < 100) ? 100 : 1);
$objWriter->writeAttribute('sz', (string) $fontSize);
}
if ($labelFont->getBold() === true) {
$objWriter->writeAttribute('b', '1');
}
if ($labelFont->getItalic() === true) {
$objWriter->writeAttribute('i', '1');
}
$fontColor = $labelFont->getChartColor();
if ($fontColor !== null) {
$this->writeColor($objWriter, $fontColor);
}
}
if ($axisText !== null) {
$this->writeEffects($objWriter, $axisText);
}
if ($labelFont !== null) {
if (!empty($labelFont->getLatin())) {
$objWriter->startElement('a:latin');
$objWriter->writeAttribute('typeface', $labelFont->getLatin());
$objWriter->endElement();
}
if (!empty($labelFont->getEastAsian())) {
$objWriter->startElement('a:eastAsian');
$objWriter->writeAttribute('typeface', $labelFont->getEastAsian());
$objWriter->endElement();
}
if (!empty($labelFont->getComplexScript())) {
$objWriter->startElement('a:complexScript');
$objWriter->writeAttribute('typeface', $labelFont->getComplexScript());
$objWriter->endElement();
}
}
$objWriter->endElement(); // a:defRPr
$objWriter->endElement(); // a:pPr
$objWriter->endElement(); // a:p
}
}

View File

@ -93,6 +93,9 @@ class DocProps extends WriterPart
// SharedDoc
$objWriter->writeElement('SharedDoc', 'false');
// HyperlinkBase
$objWriter->writeElement('HyperlinkBase', $spreadsheet->getProperties()->getHyperlinkBase());
// HyperlinksChanged
$objWriter->writeElement('HyperlinksChanged', 'false');

View File

@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
class FunctionPrefix
{
const XLFNREGEXP = '/(?:_xlfn\.)?((?:_xlws\.)?('
const XLFNREGEXP = '/(?:_xlfn\.)?((?:_xlws\.)?\b('
// functions added with Excel 2010
. 'beta[.]dist'
. '|beta[.]inv'

Some files were not shown because too many files have changed in this diff Show More