Helpers.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  1. <?php
  2. namespace Dompdf;
  3. class Helpers
  4. {
  5. /**
  6. * print_r wrapper for html/cli output
  7. *
  8. * Wraps print_r() output in < pre > tags if the current sapi is not 'cli'.
  9. * Returns the output string instead of displaying it if $return is true.
  10. *
  11. * @param mixed $mixed variable or expression to display
  12. * @param bool $return
  13. *
  14. * @return string|null
  15. */
  16. public static function pre_r($mixed, $return = false)
  17. {
  18. if ($return) {
  19. return "<pre>" . print_r($mixed, true) . "</pre>";
  20. }
  21. if (php_sapi_name() !== "cli") {
  22. echo "<pre>";
  23. }
  24. print_r($mixed);
  25. if (php_sapi_name() !== "cli") {
  26. echo "</pre>";
  27. } else {
  28. echo "\n";
  29. }
  30. flush();
  31. return null;
  32. }
  33. /**
  34. * builds a full url given a protocol, hostname, base path and url
  35. *
  36. * @param string $protocol
  37. * @param string $host
  38. * @param string $base_path
  39. * @param string $url
  40. * @return string
  41. *
  42. * Initially the trailing slash of $base_path was optional, and conditionally appended.
  43. * However on dynamically created sites, where the page is given as url parameter,
  44. * the base path might not end with an url.
  45. * Therefore do not append a slash, and **require** the $base_url to ending in a slash
  46. * when needed.
  47. * Vice versa, on using the local file system path of a file, make sure that the slash
  48. * is appended (o.k. also for Windows)
  49. */
  50. public static function build_url($protocol, $host, $base_path, $url)
  51. {
  52. $protocol = mb_strtolower($protocol);
  53. if (strlen($url) == 0) {
  54. //return $protocol . $host . rtrim($base_path, "/\\") . "/";
  55. return $protocol . $host . $base_path;
  56. }
  57. // Is the url already fully qualified, a Data URI, or a reference to a named anchor?
  58. // File-protocol URLs may require additional processing (e.g. for URLs with a relative path)
  59. if ((mb_strpos($url, "://") !== false && substr($url, 0, 7) !== "file://") || mb_substr($url, 0, 1) === "#" || mb_strpos($url, "data:") === 0 || mb_strpos($url, "mailto:") === 0 || mb_strpos($url, "tel:") === 0) {
  60. return $url;
  61. }
  62. if (strpos($url, "file://") === 0) {
  63. $url = substr($url, 7);
  64. $protocol = "";
  65. }
  66. $ret = "";
  67. if ($protocol != "file://") {
  68. $ret = $protocol;
  69. }
  70. if (!in_array(mb_strtolower($protocol), ["http://", "https://", "ftp://", "ftps://"])) {
  71. //On Windows local file, an abs path can begin also with a '\' or a drive letter and colon
  72. //drive: followed by a relative path would be a drive specific default folder.
  73. //not known in php app code, treat as abs path
  74. //($url[1] !== ':' || ($url[2]!=='\\' && $url[2]!=='/'))
  75. if ($url[0] !== '/' && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN' || (mb_strlen($url) > 1 && $url[0] !== '\\' && $url[1] !== ':'))) {
  76. // For rel path and local access we ignore the host, and run the path through realpath()
  77. $ret .= realpath($base_path) . '/';
  78. }
  79. $ret .= $url;
  80. $ret = preg_replace('/\?(.*)$/', "", $ret);
  81. return $ret;
  82. }
  83. // Protocol relative urls (e.g. "//example.org/style.css")
  84. if (strpos($url, '//') === 0) {
  85. $ret .= substr($url, 2);
  86. //remote urls with backslash in html/css are not really correct, but lets be genereous
  87. } elseif ($url[0] === '/' || $url[0] === '\\') {
  88. // Absolute path
  89. $ret .= $host . $url;
  90. } else {
  91. // Relative path
  92. //$base_path = $base_path !== "" ? rtrim($base_path, "/\\") . "/" : "";
  93. $ret .= $host . $base_path . $url;
  94. }
  95. // URL should now be complete, final cleanup
  96. $parsed_url = parse_url($ret);
  97. // reproduced from https://www.php.net/manual/en/function.parse-url.php#106731
  98. $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
  99. $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
  100. $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
  101. $user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
  102. $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
  103. $pass = ($user || $pass) ? "$pass@" : '';
  104. $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
  105. $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
  106. $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
  107. // partially reproduced from https://stackoverflow.com/a/1243431/264628
  108. /* replace '//' or '/./' or '/foo/../' with '/' */
  109. $re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#');
  110. for($n=1; $n>0; $path=preg_replace($re, '/', $path, -1, $n)) {}
  111. $ret = "$scheme$user$pass$host$port$path$query$fragment";
  112. return $ret;
  113. }
  114. /**
  115. * Builds a HTTP Content-Disposition header string using `$dispositionType`
  116. * and `$filename`.
  117. *
  118. * If the filename contains any characters not in the ISO-8859-1 character
  119. * set, a fallback filename will be included for clients not supporting the
  120. * `filename*` parameter.
  121. *
  122. * @param string $dispositionType
  123. * @param string $filename
  124. * @return string
  125. */
  126. public static function buildContentDispositionHeader($dispositionType, $filename)
  127. {
  128. $encoding = mb_detect_encoding($filename);
  129. $fallbackfilename = mb_convert_encoding($filename, "ISO-8859-1", $encoding);
  130. $fallbackfilename = str_replace("\"", "", $fallbackfilename);
  131. $encodedfilename = rawurlencode($filename);
  132. $contentDisposition = "Content-Disposition: $dispositionType; filename=\"$fallbackfilename\"";
  133. if ($fallbackfilename !== $filename) {
  134. $contentDisposition .= "; filename*=UTF-8''$encodedfilename";
  135. }
  136. return $contentDisposition;
  137. }
  138. /**
  139. * Converts decimal numbers to roman numerals
  140. *
  141. * @param int $num
  142. *
  143. * @throws Exception
  144. * @return string
  145. */
  146. public static function dec2roman($num)
  147. {
  148. static $ones = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
  149. static $tens = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
  150. static $hund = ["", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm"];
  151. static $thou = ["", "m", "mm", "mmm"];
  152. if (!is_numeric($num)) {
  153. throw new Exception("dec2roman() requires a numeric argument.");
  154. }
  155. if ($num > 4000 || $num < 0) {
  156. return "(out of range)";
  157. }
  158. $num = strrev((string)$num);
  159. $ret = "";
  160. switch (mb_strlen($num)) {
  161. /** @noinspection PhpMissingBreakStatementInspection */
  162. case 4:
  163. $ret .= $thou[$num[3]];
  164. /** @noinspection PhpMissingBreakStatementInspection */
  165. case 3:
  166. $ret .= $hund[$num[2]];
  167. /** @noinspection PhpMissingBreakStatementInspection */
  168. case 2:
  169. $ret .= $tens[$num[1]];
  170. /** @noinspection PhpMissingBreakStatementInspection */
  171. case 1:
  172. $ret .= $ones[$num[0]];
  173. default:
  174. break;
  175. }
  176. return $ret;
  177. }
  178. /**
  179. * Determines whether $value is a percentage or not
  180. *
  181. * @param float $value
  182. *
  183. * @return bool
  184. */
  185. public static function is_percent($value)
  186. {
  187. return false !== mb_strpos($value, "%");
  188. }
  189. /**
  190. * Parses a data URI scheme
  191. * http://en.wikipedia.org/wiki/Data_URI_scheme
  192. *
  193. * @param string $data_uri The data URI to parse
  194. *
  195. * @return array|bool The result with charset, mime type and decoded data
  196. */
  197. public static function parse_data_uri($data_uri)
  198. {
  199. if (!preg_match('/^data:(?P<mime>[a-z0-9\/+-.]+)(;charset=(?P<charset>[a-z0-9-])+)?(?P<base64>;base64)?\,(?P<data>.*)?/is', $data_uri, $match)) {
  200. return false;
  201. }
  202. $match['data'] = rawurldecode($match['data']);
  203. $result = [
  204. 'charset' => $match['charset'] ? $match['charset'] : 'US-ASCII',
  205. 'mime' => $match['mime'] ? $match['mime'] : 'text/plain',
  206. 'data' => $match['base64'] ? base64_decode($match['data']) : $match['data'],
  207. ];
  208. return $result;
  209. }
  210. /**
  211. * Encodes a Uniform Resource Identifier (URI) by replacing non-alphanumeric
  212. * characters with a percent (%) sign followed by two hex digits, excepting
  213. * characters in the URI reserved character set.
  214. *
  215. * Assumes that the URI is a complete URI, so does not encode reserved
  216. * characters that have special meaning in the URI.
  217. *
  218. * Simulates the encodeURI function available in JavaScript
  219. * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURI
  220. *
  221. * Source: http://stackoverflow.com/q/4929584/264628
  222. *
  223. * @param string $uri The URI to encode
  224. * @return string The original URL with special characters encoded
  225. */
  226. public static function encodeURI($uri) {
  227. $unescaped = [
  228. '%2D'=>'-','%5F'=>'_','%2E'=>'.','%21'=>'!', '%7E'=>'~',
  229. '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')'
  230. ];
  231. $reserved = [
  232. '%3B'=>';','%2C'=>',','%2F'=>'/','%3F'=>'?','%3A'=>':',
  233. '%40'=>'@','%26'=>'&','%3D'=>'=','%2B'=>'+','%24'=>'$'
  234. ];
  235. $score = [
  236. '%23'=>'#'
  237. ];
  238. return strtr(rawurlencode(rawurldecode($uri)), array_merge($reserved, $unescaped, $score));
  239. }
  240. /**
  241. * Decoder for RLE8 compression in windows bitmaps
  242. * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp
  243. *
  244. * @param string $str Data to decode
  245. * @param integer $width Image width
  246. *
  247. * @return string
  248. */
  249. public static function rle8_decode($str, $width)
  250. {
  251. $lineWidth = $width + (3 - ($width - 1) % 4);
  252. $out = '';
  253. $cnt = strlen($str);
  254. for ($i = 0; $i < $cnt; $i++) {
  255. $o = ord($str[$i]);
  256. switch ($o) {
  257. case 0: # ESCAPE
  258. $i++;
  259. switch (ord($str[$i])) {
  260. case 0: # NEW LINE
  261. $padCnt = $lineWidth - strlen($out) % $lineWidth;
  262. if ($padCnt < $lineWidth) {
  263. $out .= str_repeat(chr(0), $padCnt); # pad line
  264. }
  265. break;
  266. case 1: # END OF FILE
  267. $padCnt = $lineWidth - strlen($out) % $lineWidth;
  268. if ($padCnt < $lineWidth) {
  269. $out .= str_repeat(chr(0), $padCnt); # pad line
  270. }
  271. break 3;
  272. case 2: # DELTA
  273. $i += 2;
  274. break;
  275. default: # ABSOLUTE MODE
  276. $num = ord($str[$i]);
  277. for ($j = 0; $j < $num; $j++) {
  278. $out .= $str[++$i];
  279. }
  280. if ($num % 2) {
  281. $i++;
  282. }
  283. }
  284. break;
  285. default:
  286. $out .= str_repeat($str[++$i], $o);
  287. }
  288. }
  289. return $out;
  290. }
  291. /**
  292. * Decoder for RLE4 compression in windows bitmaps
  293. * see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/bitmaps_6x0u.asp
  294. *
  295. * @param string $str Data to decode
  296. * @param integer $width Image width
  297. *
  298. * @return string
  299. */
  300. public static function rle4_decode($str, $width)
  301. {
  302. $w = floor($width / 2) + ($width % 2);
  303. $lineWidth = $w + (3 - (($width - 1) / 2) % 4);
  304. $pixels = [];
  305. $cnt = strlen($str);
  306. $c = 0;
  307. for ($i = 0; $i < $cnt; $i++) {
  308. $o = ord($str[$i]);
  309. switch ($o) {
  310. case 0: # ESCAPE
  311. $i++;
  312. switch (ord($str[$i])) {
  313. case 0: # NEW LINE
  314. while (count($pixels) % $lineWidth != 0) {
  315. $pixels[] = 0;
  316. }
  317. break;
  318. case 1: # END OF FILE
  319. while (count($pixels) % $lineWidth != 0) {
  320. $pixels[] = 0;
  321. }
  322. break 3;
  323. case 2: # DELTA
  324. $i += 2;
  325. break;
  326. default: # ABSOLUTE MODE
  327. $num = ord($str[$i]);
  328. for ($j = 0; $j < $num; $j++) {
  329. if ($j % 2 == 0) {
  330. $c = ord($str[++$i]);
  331. $pixels[] = ($c & 240) >> 4;
  332. } else {
  333. $pixels[] = $c & 15;
  334. }
  335. }
  336. if ($num % 2 == 0) {
  337. $i++;
  338. }
  339. }
  340. break;
  341. default:
  342. $c = ord($str[++$i]);
  343. for ($j = 0; $j < $o; $j++) {
  344. $pixels[] = ($j % 2 == 0 ? ($c & 240) >> 4 : $c & 15);
  345. }
  346. }
  347. }
  348. $out = '';
  349. if (count($pixels) % 2) {
  350. $pixels[] = 0;
  351. }
  352. $cnt = count($pixels) / 2;
  353. for ($i = 0; $i < $cnt; $i++) {
  354. $out .= chr(16 * $pixels[2 * $i] + $pixels[2 * $i + 1]);
  355. }
  356. return $out;
  357. }
  358. /**
  359. * parse a full url or pathname and return an array(protocol, host, path,
  360. * file + query + fragment)
  361. *
  362. * @param string $url
  363. * @return array
  364. */
  365. public static function explode_url($url)
  366. {
  367. $protocol = "";
  368. $host = "";
  369. $path = "";
  370. $file = "";
  371. $arr = parse_url($url);
  372. if ( isset($arr["scheme"]) ) {
  373. $arr["scheme"] = mb_strtolower($arr["scheme"]);
  374. }
  375. // Exclude windows drive letters...
  376. if (isset($arr["scheme"]) && $arr["scheme"] !== "file" && strlen($arr["scheme"]) > 1) {
  377. $protocol = $arr["scheme"] . "://";
  378. if (isset($arr["user"])) {
  379. $host .= $arr["user"];
  380. if (isset($arr["pass"])) {
  381. $host .= ":" . $arr["pass"];
  382. }
  383. $host .= "@";
  384. }
  385. if (isset($arr["host"])) {
  386. $host .= $arr["host"];
  387. }
  388. if (isset($arr["port"])) {
  389. $host .= ":" . $arr["port"];
  390. }
  391. if (isset($arr["path"]) && $arr["path"] !== "") {
  392. // Do we have a trailing slash?
  393. if ($arr["path"][mb_strlen($arr["path"]) - 1] === "/") {
  394. $path = $arr["path"];
  395. $file = "";
  396. } else {
  397. $path = rtrim(dirname($arr["path"]), '/\\') . "/";
  398. $file = basename($arr["path"]);
  399. }
  400. }
  401. if (isset($arr["query"])) {
  402. $file .= "?" . $arr["query"];
  403. }
  404. if (isset($arr["fragment"])) {
  405. $file .= "#" . $arr["fragment"];
  406. }
  407. } else {
  408. $i = mb_stripos($url, "file://");
  409. if ($i !== false) {
  410. $url = mb_substr($url, $i + 7);
  411. }
  412. $protocol = ""; // "file://"; ? why doesn't this work... It's because of
  413. // network filenames like //COMPU/SHARENAME
  414. $host = ""; // localhost, really
  415. $file = basename($url);
  416. $path = dirname($url);
  417. // Check that the path exists
  418. if ($path !== false) {
  419. $path .= '/';
  420. } else {
  421. // generate a url to access the file if no real path found.
  422. $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
  423. $host = isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : php_uname("n");
  424. if (substr($arr["path"], 0, 1) === '/') {
  425. $path = dirname($arr["path"]);
  426. } else {
  427. $path = '/' . rtrim(dirname($_SERVER["SCRIPT_NAME"]), '/') . '/' . $arr["path"];
  428. }
  429. }
  430. }
  431. $ret = [$protocol, $host, $path, $file,
  432. "protocol" => $protocol,
  433. "host" => $host,
  434. "path" => $path,
  435. "file" => $file];
  436. return $ret;
  437. }
  438. /**
  439. * Print debug messages
  440. *
  441. * @param string $type The type of debug messages to print
  442. * @param string $msg The message to show
  443. */
  444. public static function dompdf_debug($type, $msg)
  445. {
  446. global $_DOMPDF_DEBUG_TYPES, $_dompdf_show_warnings, $_dompdf_debug;
  447. if (isset($_DOMPDF_DEBUG_TYPES[$type]) && ($_dompdf_show_warnings || $_dompdf_debug)) {
  448. $arr = debug_backtrace();
  449. echo basename($arr[0]["file"]) . " (" . $arr[0]["line"] . "): " . $arr[1]["function"] . ": ";
  450. Helpers::pre_r($msg);
  451. }
  452. }
  453. /**
  454. * Stores warnings in an array for display later
  455. * This function allows warnings generated by the DomDocument parser
  456. * and CSS loader ({@link Stylesheet}) to be captured and displayed
  457. * later. Without this function, errors are displayed immediately and
  458. * PDF streaming is impossible.
  459. * @see http://www.php.net/manual/en/function.set-error_handler.php
  460. *
  461. * @param int $errno
  462. * @param string $errstr
  463. * @param string $errfile
  464. * @param string $errline
  465. *
  466. * @throws Exception
  467. */
  468. public static function record_warnings($errno, $errstr, $errfile, $errline)
  469. {
  470. // Not a warning or notice
  471. if (!($errno & (E_WARNING | E_NOTICE | E_USER_NOTICE | E_USER_WARNING | E_STRICT | E_DEPRECATED | E_USER_DEPRECATED))) {
  472. throw new Exception($errstr . " $errno");
  473. }
  474. global $_dompdf_warnings;
  475. global $_dompdf_show_warnings;
  476. if ($_dompdf_show_warnings) {
  477. echo $errstr . "\n";
  478. }
  479. $_dompdf_warnings[] = $errstr;
  480. }
  481. /**
  482. * @param $c
  483. * @return bool|string
  484. */
  485. public static function unichr($c)
  486. {
  487. if ($c <= 0x7F) {
  488. return chr($c);
  489. } else if ($c <= 0x7FF) {
  490. return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
  491. } else if ($c <= 0xFFFF) {
  492. return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F)
  493. . chr(0x80 | $c & 0x3F);
  494. } else if ($c <= 0x10FFFF) {
  495. return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F)
  496. . chr(0x80 | $c >> 6 & 0x3F)
  497. . chr(0x80 | $c & 0x3F);
  498. }
  499. return false;
  500. }
  501. /**
  502. * Converts a CMYK color to RGB
  503. *
  504. * @param float|float[] $c
  505. * @param float $m
  506. * @param float $y
  507. * @param float $k
  508. *
  509. * @return float[]
  510. */
  511. public static function cmyk_to_rgb($c, $m = null, $y = null, $k = null)
  512. {
  513. if (is_array($c)) {
  514. [$c, $m, $y, $k] = $c;
  515. }
  516. $c *= 255;
  517. $m *= 255;
  518. $y *= 255;
  519. $k *= 255;
  520. $r = (1 - round(2.55 * ($c + $k)));
  521. $g = (1 - round(2.55 * ($m + $k)));
  522. $b = (1 - round(2.55 * ($y + $k)));
  523. if ($r < 0) {
  524. $r = 0;
  525. }
  526. if ($g < 0) {
  527. $g = 0;
  528. }
  529. if ($b < 0) {
  530. $b = 0;
  531. }
  532. return [
  533. $r, $g, $b,
  534. "r" => $r, "g" => $g, "b" => $b
  535. ];
  536. }
  537. /**
  538. * getimagesize doesn't give a good size for 32bit BMP image v5
  539. *
  540. * @param string $filename
  541. * @param resource $context
  542. * @return array The same format as getimagesize($filename)
  543. */
  544. public static function dompdf_getimagesize($filename, $context = null)
  545. {
  546. static $cache = [];
  547. if (isset($cache[$filename])) {
  548. return $cache[$filename];
  549. }
  550. [$width, $height, $type] = getimagesize($filename);
  551. // Custom types
  552. $types = [
  553. IMAGETYPE_JPEG => "jpeg",
  554. IMAGETYPE_GIF => "gif",
  555. IMAGETYPE_BMP => "bmp",
  556. IMAGETYPE_PNG => "png",
  557. ];
  558. $type = isset($types[$type]) ? $types[$type] : null;
  559. if ($width == null || $height == null) {
  560. [$data, $headers] = Helpers::getFileContent($filename, $context);
  561. if (!empty($data)) {
  562. if (substr($data, 0, 2) === "BM") {
  563. $meta = unpack('vtype/Vfilesize/Vreserved/Voffset/Vheadersize/Vwidth/Vheight', $data);
  564. $width = (int)$meta['width'];
  565. $height = (int)$meta['height'];
  566. $type = "bmp";
  567. } else {
  568. if (strpos($data, "<svg") !== false) {
  569. $doc = new \Svg\Document();
  570. $doc->loadFile($filename);
  571. [$width, $height] = $doc->getDimensions();
  572. $type = "svg";
  573. }
  574. }
  575. }
  576. }
  577. return $cache[$filename] = [$width, $height, $type];
  578. }
  579. /**
  580. * Credit goes to mgutt
  581. * http://www.programmierer-forum.de/function-imagecreatefrombmp-welche-variante-laeuft-t143137.htm
  582. * Modified by Fabien Menager to support RGB555 BMP format
  583. */
  584. public static function imagecreatefrombmp($filename, $context = null)
  585. {
  586. if (!function_exists("imagecreatetruecolor")) {
  587. trigger_error("The PHP GD extension is required, but is not installed.", E_ERROR);
  588. return false;
  589. }
  590. // version 1.00
  591. if (!($fh = fopen($filename, 'rb'))) {
  592. trigger_error('imagecreatefrombmp: Can not open ' . $filename, E_USER_WARNING);
  593. return false;
  594. }
  595. $bytes_read = 0;
  596. // read file header
  597. $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14));
  598. // check for bitmap
  599. if ($meta['type'] != 19778) {
  600. trigger_error('imagecreatefrombmp: ' . $filename . ' is not a bitmap!', E_USER_WARNING);
  601. return false;
  602. }
  603. // read image header
  604. $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40));
  605. $bytes_read += 40;
  606. // read additional bitfield header
  607. if ($meta['compression'] == 3) {
  608. $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12));
  609. $bytes_read += 12;
  610. }
  611. // set bytes and padding
  612. $meta['bytes'] = $meta['bits'] / 8;
  613. $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4)));
  614. if ($meta['decal'] == 4) {
  615. $meta['decal'] = 0;
  616. }
  617. // obtain imagesize
  618. if ($meta['imagesize'] < 1) {
  619. $meta['imagesize'] = $meta['filesize'] - $meta['offset'];
  620. // in rare cases filesize is equal to offset so we need to read physical size
  621. if ($meta['imagesize'] < 1) {
  622. $meta['imagesize'] = @filesize($filename) - $meta['offset'];
  623. if ($meta['imagesize'] < 1) {
  624. trigger_error('imagecreatefrombmp: Can not obtain filesize of ' . $filename . '!', E_USER_WARNING);
  625. return false;
  626. }
  627. }
  628. }
  629. // calculate colors
  630. $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors'];
  631. // read color palette
  632. $palette = [];
  633. if ($meta['bits'] < 16) {
  634. $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4));
  635. // in rare cases the color value is signed
  636. if ($palette[1] < 0) {
  637. foreach ($palette as $i => $color) {
  638. $palette[$i] = $color + 16777216;
  639. }
  640. }
  641. }
  642. // ignore extra bitmap headers
  643. if ($meta['headersize'] > $bytes_read) {
  644. fread($fh, $meta['headersize'] - $bytes_read);
  645. }
  646. // create gd image
  647. $im = imagecreatetruecolor($meta['width'], $meta['height']);
  648. $data = fread($fh, $meta['imagesize']);
  649. // uncompress data
  650. switch ($meta['compression']) {
  651. case 1:
  652. $data = Helpers::rle8_decode($data, $meta['width']);
  653. break;
  654. case 2:
  655. $data = Helpers::rle4_decode($data, $meta['width']);
  656. break;
  657. }
  658. $p = 0;
  659. $vide = chr(0);
  660. $y = $meta['height'] - 1;
  661. $error = 'imagecreatefrombmp: ' . $filename . ' has not enough data!';
  662. // loop through the image data beginning with the lower left corner
  663. while ($y >= 0) {
  664. $x = 0;
  665. while ($x < $meta['width']) {
  666. switch ($meta['bits']) {
  667. case 32:
  668. case 24:
  669. if (!($part = substr($data, $p, 3 /*$meta['bytes']*/))) {
  670. trigger_error($error, E_USER_WARNING);
  671. return $im;
  672. }
  673. $color = unpack('V', $part . $vide);
  674. break;
  675. case 16:
  676. if (!($part = substr($data, $p, 2 /*$meta['bytes']*/))) {
  677. trigger_error($error, E_USER_WARNING);
  678. return $im;
  679. }
  680. $color = unpack('v', $part);
  681. if (empty($meta['rMask']) || $meta['rMask'] != 0xf800) {
  682. $color[1] = (($color[1] & 0x7c00) >> 7) * 65536 + (($color[1] & 0x03e0) >> 2) * 256 + (($color[1] & 0x001f) << 3); // 555
  683. } else {
  684. $color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3); // 565
  685. }
  686. break;
  687. case 8:
  688. $color = unpack('n', $vide . substr($data, $p, 1));
  689. $color[1] = $palette[$color[1] + 1];
  690. break;
  691. case 4:
  692. $color = unpack('n', $vide . substr($data, floor($p), 1));
  693. $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F;
  694. $color[1] = $palette[$color[1] + 1];
  695. break;
  696. case 1:
  697. $color = unpack('n', $vide . substr($data, floor($p), 1));
  698. switch (($p * 8) % 8) {
  699. case 0:
  700. $color[1] = $color[1] >> 7;
  701. break;
  702. case 1:
  703. $color[1] = ($color[1] & 0x40) >> 6;
  704. break;
  705. case 2:
  706. $color[1] = ($color[1] & 0x20) >> 5;
  707. break;
  708. case 3:
  709. $color[1] = ($color[1] & 0x10) >> 4;
  710. break;
  711. case 4:
  712. $color[1] = ($color[1] & 0x8) >> 3;
  713. break;
  714. case 5:
  715. $color[1] = ($color[1] & 0x4) >> 2;
  716. break;
  717. case 6:
  718. $color[1] = ($color[1] & 0x2) >> 1;
  719. break;
  720. case 7:
  721. $color[1] = ($color[1] & 0x1);
  722. break;
  723. }
  724. $color[1] = $palette[$color[1] + 1];
  725. break;
  726. default:
  727. trigger_error('imagecreatefrombmp: ' . $filename . ' has ' . $meta['bits'] . ' bits and this is not supported!', E_USER_WARNING);
  728. return false;
  729. }
  730. imagesetpixel($im, $x, $y, $color[1]);
  731. $x++;
  732. $p += $meta['bytes'];
  733. }
  734. $y--;
  735. $p += $meta['decal'];
  736. }
  737. fclose($fh);
  738. return $im;
  739. }
  740. /**
  741. * Gets the content of the file at the specified path using one of
  742. * the following methods, in preferential order:
  743. * - file_get_contents: if allow_url_fopen is true or the file is local
  744. * - curl: if allow_url_fopen is false and curl is available
  745. *
  746. * @param string $uri
  747. * @param resource $context (ignored if curl is used)
  748. * @param int $offset
  749. * @param int $maxlen (ignored if curl is used)
  750. * @return string[]
  751. */
  752. public static function getFileContent($uri, $context = null, $offset = 0, $maxlen = null)
  753. {
  754. $content = null;
  755. $headers = null;
  756. [$proto, $host, $path, $file] = Helpers::explode_url($uri);
  757. $is_local_path = ($proto == '' || $proto === 'file://');
  758. set_error_handler([self::class, 'record_warnings']);
  759. try {
  760. if ($is_local_path || ini_get('allow_url_fopen')) {
  761. if ($is_local_path === false) {
  762. $uri = Helpers::encodeURI($uri);
  763. }
  764. if (isset($maxlen)) {
  765. $result = file_get_contents($uri, null, $context, $offset, $maxlen);
  766. } else {
  767. $result = file_get_contents($uri, null, $context, $offset);
  768. }
  769. if ($result !== false) {
  770. $content = $result;
  771. }
  772. if (isset($http_response_header)) {
  773. $headers = $http_response_header;
  774. }
  775. } elseif (function_exists('curl_exec')) {
  776. $curl = curl_init($uri);
  777. //TODO: use $context to define additional curl options
  778. curl_setopt($curl, CURLOPT_TIMEOUT, 10);
  779. curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
  780. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  781. curl_setopt($curl, CURLOPT_HEADER, true);
  782. if ($offset > 0) {
  783. curl_setopt($curl, CURLOPT_RESUME_FROM, $offset);
  784. }
  785. $data = curl_exec($curl);
  786. if ($data !== false && !curl_errno($curl)) {
  787. switch ($http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE)) {
  788. case 200:
  789. $raw_headers = substr($data, 0, curl_getinfo($curl, CURLINFO_HEADER_SIZE));
  790. $headers = preg_split("/[\n\r]+/", trim($raw_headers));
  791. $content = substr($data, curl_getinfo($curl, CURLINFO_HEADER_SIZE));
  792. break;
  793. }
  794. }
  795. curl_close($curl);
  796. }
  797. } finally {
  798. restore_error_handler();
  799. }
  800. return [$content, $headers];
  801. }
  802. public static function mb_ucwords($str) {
  803. $max_len = mb_strlen($str);
  804. if ($max_len === 1) {
  805. return mb_strtoupper($str);
  806. }
  807. $str = mb_strtoupper(mb_substr($str, 0, 1)) . mb_substr($str, 1);
  808. foreach ([' ', '.', ',', '!', '?', '-', '+'] as $s) {
  809. $pos = 0;
  810. while (($pos = mb_strpos($str, $s, $pos)) !== false) {
  811. $pos++;
  812. // Nothing to do if the separator is the last char of the string
  813. if ($pos !== false && $pos < $max_len) {
  814. // If the char we want to upper is the last char there is nothing to append behind
  815. if ($pos + 1 < $max_len) {
  816. $str = mb_substr($str, 0, $pos) . mb_strtoupper(mb_substr($str, $pos, 1)) . mb_substr($str, $pos + 1);
  817. } else {
  818. $str = mb_substr($str, 0, $pos) . mb_strtoupper(mb_substr($str, $pos, 1));
  819. }
  820. }
  821. }
  822. }
  823. return $str;
  824. }
  825. }