Include vendor directory

This commit is contained in:
2025-10-24 12:35:37 +02:00
parent b81164279f
commit 43bc416554
712 changed files with 151811 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
class CodePage
{
public const DEFAULT_CODE_PAGE = 'CP1252';
/** @var array<int, array<int, string>|string> */
private static array $pageArray = [
0 => 'CP1252', // CodePage is not always correctly set when the xls file was saved by Apple's Numbers program
367 => 'ASCII', // ASCII
437 => 'CP437', // OEM US
//720 => 'notsupported', // OEM Arabic
737 => 'CP737', // OEM Greek
775 => 'CP775', // OEM Baltic
850 => 'CP850', // OEM Latin I
852 => 'CP852', // OEM Latin II (Central European)
855 => 'CP855', // OEM Cyrillic
857 => 'CP857', // OEM Turkish
858 => 'CP858', // OEM Multilingual Latin I with Euro
860 => 'CP860', // OEM Portugese
861 => 'CP861', // OEM Icelandic
862 => 'CP862', // OEM Hebrew
863 => 'CP863', // OEM Canadian (French)
864 => 'CP864', // OEM Arabic
865 => 'CP865', // OEM Nordic
866 => 'CP866', // OEM Cyrillic (Russian)
869 => 'CP869', // OEM Greek (Modern)
874 => 'CP874', // ANSI Thai
932 => 'CP932', // ANSI Japanese Shift-JIS
936 => 'CP936', // ANSI Chinese Simplified GBK
949 => 'CP949', // ANSI Korean (Wansung)
950 => 'CP950', // ANSI Chinese Traditional BIG5
1200 => 'UTF-16LE', // UTF-16 (BIFF8)
1250 => 'CP1250', // ANSI Latin II (Central European)
1251 => 'CP1251', // ANSI Cyrillic
1252 => 'CP1252', // ANSI Latin I (BIFF4-BIFF7)
1253 => 'CP1253', // ANSI Greek
1254 => 'CP1254', // ANSI Turkish
1255 => 'CP1255', // ANSI Hebrew
1256 => 'CP1256', // ANSI Arabic
1257 => 'CP1257', // ANSI Baltic
1258 => 'CP1258', // ANSI Vietnamese
1361 => 'CP1361', // ANSI Korean (Johab)
10000 => 'MAC', // Apple Roman
10001 => 'CP932', // Macintosh Japanese
10002 => 'CP950', // Macintosh Chinese Traditional
10003 => 'CP1361', // Macintosh Korean
10004 => 'MACARABIC', // Apple Arabic
10005 => 'MACHEBREW', // Apple Hebrew
10006 => 'MACGREEK', // Macintosh Greek
10007 => 'MACCYRILLIC', // Macintosh Cyrillic
10008 => 'CP936', // Macintosh - Simplified Chinese (GB 2312)
10010 => 'MACROMANIA', // Macintosh Romania
10017 => 'MACUKRAINE', // Macintosh Ukraine
10021 => 'MACTHAI', // Macintosh Thai
10029 => ['MACCENTRALEUROPE', 'MAC-CENTRALEUROPE'], // Macintosh Central Europe
10079 => 'MACICELAND', // Macintosh Icelandic
10081 => 'MACTURKISH', // Macintosh Turkish
10082 => 'MACCROATIAN', // Macintosh Croatian
21010 => 'UTF-16LE', // UTF-16 (BIFF8) This isn't correct, but some Excel writer libraries erroneously use Codepage 21010 for UTF-16LE
32768 => 'MAC', // Apple Roman
//32769 => 'unsupported', // ANSI Latin I (BIFF2-BIFF3)
65000 => 'UTF-7', // Unicode (UTF-7)
65001 => 'UTF-8', // Unicode (UTF-8)
99999 => ['unsupported'], // Unicode (UTF-8)
];
public static function validate(string $codePage): bool
{
return in_array($codePage, self::$pageArray, true);
}
/**
* Convert Microsoft Code Page Identifier to Code Page Name which iconv
* and mbstring understands.
*
* @param int $codePage Microsoft Code Page Indentifier
*
* @return string Code Page Name
*/
public static function numberToName(int $codePage): string
{
if (array_key_exists($codePage, self::$pageArray)) {
$value = self::$pageArray[$codePage];
if (is_array($value)) {
foreach ($value as $encoding) {
if (@iconv('UTF-8', $encoding, ' ') !== false) {
self::$pageArray[$codePage] = $encoding;
return $encoding;
}
}
throw new PhpSpreadsheetException("Code page $codePage not implemented on this system.");
} else {
return $value;
}
}
if ($codePage == 720 || $codePage == 32769) {
throw new PhpSpreadsheetException("Code page $codePage not supported."); // OEM Arabic
}
throw new PhpSpreadsheetException('Unknown codepage: ' . $codePage);
}
/** @return array<int, array<int, string>|string> */
public static function getEncodings(): array
{
return self::$pageArray;
}
}

View File

@@ -0,0 +1,561 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
class Date
{
/** constants */
const CALENDAR_WINDOWS_1900 = 1900; // Base date of 1st Jan 1900 = 1.0
const CALENDAR_MAC_1904 = 1904; // Base date of 2nd Jan 1904 = 1.0
/**
* Names of the months of the year, indexed by shortname
* Planned usage for locale settings.
*
* @var string[]
*/
public static array $monthNames = [
'Jan' => 'January',
'Feb' => 'February',
'Mar' => 'March',
'Apr' => 'April',
'May' => 'May',
'Jun' => 'June',
'Jul' => 'July',
'Aug' => 'August',
'Sep' => 'September',
'Oct' => 'October',
'Nov' => 'November',
'Dec' => 'December',
];
/**
* @var string[]
*/
public static array $numberSuffixes = [
'st',
'nd',
'rd',
'th',
];
/**
* Base calendar year to use for calculations
* Value is either CALENDAR_WINDOWS_1900 (1900) or CALENDAR_MAC_1904 (1904).
*/
protected static int $excelCalendar = self::CALENDAR_WINDOWS_1900;
/**
* Default timezone to use for DateTime objects.
*/
protected static ?DateTimeZone $defaultTimeZone = null;
/**
* 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(?int $baseYear): bool
{
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(): int
{
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): bool
{
try {
$timeZone = self::validateTimeZone($timeZone);
self::$defaultTimeZone = $timeZone;
$retval = true;
} catch (PhpSpreadsheetException) {
$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): ?DateTimeZone
{
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.
*/
public static function convertIsoDate(mixed $value): float|int
{
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 = self::dateTimeToExcel($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(float|int $excelTimestamp, null|DateTimeZone|string $timeZone = null): DateTime
{
$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);
}
if (is_int($excelTimestamp)) {
if ($excelTimestamp >= 0) {
return $baseDate->modify("+ $excelTimestamp days");
}
return $baseDate->modify("$excelTimestamp days");
}
$days = floor($excelTimestamp);
$partDay = $excelTimestamp - $days;
$hms = 86400 * $partDay;
$microseconds = (int) round(fmod($hms, 1) * 1000000);
$hms = (int) floor($hms);
$hours = intdiv($hms, 3600);
$hms -= $hours * 3600;
$minutes = intdiv($hms, 60);
$seconds = $hms % 60;
if ($days >= 0) {
$days = '+' . $days;
}
$interval = $days . ' days';
return $baseDate->modify($interval)
->setTime($hours, $minutes, $seconds, $microseconds);
}
/**
* 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): int
{
$dto = self::excelToDateTimeObject($excelTimestamp, $timeZone);
self::roundMicroseconds($dto);
return (int) $dto->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(mixed $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): float
{
$seconds = (float) sprintf('%d.%06d', $dateValue->format('s'), $dateValue->format('u'));
return self::formattedPHPToExcel(
(int) $dateValue->format('Y'),
(int) $dateValue->format('m'),
(int) $dateValue->format('d'),
(int) $dateValue->format('H'),
(int) $dateValue->format('i'),
$seconds
);
}
/**
* 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): bool|float
{
if (!is_numeric($unixTimestamp)) {
return false;
}
return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp));
}
/**
* formattedPHPToExcel.
*
* @return float Excel date/time value
*/
public static function formattedPHPToExcel(int $year, int $month, int $day, int $hours = 0, int $minutes = 0, float|int $seconds = 0): float
{
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?
*/
public static function isDateTime(Cell $cell, mixed $value = null, bool $dateWithoutTimeOkay = true): bool
{
$result = false;
$worksheet = $cell->getWorksheetOrNull();
$spreadsheet = ($worksheet === null) ? null : $worksheet->getParent();
if ($worksheet !== null && $spreadsheet !== null) {
$index = $spreadsheet->getActiveSheetIndex();
$selected = $worksheet->getSelectedCells();
try {
if ($value === null) {
$value = Functions::flattenSingleValue(
$cell->getCalculatedValue()
);
}
$result = is_numeric($value)
&& self::isDateTimeFormat(
$worksheet->getStyle(
$cell->getCoordinate()
)->getNumberFormat(),
$dateWithoutTimeOkay
);
} catch (Exception) {
// 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?
*/
public static function isDateTimeFormat(NumberFormat $excelFormatCode, bool $dateWithoutTimeOkay = true): bool
{
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?
*/
public static function isDateTimeFormatCode(string $excelFormatCode, bool $dateWithoutTimeOkay = true): bool
{
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
$excelFormatCode = (string) NumberFormat::convertSystemFormats($excelFormatCode);
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 ((str_starts_with($excelFormatCode, '_')) || (str_starts_with($excelFormatCode, '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 (str_contains($excelFormatCode, '-00000')) {
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 (str_contains($excelFormatCode, '"')) {
$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(string $dateValue): bool|float
{
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}([.]\d+)?)?)?$/iu', $dateValue)) {
return false;
}
$dateValueNew = DateTimeExcel\DateValue::fromString($dateValue);
if (!is_float($dateValueNew)) {
return false;
}
if (str_contains($dateValue, ':')) {
$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(string $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(string $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);
}
/**
* Round the given DateTime object to seconds.
*/
public static function roundMicroseconds(DateTime $dti): void
{
$microseconds = (int) $dti->format('u');
$rounded = (int) round($microseconds, -6);
$modify = $rounded - $microseconds;
if ($modify !== 0) {
$dti->modify(($modify > 0 ? '+' : '') . $modify . ' microseconds');
}
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use SimpleXMLElement;
class Drawing
{
/**
* Convert pixels to EMU.
*
* @param int $pixelValue Value in pixels
*
* @return float|int Value in EMU
*/
public static function pixelsToEMU(int $pixelValue): int|float
{
return $pixelValue * 9525;
}
/**
* Convert EMU to pixels.
*
* @param int|SimpleXMLElement $emuValue Value in EMU
*
* @return int Value in pixels
*/
public static function EMUToPixels($emuValue): int
{
$emuValue = (int) $emuValue;
if ($emuValue != 0) {
return (int) round($emuValue / 9525);
}
return 0;
}
/**
* Convert pixels to column width. Exact algorithm not known.
* By inspection of a real Excel file using Calibri 11, one finds 1000px ~ 142.85546875
* This gives a conversion factor of 7. Also, we assume that pixels and font size are proportional.
*
* @param int $pixelValue Value in pixels
*
* @return float|int Value in cell dimension
*/
public static function pixelsToCellDimension(int $pixelValue, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont): int|float
{
// Font name and size
$name = $defaultFont->getName();
$size = $defaultFont->getSize();
if (isset(Font::DEFAULT_COLUMN_WIDTHS[$name][$size])) {
// Exact width can be determined
return $pixelValue * Font::DEFAULT_COLUMN_WIDTHS[$name][$size]['width']
/ Font::DEFAULT_COLUMN_WIDTHS[$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::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width']
/ Font::DEFAULT_COLUMN_WIDTHS['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(float $cellWidth, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont): int
{
// Font name and size
$name = $defaultFont->getName();
$size = $defaultFont->getSize();
if (isset(Font::DEFAULT_COLUMN_WIDTHS[$name][$size])) {
// Exact width can be determined
$colWidth = $cellWidth * Font::DEFAULT_COLUMN_WIDTHS[$name][$size]['px']
/ Font::DEFAULT_COLUMN_WIDTHS[$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::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px']
/ Font::DEFAULT_COLUMN_WIDTHS['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(int $pixelValue): float
{
return $pixelValue * 0.75;
}
/**
* Convert points to pixels.
*
* @param float|int $pointValue Value in points
*
* @return int Value in pixels
*/
public static function pointsToPixels($pointValue): int
{
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(int $degrees): int
{
return (int) round($degrees * 60000);
}
/**
* Convert angle to degrees.
*
* @param int|SimpleXMLElement $angle Angle
*
* @return int Degrees
*/
public static function angleToDegrees($angle): int
{
$angle = (int) $angle;
if ($angle != 0) {
return (int) round($angle / 60000);
}
return 0;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
class Escher
{
/**
* Drawing Group Container.
*/
private ?Escher\DggContainer $dggContainer = null;
/**
* Drawing Container.
*/
private ?Escher\DgContainer $dgContainer = null;
/**
* Get Drawing Group Container.
*/
public function getDggContainer(): ?Escher\DggContainer
{
return $this->dggContainer;
}
/**
* Set Drawing Group Container.
*/
public function setDggContainer(Escher\DggContainer $dggContainer): Escher\DggContainer
{
return $this->dggContainer = $dggContainer;
}
/**
* Get Drawing Container.
*/
public function getDgContainer(): ?Escher\DgContainer
{
return $this->dgContainer;
}
/**
* Set Drawing Container.
*/
public function setDgContainer(Escher\DgContainer $dgContainer): Escher\DgContainer
{
return $this->dgContainer = $dgContainer;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Escher;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
class DgContainer
{
/**
* Drawing index, 1-based.
*/
private ?int $dgId = null;
/**
* Last shape index in this drawing.
*/
private ?int $lastSpId = null;
private ?SpgrContainer $spgrContainer = null;
public function getDgId(): ?int
{
return $this->dgId;
}
public function setDgId(int $value): void
{
$this->dgId = $value;
}
public function getLastSpId(): ?int
{
return $this->lastSpId;
}
public function setLastSpId(int $value): void
{
$this->lastSpId = $value;
}
public function getSpgrContainer(): ?SpgrContainer
{
return $this->spgrContainer;
}
public function getSpgrContainerOrThrow(): SpgrContainer
{
if ($this->spgrContainer !== null) {
return $this->spgrContainer;
}
throw new SpreadsheetException('spgrContainer is unexpectedly null');
}
public function setSpgrContainer(SpgrContainer $spgrContainer): SpgrContainer
{
return $this->spgrContainer = $spgrContainer;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer;
class SpgrContainer
{
/**
* Parent Shape Group Container.
*/
private ?self $parent = null;
/**
* Shape Container collection.
*
* @var mixed[]
*/
private array $children = [];
/**
* Set parent Shape Group Container.
*/
public function setParent(?self $parent): void
{
$this->parent = $parent;
}
/**
* Get the parent Shape Group Container if any.
*/
public function getParent(): ?self
{
return $this->parent;
}
/**
* Add a child. This will be either spgrContainer or spContainer.
*
* @param SpgrContainer|SpgrContainer\SpContainer $child child to be added
*/
public function addChild(mixed $child): void
{
$this->children[] = $child;
$child->setParent($this);
}
/**
* Get collection of Shape Containers.
*
* @return mixed[]
*/
public function getChildren(): array
{
return $this->children;
}
/**
* Recursively get all spContainers within this spgrContainer.
*
* @return SpgrContainer\SpContainer[]
*/
public function getAllSpContainers(): array
{
$allSpContainers = [];
foreach ($this->children as $child) {
if ($child instanceof self) {
$allSpContainers = array_merge($allSpContainers, $child->getAllSpContainers());
} else {
$allSpContainers[] = $child;
}
}
/** @var SpgrContainer\SpContainer[] $allSpContainers */
return $allSpContainers;
}
}

View File

@@ -0,0 +1,304 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
class SpContainer
{
/**
* Parent Shape Group Container.
*/
private SpgrContainer $parent;
/**
* Is this a group shape?
*/
private bool $spgr = false;
/**
* Shape type.
*/
private int $spType;
/**
* Shape flag.
*/
private int $spFlag;
/**
* Shape index (usually group shape has index 0, and the rest: 1,2,3...).
*/
private int $spId;
/**
* Array of options.
*
* @var mixed[]
*/
private array $OPT = [];
/**
* Cell coordinates of upper-left corner of shape, e.g. 'A1'.
*/
private string $startCoordinates = '';
/**
* Horizontal offset of upper-left corner of shape measured in 1/1024 of column width.
*/
private int|float $startOffsetX;
/**
* Vertical offset of upper-left corner of shape measured in 1/256 of row height.
*/
private int|float $startOffsetY;
/**
* Cell coordinates of bottom-right corner of shape, e.g. 'B2'.
*/
private string $endCoordinates;
/**
* Horizontal offset of bottom-right corner of shape measured in 1/1024 of column width.
*/
private int|float $endOffsetX;
/**
* Vertical offset of bottom-right corner of shape measured in 1/256 of row height.
*/
private int|float $endOffsetY;
/**
* Set parent Shape Group Container.
*/
public function setParent(SpgrContainer $parent): void
{
$this->parent = $parent;
}
/**
* Get the parent Shape Group Container.
*/
public function getParent(): SpgrContainer
{
return $this->parent;
}
/**
* Set whether this is a group shape.
*/
public function setSpgr(bool $value): void
{
$this->spgr = $value;
}
/**
* Get whether this is a group shape.
*/
public function getSpgr(): bool
{
return $this->spgr;
}
/**
* Set the shape type.
*/
public function setSpType(int $value): void
{
$this->spType = $value;
}
/**
* Get the shape type.
*/
public function getSpType(): int
{
return $this->spType;
}
/**
* Set the shape flag.
*/
public function setSpFlag(int $value): void
{
$this->spFlag = $value;
}
/**
* Get the shape flag.
*/
public function getSpFlag(): int
{
return $this->spFlag;
}
/**
* Set the shape index.
*/
public function setSpId(int $value): void
{
$this->spId = $value;
}
/**
* Get the shape index.
*/
public function getSpId(): int
{
return $this->spId;
}
/**
* Set an option for the Shape Group Container.
*
* @param int $property The number specifies the option
*/
public function setOPT(int $property, mixed $value): void
{
$this->OPT[$property] = $value;
}
/**
* Get an option for the Shape Group Container.
*
* @param int $property The number specifies the option
*/
public function getOPT(int $property): mixed
{
if (isset($this->OPT[$property])) {
return $this->OPT[$property];
}
return null;
}
/**
* Get the collection of options.
*
* @return mixed[]
*/
public function getOPTCollection(): array
{
return $this->OPT;
}
/**
* Set cell coordinates of upper-left corner of shape.
*
* @param string $value eg: 'A1'
*/
public function setStartCoordinates(string $value): void
{
$this->startCoordinates = $value;
}
/**
* Get cell coordinates of upper-left corner of shape.
*/
public function getStartCoordinates(): string
{
return $this->startCoordinates;
}
/**
* Set offset in x-direction of upper-left corner of shape measured in 1/1024 of column width.
*/
public function setStartOffsetX(int|float $startOffsetX): void
{
$this->startOffsetX = $startOffsetX;
}
/**
* Get offset in x-direction of upper-left corner of shape measured in 1/1024 of column width.
*/
public function getStartOffsetX(): int|float
{
return $this->startOffsetX;
}
/**
* Set offset in y-direction of upper-left corner of shape measured in 1/256 of row height.
*/
public function setStartOffsetY(int|float $startOffsetY): void
{
$this->startOffsetY = $startOffsetY;
}
/**
* Get offset in y-direction of upper-left corner of shape measured in 1/256 of row height.
*/
public function getStartOffsetY(): int|float
{
return $this->startOffsetY;
}
/**
* Set cell coordinates of bottom-right corner of shape.
*
* @param string $value eg: 'A1'
*/
public function setEndCoordinates(string $value): void
{
$this->endCoordinates = $value;
}
/**
* Get cell coordinates of bottom-right corner of shape.
*/
public function getEndCoordinates(): string
{
return $this->endCoordinates;
}
/**
* Set offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width.
*/
public function setEndOffsetX(int|float $endOffsetX): void
{
$this->endOffsetX = $endOffsetX;
}
/**
* Get offset in x-direction of bottom-right corner of shape measured in 1/1024 of column width.
*/
public function getEndOffsetX(): int|float
{
return $this->endOffsetX;
}
/**
* Set offset in y-direction of bottom-right corner of shape measured in 1/256 of row height.
*/
public function setEndOffsetY(int|float $endOffsetY): void
{
$this->endOffsetY = $endOffsetY;
}
/**
* Get offset in y-direction of bottom-right corner of shape measured in 1/256 of row height.
*/
public function getEndOffsetY(): int|float
{
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(): int
{
$nestingLevel = 0;
$parent = $this->getParent();
while ($parent instanceof SpgrContainer) {
++$nestingLevel;
$parent = $parent->getParent();
}
return $nestingLevel;
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Escher;
class DggContainer
{
/**
* Maximum shape index of all shapes in all drawings increased by one.
*/
private int $spIdMax;
/**
* Total number of drawings saved.
*/
private int $cDgSaved;
/**
* Total number of shapes saved (including group shapes).
*/
private int $cSpSaved;
/**
* BLIP Store Container.
*/
private ?DggContainer\BstoreContainer $bstoreContainer = null;
/**
* Array of options for the drawing group.
*
* @var mixed[]
*/
private array $OPT = [];
/**
* Array of identifier clusters containg information about the maximum shape identifiers.
*
* @var mixed[]
*/
private array $IDCLs = [];
/**
* Get maximum shape index of all shapes in all drawings (plus one).
*/
public function getSpIdMax(): int
{
return $this->spIdMax;
}
/**
* Set maximum shape index of all shapes in all drawings (plus one).
*/
public function setSpIdMax(int $value): void
{
$this->spIdMax = $value;
}
/**
* Get total number of drawings saved.
*/
public function getCDgSaved(): int
{
return $this->cDgSaved;
}
/**
* Set total number of drawings saved.
*/
public function setCDgSaved(int $value): void
{
$this->cDgSaved = $value;
}
/**
* Get total number of shapes saved (including group shapes).
*/
public function getCSpSaved(): int
{
return $this->cSpSaved;
}
/**
* Set total number of shapes saved (including group shapes).
*/
public function setCSpSaved(int $value): void
{
$this->cSpSaved = $value;
}
/**
* Get BLIP Store Container.
*/
public function getBstoreContainer(): ?DggContainer\BstoreContainer
{
return $this->bstoreContainer;
}
/**
* Set BLIP Store Container.
*/
public function setBstoreContainer(DggContainer\BstoreContainer $bstoreContainer): void
{
$this->bstoreContainer = $bstoreContainer;
}
/**
* Set an option for the drawing group.
*
* @param int $property The number specifies the option
*/
public function setOPT(int $property, mixed $value): void
{
$this->OPT[$property] = $value;
}
/**
* Get an option for the drawing group.
*
* @param int $property The number specifies the option
*/
public function getOPT(int $property): mixed
{
if (isset($this->OPT[$property])) {
return $this->OPT[$property];
}
return null;
}
/**
* Get identifier clusters.
*
* @return mixed[]
*/
public function getIDCLs(): array
{
return $this->IDCLs;
}
/**
* Set identifier clusters. [<drawingId> => <max shape id>, ...].
*
* @param mixed[] $IDCLs
*/
public function setIDCLs(array $IDCLs): void
{
$this->IDCLs = $IDCLs;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer;
class BstoreContainer
{
/**
* BLIP Store Entries. Each of them holds one BLIP (Big Large Image or Picture).
*
* @var BstoreContainer\BSE[]
*/
private array $BSECollection = [];
/**
* Add a BLIP Store Entry.
*/
public function addBSE(BstoreContainer\BSE $BSE): void
{
$this->BSECollection[] = $BSE;
$BSE->setParent($this);
}
/**
* Get the collection of BLIP Store Entries.
*
* @return BstoreContainer\BSE[]
*/
public function getBSECollection(): array
{
return $this->BSECollection;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
class BSE
{
const BLIPTYPE_ERROR = 0x00;
const BLIPTYPE_UNKNOWN = 0x01;
const BLIPTYPE_EMF = 0x02;
const BLIPTYPE_WMF = 0x03;
const BLIPTYPE_PICT = 0x04;
const BLIPTYPE_JPEG = 0x05;
const BLIPTYPE_PNG = 0x06;
const BLIPTYPE_DIB = 0x07;
const BLIPTYPE_TIFF = 0x11;
const BLIPTYPE_CMYKJPEG = 0x12;
/**
* The parent BLIP Store Entry Container.
* Property is currently unused.
*/
private BstoreContainer $parent;
/**
* The BLIP (Big Large Image or Picture).
*/
private ?BSE\Blip $blip = null;
/**
* The BLIP type.
*/
private int $blipType;
/**
* Set parent BLIP Store Entry Container.
*/
public function setParent(BstoreContainer $parent): void
{
$this->parent = $parent;
}
public function getParent(): BstoreContainer
{
return $this->parent;
}
/**
* Get the BLIP.
*/
public function getBlip(): ?BSE\Blip
{
return $this->blip;
}
/**
* Set the BLIP.
*/
public function setBlip(BSE\Blip $blip): void
{
$this->blip = $blip;
$blip->setParent($this);
}
/**
* Get the BLIP type.
*/
public function getBlipType(): int
{
return $this->blipType;
}
/**
* Set the BLIP type.
*/
public function setBlipType(int $blipType): void
{
$this->blipType = $blipType;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
class Blip
{
/**
* The parent BSE.
*/
private BSE $parent;
/**
* Raw image data.
*/
private string $data;
/**
* Get the raw image data.
*/
public function getData(): string
{
return $this->data;
}
/**
* Set the raw image data.
*/
public function setData(string $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;
}
}

View File

@@ -0,0 +1,195 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use ZipArchive;
class File
{
/**
* Use Temp or File Upload Temp for temporary files.
*/
protected static bool $useUploadTempDirectory = false;
/**
* Set the flag indicating whether the File Upload Temp directory should be used for temporary files.
*/
public static function setUseUploadTempDirectory(bool $useUploadTempDir): void
{
self::$useUploadTempDirectory = (bool) $useUploadTempDir;
}
/**
* Get the flag indicating whether the File Upload Temp directory should be used for temporary files.
*/
public static function getUseUploadTempDirectory(): bool
{
return self::$useUploadTempDirectory;
}
// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
// Section 4.3.7
// Looks like there might be endian-ness considerations
private const ZIP_FIRST_4 = [
"\x50\x4b\x03\x04", // what it looks like on my system
"\x04\x03\x4b\x50", // what it says in documentation
];
private static function validateZipFirst4(string $zipFile): bool
{
$contents = @file_get_contents($zipFile, false, null, 0, 4);
return in_array($contents, self::ZIP_FIRST_4, true);
}
/**
* Verify if a file exists.
*/
public static function fileExists(string $filename): bool
{
// Sick construction, but it seems that
// file_exists returns strange values when
// doing the original file_exists on ZIP archives...
if (strtolower(substr($filename, 0, 6)) == 'zip://') {
// Open ZIP file and verify if the file exists
$zipFile = substr($filename, 6, strrpos($filename, '#') - 6);
$archiveFile = substr($filename, strrpos($filename, '#') + 1);
if (self::validateZipFirst4($zipFile)) {
$zip = new ZipArchive();
$res = $zip->open($zipFile);
if ($res === true) {
$returnValue = ($zip->getFromName($archiveFile) !== false);
$zip->close();
return $returnValue;
}
}
return false;
}
return file_exists($filename);
}
/**
* Returns canonicalized absolute pathname, also for ZIP archives.
*/
public static function realpath(string $filename): string
{
// Returnvalue
$returnValue = '';
// Try using realpath()
if (file_exists($filename)) {
$returnValue = realpath($filename) ?: '';
}
// Found something?
if ($returnValue === '') {
$pathArray = explode('/', $filename);
while (in_array('..', $pathArray) && $pathArray[0] != '..') {
$iMax = count($pathArray);
for ($i = 1; $i < $iMax; ++$i) {
if ($pathArray[$i] == '..') {
array_splice($pathArray, $i - 1, 2);
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);
}
}

View File

@@ -0,0 +1,719 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Font as FontStyle;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
class Font
{
// Methods for resolving autosize value
const AUTOSIZE_METHOD_APPROX = 'approx';
const AUTOSIZE_METHOD_EXACT = 'exact';
private const AUTOSIZE_METHODS = [
self::AUTOSIZE_METHOD_APPROX,
self::AUTOSIZE_METHOD_EXACT,
];
/** Character set codes used by BIFF5-8 in Font records */
const CHARSET_ANSI_LATIN = 0x00;
const CHARSET_SYSTEM_DEFAULT = 0x01;
const CHARSET_SYMBOL = 0x02;
const CHARSET_APPLE_ROMAN = 0x4D;
const CHARSET_ANSI_JAPANESE_SHIFTJIS = 0x80;
const CHARSET_ANSI_KOREAN_HANGUL = 0x81;
const CHARSET_ANSI_KOREAN_JOHAB = 0x82;
const CHARSET_ANSI_CHINESE_SIMIPLIFIED = 0x86; // gb2312
const CHARSET_ANSI_CHINESE_TRADITIONAL = 0x88; // big5
const CHARSET_ANSI_GREEK = 0xA1;
const CHARSET_ANSI_TURKISH = 0xA2;
const CHARSET_ANSI_VIETNAMESE = 0xA3;
const CHARSET_ANSI_HEBREW = 0xB1;
const CHARSET_ANSI_ARABIC = 0xB2;
const CHARSET_ANSI_BALTIC = 0xBA;
const CHARSET_ANSI_CYRILLIC = 0xCC;
const CHARSET_ANSI_THAI = 0xDD;
const CHARSET_ANSI_LATIN_II = 0xEE;
const CHARSET_OEM_LATIN_I = 0xFF;
// XXX: Constants created!
/** Font filenames */
const ARIAL = 'arial.ttf';
const ARIAL_BOLD = 'arialbd.ttf';
const ARIAL_ITALIC = 'ariali.ttf';
const ARIAL_BOLD_ITALIC = 'arialbi.ttf';
const CALIBRI = 'calibri.ttf';
const CALIBRI_BOLD = 'calibrib.ttf';
const CALIBRI_ITALIC = 'calibrii.ttf';
const CALIBRI_BOLD_ITALIC = 'calibriz.ttf';
const COMIC_SANS_MS = 'comic.ttf';
const COMIC_SANS_MS_BOLD = 'comicbd.ttf';
const COURIER_NEW = 'cour.ttf';
const COURIER_NEW_BOLD = 'courbd.ttf';
const COURIER_NEW_ITALIC = 'couri.ttf';
const COURIER_NEW_BOLD_ITALIC = 'courbi.ttf';
const GEORGIA = 'georgia.ttf';
const GEORGIA_BOLD = 'georgiab.ttf';
const GEORGIA_ITALIC = 'georgiai.ttf';
const GEORGIA_BOLD_ITALIC = 'georgiaz.ttf';
const IMPACT = 'impact.ttf';
const LIBERATION_SANS = 'LiberationSans-Regular.ttf';
const LIBERATION_SANS_BOLD = 'LiberationSans-Bold.ttf';
const LIBERATION_SANS_ITALIC = 'LiberationSans-Italic.ttf';
const LIBERATION_SANS_BOLD_ITALIC = 'LiberationSans-BoldItalic.ttf';
const LUCIDA_CONSOLE = 'lucon.ttf';
const LUCIDA_SANS_UNICODE = 'l_10646.ttf';
const MICROSOFT_SANS_SERIF = 'micross.ttf';
const PALATINO_LINOTYPE = 'pala.ttf';
const PALATINO_LINOTYPE_BOLD = 'palab.ttf';
const PALATINO_LINOTYPE_ITALIC = 'palai.ttf';
const PALATINO_LINOTYPE_BOLD_ITALIC = 'palabi.ttf';
const SYMBOL = 'symbol.ttf';
const TAHOMA = 'tahoma.ttf';
const TAHOMA_BOLD = 'tahomabd.ttf';
const TIMES_NEW_ROMAN = 'times.ttf';
const TIMES_NEW_ROMAN_BOLD = 'timesbd.ttf';
const TIMES_NEW_ROMAN_ITALIC = 'timesi.ttf';
const TIMES_NEW_ROMAN_BOLD_ITALIC = 'timesbi.ttf';
const TREBUCHET_MS = 'trebuc.ttf';
const TREBUCHET_MS_BOLD = 'trebucbd.ttf';
const TREBUCHET_MS_ITALIC = 'trebucit.ttf';
const TREBUCHET_MS_BOLD_ITALIC = 'trebucbi.ttf';
const VERDANA = 'verdana.ttf';
const VERDANA_BOLD = 'verdanab.ttf';
const VERDANA_ITALIC = 'verdanai.ttf';
const VERDANA_BOLD_ITALIC = 'verdanaz.ttf';
const FONT_FILE_NAMES = [
'Arial' => [
'x' => self::ARIAL,
'xb' => self::ARIAL_BOLD,
'xi' => self::ARIAL_ITALIC,
'xbi' => self::ARIAL_BOLD_ITALIC,
],
'Calibri' => [
'x' => self::CALIBRI,
'xb' => self::CALIBRI_BOLD,
'xi' => self::CALIBRI_ITALIC,
'xbi' => self::CALIBRI_BOLD_ITALIC,
],
'Comic Sans MS' => [
'x' => self::COMIC_SANS_MS,
'xb' => self::COMIC_SANS_MS_BOLD,
'xi' => self::COMIC_SANS_MS,
'xbi' => self::COMIC_SANS_MS_BOLD,
],
'Courier New' => [
'x' => self::COURIER_NEW,
'xb' => self::COURIER_NEW_BOLD,
'xi' => self::COURIER_NEW_ITALIC,
'xbi' => self::COURIER_NEW_BOLD_ITALIC,
],
'Georgia' => [
'x' => self::GEORGIA,
'xb' => self::GEORGIA_BOLD,
'xi' => self::GEORGIA_ITALIC,
'xbi' => self::GEORGIA_BOLD_ITALIC,
],
'Impact' => [
'x' => self::IMPACT,
'xb' => self::IMPACT,
'xi' => self::IMPACT,
'xbi' => self::IMPACT,
],
'Liberation Sans' => [
'x' => self::LIBERATION_SANS,
'xb' => self::LIBERATION_SANS_BOLD,
'xi' => self::LIBERATION_SANS_ITALIC,
'xbi' => self::LIBERATION_SANS_BOLD_ITALIC,
],
'Lucida Console' => [
'x' => self::LUCIDA_CONSOLE,
'xb' => self::LUCIDA_CONSOLE,
'xi' => self::LUCIDA_CONSOLE,
'xbi' => self::LUCIDA_CONSOLE,
],
'Lucida Sans Unicode' => [
'x' => self::LUCIDA_SANS_UNICODE,
'xb' => self::LUCIDA_SANS_UNICODE,
'xi' => self::LUCIDA_SANS_UNICODE,
'xbi' => self::LUCIDA_SANS_UNICODE,
],
'Microsoft Sans Serif' => [
'x' => self::MICROSOFT_SANS_SERIF,
'xb' => self::MICROSOFT_SANS_SERIF,
'xi' => self::MICROSOFT_SANS_SERIF,
'xbi' => self::MICROSOFT_SANS_SERIF,
],
'Palatino Linotype' => [
'x' => self::PALATINO_LINOTYPE,
'xb' => self::PALATINO_LINOTYPE_BOLD,
'xi' => self::PALATINO_LINOTYPE_ITALIC,
'xbi' => self::PALATINO_LINOTYPE_BOLD_ITALIC,
],
'Symbol' => [
'x' => self::SYMBOL,
'xb' => self::SYMBOL,
'xi' => self::SYMBOL,
'xbi' => self::SYMBOL,
],
'Tahoma' => [
'x' => self::TAHOMA,
'xb' => self::TAHOMA_BOLD,
'xi' => self::TAHOMA,
'xbi' => self::TAHOMA_BOLD,
],
'Times New Roman' => [
'x' => self::TIMES_NEW_ROMAN,
'xb' => self::TIMES_NEW_ROMAN_BOLD,
'xi' => self::TIMES_NEW_ROMAN_ITALIC,
'xbi' => self::TIMES_NEW_ROMAN_BOLD_ITALIC,
],
'Trebuchet MS' => [
'x' => self::TREBUCHET_MS,
'xb' => self::TREBUCHET_MS_BOLD,
'xi' => self::TREBUCHET_MS_ITALIC,
'xbi' => self::TREBUCHET_MS_BOLD_ITALIC,
],
'Verdana' => [
'x' => self::VERDANA,
'xb' => self::VERDANA_BOLD,
'xi' => self::VERDANA_ITALIC,
'xbi' => self::VERDANA_BOLD_ITALIC,
],
];
/**
* Array that can be used to supplement FONT_FILE_NAMES for calculating exact width.
*
* @var array<string, array<string, string>>
*/
private static array $extraFontArray = [];
/** @param array<string, array<string, string>> $extraFontArray */
public static function setExtraFontArray(array $extraFontArray): void
{
self::$extraFontArray = $extraFontArray;
}
/** @return array<string, array<string, string>> */
public static function getExtraFontArray(): array
{
return self::$extraFontArray;
}
/**
* AutoSize method.
*/
private static string $autoSizeMethod = self::AUTOSIZE_METHOD_APPROX;
/**
* Path to folder containing TrueType font .ttf files.
*/
private static string $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],
],
];
/**
* Set autoSize method.
*
* @param string $method see self::AUTOSIZE_METHOD_*
*
* @return bool Success or failure
*/
public static function setAutoSizeMethod(string $method): bool
{
if (!in_array($method, self::AUTOSIZE_METHODS)) {
return false;
}
self::$autoSizeMethod = $method;
return true;
}
/**
* Get autoSize method.
*/
public static function getAutoSizeMethod(): string
{
return self::$autoSizeMethod;
}
/**
* Set the path to the folder containing .ttf files. There should be a trailing slash.
* Path will be recursively searched for font file.
* Typical locations on various platforms:
* <ul>
* <li>C:/Windows/Fonts/</li>
* <li>/usr/share/fonts/truetype/</li>
* <li>~/.fonts/</li>
* </ul>.
*/
public static function setTrueTypeFontPath(string $folderPath): void
{
self::$trueTypeFontPath = $folderPath;
}
/**
* Get the path to the folder containing .ttf files.
*/
public static function getTrueTypeFontPath(): string
{
return self::$trueTypeFontPath;
}
/**
* Pad amount for exact in pixels; use best guess if null.
*/
private static null|float|int $paddingAmountExact = null;
/**
* Set pad amount for exact in pixels; use best guess if null.
*/
public static function setPaddingAmountExact(null|float|int $paddingAmountExact): void
{
self::$paddingAmountExact = $paddingAmountExact;
}
/**
* Get pad amount for exact in pixels; or null if using best guess.
*/
public static function getPaddingAmountExact(): null|float|int
{
return self::$paddingAmountExact;
}
/**
* 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 = '',
int $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 (str_contains($cellText, "\n")) {
$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) + (self::$paddingAmountExact ?? $columnWidthAdjust);
} catch (PhpSpreadsheetException) {
$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
/** @var int[] $textBox */
$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.
*
* @return int Text width in pixels (no padding added)
*/
public static function getTextWidthPixelsApprox(string $columnText, FontStyle $font, int $rotation = 0): int
{
$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 float|int $fontSizeInPoints Font size (in points)
*
* @return int Font size (in pixels)
*/
public static function fontSizeToPixels(float|int $fontSizeInPoints): int
{
return (int) ((4 / 3) * $fontSizeInPoints);
}
/**
* Calculate an (approximate) pixel size, based on inch size.
*
* @param float|int $sizeInInch Font size (in inch)
*
* @return float|int Size (in pixels)
*/
public static function inchSizeToPixels(int|float $sizeInInch): int|float
{
return $sizeInInch * 96;
}
/**
* Calculate an (approximate) pixel size, based on centimeter size.
*
* @param float|int $sizeInCm Font size (in centimeters)
*
* @return float Size (in pixels)
*/
public static function centimeterSizeToPixels(int|float $sizeInCm): float
{
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): string
{
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::findFontFile(self::$trueTypeFontPath, $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(string $fontName): int
{
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 ($returnAsPixels is true ? int : float) Column width
*/
public static function getDefaultColumnWidthByFont(FontStyle $font, bool $returnAsPixels = false): float|int
{
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): float
{
$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;
}
private static function findFontFile(string $startDirectory, string $desiredFont): ?string
{
$fontPath = null;
if ($startDirectory === '') {
return null;
}
if (file_exists("$startDirectory/$desiredFont")) {
$fontPath = "$startDirectory/$desiredFont";
} else {
$iterations = 0;
$it = new RecursiveDirectoryIterator(
$startDirectory,
RecursiveDirectoryIterator::SKIP_DOTS
| RecursiveDirectoryIterator::FOLLOW_SYMLINKS
);
foreach (
new RecursiveIteratorIterator(
$it,
RecursiveIteratorIterator::LEAVES_ONLY,
RecursiveIteratorIterator::CATCH_GET_CHILD
) as $filex
) {
/** @var string */
$file = $filex;
if (basename($file) === $desiredFont) {
$fontPath = $file;
break;
}
++$iterations;
if ($iterations > 5000) {
// @codeCoverageIgnoreStart
break;
// @codeCoverageIgnoreEnd
}
}
}
return $fontPath;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
class IntOrFloat
{
/**
* Help some functions with large results operate correctly on 32-bit,
* by returning result as int when possible, float otherwise.
*/
public static function evaluate(float|int $value): float|int
{
$iValue = (int) $value;
return ($value == $iValue) ? $iValue : $value;
}
}

View File

@@ -0,0 +1,564 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
// vim: set expandtab tabstop=4 shiftwidth=4:
// +----------------------------------------------------------------------+
// | PHP Version 4 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2002 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.02 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available at through the world-wide-web at |
// | http://www.php.net/license/2_02.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | license@php.net so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Author: Xavier Noguer <xnoguer@php.net> |
// | Based on OLE::Storage_Lite by Kawai, Takanori |
// +----------------------------------------------------------------------+
//
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\Shared\OLE\ChainedBlockStream;
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root;
/*
* Array for storing OLE instances that are accessed from
* OLE_ChainedBlockStream::stream_open().
*
* @var array
*/
$GLOBALS['_OLE_INSTANCES'] = [];
/**
* OLE package base class.
*
* @author Xavier Noguer <xnoguer@php.net>
* @author Christian Schmidt <schmidt@php.net>
*/
class OLE
{
const OLE_PPS_TYPE_ROOT = 5;
const OLE_PPS_TYPE_DIR = 1;
const OLE_PPS_TYPE_FILE = 2;
const OLE_DATA_SIZE_SMALL = 0x1000;
const OLE_LONG_INT_SIZE = 4;
const OLE_PPS_SIZE = 0x80;
/**
* The file handle for reading an OLE container.
*
* @var resource
*/
public $_file_handle;
/**
* Array of PPS's found on the OLE container.
*
* @var array<OLE\PPS|OLE\PPS\File|Root>
*/
public array $_list = [];
/**
* Root directory of OLE container.
*/
public Root $root;
/**
* Big Block Allocation Table.
*
* @var mixed[] (blockId => nextBlockId)
*/
public array $bbat;
/**
* Short Block Allocation Table.
*
* @var mixed[] (blockId => nextBlockId)
*/
public array $sbat;
/**
* Size of big blocks. This is usually 512.
*
* @var int<1, max> number of octets per block
*/
public int $bigBlockSize;
/**
* Size of small blocks. This is usually 64.
*
* @var int number of octets per block
*/
public int $smallBlockSize;
/**
* Threshold for big blocks.
*/
public int $bigBlockThreshold;
/**
* Reads an OLE container from the contents of the file given.
*
* @acces public
*
* @return bool true on success, PEAR_Error on failure
*/
public function read(string $filename): bool
{
$fh = @fopen($filename, 'rb');
if ($fh === false) {
throw new ReaderException("Can't open file $filename");
}
$this->_file_handle = $fh;
$signature = fread($fh, 8);
if ("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" != $signature) {
throw new ReaderException("File doesn't seem to be an OLE container.");
}
fseek($fh, 28);
if (fread($fh, 2) != "\xFE\xFF") {
// This shouldn't be a problem in practice
throw new ReaderException('Only Little-Endian encoding is supported.');
}
// Size of blocks and short blocks in bytes
/** @var int<1, max> */
$temp = 2 ** self::readInt2($fh);
$this->bigBlockSize = $temp;
$this->smallBlockSize = 2 ** self::readInt2($fh);
// Skip UID, revision number and version number
fseek($fh, 44);
// Number of blocks in Big Block Allocation Table
$bbatBlockCount = self::readInt4($fh);
// Root chain 1st block
$directoryFirstBlockId = self::readInt4($fh);
// Skip unused bytes
fseek($fh, 56);
// Streams shorter than this are stored using small blocks
$this->bigBlockThreshold = self::readInt4($fh);
// Block id of first sector in Short Block Allocation Table
$sbatFirstBlockId = self::readInt4($fh);
// Number of blocks in Short Block Allocation Table
$sbbatBlockCount = self::readInt4($fh);
// Block id of first sector in Master Block Allocation Table
$mbatFirstBlockId = self::readInt4($fh);
// Number of blocks in Master Block Allocation Table
$mbbatBlockCount = self::readInt4($fh);
$this->bbat = [];
// Remaining 4 * 109 bytes of current block is beginning of Master
// Block Allocation Table
$mbatBlocks = [];
for ($i = 0; $i < 109; ++$i) {
$mbatBlocks[] = self::readInt4($fh);
}
// Read rest of Master Block Allocation Table (if any is left)
$pos = $this->getBlockOffset($mbatFirstBlockId);
for ($i = 0; $i < $mbbatBlockCount; ++$i) {
fseek($fh, $pos);
for ($j = 0; $j < $this->bigBlockSize / 4 - 1; ++$j) {
$mbatBlocks[] = self::readInt4($fh);
}
// Last block id in each block points to next block
$pos = $this->getBlockOffset(self::readInt4($fh));
}
// Read Big Block Allocation Table according to chain specified by $mbatBlocks
for ($i = 0; $i < $bbatBlockCount; ++$i) {
$pos = $this->getBlockOffset($mbatBlocks[$i]);
fseek($fh, $pos);
for ($j = 0; $j < $this->bigBlockSize / 4; ++$j) {
$this->bbat[] = self::readInt4($fh);
}
}
// Read short block allocation table (SBAT)
$this->sbat = [];
$shortBlockCount = $sbbatBlockCount * $this->bigBlockSize / 4;
$sbatFh = $this->getStream($sbatFirstBlockId);
for ($blockId = 0; $blockId < $shortBlockCount; ++$blockId) {
$this->sbat[$blockId] = self::readInt4($sbatFh);
}
fclose($sbatFh);
$this->readPpsWks($directoryFirstBlockId);
return true;
}
/**
* @param int $blockId byte offset from beginning of file
*/
public function getBlockOffset(int $blockId): int
{
return 512 + $blockId * $this->bigBlockSize;
}
/**
* Returns a stream for use with fread() etc. External callers should
* use \PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File::getStream().
*
* @param int|OLE\PPS $blockIdOrPps block id or PPS
*
* @return resource read-only stream
*/
public function getStream($blockIdOrPps)
{
static $isRegistered = false;
if (!$isRegistered) {
stream_wrapper_register('ole-chainedblockstream', ChainedBlockStream::class);
$isRegistered = true;
}
// Store current instance in global array, so that it can be accessed
// in OLE_ChainedBlockStream::stream_open().
// Object is removed from self::$instances in OLE_Stream::close().
$GLOBALS['_OLE_INSTANCES'][] = $this; //* @phpstan-ignore-line
$keys = array_keys($GLOBALS['_OLE_INSTANCES']); //* @phpstan-ignore-line
$instanceId = end($keys);
$path = 'ole-chainedblockstream://oleInstanceId=' . $instanceId;
if ($blockIdOrPps instanceof OLE\PPS) {
$path .= '&blockId=' . $blockIdOrPps->startBlock;
$path .= '&size=' . $blockIdOrPps->Size;
} else {
$path .= '&blockId=' . $blockIdOrPps;
}
$resource = fopen($path, 'rb');
if ($resource === false) {
throw new Exception("Unable to open stream $path");
}
return $resource;
}
/**
* Reads a signed char.
*
* @param resource $fileHandle file handle
*/
private static function readInt1($fileHandle): int
{
[, $tmp] = unpack('c', fread($fileHandle, 1) ?: '') ?: [0, 0];
/** @var int $tmp */
return $tmp;
}
/**
* Reads an unsigned short (2 octets).
*
* @param resource $fileHandle file handle
*/
private static function readInt2($fileHandle): int
{
[, $tmp] = unpack('v', fread($fileHandle, 2) ?: '') ?: [0, 0];
/** @var int $tmp */
return $tmp;
}
private const SIGNED_4OCTET_LIMIT = 2147483648;
private const SIGNED_4OCTET_SUBTRACT = 2 * self::SIGNED_4OCTET_LIMIT;
/**
* Reads long (4 octets), interpreted as if signed on 32-bit system.
*
* @param resource $fileHandle file handle
*/
private static function readInt4($fileHandle): int
{
[, $tmp] = unpack('V', fread($fileHandle, 4) ?: '') ?: [0, 0];
/** @var int $tmp */
if ($tmp >= self::SIGNED_4OCTET_LIMIT) {
$tmp -= self::SIGNED_4OCTET_SUBTRACT;
}
return $tmp;
}
/**
* Gets information about all PPS's on the OLE container from the PPS WK's
* creates an OLE_PPS object for each one.
*
* @param int $blockId the block id of the first block
*
* @return bool true on success, PEAR_Error on failure
*/
public function readPpsWks(int $blockId): bool
{
$fh = $this->getStream($blockId);
for ($pos = 0; true; $pos += 128) {
fseek($fh, $pos, SEEK_SET);
$nameUtf16 = (string) fread($fh, 64);
$nameLength = self::readInt2($fh);
$nameUtf16 = substr($nameUtf16, 0, $nameLength - 2);
// Simple conversion from UTF-16LE to ISO-8859-1
$name = str_replace("\x00", '', $nameUtf16);
$type = self::readInt1($fh);
switch ($type) {
case self::OLE_PPS_TYPE_ROOT:
$pps = new Root(null, null, []);
$this->root = $pps;
break;
case self::OLE_PPS_TYPE_DIR:
$pps = new OLE\PPS(null, null, null, null, null, null, null, null, null, []);
break;
case self::OLE_PPS_TYPE_FILE:
$pps = new OLE\PPS\File($name);
break;
default:
throw new Exception('Unsupported PPS type');
}
fseek($fh, 1, SEEK_CUR);
$pps->Type = $type;
$pps->Name = $name;
$pps->PrevPps = self::readInt4($fh);
$pps->NextPps = self::readInt4($fh);
$pps->DirPps = self::readInt4($fh);
fseek($fh, 20, SEEK_CUR);
$pps->Time1st = self::OLE2LocalDate((string) fread($fh, 8));
$pps->Time2nd = self::OLE2LocalDate((string) fread($fh, 8));
$pps->startBlock = self::readInt4($fh);
$pps->Size = self::readInt4($fh);
$pps->No = count($this->_list);
$this->_list[] = $pps;
// check if the PPS tree (starting from root) is complete
if (isset($this->root) && $this->ppsTreeComplete($this->root->No)) {
break;
}
}
fclose($fh);
// Initialize $pps->children on directories
foreach ($this->_list as $pps) {
if ($pps->Type == self::OLE_PPS_TYPE_DIR || $pps->Type == self::OLE_PPS_TYPE_ROOT) {
$nos = [$pps->DirPps];
$pps->children = [];
while (!empty($nos)) {
$no = array_pop($nos);
if ($no != -1) {
$childPps = $this->_list[$no];
$nos[] = $childPps->PrevPps;
$nos[] = $childPps->NextPps;
$pps->children[] = $childPps;
}
}
}
}
return true;
}
/**
* It checks whether the PPS tree is complete (all PPS's read)
* starting with the given PPS (not necessarily root).
*
* @param int $index The index of the PPS from which we are checking
*
* @return bool Whether the PPS tree for the given PPS is complete
*/
private function ppsTreeComplete(int $index): bool
{
if (!isset($this->_list[$index])) {
return false;
}
$pps = $this->_list[$index];
return
($pps->PrevPps == -1
|| $this->ppsTreeComplete($pps->PrevPps))
&& ($pps->NextPps == -1
|| $this->ppsTreeComplete($pps->NextPps))
&& ($pps->DirPps == -1
|| $this->ppsTreeComplete($pps->DirPps));
}
/**
* Checks whether a PPS is a File PPS or not.
* If there is no PPS for the index given, it will return false.
*
* @param int $index The index for the PPS
*
* @return bool true if it's a File PPS, false otherwise
*/
public function isFile(int $index): bool
{
if (isset($this->_list[$index])) {
return $this->_list[$index]->Type == self::OLE_PPS_TYPE_FILE;
}
return false;
}
/**
* Checks whether a PPS is a Root PPS or not.
* If there is no PPS for the index given, it will return false.
*
* @param int $index the index for the PPS
*
* @return bool true if it's a Root PPS, false otherwise
*/
public function isRoot(int $index): bool
{
if (isset($this->_list[$index])) {
return $this->_list[$index]->Type == self::OLE_PPS_TYPE_ROOT;
}
return false;
}
/**
* Gives the total number of PPS's found in the OLE container.
*
* @return int The total number of PPS's found in the OLE container
*/
public function ppsTotal(): int
{
return count($this->_list);
}
/**
* Gets data from a PPS
* If there is no PPS for the index given, it will return an empty string.
*
* @param int $index The index for the PPS
* @param int $position The position from which to start reading
* (relative to the PPS)
* @param int $length The amount of bytes to read (at most)
*
* @return string The binary string containing the data requested
*
* @see OLE_PPS_File::getStream()
*/
public function getData(int $index, int $position, int $length): string
{
// if position is not valid return empty string
if (!isset($this->_list[$index]) || ($position >= $this->_list[$index]->Size) || ($position < 0)) {
return '';
}
$fh = $this->getStream($this->_list[$index]);
$data = (string) stream_get_contents($fh, $length, $position);
fclose($fh);
return $data;
}
/**
* Gets the data length from a PPS
* If there is no PPS for the index given, it will return 0.
*
* @param int $index The index for the PPS
*
* @return int The amount of bytes in data the PPS has
*/
public function getDataLength(int $index): int
{
if (isset($this->_list[$index])) {
return $this->_list[$index]->Size;
}
return 0;
}
/**
* Utility function to transform ASCII text to Unicode.
*
* @param string $ascii The ASCII string to transform
*
* @return string The string in Unicode
*/
public static function ascToUcs(string $ascii): string
{
$rawname = '';
$iMax = strlen($ascii);
for ($i = 0; $i < $iMax; ++$i) {
$rawname .= $ascii[$i]
. "\x00";
}
return $rawname;
}
/**
* Utility function
* Returns a string for the OLE container with the date given.
*
* @param float|int $date A timestamp
*
* @return string The string for the OLE container
*/
public static function localDateToOLE($date): string
{
if (!$date) {
return "\x00\x00\x00\x00\x00\x00\x00\x00";
}
$dateTime = Date::dateTimeFromTimestamp("$date");
// days from 1-1-1601 until the beggining of UNIX era
$days = 134774;
// calculate seconds
$big_date = $days * 24 * 3600 + (float) $dateTime->format('U');
// multiply just to make MS happy
$big_date *= 10000000;
// Make HEX string
$res = '';
$factor = 2 ** 56;
while ($factor >= 1) {
$hex = (int) floor($big_date / $factor);
$res = pack('c', $hex) . $res;
$big_date = fmod($big_date, $factor);
$factor /= 256;
}
return $res;
}
/**
* Returns a timestamp from an OLE container's date.
*
* @param string $oleTimestamp A binary string with the encoded date
*
* @return float|int The Unix timestamp corresponding to the string
*/
public static function OLE2LocalDate(string $oleTimestamp)
{
if (strlen($oleTimestamp) != 8) {
throw new ReaderException('Expecting 8 byte string');
}
// convert to units of 100 ns since 1601:
/** @var int[] */
$unpackedTimestamp = unpack('v4', $oleTimestamp) ?: [];
$timestampHigh = (float) $unpackedTimestamp[4] * 65536 + (float) $unpackedTimestamp[3];
$timestampLow = (float) $unpackedTimestamp[2] * 65536 + (float) $unpackedTimestamp[1];
// translate to seconds since 1601:
$timestampHigh /= 10000000;
$timestampLow /= 10000000;
// days from 1601 to 1970:
$days = 134774;
// translate to seconds since 1970:
$unixTimestamp = floor(65536.0 * 65536.0 * $timestampHigh + $timestampLow - $days * 24 * 3600 + 0.5);
return IntOrFloat::evaluate($unixTimestamp);
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\OLE;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Shared\OLE;
class ChainedBlockStream
{
/** @var mixed */
public $context;
/**
* The OLE container of the file that is being read.
*/
public ?OLE $ole = null;
/**
* Parameters specified by fopen().
*
* @var mixed[]
*/
public array $params = [];
/**
* The binary data of the file.
*/
public string $data;
/**
* The file pointer.
*
* @var int byte offset
*/
public int $pos = 0;
/**
* Implements support for fopen().
* For creating streams using this wrapper, use OLE_PPS_File::getStream().
*
* @param string $path resource name including scheme, e.g.
* ole-chainedblockstream://oleInstanceId=1
* @param string $mode only "r" is supported
* @param int $options mask of STREAM_REPORT_ERRORS and STREAM_USE_PATH
* @param ?string $openedPath absolute path of the opened stream (out parameter)
*
* @return bool true on success
*/
public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool // @codingStandardsIgnoreLine
{
if ($mode[0] !== 'r') {
if ($options & STREAM_REPORT_ERRORS) {
trigger_error('Only reading is supported', E_USER_WARNING);
}
return false;
}
// 25 is length of "ole-chainedblockstream://"
parse_str(substr($path, 25), $this->params);
if (!isset($this->params['oleInstanceId'], $this->params['blockId'], $GLOBALS['_OLE_INSTANCES'][$this->params['oleInstanceId']])) { //* @phpstan-ignore-line
if ($options & STREAM_REPORT_ERRORS) {
trigger_error('OLE stream not found', E_USER_WARNING);
}
return false;
}
$this->ole = $GLOBALS['_OLE_INSTANCES'][$this->params['oleInstanceId']]; //* @phpstan-ignore-line
if (!($this->ole instanceof OLE)) { //* @phpstan-ignore-line
throw new Exception('class is not OLE');
}
$blockId = $this->params['blockId'];
$this->data = '';
if (isset($this->params['size']) && $this->params['size'] < $this->ole->bigBlockThreshold && $blockId != $this->ole->root->startBlock) {
// Block id refers to small blocks
$rootPos = $this->ole->getBlockOffset((int) $this->ole->root->startBlock);
while ($blockId != -2) {
/** @var int $blockId */
$pos = $rootPos + $blockId * $this->ole->bigBlockSize;
$blockId = $this->ole->sbat[$blockId];
fseek($this->ole->_file_handle, $pos);
$this->data .= fread($this->ole->_file_handle, $this->ole->bigBlockSize);
}
} else {
// Block id refers to big blocks
while ($blockId != -2) {
/** @var int $blockId */
$pos = $this->ole->getBlockOffset($blockId);
fseek($this->ole->_file_handle, $pos);
$this->data .= fread($this->ole->_file_handle, $this->ole->bigBlockSize);
$blockId = $this->ole->bbat[$blockId];
}
}
if (isset($this->params['size'])) {
$this->data = substr($this->data, 0, $this->params['size']); //* @phpstan-ignore-line
}
if ($options & STREAM_USE_PATH) {
$openedPath = $path;
}
return true;
}
/**
* Implements support for fclose().
*/
public function stream_close(): void // @codingStandardsIgnoreLine
{
$this->ole = null;
unset($GLOBALS['_OLE_INSTANCES']);
}
/**
* Implements support for fread(), fgets() etc.
*
* @param int $count maximum number of bytes to read
*
* @return false|string
*/
public function stream_read(int $count): bool|string // @codingStandardsIgnoreLine
{
if ($this->stream_eof()) {
return false;
}
$s = substr($this->data, (int) $this->pos, $count);
$this->pos += $count;
return $s;
}
/**
* Implements support for feof().
*
* @return bool TRUE if the file pointer is at EOF; otherwise FALSE
*/
public function stream_eof(): bool // @codingStandardsIgnoreLine
{
return $this->pos >= strlen($this->data);
}
/**
* Returns the position of the file pointer, i.e. its offset into the file
* stream. Implements support for ftell().
*/
public function stream_tell(): int // @codingStandardsIgnoreLine
{
return $this->pos;
}
/**
* Implements support for fseek().
*
* @param int $offset byte offset
* @param int $whence SEEK_SET, SEEK_CUR or SEEK_END
*/
public function stream_seek(int $offset, int $whence): bool // @codingStandardsIgnoreLine
{
if ($whence == SEEK_SET && $offset >= 0) {
$this->pos = $offset;
} elseif ($whence == SEEK_CUR && -$offset <= $this->pos) {
$this->pos += $offset;
} elseif ($whence == SEEK_END && -$offset <= count($this->data)) { // @phpstan-ignore-line
$this->pos = strlen($this->data) + $offset;
} else {
return false;
}
return true;
}
/**
* Implements support for fstat(). Currently the only supported field is
* "size".
*
* @return array{size: int}
*/
public function stream_stat(): array // @codingStandardsIgnoreLine
{
return [
'size' => strlen($this->data),
];
}
// Methods used by stream_wrapper_register() that are not implemented:
// bool stream_flush ( void )
// int stream_write ( string data )
// bool rename ( string path_from, string path_to )
// bool mkdir ( string path, int mode, int options )
// bool rmdir ( string path, int options )
// bool dir_opendir ( string path, int options )
// array url_stat ( string path, int flags )
// string dir_readdir ( void )
// bool dir_rewinddir ( void )
// bool dir_closedir ( void )
}

View File

@@ -0,0 +1,211 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\OLE;
// vim: set expandtab tabstop=4 shiftwidth=4:
// +----------------------------------------------------------------------+
// | PHP Version 4 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2002 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.02 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available at through the world-wide-web at |
// | http://www.php.net/license/2_02.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | license@php.net so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Author: Xavier Noguer <xnoguer@php.net> |
// | Based on OLE::Storage_Lite by Kawai, Takanori |
// +----------------------------------------------------------------------+
//
use PhpOffice\PhpSpreadsheet\Shared\OLE;
/**
* Class for creating PPS's for OLE containers.
*
* @author Xavier Noguer <xnoguer@php.net>
*/
class PPS
{
private const ALL_ONE_BITS = (PHP_INT_SIZE > 4) ? 0xFFFFFFFF : -1;
/**
* The PPS index.
*/
public int $No;
/**
* The PPS name (in Unicode).
*/
public string $Name;
/**
* The PPS type. Dir, Root or File.
*/
public int $Type;
/**
* The index of the previous PPS.
*/
public int $PrevPps;
/**
* The index of the next PPS.
*/
public int $NextPps;
/**
* The index of it's first child if this is a Dir or Root PPS.
*/
public int $DirPps;
/**
* A timestamp.
*/
public float|int $Time1st;
/**
* A timestamp.
*/
public float|int $Time2nd;
/**
* Starting block (small or big) for this PPS's data inside the container.
*/
public ?int $startBlock = null;
/**
* The size of the PPS's data (in bytes).
*/
public int $Size;
/**
* The PPS's data (only used if it's not using a temporary file).
*/
public string $_data = '';
/**
* Array of child PPS's (only used by Root and Dir PPS's).
*
* @var mixed[]
*/
public array $children = [];
/**
* Pointer to OLE container.
*/
public OLE $ole;
/**
* The constructor.
*
* @param ?int $No The PPS index
* @param ?string $name The PPS name
* @param ?int $type The PPS type. Dir, Root or File
* @param ?int $prev The index of the previous PPS
* @param ?int $next The index of the next PPS
* @param ?int $dir The index of it's first child if this is a Dir or Root PPS
* @param null|float|int $time_1st A timestamp
* @param null|float|int $time_2nd A timestamp
* @param ?string $data The (usually binary) source data of the PPS
* @param mixed[] $children Array containing children PPS for this PPS
*/
public function __construct(?int $No, ?string $name, ?int $type, ?int $prev, ?int $next, ?int $dir, $time_1st, $time_2nd, ?string $data, array $children)
{
$this->No = (int) $No;
$this->Name = (string) $name;
$this->Type = (int) $type;
$this->PrevPps = (int) $prev;
$this->NextPps = (int) $next;
$this->DirPps = (int) $dir;
$this->Time1st = $time_1st ?? 0;
$this->Time2nd = $time_2nd ?? 0;
$this->_data = (string) $data;
$this->children = $children;
$this->Size = strlen((string) $data);
}
/**
* Returns the amount of data saved for this PPS.
*
* @return int The amount of data (in bytes)
*/
public function getDataLen(): int
{
//if (!isset($this->_data)) {
// return 0;
//}
return strlen($this->_data);
}
/**
* Returns a string with the PPS's WK (What is a WK?).
*
* @return string The binary string
*/
public function getPpsWk(): string
{
$ret = str_pad($this->Name, 64, "\x00");
$ret .= pack('v', strlen($this->Name) + 2) // 66
. pack('c', $this->Type) // 67
. pack('c', 0x00) //UK // 68
. pack('V', $this->PrevPps) //Prev // 72
. pack('V', $this->NextPps) //Next // 76
. pack('V', $this->DirPps) //Dir // 80
. "\x00\x09\x02\x00" // 84
. "\x00\x00\x00\x00" // 88
. "\xc0\x00\x00\x00" // 92
. "\x00\x00\x00\x46" // 96 // Seems to be ok only for Root
. "\x00\x00\x00\x00" // 100
. OLE::localDateToOLE($this->Time1st) // 108
. OLE::localDateToOLE($this->Time2nd) // 116
. pack('V', $this->startBlock ?? 0) // 120
. pack('V', $this->Size) // 124
. pack('V', 0); // 128
return $ret;
}
/**
* Updates index and pointers to previous, next and children PPS's for this
* PPS. I don't think it'll work with Dir PPS's.
*
* @param self[] $raList Reference to the array of PPS's for the whole OLE
* container
*
* @return int The index for this PPS
*/
public static function savePpsSetPnt(array &$raList, mixed $to_save, int $depth = 0): int
{
if (!is_array($to_save) || (empty($to_save))) {
return self::ALL_ONE_BITS;
}
/** @var self[] $to_save */
if (count($to_save) == 1) {
$cnt = count($raList);
// If the first entry, it's the root... Don't clone it!
$raList[$cnt] = ($depth == 0) ? $to_save[0] : clone $to_save[0];
$raList[$cnt]->No = $cnt;
$raList[$cnt]->PrevPps = self::ALL_ONE_BITS;
$raList[$cnt]->NextPps = self::ALL_ONE_BITS;
$raList[$cnt]->DirPps = self::savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++);
} else {
$iPos = (int) floor(count($to_save) / 2);
$aPrev = array_slice($to_save, 0, $iPos);
$aNext = array_slice($to_save, $iPos + 1);
$cnt = count($raList);
// If the first entry, it's the root... Don't clone it!
$raList[$cnt] = ($depth == 0) ? $to_save[$iPos] : clone $to_save[$iPos];
$raList[$cnt]->No = $cnt;
$raList[$cnt]->PrevPps = self::savePpsSetPnt($raList, $aPrev, $depth++);
$raList[$cnt]->NextPps = self::savePpsSetPnt($raList, $aNext, $depth++);
$raList[$cnt]->DirPps = self::savePpsSetPnt($raList, @$raList[$cnt]->children, $depth++);
}
return $cnt;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\OLE\PPS;
// vim: set expandtab tabstop=4 shiftwidth=4:
// +----------------------------------------------------------------------+
// | PHP Version 4 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2002 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.02 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available at through the world-wide-web at |
// | http://www.php.net/license/2_02.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | license@php.net so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Author: Xavier Noguer <xnoguer@php.net> |
// | Based on OLE::Storage_Lite by Kawai, Takanori |
// +----------------------------------------------------------------------+
//
use PhpOffice\PhpSpreadsheet\Shared\OLE;
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS;
/**
* Class for creating File PPS's for OLE containers.
*
* @author Xavier Noguer <xnoguer@php.net>
*/
class File extends PPS
{
/**
* The constructor.
*
* @param string $name The name of the file (in Unicode)
*
* @see OLE::ascToUcs()
*/
public function __construct(string $name)
{
parent::__construct(null, $name, OLE::OLE_PPS_TYPE_FILE, null, null, null, null, null, '', []);
}
/**
* Initialization method. Has to be called right after OLE_PPS_File().
*/
public function init(): bool
{
return true;
}
/**
* Append data to PPS.
*
* @param string $data The data to append
*/
public function append(string $data): void
{
$this->_data .= $data;
}
}

View File

@@ -0,0 +1,406 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\OLE\PPS;
// vim: set expandtab tabstop=4 shiftwidth=4:
// +----------------------------------------------------------------------+
// | PHP Version 4 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2002 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.02 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available at through the world-wide-web at |
// | http://www.php.net/license/2_02.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | license@php.net so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Author: Xavier Noguer <xnoguer@php.net> |
// | Based on OLE::Storage_Lite by Kawai, Takanori |
// +----------------------------------------------------------------------+
//
use PhpOffice\PhpSpreadsheet\Shared\OLE;
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS;
/**
* Class for creating Root PPS's for OLE containers.
*
* @author Xavier Noguer <xnoguer@php.net>
*/
class Root extends PPS
{
/**
* @var resource
*/
private $fileHandle;
private ?int $smallBlockSize = null;
private ?int $bigBlockSize = null;
/**
* @param null|float|int $time_1st A timestamp
* @param null|float|int $time_2nd A timestamp
* @param File[] $raChild
*/
public function __construct($time_1st, $time_2nd, array $raChild)
{
parent::__construct(null, OLE::ascToUcs('Root Entry'), OLE::OLE_PPS_TYPE_ROOT, null, null, null, $time_1st, $time_2nd, null, $raChild);
}
/**
* Method for saving the whole OLE container (including files).
* In fact, if called with an empty argument (or '-'), it saves to a
* temporary file and then outputs it's contents to stdout.
* If a resource pointer to a stream created by fopen() is passed
* it will be used, but you have to close such stream by yourself.
*
* @param resource $fileHandle the name of the file or stream where to save the OLE container
*
* @return bool true on success
*/
public function save($fileHandle): bool
{
$this->fileHandle = $fileHandle;
// Initial Setting for saving
$this->bigBlockSize = (int) (2 ** (
(isset($this->bigBlockSize)) ? self::adjust2($this->bigBlockSize) : 9
));
$this->smallBlockSize = (int) (2 ** (
(isset($this->smallBlockSize)) ? self::adjust2($this->smallBlockSize) : 6
));
// Make an array of PPS's (for Save)
$aList = [];
PPS::savePpsSetPnt($aList, [$this]);
// calculate values for header
[$iSBDcnt, $iBBcnt, $iPPScnt] = $this->calcSize($aList); //, $rhInfo);
// Save Header
$this->saveHeader((int) $iSBDcnt, (int) $iBBcnt, (int) $iPPScnt);
// Make Small Data string (write SBD)
$this->_data = $this->makeSmallData($aList);
// Write BB
$this->saveBigData((int) $iSBDcnt, $aList);
// Write PPS
$this->savePps($aList);
// Write Big Block Depot and BDList and Adding Header informations
$this->saveBbd((int) $iSBDcnt, (int) $iBBcnt, (int) $iPPScnt);
return true;
}
/**
* Calculate some numbers.
*
* @param PPS[] $raList Reference to an array of PPS's
*
* @return float[] The array of numbers
*/
private function calcSize(array &$raList): array
{
// Calculate Basic Setting
[$iSBDcnt, $iBBcnt, $iPPScnt] = [0, 0, 0];
$iSBcnt = 0;
$iCount = count($raList);
for ($i = 0; $i < $iCount; ++$i) {
if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) {
$raList[$i]->Size = $raList[$i]->getDataLen();
if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) {
$iSBcnt += floor($raList[$i]->Size / $this->smallBlockSize)
+ (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0);
} else {
$iBBcnt += (floor($raList[$i]->Size / $this->bigBlockSize)
+ (($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0));
}
}
}
$iSmallLen = $iSBcnt * $this->smallBlockSize;
$iSlCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE);
$iSBDcnt = floor($iSBcnt / $iSlCnt) + (($iSBcnt % $iSlCnt) ? 1 : 0);
$iBBcnt += (floor($iSmallLen / $this->bigBlockSize)
+ (($iSmallLen % $this->bigBlockSize) ? 1 : 0));
$iCnt = count($raList);
$iBdCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE;
$iPPScnt = (floor($iCnt / $iBdCnt) + (($iCnt % $iBdCnt) ? 1 : 0));
return [$iSBDcnt, $iBBcnt, $iPPScnt];
}
/**
* Helper function for caculating a magic value for block sizes.
*
* @param int $i2 The argument
*
* @see save()
*/
private static function adjust2(int $i2): float
{
$iWk = log($i2) / log(2);
return ($iWk > floor($iWk)) ? floor($iWk) + 1 : $iWk;
}
/**
* Save OLE header.
*/
private function saveHeader(int $iSBDcnt, int $iBBcnt, int $iPPScnt): void
{
$FILE = $this->fileHandle;
// Calculate Basic Setting
$iBlCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE;
$i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE;
$iBdExL = 0;
$iAll = $iBBcnt + $iPPScnt + $iSBDcnt;
$iAllW = $iAll;
$iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0);
$iBdCnt = floor(($iAll + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0);
// Calculate BD count
if ($iBdCnt > $i1stBdL) {
while (1) {
++$iBdExL;
++$iAllW;
$iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0);
$iBdCnt = floor(($iAllW + $iBdCntW) / $iBlCnt) + ((($iAllW + $iBdCntW) % $iBlCnt) ? 1 : 0);
if ($iBdCnt <= ($iBdExL * $iBlCnt + $i1stBdL)) {
break;
}
}
}
// Save Header
fwrite(
$FILE,
"\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"
. "\x00\x00\x00\x00"
. "\x00\x00\x00\x00"
. "\x00\x00\x00\x00"
. "\x00\x00\x00\x00"
. pack('v', 0x3B)
. pack('v', 0x03)
. pack('v', -2)
. pack('v', 9)
. pack('v', 6)
. pack('v', 0)
. "\x00\x00\x00\x00"
. "\x00\x00\x00\x00"
. pack('V', $iBdCnt)
. pack('V', $iBBcnt + $iSBDcnt) //ROOT START
. pack('V', 0)
. pack('V', 0x1000)
. pack('V', $iSBDcnt ? 0 : -2) //Small Block Depot
. pack('V', $iSBDcnt)
);
// Extra BDList Start, Count
if ($iBdCnt < $i1stBdL) {
fwrite(
$FILE,
pack('V', -2) // Extra BDList Start
. pack('V', 0)// Extra BDList Count
);
} else {
fwrite($FILE, pack('V', $iAll + $iBdCnt) . pack('V', $iBdExL));
}
// BDList
for ($i = 0; $i < $i1stBdL && $i < $iBdCnt; ++$i) {
fwrite($FILE, pack('V', $iAll + $i));
}
if ($i < $i1stBdL) {
$jB = $i1stBdL - $i;
for ($j = 0; $j < $jB; ++$j) {
fwrite($FILE, (pack('V', -1)));
}
}
}
/**
* Saving big data (PPS's with data bigger than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL).
*
* @param PPS[] $raList Reference to array of PPS's
*/
private function saveBigData(int $iStBlk, array &$raList): void
{
$FILE = $this->fileHandle;
// cycle through PPS's
$iCount = count($raList);
for ($i = 0; $i < $iCount; ++$i) {
if ($raList[$i]->Type != OLE::OLE_PPS_TYPE_DIR) {
$raList[$i]->Size = $raList[$i]->getDataLen();
if (($raList[$i]->Size >= OLE::OLE_DATA_SIZE_SMALL) || (($raList[$i]->Type == OLE::OLE_PPS_TYPE_ROOT) && isset($raList[$i]->_data))) {
fwrite($FILE, $raList[$i]->_data);
if ($raList[$i]->Size % $this->bigBlockSize) {
fwrite($FILE, str_repeat("\x00", $this->bigBlockSize - ($raList[$i]->Size % $this->bigBlockSize)));
}
// Set For PPS
$raList[$i]->startBlock = $iStBlk;
$iStBlk
+= ((int) floor($raList[$i]->Size / $this->bigBlockSize)
+ (($raList[$i]->Size % $this->bigBlockSize) ? 1 : 0));
}
}
}
}
/**
* get small data (PPS's with data smaller than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL).
*
* @param PPS[] $raList Reference to array of PPS's
*/
private function makeSmallData(array &$raList): string
{
$sRes = '';
$FILE = $this->fileHandle;
$iSmBlk = 0;
$iCount = count($raList);
for ($i = 0; $i < $iCount; ++$i) {
// Make SBD, small data string
if ($raList[$i]->Type == OLE::OLE_PPS_TYPE_FILE) {
if ($raList[$i]->Size <= 0) {
continue;
}
if ($raList[$i]->Size < OLE::OLE_DATA_SIZE_SMALL) {
$iSmbCnt = (int) floor($raList[$i]->Size / $this->smallBlockSize)
+ (($raList[$i]->Size % $this->smallBlockSize) ? 1 : 0);
// Add to SBD
$jB = $iSmbCnt - 1;
for ($j = 0; $j < $jB; ++$j) {
fwrite($FILE, pack('V', $j + $iSmBlk + 1));
}
fwrite($FILE, pack('V', -2));
// Add to Data String(this will be written for RootEntry)
$sRes .= $raList[$i]->_data;
if ($raList[$i]->Size % $this->smallBlockSize) {
$sRes .= str_repeat("\x00", $this->smallBlockSize - ($raList[$i]->Size % $this->smallBlockSize));
}
// Set for PPS
$raList[$i]->startBlock = $iSmBlk;
$iSmBlk += $iSmbCnt;
}
}
}
$iSbCnt = floor($this->bigBlockSize / OLE::OLE_LONG_INT_SIZE);
if ($iSmBlk % $iSbCnt) {
$iB = $iSbCnt - ($iSmBlk % $iSbCnt);
for ($i = 0; $i < $iB; ++$i) {
fwrite($FILE, pack('V', -1));
}
}
return $sRes;
}
/**
* Saves all the PPS's WKs.
*
* @param PPS[] $raList Reference to an array with all PPS's
*/
private function savePps(array &$raList): void
{
// Save each PPS WK
$iC = count($raList);
for ($i = 0; $i < $iC; ++$i) {
fwrite($this->fileHandle, $raList[$i]->getPpsWk());
}
// Adjust for Block
$iCnt = count($raList);
$iBCnt = $this->bigBlockSize / OLE::OLE_PPS_SIZE;
if ($iCnt % $iBCnt) {
fwrite($this->fileHandle, str_repeat("\x00", ($iBCnt - ($iCnt % $iBCnt)) * OLE::OLE_PPS_SIZE));
}
}
/**
* Saving Big Block Depot.
*/
private function saveBbd(int $iSbdSize, int $iBsize, int $iPpsCnt): void
{
$FILE = $this->fileHandle;
// Calculate Basic Setting
$iBbCnt = $this->bigBlockSize / OLE::OLE_LONG_INT_SIZE;
$i1stBdL = ($this->bigBlockSize - 0x4C) / OLE::OLE_LONG_INT_SIZE;
$iBdExL = 0;
$iAll = $iBsize + $iPpsCnt + $iSbdSize;
$iAllW = $iAll;
$iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0);
$iBdCnt = floor(($iAll + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0);
// Calculate BD count
if ($iBdCnt > $i1stBdL) {
while (1) {
++$iBdExL;
++$iAllW;
$iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0);
$iBdCnt = floor(($iAllW + $iBdCntW) / $iBbCnt) + ((($iAllW + $iBdCntW) % $iBbCnt) ? 1 : 0);
if ($iBdCnt <= ($iBdExL * $iBbCnt + $i1stBdL)) {
break;
}
}
}
// Making BD
// Set for SBD
if ($iSbdSize > 0) {
for ($i = 0; $i < ($iSbdSize - 1); ++$i) {
fwrite($FILE, pack('V', $i + 1));
}
fwrite($FILE, pack('V', -2));
}
// Set for B
for ($i = 0; $i < ($iBsize - 1); ++$i) {
fwrite($FILE, pack('V', $i + $iSbdSize + 1));
}
fwrite($FILE, pack('V', -2));
// Set for PPS
for ($i = 0; $i < ($iPpsCnt - 1); ++$i) {
fwrite($FILE, pack('V', $i + $iSbdSize + $iBsize + 1));
}
fwrite($FILE, pack('V', -2));
// Set for BBD itself ( 0xFFFFFFFD : BBD)
for ($i = 0; $i < $iBdCnt; ++$i) {
fwrite($FILE, pack('V', 0xFFFFFFFD));
}
// Set for ExtraBDList
for ($i = 0; $i < $iBdExL; ++$i) {
fwrite($FILE, pack('V', 0xFFFFFFFC));
}
// Adjust for Block
if (($iAllW + $iBdCnt) % $iBbCnt) {
$iBlock = ($iBbCnt - (($iAllW + $iBdCnt) % $iBbCnt));
for ($i = 0; $i < $iBlock; ++$i) {
fwrite($FILE, pack('V', -1));
}
}
// Extra BDList
if ($iBdCnt > $i1stBdL) {
$iN = 0;
$iNb = 0;
for ($i = $i1stBdL; $i < $iBdCnt; $i++, ++$iN) {
if ($iN >= ($iBbCnt - 1)) {
$iN = 0;
++$iNb;
fwrite($FILE, pack('V', $iAll + $iBdCnt + $iNb));
}
fwrite($FILE, pack('V', $iBsize + $iSbdSize + $iPpsCnt + $i));
}
if (($iBdCnt - $i1stBdL) % ($iBbCnt - 1)) {
$iB = ($iBbCnt - 1) - (($iBdCnt - $i1stBdL) % ($iBbCnt - 1));
for ($i = 0; $i < $iB; ++$i) {
fwrite($FILE, pack('V', -1));
}
}
fwrite($FILE, pack('V', -2));
}
}
}

View File

@@ -0,0 +1,314 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
class OLERead
{
private string $data = '';
// Size of a sector = 512 bytes
const BIG_BLOCK_SIZE = 0x200;
// Size of a short sector = 64 bytes
const SMALL_BLOCK_SIZE = 0x40;
// Size of a directory entry always = 128 bytes
const PROPERTY_STORAGE_BLOCK_SIZE = 0x80;
// Minimum size of a standard stream = 4096 bytes, streams smaller than this are stored as short streams
const SMALL_BLOCK_THRESHOLD = 0x1000;
// header offsets
const NUM_BIG_BLOCK_DEPOT_BLOCKS_POS = 0x2C;
const ROOT_START_BLOCK_POS = 0x30;
const SMALL_BLOCK_DEPOT_BLOCK_POS = 0x3C;
const EXTENSION_BLOCK_POS = 0x44;
const NUM_EXTENSION_BLOCK_POS = 0x48;
const BIG_BLOCK_DEPOT_BLOCKS_POS = 0x4C;
// property storage offsets (directory offsets)
const SIZE_OF_NAME_POS = 0x40;
const TYPE_POS = 0x42;
const START_BLOCK_POS = 0x74;
const SIZE_POS = 0x78;
public ?int $wrkbook = null;
public ?int $summaryInformation = null;
public ?int $documentSummaryInformation = null;
private int $numBigBlockDepotBlocks;
private int $rootStartBlock;
private int $sbdStartBlock;
private int $extensionBlock;
private int $numExtensionBlocks;
private string $bigBlockChain;
private string $smallBlockChain;
private string $entry;
private int $rootentry;
/** @var mixed[][] */
private array $props = [];
/**
* Read the file.
*/
public function read(string $filename): void
{
File::assertFile($filename);
// Get the file identifier
// Don't bother reading the whole file until we know it's a valid OLE file
$this->data = (string) file_get_contents($filename, false, null, 0, 8);
// Check OLE identifier
$identifierOle = pack('CCCCCCCC', 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1);
if ($this->data != $identifierOle) {
throw new ReaderException('The filename ' . $filename . ' is not recognised as an OLE file');
}
// Get the file data
$this->data = (string) file_get_contents($filename);
// Total number of sectors used for the SAT
$this->numBigBlockDepotBlocks = self::getInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS);
// SecID of the first sector of the directory stream
$this->rootStartBlock = self::getInt4d($this->data, self::ROOT_START_BLOCK_POS);
// SecID of the first sector of the SSAT (or -2 if not extant)
$this->sbdStartBlock = self::getInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS);
// SecID of the first sector of the MSAT (or -2 if no additional sectors are used)
$this->extensionBlock = self::getInt4d($this->data, self::EXTENSION_BLOCK_POS);
// Total number of sectors used by MSAT
$this->numExtensionBlocks = self::getInt4d($this->data, self::NUM_EXTENSION_BLOCK_POS);
$bigBlockDepotBlocks = [];
$pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS;
$bbdBlocks = $this->numBigBlockDepotBlocks;
if ($this->numExtensionBlocks !== 0) {
$bbdBlocks = (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS) / 4;
}
for ($i = 0; $i < $bbdBlocks; ++$i) {
$bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos);
$pos += 4;
}
for ($j = 0; $j < $this->numExtensionBlocks; ++$j) {
$pos = ($this->extensionBlock + 1) * self::BIG_BLOCK_SIZE;
$blocksToRead = min($this->numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1);
for ($i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; ++$i) {
$bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos);
$pos += 4;
}
$bbdBlocks += $blocksToRead;
if ($bbdBlocks < $this->numBigBlockDepotBlocks) {
$this->extensionBlock = self::getInt4d($this->data, $pos);
}
}
$pos = 0;
$this->bigBlockChain = '';
$bbs = self::BIG_BLOCK_SIZE / 4;
for ($i = 0; $i < $this->numBigBlockDepotBlocks; ++$i) {
$pos = ($bigBlockDepotBlocks[$i] + 1) * self::BIG_BLOCK_SIZE;
$this->bigBlockChain .= substr($this->data, $pos, 4 * $bbs);
$pos += 4 * $bbs;
}
$sbdBlock = $this->sbdStartBlock;
$this->smallBlockChain = '';
while ($sbdBlock != -2) {
$pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE;
$this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs);
$pos += 4 * $bbs;
$sbdBlock = self::getInt4d($this->bigBlockChain, $sbdBlock * 4);
}
// read the directory stream
$block = $this->rootStartBlock;
$this->entry = $this->readData($block);
$this->readPropertySets();
}
/**
* Extract binary stream data.
*/
public function getStream(?int $stream): ?string
{
if ($stream === null) {
return null;
}
$streamData = '';
if ($this->props[$stream]['size'] < self::SMALL_BLOCK_THRESHOLD) {
/** @var int */
$temp = $this->props[$this->rootentry]['startBlock'];
$rootdata = $this->readData($temp);
/** @var int */
$block = $this->props[$stream]['startBlock'];
while ($block != -2) {
$pos = $block * self::SMALL_BLOCK_SIZE;
$streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE);
$block = self::getInt4d($this->smallBlockChain, $block * 4);
}
return $streamData;
}
/** @var int */
$temp = $this->props[$stream]['size'];
$numBlocks = $temp / self::BIG_BLOCK_SIZE;
if ($temp % self::BIG_BLOCK_SIZE != 0) {
++$numBlocks;
}
if ($numBlocks == 0) {
return '';
}
/** @var int */
$block = $this->props[$stream]['startBlock'];
while ($block != -2) {
$pos = ($block + 1) * self::BIG_BLOCK_SIZE;
$streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE);
$block = self::getInt4d($this->bigBlockChain, $block * 4);
}
return $streamData;
}
/**
* Read a standard stream (by joining sectors using information from SAT).
*
* @param int $block Sector ID where the stream starts
*
* @return string Data for standard stream
*/
private function readData(int $block): string
{
$data = '';
while ($block != -2) {
$pos = ($block + 1) * self::BIG_BLOCK_SIZE;
$data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE);
$block = self::getInt4d($this->bigBlockChain, $block * 4);
}
return $data;
}
/**
* Read entries in the directory stream.
*/
private function readPropertySets(): void
{
$offset = 0;
// loop through entires, each entry is 128 bytes
$entryLen = strlen($this->entry);
while ($offset < $entryLen) {
// entry data (128 bytes)
$d = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE);
// size in bytes of name
$nameSize = ord($d[self::SIZE_OF_NAME_POS]) | (ord($d[self::SIZE_OF_NAME_POS + 1]) << 8);
// type of entry
$type = ord($d[self::TYPE_POS]);
// sectorID of first sector or short sector, if this entry refers to a stream (the case with workbook)
// sectorID of first sector of the short-stream container stream, if this entry is root entry
$startBlock = self::getInt4d($d, self::START_BLOCK_POS);
$size = self::getInt4d($d, self::SIZE_POS);
$name = str_replace("\x00", '', substr($d, 0, $nameSize));
$this->props[] = [
'name' => $name,
'type' => $type,
'startBlock' => $startBlock,
'size' => $size,
];
// tmp helper to simplify checks
$upName = strtoupper($name);
// Workbook directory entry (BIFF5 uses Book, BIFF8 uses Workbook)
if (($upName === 'WORKBOOK') || ($upName === 'BOOK')) {
$this->wrkbook = count($this->props) - 1;
} elseif ($upName === 'ROOT ENTRY' || $upName === 'R') {
// Root entry
$this->rootentry = count($this->props) - 1;
}
// Summary information
if ($name == chr(5) . 'SummaryInformation') {
$this->summaryInformation = count($this->props) - 1;
}
// Additional Document Summary information
if ($name == chr(5) . 'DocumentSummaryInformation') {
$this->documentSummaryInformation = count($this->props) - 1;
}
$offset += self::PROPERTY_STORAGE_BLOCK_SIZE;
}
}
/**
* Read 4 bytes of data at specified position.
*/
private static function getInt4d(string $data, int $pos): int
{
if ($pos < 0) {
// Invalid position
throw new ReaderException('Parameter pos=' . $pos . ' is invalid.');
}
$len = strlen($data);
if ($len < $pos + 4) {
$data .= str_repeat("\0", $pos + 4 - $len);
}
// FIX: represent numbers correctly on 64-bit system
// http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
// Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
$_or_24 = ord($data[$pos + 3]);
if ($_or_24 >= 128) {
// negative number
$_ord_24 = -abs((256 - $_or_24) << 24);
} else {
$_ord_24 = ($_or_24 & 127) << 24;
}
return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24;
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use PhpOffice\PhpSpreadsheet\Exception as SpException;
use PhpOffice\PhpSpreadsheet\Worksheet\Protection;
class PasswordHasher
{
const MAX_PASSWORD_LENGTH = 255;
/**
* Get algorithm name for PHP.
*/
private static function getAlgorithm(string $algorithmName): string
{
if (!$algorithmName) {
return '';
}
// Mapping between algorithm name in Excel and algorithm name in PHP
$mapping = [
Protection::ALGORITHM_MD2 => 'md2',
Protection::ALGORITHM_MD4 => 'md4',
Protection::ALGORITHM_MD5 => 'md5',
Protection::ALGORITHM_SHA_1 => 'sha1',
Protection::ALGORITHM_SHA_256 => 'sha256',
Protection::ALGORITHM_SHA_384 => 'sha384',
Protection::ALGORITHM_SHA_512 => 'sha512',
Protection::ALGORITHM_RIPEMD_128 => 'ripemd128',
Protection::ALGORITHM_RIPEMD_160 => 'ripemd160',
Protection::ALGORITHM_WHIRLPOOL => 'whirlpool',
];
if (array_key_exists($algorithmName, $mapping)) {
return $mapping[$algorithmName];
}
throw new SpException('Unsupported password algorithm: ' . $algorithmName);
}
/**
* Create a password hash from a given string.
*
* This method is based on the spec at:
* https://interoperability.blob.core.windows.net/files/MS-OFFCRYPTO/[MS-OFFCRYPTO].pdf
* 2.3.7.1 Binary Document Password Verifier Derivation Method 1
*
* It replaces a method based on the algorithm provided by
* Daniel Rentz of OpenOffice and the PEAR package
* Spreadsheet_Excel_Writer by Xavier Noguer <xnoguer@rezebra.com>.
*
* @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 . $encodedPassword, true);
for ($i = 0; $i < $spinCount; ++$i) {
$hashValue = hash($phpAlgorithm, $hashValue . pack('L', $i), true);
}
return base64_encode($hashValue);
}
}

View File

@@ -0,0 +1,736 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use Composer\Pcre\Preg;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use Stringable;
class StringHelper
{
private const CONTROL_CHARACTERS_KEYS = [
"\x00",
"\x01",
"\x02",
"\x03",
"\x04",
"\x05",
"\x06",
"\x07",
"\x08",
"\x0b",
"\x0c",
"\x0e",
"\x0f",
"\x10",
"\x11",
"\x12",
"\x13",
"\x14",
"\x15",
"\x16",
"\x17",
"\x18",
"\x19",
"\x1a",
"\x1b",
"\x1c",
"\x1d",
"\x1e",
"\x1f",
];
private const CONTROL_CHARACTERS_VALUES = [
'_x0000_',
'_x0001_',
'_x0002_',
'_x0003_',
'_x0004_',
'_x0005_',
'_x0006_',
'_x0007_',
'_x0008_',
'_x000B_',
'_x000C_',
'_x000E_',
'_x000F_',
'_x0010_',
'_x0011_',
'_x0012_',
'_x0013_',
'_x0014_',
'_x0015_',
'_x0016_',
'_x0017_',
'_x0018_',
'_x0019_',
'_x001A_',
'_x001B_',
'_x001C_',
'_x001D_',
'_x001E_',
'_x001F_',
];
/**
* SYLK Characters array.
*/
private const SYLK_CHARACTERS = [
"\x1B 0" => "\x00",
"\x1B 1" => "\x01",
"\x1B 2" => "\x02",
"\x1B 3" => "\x03",
"\x1B 4" => "\x04",
"\x1B 5" => "\x05",
"\x1B 6" => "\x06",
"\x1B 7" => "\x07",
"\x1B 8" => "\x08",
"\x1B 9" => "\x09",
"\x1B :" => "\x0a",
"\x1B ;" => "\x0b",
"\x1B <" => "\x0c",
"\x1B =" => "\x0d",
"\x1B >" => "\x0e",
"\x1B ?" => "\x0f",
"\x1B!0" => "\x10",
"\x1B!1" => "\x11",
"\x1B!2" => "\x12",
"\x1B!3" => "\x13",
"\x1B!4" => "\x14",
"\x1B!5" => "\x15",
"\x1B!6" => "\x16",
"\x1B!7" => "\x17",
"\x1B!8" => "\x18",
"\x1B!9" => "\x19",
"\x1B!:" => "\x1a",
"\x1B!;" => "\x1b",
"\x1B!<" => "\x1c",
"\x1B!=" => "\x1d",
"\x1B!>" => "\x1e",
"\x1B!?" => "\x1f",
"\x1B'?" => "\x7f",
"\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
];
/**
* Decimal separator.
*/
private static ?string $decimalSeparator = null;
/**
* Thousands separator.
*/
private static ?string $thousandsSeparator = null;
/**
* Currency code.
*/
private static ?string $currencyCode = null;
/**
* Is iconv extension avalable?
*/
private static ?bool $isIconvEnabled = null;
/**
* iconv options.
*/
private static string $iconvOptions = '//IGNORE//TRANSLIT';
/**
* Get whether iconv extension is available.
*/
public static function getIsIconvEnabled(): bool
{
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;
}
/**
* Convert from OpenXML escaped control character to PHP control character.
*
* Excel 2007 team:
* ----------------
* That's correct, control characters are stored directly in the shared-strings table.
* We do encode characters that cannot be represented in XML using the following escape sequence:
* _xHHHH_ where H represents a hexadecimal character in the character's value...
* So you could end up with something like _x0008_ in a string (either in a cell value (<v>)
* element or in the shared string <t> element.
*
* @param string $textValue Value to unescape
*/
public static function controlCharacterOOXML2PHP(string $textValue): string
{
return str_replace(self::CONTROL_CHARACTERS_VALUES, self::CONTROL_CHARACTERS_KEYS, $textValue);
}
/**
* Convert from PHP control character to OpenXML escaped control character.
*
* Excel 2007 team:
* ----------------
* That's correct, control characters are stored directly in the shared-strings table.
* We do encode characters that cannot be represented in XML using the following escape sequence:
* _xHHHH_ where H represents a hexadecimal character in the character's value...
* So you could end up with something like _x0008_ in a string (either in a cell value (<v>)
* element or in the shared string <t> element.
*
* @param string $textValue Value to escape
*/
public static function controlCharacterPHP2OOXML(string $textValue): string
{
return str_replace(self::CONTROL_CHARACTERS_KEYS, self::CONTROL_CHARACTERS_VALUES, $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
$returnValue = (string) mb_convert_encoding($textValue, 'UTF-8', 'UTF-8');
mb_substitute_character($subst);
return $returnValue;
}
/**
* 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.
*/
public static function formatNumber(float|int|string|null $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 array<int, array{strlen: int, fontidx: int}> $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
{
// characters
$chars = self::convertEncoding($textValue, 'UTF-16LE', 'UTF-8');
$ln = (int) (strlen($chars) / 2); // N.B. - strlen, not mb_strlen issue #642
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 (string) 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.
*
* @return string[]
*/
public static function mbStrSplit(string $string): array
{
// Split at all position not after the start: ^
// and not before the end: $
$split = Preg::split('/(?<!^)(?!$)/u', $string);
return $split;
}
/**
* Reverse the case of a string, so that all uppercase characters become lowercase
* and all lowercase characters become uppercase.
*
* @param string $textValue UTF-8 encoded string
*/
public static function strCaseReverse(string $textValue): string
{
$characters = self::mbStrSplit($textValue);
foreach ($characters as &$character) {
if (self::mbIsUpper($character)) {
$character = mb_strtolower($character, 'UTF-8');
} else {
$character = mb_strtoupper($character, 'UTF-8');
}
}
return implode('', $characters);
}
private static function useAlt(string $altValue, string $default, bool $trimAlt): string
{
return ($trimAlt ? trim($altValue) : $altValue) ?: $default;
}
private static function getLocaleValue(string $key, string $altKey, string $default, bool $trimAlt = false): string
{
/** @var string[] */
$localeconv = localeconv();
$rslt = $localeconv[$key];
// win-1252 implements Euro as 0x80 plus other symbols
// Not suitable for Composer\Pcre\Preg
if (preg_match('//u', $rslt) !== 1) {
$rslt = '';
}
return $rslt ?: self::useAlt($localeconv[$altKey], $default, $trimAlt);
}
/**
* Get the decimal separator. If it has not yet been set explicitly, try to obtain number
* formatting information from locale.
*/
public static function getDecimalSeparator(): string
{
if (!isset(self::$decimalSeparator)) {
self::$decimalSeparator = self::getLocaleValue('decimal_point', 'mon_decimal_point', '.');
}
return self::$decimalSeparator;
}
/**
* Set the decimal separator. Only used by NumberFormat::toFormattedString()
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
*
* @param ?string $separator Character for decimal separator
*/
public static function setDecimalSeparator(?string $separator): void
{
self::$decimalSeparator = $separator;
}
/**
* Get the thousands separator. If it has not yet been set explicitly, try to obtain number
* formatting information from locale.
*/
public static function getThousandsSeparator(): string
{
if (!isset(self::$thousandsSeparator)) {
self::$thousandsSeparator = self::getLocaleValue('thousands_sep', 'mon_thousands_sep', ',');
}
return self::$thousandsSeparator;
}
/**
* Set the thousands separator. Only used by NumberFormat::toFormattedString()
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
*
* @param ?string $separator Character for thousands separator
*/
public static function setThousandsSeparator(?string $separator): void
{
self::$thousandsSeparator = $separator;
}
/**
* Get the currency code. If it has not yet been set explicitly, try to obtain the
* symbol information from locale.
*/
public static function getCurrencyCode(bool $trimAlt = false): string
{
if (!isset(self::$currencyCode)) {
self::$currencyCode = self::getLocaleValue('currency_symbol', 'int_curr_symbol', '$', $trimAlt);
}
return self::$currencyCode;
}
/**
* Set the currency code. Only used by NumberFormat::toFormattedString()
* to format output by \PhpOffice\PhpSpreadsheet\Writer\Html and \PhpOffice\PhpSpreadsheet\Writer\Pdf.
*
* @param ?string $currencyCode Character for currency code
*/
public static function setCurrencyCode(?string $currencyCode): void
{
self::$currencyCode = $currencyCode;
}
/**
* Convert SYLK encoded string to UTF-8.
*
* @param string $textValue SYLK encoded string
*
* @return string UTF-8 encoded string
*/
public static function SYLKtoUTF8(string $textValue): string
{
// If there is no escape character in the string there is nothing to do
if (!str_contains($textValue, "\x1b")) {
return $textValue;
}
foreach (self::SYLK_CHARACTERS as $k => $v) {
$textValue = str_replace($k, $v, $textValue);
}
return $textValue;
}
/**
* Retrieve any leading numeric part of a string, or return the full string if no leading numeric
* (handles basic integer or float, but not exponent or non decimal).
*
* @return float|string string or only the leading numeric part of the string
*/
public static function testStringAsNumeric(string $textValue): float|string
{
if (is_numeric($textValue)) {
return $textValue;
}
$v = (float) $textValue;
return (is_numeric(substr($textValue, 0, strlen((string) $v)))) ? $v : $textValue;
}
public static function strlenAllowNull(?string $string): int
{
return strlen("$string");
}
/** @param bool $convertBool If true, convert bool to locale-aware TRUE/FALSE rather than 1/null-string */
public static function convertToString(mixed $value, bool $throw = true, string $default = '', bool $convertBool = false): string
{
if ($convertBool && is_bool($value)) {
return $value ? Calculation::getTRUE() : Calculation::getFALSE();
}
if (is_float($value)) {
$string = (string) $value;
// look out for scientific notation
if (!Preg::isMatch('/[^-+0-9.]/', $string)) {
$minus = $value < 0 ? '-' : '';
$positive = abs($value);
$floor = floor($positive);
$oldFrac = (string) ($positive - $floor);
$frac = Preg::replace('/^0[.](\d+)$/', '$1', $oldFrac);
if ($frac !== $oldFrac) {
return "$minus$floor.$frac";
}
}
return $string;
}
if ($value === null || is_scalar($value) || $value instanceof Stringable) {
return (string) $value;
}
if ($throw) {
throw new SpreadsheetException('Unable to convert to string');
}
return $default;
}
/**
* Assist with POST items when samples are run in browser.
* Never run as part of unit tests, which are command line.
*
* @codeCoverageIgnore
*/
public static function convertPostToString(string $index, string $default = ''): string
{
if (isset($_POST[$index])) {
return htmlentities(self::convertToString($_POST[$index], false, $default));
}
return $default;
}
/**
* Php introduced str_increment with Php8.3,
* but didn't issue deprecation notices till 8.5.
*
* @codeCoverageIgnore
*/
public static function stringIncrement(string &$str): string
{
if (function_exists('str_increment')) {
$str = str_increment($str); // @phpstan-ignore-line
} else {
++$str; // @phpstan-ignore-line
}
return $str; // @phpstan-ignore-line
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use DateTimeZone;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
class TimeZone
{
/**
* Default Timezone used for date/time conversions.
*/
protected static string $timezone = 'UTC';
/**
* Validate a Timezone name.
*
* @param string $timezoneName Time zone (e.g. 'Europe/London')
*
* @return bool Success or failure
*/
private static function validateTimeZone(string $timezoneName): bool
{
return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), true);
}
/**
* Set the Default Timezone used for date/time conversions.
*
* @param string $timezoneName Time zone (e.g. 'Europe/London')
*
* @return bool Success or failure
*/
public static function setTimeZone(string $timezoneName): bool
{
if (self::validateTimeZone($timezoneName)) {
self::$timezone = $timezoneName;
return true;
}
return false;
}
/**
* Return the Default Timezone used for date/time conversions.
*
* @return string Timezone (e.g. 'Europe/London')
*/
public static function getTimeZone(): string
{
return self::$timezone;
}
/**
* Return the Timezone offset used for date/time conversions to/from UST
* This requires both the timezone and the calculated date/time to allow for local DST.
*
* @param ?string $timezoneName The timezone for finding the adjustment to UST
* @param float|int $timestamp PHP date/time value
*
* @return int Number of seconds for timezone adjustment
*/
public static function getTimeZoneAdjustment(?string $timezoneName, $timestamp): int
{
$timezoneName = $timezoneName ?? self::$timezone;
$dtobj = Date::dateTimeFromTimestamp("$timestamp");
if (!self::validateTimeZone($timezoneName)) {
throw new PhpSpreadsheetException("Invalid timezone $timezoneName");
}
$dtobj->setTimeZone(new DateTimeZone($timezoneName));
return $dtobj->getOffset();
}
}

View File

@@ -0,0 +1,429 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
abstract class BestFit
{
/**
* Indicator flag for a calculation error.
*/
protected bool $error = false;
/**
* Algorithm type to use for best-fit.
*/
protected string $bestFitType = 'undetermined';
/**
* Number of entries in the sets of x- and y-value arrays.
*/
protected int $valueCount;
/**
* X-value dataseries of values.
*
* @var float[]
*/
protected array $xValues = [];
/**
* Y-value dataseries of values.
*
* @var float[]
*/
protected array $yValues = [];
/**
* Flag indicating whether values should be adjusted to Y=0.
*/
protected bool $adjustToZero = false;
/**
* Y-value series of best-fit values.
*
* @var float[]
*/
protected array $yBestFitValues = [];
protected float $goodnessOfFit = 1;
protected float $stdevOfResiduals = 0;
protected float $covariance = 0;
protected float $correlation = 0;
protected float $SSRegression = 0;
protected float $SSResiduals = 0;
protected float $DFResiduals = 0;
protected float $f = 0;
protected float $slope = 0;
protected float $slopeSE = 0;
protected float $intersect = 0;
protected float $intersectSE = 0;
protected float $xOffset = 0;
protected float $yOffset = 0;
public function getError(): bool
{
return $this->error;
}
public function getBestFitType(): string
{
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(float $xValue): float;
/**
* Return the X-Value for a specified value of Y.
*
* @param float $yValue Y-Value
*
* @return float X-Value
*/
abstract public function getValueOfXForY(float $yValue): float;
/**
* Return the original set of X-Values.
*
* @return float[] X-Values
*/
public function getXValues(): array
{
return $this->xValues;
}
/**
* Return the Equation of the best-fit line.
*
* @param int $dp Number of places of decimal precision to display
*/
abstract public function getEquation(int $dp = 0): string;
/**
* Return the Slope of the line.
*
* @param int $dp Number of places of decimal precision to display
*/
public function getSlope(int $dp = 0): float
{
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
*/
public function getSlopeSE(int $dp = 0): float
{
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
*/
public function getIntersect(int $dp = 0): float
{
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
*/
public function getIntersectSE(int $dp = 0): float
{
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
*/
public function getGoodnessOfFit(int $dp = 0): float
{
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
*/
public function getGoodnessOfFitPercent(int $dp = 0): float
{
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
*/
public function getStdevOfResiduals(int $dp = 0): float
{
if ($dp != 0) {
return round($this->stdevOfResiduals, $dp);
}
return $this->stdevOfResiduals;
}
/**
* @param int $dp Number of places of decimal precision to return
*/
public function getSSRegression(int $dp = 0): float
{
if ($dp != 0) {
return round($this->SSRegression, $dp);
}
return $this->SSRegression;
}
/**
* @param int $dp Number of places of decimal precision to return
*/
public function getSSResiduals(int $dp = 0): float
{
if ($dp != 0) {
return round($this->SSResiduals, $dp);
}
return $this->SSResiduals;
}
/**
* @param int $dp Number of places of decimal precision to return
*/
public function getDFResiduals(int $dp = 0): float
{
if ($dp != 0) {
return round($this->DFResiduals, $dp);
}
return $this->DFResiduals;
}
/**
* @param int $dp Number of places of decimal precision to return
*/
public function getF(int $dp = 0): float
{
if ($dp != 0) {
return round($this->f, $dp);
}
return $this->f;
}
/**
* @param int $dp Number of places of decimal precision to return
*/
public function getCovariance(int $dp = 0): float
{
if ($dp != 0) {
return round($this->covariance, $dp);
}
return $this->covariance;
}
/**
* @param int $dp Number of places of decimal precision to return
*/
public function getCorrelation(int $dp = 0): float
{
if ($dp != 0) {
return round($this->correlation, $dp);
}
return $this->correlation;
}
/**
* @return float[]
*/
public function getYBestFitValues(): array
{
return $this->yBestFitValues;
}
protected function calculateGoodnessOfFit(float $sumX, float $sumY, float $sumX2, float $sumY2, float $sumXY, float $meanX, float $meanY, bool|int $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);
}
if ($SStot == 0.0 || $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;
}
}
}
/**
* @param array<float|int> $values
*
* @return float|int
*/
private function sumSquares(array $values)
{
return array_sum(
array_map(
fn ($value): float|int => $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(array $yValues, array $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;
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
class ExponentialBestFit extends BestFit
{
/**
* Algorithm type to use for best-fit
* (Name of this Trend class).
*/
protected string $bestFitType = 'exponential';
/**
* Return the Y-Value for a specified value of X.
*
* @param float $xValue X-Value
*
* @return float Y-Value
*/
public function getValueOfYForX(float $xValue): float
{
return $this->getIntersect() * $this->getSlope() ** ($xValue - $this->xOffset);
}
/**
* Return the X-Value for a specified value of Y.
*
* @param float $yValue Y-Value
*
* @return float X-Value
*/
public function getValueOfXForY(float $yValue): float
{
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
*/
public function getEquation(int $dp = 0): string
{
$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
*/
public function getSlope(int $dp = 0): float
{
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
*/
public function getIntersect(int $dp = 0): float
{
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(
fn ($value): float => ($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
*/
public function __construct(array $yValues, array $xValues = [], bool $const = true)
{
parent::__construct($yValues, $xValues);
if (!$this->error) {
$this->exponentialRegression($yValues, $xValues, (bool) $const);
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
class LinearBestFit extends BestFit
{
/**
* Algorithm type to use for best-fit
* (Name of this Trend class).
*/
protected string $bestFitType = 'linear';
/**
* Return the Y-Value for a specified value of X.
*
* @param float $xValue X-Value
*
* @return float Y-Value
*/
public function getValueOfYForX(float $xValue): float
{
return $this->getIntersect() + $this->getSlope() * $xValue;
}
/**
* Return the X-Value for a specified value of Y.
*
* @param float $yValue Y-Value
*
* @return float X-Value
*/
public function getValueOfXForY(float $yValue): float
{
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
*/
public function getEquation(int $dp = 0): string
{
$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
*/
public function __construct(array $yValues, array $xValues = [], bool $const = true)
{
parent::__construct($yValues, $xValues);
if (!$this->error) {
$this->linearRegression($yValues, $xValues, (bool) $const);
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
class LogarithmicBestFit extends BestFit
{
/**
* Algorithm type to use for best-fit
* (Name of this Trend class).
*/
protected string $bestFitType = 'logarithmic';
/**
* Return the Y-Value for a specified value of X.
*
* @param float $xValue X-Value
*
* @return float Y-Value
*/
public function getValueOfYForX(float $xValue): float
{
return $this->getIntersect() + $this->getSlope() * log($xValue - $this->xOffset);
}
/**
* Return the X-Value for a specified value of Y.
*
* @param float $yValue Y-Value
*
* @return float X-Value
*/
public function getValueOfXForY(float $yValue): float
{
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
*/
public function getEquation(int $dp = 0): string
{
$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(
fn ($value): float => ($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
*/
public function __construct(array $yValues, array $xValues = [], bool $const = true)
{
parent::__construct($yValues, $xValues);
if (!$this->error) {
$this->logarithmicRegression($yValues, $xValues, (bool) $const);
}
}
}

View File

@@ -0,0 +1,220 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
use Matrix\Matrix;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
// Phpstan and Scrutinizer seem to have legitimate complaints.
// $this->slope is specified where an array is expected in several places.
// But it seems that it should always be float.
// This code is probably not exercised at all in unit tests.
// Private bool property $implemented is set to indicate
// whether this implementation is correct.
class PolynomialBestFit extends BestFit
{
/**
* Algorithm type to use for best-fit
* (Name of this Trend class).
*/
protected string $bestFitType = 'polynomial';
/**
* Polynomial order.
*/
protected int $order = 0;
private bool $implemented = false;
/**
* Return the order of this polynomial.
*/
public function getOrder(): int
{
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(float $xValue): float
{
$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) {
/** @var float $value */
if ($value != 0.0) {
/** @var int $key */
$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(float $yValue): float
{
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
*/
public function getEquation(int $dp = 0): string
{
$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) {
/** @var float|int $value */
if ($value != 0.0) {
$equation .= ' + ' . $value . ' * X';
/** @var int $key */
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
*/
public function getSlope(int $dp = 0): float
{
if ($dp != 0) {
$coefficients = [];
//* @phpstan-ignore-next-line
foreach ($this->slope as $coefficient) {
/** @var float|int $coefficient */
$coefficients[] = round($coefficient, $dp);
}
// @phpstan-ignore-next-line
return $coefficients;
}
return $this->slope;
}
/** @return array<float|int> */
public function getCoefficients(int $dp = 0): array
{
// 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(int $order, array $yValues, array $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 (!is_numeric($r) || abs($r + 0) <= 10 ** (-9)) {
$r = 0;
} else {
$r += 0;
}
$coefficients[] = $r;
}
$this->intersect = (float) array_shift($coefficients);
// Phpstan is 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(int $order, array $yValues, array $xValues = [])
{
if (!$this->implemented) {
throw new SpreadsheetException('Polynomial Best Fit not yet implemented');
}
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;
}
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
class PowerBestFit extends BestFit
{
/**
* Algorithm type to use for best-fit
* (Name of this Trend class).
*/
protected string $bestFitType = 'power';
/**
* Return the Y-Value for a specified value of X.
*
* @param float $xValue X-Value
*
* @return float Y-Value
*/
public function getValueOfYForX(float $xValue): float
{
return $this->getIntersect() * ($xValue - $this->xOffset) ** $this->getSlope();
}
/**
* Return the X-Value for a specified value of Y.
*
* @param float $yValue Y-Value
*
* @return float X-Value
*/
public function getValueOfXForY(float $yValue): float
{
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
*/
public function getEquation(int $dp = 0): string
{
$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
*/
public function getIntersect(int $dp = 0): float
{
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(
fn ($value): float => ($value < 0.0) ? 0 - log(abs($value)) : log($value),
$yValues
);
$adjustedXValues = array_map(
fn ($value): float => ($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
*/
public function __construct(array $yValues, array $xValues = [], bool $const = true)
{
parent::__construct($yValues, $xValues);
if (!$this->error) {
$this->powerRegression($yValues, $xValues, (bool) $const);
}
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared\Trend;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
class Trend
{
const TREND_LINEAR = 'Linear';
const TREND_LOGARITHMIC = 'Logarithmic';
const TREND_EXPONENTIAL = 'Exponential';
const TREND_POWER = 'Power';
const TREND_POLYNOMIAL_2 = 'Polynomial_2';
const TREND_POLYNOMIAL_3 = 'Polynomial_3';
const TREND_POLYNOMIAL_4 = 'Polynomial_4';
const TREND_POLYNOMIAL_5 = 'Polynomial_5';
const TREND_POLYNOMIAL_6 = 'Polynomial_6';
const TREND_BEST_FIT = 'Bestfit';
const TREND_BEST_FIT_NO_POLY = 'Bestfit_no_Polynomials';
/**
* Names of the best-fit Trend analysis methods.
*/
private const TREND_TYPES = [
self::TREND_LINEAR,
self::TREND_LOGARITHMIC,
self::TREND_EXPONENTIAL,
self::TREND_POWER,
];
/**
* Names of the best-fit Trend polynomial orders.
*
* @var string[]
*/
private static array $trendTypePolynomialOrders = [
self::TREND_POLYNOMIAL_2,
self::TREND_POLYNOMIAL_3,
self::TREND_POLYNOMIAL_4,
self::TREND_POLYNOMIAL_5,
self::TREND_POLYNOMIAL_6,
];
/**
* Cached results for each method when trying to identify which provides the best fit.
*
* @var BestFit[]
*/
private static array $trendCache = [];
/**
* @param mixed[] $yValues
* @param mixed[] $xValues
*/
public static function calculate(string $trendType = self::TREND_BEST_FIT, array $yValues = [], array $xValues = [], bool $const = true): BestFit
{
// Calculate number of points in each dataset
/** @var float[] $xValues */
$nY = count($yValues);
/** @var float[] $xValues */
$nX = count($xValues);
// Define X Values if necessary
if ($nX === 0) {
$xValues = range(1, $nY);
} elseif ($nY !== $nX) {
// Ensure both arrays of points are the same size
throw new SpreadsheetException('Trend(): Number of elements in coordinate arrays do not match.');
}
$key = md5($trendType . $const . serialize($yValues) . serialize($xValues));
// Determine which Trend method has been requested
switch ($trendType) {
// Instantiate and return the class for the requested Trend method
case self::TREND_LINEAR:
case self::TREND_LOGARITHMIC:
case self::TREND_EXPONENTIAL:
case self::TREND_POWER:
if (!isset(self::$trendCache[$key])) {
/** @var float[] $yValues */
$className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendType . 'BestFit';
/** @var float[] $xValues */
self::$trendCache[$key] = new $className($yValues, $xValues, $const);
}
return self::$trendCache[$key];
case self::TREND_POLYNOMIAL_2:
case self::TREND_POLYNOMIAL_3:
case self::TREND_POLYNOMIAL_4:
case self::TREND_POLYNOMIAL_5:
case self::TREND_POLYNOMIAL_6:
if (!isset(self::$trendCache[$key])) {
$order = (int) substr($trendType, -1);
/** @var float[] $yValues */
self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues);
}
return self::$trendCache[$key];
case self::TREND_BEST_FIT:
case self::TREND_BEST_FIT_NO_POLY:
// If the request is to determine the best fit regression, then we test each Trend line in turn
// Start by generating an instance of each available Trend method
/** @var float[] $yValues */
$bestFit = [];
/** @var float[] $xValues */
$bestFitValue = [];
foreach (self::TREND_TYPES as $trendMethod) {
$className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendMethod . 'BestFit';
$bestFit[$trendMethod] = new $className($yValues, $xValues, $const);
$bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit();
}
if ($trendType !== self::TREND_BEST_FIT_NO_POLY) {
foreach (self::$trendTypePolynomialOrders as $trendMethod) {
$order = (int) substr($trendMethod, -1);
$bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues);
if ($bestFit[$trendMethod]->getError()) {
unset($bestFit[$trendMethod]);
} else {
$bestFitValue[$trendMethod] = $bestFit[$trendMethod]->getGoodnessOfFit();
}
}
}
// Determine which of our Trend lines is the best fit, and then we return the instance of that Trend class
arsort($bestFitValue);
$bestFitType = key($bestFitValue);
return $bestFit[$bestFitType];
default:
throw new SpreadsheetException("Unknown trend type $trendType");
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
class XMLWriter extends \XMLWriter
{
public static bool $debugEnabled = false;
/** Temporary storage method */
const STORAGE_MEMORY = 1;
const STORAGE_DISK = 2;
/**
* Temporary filename.
*/
private string $tempFileName = '';
/**
* Create a new XMLWriter instance.
*
* @param int $temporaryStorage Temporary storage location
* @param ?string $temporaryStorageFolder Temporary storage folder
*/
public function __construct(int $temporaryStorage = self::STORAGE_MEMORY, ?string $temporaryStorageFolder = null)
{
// Open temporary storage
if ($temporaryStorage == self::STORAGE_MEMORY) {
$this->openMemory();
} else {
// Create temporary filename
if ($temporaryStorageFolder === null) {
$temporaryStorageFolder = File::sysGetTempDir();
}
$this->tempFileName = (string) @tempnam($temporaryStorageFolder, 'xml');
// Open storage
if (empty($this->tempFileName) || $this->openUri($this->tempFileName) === false) {
// Fallback to memory...
$this->openMemory();
}
}
// Set default values
if (self::$debugEnabled) {
$this->setIndent(true);
}
}
/**
* Destructor.
*/
public function __destruct()
{
// Unlink temporary files
// There is nothing reasonable to do if unlink fails.
if ($this->tempFileName != '') {
@unlink($this->tempFileName);
}
}
public function __wakeup(): void
{
$this->tempFileName = '';
throw new SpreadsheetException('Unserialize not permitted');
}
/**
* Get written data.
*/
public function getData(): string
{
if ($this->tempFileName == '') {
return $this->outputMemory(true);
}
$this->flush();
return file_get_contents($this->tempFileName) ?: '';
}
/**
* Wrapper method for writeRaw.
*
* @param null|string|string[] $rawTextData
*/
public function writeRawData($rawTextData): bool
{
if (is_array($rawTextData)) {
$rawTextData = implode("\n", $rawTextData);
}
return $this->text($rawTextData ?? '');
}
}

View File

@@ -0,0 +1,275 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Shared;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Helper\Dimension;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class Xls
{
/**
* Get the width of a column in pixels. We use the relationship y = ceil(7x) where
* x is the width in intrinsic Excel units (measuring width in number of normal characters)
* This holds for Arial 10.
*
* @param Worksheet $worksheet The sheet
* @param string $col The column
*
* @return int The width in pixels
*/
public static function sizeCol(Worksheet $worksheet, string $col = 'A'): int
{
// default font of the workbook
$font = $worksheet->getParentOrThrow()->getDefaultStyle()->getFont();
$columnDimensions = $worksheet->getColumnDimensions();
// first find the true column width in pixels (uncollapsed and unhidden)
if (isset($columnDimensions[$col]) && $columnDimensions[$col]->getWidth() != -1) {
// then we have column dimension with explicit width
$columnDimension = $columnDimensions[$col];
$width = $columnDimension->getWidth();
$pixelWidth = Drawing::cellDimensionToPixels($width, $font);
} elseif ($worksheet->getDefaultColumnDimension()->getWidth() != -1) {
// then we have default column dimension with explicit width
$defaultColumnDimension = $worksheet->getDefaultColumnDimension();
$width = $defaultColumnDimension->getWidth();
$pixelWidth = Drawing::cellDimensionToPixels($width, $font);
} else {
// we don't even have any default column dimension. Width depends on default font
$pixelWidth = Font::getDefaultColumnWidthByFont($font, true);
}
// now find the effective column width in pixels
if (isset($columnDimensions[$col]) && !$columnDimensions[$col]->getVisible()) {
$effectivePixelWidth = 0;
} else {
$effectivePixelWidth = $pixelWidth;
}
return $effectivePixelWidth;
}
/**
* Convert the height of a cell from user's units to pixels. By interpolation
* the relationship is: y = 4/3x. If the height hasn't been set by the user we
* use the default value. If the row is hidden we use a value of zero.
*
* @param Worksheet $worksheet The sheet
* @param int $row The row index (1-based)
*
* @return int The width in pixels
*/
public static function sizeRow(Worksheet $worksheet, int $row = 1): int
{
// default font of the workbook
$font = $worksheet->getParentOrThrow()->getDefaultStyle()->getFont();
$rowDimensions = $worksheet->getRowDimensions();
// first find the true row height in pixels (uncollapsed and unhidden)
if (isset($rowDimensions[$row]) && $rowDimensions[$row]->getRowHeight() != -1) {
// then we have a row dimension
$rowDimension = $rowDimensions[$row];
$rowHeight = $rowDimension->getRowHeight();
$pixelRowHeight = (int) ceil(4 * $rowHeight / 3); // here we assume Arial 10
} elseif ($worksheet->getDefaultRowDimension()->getRowHeight() != -1) {
// then we have a default row dimension with explicit height
$defaultRowDimension = $worksheet->getDefaultRowDimension();
$pixelRowHeight = $defaultRowDimension->getRowHeight(Dimension::UOM_PIXELS);
} else {
// we don't even have any default row dimension. Height depends on default font
$pointRowHeight = Font::getDefaultRowHeightByFont($font);
$pixelRowHeight = Font::fontSizeToPixels((int) $pointRowHeight);
}
// now find the effective row height in pixels
if (isset($rowDimensions[$row]) && !$rowDimensions[$row]->getVisible()) {
$effectivePixelRowHeight = 0;
} else {
$effectivePixelRowHeight = $pixelRowHeight;
}
return (int) $effectivePixelRowHeight;
}
/**
* Get the horizontal distance in pixels between two anchors
* The distanceX is found as sum of all the spanning columns widths minus correction for the two offsets.
*
* @param float|int $startOffsetX Offset within start cell measured in 1/1024 of the cell width
* @param float|int $endOffsetX Offset within end cell measured in 1/1024 of the cell width
*
* @return int Horizontal measured in pixels
*/
public static function getDistanceX(Worksheet $worksheet, string $startColumn = 'A', float|int $startOffsetX = 0, string $endColumn = 'A', float|int $endOffsetX = 0): int
{
$distanceX = 0;
// add the widths of the spanning columns
$startColumnIndex = Coordinate::columnIndexFromString($startColumn);
$endColumnIndex = Coordinate::columnIndexFromString($endColumn);
for ($i = $startColumnIndex; $i <= $endColumnIndex; ++$i) {
$distanceX += self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($i));
}
// correct for offsetX in startcell
$distanceX -= (int) floor(self::sizeCol($worksheet, $startColumn) * $startOffsetX / 1024);
// correct for offsetX in endcell
$distanceX -= (int) floor(self::sizeCol($worksheet, $endColumn) * (1 - $endOffsetX / 1024));
return $distanceX;
}
/**
* Get the vertical distance in pixels between two anchors
* The distanceY is found as sum of all the spanning rows minus two offsets.
*
* @param int $startRow (1-based)
* @param float|int $startOffsetY Offset within start cell measured in 1/256 of the cell height
* @param int $endRow (1-based)
* @param float|int $endOffsetY Offset within end cell measured in 1/256 of the cell height
*
* @return int Vertical distance measured in pixels
*/
public static function getDistanceY(Worksheet $worksheet, int $startRow = 1, float|int $startOffsetY = 0, int $endRow = 1, float|int $endOffsetY = 0): int
{
$distanceY = 0;
// add the widths of the spanning rows
for ($row = $startRow; $row <= $endRow; ++$row) {
$distanceY += self::sizeRow($worksheet, $row);
}
// correct for offsetX in startcell
$distanceY -= (int) floor(self::sizeRow($worksheet, $startRow) * $startOffsetY / 256);
// correct for offsetX in endcell
$distanceY -= (int) floor(self::sizeRow($worksheet, $endRow) * (1 - $endOffsetY / 256));
return $distanceY;
}
/**
* Convert 1-cell anchor coordinates to 2-cell anchor coordinates
* This function is ported from PEAR Spreadsheet_Writer_Excel with small modifications.
*
* Calculate the vertices that define the position of the image as required by
* the OBJ record.
*
* +------------+------------+
* | A | B |
* +-----+------------+------------+
* | |(x1,y1) | |
* | 1 |(A1)._______|______ |
* | | | | |
* | | | | |
* +-----+----| BITMAP |-----+
* | | | | |
* | 2 | |______________. |
* | | | (B2)|
* | | | (x2,y2)|
* +---- +------------+------------+
*
* Example of a bitmap that covers some of the area from cell A1 to cell B2.
*
* Based on the width and height of the bitmap we need to calculate 8 vars:
* $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2.
* The width and height of the cells are also variable and have to be taken into
* account.
* The values of $col_start and $row_start are passed in from the calling
* function. The values of $col_end and $row_end are calculated by subtracting
* the width and height of the bitmap from the width and height of the
* underlying cells.
* The vertices are expressed as a percentage of the underlying cell width as
* follows (rhs values are in pixels):
*
* x1 = X / W *1024
* y1 = Y / H *256
* x2 = (X-1) / W *1024
* y2 = (Y-1) / H *256
*
* Where: X is distance from the left side of the underlying cell
* Y is distance from the top of the underlying cell
* W is the width of the cell
* H is the height of the cell
*
* @param string $coordinates E.g. 'A1'
* @param int $offsetX Horizontal offset in pixels
* @param int $offsetY Vertical offset in pixels
* @param int $width Width in pixels
* @param int $height Height in pixels
*
* @return array{startCoordinates: string, startOffsetX: int, startOffsetY: int, endCoordinates: string, endOffsetX: int, endOffsetY: int}
*/
public static function oneAnchor2twoAnchor(Worksheet $worksheet, string $coordinates, int $offsetX, int $offsetY, int $width, int $height): ?array
{
[$col_start, $row] = Coordinate::indexesFromString($coordinates);
$row_start = $row - 1;
$x1 = $offsetX;
$y1 = $offsetY;
// Initialise end cell to the same as the start cell
$col_end = $col_start; // Col containing lower right corner of object
$row_end = $row_start; // Row containing bottom right corner of object
// Zero the specified offset if greater than the cell dimensions
if ($x1 >= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start))) {
$x1 = 0;
}
if ($y1 >= self::sizeRow($worksheet, $row_start + 1)) {
$y1 = 0;
}
$width = $width + $x1 - 1;
$height = $height + $y1 - 1;
// Subtract the underlying cell widths to find the end cell of the image
while ($width >= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end))) {
$width -= self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end));
++$col_end;
}
// Subtract the underlying cell heights to find the end cell of the image
while ($height >= self::sizeRow($worksheet, $row_end + 1)) {
$height -= self::sizeRow($worksheet, $row_end + 1);
++$row_end;
}
// Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
// with zero height or width.
if (self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start)) == 0) {
return null;
}
if (self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end)) == 0) {
return null;
}
if (self::sizeRow($worksheet, $row_start + 1) == 0) {
return null;
}
if (self::sizeRow($worksheet, $row_end + 1) == 0) {
return null;
}
// Convert the pixel values to the percentage value expected by Excel
$x1 = $x1 / self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_start)) * 1024;
$y1 = $y1 / self::sizeRow($worksheet, $row_start + 1) * 256;
$x2 = ($width + 1) / self::sizeCol($worksheet, Coordinate::stringFromColumnIndex($col_end)) * 1024; // Distance to right side of object
$y2 = ($height + 1) / self::sizeRow($worksheet, $row_end + 1) * 256; // Distance to bottom of object
$startCoordinates = Coordinate::stringFromColumnIndex($col_start) . ($row_start + 1);
$endCoordinates = Coordinate::stringFromColumnIndex($col_end) . ($row_end + 1);
return [
'startCoordinates' => $startCoordinates,
'startOffsetX' => $x1,
'startOffsetY' => $y1,
'endCoordinates' => $endCoordinates,
'endOffsetX' => $x2,
'endOffsetY' => $y2,
];
}
}