You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

OC_Image.php 36KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bartek Przybylski <bart.p.pl@gmail.com>
  6. * @author Bart Visscher <bartv@thisnet.nl>
  7. * @author Björn Schießle <bjoern@schiessle.org>
  8. * @author Byron Marohn <combustible@live.com>
  9. * @author Christopher Schäpers <kondou@ts.unde.re>
  10. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  11. * @author Georg Ehrke <oc.list@georgehrke.com>
  12. * @author J0WI <J0WI@users.noreply.github.com>
  13. * @author j-ed <juergen@eisfair.org>
  14. * @author Joas Schilling <coding@schilljs.com>
  15. * @author Johannes Willnecker <johannes@willnecker.com>
  16. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  17. * @author Julius Härtl <jus@bitgrid.net>
  18. * @author Lukas Reschke <lukas@statuscode.ch>
  19. * @author Morris Jobke <hey@morrisjobke.de>
  20. * @author Olivier Paroz <github@oparoz.com>
  21. * @author Robin Appelman <robin@icewind.nl>
  22. * @author Roeland Jago Douma <roeland@famdouma.nl>
  23. * @author Samuel CHEMLA <chemla.samuel@gmail.com>
  24. * @author Thomas Müller <thomas.mueller@tmit.eu>
  25. * @author Thomas Tanghus <thomas@tanghus.net>
  26. *
  27. * @license AGPL-3.0
  28. *
  29. * This code is free software: you can redistribute it and/or modify
  30. * it under the terms of the GNU Affero General Public License, version 3,
  31. * as published by the Free Software Foundation.
  32. *
  33. * This program is distributed in the hope that it will be useful,
  34. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  35. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  36. * GNU Affero General Public License for more details.
  37. *
  38. * You should have received a copy of the GNU Affero General Public License, version 3,
  39. * along with this program. If not, see <http://www.gnu.org/licenses/>
  40. *
  41. */
  42. use OCP\IImage;
  43. /**
  44. * Class for basic image manipulation
  45. */
  46. class OC_Image implements \OCP\IImage {
  47. /** @var false|resource */
  48. protected $resource = false; // tmp resource.
  49. /** @var int */
  50. protected $imageType = IMAGETYPE_PNG; // Default to png if file type isn't evident.
  51. /** @var string */
  52. protected $mimeType = 'image/png'; // Default to png
  53. /** @var int */
  54. protected $bitDepth = 24;
  55. /** @var null|string */
  56. protected $filePath = null;
  57. /** @var finfo */
  58. private $fileInfo;
  59. /** @var \OCP\ILogger */
  60. private $logger;
  61. /** @var \OCP\IConfig */
  62. private $config;
  63. /** @var array */
  64. private $exif;
  65. /**
  66. * Constructor.
  67. *
  68. * @param resource|string $imageRef The path to a local file, a base64 encoded string or a resource created by
  69. * an imagecreate* function.
  70. * @param \OCP\ILogger $logger
  71. * @param \OCP\IConfig $config
  72. * @throws \InvalidArgumentException in case the $imageRef parameter is not null
  73. */
  74. public function __construct($imageRef = null, \OCP\ILogger $logger = null, \OCP\IConfig $config = null) {
  75. $this->logger = $logger;
  76. if ($logger === null) {
  77. $this->logger = \OC::$server->getLogger();
  78. }
  79. $this->config = $config;
  80. if ($config === null) {
  81. $this->config = \OC::$server->getConfig();
  82. }
  83. if (\OC_Util::fileInfoLoaded()) {
  84. $this->fileInfo = new finfo(FILEINFO_MIME_TYPE);
  85. }
  86. if ($imageRef !== null) {
  87. throw new \InvalidArgumentException('The first parameter in the constructor is not supported anymore. Please use any of the load* methods of the image object to load an image.');
  88. }
  89. }
  90. /**
  91. * Determine whether the object contains an image resource.
  92. *
  93. * @return bool
  94. */
  95. public function valid() { // apparently you can't name a method 'empty'...
  96. if (is_resource($this->resource)) {
  97. return true;
  98. }
  99. if (is_object($this->resource) && get_class($this->resource) === \GdImage::class) {
  100. return true;
  101. }
  102. return false;
  103. }
  104. /**
  105. * Returns the MIME type of the image or an empty string if no image is loaded.
  106. *
  107. * @return string
  108. */
  109. public function mimeType() {
  110. return $this->valid() ? $this->mimeType : '';
  111. }
  112. /**
  113. * Returns the width of the image or -1 if no image is loaded.
  114. *
  115. * @return int
  116. */
  117. public function width() {
  118. return $this->valid() ? imagesx($this->resource) : -1;
  119. }
  120. /**
  121. * Returns the height of the image or -1 if no image is loaded.
  122. *
  123. * @return int
  124. */
  125. public function height() {
  126. return $this->valid() ? imagesy($this->resource) : -1;
  127. }
  128. /**
  129. * Returns the width when the image orientation is top-left.
  130. *
  131. * @return int
  132. */
  133. public function widthTopLeft() {
  134. $o = $this->getOrientation();
  135. $this->logger->debug('OC_Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']);
  136. switch ($o) {
  137. case -1:
  138. case 1:
  139. case 2: // Not tested
  140. case 3:
  141. case 4: // Not tested
  142. return $this->width();
  143. case 5: // Not tested
  144. case 6:
  145. case 7: // Not tested
  146. case 8:
  147. return $this->height();
  148. }
  149. return $this->width();
  150. }
  151. /**
  152. * Returns the height when the image orientation is top-left.
  153. *
  154. * @return int
  155. */
  156. public function heightTopLeft() {
  157. $o = $this->getOrientation();
  158. $this->logger->debug('OC_Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']);
  159. switch ($o) {
  160. case -1:
  161. case 1:
  162. case 2: // Not tested
  163. case 3:
  164. case 4: // Not tested
  165. return $this->height();
  166. case 5: // Not tested
  167. case 6:
  168. case 7: // Not tested
  169. case 8:
  170. return $this->width();
  171. }
  172. return $this->height();
  173. }
  174. /**
  175. * Outputs the image.
  176. *
  177. * @param string $mimeType
  178. * @return bool
  179. */
  180. public function show($mimeType = null) {
  181. if ($mimeType === null) {
  182. $mimeType = $this->mimeType();
  183. }
  184. header('Content-Type: ' . $mimeType);
  185. return $this->_output(null, $mimeType);
  186. }
  187. /**
  188. * Saves the image.
  189. *
  190. * @param string $filePath
  191. * @param string $mimeType
  192. * @return bool
  193. */
  194. public function save($filePath = null, $mimeType = null) {
  195. if ($mimeType === null) {
  196. $mimeType = $this->mimeType();
  197. }
  198. if ($filePath === null) {
  199. if ($this->filePath === null) {
  200. $this->logger->error(__METHOD__ . '(): called with no path.', ['app' => 'core']);
  201. return false;
  202. } else {
  203. $filePath = $this->filePath;
  204. }
  205. }
  206. return $this->_output($filePath, $mimeType);
  207. }
  208. /**
  209. * Outputs/saves the image.
  210. *
  211. * @param string $filePath
  212. * @param string $mimeType
  213. * @return bool
  214. * @throws Exception
  215. */
  216. private function _output($filePath = null, $mimeType = null) {
  217. if ($filePath) {
  218. if (!file_exists(dirname($filePath))) {
  219. mkdir(dirname($filePath), 0777, true);
  220. }
  221. $isWritable = is_writable(dirname($filePath));
  222. if (!$isWritable) {
  223. $this->logger->error(__METHOD__ . '(): Directory \'' . dirname($filePath) . '\' is not writable.', ['app' => 'core']);
  224. return false;
  225. } elseif ($isWritable && file_exists($filePath) && !is_writable($filePath)) {
  226. $this->logger->error(__METHOD__ . '(): File \'' . $filePath . '\' is not writable.', ['app' => 'core']);
  227. return false;
  228. }
  229. }
  230. if (!$this->valid()) {
  231. return false;
  232. }
  233. $imageType = $this->imageType;
  234. if ($mimeType !== null) {
  235. switch ($mimeType) {
  236. case 'image/gif':
  237. $imageType = IMAGETYPE_GIF;
  238. break;
  239. case 'image/jpeg':
  240. $imageType = IMAGETYPE_JPEG;
  241. break;
  242. case 'image/png':
  243. $imageType = IMAGETYPE_PNG;
  244. break;
  245. case 'image/x-xbitmap':
  246. $imageType = IMAGETYPE_XBM;
  247. break;
  248. case 'image/bmp':
  249. case 'image/x-ms-bmp':
  250. $imageType = IMAGETYPE_BMP;
  251. break;
  252. default:
  253. throw new Exception('\OC_Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format');
  254. }
  255. }
  256. switch ($imageType) {
  257. case IMAGETYPE_GIF:
  258. $retVal = imagegif($this->resource, $filePath);
  259. break;
  260. case IMAGETYPE_JPEG:
  261. $retVal = imagejpeg($this->resource, $filePath, $this->getJpegQuality());
  262. break;
  263. case IMAGETYPE_PNG:
  264. $retVal = imagepng($this->resource, $filePath);
  265. break;
  266. case IMAGETYPE_XBM:
  267. if (function_exists('imagexbm')) {
  268. $retVal = imagexbm($this->resource, $filePath);
  269. } else {
  270. throw new Exception('\OC_Image::_output(): imagexbm() is not supported.');
  271. }
  272. break;
  273. case IMAGETYPE_WBMP:
  274. $retVal = imagewbmp($this->resource, $filePath);
  275. break;
  276. case IMAGETYPE_BMP:
  277. $retVal = imagebmp($this->resource, $filePath, $this->bitDepth);
  278. break;
  279. default:
  280. $retVal = imagepng($this->resource, $filePath);
  281. }
  282. return $retVal;
  283. }
  284. /**
  285. * Prints the image when called as $image().
  286. */
  287. public function __invoke() {
  288. return $this->show();
  289. }
  290. /**
  291. * @param resource|\GdImage $resource
  292. * @throws \InvalidArgumentException in case the supplied resource does not have the type "gd"
  293. */
  294. public function setResource($resource) {
  295. // For PHP<8
  296. if (is_resource($resource) && get_resource_type($resource) === 'gd') {
  297. $this->resource = $resource;
  298. return;
  299. }
  300. // PHP 8 has real objects for GD stuff
  301. if (is_object($resource) && get_class($resource) === \GdImage::class) {
  302. $this->resource = $resource;
  303. return;
  304. }
  305. throw new \InvalidArgumentException('Supplied resource is not of type "gd".');
  306. }
  307. /**
  308. * @return resource|\GdImage Returns the image resource in any.
  309. */
  310. public function resource() {
  311. return $this->resource;
  312. }
  313. /**
  314. * @return string Returns the mimetype of the data. Returns the empty string
  315. * if the data is not valid.
  316. */
  317. public function dataMimeType() {
  318. if (!$this->valid()) {
  319. return '';
  320. }
  321. switch ($this->mimeType) {
  322. case 'image/png':
  323. case 'image/jpeg':
  324. case 'image/gif':
  325. return $this->mimeType;
  326. default:
  327. return 'image/png';
  328. }
  329. }
  330. /**
  331. * @return null|string Returns the raw image data.
  332. */
  333. public function data() {
  334. if (!$this->valid()) {
  335. return null;
  336. }
  337. ob_start();
  338. switch ($this->mimeType) {
  339. case "image/png":
  340. $res = imagepng($this->resource);
  341. break;
  342. case "image/jpeg":
  343. $quality = $this->getJpegQuality();
  344. if ($quality !== null) {
  345. $res = imagejpeg($this->resource, null, $quality);
  346. } else {
  347. $res = imagejpeg($this->resource);
  348. }
  349. break;
  350. case "image/gif":
  351. $res = imagegif($this->resource);
  352. break;
  353. default:
  354. $res = imagepng($this->resource);
  355. $this->logger->info('OC_Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']);
  356. break;
  357. }
  358. if (!$res) {
  359. $this->logger->error('OC_Image->data. Error getting image data.', ['app' => 'core']);
  360. }
  361. return ob_get_clean();
  362. }
  363. /**
  364. * @return string - base64 encoded, which is suitable for embedding in a VCard.
  365. */
  366. public function __toString() {
  367. return base64_encode($this->data());
  368. }
  369. /**
  370. * @return int|null
  371. */
  372. protected function getJpegQuality() {
  373. $quality = $this->config->getAppValue('preview', 'jpeg_quality', 90);
  374. if ($quality !== null) {
  375. $quality = min(100, max(10, (int) $quality));
  376. }
  377. return $quality;
  378. }
  379. /**
  380. * (I'm open for suggestions on better method name ;)
  381. * Get the orientation based on EXIF data.
  382. *
  383. * @return int The orientation or -1 if no EXIF data is available.
  384. */
  385. public function getOrientation() {
  386. if ($this->exif !== null) {
  387. return $this->exif['Orientation'];
  388. }
  389. if ($this->imageType !== IMAGETYPE_JPEG) {
  390. $this->logger->debug('OC_Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']);
  391. return -1;
  392. }
  393. if (!is_callable('exif_read_data')) {
  394. $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
  395. return -1;
  396. }
  397. if (!$this->valid()) {
  398. $this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']);
  399. return -1;
  400. }
  401. if (is_null($this->filePath) || !is_readable($this->filePath)) {
  402. $this->logger->debug('OC_Image->fixOrientation() No readable file path set.', ['app' => 'core']);
  403. return -1;
  404. }
  405. $exif = @exif_read_data($this->filePath, 'IFD0');
  406. if (!$exif) {
  407. return -1;
  408. }
  409. if (!isset($exif['Orientation'])) {
  410. return -1;
  411. }
  412. $this->exif = $exif;
  413. return $exif['Orientation'];
  414. }
  415. public function readExif($data) {
  416. if (!is_callable('exif_read_data')) {
  417. $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
  418. return;
  419. }
  420. if (!$this->valid()) {
  421. $this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']);
  422. return;
  423. }
  424. $exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($data));
  425. if (!$exif) {
  426. return;
  427. }
  428. if (!isset($exif['Orientation'])) {
  429. return;
  430. }
  431. $this->exif = $exif;
  432. }
  433. /**
  434. * (I'm open for suggestions on better method name ;)
  435. * Fixes orientation based on EXIF data.
  436. *
  437. * @return bool
  438. */
  439. public function fixOrientation() {
  440. $o = $this->getOrientation();
  441. $this->logger->debug('OC_Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']);
  442. $rotate = 0;
  443. $flip = false;
  444. switch ($o) {
  445. case -1:
  446. return false; //Nothing to fix
  447. case 1:
  448. $rotate = 0;
  449. break;
  450. case 2:
  451. $rotate = 0;
  452. $flip = true;
  453. break;
  454. case 3:
  455. $rotate = 180;
  456. break;
  457. case 4:
  458. $rotate = 180;
  459. $flip = true;
  460. break;
  461. case 5:
  462. $rotate = 90;
  463. $flip = true;
  464. break;
  465. case 6:
  466. $rotate = 270;
  467. break;
  468. case 7:
  469. $rotate = 270;
  470. $flip = true;
  471. break;
  472. case 8:
  473. $rotate = 90;
  474. break;
  475. }
  476. if ($flip && function_exists('imageflip')) {
  477. imageflip($this->resource, IMG_FLIP_HORIZONTAL);
  478. }
  479. if ($rotate) {
  480. $res = imagerotate($this->resource, $rotate, 0);
  481. if ($res) {
  482. if (imagealphablending($res, true)) {
  483. if (imagesavealpha($res, true)) {
  484. imagedestroy($this->resource);
  485. $this->resource = $res;
  486. return true;
  487. } else {
  488. $this->logger->debug('OC_Image->fixOrientation() Error during alpha-saving', ['app' => 'core']);
  489. return false;
  490. }
  491. } else {
  492. $this->logger->debug('OC_Image->fixOrientation() Error during alpha-blending', ['app' => 'core']);
  493. return false;
  494. }
  495. } else {
  496. $this->logger->debug('OC_Image->fixOrientation() Error during orientation fixing', ['app' => 'core']);
  497. return false;
  498. }
  499. }
  500. return false;
  501. }
  502. /**
  503. * Loads an image from an open file handle.
  504. * It is the responsibility of the caller to position the pointer at the correct place and to close the handle again.
  505. *
  506. * @param resource $handle
  507. * @return resource|\GdImage|false An image resource or false on error
  508. */
  509. public function loadFromFileHandle($handle) {
  510. $contents = stream_get_contents($handle);
  511. if ($this->loadFromData($contents)) {
  512. return $this->resource;
  513. }
  514. return false;
  515. }
  516. /**
  517. * Loads an image from a local file.
  518. *
  519. * @param bool|string $imagePath The path to a local file.
  520. * @return bool|resource|\GdImage An image resource or false on error
  521. */
  522. public function loadFromFile($imagePath = false) {
  523. // exif_imagetype throws "read error!" if file is less than 12 byte
  524. if (is_bool($imagePath) || !@is_file($imagePath) || !file_exists($imagePath) || filesize($imagePath) < 12 || !is_readable($imagePath)) {
  525. return false;
  526. }
  527. $iType = exif_imagetype($imagePath);
  528. switch ($iType) {
  529. case IMAGETYPE_GIF:
  530. if (imagetypes() & IMG_GIF) {
  531. $this->resource = imagecreatefromgif($imagePath);
  532. if ($this->resource) {
  533. // Preserve transparency
  534. imagealphablending($this->resource, true);
  535. imagesavealpha($this->resource, true);
  536. } else {
  537. $this->logger->debug('OC_Image->loadFromFile, GIF image not valid: ' . $imagePath, ['app' => 'core']);
  538. }
  539. } else {
  540. $this->logger->debug('OC_Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']);
  541. }
  542. break;
  543. case IMAGETYPE_JPEG:
  544. if (imagetypes() & IMG_JPG) {
  545. if (getimagesize($imagePath) !== false) {
  546. $this->resource = @imagecreatefromjpeg($imagePath);
  547. } else {
  548. $this->logger->debug('OC_Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']);
  549. }
  550. } else {
  551. $this->logger->debug('OC_Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']);
  552. }
  553. break;
  554. case IMAGETYPE_PNG:
  555. if (imagetypes() & IMG_PNG) {
  556. $this->resource = @imagecreatefrompng($imagePath);
  557. if ($this->resource) {
  558. // Preserve transparency
  559. imagealphablending($this->resource, true);
  560. imagesavealpha($this->resource, true);
  561. } else {
  562. $this->logger->debug('OC_Image->loadFromFile, PNG image not valid: ' . $imagePath, ['app' => 'core']);
  563. }
  564. } else {
  565. $this->logger->debug('OC_Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']);
  566. }
  567. break;
  568. case IMAGETYPE_XBM:
  569. if (imagetypes() & IMG_XPM) {
  570. $this->resource = @imagecreatefromxbm($imagePath);
  571. } else {
  572. $this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']);
  573. }
  574. break;
  575. case IMAGETYPE_WBMP:
  576. if (imagetypes() & IMG_WBMP) {
  577. $this->resource = @imagecreatefromwbmp($imagePath);
  578. } else {
  579. $this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']);
  580. }
  581. break;
  582. case IMAGETYPE_BMP:
  583. $this->resource = $this->imagecreatefrombmp($imagePath);
  584. break;
  585. case IMAGETYPE_WEBP:
  586. if (imagetypes() & IMG_WEBP) {
  587. $this->resource = @imagecreatefromwebp($imagePath);
  588. } else {
  589. $this->logger->debug('OC_Image->loadFromFile, webp images not supported: ' . $imagePath, ['app' => 'core']);
  590. }
  591. break;
  592. /*
  593. case IMAGETYPE_TIFF_II: // (intel byte order)
  594. break;
  595. case IMAGETYPE_TIFF_MM: // (motorola byte order)
  596. break;
  597. case IMAGETYPE_JPC:
  598. break;
  599. case IMAGETYPE_JP2:
  600. break;
  601. case IMAGETYPE_JPX:
  602. break;
  603. case IMAGETYPE_JB2:
  604. break;
  605. case IMAGETYPE_SWC:
  606. break;
  607. case IMAGETYPE_IFF:
  608. break;
  609. case IMAGETYPE_ICO:
  610. break;
  611. case IMAGETYPE_SWF:
  612. break;
  613. case IMAGETYPE_PSD:
  614. break;
  615. */
  616. default:
  617. // this is mostly file created from encrypted file
  618. $this->resource = imagecreatefromstring(file_get_contents($imagePath));
  619. $iType = IMAGETYPE_PNG;
  620. $this->logger->debug('OC_Image->loadFromFile, Default', ['app' => 'core']);
  621. break;
  622. }
  623. if ($this->valid()) {
  624. $this->imageType = $iType;
  625. $this->mimeType = image_type_to_mime_type($iType);
  626. $this->filePath = $imagePath;
  627. }
  628. return $this->resource;
  629. }
  630. /**
  631. * Loads an image from a string of data.
  632. *
  633. * @param string $str A string of image data as read from a file.
  634. * @return bool|resource|\GdImage An image resource or false on error
  635. */
  636. public function loadFromData($str) {
  637. if (!is_string($str)) {
  638. return false;
  639. }
  640. $this->resource = @imagecreatefromstring($str);
  641. if ($this->fileInfo) {
  642. $this->mimeType = $this->fileInfo->buffer($str);
  643. }
  644. if ($this->valid()) {
  645. imagealphablending($this->resource, false);
  646. imagesavealpha($this->resource, true);
  647. }
  648. if (!$this->resource) {
  649. $this->logger->debug('OC_Image->loadFromFile, could not load', ['app' => 'core']);
  650. return false;
  651. }
  652. return $this->resource;
  653. }
  654. /**
  655. * Loads an image from a base64 encoded string.
  656. *
  657. * @param string $str A string base64 encoded string of image data.
  658. * @return bool|resource|\GdImage An image resource or false on error
  659. */
  660. public function loadFromBase64($str) {
  661. if (!is_string($str)) {
  662. return false;
  663. }
  664. $data = base64_decode($str);
  665. if ($data) { // try to load from string data
  666. $this->resource = @imagecreatefromstring($data);
  667. if ($this->fileInfo) {
  668. $this->mimeType = $this->fileInfo->buffer($data);
  669. }
  670. if (!$this->resource) {
  671. $this->logger->debug('OC_Image->loadFromBase64, could not load', ['app' => 'core']);
  672. return false;
  673. }
  674. return $this->resource;
  675. } else {
  676. return false;
  677. }
  678. }
  679. /**
  680. * Create a new image from file or URL
  681. *
  682. * @link http://www.programmierer-forum.de/function-imagecreatefrombmp-laeuft-mit-allen-bitraten-t143137.htm
  683. * @version 1.00
  684. * @param string $fileName <p>
  685. * Path to the BMP image.
  686. * </p>
  687. * @return bool|resource|\GdImage an image resource identifier on success, <b>FALSE</b> on errors.
  688. */
  689. private function imagecreatefrombmp($fileName) {
  690. if (!($fh = fopen($fileName, 'rb'))) {
  691. $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName, ['app' => 'core']);
  692. return false;
  693. }
  694. // read file header
  695. $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14));
  696. // check for bitmap
  697. if ($meta['type'] != 19778) {
  698. fclose($fh);
  699. $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
  700. return false;
  701. }
  702. // read image header
  703. $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40));
  704. // read additional 16bit header
  705. if ($meta['bits'] == 16) {
  706. $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12));
  707. }
  708. // set bytes and padding
  709. $meta['bytes'] = $meta['bits'] / 8;
  710. $this->bitDepth = $meta['bits']; //remember the bit depth for the imagebmp call
  711. $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4)));
  712. if ($meta['decal'] == 4) {
  713. $meta['decal'] = 0;
  714. }
  715. // obtain imagesize
  716. if ($meta['imagesize'] < 1) {
  717. $meta['imagesize'] = $meta['filesize'] - $meta['offset'];
  718. // in rare cases filesize is equal to offset so we need to read physical size
  719. if ($meta['imagesize'] < 1) {
  720. $meta['imagesize'] = @filesize($fileName) - $meta['offset'];
  721. if ($meta['imagesize'] < 1) {
  722. fclose($fh);
  723. $this->logger->warning('imagecreatefrombmp: Can not obtain file size of ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
  724. return false;
  725. }
  726. }
  727. }
  728. // calculate colors
  729. $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors'];
  730. // read color palette
  731. $palette = [];
  732. if ($meta['bits'] < 16) {
  733. $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4));
  734. // in rare cases the color value is signed
  735. if ($palette[1] < 0) {
  736. foreach ($palette as $i => $color) {
  737. $palette[$i] = $color + 16777216;
  738. }
  739. }
  740. }
  741. // create gd image
  742. $im = imagecreatetruecolor($meta['width'], $meta['height']);
  743. if ($im == false) {
  744. fclose($fh);
  745. $this->logger->warning(
  746. 'imagecreatefrombmp: imagecreatetruecolor failed for file "' . $fileName . '" with dimensions ' . $meta['width'] . 'x' . $meta['height'],
  747. ['app' => 'core']);
  748. return false;
  749. }
  750. $data = fread($fh, $meta['imagesize']);
  751. $p = 0;
  752. $vide = chr(0);
  753. $y = $meta['height'] - 1;
  754. $error = 'imagecreatefrombmp: ' . $fileName . ' has not enough data!';
  755. // loop through the image data beginning with the lower left corner
  756. while ($y >= 0) {
  757. $x = 0;
  758. while ($x < $meta['width']) {
  759. switch ($meta['bits']) {
  760. case 32:
  761. case 24:
  762. if (!($part = substr($data, $p, 3))) {
  763. $this->logger->warning($error, ['app' => 'core']);
  764. return $im;
  765. }
  766. $color = @unpack('V', $part . $vide);
  767. break;
  768. case 16:
  769. if (!($part = substr($data, $p, 2))) {
  770. fclose($fh);
  771. $this->logger->warning($error, ['app' => 'core']);
  772. return $im;
  773. }
  774. $color = @unpack('v', $part);
  775. $color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3);
  776. break;
  777. case 8:
  778. $color = @unpack('n', $vide . ($data[$p] ?? ''));
  779. $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
  780. break;
  781. case 4:
  782. $color = @unpack('n', $vide . ($data[floor($p)] ?? ''));
  783. $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F;
  784. $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
  785. break;
  786. case 1:
  787. $color = @unpack('n', $vide . ($data[floor($p)] ?? ''));
  788. switch (($p * 8) % 8) {
  789. case 0:
  790. $color[1] = $color[1] >> 7;
  791. break;
  792. case 1:
  793. $color[1] = ($color[1] & 0x40) >> 6;
  794. break;
  795. case 2:
  796. $color[1] = ($color[1] & 0x20) >> 5;
  797. break;
  798. case 3:
  799. $color[1] = ($color[1] & 0x10) >> 4;
  800. break;
  801. case 4:
  802. $color[1] = ($color[1] & 0x8) >> 3;
  803. break;
  804. case 5:
  805. $color[1] = ($color[1] & 0x4) >> 2;
  806. break;
  807. case 6:
  808. $color[1] = ($color[1] & 0x2) >> 1;
  809. break;
  810. case 7:
  811. $color[1] = ($color[1] & 0x1);
  812. break;
  813. }
  814. $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
  815. break;
  816. default:
  817. fclose($fh);
  818. $this->logger->warning('imagecreatefrombmp: ' . $fileName . ' has ' . $meta['bits'] . ' bits and this is not supported!', ['app' => 'core']);
  819. return false;
  820. }
  821. imagesetpixel($im, $x, $y, $color[1]);
  822. $x++;
  823. $p += $meta['bytes'];
  824. }
  825. $y--;
  826. $p += $meta['decal'];
  827. }
  828. fclose($fh);
  829. return $im;
  830. }
  831. /**
  832. * Resizes the image preserving ratio.
  833. *
  834. * @param integer $maxSize The maximum size of either the width or height.
  835. * @return bool
  836. */
  837. public function resize($maxSize) {
  838. $result = $this->resizeNew($maxSize);
  839. imagedestroy($this->resource);
  840. $this->resource = $result;
  841. return $this->valid();
  842. }
  843. /**
  844. * @param $maxSize
  845. * @return resource|bool|\GdImage
  846. */
  847. private function resizeNew($maxSize) {
  848. if (!$this->valid()) {
  849. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  850. return false;
  851. }
  852. $widthOrig = imagesx($this->resource);
  853. $heightOrig = imagesy($this->resource);
  854. $ratioOrig = $widthOrig / $heightOrig;
  855. if ($ratioOrig > 1) {
  856. $newHeight = round($maxSize / $ratioOrig);
  857. $newWidth = $maxSize;
  858. } else {
  859. $newWidth = round($maxSize * $ratioOrig);
  860. $newHeight = $maxSize;
  861. }
  862. return $this->preciseResizeNew((int)round($newWidth), (int)round($newHeight));
  863. }
  864. /**
  865. * @param int $width
  866. * @param int $height
  867. * @return bool
  868. */
  869. public function preciseResize(int $width, int $height): bool {
  870. $result = $this->preciseResizeNew($width, $height);
  871. imagedestroy($this->resource);
  872. $this->resource = $result;
  873. return $this->valid();
  874. }
  875. /**
  876. * @param int $width
  877. * @param int $height
  878. * @return resource|bool|\GdImage
  879. */
  880. public function preciseResizeNew(int $width, int $height) {
  881. if (!$this->valid()) {
  882. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  883. return false;
  884. }
  885. $widthOrig = imagesx($this->resource);
  886. $heightOrig = imagesy($this->resource);
  887. $process = imagecreatetruecolor($width, $height);
  888. if ($process === false) {
  889. $this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
  890. return false;
  891. }
  892. // preserve transparency
  893. if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
  894. imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
  895. imagealphablending($process, false);
  896. imagesavealpha($process, true);
  897. }
  898. $res = imagecopyresampled($process, $this->resource, 0, 0, 0, 0, $width, $height, $widthOrig, $heightOrig);
  899. if ($res === false) {
  900. $this->logger->error(__METHOD__ . '(): Error re-sampling process image', ['app' => 'core']);
  901. imagedestroy($process);
  902. return false;
  903. }
  904. return $process;
  905. }
  906. /**
  907. * Crops the image to the middle square. If the image is already square it just returns.
  908. *
  909. * @param int $size maximum size for the result (optional)
  910. * @return bool for success or failure
  911. */
  912. public function centerCrop($size = 0) {
  913. if (!$this->valid()) {
  914. $this->logger->error('OC_Image->centerCrop, No image loaded', ['app' => 'core']);
  915. return false;
  916. }
  917. $widthOrig = imagesx($this->resource);
  918. $heightOrig = imagesy($this->resource);
  919. if ($widthOrig === $heightOrig and $size == 0) {
  920. return true;
  921. }
  922. $ratioOrig = $widthOrig / $heightOrig;
  923. $width = $height = min($widthOrig, $heightOrig);
  924. if ($ratioOrig > 1) {
  925. $x = ($widthOrig / 2) - ($width / 2);
  926. $y = 0;
  927. } else {
  928. $y = ($heightOrig / 2) - ($height / 2);
  929. $x = 0;
  930. }
  931. if ($size > 0) {
  932. $targetWidth = $size;
  933. $targetHeight = $size;
  934. } else {
  935. $targetWidth = $width;
  936. $targetHeight = $height;
  937. }
  938. $process = imagecreatetruecolor($targetWidth, $targetHeight);
  939. if ($process == false) {
  940. $this->logger->error('OC_Image->centerCrop, Error creating true color image', ['app' => 'core']);
  941. imagedestroy($process);
  942. return false;
  943. }
  944. // preserve transparency
  945. if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
  946. imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
  947. imagealphablending($process, false);
  948. imagesavealpha($process, true);
  949. }
  950. imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height);
  951. if ($process == false) {
  952. $this->logger->error('OC_Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']);
  953. imagedestroy($process);
  954. return false;
  955. }
  956. imagedestroy($this->resource);
  957. $this->resource = $process;
  958. return true;
  959. }
  960. /**
  961. * Crops the image from point $x$y with dimension $wx$h.
  962. *
  963. * @param int $x Horizontal position
  964. * @param int $y Vertical position
  965. * @param int $w Width
  966. * @param int $h Height
  967. * @return bool for success or failure
  968. */
  969. public function crop(int $x, int $y, int $w, int $h): bool {
  970. $result = $this->cropNew($x, $y, $w, $h);
  971. imagedestroy($this->resource);
  972. $this->resource = $result;
  973. return $this->valid();
  974. }
  975. /**
  976. * Crops the image from point $x$y with dimension $wx$h.
  977. *
  978. * @param int $x Horizontal position
  979. * @param int $y Vertical position
  980. * @param int $w Width
  981. * @param int $h Height
  982. * @return resource | bool
  983. */
  984. public function cropNew(int $x, int $y, int $w, int $h) {
  985. if (!$this->valid()) {
  986. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  987. return false;
  988. }
  989. $process = imagecreatetruecolor($w, $h);
  990. if ($process == false) {
  991. $this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
  992. imagedestroy($process);
  993. return false;
  994. }
  995. // preserve transparency
  996. if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
  997. imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
  998. imagealphablending($process, false);
  999. imagesavealpha($process, true);
  1000. }
  1001. imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $w, $h, $w, $h);
  1002. if ($process == false) {
  1003. $this->logger->error(__METHOD__ . '(): Error re-sampling process image ' . $w . 'x' . $h, ['app' => 'core']);
  1004. imagedestroy($process);
  1005. return false;
  1006. }
  1007. return $process;
  1008. }
  1009. /**
  1010. * Resizes the image to fit within a boundary while preserving ratio.
  1011. *
  1012. * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up
  1013. *
  1014. * @param integer $maxWidth
  1015. * @param integer $maxHeight
  1016. * @return bool
  1017. */
  1018. public function fitIn($maxWidth, $maxHeight) {
  1019. if (!$this->valid()) {
  1020. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  1021. return false;
  1022. }
  1023. $widthOrig = imagesx($this->resource);
  1024. $heightOrig = imagesy($this->resource);
  1025. $ratio = $widthOrig / $heightOrig;
  1026. $newWidth = min($maxWidth, $ratio * $maxHeight);
  1027. $newHeight = min($maxHeight, $maxWidth / $ratio);
  1028. $this->preciseResize((int)round($newWidth), (int)round($newHeight));
  1029. return true;
  1030. }
  1031. /**
  1032. * Shrinks larger images to fit within specified boundaries while preserving ratio.
  1033. *
  1034. * @param integer $maxWidth
  1035. * @param integer $maxHeight
  1036. * @return bool
  1037. */
  1038. public function scaleDownToFit($maxWidth, $maxHeight) {
  1039. if (!$this->valid()) {
  1040. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  1041. return false;
  1042. }
  1043. $widthOrig = imagesx($this->resource);
  1044. $heightOrig = imagesy($this->resource);
  1045. if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) {
  1046. return $this->fitIn($maxWidth, $maxHeight);
  1047. }
  1048. return false;
  1049. }
  1050. public function copy(): IImage {
  1051. $image = new OC_Image(null, $this->logger, $this->config);
  1052. $image->resource = imagecreatetruecolor($this->width(), $this->height());
  1053. imagecopy(
  1054. $image->resource(),
  1055. $this->resource(),
  1056. 0,
  1057. 0,
  1058. 0,
  1059. 0,
  1060. $this->width(),
  1061. $this->height()
  1062. );
  1063. return $image;
  1064. }
  1065. public function cropCopy(int $x, int $y, int $w, int $h): IImage {
  1066. $image = new OC_Image(null, $this->logger, $this->config);
  1067. $image->imageType = $this->imageType;
  1068. $image->mimeType = $this->mimeType;
  1069. $image->bitDepth = $this->bitDepth;
  1070. $image->resource = $this->cropNew($x, $y, $w, $h);
  1071. return $image;
  1072. }
  1073. public function preciseResizeCopy(int $width, int $height): IImage {
  1074. $image = new OC_Image(null, $this->logger, $this->config);
  1075. $image->imageType = $this->imageType;
  1076. $image->mimeType = $this->mimeType;
  1077. $image->bitDepth = $this->bitDepth;
  1078. $image->resource = $this->preciseResizeNew($width, $height);
  1079. return $image;
  1080. }
  1081. public function resizeCopy(int $maxSize): IImage {
  1082. $image = new OC_Image(null, $this->logger, $this->config);
  1083. $image->imageType = $this->imageType;
  1084. $image->mimeType = $this->mimeType;
  1085. $image->bitDepth = $this->bitDepth;
  1086. $image->resource = $this->resizeNew($maxSize);
  1087. return $image;
  1088. }
  1089. /**
  1090. * Destroys the current image and resets the object
  1091. */
  1092. public function destroy() {
  1093. if ($this->valid()) {
  1094. imagedestroy($this->resource);
  1095. }
  1096. $this->resource = null;
  1097. }
  1098. public function __destruct() {
  1099. $this->destroy();
  1100. }
  1101. }
  1102. if (!function_exists('imagebmp')) {
  1103. /**
  1104. * Output a BMP image to either the browser or a file
  1105. *
  1106. * @link http://www.ugia.cn/wp-data/imagebmp.php
  1107. * @author legend <legendsky@hotmail.com>
  1108. * @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm
  1109. * @author mgutt <marc@gutt.it>
  1110. * @version 1.00
  1111. * @param resource|\GdImage $im
  1112. * @param string $fileName [optional] <p>The path to save the file to.</p>
  1113. * @param int $bit [optional] <p>Bit depth, (default is 24).</p>
  1114. * @param int $compression [optional]
  1115. * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
  1116. */
  1117. function imagebmp($im, $fileName = '', $bit = 24, $compression = 0) {
  1118. if (!in_array($bit, [1, 4, 8, 16, 24, 32])) {
  1119. $bit = 24;
  1120. } elseif ($bit == 32) {
  1121. $bit = 24;
  1122. }
  1123. $bits = (int)pow(2, $bit);
  1124. imagetruecolortopalette($im, true, $bits);
  1125. $width = imagesx($im);
  1126. $height = imagesy($im);
  1127. $colorsNum = imagecolorstotal($im);
  1128. $rgbQuad = '';
  1129. if ($bit <= 8) {
  1130. for ($i = 0; $i < $colorsNum; $i++) {
  1131. $colors = imagecolorsforindex($im, $i);
  1132. $rgbQuad .= chr($colors['blue']) . chr($colors['green']) . chr($colors['red']) . "\0";
  1133. }
  1134. $bmpData = '';
  1135. if ($compression == 0 || $bit < 8) {
  1136. $compression = 0;
  1137. $extra = '';
  1138. $padding = 4 - ceil($width / (8 / $bit)) % 4;
  1139. if ($padding % 4 != 0) {
  1140. $extra = str_repeat("\0", $padding);
  1141. }
  1142. for ($j = $height - 1; $j >= 0; $j--) {
  1143. $i = 0;
  1144. while ($i < $width) {
  1145. $bin = 0;
  1146. $limit = $width - $i < 8 / $bit ? (8 / $bit - $width + $i) * $bit : 0;
  1147. for ($k = 8 - $bit; $k >= $limit; $k -= $bit) {
  1148. $index = imagecolorat($im, $i, $j);
  1149. $bin |= $index << $k;
  1150. $i++;
  1151. }
  1152. $bmpData .= chr($bin);
  1153. }
  1154. $bmpData .= $extra;
  1155. }
  1156. } // RLE8
  1157. elseif ($compression == 1 && $bit == 8) {
  1158. for ($j = $height - 1; $j >= 0; $j--) {
  1159. $lastIndex = null;
  1160. $sameNum = 0;
  1161. for ($i = 0; $i <= $width; $i++) {
  1162. $index = imagecolorat($im, $i, $j);
  1163. if ($index !== $lastIndex || $sameNum > 255) {
  1164. if ($sameNum != 0) {
  1165. $bmpData .= chr($sameNum) . chr($lastIndex);
  1166. }
  1167. $lastIndex = $index;
  1168. $sameNum = 1;
  1169. } else {
  1170. $sameNum++;
  1171. }
  1172. }
  1173. $bmpData .= "\0\0";
  1174. }
  1175. $bmpData .= "\0\1";
  1176. }
  1177. $sizeQuad = strlen($rgbQuad);
  1178. $sizeData = strlen($bmpData);
  1179. } else {
  1180. $extra = '';
  1181. $padding = 4 - ($width * ($bit / 8)) % 4;
  1182. if ($padding % 4 != 0) {
  1183. $extra = str_repeat("\0", $padding);
  1184. }
  1185. $bmpData = '';
  1186. for ($j = $height - 1; $j >= 0; $j--) {
  1187. for ($i = 0; $i < $width; $i++) {
  1188. $index = imagecolorat($im, $i, $j);
  1189. $colors = imagecolorsforindex($im, $index);
  1190. if ($bit == 16) {
  1191. $bin = 0 << $bit;
  1192. $bin |= ($colors['red'] >> 3) << 10;
  1193. $bin |= ($colors['green'] >> 3) << 5;
  1194. $bin |= $colors['blue'] >> 3;
  1195. $bmpData .= pack("v", $bin);
  1196. } else {
  1197. $bmpData .= pack("c*", $colors['blue'], $colors['green'], $colors['red']);
  1198. }
  1199. }
  1200. $bmpData .= $extra;
  1201. }
  1202. $sizeQuad = 0;
  1203. $sizeData = strlen($bmpData);
  1204. $colorsNum = 0;
  1205. }
  1206. $fileHeader = 'BM' . pack('V3', 54 + $sizeQuad + $sizeData, 0, 54 + $sizeQuad);
  1207. $infoHeader = pack('V3v2V*', 0x28, $width, $height, 1, $bit, $compression, $sizeData, 0, 0, $colorsNum, 0);
  1208. if ($fileName != '') {
  1209. $fp = fopen($fileName, 'wb');
  1210. fwrite($fp, $fileHeader . $infoHeader . $rgbQuad . $bmpData);
  1211. fclose($fp);
  1212. return true;
  1213. }
  1214. echo $fileHeader . $infoHeader . $rgbQuad . $bmpData;
  1215. return true;
  1216. }
  1217. }
  1218. if (!function_exists('exif_imagetype')) {
  1219. /**
  1220. * Workaround if exif_imagetype does not exist
  1221. *
  1222. * @link https://www.php.net/manual/en/function.exif-imagetype.php#80383
  1223. * @param string $fileName
  1224. * @return string|boolean
  1225. */
  1226. function exif_imagetype($fileName) {
  1227. if (($info = getimagesize($fileName)) !== false) {
  1228. return $info[2];
  1229. }
  1230. return false;
  1231. }
  1232. }