diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/CodePage.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/CodePage.php new file mode 100644 index 00000000000..8718a6135ed --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/CodePage.php @@ -0,0 +1,114 @@ + '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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php new file mode 100644 index 00000000000..4f196731131 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php @@ -0,0 +1,556 @@ + '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); + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Drawing.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Drawing.php new file mode 100644 index 00000000000..f69310fc617 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Drawing.php @@ -0,0 +1,177 @@ +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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher.php new file mode 100644 index 00000000000..466e7e8254c --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher.php @@ -0,0 +1,64 @@ +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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DgContainer.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DgContainer.php new file mode 100644 index 00000000000..51c6860cbe7 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DgContainer.php @@ -0,0 +1,65 @@ +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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer.php new file mode 100644 index 00000000000..260df9cd4c0 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer.php @@ -0,0 +1,75 @@ +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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer/SpContainer.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer/SpContainer.php new file mode 100644 index 00000000000..8a81ff57974 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DgContainer/SpgrContainer/SpContainer.php @@ -0,0 +1,369 @@ +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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer.php new file mode 100644 index 00000000000..ba5e7980b29 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer.php @@ -0,0 +1,175 @@ +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. [ => , ...]. + * + * @param array $IDCLs + */ + public function setIDCLs($IDCLs): void + { + $this->IDCLs = $IDCLs; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php new file mode 100644 index 00000000000..7203b66bea2 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer.php @@ -0,0 +1,32 @@ +BSECollection[] = $BSE; + $BSE->setParent($this); + } + + /** + * Get the collection of BLIP Store Entries. + * + * @return BstoreContainer\BSE[] + */ + public function getBSECollection() + { + return $this->BSECollection; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php new file mode 100644 index 00000000000..328ac6b6c24 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php @@ -0,0 +1,88 @@ +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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php new file mode 100644 index 00000000000..03b261f8feb --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE/Blip.php @@ -0,0 +1,58 @@ +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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php new file mode 100644 index 00000000000..737a6eb5912 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php @@ -0,0 +1,197 @@ +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); + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php new file mode 100644 index 00000000000..90c1992a3ab --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php @@ -0,0 +1,675 @@ + [ + '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: + * . + * + * @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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/IntOrFloat.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/IntOrFloat.php new file mode 100644 index 00000000000..060f09c8831 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/IntOrFloat.php @@ -0,0 +1,21 @@ + '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 . + * + * 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); + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php new file mode 100644 index 00000000000..c6c198e203c --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php @@ -0,0 +1,684 @@ + 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 () + * element or in the shared string 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 () + * element or in the shared string 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('/(? $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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php new file mode 100644 index 00000000000..324e3424dd2 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php @@ -0,0 +1,77 @@ +setTimeZone(new DateTimeZone($timezoneName)); + + return $dtobj->getOffset(); + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/BestFit.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/BestFit.php new file mode 100644 index 00000000000..a8d7c93b7d6 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/BestFit.php @@ -0,0 +1,501 @@ +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; + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php new file mode 100644 index 00000000000..eb8cd746d36 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php @@ -0,0 +1,119 @@ +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); + } + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php new file mode 100644 index 00000000000..65d6b4ff44d --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php @@ -0,0 +1,80 @@ +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); + } + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php new file mode 100644 index 00000000000..2366dc636aa --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php @@ -0,0 +1,87 @@ +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); + } + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php new file mode 100644 index 00000000000..222a4230045 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php @@ -0,0 +1,219 @@ +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; + } + } + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php new file mode 100644 index 00000000000..cafd01158e9 --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php @@ -0,0 +1,109 @@ +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); + } + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php new file mode 100644 index 00000000000..117848c778b --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php @@ -0,0 +1,130 @@ +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; + } + } +} diff --git a/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php new file mode 100644 index 00000000000..d9f403d7ada --- /dev/null +++ b/lib/phpspreadsheet/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php @@ -0,0 +1,104 @@ +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 ?? '')); + } +}