| 1 |
<?php |
| 2 |
/** |
| 3 |
* @package dompdf |
| 4 |
* @link https://github.com/dompdf/dompdf |
| 5 |
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License |
| 6 |
*/ |
| 7 |
namespace Dompdf; |
| 8 |
|
| 9 |
class Helpers |
| 10 |
{ |
| 11 |
/** |
| 12 |
* print_r wrapper for html/cli output |
| 13 |
* |
| 14 |
* Wraps print_r() output in < pre > tags if the current sapi is not 'cli'. |
| 15 |
* Returns the output string instead of displaying it if $return is true. |
| 16 |
* |
| 17 |
* @param mixed $mixed variable or expression to display |
| 18 |
* @param bool $return |
| 19 |
* |
| 20 |
* @return string|null |
| 21 |
*/ |
| 22 |
public static function pre_r($mixed, $return = false) |
| 23 |
{ |
| 24 |
if ($return) { |
| 25 |
return "<pre>" . print_r($mixed, true) . "</pre>"; |
| 26 |
} |
| 27 |
|
| 28 |
if (php_sapi_name() !== "cli") { |
| 29 |
echo "<pre>"; |
| 30 |
} |
| 31 |
|
| 32 |
print_r($mixed); |
| 33 |
|
| 34 |
if (php_sapi_name() !== "cli") { |
| 35 |
echo "</pre>"; |
| 36 |
} else { |
| 37 |
echo "\n"; |
| 38 |
} |
| 39 |
|
| 40 |
flush(); |
| 41 |
|
| 42 |
return null; |
| 43 |
} |
| 44 |
|
| 45 |
/** |
| 46 |
* builds a full url given a protocol, hostname, base path and url |
| 47 |
* |
| 48 |
* @param string $protocol |
| 49 |
* @param string $host |
| 50 |
* @param string $base_path |
| 51 |
* @param string $url |
| 52 |
* @return string |
| 53 |
* |
| 54 |
* Initially the trailing slash of $base_path was optional, and conditionally appended. |
| 55 |
* However on dynamically created sites, where the page is given as url parameter, |
| 56 |
* the base path might not end with an url. |
| 57 |
* Therefore do not append a slash, and **require** the $base_url to ending in a slash |
| 58 |
* when needed. |
| 59 |
* Vice versa, on using the local file system path of a file, make sure that the slash |
| 60 |
* is appended (o.k. also for Windows) |
| 61 |
*/ |
| 62 |
public static function build_url($protocol, $host, $base_path, $url) |
| 63 |
{ |
| 64 |
$protocol = mb_strtolower($protocol); |
| 65 |
if (empty($protocol)) { |
| 66 |
$protocol = "file://"; |
| 67 |
} |
| 68 |
if ($url === "") { |
| 69 |
return null; |
| 70 |
} |
| 71 |
|
| 72 |
$url_lc = mb_strtolower($url); |
| 73 |
|
| 74 |
// Is the url already fully qualified, a Data URI, or a reference to a named anchor? |
| 75 |
// File-protocol URLs may require additional processing (e.g. for URLs with a relative path) |
| 76 |
if ( |
| 77 |
( |
| 78 |
mb_strpos($url_lc, "://") !== false |
| 79 |
&& !in_array(substr($url_lc, 0, 7), ["file://", "phar://"], true) |
| 80 |
) |
| 81 |
|| mb_substr($url_lc, 0, 1) === "#" |
| 82 |
|| mb_strpos($url_lc, "data:") === 0 |
| 83 |
|| mb_strpos($url_lc, "mailto:") === 0 |
| 84 |
|| mb_strpos($url_lc, "tel:") === 0 |
| 85 |
) { |
| 86 |
return $url; |
| 87 |
} |
| 88 |
|
| 89 |
$res = ""; |
| 90 |
if (strpos($url_lc, "file://") === 0) { |
| 91 |
$url = substr($url, 7); |
| 92 |
$protocol = "file://"; |
| 93 |
} elseif (strpos($url_lc, "phar://") === 0) { |
| 94 |
$res = substr($url, strpos($url_lc, ".phar")+5); |
| 95 |
$url = substr($url, 7, strpos($url_lc, ".phar")-2); |
| 96 |
$protocol = "phar://"; |
| 97 |
} |
| 98 |
|
| 99 |
$ret = ""; |
| 100 |
|
| 101 |
$is_local_path = in_array($protocol, ["file://", "phar://"], true); |
| 102 |
|
| 103 |
if ($is_local_path) { |
| 104 |
//On Windows local file, an abs path can begin also with a '\' or a drive letter and colon |
| 105 |
//drive: followed by a relative path would be a drive specific default folder. |
| 106 |
//not known in php app code, treat as abs path |
| 107 |
//($url[1] !== ':' || ($url[2]!=='\\' && $url[2]!=='/')) |
| 108 |
if ($url[0] !== '/' && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN' || (mb_strlen($url) > 1 && $url[0] !== '\\' && $url[1] !== ':'))) { |
| 109 |
// For rel path and local access we ignore the host, and run the path through realpath() |
| 110 |
$ret .= realpath($base_path) . '/'; |
| 111 |
} |
| 112 |
$ret .= $url; |
| 113 |
$ret = preg_replace('/\?(.*)$/', "", $ret); |
| 114 |
|
| 115 |
$filepath = realpath($ret); |
| 116 |
if ($filepath === false) { |
| 117 |
return null; |
| 118 |
} |
| 119 |
|
| 120 |
$ret = "$protocol$filepath$res"; |
| 121 |
|
| 122 |
return $ret; |
| 123 |
} |
| 124 |
|
| 125 |
$ret = $protocol; |
| 126 |
// Protocol relative urls (e.g. "//example.org/style.css") |
| 127 |
if (strpos($url, '//') === 0) { |
| 128 |
$ret .= substr($url, 2); |
| 129 |
//remote urls with backslash in html/css are not really correct, but lets be genereous |
| 130 |
} elseif ($url[0] === '/' || $url[0] === '\\') { |
| 131 |
// Absolute path |
| 132 |
$ret .= $host . $url; |
| 133 |
} else { |
| 134 |
// Relative path |
| 135 |
//$base_path = $base_path !== "" ? rtrim($base_path, "/\\") . "/" : ""; |
| 136 |
$ret .= $host . $base_path . $url; |
| 137 |
} |
| 138 |
|
| 139 |
// URL should now be complete, final cleanup |
| 140 |
$parsed_url = parse_url($ret); |
| 141 |
|
| 142 |
// reproduced from https://www.php.net/manual/en/function.parse-url.php#106731 |
| 143 |
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; |
| 144 |
$host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; |
| 145 |
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; |
| 146 |
$user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; |
| 147 |
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; |
| 148 |
$pass = ($user || $pass) ? "$pass@" : ''; |
| 149 |
$path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; |
| 150 |
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; |
| 151 |
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; |
| 152 |
|
| 153 |
// partially reproduced from https://stackoverflow.com/a/1243431/264628 |
| 154 |
/* replace '//' or '/./' or '/foo/../' with '/' */ |
| 155 |
$re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#'); |
| 156 |
for ($n=1; $n>0; $path=preg_replace($re, '/', $path, -1, $n)) {} |
| 157 |
|
| 158 |
$ret = "$scheme$user$pass$host$port$path$query$fragment"; |
| 159 |
|
| 160 |
return $ret; |
| 161 |
} |
| 162 |
|
| 163 |
/** |
| 164 |
* Builds a HTTP Content-Disposition header string using `$dispositionType` |
| 165 |
* and `$filename`. |
| 166 |
* |
| 167 |
* If the filename contains any characters not in the ISO-8859-1 character |
| 168 |
* set, a fallback filename will be included for clients not supporting the |
| 169 |
* `filename*` parameter. |
| 170 |
* |
| 171 |
* @param string $dispositionType |
| 172 |
* @param string $filename |
| 173 |
* @return string |
| 174 |
*/ |
| 175 |
public static function buildContentDispositionHeader($dispositionType, $filename) |
| 176 |
{ |
| 177 |
$encoding = mb_detect_encoding($filename); |
| 178 |
$fallbackfilename = mb_convert_encoding($filename, "ISO-8859-1", $encoding); |
| 179 |
$fallbackfilename = str_replace("\"", "", $fallbackfilename); |
| 180 |
$encodedfilename = rawurlencode($filename); |
| 181 |
|
| 182 |
$contentDisposition = "Content-Disposition: $dispositionType; filename=\"$fallbackfilename\""; |
| 183 |
if ($fallbackfilename !== $filename) { |
| 184 |
$contentDisposition .= "; filename*=UTF-8''$encodedfilename"; |
| 185 |
} |
| 186 |
|
| 187 |
return $contentDisposition; |
| 188 |
} |
| 189 |
|
| 190 |
/** |
| 191 |
* Converts decimal numbers to roman numerals. |
| 192 |
* |
| 193 |
* As numbers larger than 3999 (and smaller than 1) cannot be represented in |
| 194 |
* the standard form of roman numerals, those are left in decimal form. |
| 195 |
* |
| 196 |
* See https://en.wikipedia.org/wiki/Roman_numerals#Standard_form |
| 197 |
* |
| 198 |
* @param int|string $num |
| 199 |
* |
| 200 |
* @throws Exception |
| 201 |
* @return string |
| 202 |
*/ |
| 203 |
public static function dec2roman($num): string |
| 204 |
{ |
| 205 |
|
| 206 |
static $ones = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"]; |
| 207 |
static $tens = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"]; |
| 208 |
static $hund = ["", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm"]; |
| 209 |
static $thou = ["", "m", "mm", "mmm"]; |
| 210 |
|
| 211 |
if (!is_numeric($num)) { |
| 212 |
throw new Exception("dec2roman() requires a numeric argument."); |
| 213 |
} |
| 214 |
|
| 215 |
if ($num >= 4000 || $num <= 0) { |
| 216 |
return (string) $num; |
| 217 |
} |
| 218 |
|
| 219 |
$num = strrev((string)$num); |
| 220 |
|
| 221 |
$ret = ""; |
| 222 |
switch (mb_strlen($num)) { |
| 223 |
/** @noinspection PhpMissingBreakStatementInspection */ |
| 224 |
case 4: |
| 225 |
$ret .= $thou[$num[3]]; |
| 226 |
/** @noinspection PhpMissingBreakStatementInspection */ |
| 227 |
case 3: |
| 228 |
$ret .= $hund[$num[2]]; |
| 229 |
/** @noinspection PhpMissingBreakStatementInspection */ |
| 230 |
case 2: |
| 231 |
$ret .= $tens[$num[1]]; |
| 232 |
/** @noinspection PhpMissingBreakStatementInspection */ |
| 233 |
case 1: |
| 234 |
$ret .= $ones[$num[0]]; |
| 235 |
default: |
| 236 |
break; |
| 237 |
} |
| 238 |
|
| 239 |
return $ret; |
| 240 |
} |
| 241 |
|
| 242 |
/** |
| 243 |
* Restrict a length to the given range. |
| 244 |
* |
| 245 |
* If min > max, the result is min. |
| 246 |
* |
| 247 |
* @param float $length |
| 248 |
* @param float $min |
| 249 |
* @param float $max |
| 250 |
* |
| 251 |
* @return float |
| 252 |
*/ |
| 253 |
public static function clamp(float $length, float $min, float $max): float |
| 254 |
{ |
| 255 |
return max($min, min($length, $max)); |
| 256 |
} |
| 257 |
|
| 258 |
/** |
| 259 |
* Determines whether $value is a percentage or not |
| 260 |
* |
| 261 |
* @param string|float|int $value |
| 262 |
* |
| 263 |
* @return bool |
| 264 |
*/ |
| 265 |
public static function is_percent($value): bool |
| 266 |
{ |
| 267 |
return is_string($value) && false !== mb_strpos($value, "%"); |
| 268 |
} |
| 269 |
|
| 270 |
/** |
| 271 |
* Parses a data URI scheme |
| 272 |
* http://en.wikipedia.org/wiki/Data_URI_scheme |
| 273 |
* |
| 274 |
* @param string $data_uri The data URI to parse |
| 275 |
* |
| 276 |
* @return array|bool The result with charset, mime type and decoded data |
| 277 |
*/ |
| 278 |
public static function parse_data_uri($data_uri) |
| 279 |
{ |
| 280 |
if (!preg_match('/^data:(?P<mime>[a-z0-9\/+-.]+)(;charset=(?P<charset>[a-z0-9-])+)?(?P<base64>;base64)?\,(?P<data>.*)?/is', $data_uri, $match)) { |
| 281 |
return false; |
| 282 |
} |
| 283 |
|
| 284 |
$match['data'] = rawurldecode($match['data']); |
| 285 |
$result = [ |
| 286 |
'charset' => $match['charset'] ? $match['charset'] : 'US-ASCII', |
| 287 |
'mime' => $match['mime'] ? $match['mime'] : 'text/plain', |
| 288 |
'data' => $match['base64'] ? base64_decode($match['data']) : $match['data'], |
| 289 |
]; |
| 290 |
|
| 291 |
return $result; |
| 292 |
} |
| 293 |
|
| 294 |
/** |
| 295 |
* Encodes a Uniform Resource Identifier (URI) by replacing non-alphanumeric |
| 296 |
* characters with a percent (%) sign followed by two hex digits, excepting |
| 297 |
* characters in the URI reserved character set. |
| 298 |
* |
| 299 |
* Assumes that the URI is a complete URI, so does not encode reserved |
| 300 |
* characters that have special meaning in the URI. |
| 301 |
* |
| 302 |
* Simulates the encodeURI function available in JavaScript |
| 303 |
* https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURI |
| 304 |
* |
| 305 |
* Source: http://stackoverflow.com/q/4929584/264628 |
| 306 |
* |
| 307 |
* @param string $uri The URI to encode |
| 308 |
* @return string The original URL with special characters encoded |
| 309 |
*/ |
| 310 |
public static function encodeURI($uri) { |
| 311 |
$unescaped = [ |
| 312 |
'%2D'=>'-','%5F'=>'_','%2E'=>'.','%21'=>'!', '%7E'=>'~', |
| 313 |
'%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')' |
| 314 |
]; |
| 315 |
$reserved = [ |
| 316 |
'%3B'=>';','%2C'=>',','%2F'=>'/','%3F'=>'?','%3A'=>':', |
| 317 |
'%40'=>'@','%26'=>'&','%3D'=>'=','%2B'=>'+','%24'=>'$' |
| 318 |
]; |
| 319 |
$score = [ |
| 320 |
'%23'=>'#' |
| 321 |
]; |
| 322 |
return strtr(rawurlencode(rawurldecode($uri)), array_merge($reserved, $unescaped, $score)); |
| 323 |
} |
| 324 |
|
| 325 |
/** |
| 326 |
* Decoder for RLE8 compression in windows bitmaps |
| 327 |
* http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp |
| 328 |
* |
| 329 |
* @param string $str Data to decode |
| 330 |
* @param int $width Image width |
| 331 |
* |
| 332 |
* @return string |
| 333 |
*/ |
| 334 |
public static function rle8_decode($str, $width) |
| 335 |
{ |
| 336 |
$lineWidth = $width + (3 - ($width - 1) % 4); |
| 337 |
$out = ''; |
| 338 |
$cnt = strlen($str); |
| 339 |
|
| 340 |
for ($i = 0; $i < $cnt; $i++) { |
| 341 |
$o = ord($str[$i]); |
| 342 |
switch ($o) { |
| 343 |
case 0: # ESCAPE |
| 344 |
$i++; |
| 345 |
switch (ord($str[$i])) { |
| 346 |
case 0: # NEW LINE |
| 347 |
$padCnt = $lineWidth - strlen($out) % $lineWidth; |
| 348 |
if ($padCnt < $lineWidth) { |
| 349 |
$out .= str_repeat(chr(0), $padCnt); # pad line |
| 350 |
} |
| 351 |
break; |
| 352 |
case 1: # END OF FILE |
| 353 |
$padCnt = $lineWidth - strlen($out) % $lineWidth; |
| 354 |
if ($padCnt < $lineWidth) { |
| 355 |
$out .= str_repeat(chr(0), $padCnt); # pad line |
| 356 |
} |
| 357 |
break 3; |
| 358 |
case 2: # DELTA |
| 359 |
$i += 2; |
| 360 |
break; |
| 361 |
default: # ABSOLUTE MODE |
| 362 |
$num = ord($str[$i]); |
| 363 |
for ($j = 0; $j < $num; $j++) { |
| 364 |
$out .= $str[++$i]; |
| 365 |
} |
| 366 |
if ($num % 2) { |
| 367 |
$i++; |
| 368 |
} |
| 369 |
} |
| 370 |
break; |
| 371 |
default: |
| 372 |
$out .= str_repeat($str[++$i], $o); |
| 373 |
} |
| 374 |
} |
| 375 |
return $out; |
| 376 |
} |
| 377 |
|
| 378 |
/** |
| 379 |
* Decoder for RLE4 compression in windows bitmaps |
| 380 |
* see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp |
| 381 |
* |
| 382 |
* @param string $str Data to decode |
| 383 |
* @param int $width Image width |
| 384 |
* |
| 385 |
* @return string |
| 386 |
*/ |
| 387 |
public static function rle4_decode($str, $width) |
| 388 |
{ |
| 389 |
$w = floor($width / 2) + ($width % 2); |
| 390 |
$lineWidth = $w + (3 - (($width - 1) / 2) % 4); |
| 391 |
$pixels = []; |
| 392 |
$cnt = strlen($str); |
| 393 |
$c = 0; |
| 394 |
|
| 395 |
for ($i = 0; $i < $cnt; $i++) { |
| 396 |
$o = ord($str[$i]); |
| 397 |
switch ($o) { |
| 398 |
case 0: # ESCAPE |
| 399 |
$i++; |
| 400 |
switch (ord($str[$i])) { |
| 401 |
case 0: # NEW LINE |
| 402 |
while (count($pixels) % $lineWidth != 0) { |
| 403 |
$pixels[] = 0; |
| 404 |
} |
| 405 |
break; |
| 406 |
case 1: # END OF FILE |
| 407 |
while (count($pixels) % $lineWidth != 0) { |
| 408 |
$pixels[] = 0; |
| 409 |
} |
| 410 |
break 3; |
| 411 |
case 2: # DELTA |
| 412 |
$i += 2; |
| 413 |
break; |
| 414 |
default: # ABSOLUTE MODE |
| 415 |
$num = ord($str[$i]); |
| 416 |
for ($j = 0; $j < $num; $j++) { |
| 417 |
if ($j % 2 == 0) { |
| 418 |
$c = ord($str[++$i]); |
| 419 |
$pixels[] = ($c & 240) >> 4; |
| 420 |
} else { |
| 421 |
$pixels[] = $c & 15; |
| 422 |
} |
| 423 |
} |
| 424 |
|
| 425 |
if ($num % 2 == 0) { |
| 426 |
$i++; |
| 427 |
} |
| 428 |
} |
| 429 |
break; |
| 430 |
default: |
| 431 |
$c = ord($str[++$i]); |
| 432 |
for ($j = 0; $j < $o; $j++) { |
| 433 |
$pixels[] = ($j % 2 == 0 ? ($c & 240) >> 4 : $c & 15); |
| 434 |
} |
| 435 |
} |
| 436 |
} |
| 437 |
|
| 438 |
$out = ''; |
| 439 |
if (count($pixels) % 2) { |
| 440 |
$pixels[] = 0; |
| 441 |
} |
| 442 |
|
| 443 |
$cnt = count($pixels) / 2; |
| 444 |
|
| 445 |
for ($i = 0; $i < $cnt; $i++) { |
| 446 |
$out .= chr(16 * $pixels[2 * $i] + $pixels[2 * $i + 1]); |
| 447 |
} |
| 448 |
|
| 449 |
return $out; |
| 450 |
} |
| 451 |
|
| 452 |
/** |
| 453 |
* parse a full url or pathname and return an array(protocol, host, path, |
| 454 |
* file + query + fragment) |
| 455 |
* |
| 456 |
* @param string $url |
| 457 |
* @return array |
| 458 |
*/ |
| 459 |
public static function explode_url($url) |
| 460 |
{ |
| 461 |
$protocol = ""; |
| 462 |
$host = ""; |
| 463 |
$path = ""; |
| 464 |
$file = ""; |
| 465 |
$res = ""; |
| 466 |
|
| 467 |
$arr = parse_url($url); |
| 468 |
if ( isset($arr["scheme"]) ) { |
| 469 |
$arr["scheme"] = mb_strtolower($arr["scheme"]); |
| 470 |
} |
| 471 |
|
| 472 |
if (isset($arr["scheme"]) && $arr["scheme"] !== "file" && $arr["scheme"] !== "phar" && strlen($arr["scheme"]) > 1) { |
| 473 |
$protocol = $arr["scheme"] . "://"; |
| 474 |
|
| 475 |
if (isset($arr["user"])) { |
| 476 |
$host .= $arr["user"]; |
| 477 |
|
| 478 |
if (isset($arr["pass"])) { |
| 479 |
$host .= ":" . $arr["pass"]; |
| 480 |
} |
| 481 |
|
| 482 |
$host .= "@"; |
| 483 |
} |
| 484 |
|
| 485 |
if (isset($arr["host"])) { |
| 486 |
$host .= $arr["host"]; |
| 487 |
} |
| 488 |
|
| 489 |
if (isset($arr["port"])) { |
| 490 |
$host .= ":" . $arr["port"]; |
| 491 |
} |
| 492 |
|
| 493 |
if (isset($arr["path"]) && $arr["path"] !== "") { |
| 494 |
// Do we have a trailing slash? |
| 495 |
if ($arr["path"][mb_strlen($arr["path"]) - 1] === "/") { |
| 496 |
$path = $arr["path"]; |
| 497 |
$file = ""; |
| 498 |
} else { |
| 499 |
$path = rtrim(dirname($arr["path"]), '/\\') . "/"; |
| 500 |
$file = basename($arr["path"]); |
| 501 |
} |
| 502 |
} |
| 503 |
|
| 504 |
if (isset($arr["query"])) { |
| 505 |
$file .= "?" . $arr["query"]; |
| 506 |
} |
| 507 |
|
| 508 |
if (isset($arr["fragment"])) { |
| 509 |
$file .= "#" . $arr["fragment"]; |
| 510 |
} |
| 511 |
|
| 512 |
} else { |
| 513 |
|
| 514 |
$protocol = ""; |
| 515 |
$host = ""; // localhost, really |
| 516 |
|
| 517 |
$i = mb_stripos($url, "://"); |
| 518 |
if ($i !== false) { |
| 519 |
$protocol = mb_strtolower(mb_substr($url, 0, $i + 3)); |
| 520 |
$url = mb_substr($url, $i + 3); |
| 521 |
} else { |
| 522 |
$protocol = "file://"; |
| 523 |
} |
| 524 |
|
| 525 |
if ($protocol === "phar://") { |
| 526 |
$res = substr($url, stripos($url, ".phar")+5); |
| 527 |
$url = substr($url, 7, stripos($url, ".phar")-2); |
| 528 |
} |
| 529 |
|
| 530 |
$file = basename($url); |
| 531 |
$path = dirname($url) . "/"; |
| 532 |
} |
| 533 |
|
| 534 |
$ret = [$protocol, $host, $path, $file, |
| 535 |
"protocol" => $protocol, |
| 536 |
"host" => $host, |
| 537 |
"path" => $path, |
| 538 |
"file" => $file, |
| 539 |
"resource" => $res]; |
| 540 |
return $ret; |
| 541 |
} |
| 542 |
|
| 543 |
/** |
| 544 |
* Print debug messages |
| 545 |
* |
| 546 |
* @param string $type The type of debug messages to print |
| 547 |
* @param string $msg The message to show |
| 548 |
*/ |
| 549 |
public static function dompdf_debug($type, $msg) |
| 550 |
{ |
| 551 |
global $_DOMPDF_DEBUG_TYPES, $_dompdf_show_warnings, $_dompdf_debug; |
| 552 |
if (isset($_DOMPDF_DEBUG_TYPES[$type]) && ($_dompdf_show_warnings || $_dompdf_debug)) { |
| 553 |
$arr = debug_backtrace(); |
| 554 |
|
| 555 |
echo basename($arr[0]["file"]) . " (" . $arr[0]["line"] . "): " . $arr[1]["function"] . ": "; |
| 556 |
Helpers::pre_r($msg); |
| 557 |
} |
| 558 |
} |
| 559 |
|
| 560 |
/** |
| 561 |
* Stores warnings in an array for display later |
| 562 |
* This function allows warnings generated by the DomDocument parser |
| 563 |
* and CSS loader ({@link Stylesheet}) to be captured and displayed |
| 564 |
* later. Without this function, errors are displayed immediately and |
| 565 |
* PDF streaming is impossible. |
| 566 |
* @see http://www.php.net/manual/en/function.set-error_handler.php |
| 567 |
* |
| 568 |
* @param int $errno |
| 569 |
* @param string $errstr |
| 570 |
* @param string $errfile |
| 571 |
* @param string $errline |
| 572 |
* |
| 573 |
* @throws Exception |
| 574 |
*/ |
| 575 |
public static function record_warnings($errno, $errstr, $errfile, $errline) |
| 576 |
{ |
| 577 |
// Not a warning or notice |
| 578 |
if (!($errno & (E_WARNING | E_NOTICE | E_USER_NOTICE | E_USER_WARNING | E_STRICT | E_DEPRECATED | E_USER_DEPRECATED))) { |
| 579 |
throw new Exception($errstr . " $errno"); |
| 580 |
} |
| 581 |
|
| 582 |
global $_dompdf_warnings; |
| 583 |
global $_dompdf_show_warnings; |
| 584 |
|
| 585 |
if ($_dompdf_show_warnings) { |
| 586 |
echo $errstr . "\n"; |
| 587 |
} |
| 588 |
|
| 589 |
$_dompdf_warnings[] = $errstr; |
| 590 |
} |
| 591 |
|
| 592 |
/** |
| 593 |
* @param $c |
| 594 |
* @return bool|string |
| 595 |
*/ |
| 596 |
public static function unichr($c) |
| 597 |
{ |
| 598 |
if ($c <= 0x7F) { |
| 599 |
return chr($c); |
| 600 |
} elseif ($c <= 0x7FF) { |
| 601 |
return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F); |
| 602 |
} elseif ($c <= 0xFFFF) { |
| 603 |
return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F) |
| 604 |
. chr(0x80 | $c & 0x3F); |
| 605 |
} elseif ($c <= 0x10FFFF) { |
| 606 |
return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F) |
| 607 |
. chr(0x80 | $c >> 6 & 0x3F) |
| 608 |
. chr(0x80 | $c & 0x3F); |
| 609 |
} |
| 610 |
return false; |
| 611 |
} |
| 612 |
|
| 613 |
/** |
| 614 |
* Converts a CMYK color to RGB |
| 615 |
* |
| 616 |
* @param float|float[] $c |
| 617 |
* @param float $m |
| 618 |
* @param float $y |
| 619 |
* @param float $k |
| 620 |
* |
| 621 |
* @return float[] |
| 622 |
*/ |
| 623 |
public static function cmyk_to_rgb($c, $m = null, $y = null, $k = null) |
| 624 |
{ |
| 625 |
if (is_array($c)) { |
| 626 |
[$c, $m, $y, $k] = $c; |
| 627 |
} |
| 628 |
|
| 629 |
$c *= 255; |
| 630 |
$m *= 255; |
| 631 |
$y *= 255; |
| 632 |
$k *= 255; |
| 633 |
|
| 634 |
$r = (1 - round(2.55 * ($c + $k))); |
| 635 |
$g = (1 - round(2.55 * ($m + $k))); |
| 636 |
$b = (1 - round(2.55 * ($y + $k))); |
| 637 |
|
| 638 |
if ($r < 0) { |
| 639 |
$r = 0; |
| 640 |
} |
| 641 |
if ($g < 0) { |
| 642 |
$g = 0; |
| 643 |
} |
| 644 |
if ($b < 0) { |
| 645 |
$b = 0; |
| 646 |
} |
| 647 |
|
| 648 |
return [ |
| 649 |
$r, $g, $b, |
| 650 |
"r" => $r, "g" => $g, "b" => $b |
| 651 |
]; |
| 652 |
} |
| 653 |
|
| 654 |
/** |
| 655 |
* getimagesize doesn't give a good size for 32bit BMP image v5 |
| 656 |
* |
| 657 |
* @param string $filename |
| 658 |
* @param resource $context |
| 659 |
* @return array An array of three elements: width and height as |
| 660 |
* `float|int`, and image type as `string|null`. |
| 661 |
*/ |
| 662 |
public static function dompdf_getimagesize($filename, $context = null) |
| 663 |
{ |
| 664 |
static $cache = []; |
| 665 |
|
| 666 |
if (isset($cache[$filename])) { |
| 667 |
return $cache[$filename]; |
| 668 |
} |
| 669 |
|
| 670 |
[$width, $height, $type] = getimagesize($filename); |
| 671 |
|
| 672 |
// Custom types |
| 673 |
$types = [ |
| 674 |
IMAGETYPE_JPEG => "jpeg", |
| 675 |
IMAGETYPE_GIF => "gif", |
| 676 |
IMAGETYPE_BMP => "bmp", |
| 677 |
IMAGETYPE_PNG => "png", |
| 678 |
IMAGETYPE_WEBP => "webp", |
| 679 |
]; |
| 680 |
|
| 681 |
$type = $types[$type] ?? null; |
| 682 |
|
| 683 |
if ($width == null || $height == null) { |
| 684 |
[$data] = Helpers::getFileContent($filename, $context); |
| 685 |
|
| 686 |
if ($data !== null) { |
| 687 |
if (substr($data, 0, 2) === "BM") { |
| 688 |
$meta = unpack("vtype/Vfilesize/Vreserved/Voffset/Vheadersize/Vwidth/Vheight", $data); |
| 689 |
$width = (int) $meta["width"]; |
| 690 |
$height = (int) $meta["height"]; |
| 691 |
$type = "bmp"; |
| 692 |
} elseif (strpos($data, "<svg") !== false) { |
| 693 |
$doc = new \Svg\Document(); |
| 694 |
$doc->loadFile($filename); |
| 695 |
|
| 696 |
[$width, $height] = $doc->getDimensions(); |
| 697 |
$width = (float) $width; |
| 698 |
$height = (float) $height; |
| 699 |
$type = "svg"; |
| 700 |
} |
| 701 |
} |
| 702 |
} |
| 703 |
|
| 704 |
return $cache[$filename] = [$width ?? 0, $height ?? 0, $type]; |
| 705 |
} |
| 706 |
|
| 707 |
/** |
| 708 |
* Credit goes to mgutt |
| 709 |
* http://www.programmierer-forum.de/function-imagecreatefrombmp-welche-variante-laeuft-t143137.htm |
| 710 |
* Modified by Fabien Menager to support RGB555 BMP format |
| 711 |
*/ |
| 712 |
public static function imagecreatefrombmp($filename, $context = null) |
| 713 |
{ |
| 714 |
if (!function_exists("imagecreatetruecolor")) { |
| 715 |
trigger_error("The PHP GD extension is required, but is not installed.", E_ERROR); |
| 716 |
return false; |
| 717 |
} |
| 718 |
|
| 719 |
// version 1.00 |
| 720 |
if (!($fh = fopen($filename, 'rb'))) { |
| 721 |
trigger_error('imagecreatefrombmp: Can not open ' . $filename, E_USER_WARNING); |
| 722 |
return false; |
| 723 |
} |
| 724 |
|
| 725 |
$bytes_read = 0; |
| 726 |
|
| 727 |
// read file header |
| 728 |
$meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14)); |
| 729 |
|
| 730 |
// check for bitmap |
| 731 |
if ($meta['type'] != 19778) { |
| 732 |
trigger_error('imagecreatefrombmp: ' . $filename . ' is not a bitmap!', E_USER_WARNING); |
| 733 |
return false; |
| 734 |
} |
| 735 |
|
| 736 |
// read image header |
| 737 |
$meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40)); |
| 738 |
$bytes_read += 40; |
| 739 |
|
| 740 |
// read additional bitfield header |
| 741 |
if ($meta['compression'] == 3) { |
| 742 |
$meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12)); |
| 743 |
$bytes_read += 12; |
| 744 |
} |
| 745 |
|
| 746 |
// set bytes and padding |
| 747 |
$meta['bytes'] = $meta['bits'] / 8; |
| 748 |
$meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4))); |
| 749 |
if ($meta['decal'] == 4) { |
| 750 |
$meta['decal'] = 0; |
| 751 |
} |
| 752 |
|
| 753 |
// obtain imagesize |
| 754 |
if ($meta['imagesize'] < 1) { |
| 755 |
$meta['imagesize'] = $meta['filesize'] - $meta['offset']; |
| 756 |
// in rare cases filesize is equal to offset so we need to read physical size |
| 757 |
if ($meta['imagesize'] < 1) { |
| 758 |
$meta['imagesize'] = @filesize($filename) - $meta['offset']; |
| 759 |
if ($meta['imagesize'] < 1) { |
| 760 |
trigger_error('imagecreatefrombmp: Can not obtain filesize of ' . $filename . '!', E_USER_WARNING); |
| 761 |
return false; |
| 762 |
} |
| 763 |
} |
| 764 |
} |
| 765 |
|
| 766 |
// calculate colors |
| 767 |
$meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors']; |
| 768 |
|
| 769 |
// read color palette |
| 770 |
$palette = []; |
| 771 |
if ($meta['bits'] < 16) { |
| 772 |
$palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4)); |
| 773 |
// in rare cases the color value is signed |
| 774 |
if ($palette[1] < 0) { |
| 775 |
foreach ($palette as $i => $color) { |
| 776 |
$palette[$i] = $color + 16777216; |
| 777 |
} |
| 778 |
} |
| 779 |
} |
| 780 |
|
| 781 |
// ignore extra bitmap headers |
| 782 |
if ($meta['headersize'] > $bytes_read) { |
| 783 |
fread($fh, $meta['headersize'] - $bytes_read); |
| 784 |
} |
| 785 |
|
| 786 |
// create gd image |
| 787 |
$im = imagecreatetruecolor($meta['width'], $meta['height']); |
| 788 |
$data = fread($fh, $meta['imagesize']); |
| 789 |
|
| 790 |
// uncompress data |
| 791 |
switch ($meta['compression']) { |
| 792 |
case 1: |
| 793 |
$data = Helpers::rle8_decode($data, $meta['width']); |
| 794 |
break; |
| 795 |
case 2: |
| 796 |
$data = Helpers::rle4_decode($data, $meta['width']); |
| 797 |
break; |
| 798 |
} |
| 799 |
|
| 800 |
$p = 0; |
| 801 |
$vide = chr(0); |
| 802 |
$y = $meta['height'] - 1; |
| 803 |
$error = 'imagecreatefrombmp: ' . $filename . ' has not enough data!'; |
| 804 |
|
| 805 |
// loop through the image data beginning with the lower left corner |
| 806 |
while ($y >= 0) { |
| 807 |
$x = 0; |
| 808 |
while ($x < $meta['width']) { |
| 809 |
switch ($meta['bits']) { |
| 810 |
case 32: |
| 811 |
case 24: |
| 812 |
if (!($part = substr($data, $p, 3 /*$meta['bytes']*/))) { |
| 813 |
trigger_error($error, E_USER_WARNING); |
| 814 |
return $im; |
| 815 |
} |
| 816 |
$color = unpack('V', $part . $vide); |
| 817 |
break; |
| 818 |
case 16: |
| 819 |
if (!($part = substr($data, $p, 2 /*$meta['bytes']*/))) { |
| 820 |
trigger_error($error, E_USER_WARNING); |
| 821 |
return $im; |
| 822 |
} |
| 823 |
$color = unpack('v', $part); |
| 824 |
|
| 825 |
if (empty($meta['rMask']) || $meta['rMask'] != 0xf800) { |
| 826 |
$color[1] = (($color[1] & 0x7c00) >> 7) * 65536 + (($color[1] & 0x03e0) >> 2) * 256 + (($color[1] & 0x001f) << 3); // 555 |
| 827 |
} else { |
| 828 |
$color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3); // 565 |
| 829 |
} |
| 830 |
break; |
| 831 |
case 8: |
| 832 |
$color = unpack('n', $vide . substr($data, $p, 1)); |
| 833 |
$color[1] = $palette[$color[1] + 1]; |
| 834 |
break; |
| 835 |
case 4: |
| 836 |
$color = unpack('n', $vide . substr($data, floor($p), 1)); |
| 837 |
$color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F; |
| 838 |
$color[1] = $palette[$color[1] + 1]; |
| 839 |
break; |
| 840 |
case 1: |
| 841 |
$color = unpack('n', $vide . substr($data, floor($p), 1)); |
| 842 |
switch (($p * 8) % 8) { |
| 843 |
case 0: |
| 844 |
$color[1] = $color[1] >> 7; |
| 845 |
break; |
| 846 |
case 1: |
| 847 |
$color[1] = ($color[1] & 0x40) >> 6; |
| 848 |
break; |
| 849 |
case 2: |
| 850 |
$color[1] = ($color[1] & 0x20) >> 5; |
| 851 |
break; |
| 852 |
case 3: |
| 853 |
$color[1] = ($color[1] & 0x10) >> 4; |
| 854 |
break; |
| 855 |
case 4: |
| 856 |
$color[1] = ($color[1] & 0x8) >> 3; |
| 857 |
break; |
| 858 |
case 5: |
| 859 |
$color[1] = ($color[1] & 0x4) >> 2; |
| 860 |
break; |
| 861 |
case 6: |
| 862 |
$color[1] = ($color[1] & 0x2) >> 1; |
| 863 |
break; |
| 864 |
case 7: |
| 865 |
$color[1] = ($color[1] & 0x1); |
| 866 |
break; |
| 867 |
} |
| 868 |
$color[1] = $palette[$color[1] + 1]; |
| 869 |
break; |
| 870 |
default: |
| 871 |
trigger_error('imagecreatefrombmp: ' . $filename . ' has ' . $meta['bits'] . ' bits and this is not supported!', E_USER_WARNING); |
| 872 |
return false; |
| 873 |
} |
| 874 |
imagesetpixel($im, $x, $y, $color[1]); |
| 875 |
$x++; |
| 876 |
$p += $meta['bytes']; |
| 877 |
} |
| 878 |
$y--; |
| 879 |
$p += $meta['decal']; |
| 880 |
} |
| 881 |
fclose($fh); |
| 882 |
return $im; |
| 883 |
} |
| 884 |
|
| 885 |
/** |
| 886 |
* Gets the content of the file at the specified path using one of |
| 887 |
* the following methods, in preferential order: |
| 888 |
* - file_get_contents: if allow_url_fopen is true or the file is local |
| 889 |
* - curl: if allow_url_fopen is false and curl is available |
| 890 |
* |
| 891 |
* @param string $uri |
| 892 |
* @param resource $context |
| 893 |
* @param int $offset |
| 894 |
* @param int $maxlen |
| 895 |
* @return string[] |
| 896 |
*/ |
| 897 |
public static function getFileContent($uri, $context = null, $offset = 0, $maxlen = null) |
| 898 |
{ |
| 899 |
$content = null; |
| 900 |
$headers = null; |
| 901 |
[$protocol] = Helpers::explode_url($uri); |
| 902 |
$is_local_path = in_array(strtolower($protocol), ["", "file://", "phar://"], true); |
| 903 |
$can_use_curl = in_array(strtolower($protocol), ["http://", "https://"], true); |
| 904 |
|
| 905 |
set_error_handler([self::class, 'record_warnings']); |
| 906 |
|
| 907 |
try { |
| 908 |
if ($is_local_path || ini_get('allow_url_fopen') || !$can_use_curl) { |
| 909 |
if ($is_local_path === false) { |
| 910 |
$uri = Helpers::encodeURI($uri); |
| 911 |
} |
| 912 |
if (isset($maxlen)) { |
| 913 |
$result = file_get_contents($uri, false, $context, $offset, $maxlen); |
| 914 |
} else { |
| 915 |
$result = file_get_contents($uri, false, $context, $offset); |
| 916 |
} |
| 917 |
if ($result !== false) { |
| 918 |
$content = $result; |
| 919 |
} |
| 920 |
if (isset($http_response_header)) { |
| 921 |
$headers = $http_response_header; |
| 922 |
} |
| 923 |
|
| 924 |
} elseif ($can_use_curl && function_exists('curl_exec')) { |
| 925 |
$curl = curl_init($uri); |
| 926 |
|
| 927 |
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); |
| 928 |
curl_setopt($curl, CURLOPT_HEADER, true); |
| 929 |
if ($offset > 0) { |
| 930 |
curl_setopt($curl, CURLOPT_RESUME_FROM, $offset); |
| 931 |
} |
| 932 |
|
| 933 |
if ($maxlen > 0) { |
| 934 |
curl_setopt($curl, CURLOPT_BUFFERSIZE, 128); |
| 935 |
curl_setopt($curl, CURLOPT_NOPROGRESS, false); |
| 936 |
curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, function ($res, $download_size_total, $download_size, $upload_size_total, $upload_size) use ($maxlen) { |
| 937 |
return ($download_size > $maxlen) ? 1 : 0; |
| 938 |
}); |
| 939 |
} |
| 940 |
|
| 941 |
$context_options = []; |
| 942 |
if (!is_null($context)) { |
| 943 |
$context_options = stream_context_get_options($context); |
| 944 |
} |
| 945 |
foreach ($context_options as $stream => $options) { |
| 946 |
foreach ($options as $option => $value) { |
| 947 |
$key = strtolower($stream) . ":" . strtolower($option); |
| 948 |
switch ($key) { |
| 949 |
case "curl:curl_verify_ssl_host": |
| 950 |
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, !$value ? 0 : 2); |
| 951 |
break; |
| 952 |
case "curl:max_redirects": |
| 953 |
curl_setopt($curl, CURLOPT_MAXREDIRS, $value); |
| 954 |
break; |
| 955 |
case "http:follow_location": |
| 956 |
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, $value); |
| 957 |
break; |
| 958 |
case "http:header": |
| 959 |
if (is_string($value)) { |
| 960 |
curl_setopt($curl, CURLOPT_HTTPHEADER, [$value]); |
| 961 |
} else { |
| 962 |
curl_setopt($curl, CURLOPT_HTTPHEADER, $value); |
| 963 |
} |
| 964 |
break; |
| 965 |
case "http:timeout": |
| 966 |
curl_setopt($curl, CURLOPT_TIMEOUT, $value); |
| 967 |
break; |
| 968 |
case "http:user_agent": |
| 969 |
curl_setopt($curl, CURLOPT_USERAGENT, $value); |
| 970 |
break; |
| 971 |
case "curl:curl_verify_ssl_peer": |
| 972 |
case "ssl:verify_peer": |
| 973 |
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $value); |
| 974 |
break; |
| 975 |
} |
| 976 |
} |
| 977 |
} |
| 978 |
|
| 979 |
$data = curl_exec($curl); |
| 980 |
|
| 981 |
if ($data !== false && !curl_errno($curl)) { |
| 982 |
switch ($http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE)) { |
| 983 |
case 200: |
| 984 |
$raw_headers = substr($data, 0, curl_getinfo($curl, CURLINFO_HEADER_SIZE)); |
| 985 |
$headers = preg_split("/[\n\r]+/", trim($raw_headers)); |
| 986 |
$content = substr($data, curl_getinfo($curl, CURLINFO_HEADER_SIZE)); |
| 987 |
break; |
| 988 |
} |
| 989 |
} |
| 990 |
curl_close($curl); |
| 991 |
} |
| 992 |
} finally { |
| 993 |
restore_error_handler(); |
| 994 |
} |
| 995 |
|
| 996 |
return [$content, $headers]; |
| 997 |
} |
| 998 |
|
| 999 |
/** |
| 1000 |
* @param string $str |
| 1001 |
* @return string |
| 1002 |
*/ |
| 1003 |
public static function mb_ucwords(string $str): string |
| 1004 |
{ |
| 1005 |
$max_len = mb_strlen($str); |
| 1006 |
if ($max_len === 1) { |
| 1007 |
return mb_strtoupper($str); |
| 1008 |
} |
| 1009 |
|
| 1010 |
$str = mb_strtoupper(mb_substr($str, 0, 1)) . mb_substr($str, 1); |
| 1011 |
|
| 1012 |
foreach ([' ', '.', ',', '!', '?', '-', '+'] as $s) { |
| 1013 |
$pos = 0; |
| 1014 |
while (($pos = mb_strpos($str, $s, $pos)) !== false) { |
| 1015 |
$pos++; |
| 1016 |
// Nothing to do if the separator is the last char of the string |
| 1017 |
if ($pos !== false && $pos < $max_len) { |
| 1018 |
// If the char we want to upper is the last char there is nothing to append behind |
| 1019 |
if ($pos + 1 < $max_len) { |
| 1020 |
$str = mb_substr($str, 0, $pos) . mb_strtoupper(mb_substr($str, $pos, 1)) . mb_substr($str, $pos + 1); |
| 1021 |
} else { |
| 1022 |
$str = mb_substr($str, 0, $pos) . mb_strtoupper(mb_substr($str, $pos, 1)); |
| 1023 |
} |
| 1024 |
} |
| 1025 |
} |
| 1026 |
} |
| 1027 |
|
| 1028 |
return $str; |
| 1029 |
} |
| 1030 |
|
| 1031 |
/** |
| 1032 |
* Check whether two lengths should be considered equal, accounting for |
| 1033 |
* inaccuracies in float computation. |
| 1034 |
* |
| 1035 |
* The implementation relies on the fact that we are neither dealing with |
| 1036 |
* very large, nor with very small numbers in layout. Adapted from |
| 1037 |
* https://floating-point-gui.de/errors/comparison/. |
| 1038 |
* |
| 1039 |
* @param float $a |
| 1040 |
* @param float $b |
| 1041 |
* |
| 1042 |
* @return bool |
| 1043 |
*/ |
| 1044 |
public static function lengthEqual(float $a, float $b): bool |
| 1045 |
{ |
| 1046 |
// The epsilon results in a precision of at least: |
| 1047 |
// * 7 decimal digits at around 1 |
| 1048 |
// * 4 decimal digits at around 1000 (around the size of common paper formats) |
| 1049 |
// * 2 decimal digits at around 100,000 (100,000pt ~ 35.28m) |
| 1050 |
static $epsilon = 1e-8; |
| 1051 |
static $almostZero = 1e-12; |
| 1052 |
|
| 1053 |
$diff = abs($a - $b); |
| 1054 |
|
| 1055 |
if ($a === $b || $diff < $almostZero) { |
| 1056 |
return true; |
| 1057 |
} |
| 1058 |
|
| 1059 |
return $diff < $epsilon * max(abs($a), abs($b)); |
| 1060 |
} |
| 1061 |
|
| 1062 |
/** |
| 1063 |
* Check `$a < $b`, accounting for inaccuracies in float computation. |
| 1064 |
*/ |
| 1065 |
public static function lengthLess(float $a, float $b): bool |
| 1066 |
{ |
| 1067 |
return $a < $b && !self::lengthEqual($a, $b); |
| 1068 |
} |
| 1069 |
|
| 1070 |
/** |
| 1071 |
* Check `$a <= $b`, accounting for inaccuracies in float computation. |
| 1072 |
*/ |
| 1073 |
public static function lengthLessOrEqual(float $a, float $b): bool |
| 1074 |
{ |
| 1075 |
return $a <= $b || self::lengthEqual($a, $b); |
| 1076 |
} |
| 1077 |
|
| 1078 |
/** |
| 1079 |
* Check `$a > $b`, accounting for inaccuracies in float computation. |
| 1080 |
*/ |
| 1081 |
public static function lengthGreater(float $a, float $b): bool |
| 1082 |
{ |
| 1083 |
return $a > $b && !self::lengthEqual($a, $b); |
| 1084 |
} |
| 1085 |
|
| 1086 |
/** |
| 1087 |
* Check `$a >= $b`, accounting for inaccuracies in float computation. |
| 1088 |
*/ |
| 1089 |
public static function lengthGreaterOrEqual(float $a, float $b): bool |
| 1090 |
{ |
| 1091 |
return $a >= $b || self::lengthEqual($a, $b); |
| 1092 |
} |
| 1093 |
} |
| 1094 |
|