Merge branch 'MDL-83010_exception-when-exporting-SCORM-reports-to-Excel-format' of https://github.com/ziegenberg/moodle
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
|
||||
|
||||
class CodePage
|
||||
{
|
||||
public const DEFAULT_CODE_PAGE = 'CP1252';
|
||||
|
||||
/** @var array */
|
||||
private static $pageArray = [
|
||||
0 => 'CP1252', // CodePage is not always correctly set when the xls file was saved by Apple's Numbers program
|
||||
367 => 'ASCII', // ASCII
|
||||
437 => 'CP437', // OEM US
|
||||
//720 => 'notsupported', // OEM Arabic
|
||||
737 => 'CP737', // OEM Greek
|
||||
775 => 'CP775', // OEM Baltic
|
||||
850 => 'CP850', // OEM Latin I
|
||||
852 => 'CP852', // OEM Latin II (Central European)
|
||||
855 => 'CP855', // OEM Cyrillic
|
||||
857 => 'CP857', // OEM Turkish
|
||||
858 => 'CP858', // OEM Multilingual Latin I with Euro
|
||||
860 => 'CP860', // OEM Portugese
|
||||
861 => 'CP861', // OEM Icelandic
|
||||
862 => 'CP862', // OEM Hebrew
|
||||
863 => 'CP863', // OEM Canadian (French)
|
||||
864 => 'CP864', // OEM Arabic
|
||||
865 => 'CP865', // OEM Nordic
|
||||
866 => 'CP866', // OEM Cyrillic (Russian)
|
||||
869 => 'CP869', // OEM Greek (Modern)
|
||||
874 => 'CP874', // ANSI Thai
|
||||
932 => 'CP932', // ANSI Japanese Shift-JIS
|
||||
936 => 'CP936', // ANSI Chinese Simplified GBK
|
||||
949 => 'CP949', // ANSI Korean (Wansung)
|
||||
950 => 'CP950', // ANSI Chinese Traditional BIG5
|
||||
1200 => 'UTF-16LE', // UTF-16 (BIFF8)
|
||||
1250 => 'CP1250', // ANSI Latin II (Central European)
|
||||
1251 => 'CP1251', // ANSI Cyrillic
|
||||
1252 => 'CP1252', // ANSI Latin I (BIFF4-BIFF7)
|
||||
1253 => 'CP1253', // ANSI Greek
|
||||
1254 => 'CP1254', // ANSI Turkish
|
||||
1255 => 'CP1255', // ANSI Hebrew
|
||||
1256 => 'CP1256', // ANSI Arabic
|
||||
1257 => 'CP1257', // ANSI Baltic
|
||||
1258 => 'CP1258', // ANSI Vietnamese
|
||||
1361 => 'CP1361', // ANSI Korean (Johab)
|
||||
10000 => 'MAC', // Apple Roman
|
||||
10001 => 'CP932', // Macintosh Japanese
|
||||
10002 => 'CP950', // Macintosh Chinese Traditional
|
||||
10003 => 'CP1361', // Macintosh Korean
|
||||
10004 => 'MACARABIC', // Apple Arabic
|
||||
10005 => 'MACHEBREW', // Apple Hebrew
|
||||
10006 => 'MACGREEK', // Macintosh Greek
|
||||
10007 => 'MACCYRILLIC', // Macintosh Cyrillic
|
||||
10008 => 'CP936', // Macintosh - Simplified Chinese (GB 2312)
|
||||
10010 => 'MACROMANIA', // Macintosh Romania
|
||||
10017 => 'MACUKRAINE', // Macintosh Ukraine
|
||||
10021 => 'MACTHAI', // Macintosh Thai
|
||||
10029 => ['MACCENTRALEUROPE', 'MAC-CENTRALEUROPE'], // Macintosh Central Europe
|
||||
10079 => 'MACICELAND', // Macintosh Icelandic
|
||||
10081 => 'MACTURKISH', // Macintosh Turkish
|
||||
10082 => 'MACCROATIAN', // Macintosh Croatian
|
||||
21010 => 'UTF-16LE', // UTF-16 (BIFF8) This isn't correct, but some Excel writer libraries erroneously use Codepage 21010 for UTF-16LE
|
||||
32768 => 'MAC', // Apple Roman
|
||||
//32769 => 'unsupported', // ANSI Latin I (BIFF2-BIFF3)
|
||||
65000 => 'UTF-7', // Unicode (UTF-7)
|
||||
65001 => 'UTF-8', // Unicode (UTF-8)
|
||||
99999 => ['unsupported'], // Unicode (UTF-8)
|
||||
];
|
||||
|
||||
public static function validate(string $codePage): bool
|
||||
{
|
||||
return in_array($codePage, self::$pageArray, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Microsoft Code Page Identifier to Code Page Name which iconv
|
||||
* and mbstring understands.
|
||||
*
|
||||
* @param int $codePage Microsoft Code Page Indentifier
|
||||
*
|
||||
* @return string Code Page Name
|
||||
*/
|
||||
public static function numberToName(int $codePage): string
|
||||
{
|
||||
if (array_key_exists($codePage, self::$pageArray)) {
|
||||
$value = self::$pageArray[$codePage];
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $encoding) {
|
||||
if (@iconv('UTF-8', $encoding, ' ') !== false) {
|
||||
self::$pageArray[$codePage] = $encoding;
|
||||
|
||||
return $encoding;
|
||||
}
|
||||
}
|
||||
|
||||
throw new PhpSpreadsheetException("Code page $codePage not implemented on this system.");
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
if ($codePage == 720 || $codePage == 32769) {
|
||||
throw new PhpSpreadsheetException("Code page $codePage not supported."); // OEM Arabic
|
||||
}
|
||||
|
||||
throw new PhpSpreadsheetException('Unknown codepage: ' . $codePage);
|
||||
}
|
||||
|
||||
public static function getEncodings(): array
|
||||
{
|
||||
return self::$pageArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
|
||||
class Date
|
||||
{
|
||||
/** constants */
|
||||
const CALENDAR_WINDOWS_1900 = 1900; // Base date of 1st Jan 1900 = 1.0
|
||||
const CALENDAR_MAC_1904 = 1904; // Base date of 2nd Jan 1904 = 1.0
|
||||
|
||||
/**
|
||||
* Names of the months of the year, indexed by shortname
|
||||
* Planned usage for locale settings.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public static $monthNames = [
|
||||
'Jan' => 'January',
|
||||
'Feb' => 'February',
|
||||
'Mar' => 'March',
|
||||
'Apr' => 'April',
|
||||
'May' => 'May',
|
||||
'Jun' => 'June',
|
||||
'Jul' => 'July',
|
||||
'Aug' => 'August',
|
||||
'Sep' => 'September',
|
||||
'Oct' => 'October',
|
||||
'Nov' => 'November',
|
||||
'Dec' => 'December',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public static $numberSuffixes = [
|
||||
'st',
|
||||
'nd',
|
||||
'rd',
|
||||
'th',
|
||||
];
|
||||
|
||||
/**
|
||||
* Base calendar year to use for calculations
|
||||
* Value is either CALENDAR_WINDOWS_1900 (1900) or CALENDAR_MAC_1904 (1904).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected static $excelCalendar = self::CALENDAR_WINDOWS_1900;
|
||||
|
||||
/**
|
||||
* Default timezone to use for DateTime objects.
|
||||
*
|
||||
* @var null|DateTimeZone
|
||||
*/
|
||||
protected static $defaultTimeZone;
|
||||
|
||||
/**
|
||||
* Set the Excel calendar (Windows 1900 or Mac 1904).
|
||||
*
|
||||
* @param int $baseYear Excel base date (1900 or 1904)
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public static function setExcelCalendar($baseYear)
|
||||
{
|
||||
if (
|
||||
($baseYear == self::CALENDAR_WINDOWS_1900) ||
|
||||
($baseYear == self::CALENDAR_MAC_1904)
|
||||
) {
|
||||
self::$excelCalendar = $baseYear;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Excel calendar (Windows 1900 or Mac 1904).
|
||||
*
|
||||
* @return int Excel base date (1900 or 1904)
|
||||
*/
|
||||
public static function getExcelCalendar()
|
||||
{
|
||||
return self::$excelCalendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Default timezone to use for dates.
|
||||
*
|
||||
* @param null|DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public static function setDefaultTimezone($timeZone)
|
||||
{
|
||||
try {
|
||||
$timeZone = self::validateTimeZone($timeZone);
|
||||
self::$defaultTimeZone = $timeZone;
|
||||
$retval = true;
|
||||
} catch (PhpSpreadsheetException $e) {
|
||||
$retval = false;
|
||||
}
|
||||
|
||||
return $retval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Default timezone, or UTC if default not set.
|
||||
*/
|
||||
public static function getDefaultTimezone(): DateTimeZone
|
||||
{
|
||||
return self::$defaultTimeZone ?? new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Default timezone, or local timezone if default is not set.
|
||||
*/
|
||||
public static function getDefaultOrLocalTimezone(): DateTimeZone
|
||||
{
|
||||
return self::$defaultTimeZone ?? new DateTimeZone(date_default_timezone_get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Default timezone even if null.
|
||||
*/
|
||||
public static function getDefaultTimezoneOrNull(): ?DateTimeZone
|
||||
{
|
||||
return self::$defaultTimeZone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a timezone.
|
||||
*
|
||||
* @param null|DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object
|
||||
*
|
||||
* @return ?DateTimeZone The timezone as a timezone object
|
||||
*/
|
||||
private static function validateTimeZone($timeZone)
|
||||
{
|
||||
if ($timeZone instanceof DateTimeZone || $timeZone === null) {
|
||||
return $timeZone;
|
||||
}
|
||||
if (in_array($timeZone, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC))) {
|
||||
return new DateTimeZone($timeZone);
|
||||
}
|
||||
|
||||
throw new PhpSpreadsheetException('Invalid timezone');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value Converts a date/time in ISO-8601 standard format date string to an Excel
|
||||
* serialized timestamp.
|
||||
* See https://en.wikipedia.org/wiki/ISO_8601 for details of the ISO-8601 standard format.
|
||||
*
|
||||
* @return float|int
|
||||
*/
|
||||
public static function convertIsoDate($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new Exception('Non-string value supplied for Iso Date conversion');
|
||||
}
|
||||
|
||||
$date = new DateTime($value);
|
||||
$dateErrors = DateTime::getLastErrors();
|
||||
|
||||
if (is_array($dateErrors) && ($dateErrors['warning_count'] > 0 || $dateErrors['error_count'] > 0)) {
|
||||
throw new Exception("Invalid string $value supplied for datatype Date");
|
||||
}
|
||||
|
||||
$newValue = SharedDate::PHPToExcel($date);
|
||||
if ($newValue === false) {
|
||||
throw new Exception("Invalid string $value supplied for datatype Date");
|
||||
}
|
||||
|
||||
if (preg_match('/^\\s*\\d?\\d:\\d\\d(:\\d\\d([.]\\d+)?)?\\s*(am|pm)?\\s*$/i', $value) == 1) {
|
||||
$newValue = fmod($newValue, 1.0);
|
||||
}
|
||||
|
||||
return $newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a MS serialized datetime value from Excel to a PHP Date/Time object.
|
||||
*
|
||||
* @param float|int $excelTimestamp MS Excel serialized date/time value
|
||||
* @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
|
||||
* if you don't want to treat it as a UTC value
|
||||
* Use the default (UTC) unless you absolutely need a conversion
|
||||
*
|
||||
* @return DateTime PHP date/time object
|
||||
*/
|
||||
public static function excelToDateTimeObject($excelTimestamp, $timeZone = null)
|
||||
{
|
||||
$timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone);
|
||||
if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) {
|
||||
if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) {
|
||||
// Unix timestamp base date
|
||||
$baseDate = new DateTime('1970-01-01', $timeZone);
|
||||
} else {
|
||||
// MS Excel calendar base dates
|
||||
if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
|
||||
// Allow adjustment for 1900 Leap Year in MS Excel
|
||||
$baseDate = ($excelTimestamp < 60) ? new DateTime('1899-12-31', $timeZone) : new DateTime('1899-12-30', $timeZone);
|
||||
} else {
|
||||
$baseDate = new DateTime('1904-01-01', $timeZone);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$baseDate = new DateTime('1899-12-30', $timeZone);
|
||||
}
|
||||
|
||||
$days = floor($excelTimestamp);
|
||||
$partDay = $excelTimestamp - $days;
|
||||
$hours = floor($partDay * 24);
|
||||
$partDay = $partDay * 24 - $hours;
|
||||
$minutes = floor($partDay * 60);
|
||||
$partDay = $partDay * 60 - $minutes;
|
||||
$seconds = round($partDay * 60);
|
||||
|
||||
if ($days >= 0) {
|
||||
$days = '+' . $days;
|
||||
}
|
||||
$interval = $days . ' days';
|
||||
|
||||
return $baseDate->modify($interval)
|
||||
->setTime((int) $hours, (int) $minutes, (int) $seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a MS serialized datetime value from Excel to a unix timestamp.
|
||||
* The use of Unix timestamps, and therefore this function, is discouraged.
|
||||
* They are not Y2038-safe on a 32-bit system, and have no timezone info.
|
||||
*
|
||||
* @param float|int $excelTimestamp MS Excel serialized date/time value
|
||||
* @param null|DateTimeZone|string $timeZone The timezone to assume for the Excel timestamp,
|
||||
* if you don't want to treat it as a UTC value
|
||||
* Use the default (UTC) unless you absolutely need a conversion
|
||||
*
|
||||
* @return int Unix timetamp for this date/time
|
||||
*/
|
||||
public static function excelToTimestamp($excelTimestamp, $timeZone = null)
|
||||
{
|
||||
return (int) self::excelToDateTimeObject($excelTimestamp, $timeZone)
|
||||
->format('U');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a date from PHP to an MS Excel serialized date/time value.
|
||||
*
|
||||
* @param mixed $dateValue PHP DateTime object or a string - Unix timestamp is also permitted, but discouraged;
|
||||
* not Y2038-safe on a 32-bit system, and no timezone info
|
||||
*
|
||||
* @return false|float Excel date/time value
|
||||
* or boolean FALSE on failure
|
||||
*/
|
||||
public static function PHPToExcel($dateValue)
|
||||
{
|
||||
if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) {
|
||||
return self::dateTimeToExcel($dateValue);
|
||||
} elseif (is_numeric($dateValue)) {
|
||||
return self::timestampToExcel($dateValue);
|
||||
} elseif (is_string($dateValue)) {
|
||||
return self::stringToExcel($dateValue);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a PHP DateTime object to an MS Excel serialized date/time value.
|
||||
*
|
||||
* @param DateTimeInterface $dateValue PHP DateTime object
|
||||
*
|
||||
* @return float MS Excel serialized date/time value
|
||||
*/
|
||||
public static function dateTimeToExcel(DateTimeInterface $dateValue)
|
||||
{
|
||||
return self::formattedPHPToExcel(
|
||||
(int) $dateValue->format('Y'),
|
||||
(int) $dateValue->format('m'),
|
||||
(int) $dateValue->format('d'),
|
||||
(int) $dateValue->format('H'),
|
||||
(int) $dateValue->format('i'),
|
||||
(int) $dateValue->format('s')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Unix timestamp to an MS Excel serialized date/time value.
|
||||
* The use of Unix timestamps, and therefore this function, is discouraged.
|
||||
* They are not Y2038-safe on a 32-bit system, and have no timezone info.
|
||||
*
|
||||
* @param float|int|string $unixTimestamp Unix Timestamp
|
||||
*
|
||||
* @return false|float MS Excel serialized date/time value
|
||||
*/
|
||||
public static function timestampToExcel($unixTimestamp)
|
||||
{
|
||||
if (!is_numeric($unixTimestamp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* formattedPHPToExcel.
|
||||
*
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @param int $day
|
||||
* @param int $hours
|
||||
* @param int $minutes
|
||||
* @param int $seconds
|
||||
*
|
||||
* @return float Excel date/time value
|
||||
*/
|
||||
public static function formattedPHPToExcel($year, $month, $day, $hours = 0, $minutes = 0, $seconds = 0)
|
||||
{
|
||||
if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) {
|
||||
//
|
||||
// Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel
|
||||
// This affects every date following 28th February 1900
|
||||
//
|
||||
$excel1900isLeapYear = true;
|
||||
if (($year == 1900) && ($month <= 2)) {
|
||||
$excel1900isLeapYear = false;
|
||||
}
|
||||
$myexcelBaseDate = 2415020;
|
||||
} else {
|
||||
$myexcelBaseDate = 2416481;
|
||||
$excel1900isLeapYear = false;
|
||||
}
|
||||
|
||||
// Julian base date Adjustment
|
||||
if ($month > 2) {
|
||||
$month -= 3;
|
||||
} else {
|
||||
$month += 9;
|
||||
--$year;
|
||||
}
|
||||
|
||||
// Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0)
|
||||
$century = (int) substr((string) $year, 0, 2);
|
||||
$decade = (int) substr((string) $year, 2, 2);
|
||||
$excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear;
|
||||
|
||||
$excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400;
|
||||
|
||||
return (float) $excelDate + $excelTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a given cell a date/time?
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDateTime(Cell $cell, $value = null, bool $dateWithoutTimeOkay = true)
|
||||
{
|
||||
$result = false;
|
||||
$worksheet = $cell->getWorksheetOrNull();
|
||||
$spreadsheet = ($worksheet === null) ? null : $worksheet->getParent();
|
||||
if ($worksheet !== null && $spreadsheet !== null) {
|
||||
$index = $spreadsheet->getActiveSheetIndex();
|
||||
$selected = $worksheet->getSelectedCells();
|
||||
|
||||
try {
|
||||
$result = is_numeric($value ?? $cell->getCalculatedValue()) &&
|
||||
self::isDateTimeFormat(
|
||||
$worksheet->getStyle(
|
||||
$cell->getCoordinate()
|
||||
)->getNumberFormat(),
|
||||
$dateWithoutTimeOkay
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
// Result is already false, so no need to actually do anything here
|
||||
}
|
||||
$worksheet->setSelectedCells($selected);
|
||||
$spreadsheet->setActiveSheetIndex($index);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a given NumberFormat code a date/time format code?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDateTimeFormat(NumberFormat $excelFormatCode, bool $dateWithoutTimeOkay = true)
|
||||
{
|
||||
return self::isDateTimeFormatCode((string) $excelFormatCode->getFormatCode(), $dateWithoutTimeOkay);
|
||||
}
|
||||
|
||||
private const POSSIBLE_DATETIME_FORMAT_CHARACTERS = 'eymdHs';
|
||||
private const POSSIBLE_TIME_FORMAT_CHARACTERS = 'Hs'; // note - no 'm' due to ambiguity
|
||||
|
||||
/**
|
||||
* Is a given number format code a date/time?
|
||||
*
|
||||
* @param string $excelFormatCode
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDateTimeFormatCode($excelFormatCode, bool $dateWithoutTimeOkay = true)
|
||||
{
|
||||
if (strtolower($excelFormatCode) === strtolower(NumberFormat::FORMAT_GENERAL)) {
|
||||
// "General" contains an epoch letter 'e', so we trap for it explicitly here (case-insensitive check)
|
||||
return false;
|
||||
}
|
||||
if (preg_match('/[0#]E[+-]0/i', $excelFormatCode)) {
|
||||
// Scientific format
|
||||
return false;
|
||||
}
|
||||
|
||||
// Switch on formatcode
|
||||
if (in_array($excelFormatCode, NumberFormat::DATE_TIME_OR_DATETIME_ARRAY, true)) {
|
||||
return $dateWithoutTimeOkay || in_array($excelFormatCode, NumberFormat::TIME_OR_DATETIME_ARRAY);
|
||||
}
|
||||
|
||||
// Typically number, currency or accounting (or occasionally fraction) formats
|
||||
if ((substr($excelFormatCode, 0, 1) == '_') || (substr($excelFormatCode, 0, 2) == '0 ')) {
|
||||
return false;
|
||||
}
|
||||
// Some "special formats" provided in German Excel versions were detected as date time value,
|
||||
// so filter them out here - "\C\H\-00000" (Switzerland) and "\D-00000" (Germany).
|
||||
if (\strpos($excelFormatCode, '-00000') !== false) {
|
||||
return false;
|
||||
}
|
||||
$possibleFormatCharacters = $dateWithoutTimeOkay ? self::POSSIBLE_DATETIME_FORMAT_CHARACTERS : self::POSSIBLE_TIME_FORMAT_CHARACTERS;
|
||||
// Try checking for any of the date formatting characters that don't appear within square braces
|
||||
if (preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $excelFormatCode)) {
|
||||
// We might also have a format mask containing quoted strings...
|
||||
// we don't want to test for any of our characters within the quoted blocks
|
||||
if (strpos($excelFormatCode, '"') !== false) {
|
||||
$segMatcher = false;
|
||||
foreach (explode('"', $excelFormatCode) as $subVal) {
|
||||
// Only test in alternate array entries (the non-quoted blocks)
|
||||
$segMatcher = $segMatcher === false;
|
||||
if (
|
||||
$segMatcher &&
|
||||
(preg_match('/(^|\])[^\[]*[' . $possibleFormatCharacters . ']/i', $subVal))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// No date...
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a date/time string to Excel time.
|
||||
*
|
||||
* @param string $dateValue Examples: '2009-12-31', '2009-12-31 15:59', '2009-12-31 15:59:10'
|
||||
*
|
||||
* @return false|float Excel date/time serial value
|
||||
*/
|
||||
public static function stringToExcel($dateValue)
|
||||
{
|
||||
if (strlen($dateValue) < 2) {
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/^(\d{1,4}[ \.\/\-][A-Z]{3,9}([ \.\/\-]\d{1,4})?|[A-Z]{3,9}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?|\d{1,4}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?)( \d{1,2}:\d{1,2}(:\d{1,2})?)?$/iu', $dateValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dateValueNew = DateTimeExcel\DateValue::fromString($dateValue);
|
||||
|
||||
if (!is_float($dateValueNew)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strpos($dateValue, ':') !== false) {
|
||||
$timeValue = DateTimeExcel\TimeValue::fromString($dateValue);
|
||||
if (!is_float($timeValue)) {
|
||||
return false;
|
||||
}
|
||||
$dateValueNew += $timeValue;
|
||||
}
|
||||
|
||||
return $dateValueNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a month name (either a long or a short name) to a month number.
|
||||
*
|
||||
* @param string $monthName Month name or abbreviation
|
||||
*
|
||||
* @return int|string Month number (1 - 12), or the original string argument if it isn't a valid month name
|
||||
*/
|
||||
public static function monthStringToNumber($monthName)
|
||||
{
|
||||
$monthIndex = 1;
|
||||
foreach (self::$monthNames as $shortMonthName => $longMonthName) {
|
||||
if (($monthName === $longMonthName) || ($monthName === $shortMonthName)) {
|
||||
return $monthIndex;
|
||||
}
|
||||
++$monthIndex;
|
||||
}
|
||||
|
||||
return $monthName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips an ordinal from a numeric value.
|
||||
*
|
||||
* @param string $day Day number with an ordinal
|
||||
*
|
||||
* @return int|string The integer value with any ordinal stripped, or the original string argument if it isn't a valid numeric
|
||||
*/
|
||||
public static function dayStringToNumber($day)
|
||||
{
|
||||
$strippedDayValue = (str_replace(self::$numberSuffixes, '', $day));
|
||||
if (is_numeric($strippedDayValue)) {
|
||||
return (int) $strippedDayValue;
|
||||
}
|
||||
|
||||
return $day;
|
||||
}
|
||||
|
||||
public static function dateTimeFromTimestamp(string $date, ?DateTimeZone $timeZone = null): DateTime
|
||||
{
|
||||
$dtobj = DateTime::createFromFormat('U', $date) ?: new DateTime();
|
||||
$dtobj->setTimeZone($timeZone ?? self::getDefaultOrLocalTimezone());
|
||||
|
||||
return $dtobj;
|
||||
}
|
||||
|
||||
public static function formattedDateTimeFromTimestamp(string $date, string $format, ?DateTimeZone $timeZone = null): string
|
||||
{
|
||||
$dtobj = self::dateTimeFromTimestamp($date, $timeZone);
|
||||
|
||||
return $dtobj->format($format);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use GdImage;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
|
||||
use SimpleXMLElement;
|
||||
|
||||
class Drawing
|
||||
{
|
||||
/**
|
||||
* Convert pixels to EMU.
|
||||
*
|
||||
* @param int $pixelValue Value in pixels
|
||||
*
|
||||
* @return int Value in EMU
|
||||
*/
|
||||
public static function pixelsToEMU($pixelValue)
|
||||
{
|
||||
return $pixelValue * 9525;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert EMU to pixels.
|
||||
*
|
||||
* @param int|SimpleXMLElement $emuValue Value in EMU
|
||||
*
|
||||
* @return int Value in pixels
|
||||
*/
|
||||
public static function EMUToPixels($emuValue)
|
||||
{
|
||||
$emuValue = (int) $emuValue;
|
||||
if ($emuValue != 0) {
|
||||
return (int) round($emuValue / 9525);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pixels to column width. Exact algorithm not known.
|
||||
* By inspection of a real Excel file using Calibri 11, one finds 1000px ~ 142.85546875
|
||||
* This gives a conversion factor of 7. Also, we assume that pixels and font size are proportional.
|
||||
*
|
||||
* @param int $pixelValue Value in pixels
|
||||
*
|
||||
* @return float|int Value in cell dimension
|
||||
*/
|
||||
public static function pixelsToCellDimension($pixelValue, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont)
|
||||
{
|
||||
// Font name and size
|
||||
$name = $defaultFont->getName();
|
||||
$size = $defaultFont->getSize();
|
||||
|
||||
if (isset(Font::$defaultColumnWidths[$name][$size])) {
|
||||
// Exact width can be determined
|
||||
return $pixelValue * Font::$defaultColumnWidths[$name][$size]['width']
|
||||
/ Font::$defaultColumnWidths[$name][$size]['px'];
|
||||
}
|
||||
|
||||
// We don't have data for this particular font and size, use approximation by
|
||||
// extrapolating from Calibri 11
|
||||
return $pixelValue * 11 * Font::$defaultColumnWidths['Calibri'][11]['width']
|
||||
/ Font::$defaultColumnWidths['Calibri'][11]['px'] / $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert column width from (intrinsic) Excel units to pixels.
|
||||
*
|
||||
* @param float $cellWidth Value in cell dimension
|
||||
* @param \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont Default font of the workbook
|
||||
*
|
||||
* @return int Value in pixels
|
||||
*/
|
||||
public static function cellDimensionToPixels($cellWidth, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont)
|
||||
{
|
||||
// Font name and size
|
||||
$name = $defaultFont->getName();
|
||||
$size = $defaultFont->getSize();
|
||||
|
||||
if (isset(Font::$defaultColumnWidths[$name][$size])) {
|
||||
// Exact width can be determined
|
||||
$colWidth = $cellWidth * Font::$defaultColumnWidths[$name][$size]['px']
|
||||
/ Font::$defaultColumnWidths[$name][$size]['width'];
|
||||
} else {
|
||||
// We don't have data for this particular font and size, use approximation by
|
||||
// extrapolating from Calibri 11
|
||||
$colWidth = $cellWidth * $size * Font::$defaultColumnWidths['Calibri'][11]['px']
|
||||
/ Font::$defaultColumnWidths['Calibri'][11]['width'] / 11;
|
||||
}
|
||||
|
||||
// Round pixels to closest integer
|
||||
$colWidth = (int) round($colWidth);
|
||||
|
||||
return $colWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pixels to points.
|
||||
*
|
||||
* @param int $pixelValue Value in pixels
|
||||
*
|
||||
* @return float Value in points
|
||||
*/
|
||||
public static function pixelsToPoints($pixelValue)
|
||||
{
|
||||
return $pixelValue * 0.75;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert points to pixels.
|
||||
*
|
||||
* @param int $pointValue Value in points
|
||||
*
|
||||
* @return int Value in pixels
|
||||
*/
|
||||
public static function pointsToPixels($pointValue)
|
||||
{
|
||||
if ($pointValue != 0) {
|
||||
return (int) ceil($pointValue / 0.75);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert degrees to angle.
|
||||
*
|
||||
* @param int $degrees Degrees
|
||||
*
|
||||
* @return int Angle
|
||||
*/
|
||||
public static function degreesToAngle($degrees)
|
||||
{
|
||||
return (int) round($degrees * 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert angle to degrees.
|
||||
*
|
||||
* @param int|SimpleXMLElement $angle Angle
|
||||
*
|
||||
* @return int Degrees
|
||||
*/
|
||||
public static function angleToDegrees($angle)
|
||||
{
|
||||
$angle = (int) $angle;
|
||||
if ($angle != 0) {
|
||||
return (int) round($angle / 60000);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new image from file. By alexander at alexauto dot nl.
|
||||
*
|
||||
* @see http://www.php.net/manual/en/function.imagecreatefromwbmp.php#86214
|
||||
*
|
||||
* @param string $bmpFilename Path to Windows DIB (BMP) image
|
||||
*
|
||||
* @return GdImage|resource
|
||||
*
|
||||
* @deprecated 1.26 use Php function imagecreatefrombmp instead
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function imagecreatefrombmp($bmpFilename)
|
||||
{
|
||||
$retVal = @imagecreatefrombmp($bmpFilename);
|
||||
if ($retVal === false) {
|
||||
throw new ReaderException("Unable to create image from $bmpFilename");
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
class Escher
|
||||
{
|
||||
/**
|
||||
* Drawing Group Container.
|
||||
*
|
||||
* @var ?Escher\DggContainer
|
||||
*/
|
||||
private $dggContainer;
|
||||
|
||||
/**
|
||||
* Drawing Container.
|
||||
*
|
||||
* @var ?Escher\DgContainer
|
||||
*/
|
||||
private $dgContainer;
|
||||
|
||||
/**
|
||||
* Get Drawing Group Container.
|
||||
*
|
||||
* @return ?Escher\DggContainer
|
||||
*/
|
||||
public function getDggContainer()
|
||||
{
|
||||
return $this->dggContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Drawing Group Container.
|
||||
*
|
||||
* @param Escher\DggContainer $dggContainer
|
||||
*
|
||||
* @return Escher\DggContainer
|
||||
*/
|
||||
public function setDggContainer($dggContainer)
|
||||
{
|
||||
return $this->dggContainer = $dggContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Drawing Container.
|
||||
*
|
||||
* @return ?Escher\DgContainer
|
||||
*/
|
||||
public function getDgContainer()
|
||||
{
|
||||
return $this->dgContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Drawing Container.
|
||||
*
|
||||
* @param Escher\DgContainer $dgContainer
|
||||
*
|
||||
* @return Escher\DgContainer
|
||||
*/
|
||||
public function setDgContainer($dgContainer)
|
||||
{
|
||||
return $this->dgContainer = $dgContainer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Escher;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
|
||||
|
||||
class DgContainer
|
||||
{
|
||||
/**
|
||||
* Drawing index, 1-based.
|
||||
*
|
||||
* @var ?int
|
||||
*/
|
||||
private $dgId;
|
||||
|
||||
/**
|
||||
* Last shape index in this drawing.
|
||||
*
|
||||
* @var ?int
|
||||
*/
|
||||
private $lastSpId;
|
||||
|
||||
/** @var ?DgContainer\SpgrContainer */
|
||||
private $spgrContainer;
|
||||
|
||||
public function getDgId(): ?int
|
||||
{
|
||||
return $this->dgId;
|
||||
}
|
||||
|
||||
public function setDgId(int $value): void
|
||||
{
|
||||
$this->dgId = $value;
|
||||
}
|
||||
|
||||
public function getLastSpId(): ?int
|
||||
{
|
||||
return $this->lastSpId;
|
||||
}
|
||||
|
||||
public function setLastSpId(int $value): void
|
||||
{
|
||||
$this->lastSpId = $value;
|
||||
}
|
||||
|
||||
public function getSpgrContainer(): ?DgContainer\SpgrContainer
|
||||
{
|
||||
return $this->spgrContainer;
|
||||
}
|
||||
|
||||
public function getSpgrContainerOrThrow(): DgContainer\SpgrContainer
|
||||
{
|
||||
if ($this->spgrContainer !== null) {
|
||||
return $this->spgrContainer;
|
||||
}
|
||||
|
||||
throw new SpreadsheetException('spgrContainer is unexpectedly null');
|
||||
}
|
||||
|
||||
/** @param DgContainer\SpgrContainer $spgrContainer */
|
||||
public function setSpgrContainer($spgrContainer): DgContainer\SpgrContainer
|
||||
{
|
||||
return $this->spgrContainer = $spgrContainer;
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer;
|
||||
|
||||
class SpgrContainer
|
||||
{
|
||||
/**
|
||||
* Parent Shape Group Container.
|
||||
*
|
||||
* @var null|SpgrContainer
|
||||
*/
|
||||
private $parent;
|
||||
|
||||
/**
|
||||
* Shape Container collection.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $children = [];
|
||||
|
||||
/**
|
||||
* Set parent Shape Group Container.
|
||||
*/
|
||||
public function setParent(?self $parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent Shape Group Container if any.
|
||||
*/
|
||||
public function getParent(): ?self
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a child. This will be either spgrContainer or spContainer.
|
||||
*
|
||||
* @param mixed $child
|
||||
*/
|
||||
public function addChild($child): void
|
||||
{
|
||||
$this->children[] = $child;
|
||||
$child->setParent($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collection of Shape Containers.
|
||||
*/
|
||||
public function getChildren(): array
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively get all spContainers within this spgrContainer.
|
||||
*
|
||||
* @return SpgrContainer\SpContainer[]
|
||||
*/
|
||||
public function getAllSpContainers()
|
||||
{
|
||||
$allSpContainers = [];
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
if ($child instanceof self) {
|
||||
$allSpContainers = array_merge($allSpContainers, $child->getAllSpContainers());
|
||||
} else {
|
||||
$allSpContainers[] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
return $allSpContainers;
|
||||
}
|
||||
}
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
|
||||
|
||||
class SpContainer
|
||||
{
|
||||
/**
|
||||
* Parent Shape Group Container.
|
||||
*
|
||||
* @var SpgrContainer
|
||||
*/
|
||||
private $parent;
|
||||
|
||||
/**
|
||||
* Is this a group shape?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $spgr = false;
|
||||
|
||||
/**
|
||||
* Shape type.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $spType;
|
||||
|
||||
/**
|
||||
* Shape flag.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $spFlag;
|
||||
|
||||
/**
|
||||
* Shape index (usually group shape has index 0, and the rest: 1,2,3...).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $spId;
|
||||
|
||||
/**
|
||||
* Array of options.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $OPT;
|
||||
|
||||
/**
|
||||
* Cell coordinates of upper-left corner of shape, e.g. 'A1'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $startCoordinates;
|
||||
|
||||
/**
|
||||
* Horizontal offset of upper-left corner of shape measured in 1/1024 of column width.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $startOffsetX;
|
||||
|
||||
/**
|
||||
* Vertical offset of upper-left corner of shape measured in 1/256 of row height.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $startOffsetY;
|
||||
|
||||
/**
|
||||
* Cell coordinates of bottom-right corner of shape, e.g. 'B2'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $endCoordinates;
|
||||
|
||||
/**
|
||||
* Horizontal offset of bottom-right corner of shape measured in 1/1024 of column width.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $endOffsetX;
|
||||
|
||||
/**
|
||||
* Vertical offset of bottom-right corner of shape measured in 1/256 of row height.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $endOffsetY;
|
||||
|
||||
/**
|
||||
* Set parent Shape Group Container.
|
||||
*
|
||||
* @param SpgrContainer $parent
|
||||
*/
|
||||
public function setParent($parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent Shape Group Container.
|
||||
*
|
||||
* @return SpgrContainer
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this is a group shape.
|
||||
*
|
||||
* @param bool $value
|
||||
*/
|
||||
public function setSpgr($value): void
|
||||
{
|
||||
$this->spgr = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether this is a group shape.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getSpgr()
|
||||
{
|
||||
return $this->spgr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the shape type.
|
||||
*
|
||||
* @param int $value
|
||||
*/
|
||||
public function setSpType($value): void
|
||||
{
|
||||
$this->spType = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shape type.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSpType()
|
||||
{
|
||||
return $this->spType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the shape flag.
|
||||
*
|
||||
* @param int $value
|
||||
*/
|
||||
public function setSpFlag($value): void
|
||||
{
|
||||
$this->spFlag = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shape flag.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSpFlag()
|
||||
{
|
||||
return $this->spFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the shape index.
|
||||
*
|
||||
* @param int $value
|
||||
*/
|
||||
public function setSpId($value): void
|
||||
{
|
||||
$this->spId = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shape index.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSpId()
|
||||
{
|
||||
return $this->spId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an option for the Shape Group Container.
|
||||
*
|
||||
* @param int $property The number specifies the option
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function setOPT($property, $value): void
|
||||
{
|
||||
$this->OPT[$property] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an option for the Shape Group Container.
|
||||
*
|
||||
* @param int $property The number specifies the option
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getOPT($property)
|
||||
{
|
||||
if (isset($this->OPT[$property])) {
|
||||
return $this->OPT[$property];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection of options.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOPTCollection()
|
||||
{
|
||||
return $this->OPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cell coordinates of upper-left corner of shape.
|
||||
*
|
||||
* @param string $value eg: 'A1'
|
||||
*/
|
||||
public function setStartCoordinates($value): void
|
||||
{
|
||||
$this->startCoordinates = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cell coordinates of upper-left corner of shape.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStartCoordinates()
|
||||
{
|
||||
return $this->startCoordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set offset in x-direction of upper-left corner of shape measured in 1/1024 of column width.
|
||||
*
|
||||
* @param int $startOffsetX
|
||||
*/
|
||||
public function setStartOffsetX($startOffsetX): void
|
||||
{
|
||||
$this->startOffsetX = $startOffsetX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offset in x-direction of upper-left corner of shape measured in 1/1024 of column width.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStartOffsetX()
|
||||
{
|
||||
return $this->startOffsetX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set offset in y-direction of upper-left corner of shape measured in 1/256 of row height.
|
||||
*
|
||||
* @param int $startOffsetY
|
||||
*/
|
||||
public function setStartOffsetY($startOffsetY): void
|
||||
{
|
||||
$this->startOffsetY = $startOffsetY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offset in y-direction of upper-left corner of shape measured in 1/256 of row height.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStartOffsetY()
|
||||
{
|
||||
return $this->startOffsetY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cell coordinates of bottom-right corner of shape.
|
||||
*
|
||||
* @param string $value eg: 'A1'
|
||||
*/
|
||||
public function setEndCoordinates($value): void
|
||||
{
|
||||
$this->endCoordinates = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cell coordinates of bottom-right corner of shape.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEndCoordinates()
|
||||
{
|
||||
return $this->endCoordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width.
|
||||
*
|
||||
* @param int $endOffsetX
|
||||
*/
|
||||
public function setEndOffsetX($endOffsetX): void
|
||||
{
|
||||
$this->endOffsetX = $endOffsetX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getEndOffsetX()
|
||||
{
|
||||
return $this->endOffsetX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set offset in y-direction of bottom-right corner of shape measured in 1/256 of row height.
|
||||
*
|
||||
* @param int $endOffsetY
|
||||
*/
|
||||
public function setEndOffsetY($endOffsetY): void
|
||||
{
|
||||
$this->endOffsetY = $endOffsetY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offset in y-direction of bottom-right corner of shape measured in 1/256 of row height.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getEndOffsetY()
|
||||
{
|
||||
return $this->endOffsetY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nesting level of this spContainer. This is the number of spgrContainers between this spContainer and
|
||||
* the dgContainer. A value of 1 = immediately within first spgrContainer
|
||||
* Higher nesting level occurs if and only if spContainer is part of a shape group.
|
||||
*
|
||||
* @return int Nesting level
|
||||
*/
|
||||
public function getNestingLevel()
|
||||
{
|
||||
$nestingLevel = 0;
|
||||
|
||||
$parent = $this->getParent();
|
||||
while ($parent instanceof SpgrContainer) {
|
||||
++$nestingLevel;
|
||||
$parent = $parent->getParent();
|
||||
}
|
||||
|
||||
return $nestingLevel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Escher;
|
||||
|
||||
class DggContainer
|
||||
{
|
||||
/**
|
||||
* Maximum shape index of all shapes in all drawings increased by one.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $spIdMax;
|
||||
|
||||
/**
|
||||
* Total number of drawings saved.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $cDgSaved;
|
||||
|
||||
/**
|
||||
* Total number of shapes saved (including group shapes).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $cSpSaved;
|
||||
|
||||
/**
|
||||
* BLIP Store Container.
|
||||
*
|
||||
* @var ?DggContainer\BstoreContainer
|
||||
*/
|
||||
private $bstoreContainer;
|
||||
|
||||
/**
|
||||
* Array of options for the drawing group.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $OPT = [];
|
||||
|
||||
/**
|
||||
* Array of identifier clusters containg information about the maximum shape identifiers.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $IDCLs = [];
|
||||
|
||||
/**
|
||||
* Get maximum shape index of all shapes in all drawings (plus one).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSpIdMax()
|
||||
{
|
||||
return $this->spIdMax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum shape index of all shapes in all drawings (plus one).
|
||||
*
|
||||
* @param int $value
|
||||
*/
|
||||
public function setSpIdMax($value): void
|
||||
{
|
||||
$this->spIdMax = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of drawings saved.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCDgSaved()
|
||||
{
|
||||
return $this->cDgSaved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set total number of drawings saved.
|
||||
*
|
||||
* @param int $value
|
||||
*/
|
||||
public function setCDgSaved($value): void
|
||||
{
|
||||
$this->cDgSaved = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of shapes saved (including group shapes).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCSpSaved()
|
||||
{
|
||||
return $this->cSpSaved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set total number of shapes saved (including group shapes).
|
||||
*
|
||||
* @param int $value
|
||||
*/
|
||||
public function setCSpSaved($value): void
|
||||
{
|
||||
$this->cSpSaved = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get BLIP Store Container.
|
||||
*
|
||||
* @return ?DggContainer\BstoreContainer
|
||||
*/
|
||||
public function getBstoreContainer()
|
||||
{
|
||||
return $this->bstoreContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set BLIP Store Container.
|
||||
*
|
||||
* @param DggContainer\BstoreContainer $bstoreContainer
|
||||
*/
|
||||
public function setBstoreContainer($bstoreContainer): void
|
||||
{
|
||||
$this->bstoreContainer = $bstoreContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an option for the drawing group.
|
||||
*
|
||||
* @param int $property The number specifies the option
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function setOPT($property, $value): void
|
||||
{
|
||||
$this->OPT[$property] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an option for the drawing group.
|
||||
*
|
||||
* @param int $property The number specifies the option
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getOPT($property)
|
||||
{
|
||||
if (isset($this->OPT[$property])) {
|
||||
return $this->OPT[$property];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get identifier clusters.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getIDCLs()
|
||||
{
|
||||
return $this->IDCLs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set identifier clusters. [<drawingId> => <max shape id>, ...].
|
||||
*
|
||||
* @param array $IDCLs
|
||||
*/
|
||||
public function setIDCLs($IDCLs): void
|
||||
{
|
||||
$this->IDCLs = $IDCLs;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer;
|
||||
|
||||
class BstoreContainer
|
||||
{
|
||||
/**
|
||||
* BLIP Store Entries. Each of them holds one BLIP (Big Large Image or Picture).
|
||||
*
|
||||
* @var BstoreContainer\BSE[]
|
||||
*/
|
||||
private $BSECollection = [];
|
||||
|
||||
/**
|
||||
* Add a BLIP Store Entry.
|
||||
*/
|
||||
public function addBSE(BstoreContainer\BSE $BSE): void
|
||||
{
|
||||
$this->BSECollection[] = $BSE;
|
||||
$BSE->setParent($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection of BLIP Store Entries.
|
||||
*
|
||||
* @return BstoreContainer\BSE[]
|
||||
*/
|
||||
public function getBSECollection()
|
||||
{
|
||||
return $this->BSECollection;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
|
||||
|
||||
class BSE
|
||||
{
|
||||
const BLIPTYPE_ERROR = 0x00;
|
||||
const BLIPTYPE_UNKNOWN = 0x01;
|
||||
const BLIPTYPE_EMF = 0x02;
|
||||
const BLIPTYPE_WMF = 0x03;
|
||||
const BLIPTYPE_PICT = 0x04;
|
||||
const BLIPTYPE_JPEG = 0x05;
|
||||
const BLIPTYPE_PNG = 0x06;
|
||||
const BLIPTYPE_DIB = 0x07;
|
||||
const BLIPTYPE_TIFF = 0x11;
|
||||
const BLIPTYPE_CMYKJPEG = 0x12;
|
||||
|
||||
/**
|
||||
* The parent BLIP Store Entry Container.
|
||||
* Property is never currently read.
|
||||
*
|
||||
* @var BstoreContainer
|
||||
*/
|
||||
private $parent; // @phpstan-ignore-line
|
||||
|
||||
/**
|
||||
* The BLIP (Big Large Image or Picture).
|
||||
*
|
||||
* @var ?BSE\Blip
|
||||
*/
|
||||
private $blip;
|
||||
|
||||
/**
|
||||
* The BLIP type.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $blipType;
|
||||
|
||||
/**
|
||||
* Set parent BLIP Store Entry Container.
|
||||
*/
|
||||
public function setParent(BstoreContainer $parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the BLIP.
|
||||
*
|
||||
* @return ?BSE\Blip
|
||||
*/
|
||||
public function getBlip()
|
||||
{
|
||||
return $this->blip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the BLIP.
|
||||
*/
|
||||
public function setBlip(BSE\Blip $blip): void
|
||||
{
|
||||
$this->blip = $blip;
|
||||
$blip->setParent($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the BLIP type.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getBlipType()
|
||||
{
|
||||
return $this->blipType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the BLIP type.
|
||||
*
|
||||
* @param int $blipType
|
||||
*/
|
||||
public function setBlipType($blipType): void
|
||||
{
|
||||
$this->blipType = $blipType;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
|
||||
|
||||
class Blip
|
||||
{
|
||||
/**
|
||||
* The parent BSE.
|
||||
*
|
||||
* @var BSE
|
||||
*/
|
||||
private $parent;
|
||||
|
||||
/**
|
||||
* Raw image data.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* Get the raw image data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the raw image data.
|
||||
*
|
||||
* @param string $data
|
||||
*/
|
||||
public function setData($data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set parent BSE.
|
||||
*/
|
||||
public function setParent(BSE $parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parent BSE.
|
||||
*/
|
||||
public function getParent(): BSE
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
|
||||
use ZipArchive;
|
||||
|
||||
class File
|
||||
{
|
||||
/**
|
||||
* Use Temp or File Upload Temp for temporary files.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $useUploadTempDirectory = false;
|
||||
|
||||
/**
|
||||
* Set the flag indicating whether the File Upload Temp directory should be used for temporary files.
|
||||
*/
|
||||
public static function setUseUploadTempDirectory(bool $useUploadTempDir): void
|
||||
{
|
||||
self::$useUploadTempDirectory = (bool) $useUploadTempDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the flag indicating whether the File Upload Temp directory should be used for temporary files.
|
||||
*/
|
||||
public static function getUseUploadTempDirectory(): bool
|
||||
{
|
||||
return self::$useUploadTempDirectory;
|
||||
}
|
||||
|
||||
// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
||||
// Section 4.3.7
|
||||
// Looks like there might be endian-ness considerations
|
||||
private const ZIP_FIRST_4 = [
|
||||
"\x50\x4b\x03\x04", // what it looks like on my system
|
||||
"\x04\x03\x4b\x50", // what it says in documentation
|
||||
];
|
||||
|
||||
private static function validateZipFirst4(string $zipFile): bool
|
||||
{
|
||||
$contents = @file_get_contents($zipFile, false, null, 0, 4);
|
||||
|
||||
return in_array($contents, self::ZIP_FIRST_4, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a file exists.
|
||||
*/
|
||||
public static function fileExists(string $filename): bool
|
||||
{
|
||||
// Sick construction, but it seems that
|
||||
// file_exists returns strange values when
|
||||
// doing the original file_exists on ZIP archives...
|
||||
if (strtolower(substr($filename, 0, 6)) == 'zip://') {
|
||||
// Open ZIP file and verify if the file exists
|
||||
$zipFile = substr($filename, 6, strrpos($filename, '#') - 6);
|
||||
$archiveFile = substr($filename, strrpos($filename, '#') + 1);
|
||||
|
||||
if (self::validateZipFirst4($zipFile)) {
|
||||
$zip = new ZipArchive();
|
||||
$res = $zip->open($zipFile);
|
||||
if ($res === true) {
|
||||
$returnValue = ($zip->getFromName($archiveFile) !== false);
|
||||
$zip->close();
|
||||
|
||||
return $returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return file_exists($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns canonicalized absolute pathname, also for ZIP archives.
|
||||
*/
|
||||
public static function realpath(string $filename): string
|
||||
{
|
||||
// Returnvalue
|
||||
$returnValue = '';
|
||||
|
||||
// Try using realpath()
|
||||
if (file_exists($filename)) {
|
||||
$returnValue = realpath($filename) ?: '';
|
||||
}
|
||||
|
||||
// Found something?
|
||||
if ($returnValue === '') {
|
||||
$pathArray = explode('/', $filename);
|
||||
while (in_array('..', $pathArray) && $pathArray[0] != '..') {
|
||||
$iMax = count($pathArray);
|
||||
for ($i = 0; $i < $iMax; ++$i) {
|
||||
if ($pathArray[$i] == '..' && $i > 0) {
|
||||
unset($pathArray[$i], $pathArray[$i - 1]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$returnValue = implode('/', $pathArray);
|
||||
}
|
||||
|
||||
// Return
|
||||
return $returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the systems temporary directory.
|
||||
*/
|
||||
public static function sysGetTempDir(): string
|
||||
{
|
||||
$path = sys_get_temp_dir();
|
||||
if (self::$useUploadTempDirectory) {
|
||||
// use upload-directory when defined to allow running on environments having very restricted
|
||||
// open_basedir configs
|
||||
if (ini_get('upload_tmp_dir') !== false) {
|
||||
if ($temp = ini_get('upload_tmp_dir')) {
|
||||
if (file_exists($temp)) {
|
||||
$path = $temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return realpath($path) ?: '';
|
||||
}
|
||||
|
||||
public static function temporaryFilename(): string
|
||||
{
|
||||
$filename = tempnam(self::sysGetTempDir(), 'phpspreadsheet');
|
||||
if ($filename === false) {
|
||||
throw new Exception('Could not create temporary file');
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that given path is an existing file and is readable, otherwise throw exception.
|
||||
*/
|
||||
public static function assertFile(string $filename, string $zipMember = ''): void
|
||||
{
|
||||
if (!is_file($filename)) {
|
||||
throw new ReaderException('File "' . $filename . '" does not exist.');
|
||||
}
|
||||
|
||||
if (!is_readable($filename)) {
|
||||
throw new ReaderException('Could not open "' . $filename . '" for reading.');
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
public static function testFileNoThrow(string $filename, ?string $zipMember = null): bool
|
||||
{
|
||||
if (!is_file($filename)) {
|
||||
return false;
|
||||
}
|
||||
if (!is_readable($filename)) {
|
||||
return false;
|
||||
}
|
||||
if ($zipMember === null) {
|
||||
return true;
|
||||
}
|
||||
// validate zip, but don't check specific member
|
||||
if ($zipMember === '') {
|
||||
return self::validateZipFirst4($filename);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
|
||||
use PhpOffice\PhpSpreadsheet\RichText\RichText;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Font as FontStyle;
|
||||
|
||||
class Font
|
||||
{
|
||||
// Methods for resolving autosize value
|
||||
const AUTOSIZE_METHOD_APPROX = 'approx';
|
||||
const AUTOSIZE_METHOD_EXACT = 'exact';
|
||||
|
||||
private const AUTOSIZE_METHODS = [
|
||||
self::AUTOSIZE_METHOD_APPROX,
|
||||
self::AUTOSIZE_METHOD_EXACT,
|
||||
];
|
||||
|
||||
/** Character set codes used by BIFF5-8 in Font records */
|
||||
const CHARSET_ANSI_LATIN = 0x00;
|
||||
const CHARSET_SYSTEM_DEFAULT = 0x01;
|
||||
const CHARSET_SYMBOL = 0x02;
|
||||
const CHARSET_APPLE_ROMAN = 0x4D;
|
||||
const CHARSET_ANSI_JAPANESE_SHIFTJIS = 0x80;
|
||||
const CHARSET_ANSI_KOREAN_HANGUL = 0x81;
|
||||
const CHARSET_ANSI_KOREAN_JOHAB = 0x82;
|
||||
const CHARSET_ANSI_CHINESE_SIMIPLIFIED = 0x86; // gb2312
|
||||
const CHARSET_ANSI_CHINESE_TRADITIONAL = 0x88; // big5
|
||||
const CHARSET_ANSI_GREEK = 0xA1;
|
||||
const CHARSET_ANSI_TURKISH = 0xA2;
|
||||
const CHARSET_ANSI_VIETNAMESE = 0xA3;
|
||||
const CHARSET_ANSI_HEBREW = 0xB1;
|
||||
const CHARSET_ANSI_ARABIC = 0xB2;
|
||||
const CHARSET_ANSI_BALTIC = 0xBA;
|
||||
const CHARSET_ANSI_CYRILLIC = 0xCC;
|
||||
const CHARSET_ANSI_THAI = 0xDD;
|
||||
const CHARSET_ANSI_LATIN_II = 0xEE;
|
||||
const CHARSET_OEM_LATIN_I = 0xFF;
|
||||
|
||||
// XXX: Constants created!
|
||||
/** Font filenames */
|
||||
const ARIAL = 'arial.ttf';
|
||||
const ARIAL_BOLD = 'arialbd.ttf';
|
||||
const ARIAL_ITALIC = 'ariali.ttf';
|
||||
const ARIAL_BOLD_ITALIC = 'arialbi.ttf';
|
||||
|
||||
const CALIBRI = 'calibri.ttf';
|
||||
const CALIBRI_BOLD = 'calibrib.ttf';
|
||||
const CALIBRI_ITALIC = 'calibrii.ttf';
|
||||
const CALIBRI_BOLD_ITALIC = 'calibriz.ttf';
|
||||
|
||||
const COMIC_SANS_MS = 'comic.ttf';
|
||||
const COMIC_SANS_MS_BOLD = 'comicbd.ttf';
|
||||
|
||||
const COURIER_NEW = 'cour.ttf';
|
||||
const COURIER_NEW_BOLD = 'courbd.ttf';
|
||||
const COURIER_NEW_ITALIC = 'couri.ttf';
|
||||
const COURIER_NEW_BOLD_ITALIC = 'courbi.ttf';
|
||||
|
||||
const GEORGIA = 'georgia.ttf';
|
||||
const GEORGIA_BOLD = 'georgiab.ttf';
|
||||
const GEORGIA_ITALIC = 'georgiai.ttf';
|
||||
const GEORGIA_BOLD_ITALIC = 'georgiaz.ttf';
|
||||
|
||||
const IMPACT = 'impact.ttf';
|
||||
|
||||
const LIBERATION_SANS = 'LiberationSans-Regular.ttf';
|
||||
const LIBERATION_SANS_BOLD = 'LiberationSans-Bold.ttf';
|
||||
const LIBERATION_SANS_ITALIC = 'LiberationSans-Italic.ttf';
|
||||
const LIBERATION_SANS_BOLD_ITALIC = 'LiberationSans-BoldItalic.ttf';
|
||||
|
||||
const LUCIDA_CONSOLE = 'lucon.ttf';
|
||||
const LUCIDA_SANS_UNICODE = 'l_10646.ttf';
|
||||
|
||||
const MICROSOFT_SANS_SERIF = 'micross.ttf';
|
||||
|
||||
const PALATINO_LINOTYPE = 'pala.ttf';
|
||||
const PALATINO_LINOTYPE_BOLD = 'palab.ttf';
|
||||
const PALATINO_LINOTYPE_ITALIC = 'palai.ttf';
|
||||
const PALATINO_LINOTYPE_BOLD_ITALIC = 'palabi.ttf';
|
||||
|
||||
const SYMBOL = 'symbol.ttf';
|
||||
|
||||
const TAHOMA = 'tahoma.ttf';
|
||||
const TAHOMA_BOLD = 'tahomabd.ttf';
|
||||
|
||||
const TIMES_NEW_ROMAN = 'times.ttf';
|
||||
const TIMES_NEW_ROMAN_BOLD = 'timesbd.ttf';
|
||||
const TIMES_NEW_ROMAN_ITALIC = 'timesi.ttf';
|
||||
const TIMES_NEW_ROMAN_BOLD_ITALIC = 'timesbi.ttf';
|
||||
|
||||
const TREBUCHET_MS = 'trebuc.ttf';
|
||||
const TREBUCHET_MS_BOLD = 'trebucbd.ttf';
|
||||
const TREBUCHET_MS_ITALIC = 'trebucit.ttf';
|
||||
const TREBUCHET_MS_BOLD_ITALIC = 'trebucbi.ttf';
|
||||
|
||||
const VERDANA = 'verdana.ttf';
|
||||
const VERDANA_BOLD = 'verdanab.ttf';
|
||||
const VERDANA_ITALIC = 'verdanai.ttf';
|
||||
const VERDANA_BOLD_ITALIC = 'verdanaz.ttf';
|
||||
|
||||
const FONT_FILE_NAMES = [
|
||||
'Arial' => [
|
||||
'x' => self::ARIAL,
|
||||
'xb' => self::ARIAL_BOLD,
|
||||
'xi' => self::ARIAL_ITALIC,
|
||||
'xbi' => self::ARIAL_BOLD_ITALIC,
|
||||
],
|
||||
'Calibri' => [
|
||||
'x' => self::CALIBRI,
|
||||
'xb' => self::CALIBRI_BOLD,
|
||||
'xi' => self::CALIBRI_ITALIC,
|
||||
'xbi' => self::CALIBRI_BOLD_ITALIC,
|
||||
],
|
||||
'Comic Sans MS' => [
|
||||
'x' => self::COMIC_SANS_MS,
|
||||
'xb' => self::COMIC_SANS_MS_BOLD,
|
||||
'xi' => self::COMIC_SANS_MS,
|
||||
'xbi' => self::COMIC_SANS_MS_BOLD,
|
||||
],
|
||||
'Courier New' => [
|
||||
'x' => self::COURIER_NEW,
|
||||
'xb' => self::COURIER_NEW_BOLD,
|
||||
'xi' => self::COURIER_NEW_ITALIC,
|
||||
'xbi' => self::COURIER_NEW_BOLD_ITALIC,
|
||||
],
|
||||
'Georgia' => [
|
||||
'x' => self::GEORGIA,
|
||||
'xb' => self::GEORGIA_BOLD,
|
||||
'xi' => self::GEORGIA_ITALIC,
|
||||
'xbi' => self::GEORGIA_BOLD_ITALIC,
|
||||
],
|
||||
'Impact' => [
|
||||
'x' => self::IMPACT,
|
||||
'xb' => self::IMPACT,
|
||||
'xi' => self::IMPACT,
|
||||
'xbi' => self::IMPACT,
|
||||
],
|
||||
'Liberation Sans' => [
|
||||
'x' => self::LIBERATION_SANS,
|
||||
'xb' => self::LIBERATION_SANS_BOLD,
|
||||
'xi' => self::LIBERATION_SANS_ITALIC,
|
||||
'xbi' => self::LIBERATION_SANS_BOLD_ITALIC,
|
||||
],
|
||||
'Lucida Console' => [
|
||||
'x' => self::LUCIDA_CONSOLE,
|
||||
'xb' => self::LUCIDA_CONSOLE,
|
||||
'xi' => self::LUCIDA_CONSOLE,
|
||||
'xbi' => self::LUCIDA_CONSOLE,
|
||||
],
|
||||
'Lucida Sans Unicode' => [
|
||||
'x' => self::LUCIDA_SANS_UNICODE,
|
||||
'xb' => self::LUCIDA_SANS_UNICODE,
|
||||
'xi' => self::LUCIDA_SANS_UNICODE,
|
||||
'xbi' => self::LUCIDA_SANS_UNICODE,
|
||||
],
|
||||
'Microsoft Sans Serif' => [
|
||||
'x' => self::MICROSOFT_SANS_SERIF,
|
||||
'xb' => self::MICROSOFT_SANS_SERIF,
|
||||
'xi' => self::MICROSOFT_SANS_SERIF,
|
||||
'xbi' => self::MICROSOFT_SANS_SERIF,
|
||||
],
|
||||
'Palatino Linotype' => [
|
||||
'x' => self::PALATINO_LINOTYPE,
|
||||
'xb' => self::PALATINO_LINOTYPE_BOLD,
|
||||
'xi' => self::PALATINO_LINOTYPE_ITALIC,
|
||||
'xbi' => self::PALATINO_LINOTYPE_BOLD_ITALIC,
|
||||
],
|
||||
'Symbol' => [
|
||||
'x' => self::SYMBOL,
|
||||
'xb' => self::SYMBOL,
|
||||
'xi' => self::SYMBOL,
|
||||
'xbi' => self::SYMBOL,
|
||||
],
|
||||
'Tahoma' => [
|
||||
'x' => self::TAHOMA,
|
||||
'xb' => self::TAHOMA_BOLD,
|
||||
'xi' => self::TAHOMA,
|
||||
'xbi' => self::TAHOMA_BOLD,
|
||||
],
|
||||
'Times New Roman' => [
|
||||
'x' => self::TIMES_NEW_ROMAN,
|
||||
'xb' => self::TIMES_NEW_ROMAN_BOLD,
|
||||
'xi' => self::TIMES_NEW_ROMAN_ITALIC,
|
||||
'xbi' => self::TIMES_NEW_ROMAN_BOLD_ITALIC,
|
||||
],
|
||||
'Trebuchet MS' => [
|
||||
'x' => self::TREBUCHET_MS,
|
||||
'xb' => self::TREBUCHET_MS_BOLD,
|
||||
'xi' => self::TREBUCHET_MS_ITALIC,
|
||||
'xbi' => self::TREBUCHET_MS_BOLD_ITALIC,
|
||||
],
|
||||
'Verdana' => [
|
||||
'x' => self::VERDANA,
|
||||
'xb' => self::VERDANA_BOLD,
|
||||
'xi' => self::VERDANA_ITALIC,
|
||||
'xbi' => self::VERDANA_BOLD_ITALIC,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Array that can be used to supplement FONT_FILE_NAMES for calculating exact width.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $extraFontArray = [];
|
||||
|
||||
public static function setExtraFontArray(array $extraFontArray): void
|
||||
{
|
||||
self::$extraFontArray = $extraFontArray;
|
||||
}
|
||||
|
||||
public static function getExtraFontArray(): array
|
||||
{
|
||||
return self::$extraFontArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* AutoSize method.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $autoSizeMethod = self::AUTOSIZE_METHOD_APPROX;
|
||||
|
||||
/**
|
||||
* Path to folder containing TrueType font .ttf files.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $trueTypeFontPath = '';
|
||||
|
||||
/**
|
||||
* How wide is a default column for a given default font and size?
|
||||
* Empirical data found by inspecting real Excel files and reading off the pixel width
|
||||
* in Microsoft Office Excel 2007.
|
||||
* Added height in points.
|
||||
*/
|
||||
public const DEFAULT_COLUMN_WIDTHS = [
|
||||
'Arial' => [
|
||||
1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0],
|
||||
|
||||
4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75],
|
||||
5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25],
|
||||
6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25],
|
||||
7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0],
|
||||
8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25],
|
||||
9 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.0],
|
||||
10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75],
|
||||
],
|
||||
'Calibri' => [
|
||||
1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.00],
|
||||
4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75],
|
||||
5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25],
|
||||
6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25],
|
||||
7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0],
|
||||
8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25],
|
||||
9 => ['px' => 56, 'width' => 9.33203125, 'height' => 12.0],
|
||||
10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75],
|
||||
11 => ['px' => 64, 'width' => 9.14062500, 'height' => 15.0],
|
||||
],
|
||||
'Verdana' => [
|
||||
1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25],
|
||||
3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0],
|
||||
4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75],
|
||||
5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25],
|
||||
6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25],
|
||||
7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0],
|
||||
8 => ['px' => 64, 'width' => 9.14062500, 'height' => 10.5],
|
||||
9 => ['px' => 72, 'width' => 9.00000000, 'height' => 11.25],
|
||||
10 => ['px' => 72, 'width' => 9.00000000, 'height' => 12.75],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* List of column widths. Replaced by constant;
|
||||
* previously it was public and updateable, allowing
|
||||
* user to make inappropriate alterations.
|
||||
*
|
||||
* @deprecated 1.25.0 Use DEFAULT_COLUMN_WIDTHS constant instead.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $defaultColumnWidths = self::DEFAULT_COLUMN_WIDTHS;
|
||||
|
||||
/**
|
||||
* Set autoSize method.
|
||||
*
|
||||
* @param string $method see self::AUTOSIZE_METHOD_*
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public static function setAutoSizeMethod($method)
|
||||
{
|
||||
if (!in_array($method, self::AUTOSIZE_METHODS)) {
|
||||
return false;
|
||||
}
|
||||
self::$autoSizeMethod = $method;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get autoSize method.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAutoSizeMethod()
|
||||
{
|
||||
return self::$autoSizeMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path to the folder containing .ttf files. There should be a trailing slash.
|
||||
* Typical locations on variout some platforms:
|
||||
* <ul>
|
||||
* <li>C:/Windows/Fonts/</li>
|
||||
* <li>/usr/share/fonts/truetype/</li>
|
||||
* <li>~/.fonts/</li>
|
||||
* </ul>.
|
||||
*
|
||||
* @param string $folderPath
|
||||
*/
|
||||
public static function setTrueTypeFontPath($folderPath): void
|
||||
{
|
||||
self::$trueTypeFontPath = $folderPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder containing .ttf files.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTrueTypeFontPath()
|
||||
{
|
||||
return self::$trueTypeFontPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an (approximate) OpenXML column width, based on font size and text contained.
|
||||
*
|
||||
* @param FontStyle $font Font object
|
||||
* @param null|RichText|string $cellText Text to calculate width
|
||||
* @param int $rotation Rotation angle
|
||||
* @param null|FontStyle $defaultFont Font object
|
||||
* @param bool $filterAdjustment Add space for Autofilter or Table dropdown
|
||||
*/
|
||||
public static function calculateColumnWidth(
|
||||
FontStyle $font,
|
||||
$cellText = '',
|
||||
$rotation = 0,
|
||||
?FontStyle $defaultFont = null,
|
||||
bool $filterAdjustment = false,
|
||||
int $indentAdjustment = 0
|
||||
): float {
|
||||
// If it is rich text, use plain text
|
||||
if ($cellText instanceof RichText) {
|
||||
$cellText = $cellText->getPlainText();
|
||||
}
|
||||
|
||||
// Special case if there are one or more newline characters ("\n")
|
||||
$cellText = (string) $cellText;
|
||||
if (strpos($cellText, "\n") !== false) {
|
||||
$lineTexts = explode("\n", $cellText);
|
||||
$lineWidths = [];
|
||||
foreach ($lineTexts as $lineText) {
|
||||
$lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont, $filterAdjustment);
|
||||
}
|
||||
|
||||
return max($lineWidths); // width of longest line in cell
|
||||
}
|
||||
|
||||
// Try to get the exact text width in pixels
|
||||
$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))),
|
||||
$font,
|
||||
0
|
||||
) * 1.07
|
||||
);
|
||||
|
||||
// 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;
|
||||
} catch (PhpSpreadsheetException $e) {
|
||||
$approximate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($approximate) {
|
||||
$columnWidthAdjust = self::getTextWidthPixelsApprox(
|
||||
str_repeat('n', 1 * (($filterAdjustment ? 3 : 1) + ($indentAdjustment * 2))),
|
||||
$font,
|
||||
0
|
||||
);
|
||||
// Width of text in pixels excl. padding, approximation
|
||||
// and addition because Excel adds some padding, just use approx width of 'n' glyph
|
||||
$columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust;
|
||||
}
|
||||
|
||||
// Convert from pixel width to column width
|
||||
$columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont ?? new FontStyle());
|
||||
|
||||
// Return
|
||||
return round($columnWidth, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GD text width in pixels for a string of text in a certain font at a certain rotation angle.
|
||||
*/
|
||||
public static function getTextWidthPixelsExact(string $text, FontStyle $font, int $rotation = 0): float
|
||||
{
|
||||
// font size should really be supplied in pixels in GD2,
|
||||
// but since GD2 seems to assume 72dpi, pixels and points are the same
|
||||
$fontFile = self::getTrueTypeFontFileFromFont($font);
|
||||
$textBox = imagettfbbox($font->getSize() ?? 10.0, $rotation, $fontFile, $text);
|
||||
if ($textBox === false) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new PhpSpreadsheetException('imagettfbbox failed');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
// Get corners positions
|
||||
$lowerLeftCornerX = $textBox[0];
|
||||
$lowerRightCornerX = $textBox[2];
|
||||
$upperRightCornerX = $textBox[4];
|
||||
$upperLeftCornerX = $textBox[6];
|
||||
|
||||
// Consider the rotation when calculating the width
|
||||
return round(max($lowerRightCornerX - $upperLeftCornerX, $upperRightCornerX - $lowerLeftCornerX), 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approximate width in pixels for a string of text in a certain font at a certain rotation angle.
|
||||
*
|
||||
* @param string $columnText
|
||||
* @param int $rotation
|
||||
*
|
||||
* @return int Text width in pixels (no padding added)
|
||||
*/
|
||||
public static function getTextWidthPixelsApprox($columnText, FontStyle $font, $rotation = 0)
|
||||
{
|
||||
$fontName = $font->getName();
|
||||
$fontSize = $font->getSize();
|
||||
|
||||
// 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 'Arial':
|
||||
// value 8 was set because of experience in different exports at Arial 10 font.
|
||||
$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::countCharactersDbcs($columnText));
|
||||
$columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size
|
||||
|
||||
break;
|
||||
default:
|
||||
// just assume Calibri
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Calculate approximate rotated column width
|
||||
if ($rotation !== 0) {
|
||||
if ($rotation == Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) {
|
||||
// stacked text
|
||||
$columnWidth = 4; // approximation
|
||||
} else {
|
||||
// rotated text
|
||||
$columnWidth = $columnWidth * cos(deg2rad($rotation))
|
||||
+ $fontSize * abs(sin(deg2rad($rotation))) / 5; // approximation
|
||||
}
|
||||
}
|
||||
|
||||
// pixel width is an integer
|
||||
return (int) $columnWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an (approximate) pixel size, based on a font points size.
|
||||
*
|
||||
* @param int $fontSizeInPoints Font size (in points)
|
||||
*
|
||||
* @return int Font size (in pixels)
|
||||
*/
|
||||
public static function fontSizeToPixels($fontSizeInPoints)
|
||||
{
|
||||
return (int) ((4 / 3) * $fontSizeInPoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an (approximate) pixel size, based on inch size.
|
||||
*
|
||||
* @param int $sizeInInch Font size (in inch)
|
||||
*
|
||||
* @return int Size (in pixels)
|
||||
*/
|
||||
public static function inchSizeToPixels($sizeInInch)
|
||||
{
|
||||
return $sizeInInch * 96;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an (approximate) pixel size, based on centimeter size.
|
||||
*
|
||||
* @param int $sizeInCm Font size (in centimeters)
|
||||
*
|
||||
* @return float Size (in pixels)
|
||||
*/
|
||||
public static function centimeterSizeToPixels($sizeInCm)
|
||||
{
|
||||
return $sizeInCm * 37.795275591;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the font path given the font.
|
||||
*
|
||||
* @return string Path to TrueType font file
|
||||
*/
|
||||
public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkPath = true)
|
||||
{
|
||||
if ($checkPath && (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath))) {
|
||||
throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified');
|
||||
}
|
||||
|
||||
$name = $font->getName();
|
||||
$fontArray = array_merge(self::FONT_FILE_NAMES, self::$extraFontArray);
|
||||
if (!isset($fontArray[$name])) {
|
||||
throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file');
|
||||
}
|
||||
$bold = $font->getBold();
|
||||
$italic = $font->getItalic();
|
||||
$index = 'x';
|
||||
if ($bold) {
|
||||
$index .= 'b';
|
||||
}
|
||||
if ($italic) {
|
||||
$index .= 'i';
|
||||
}
|
||||
$fontFile = $fontArray[$name][$index];
|
||||
|
||||
$separator = '';
|
||||
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) && !$fontFileAbsolute) {
|
||||
$alternateName = $name;
|
||||
if ($index !== 'x' && $fontArray[$name][$index] !== $fontArray[$name]['x']) {
|
||||
// Bold but no italic:
|
||||
// Comic Sans
|
||||
// Tahoma
|
||||
// Neither bold nor italic:
|
||||
// Impact
|
||||
// Lucida Console
|
||||
// Lucida Sans Unicode
|
||||
// Microsoft Sans Serif
|
||||
// Symbol
|
||||
if ($index === 'xb') {
|
||||
$alternateName .= ' Bold';
|
||||
} elseif ($index === 'xi') {
|
||||
$alternateName .= ' Italic';
|
||||
} elseif ($fontArray[$name]['xb'] === $fontArray[$name]['xbi']) {
|
||||
$alternateName .= ' Bold';
|
||||
} else {
|
||||
$alternateName .= ' Bold Italic';
|
||||
}
|
||||
}
|
||||
$fontFile = self::$trueTypeFontPath . $separator . $alternateName . '.ttf';
|
||||
if (!file_exists($fontFile)) {
|
||||
throw new PhpSpreadsheetException('TrueType Font file not found');
|
||||
}
|
||||
}
|
||||
|
||||
return $fontFile;
|
||||
}
|
||||
|
||||
public const CHARSET_FROM_FONT_NAME = [
|
||||
'EucrosiaUPC' => self::CHARSET_ANSI_THAI,
|
||||
'Wingdings' => self::CHARSET_SYMBOL,
|
||||
'Wingdings 2' => self::CHARSET_SYMBOL,
|
||||
'Wingdings 3' => self::CHARSET_SYMBOL,
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the associated charset for the font name.
|
||||
*
|
||||
* @param string $fontName Font name
|
||||
*
|
||||
* @return int Character set code
|
||||
*/
|
||||
public static function getCharsetFromFontName($fontName)
|
||||
{
|
||||
return self::CHARSET_FROM_FONT_NAME[$fontName] ?? self::CHARSET_ANSI_LATIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective column width for columns without a column dimension or column with width -1
|
||||
* For example, for Calibri 11 this is 9.140625 (64 px).
|
||||
*
|
||||
* @param FontStyle $font The workbooks default font
|
||||
* @param bool $returnAsPixels true = return column width in pixels, false = return in OOXML units
|
||||
*
|
||||
* @return mixed Column width
|
||||
*/
|
||||
public static function getDefaultColumnWidthByFont(FontStyle $font, $returnAsPixels = false)
|
||||
{
|
||||
if (isset(self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()])) {
|
||||
// Exact width can be determined
|
||||
$columnWidth = $returnAsPixels ?
|
||||
self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['px']
|
||||
: self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['width'];
|
||||
} else {
|
||||
// We don't have data for this particular font and size, use approximation by
|
||||
// extrapolating from Calibri 11
|
||||
$columnWidth = $returnAsPixels ?
|
||||
self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px']
|
||||
: self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width'];
|
||||
$columnWidth = $columnWidth * $font->getSize() / 11;
|
||||
|
||||
// Round pixels to closest integer
|
||||
if ($returnAsPixels) {
|
||||
$columnWidth = (int) round($columnWidth);
|
||||
}
|
||||
}
|
||||
|
||||
return $columnWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective row height for rows without a row dimension or rows with height -1
|
||||
* For example, for Calibri 11 this is 15 points.
|
||||
*
|
||||
* @param FontStyle $font The workbooks default font
|
||||
*
|
||||
* @return float Row height in points
|
||||
*/
|
||||
public static function getDefaultRowHeightByFont(FontStyle $font)
|
||||
{
|
||||
$name = $font->getName();
|
||||
$size = $font->getSize();
|
||||
if (isset(self::DEFAULT_COLUMN_WIDTHS[$name][$size])) {
|
||||
$rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][$size]['height'];
|
||||
} elseif ($name === 'Arial' || $name === 'Verdana') {
|
||||
$rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][10]['height'] * $size / 10.0;
|
||||
} else {
|
||||
$rowHeight = self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['height'] * $size / 11.0;
|
||||
}
|
||||
|
||||
return $rowHeight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
class IntOrFloat
|
||||
{
|
||||
/**
|
||||
* Help some functions with large results operate correctly on 32-bit,
|
||||
* by returning result as int when possible, float otherwise.
|
||||
*
|
||||
* @param float|int $value
|
||||
*
|
||||
* @return float|int
|
||||
*/
|
||||
public static function evaluate($value)
|
||||
{
|
||||
$iValue = (int) $value;
|
||||
|
||||
return ($value == $iValue) ? $iValue : $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpException;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Protection;
|
||||
|
||||
class PasswordHasher
|
||||
{
|
||||
const MAX_PASSWORD_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Get algorithm name for PHP.
|
||||
*/
|
||||
private static function getAlgorithm(string $algorithmName): string
|
||||
{
|
||||
if (!$algorithmName) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Mapping between algorithm name in Excel and algorithm name in PHP
|
||||
$mapping = [
|
||||
Protection::ALGORITHM_MD2 => 'md2',
|
||||
Protection::ALGORITHM_MD4 => 'md4',
|
||||
Protection::ALGORITHM_MD5 => 'md5',
|
||||
Protection::ALGORITHM_SHA_1 => 'sha1',
|
||||
Protection::ALGORITHM_SHA_256 => 'sha256',
|
||||
Protection::ALGORITHM_SHA_384 => 'sha384',
|
||||
Protection::ALGORITHM_SHA_512 => 'sha512',
|
||||
Protection::ALGORITHM_RIPEMD_128 => 'ripemd128',
|
||||
Protection::ALGORITHM_RIPEMD_160 => 'ripemd160',
|
||||
Protection::ALGORITHM_WHIRLPOOL => 'whirlpool',
|
||||
];
|
||||
|
||||
if (array_key_exists($algorithmName, $mapping)) {
|
||||
return $mapping[$algorithmName];
|
||||
}
|
||||
|
||||
throw new SpException('Unsupported password algorithm: ' . $algorithmName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a password hash from a given string.
|
||||
*
|
||||
* This method is based on the spec at:
|
||||
* https://interoperability.blob.core.windows.net/files/MS-OFFCRYPTO/[MS-OFFCRYPTO].pdf
|
||||
* 2.3.7.1 Binary Document Password Verifier Derivation Method 1
|
||||
*
|
||||
* It replaces a method based on the algorithm provided by
|
||||
* Daniel Rentz of OpenOffice and the PEAR package
|
||||
* Spreadsheet_Excel_Writer by Xavier Noguer <[email protected]>.
|
||||
*
|
||||
* Scrutinizer will squawk at the use of bitwise operations here,
|
||||
* but it should ultimately pass.
|
||||
*
|
||||
* @param string $password Password to hash
|
||||
*/
|
||||
private static function defaultHashPassword(string $password): string
|
||||
{
|
||||
$verifier = 0;
|
||||
$pwlen = strlen($password);
|
||||
$passwordArray = pack('c', $pwlen) . $password;
|
||||
for ($i = $pwlen; $i >= 0; --$i) {
|
||||
$intermediate1 = (($verifier & 0x4000) === 0) ? 0 : 1;
|
||||
$intermediate2 = 2 * $verifier;
|
||||
$intermediate2 = $intermediate2 & 0x7fff;
|
||||
$intermediate3 = $intermediate1 | $intermediate2;
|
||||
$verifier = $intermediate3 ^ ord($passwordArray[$i]);
|
||||
}
|
||||
$verifier ^= 0xCE4B;
|
||||
|
||||
return strtoupper(dechex($verifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a password hash from a given string by a specific algorithm.
|
||||
*
|
||||
* 2.4.2.4 ISO Write Protection Method
|
||||
*
|
||||
* @see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f
|
||||
*
|
||||
* @param string $password Password to hash
|
||||
* @param string $algorithm Hash algorithm used to compute the password hash value
|
||||
* @param string $salt Pseudorandom string
|
||||
* @param int $spinCount Number of times to iterate on a hash of a password
|
||||
*
|
||||
* @return string Hashed password
|
||||
*/
|
||||
public static function hashPassword(string $password, string $algorithm = '', string $salt = '', int $spinCount = 10000): string
|
||||
{
|
||||
if (strlen($password) > self::MAX_PASSWORD_LENGTH) {
|
||||
throw new SpException('Password exceeds ' . self::MAX_PASSWORD_LENGTH . ' characters');
|
||||
}
|
||||
$phpAlgorithm = self::getAlgorithm($algorithm);
|
||||
if (!$phpAlgorithm) {
|
||||
return self::defaultHashPassword($password);
|
||||
}
|
||||
|
||||
$saltValue = base64_decode($salt);
|
||||
$encodedPassword = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8');
|
||||
|
||||
$hashValue = hash($phpAlgorithm, $saltValue . /** @scrutinizer ignore-type */ $encodedPassword, true);
|
||||
for ($i = 0; $i < $spinCount; ++$i) {
|
||||
$hashValue = hash($phpAlgorithm, $hashValue . pack('L', $i), true);
|
||||
}
|
||||
|
||||
return base64_encode($hashValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,684 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
class StringHelper
|
||||
{
|
||||
/**
|
||||
* Control characters array.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private static $controlCharacters = [];
|
||||
|
||||
/**
|
||||
* SYLK Characters array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $SYLKCharacters = [];
|
||||
|
||||
/**
|
||||
* Decimal separator.
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
private static $decimalSeparator;
|
||||
|
||||
/**
|
||||
* Thousands separator.
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
private static $thousandsSeparator;
|
||||
|
||||
/**
|
||||
* Currency code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $currencyCode;
|
||||
|
||||
/**
|
||||
* Is iconv extension avalable?
|
||||
*
|
||||
* @var ?bool
|
||||
*/
|
||||
private static $isIconvEnabled;
|
||||
|
||||
/**
|
||||
* iconv options.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $iconvOptions = '//IGNORE//TRANSLIT';
|
||||
|
||||
/**
|
||||
* Build control characters array.
|
||||
*/
|
||||
private static function buildControlCharacters(): void
|
||||
{
|
||||
for ($i = 0; $i <= 31; ++$i) {
|
||||
if ($i != 9 && $i != 10 && $i != 13) {
|
||||
$find = '_x' . sprintf('%04s', strtoupper(dechex($i))) . '_';
|
||||
$replace = chr($i);
|
||||
self::$controlCharacters[$find] = $replace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build SYLK characters array.
|
||||
*/
|
||||
private static function buildSYLKCharacters(): void
|
||||
{
|
||||
self::$SYLKCharacters = [
|
||||
"\x1B 0" => chr(0),
|
||||
"\x1B 1" => chr(1),
|
||||
"\x1B 2" => chr(2),
|
||||
"\x1B 3" => chr(3),
|
||||
"\x1B 4" => chr(4),
|
||||
"\x1B 5" => chr(5),
|
||||
"\x1B 6" => chr(6),
|
||||
"\x1B 7" => chr(7),
|
||||
"\x1B 8" => chr(8),
|
||||
"\x1B 9" => chr(9),
|
||||
"\x1B :" => chr(10),
|
||||
"\x1B ;" => chr(11),
|
||||
"\x1B <" => chr(12),
|
||||
"\x1B =" => chr(13),
|
||||
"\x1B >" => chr(14),
|
||||
"\x1B ?" => chr(15),
|
||||
"\x1B!0" => chr(16),
|
||||
"\x1B!1" => chr(17),
|
||||
"\x1B!2" => chr(18),
|
||||
"\x1B!3" => chr(19),
|
||||
"\x1B!4" => chr(20),
|
||||
"\x1B!5" => chr(21),
|
||||
"\x1B!6" => chr(22),
|
||||
"\x1B!7" => chr(23),
|
||||
"\x1B!8" => chr(24),
|
||||
"\x1B!9" => chr(25),
|
||||
"\x1B!:" => chr(26),
|
||||
"\x1B!;" => chr(27),
|
||||
"\x1B!<" => chr(28),
|
||||
"\x1B!=" => chr(29),
|
||||
"\x1B!>" => chr(30),
|
||||
"\x1B!?" => chr(31),
|
||||
"\x1B'?" => chr(127),
|
||||
"\x1B(0" => '€', // 128 in CP1252
|
||||
"\x1B(2" => '‚', // 130 in CP1252
|
||||
"\x1B(3" => 'ƒ', // 131 in CP1252
|
||||
"\x1B(4" => '„', // 132 in CP1252
|
||||
"\x1B(5" => '…', // 133 in CP1252
|
||||
"\x1B(6" => '†', // 134 in CP1252
|
||||
"\x1B(7" => '‡', // 135 in CP1252
|
||||
"\x1B(8" => 'ˆ', // 136 in CP1252
|
||||
"\x1B(9" => '‰', // 137 in CP1252
|
||||
"\x1B(:" => 'Š', // 138 in CP1252
|
||||
"\x1B(;" => '‹', // 139 in CP1252
|
||||
"\x1BNj" => 'Œ', // 140 in CP1252
|
||||
"\x1B(>" => 'Ž', // 142 in CP1252
|
||||
"\x1B)1" => '‘', // 145 in CP1252
|
||||
"\x1B)2" => '’', // 146 in CP1252
|
||||
"\x1B)3" => '“', // 147 in CP1252
|
||||
"\x1B)4" => '”', // 148 in CP1252
|
||||
"\x1B)5" => '•', // 149 in CP1252
|
||||
"\x1B)6" => '–', // 150 in CP1252
|
||||
"\x1B)7" => '—', // 151 in CP1252
|
||||
"\x1B)8" => '˜', // 152 in CP1252
|
||||
"\x1B)9" => '™', // 153 in CP1252
|
||||
"\x1B):" => 'š', // 154 in CP1252
|
||||
"\x1B);" => '›', // 155 in CP1252
|
||||
"\x1BNz" => 'œ', // 156 in CP1252
|
||||
"\x1B)>" => 'ž', // 158 in CP1252
|
||||
"\x1B)?" => 'Ÿ', // 159 in CP1252
|
||||
"\x1B*0" => ' ', // 160 in CP1252
|
||||
"\x1BN!" => '¡', // 161 in CP1252
|
||||
"\x1BN\"" => '¢', // 162 in CP1252
|
||||
"\x1BN#" => '£', // 163 in CP1252
|
||||
"\x1BN(" => '¤', // 164 in CP1252
|
||||
"\x1BN%" => '¥', // 165 in CP1252
|
||||
"\x1B*6" => '¦', // 166 in CP1252
|
||||
"\x1BN'" => '§', // 167 in CP1252
|
||||
"\x1BNH " => '¨', // 168 in CP1252
|
||||
"\x1BNS" => '©', // 169 in CP1252
|
||||
"\x1BNc" => 'ª', // 170 in CP1252
|
||||
"\x1BN+" => '«', // 171 in CP1252
|
||||
"\x1B*<" => '¬', // 172 in CP1252
|
||||
"\x1B*=" => '', // 173 in CP1252
|
||||
"\x1BNR" => '®', // 174 in CP1252
|
||||
"\x1B*?" => '¯', // 175 in CP1252
|
||||
"\x1BN0" => '°', // 176 in CP1252
|
||||
"\x1BN1" => '±', // 177 in CP1252
|
||||
"\x1BN2" => '²', // 178 in CP1252
|
||||
"\x1BN3" => '³', // 179 in CP1252
|
||||
"\x1BNB " => '´', // 180 in CP1252
|
||||
"\x1BN5" => 'µ', // 181 in CP1252
|
||||
"\x1BN6" => '¶', // 182 in CP1252
|
||||
"\x1BN7" => '·', // 183 in CP1252
|
||||
"\x1B+8" => '¸', // 184 in CP1252
|
||||
"\x1BNQ" => '¹', // 185 in CP1252
|
||||
"\x1BNk" => 'º', // 186 in CP1252
|
||||
"\x1BN;" => '»', // 187 in CP1252
|
||||
"\x1BN<" => '¼', // 188 in CP1252
|
||||
"\x1BN=" => '½', // 189 in CP1252
|
||||
"\x1BN>" => '¾', // 190 in CP1252
|
||||
"\x1BN?" => '¿', // 191 in CP1252
|
||||
"\x1BNAA" => 'À', // 192 in CP1252
|
||||
"\x1BNBA" => 'Á', // 193 in CP1252
|
||||
"\x1BNCA" => 'Â', // 194 in CP1252
|
||||
"\x1BNDA" => 'Ã', // 195 in CP1252
|
||||
"\x1BNHA" => 'Ä', // 196 in CP1252
|
||||
"\x1BNJA" => 'Å', // 197 in CP1252
|
||||
"\x1BNa" => 'Æ', // 198 in CP1252
|
||||
"\x1BNKC" => 'Ç', // 199 in CP1252
|
||||
"\x1BNAE" => 'È', // 200 in CP1252
|
||||
"\x1BNBE" => 'É', // 201 in CP1252
|
||||
"\x1BNCE" => 'Ê', // 202 in CP1252
|
||||
"\x1BNHE" => 'Ë', // 203 in CP1252
|
||||
"\x1BNAI" => 'Ì', // 204 in CP1252
|
||||
"\x1BNBI" => 'Í', // 205 in CP1252
|
||||
"\x1BNCI" => 'Î', // 206 in CP1252
|
||||
"\x1BNHI" => 'Ï', // 207 in CP1252
|
||||
"\x1BNb" => 'Ð', // 208 in CP1252
|
||||
"\x1BNDN" => 'Ñ', // 209 in CP1252
|
||||
"\x1BNAO" => 'Ò', // 210 in CP1252
|
||||
"\x1BNBO" => 'Ó', // 211 in CP1252
|
||||
"\x1BNCO" => 'Ô', // 212 in CP1252
|
||||
"\x1BNDO" => 'Õ', // 213 in CP1252
|
||||
"\x1BNHO" => 'Ö', // 214 in CP1252
|
||||
"\x1B-7" => '×', // 215 in CP1252
|
||||
"\x1BNi" => 'Ø', // 216 in CP1252
|
||||
"\x1BNAU" => 'Ù', // 217 in CP1252
|
||||
"\x1BNBU" => 'Ú', // 218 in CP1252
|
||||
"\x1BNCU" => 'Û', // 219 in CP1252
|
||||
"\x1BNHU" => 'Ü', // 220 in CP1252
|
||||
"\x1B-=" => 'Ý', // 221 in CP1252
|
||||
"\x1BNl" => 'Þ', // 222 in CP1252
|
||||
"\x1BN{" => 'ß', // 223 in CP1252
|
||||
"\x1BNAa" => 'à', // 224 in CP1252
|
||||
"\x1BNBa" => 'á', // 225 in CP1252
|
||||
"\x1BNCa" => 'â', // 226 in CP1252
|
||||
"\x1BNDa" => 'ã', // 227 in CP1252
|
||||
"\x1BNHa" => 'ä', // 228 in CP1252
|
||||
"\x1BNJa" => 'å', // 229 in CP1252
|
||||
"\x1BNq" => 'æ', // 230 in CP1252
|
||||
"\x1BNKc" => 'ç', // 231 in CP1252
|
||||
"\x1BNAe" => 'è', // 232 in CP1252
|
||||
"\x1BNBe" => 'é', // 233 in CP1252
|
||||
"\x1BNCe" => 'ê', // 234 in CP1252
|
||||
"\x1BNHe" => 'ë', // 235 in CP1252
|
||||
"\x1BNAi" => 'ì', // 236 in CP1252
|
||||
"\x1BNBi" => 'í', // 237 in CP1252
|
||||
"\x1BNCi" => 'î', // 238 in CP1252
|
||||
"\x1BNHi" => 'ï', // 239 in CP1252
|
||||
"\x1BNs" => 'ð', // 240 in CP1252
|
||||
"\x1BNDn" => 'ñ', // 241 in CP1252
|
||||
"\x1BNAo" => 'ò', // 242 in CP1252
|
||||
"\x1BNBo" => 'ó', // 243 in CP1252
|
||||
"\x1BNCo" => 'ô', // 244 in CP1252
|
||||
"\x1BNDo" => 'õ', // 245 in CP1252
|
||||
"\x1BNHo" => 'ö', // 246 in CP1252
|
||||
"\x1B/7" => '÷', // 247 in CP1252
|
||||
"\x1BNy" => 'ø', // 248 in CP1252
|
||||
"\x1BNAu" => 'ù', // 249 in CP1252
|
||||
"\x1BNBu" => 'ú', // 250 in CP1252
|
||||
"\x1BNCu" => 'û', // 251 in CP1252
|
||||
"\x1BNHu" => 'ü', // 252 in CP1252
|
||||
"\x1B/=" => 'ý', // 253 in CP1252
|
||||
"\x1BN|" => 'þ', // 254 in CP1252
|
||||
"\x1BNHy" => 'ÿ', // 255 in CP1252
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether iconv extension is available.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function getIsIconvEnabled()
|
||||
{
|
||||
if (isset(self::$isIconvEnabled)) {
|
||||
return self::$isIconvEnabled;
|
||||
}
|
||||
|
||||
// Assume no problems with iconv
|
||||
self::$isIconvEnabled = true;
|
||||
|
||||
// Fail if iconv doesn't exist
|
||||
if (!function_exists('iconv')) {
|
||||
self::$isIconvEnabled = false;
|
||||
} elseif (!@iconv('UTF-8', 'UTF-16LE', 'x')) {
|
||||
// Sometimes iconv is not working, and e.g. iconv('UTF-8', 'UTF-16LE', 'x') just returns false,
|
||||
self::$isIconvEnabled = false;
|
||||
} elseif (defined('PHP_OS') && @stristr(PHP_OS, 'AIX') && defined('ICONV_IMPL') && (@strcasecmp(ICONV_IMPL, 'unknown') == 0) && defined('ICONV_VERSION') && (@strcasecmp(ICONV_VERSION, 'unknown') == 0)) {
|
||||
// CUSTOM: IBM AIX iconv() does not work
|
||||
self::$isIconvEnabled = false;
|
||||
}
|
||||
|
||||
// Deactivate iconv default options if they fail (as seen on IMB i)
|
||||
if (self::$isIconvEnabled && !@iconv('UTF-8', 'UTF-16LE' . self::$iconvOptions, 'x')) {
|
||||
self::$iconvOptions = '';
|
||||
}
|
||||
|
||||
return self::$isIconvEnabled;
|
||||
}
|
||||
|
||||
private static function buildCharacterSets(): void
|
||||
{
|
||||
if (empty(self::$controlCharacters)) {
|
||||
self::buildControlCharacters();
|
||||
}
|
||||
|
||||
if (empty(self::$SYLKCharacters)) {
|
||||
self::buildSYLKCharacters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from OpenXML escaped control character to PHP control character.
|
||||
*
|
||||
* Excel 2007 team:
|
||||
* ----------------
|
||||
* That's correct, control characters are stored directly in the shared-strings table.
|
||||
* We do encode characters that cannot be represented in XML using the following escape sequence:
|
||||
* _xHHHH_ where H represents a hexadecimal character in the character's value...
|
||||
* So you could end up with something like _x0008_ in a string (either in a cell value (<v>)
|
||||
* element or in the shared string <t> element.
|
||||
*
|
||||
* @param string $textValue Value to unescape
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function controlCharacterOOXML2PHP($textValue)
|
||||
{
|
||||
self::buildCharacterSets();
|
||||
|
||||
return str_replace(array_keys(self::$controlCharacters), array_values(self::$controlCharacters), $textValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from PHP control character to OpenXML escaped control character.
|
||||
*
|
||||
* Excel 2007 team:
|
||||
* ----------------
|
||||
* That's correct, control characters are stored directly in the shared-strings table.
|
||||
* We do encode characters that cannot be represented in XML using the following escape sequence:
|
||||
* _xHHHH_ where H represents a hexadecimal character in the character's value...
|
||||
* So you could end up with something like _x0008_ in a string (either in a cell value (<v>)
|
||||
* element or in the shared string <t> element.
|
||||
*
|
||||
* @param string $textValue Value to escape
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function controlCharacterPHP2OOXML($textValue)
|
||||
{
|
||||
self::buildCharacterSets();
|
||||
|
||||
return str_replace(array_values(self::$controlCharacters), array_keys(self::$controlCharacters), $textValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to sanitize UTF8, replacing invalid sequences with Unicode substitution characters.
|
||||
*/
|
||||
public static function sanitizeUTF8(string $textValue): string
|
||||
{
|
||||
$textValue = str_replace(["\xef\xbf\xbe", "\xef\xbf\xbf"], "\xef\xbf\xbd", $textValue);
|
||||
$subst = mb_substitute_character(); // default is question mark
|
||||
mb_substitute_character(65533); // Unicode substitution character
|
||||
// Phpstan does not think this can return false.
|
||||
$returnValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8');
|
||||
mb_substitute_character(/** @scrutinizer ignore-type */ $subst);
|
||||
|
||||
return self::returnString($returnValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strictly to satisfy Scrutinizer.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function returnString($value): string
|
||||
{
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string contains UTF8 data.
|
||||
*/
|
||||
public static function isUTF8(string $textValue): bool
|
||||
{
|
||||
return $textValue === self::sanitizeUTF8($textValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a numeric value as a string for output in various output writers forcing
|
||||
* point as decimal separator in case locale is other than English.
|
||||
*
|
||||
* @param float|int|string $numericValue
|
||||
*/
|
||||
public static function formatNumber($numericValue): string
|
||||
{
|
||||
if (is_float($numericValue)) {
|
||||
return str_replace(',', '.', (string) $numericValue);
|
||||
}
|
||||
|
||||
return (string) $numericValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UTF-8 string into BIFF8 Unicode string data (8-bit string length)
|
||||
* Writes the string using uncompressed notation, no rich text, no Asian phonetics
|
||||
* If mbstring extension is not available, ASCII is assumed, and compressed notation is used
|
||||
* although this will give wrong results for non-ASCII strings
|
||||
* see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3.
|
||||
*
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
* @param mixed[] $arrcRuns Details of rich text runs in $value
|
||||
*/
|
||||
public static function UTF8toBIFF8UnicodeShort(string $textValue, array $arrcRuns = []): string
|
||||
{
|
||||
// character count
|
||||
$ln = self::countCharacters($textValue, 'UTF-8');
|
||||
// option flags
|
||||
if (empty($arrcRuns)) {
|
||||
$data = pack('CC', $ln, 0x0001);
|
||||
// characters
|
||||
$data .= self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8');
|
||||
} else {
|
||||
$data = pack('vC', $ln, 0x09);
|
||||
$data .= pack('v', count($arrcRuns));
|
||||
// characters
|
||||
$data .= self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8');
|
||||
foreach ($arrcRuns as $cRun) {
|
||||
$data .= pack('v', $cRun['strlen']);
|
||||
$data .= pack('v', $cRun['fontidx']);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UTF-8 string into BIFF8 Unicode string data (16-bit string length)
|
||||
* Writes the string using uncompressed notation, no rich text, no Asian phonetics
|
||||
* If mbstring extension is not available, ASCII is assumed, and compressed notation is used
|
||||
* although this will give wrong results for non-ASCII strings
|
||||
* see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3.
|
||||
*
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function UTF8toBIFF8UnicodeLong(string $textValue): string
|
||||
{
|
||||
// character count
|
||||
$ln = self::countCharacters($textValue, 'UTF-8');
|
||||
|
||||
// characters
|
||||
$chars = self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8');
|
||||
|
||||
return pack('vC', $ln, 0x0001) . $chars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string from one encoding to another.
|
||||
*
|
||||
* @param string $to Encoding to convert to, e.g. 'UTF-8'
|
||||
* @param string $from Encoding to convert from, e.g. 'UTF-16LE'
|
||||
*/
|
||||
public static function convertEncoding(string $textValue, string $to, string $from): string
|
||||
{
|
||||
if (self::getIsIconvEnabled()) {
|
||||
$result = iconv($from, $to . self::$iconvOptions, $textValue);
|
||||
if (false !== $result) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return self::returnString(mb_convert_encoding($textValue, $to, $from));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get character count.
|
||||
*
|
||||
* @param string $encoding Encoding
|
||||
*
|
||||
* @return int Character count
|
||||
*/
|
||||
public static function countCharacters(string $textValue, string $encoding = 'UTF-8'): int
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
* @param int $offset Start offset
|
||||
* @param ?int $length Maximum number of characters in substring
|
||||
*/
|
||||
public static function substring(string $textValue, int $offset, ?int $length = 0): string
|
||||
{
|
||||
return mb_substr($textValue, $offset, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a UTF-8 encoded string to upper case.
|
||||
*
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function strToUpper(string $textValue): string
|
||||
{
|
||||
return mb_convert_case($textValue, MB_CASE_UPPER, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a UTF-8 encoded string to lower case.
|
||||
*
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function strToLower(string $textValue): string
|
||||
{
|
||||
return mb_convert_case($textValue, MB_CASE_LOWER, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a UTF-8 encoded string to title/proper case
|
||||
* (uppercase every first character in each word, lower case all other characters).
|
||||
*
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function strToTitle(string $textValue): string
|
||||
{
|
||||
return mb_convert_case($textValue, MB_CASE_TITLE, 'UTF-8');
|
||||
}
|
||||
|
||||
public static function mbIsUpper(string $character): bool
|
||||
{
|
||||
return mb_strtolower($character, 'UTF-8') !== $character;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a UTF-8 string into an array of individual characters.
|
||||
*/
|
||||
public static function mbStrSplit(string $string): array
|
||||
{
|
||||
// Split at all position not after the start: ^
|
||||
// and not before the end: $
|
||||
$split = preg_split('/(?<!^)(?!$)/u', $string);
|
||||
|
||||
return ($split === false) ? [] : $split;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the case of a string, so that all uppercase characters become lowercase
|
||||
* and all lowercase characters become uppercase.
|
||||
*
|
||||
* @param string $textValue UTF-8 encoded string
|
||||
*/
|
||||
public static function strCaseReverse(string $textValue): string
|
||||
{
|
||||
$characters = self::mbStrSplit($textValue);
|
||||
foreach ($characters as &$character) {
|
||||
if (self::mbIsUpper($character)) {
|
||||
$character = mb_strtolower($character, 'UTF-8');
|
||||
} else {
|
||||
$character = mb_strtoupper($character, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
return implode('', $characters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the decimal separator. If it has not yet been set explicitly, try to obtain number
|
||||
* formatting information from locale.
|
||||
*/
|
||||
public static function getDecimalSeparator(): string
|
||||
{
|
||||
if (!isset(self::$decimalSeparator)) {
|
||||
$localeconv = localeconv();
|
||||
self::$decimalSeparator = ($localeconv['decimal_point'] != '')
|
||||
? $localeconv['decimal_point'] : $localeconv['mon_decimal_point'];
|
||||
|
||||
if (self::$decimalSeparator == '') {
|
||||
// Default to .
|
||||
self::$decimalSeparator = '.';
|
||||
}
|
||||
}
|
||||
|
||||
return self::$decimalSeparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the decimal separator. Only used by NumberFormat::toFormattedString()
|
||||
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
|
||||
*
|
||||
* @param string $separator Character for decimal separator
|
||||
*/
|
||||
public static function setDecimalSeparator(string $separator): void
|
||||
{
|
||||
self::$decimalSeparator = $separator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thousands separator. If it has not yet been set explicitly, try to obtain number
|
||||
* formatting information from locale.
|
||||
*/
|
||||
public static function getThousandsSeparator(): string
|
||||
{
|
||||
if (!isset(self::$thousandsSeparator)) {
|
||||
$localeconv = localeconv();
|
||||
self::$thousandsSeparator = ($localeconv['thousands_sep'] != '')
|
||||
? $localeconv['thousands_sep'] : $localeconv['mon_thousands_sep'];
|
||||
|
||||
if (self::$thousandsSeparator == '') {
|
||||
// Default to .
|
||||
self::$thousandsSeparator = ',';
|
||||
}
|
||||
}
|
||||
|
||||
return self::$thousandsSeparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the thousands separator. Only used by NumberFormat::toFormattedString()
|
||||
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
|
||||
*
|
||||
* @param string $separator Character for thousands separator
|
||||
*/
|
||||
public static function setThousandsSeparator(string $separator): void
|
||||
{
|
||||
self::$thousandsSeparator = $separator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currency code. If it has not yet been set explicitly, try to obtain the
|
||||
* symbol information from locale.
|
||||
*/
|
||||
public static function getCurrencyCode(): string
|
||||
{
|
||||
if (!empty(self::$currencyCode)) {
|
||||
return self::$currencyCode;
|
||||
}
|
||||
self::$currencyCode = '$';
|
||||
$localeconv = localeconv();
|
||||
if (!empty($localeconv['currency_symbol'])) {
|
||||
self::$currencyCode = $localeconv['currency_symbol'];
|
||||
|
||||
return self::$currencyCode;
|
||||
}
|
||||
if (!empty($localeconv['int_curr_symbol'])) {
|
||||
self::$currencyCode = $localeconv['int_curr_symbol'];
|
||||
|
||||
return self::$currencyCode;
|
||||
}
|
||||
|
||||
return self::$currencyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the currency code. Only used by NumberFormat::toFormattedString()
|
||||
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
|
||||
*
|
||||
* @param string $currencyCode Character for currency code
|
||||
*/
|
||||
public static function setCurrencyCode(string $currencyCode): void
|
||||
{
|
||||
self::$currencyCode = $currencyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SYLK encoded string to UTF-8.
|
||||
*
|
||||
* @param string $textValue SYLK encoded string
|
||||
*
|
||||
* @return string UTF-8 encoded string
|
||||
*/
|
||||
public static function SYLKtoUTF8(string $textValue): string
|
||||
{
|
||||
self::buildCharacterSets();
|
||||
|
||||
// If there is no escape character in the string there is nothing to do
|
||||
if (strpos($textValue, '') === false) {
|
||||
return $textValue;
|
||||
}
|
||||
|
||||
foreach (self::$SYLKCharacters as $k => $v) {
|
||||
$textValue = str_replace($k, $v, $textValue);
|
||||
}
|
||||
|
||||
return $textValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve any leading numeric part of a string, or return the full string if no leading numeric
|
||||
* (handles basic integer or float, but not exponent or non decimal).
|
||||
*
|
||||
* @param string $textValue
|
||||
*
|
||||
* @return mixed string or only the leading numeric part of the string
|
||||
*/
|
||||
public static function testStringAsNumeric($textValue)
|
||||
{
|
||||
if (is_numeric($textValue)) {
|
||||
return $textValue;
|
||||
}
|
||||
$v = (float) $textValue;
|
||||
|
||||
return (is_numeric(substr($textValue, 0, strlen((string) $v)))) ? $v : $textValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use DateTimeZone;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
|
||||
|
||||
class TimeZone
|
||||
{
|
||||
/**
|
||||
* Default Timezone used for date/time conversions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $timezone = 'UTC';
|
||||
|
||||
/**
|
||||
* Validate a Timezone name.
|
||||
*
|
||||
* @param string $timezoneName Time zone (e.g. 'Europe/London')
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
private static function validateTimeZone(string $timezoneName): bool
|
||||
{
|
||||
return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Default Timezone used for date/time conversions.
|
||||
*
|
||||
* @param string $timezoneName Time zone (e.g. 'Europe/London')
|
||||
*
|
||||
* @return bool Success or failure
|
||||
*/
|
||||
public static function setTimeZone(string $timezoneName): bool
|
||||
{
|
||||
if (self::validateTimeZone($timezoneName)) {
|
||||
self::$timezone = $timezoneName;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Default Timezone used for date/time conversions.
|
||||
*
|
||||
* @return string Timezone (e.g. 'Europe/London')
|
||||
*/
|
||||
public static function getTimeZone(): string
|
||||
{
|
||||
return self::$timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Timezone offset used for date/time conversions to/from UST
|
||||
* This requires both the timezone and the calculated date/time to allow for local DST.
|
||||
*
|
||||
* @param ?string $timezoneName The timezone for finding the adjustment to UST
|
||||
* @param float|int $timestamp PHP date/time value
|
||||
*
|
||||
* @return int Number of seconds for timezone adjustment
|
||||
*/
|
||||
public static function getTimeZoneAdjustment(?string $timezoneName, $timestamp): int
|
||||
{
|
||||
$timezoneName = $timezoneName ?? self::$timezone;
|
||||
$dtobj = Date::dateTimeFromTimestamp("$timestamp");
|
||||
if (!self::validateTimeZone($timezoneName)) {
|
||||
throw new PhpSpreadsheetException("Invalid timezone $timezoneName");
|
||||
}
|
||||
$dtobj->setTimeZone(new DateTimeZone($timezoneName));
|
||||
|
||||
return $dtobj->getOffset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
|
||||
|
||||
abstract class BestFit
|
||||
{
|
||||
/**
|
||||
* Indicator flag for a calculation error.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $error = false;
|
||||
|
||||
/**
|
||||
* Algorithm type to use for best-fit.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bestFitType = 'undetermined';
|
||||
|
||||
/**
|
||||
* Number of entries in the sets of x- and y-value arrays.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $valueCount = 0;
|
||||
|
||||
/**
|
||||
* X-value dataseries of values.
|
||||
*
|
||||
* @var float[]
|
||||
*/
|
||||
protected $xValues = [];
|
||||
|
||||
/**
|
||||
* Y-value dataseries of values.
|
||||
*
|
||||
* @var float[]
|
||||
*/
|
||||
protected $yValues = [];
|
||||
|
||||
/**
|
||||
* Flag indicating whether values should be adjusted to Y=0.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $adjustToZero = false;
|
||||
|
||||
/**
|
||||
* Y-value series of best-fit values.
|
||||
*
|
||||
* @var float[]
|
||||
*/
|
||||
protected $yBestFitValues = [];
|
||||
|
||||
/** @var float */
|
||||
protected $goodnessOfFit = 1;
|
||||
|
||||
/** @var float */
|
||||
protected $stdevOfResiduals = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $covariance = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $correlation = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $SSRegression = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $SSResiduals = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $DFResiduals = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $f = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $slope = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $slopeSE = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $intersect = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $intersectSE = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $xOffset = 0;
|
||||
|
||||
/** @var float */
|
||||
protected $yOffset = 0;
|
||||
|
||||
/** @return bool */
|
||||
public function getError()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getBestFitType()
|
||||
{
|
||||
return $this->bestFitType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Y-Value for a specified value of X.
|
||||
*
|
||||
* @param float $xValue X-Value
|
||||
*
|
||||
* @return float Y-Value
|
||||
*/
|
||||
abstract public function getValueOfYForX($xValue);
|
||||
|
||||
/**
|
||||
* Return the X-Value for a specified value of Y.
|
||||
*
|
||||
* @param float $yValue Y-Value
|
||||
*
|
||||
* @return float X-Value
|
||||
*/
|
||||
abstract public function getValueOfXForY($yValue);
|
||||
|
||||
/**
|
||||
* Return the original set of X-Values.
|
||||
*
|
||||
* @return float[] X-Values
|
||||
*/
|
||||
public function getXValues()
|
||||
{
|
||||
return $this->xValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Equation of the best-fit line.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getEquation($dp = 0);
|
||||
|
||||
/**
|
||||
* Return the Slope of the line.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getSlope($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->slope, $dp);
|
||||
}
|
||||
|
||||
return $this->slope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the standard error of the Slope.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getSlopeSE($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->slopeSE, $dp);
|
||||
}
|
||||
|
||||
return $this->slopeSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Value of X where it intersects Y = 0.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getIntersect($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->intersect, $dp);
|
||||
}
|
||||
|
||||
return $this->intersect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the standard error of the Intersect.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getIntersectSE($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->intersectSE, $dp);
|
||||
}
|
||||
|
||||
return $this->intersectSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the goodness of fit for this regression.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to return
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getGoodnessOfFit($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->goodnessOfFit, $dp);
|
||||
}
|
||||
|
||||
return $this->goodnessOfFit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the goodness of fit for this regression.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to return
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getGoodnessOfFitPercent($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->goodnessOfFit * 100, $dp);
|
||||
}
|
||||
|
||||
return $this->goodnessOfFit * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the standard deviation of the residuals for this regression.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to return
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getStdevOfResiduals($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->stdevOfResiduals, $dp);
|
||||
}
|
||||
|
||||
return $this->stdevOfResiduals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dp Number of places of decimal precision to return
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getSSRegression($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->SSRegression, $dp);
|
||||
}
|
||||
|
||||
return $this->SSRegression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dp Number of places of decimal precision to return
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getSSResiduals($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->SSResiduals, $dp);
|
||||
}
|
||||
|
||||
return $this->SSResiduals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dp Number of places of decimal precision to return
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getDFResiduals($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->DFResiduals, $dp);
|
||||
}
|
||||
|
||||
return $this->DFResiduals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dp Number of places of decimal precision to return
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getF($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->f, $dp);
|
||||
}
|
||||
|
||||
return $this->f;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dp Number of places of decimal precision to return
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getCovariance($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->covariance, $dp);
|
||||
}
|
||||
|
||||
return $this->covariance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dp Number of places of decimal precision to return
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getCorrelation($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round($this->correlation, $dp);
|
||||
}
|
||||
|
||||
return $this->correlation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float[]
|
||||
*/
|
||||
public function getYBestFitValues()
|
||||
{
|
||||
return $this->yBestFitValues;
|
||||
}
|
||||
|
||||
/** @var mixed */
|
||||
private static $scrutinizerZeroPointZero = 0.0;
|
||||
|
||||
/**
|
||||
* @param mixed $x
|
||||
* @param mixed $y
|
||||
*/
|
||||
private static function scrutinizerLooseCompare($x, $y): bool
|
||||
{
|
||||
return $x == $y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $sumX
|
||||
* @param float $sumY
|
||||
* @param float $sumX2
|
||||
* @param float $sumY2
|
||||
* @param float $sumXY
|
||||
* @param float $meanX
|
||||
* @param float $meanY
|
||||
* @param bool|int $const
|
||||
*/
|
||||
protected function calculateGoodnessOfFit($sumX, $sumY, $sumX2, $sumY2, $sumXY, $meanX, $meanY, $const): void
|
||||
{
|
||||
$SSres = $SScov = $SStot = $SSsex = 0.0;
|
||||
foreach ($this->xValues as $xKey => $xValue) {
|
||||
$bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
|
||||
|
||||
$SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY);
|
||||
if ($const === true) {
|
||||
$SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY);
|
||||
} else {
|
||||
$SStot += $this->yValues[$xKey] * $this->yValues[$xKey];
|
||||
}
|
||||
$SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY);
|
||||
if ($const === true) {
|
||||
$SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX);
|
||||
} else {
|
||||
$SSsex += $this->xValues[$xKey] * $this->xValues[$xKey];
|
||||
}
|
||||
}
|
||||
|
||||
$this->SSResiduals = $SSres;
|
||||
$this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0);
|
||||
|
||||
if ($this->DFResiduals == 0.0) {
|
||||
$this->stdevOfResiduals = 0.0;
|
||||
} else {
|
||||
$this->stdevOfResiduals = sqrt($SSres / $this->DFResiduals);
|
||||
}
|
||||
// Scrutinizer thinks $SSres == $SStot is always true. It is wrong.
|
||||
if ($SStot == self::$scrutinizerZeroPointZero || self::scrutinizerLooseCompare($SSres, $SStot)) {
|
||||
$this->goodnessOfFit = 1;
|
||||
} else {
|
||||
$this->goodnessOfFit = 1 - ($SSres / $SStot);
|
||||
}
|
||||
|
||||
$this->SSRegression = $this->goodnessOfFit * $SStot;
|
||||
$this->covariance = $SScov / $this->valueCount;
|
||||
$this->correlation = ($this->valueCount * $sumXY - $sumX * $sumY) / sqrt(($this->valueCount * $sumX2 - $sumX ** 2) * ($this->valueCount * $sumY2 - $sumY ** 2));
|
||||
$this->slopeSE = $this->stdevOfResiduals / sqrt($SSsex);
|
||||
$this->intersectSE = $this->stdevOfResiduals * sqrt(1 / ($this->valueCount - ($sumX * $sumX) / $sumX2));
|
||||
if ($this->SSResiduals != 0.0) {
|
||||
if ($this->DFResiduals == 0.0) {
|
||||
$this->f = 0.0;
|
||||
} else {
|
||||
$this->f = $this->SSRegression / ($this->SSResiduals / $this->DFResiduals);
|
||||
}
|
||||
} else {
|
||||
if ($this->DFResiduals == 0.0) {
|
||||
$this->f = 0.0;
|
||||
} else {
|
||||
$this->f = $this->SSRegression / $this->DFResiduals;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return float|int */
|
||||
private function sumSquares(array $values)
|
||||
{
|
||||
return array_sum(
|
||||
array_map(
|
||||
function ($value) {
|
||||
return $value ** 2;
|
||||
},
|
||||
$values
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float[] $yValues
|
||||
* @param float[] $xValues
|
||||
*/
|
||||
protected function leastSquareFit(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
// calculate sums
|
||||
$sumValuesX = array_sum($xValues);
|
||||
$sumValuesY = array_sum($yValues);
|
||||
$meanValueX = $sumValuesX / $this->valueCount;
|
||||
$meanValueY = $sumValuesY / $this->valueCount;
|
||||
$sumSquaresX = $this->sumSquares($xValues);
|
||||
$sumSquaresY = $this->sumSquares($yValues);
|
||||
$mBase = $mDivisor = 0.0;
|
||||
$xy_sum = 0.0;
|
||||
for ($i = 0; $i < $this->valueCount; ++$i) {
|
||||
$xy_sum += $xValues[$i] * $yValues[$i];
|
||||
|
||||
if ($const === true) {
|
||||
$mBase += ($xValues[$i] - $meanValueX) * ($yValues[$i] - $meanValueY);
|
||||
$mDivisor += ($xValues[$i] - $meanValueX) * ($xValues[$i] - $meanValueX);
|
||||
} else {
|
||||
$mBase += $xValues[$i] * $yValues[$i];
|
||||
$mDivisor += $xValues[$i] * $xValues[$i];
|
||||
}
|
||||
}
|
||||
|
||||
// calculate slope
|
||||
$this->slope = $mBase / $mDivisor;
|
||||
|
||||
// calculate intersect
|
||||
$this->intersect = ($const === true) ? $meanValueY - ($this->slope * $meanValueX) : 0.0;
|
||||
|
||||
$this->calculateGoodnessOfFit($sumValuesX, $sumValuesY, $sumSquaresX, $sumSquaresY, $xy_sum, $meanValueX, $meanValueY, $const);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the regression.
|
||||
*
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
*/
|
||||
public function __construct($yValues, $xValues = [])
|
||||
{
|
||||
// Calculate number of points
|
||||
$yValueCount = count($yValues);
|
||||
$xValueCount = count($xValues);
|
||||
|
||||
// Define X Values if necessary
|
||||
if ($xValueCount === 0) {
|
||||
$xValues = range(1, $yValueCount);
|
||||
} elseif ($yValueCount !== $xValueCount) {
|
||||
// Ensure both arrays of points are the same size
|
||||
$this->error = true;
|
||||
}
|
||||
|
||||
$this->valueCount = $yValueCount;
|
||||
$this->xValues = $xValues;
|
||||
$this->yValues = $yValues;
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
|
||||
|
||||
class ExponentialBestFit extends BestFit
|
||||
{
|
||||
/**
|
||||
* Algorithm type to use for best-fit
|
||||
* (Name of this Trend class).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bestFitType = 'exponential';
|
||||
|
||||
/**
|
||||
* Return the Y-Value for a specified value of X.
|
||||
*
|
||||
* @param float $xValue X-Value
|
||||
*
|
||||
* @return float Y-Value
|
||||
*/
|
||||
public function getValueOfYForX($xValue)
|
||||
{
|
||||
return $this->getIntersect() * $this->getSlope() ** ($xValue - $this->xOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the X-Value for a specified value of Y.
|
||||
*
|
||||
* @param float $yValue Y-Value
|
||||
*
|
||||
* @return float X-Value
|
||||
*/
|
||||
public function getValueOfXForY($yValue)
|
||||
{
|
||||
return log(($yValue + $this->yOffset) / $this->getIntersect()) / log($this->getSlope());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Equation of the best-fit line.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEquation($dp = 0)
|
||||
{
|
||||
$slope = $this->getSlope($dp);
|
||||
$intersect = $this->getIntersect($dp);
|
||||
|
||||
return 'Y = ' . $intersect . ' * ' . $slope . '^X';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Slope of the line.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getSlope($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round(exp($this->slope), $dp);
|
||||
}
|
||||
|
||||
return exp($this->slope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Value of X where it intersects Y = 0.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getIntersect($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round(exp($this->intersect), $dp);
|
||||
}
|
||||
|
||||
return exp($this->intersect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
*/
|
||||
private function exponentialRegression(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
$adjustedYValues = array_map(
|
||||
function ($value) {
|
||||
return ($value < 0.0) ? 0 - log(abs($value)) : log($value);
|
||||
},
|
||||
$yValues
|
||||
);
|
||||
|
||||
$this->leastSquareFit($adjustedYValues, $xValues, $const);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
* @param bool $const
|
||||
*/
|
||||
public function __construct($yValues, $xValues = [], $const = true)
|
||||
{
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
if (!$this->error) {
|
||||
$this->exponentialRegression($yValues, $xValues, (bool) $const);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
|
||||
|
||||
class LinearBestFit extends BestFit
|
||||
{
|
||||
/**
|
||||
* Algorithm type to use for best-fit
|
||||
* (Name of this Trend class).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bestFitType = 'linear';
|
||||
|
||||
/**
|
||||
* Return the Y-Value for a specified value of X.
|
||||
*
|
||||
* @param float $xValue X-Value
|
||||
*
|
||||
* @return float Y-Value
|
||||
*/
|
||||
public function getValueOfYForX($xValue)
|
||||
{
|
||||
return $this->getIntersect() + $this->getSlope() * $xValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the X-Value for a specified value of Y.
|
||||
*
|
||||
* @param float $yValue Y-Value
|
||||
*
|
||||
* @return float X-Value
|
||||
*/
|
||||
public function getValueOfXForY($yValue)
|
||||
{
|
||||
return ($yValue - $this->getIntersect()) / $this->getSlope();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Equation of the best-fit line.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEquation($dp = 0)
|
||||
{
|
||||
$slope = $this->getSlope($dp);
|
||||
$intersect = $this->getIntersect($dp);
|
||||
|
||||
return 'Y = ' . $intersect . ' + ' . $slope . ' * X';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
*/
|
||||
private function linearRegression(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
$this->leastSquareFit($yValues, $xValues, $const);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
* @param bool $const
|
||||
*/
|
||||
public function __construct($yValues, $xValues = [], $const = true)
|
||||
{
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
if (!$this->error) {
|
||||
$this->linearRegression($yValues, $xValues, (bool) $const);
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
|
||||
|
||||
class LogarithmicBestFit extends BestFit
|
||||
{
|
||||
/**
|
||||
* Algorithm type to use for best-fit
|
||||
* (Name of this Trend class).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bestFitType = 'logarithmic';
|
||||
|
||||
/**
|
||||
* Return the Y-Value for a specified value of X.
|
||||
*
|
||||
* @param float $xValue X-Value
|
||||
*
|
||||
* @return float Y-Value
|
||||
*/
|
||||
public function getValueOfYForX($xValue)
|
||||
{
|
||||
return $this->getIntersect() + $this->getSlope() * log($xValue - $this->xOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the X-Value for a specified value of Y.
|
||||
*
|
||||
* @param float $yValue Y-Value
|
||||
*
|
||||
* @return float X-Value
|
||||
*/
|
||||
public function getValueOfXForY($yValue)
|
||||
{
|
||||
return exp(($yValue - $this->getIntersect()) / $this->getSlope());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Equation of the best-fit line.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEquation($dp = 0)
|
||||
{
|
||||
$slope = $this->getSlope($dp);
|
||||
$intersect = $this->getIntersect($dp);
|
||||
|
||||
return 'Y = ' . $slope . ' * log(' . $intersect . ' * X)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
*/
|
||||
private function logarithmicRegression(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
$adjustedYValues = array_map(
|
||||
function ($value) {
|
||||
return ($value < 0.0) ? 0 - log(abs($value)) : log($value);
|
||||
},
|
||||
$yValues
|
||||
);
|
||||
|
||||
$this->leastSquareFit($adjustedYValues, $xValues, $const);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
* @param bool $const
|
||||
*/
|
||||
public function __construct($yValues, $xValues = [], $const = true)
|
||||
{
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
if (!$this->error) {
|
||||
$this->logarithmicRegression($yValues, $xValues, (bool) $const);
|
||||
}
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
|
||||
|
||||
use Matrix\Matrix;
|
||||
|
||||
// Phpstan and Scrutinizer seem to have legitimate complaints.
|
||||
// $this->slope is specified where an array is expected in several places.
|
||||
// But it seems that it should always be float.
|
||||
// This code is probably not exercised at all in unit tests.
|
||||
class PolynomialBestFit extends BestFit
|
||||
{
|
||||
/**
|
||||
* Algorithm type to use for best-fit
|
||||
* (Name of this Trend class).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bestFitType = 'polynomial';
|
||||
|
||||
/**
|
||||
* Polynomial order.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $order = 0;
|
||||
|
||||
/**
|
||||
* Return the order of this polynomial.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Y-Value for a specified value of X.
|
||||
*
|
||||
* @param float $xValue X-Value
|
||||
*
|
||||
* @return float Y-Value
|
||||
*/
|
||||
public function getValueOfYForX($xValue)
|
||||
{
|
||||
$retVal = $this->getIntersect();
|
||||
$slope = $this->getSlope();
|
||||
// Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
|
||||
// @phpstan-ignore-next-line
|
||||
foreach ($slope as $key => $value) {
|
||||
if ($value != 0.0) {
|
||||
$retVal += $value * $xValue ** ($key + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the X-Value for a specified value of Y.
|
||||
*
|
||||
* @param float $yValue Y-Value
|
||||
*
|
||||
* @return float X-Value
|
||||
*/
|
||||
public function getValueOfXForY($yValue)
|
||||
{
|
||||
return ($yValue - $this->getIntersect()) / $this->getSlope();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Equation of the best-fit line.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEquation($dp = 0)
|
||||
{
|
||||
$slope = $this->getSlope($dp);
|
||||
$intersect = $this->getIntersect($dp);
|
||||
|
||||
$equation = 'Y = ' . $intersect;
|
||||
// Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
|
||||
// @phpstan-ignore-next-line
|
||||
foreach ($slope as $key => $value) {
|
||||
if ($value != 0.0) {
|
||||
$equation .= ' + ' . $value . ' * X';
|
||||
if ($key > 0) {
|
||||
$equation .= '^' . ($key + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $equation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Slope of the line.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getSlope($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
$coefficients = [];
|
||||
// Scrutinizer is correct - $this->slope is float, not array.
|
||||
//* @phpstan-ignore-next-line
|
||||
foreach ($this->slope as $coefficient) {
|
||||
$coefficients[] = round($coefficient, $dp);
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return $coefficients;
|
||||
}
|
||||
|
||||
return $this->slope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dp
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCoefficients($dp = 0)
|
||||
{
|
||||
// Phpstan and Scrutinizer are both correct - getSlope returns float, not array.
|
||||
// @phpstan-ignore-next-line
|
||||
return array_merge([$this->getIntersect($dp)], $this->getSlope($dp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param int $order Order of Polynomial for this regression
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
*/
|
||||
private function polynomialRegression($order, $yValues, $xValues): void
|
||||
{
|
||||
// calculate sums
|
||||
$x_sum = array_sum($xValues);
|
||||
$y_sum = array_sum($yValues);
|
||||
$xx_sum = $xy_sum = $yy_sum = 0;
|
||||
for ($i = 0; $i < $this->valueCount; ++$i) {
|
||||
$xy_sum += $xValues[$i] * $yValues[$i];
|
||||
$xx_sum += $xValues[$i] * $xValues[$i];
|
||||
$yy_sum += $yValues[$i] * $yValues[$i];
|
||||
}
|
||||
/*
|
||||
* This routine uses logic from the PHP port of polyfit version 0.1
|
||||
* written by Michael Bommarito and Paul Meagher
|
||||
*
|
||||
* The function fits a polynomial function of order $order through
|
||||
* a series of x-y data points using least squares.
|
||||
*
|
||||
*/
|
||||
$A = [];
|
||||
$B = [];
|
||||
for ($i = 0; $i < $this->valueCount; ++$i) {
|
||||
for ($j = 0; $j <= $order; ++$j) {
|
||||
$A[$i][$j] = $xValues[$i] ** $j;
|
||||
}
|
||||
}
|
||||
for ($i = 0; $i < $this->valueCount; ++$i) {
|
||||
$B[$i] = [$yValues[$i]];
|
||||
}
|
||||
$matrixA = new Matrix($A);
|
||||
$matrixB = new Matrix($B);
|
||||
$C = $matrixA->solve($matrixB);
|
||||
|
||||
$coefficients = [];
|
||||
for ($i = 0; $i < $C->rows; ++$i) {
|
||||
$r = $C->getValue($i + 1, 1); // row and column are origin-1
|
||||
if (abs($r) <= 10 ** (-9)) {
|
||||
$r = 0;
|
||||
}
|
||||
$coefficients[] = $r;
|
||||
}
|
||||
|
||||
$this->intersect = array_shift($coefficients);
|
||||
// Phpstan (and maybe Scrutinizer) are correct
|
||||
//* @phpstan-ignore-next-line
|
||||
$this->slope = $coefficients;
|
||||
|
||||
$this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, 0, 0, 0);
|
||||
foreach ($this->xValues as $xKey => $xValue) {
|
||||
$this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param int $order Order of Polynomial for this regression
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
*/
|
||||
public function __construct($order, $yValues, $xValues = [])
|
||||
{
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
if (!$this->error) {
|
||||
if ($order < $this->valueCount) {
|
||||
$this->bestFitType .= '_' . $order;
|
||||
$this->order = $order;
|
||||
$this->polynomialRegression($order, $yValues, $xValues);
|
||||
if (($this->getGoodnessOfFit() < 0.0) || ($this->getGoodnessOfFit() > 1.0)) {
|
||||
$this->error = true;
|
||||
}
|
||||
} else {
|
||||
$this->error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
|
||||
|
||||
class PowerBestFit extends BestFit
|
||||
{
|
||||
/**
|
||||
* Algorithm type to use for best-fit
|
||||
* (Name of this Trend class).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bestFitType = 'power';
|
||||
|
||||
/**
|
||||
* Return the Y-Value for a specified value of X.
|
||||
*
|
||||
* @param float $xValue X-Value
|
||||
*
|
||||
* @return float Y-Value
|
||||
*/
|
||||
public function getValueOfYForX($xValue)
|
||||
{
|
||||
return $this->getIntersect() * ($xValue - $this->xOffset) ** $this->getSlope();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the X-Value for a specified value of Y.
|
||||
*
|
||||
* @param float $yValue Y-Value
|
||||
*
|
||||
* @return float X-Value
|
||||
*/
|
||||
public function getValueOfXForY($yValue)
|
||||
{
|
||||
return (($yValue + $this->yOffset) / $this->getIntersect()) ** (1 / $this->getSlope());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Equation of the best-fit line.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEquation($dp = 0)
|
||||
{
|
||||
$slope = $this->getSlope($dp);
|
||||
$intersect = $this->getIntersect($dp);
|
||||
|
||||
return 'Y = ' . $intersect . ' * X^' . $slope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Value of X where it intersects Y = 0.
|
||||
*
|
||||
* @param int $dp Number of places of decimal precision to display
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getIntersect($dp = 0)
|
||||
{
|
||||
if ($dp != 0) {
|
||||
return round(exp($this->intersect), $dp);
|
||||
}
|
||||
|
||||
return exp($this->intersect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
*/
|
||||
private function powerRegression(array $yValues, array $xValues, bool $const): void
|
||||
{
|
||||
$adjustedYValues = array_map(
|
||||
function ($value) {
|
||||
return ($value < 0.0) ? 0 - log(abs($value)) : log($value);
|
||||
},
|
||||
$yValues
|
||||
);
|
||||
$adjustedXValues = array_map(
|
||||
function ($value) {
|
||||
return ($value < 0.0) ? 0 - log(abs($value)) : log($value);
|
||||
},
|
||||
$xValues
|
||||
);
|
||||
|
||||
$this->leastSquareFit($adjustedYValues, $adjustedXValues, $const);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the regression and calculate the goodness of fit for a set of X and Y data values.
|
||||
*
|
||||
* @param float[] $yValues The set of Y-values for this regression
|
||||
* @param float[] $xValues The set of X-values for this regression
|
||||
* @param bool $const
|
||||
*/
|
||||
public function __construct($yValues, $xValues = [], $const = true)
|
||||
{
|
||||
parent::__construct($yValues, $xValues);
|
||||
|
||||
if (!$this->error) {
|
||||
$this->powerRegression($yValues, $xValues, (bool) $const);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
|
||||
|
||||
class Trend
|
||||
{
|
||||
const TREND_LINEAR = 'Linear';
|
||||
const TREND_LOGARITHMIC = 'Logarithmic';
|
||||
const TREND_EXPONENTIAL = 'Exponential';
|
||||
const TREND_POWER = 'Power';
|
||||
const TREND_POLYNOMIAL_2 = 'Polynomial_2';
|
||||
const TREND_POLYNOMIAL_3 = 'Polynomial_3';
|
||||
const TREND_POLYNOMIAL_4 = 'Polynomial_4';
|
||||
const TREND_POLYNOMIAL_5 = 'Polynomial_5';
|
||||
const TREND_POLYNOMIAL_6 = 'Polynomial_6';
|
||||
const TREND_BEST_FIT = 'Bestfit';
|
||||
const TREND_BEST_FIT_NO_POLY = 'Bestfit_no_Polynomials';
|
||||
|
||||
/**
|
||||
* Names of the best-fit Trend analysis methods.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private static $trendTypes = [
|
||||
self::TREND_LINEAR,
|
||||
self::TREND_LOGARITHMIC,
|
||||
self::TREND_EXPONENTIAL,
|
||||
self::TREND_POWER,
|
||||
];
|
||||
|
||||
/**
|
||||
* Names of the best-fit Trend polynomial orders.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private static $trendTypePolynomialOrders = [
|
||||
self::TREND_POLYNOMIAL_2,
|
||||
self::TREND_POLYNOMIAL_3,
|
||||
self::TREND_POLYNOMIAL_4,
|
||||
self::TREND_POLYNOMIAL_5,
|
||||
self::TREND_POLYNOMIAL_6,
|
||||
];
|
||||
|
||||
/**
|
||||
* Cached results for each method when trying to identify which provides the best fit.
|
||||
*
|
||||
* @var BestFit[]
|
||||
*/
|
||||
private static $trendCache = [];
|
||||
|
||||
/**
|
||||
* @param string $trendType
|
||||
* @param array $yValues
|
||||
* @param array $xValues
|
||||
* @param bool $const
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function calculate($trendType = self::TREND_BEST_FIT, $yValues = [], $xValues = [], $const = true)
|
||||
{
|
||||
// Calculate number of points in each dataset
|
||||
$nY = count($yValues);
|
||||
$nX = count($xValues);
|
||||
|
||||
// Define X Values if necessary
|
||||
if ($nX === 0) {
|
||||
$xValues = range(1, $nY);
|
||||
} elseif ($nY !== $nX) {
|
||||
// Ensure both arrays of points are the same size
|
||||
trigger_error('Trend(): Number of elements in coordinate arrays do not match.', E_USER_ERROR);
|
||||
}
|
||||
|
||||
$key = md5($trendType . $const . serialize($yValues) . serialize($xValues));
|
||||
// Determine which Trend method has been requested
|
||||
switch ($trendType) {
|
||||
// Instantiate and return the class for the requested Trend method
|
||||
case self::TREND_LINEAR:
|
||||
case self::TREND_LOGARITHMIC:
|
||||
case self::TREND_EXPONENTIAL:
|
||||
case self::TREND_POWER:
|
||||
if (!isset(self::$trendCache[$key])) {
|
||||
$className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendType . 'BestFit';
|
||||
self::$trendCache[$key] = new $className($yValues, $xValues, $const);
|
||||
}
|
||||
|
||||
return self::$trendCache[$key];
|
||||
case self::TREND_POLYNOMIAL_2:
|
||||
case self::TREND_POLYNOMIAL_3:
|
||||
case self::TREND_POLYNOMIAL_4:
|
||||
case self::TREND_POLYNOMIAL_5:
|
||||
case self::TREND_POLYNOMIAL_6:
|
||||
if (!isset(self::$trendCache[$key])) {
|
||||
$order = (int) substr($trendType, -1);
|
||||
self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues);
|
||||
}
|
||||
|
||||
return self::$trendCache[$key];
|
||||
case self::TREND_BEST_FIT:
|
||||
case self::TREND_BEST_FIT_NO_POLY:
|
||||
// If the request is to determine the best fit regression, then we test each Trend line in turn
|
||||
// Start by generating an instance of each available Trend method
|
||||
$bestFit = [];
|
||||
$bestFitValue = [];
|
||||
foreach (self::$trendTypes as $trendMethod) {
|
||||
$className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendType . 'BestFit';
|
||||
//* @phpstan-ignore-next-line
|
||||
$bestFit[$trendMethod] = new $className($yValues, $xValues, $const);
|
||||
$bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit();
|
||||
}
|
||||
if ($trendType != self::TREND_BEST_FIT_NO_POLY) {
|
||||
foreach (self::$trendTypePolynomialOrders as $trendMethod) {
|
||||
$order = (int) substr($trendMethod, -1);
|
||||
$bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues);
|
||||
if ($bestFit[$trendMethod]->getError()) {
|
||||
unset($bestFit[$trendMethod]);
|
||||
} else {
|
||||
$bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Determine which of our Trend lines is the best fit, and then we return the instance of that Trend class
|
||||
arsort($bestFitValue);
|
||||
$bestFitType = key($bestFitValue);
|
||||
|
||||
return $bestFit[$bestFitType];
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Shared;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
|
||||
|
||||
class XMLWriter extends \XMLWriter
|
||||
{
|
||||
/** @var bool */
|
||||
public static $debugEnabled = false;
|
||||
|
||||
/** Temporary storage method */
|
||||
const STORAGE_MEMORY = 1;
|
||||
const STORAGE_DISK = 2;
|
||||
|
||||
/**
|
||||
* Temporary filename.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $tempFileName = '';
|
||||
|
||||
/**
|
||||
* Create a new XMLWriter instance.
|
||||
*
|
||||
* @param int $temporaryStorage Temporary storage location
|
||||
* @param string $temporaryStorageFolder Temporary storage folder
|
||||
*/
|
||||
public function __construct($temporaryStorage = self::STORAGE_MEMORY, $temporaryStorageFolder = null)
|
||||
{
|
||||
// Open temporary storage
|
||||
if ($temporaryStorage == self::STORAGE_MEMORY) {
|
||||
$this->openMemory();
|
||||
} else {
|
||||
// Create temporary filename
|
||||
if ($temporaryStorageFolder === null) {
|
||||
$temporaryStorageFolder = File::sysGetTempDir();
|
||||
}
|
||||
$this->tempFileName = (string) @tempnam($temporaryStorageFolder, 'xml');
|
||||
|
||||
// Open storage
|
||||
if (empty($this->tempFileName) || $this->openUri($this->tempFileName) === false) {
|
||||
// Fallback to memory...
|
||||
$this->openMemory();
|
||||
}
|
||||
}
|
||||
|
||||
// Set default values
|
||||
if (self::$debugEnabled) {
|
||||
$this->setIndent(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
// Unlink temporary files
|
||||
// There is nothing reasonable to do if unlink fails.
|
||||
if ($this->tempFileName != '') {
|
||||
/** @scrutinizer ignore-unhandled */
|
||||
@unlink($this->tempFileName);
|
||||
}
|
||||
}
|
||||
|
||||
public function __wakeup(): void
|
||||
{
|
||||
$this->tempFileName = '';
|
||||
|
||||
throw new SpreadsheetException('Unserialize not permitted');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get written data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
if ($this->tempFileName == '') {
|
||||
return $this->outputMemory(true);
|
||||
}
|
||||
$this->flush();
|
||||
|
||||
return file_get_contents($this->tempFileName) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper method for writeRaw.
|
||||
*
|
||||
* @param null|string|string[] $rawTextData
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function writeRawData($rawTextData)
|
||||
{
|
||||
if (is_array($rawTextData)) {
|
||||
$rawTextData = implode("\n", $rawTextData);
|
||||
}
|
||||
|
||||
return $this->writeRaw(htmlspecialchars($rawTextData ?? ''));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user