PluginProbe
MBE eShip / trunk
MBE eShip vtrunk
2.8.1 trunk 1.0.0 1.1.0 1.1.3 1.2.1 1.2.2 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.6.0 1.7.0 1.7.1 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1.0 2.1.1 2.1.2 2.2.1 All 37 releases
mail-boxes-etc / lib / dompdf / src / Helpers.php

Helpers.php in MBE eShip trunk, at lib/dompdf/src/Helpers.php

1,094 lines 37.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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