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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302
  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') {
  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 Returns the image resource in any.
  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') {
  302. $this->resource = $resource;
  303. return;
  304. }
  305. throw new \InvalidArgumentException('Supplied resource is not of type "gd".');
  306. }
  307. /**
  308. * @return resource 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|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 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_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. // Preserve transparency
  533. imagealphablending($this->resource, true);
  534. imagesavealpha($this->resource, true);
  535. } else {
  536. $this->logger->debug('OC_Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']);
  537. }
  538. break;
  539. case IMAGETYPE_JPEG:
  540. if (imagetypes() & IMG_JPG) {
  541. if (getimagesize($imagePath) !== false) {
  542. $this->resource = @imagecreatefromjpeg($imagePath);
  543. } else {
  544. $this->logger->debug('OC_Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']);
  545. }
  546. } else {
  547. $this->logger->debug('OC_Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']);
  548. }
  549. break;
  550. case IMAGETYPE_PNG:
  551. if (imagetypes() & IMG_PNG) {
  552. $this->resource = @imagecreatefrompng($imagePath);
  553. // Preserve transparency
  554. imagealphablending($this->resource, true);
  555. imagesavealpha($this->resource, true);
  556. } else {
  557. $this->logger->debug('OC_Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']);
  558. }
  559. break;
  560. case IMAGETYPE_XBM:
  561. if (imagetypes() & IMG_XPM) {
  562. $this->resource = @imagecreatefromxbm($imagePath);
  563. } else {
  564. $this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']);
  565. }
  566. break;
  567. case IMAGETYPE_WBMP:
  568. if (imagetypes() & IMG_WBMP) {
  569. $this->resource = @imagecreatefromwbmp($imagePath);
  570. } else {
  571. $this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']);
  572. }
  573. break;
  574. case IMAGETYPE_BMP:
  575. $this->resource = $this->imagecreatefrombmp($imagePath);
  576. break;
  577. /*
  578. case IMAGETYPE_TIFF_II: // (intel byte order)
  579. break;
  580. case IMAGETYPE_TIFF_MM: // (motorola byte order)
  581. break;
  582. case IMAGETYPE_JPC:
  583. break;
  584. case IMAGETYPE_JP2:
  585. break;
  586. case IMAGETYPE_JPX:
  587. break;
  588. case IMAGETYPE_JB2:
  589. break;
  590. case IMAGETYPE_SWC:
  591. break;
  592. case IMAGETYPE_IFF:
  593. break;
  594. case IMAGETYPE_ICO:
  595. break;
  596. case IMAGETYPE_SWF:
  597. break;
  598. case IMAGETYPE_PSD:
  599. break;
  600. */
  601. default:
  602. // this is mostly file created from encrypted file
  603. $this->resource = imagecreatefromstring(\OC\Files\Filesystem::file_get_contents(\OC\Files\Filesystem::getLocalPath($imagePath)));
  604. $iType = IMAGETYPE_PNG;
  605. $this->logger->debug('OC_Image->loadFromFile, Default', ['app' => 'core']);
  606. break;
  607. }
  608. if ($this->valid()) {
  609. $this->imageType = $iType;
  610. $this->mimeType = image_type_to_mime_type($iType);
  611. $this->filePath = $imagePath;
  612. }
  613. return $this->resource;
  614. }
  615. /**
  616. * Loads an image from a string of data.
  617. *
  618. * @param string $str A string of image data as read from a file.
  619. * @return bool|resource An image resource or false on error
  620. */
  621. public function loadFromData($str) {
  622. if (is_resource($str)) {
  623. return false;
  624. }
  625. $this->resource = @imagecreatefromstring($str);
  626. if ($this->fileInfo) {
  627. $this->mimeType = $this->fileInfo->buffer($str);
  628. }
  629. if (is_resource($this->resource)) {
  630. imagealphablending($this->resource, false);
  631. imagesavealpha($this->resource, true);
  632. }
  633. if (!$this->resource) {
  634. $this->logger->debug('OC_Image->loadFromFile, could not load', ['app' => 'core']);
  635. return false;
  636. }
  637. return $this->resource;
  638. }
  639. /**
  640. * Loads an image from a base64 encoded string.
  641. *
  642. * @param string $str A string base64 encoded string of image data.
  643. * @return bool|resource An image resource or false on error
  644. */
  645. public function loadFromBase64($str) {
  646. if (!is_string($str)) {
  647. return false;
  648. }
  649. $data = base64_decode($str);
  650. if ($data) { // try to load from string data
  651. $this->resource = @imagecreatefromstring($data);
  652. if ($this->fileInfo) {
  653. $this->mimeType = $this->fileInfo->buffer($data);
  654. }
  655. if (!$this->resource) {
  656. $this->logger->debug('OC_Image->loadFromBase64, could not load', ['app' => 'core']);
  657. return false;
  658. }
  659. return $this->resource;
  660. } else {
  661. return false;
  662. }
  663. }
  664. /**
  665. * Create a new image from file or URL
  666. *
  667. * @link http://www.programmierer-forum.de/function-imagecreatefrombmp-laeuft-mit-allen-bitraten-t143137.htm
  668. * @version 1.00
  669. * @param string $fileName <p>
  670. * Path to the BMP image.
  671. * </p>
  672. * @return bool|resource an image resource identifier on success, <b>FALSE</b> on errors.
  673. */
  674. private function imagecreatefrombmp($fileName) {
  675. if (!($fh = fopen($fileName, 'rb'))) {
  676. $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName, ['app' => 'core']);
  677. return false;
  678. }
  679. // read file header
  680. $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14));
  681. // check for bitmap
  682. if ($meta['type'] != 19778) {
  683. fclose($fh);
  684. $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
  685. return false;
  686. }
  687. // read image header
  688. $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40));
  689. // read additional 16bit header
  690. if ($meta['bits'] == 16) {
  691. $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12));
  692. }
  693. // set bytes and padding
  694. $meta['bytes'] = $meta['bits'] / 8;
  695. $this->bitDepth = $meta['bits']; //remember the bit depth for the imagebmp call
  696. $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4)));
  697. if ($meta['decal'] == 4) {
  698. $meta['decal'] = 0;
  699. }
  700. // obtain imagesize
  701. if ($meta['imagesize'] < 1) {
  702. $meta['imagesize'] = $meta['filesize'] - $meta['offset'];
  703. // in rare cases filesize is equal to offset so we need to read physical size
  704. if ($meta['imagesize'] < 1) {
  705. $meta['imagesize'] = @filesize($fileName) - $meta['offset'];
  706. if ($meta['imagesize'] < 1) {
  707. fclose($fh);
  708. $this->logger->warning('imagecreatefrombmp: Can not obtain file size of ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
  709. return false;
  710. }
  711. }
  712. }
  713. // calculate colors
  714. $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors'];
  715. // read color palette
  716. $palette = [];
  717. if ($meta['bits'] < 16) {
  718. $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4));
  719. // in rare cases the color value is signed
  720. if ($palette[1] < 0) {
  721. foreach ($palette as $i => $color) {
  722. $palette[$i] = $color + 16777216;
  723. }
  724. }
  725. }
  726. // create gd image
  727. $im = imagecreatetruecolor($meta['width'], $meta['height']);
  728. if ($im == false) {
  729. fclose($fh);
  730. $this->logger->warning(
  731. 'imagecreatefrombmp: imagecreatetruecolor failed for file "' . $fileName . '" with dimensions ' . $meta['width'] . 'x' . $meta['height'],
  732. ['app' => 'core']);
  733. return false;
  734. }
  735. $data = fread($fh, $meta['imagesize']);
  736. $p = 0;
  737. $vide = chr(0);
  738. $y = $meta['height'] - 1;
  739. $error = 'imagecreatefrombmp: ' . $fileName . ' has not enough data!';
  740. // loop through the image data beginning with the lower left corner
  741. while ($y >= 0) {
  742. $x = 0;
  743. while ($x < $meta['width']) {
  744. switch ($meta['bits']) {
  745. case 32:
  746. case 24:
  747. if (!($part = substr($data, $p, 3))) {
  748. $this->logger->warning($error, ['app' => 'core']);
  749. return $im;
  750. }
  751. $color = @unpack('V', $part . $vide);
  752. break;
  753. case 16:
  754. if (!($part = substr($data, $p, 2))) {
  755. fclose($fh);
  756. $this->logger->warning($error, ['app' => 'core']);
  757. return $im;
  758. }
  759. $color = @unpack('v', $part);
  760. $color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3);
  761. break;
  762. case 8:
  763. $color = @unpack('n', $vide . ($data[$p] ?? ''));
  764. $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
  765. break;
  766. case 4:
  767. $color = @unpack('n', $vide . ($data[floor($p)] ?? ''));
  768. $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F;
  769. $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
  770. break;
  771. case 1:
  772. $color = @unpack('n', $vide . ($data[floor($p)] ?? ''));
  773. switch (($p * 8) % 8) {
  774. case 0:
  775. $color[1] = $color[1] >> 7;
  776. break;
  777. case 1:
  778. $color[1] = ($color[1] & 0x40) >> 6;
  779. break;
  780. case 2:
  781. $color[1] = ($color[1] & 0x20) >> 5;
  782. break;
  783. case 3:
  784. $color[1] = ($color[1] & 0x10) >> 4;
  785. break;
  786. case 4:
  787. $color[1] = ($color[1] & 0x8) >> 3;
  788. break;
  789. case 5:
  790. $color[1] = ($color[1] & 0x4) >> 2;
  791. break;
  792. case 6:
  793. $color[1] = ($color[1] & 0x2) >> 1;
  794. break;
  795. case 7:
  796. $color[1] = ($color[1] & 0x1);
  797. break;
  798. }
  799. $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
  800. break;
  801. default:
  802. fclose($fh);
  803. $this->logger->warning('imagecreatefrombmp: ' . $fileName . ' has ' . $meta['bits'] . ' bits and this is not supported!', ['app' => 'core']);
  804. return false;
  805. }
  806. imagesetpixel($im, $x, $y, $color[1]);
  807. $x++;
  808. $p += $meta['bytes'];
  809. }
  810. $y--;
  811. $p += $meta['decal'];
  812. }
  813. fclose($fh);
  814. return $im;
  815. }
  816. /**
  817. * Resizes the image preserving ratio.
  818. *
  819. * @param integer $maxSize The maximum size of either the width or height.
  820. * @return bool
  821. */
  822. public function resize($maxSize) {
  823. $result = $this->resizeNew($maxSize);
  824. imagedestroy($this->resource);
  825. $this->resource = $result;
  826. return is_resource($result);
  827. }
  828. /**
  829. * @param $maxSize
  830. * @return resource | bool
  831. */
  832. private function resizeNew($maxSize) {
  833. if (!$this->valid()) {
  834. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  835. return false;
  836. }
  837. $widthOrig = imagesx($this->resource);
  838. $heightOrig = imagesy($this->resource);
  839. $ratioOrig = $widthOrig / $heightOrig;
  840. if ($ratioOrig > 1) {
  841. $newHeight = round($maxSize / $ratioOrig);
  842. $newWidth = $maxSize;
  843. } else {
  844. $newWidth = round($maxSize * $ratioOrig);
  845. $newHeight = $maxSize;
  846. }
  847. return $this->preciseResizeNew((int)round($newWidth), (int)round($newHeight));
  848. }
  849. /**
  850. * @param int $width
  851. * @param int $height
  852. * @return bool
  853. */
  854. public function preciseResize(int $width, int $height): bool {
  855. $result = $this->preciseResizeNew($width, $height);
  856. imagedestroy($this->resource);
  857. $this->resource = $result;
  858. return is_resource($result);
  859. }
  860. /**
  861. * @param int $width
  862. * @param int $height
  863. * @return resource | bool
  864. */
  865. public function preciseResizeNew(int $width, int $height) {
  866. if (!$this->valid()) {
  867. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  868. return false;
  869. }
  870. $widthOrig = imagesx($this->resource);
  871. $heightOrig = imagesy($this->resource);
  872. $process = imagecreatetruecolor($width, $height);
  873. if ($process === false) {
  874. $this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
  875. return false;
  876. }
  877. // preserve transparency
  878. if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
  879. imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
  880. imagealphablending($process, false);
  881. imagesavealpha($process, true);
  882. }
  883. $res = imagecopyresampled($process, $this->resource, 0, 0, 0, 0, $width, $height, $widthOrig, $heightOrig);
  884. if ($res === false) {
  885. $this->logger->error(__METHOD__ . '(): Error re-sampling process image', ['app' => 'core']);
  886. imagedestroy($process);
  887. return false;
  888. }
  889. return $process;
  890. }
  891. /**
  892. * Crops the image to the middle square. If the image is already square it just returns.
  893. *
  894. * @param int $size maximum size for the result (optional)
  895. * @return bool for success or failure
  896. */
  897. public function centerCrop($size = 0) {
  898. if (!$this->valid()) {
  899. $this->logger->error('OC_Image->centerCrop, No image loaded', ['app' => 'core']);
  900. return false;
  901. }
  902. $widthOrig = imagesx($this->resource);
  903. $heightOrig = imagesy($this->resource);
  904. if ($widthOrig === $heightOrig and $size == 0) {
  905. return true;
  906. }
  907. $ratioOrig = $widthOrig / $heightOrig;
  908. $width = $height = min($widthOrig, $heightOrig);
  909. if ($ratioOrig > 1) {
  910. $x = ($widthOrig / 2) - ($width / 2);
  911. $y = 0;
  912. } else {
  913. $y = ($heightOrig / 2) - ($height / 2);
  914. $x = 0;
  915. }
  916. if ($size > 0) {
  917. $targetWidth = $size;
  918. $targetHeight = $size;
  919. } else {
  920. $targetWidth = $width;
  921. $targetHeight = $height;
  922. }
  923. $process = imagecreatetruecolor($targetWidth, $targetHeight);
  924. if ($process == false) {
  925. $this->logger->error('OC_Image->centerCrop, Error creating true color image', ['app' => 'core']);
  926. imagedestroy($process);
  927. return false;
  928. }
  929. // preserve transparency
  930. if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
  931. imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
  932. imagealphablending($process, false);
  933. imagesavealpha($process, true);
  934. }
  935. imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height);
  936. if ($process == false) {
  937. $this->logger->error('OC_Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']);
  938. imagedestroy($process);
  939. return false;
  940. }
  941. imagedestroy($this->resource);
  942. $this->resource = $process;
  943. return true;
  944. }
  945. /**
  946. * Crops the image from point $x$y with dimension $wx$h.
  947. *
  948. * @param int $x Horizontal position
  949. * @param int $y Vertical position
  950. * @param int $w Width
  951. * @param int $h Height
  952. * @return bool for success or failure
  953. */
  954. public function crop(int $x, int $y, int $w, int $h): bool {
  955. $result = $this->cropNew($x, $y, $w, $h);
  956. imagedestroy($this->resource);
  957. $this->resource = $result;
  958. return is_resource($result);
  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 resource | bool
  968. */
  969. public function cropNew(int $x, int $y, int $w, int $h) {
  970. if (!$this->valid()) {
  971. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  972. return false;
  973. }
  974. $process = imagecreatetruecolor($w, $h);
  975. if ($process == false) {
  976. $this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
  977. imagedestroy($process);
  978. return false;
  979. }
  980. // preserve transparency
  981. if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
  982. imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
  983. imagealphablending($process, false);
  984. imagesavealpha($process, true);
  985. }
  986. imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $w, $h, $w, $h);
  987. if ($process == false) {
  988. $this->logger->error(__METHOD__ . '(): Error re-sampling process image ' . $w . 'x' . $h, ['app' => 'core']);
  989. imagedestroy($process);
  990. return false;
  991. }
  992. return $process;
  993. }
  994. /**
  995. * Resizes the image to fit within a boundary while preserving ratio.
  996. *
  997. * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up
  998. *
  999. * @param integer $maxWidth
  1000. * @param integer $maxHeight
  1001. * @return bool
  1002. */
  1003. public function fitIn($maxWidth, $maxHeight) {
  1004. if (!$this->valid()) {
  1005. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  1006. return false;
  1007. }
  1008. $widthOrig = imagesx($this->resource);
  1009. $heightOrig = imagesy($this->resource);
  1010. $ratio = $widthOrig / $heightOrig;
  1011. $newWidth = min($maxWidth, $ratio * $maxHeight);
  1012. $newHeight = min($maxHeight, $maxWidth / $ratio);
  1013. $this->preciseResize((int)round($newWidth), (int)round($newHeight));
  1014. return true;
  1015. }
  1016. /**
  1017. * Shrinks larger images to fit within specified boundaries while preserving ratio.
  1018. *
  1019. * @param integer $maxWidth
  1020. * @param integer $maxHeight
  1021. * @return bool
  1022. */
  1023. public function scaleDownToFit($maxWidth, $maxHeight) {
  1024. if (!$this->valid()) {
  1025. $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
  1026. return false;
  1027. }
  1028. $widthOrig = imagesx($this->resource);
  1029. $heightOrig = imagesy($this->resource);
  1030. if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) {
  1031. return $this->fitIn($maxWidth, $maxHeight);
  1032. }
  1033. return false;
  1034. }
  1035. public function copy(): IImage {
  1036. $image = new OC_Image(null, $this->logger, $this->config);
  1037. $image->resource = imagecreatetruecolor($this->width(), $this->height());
  1038. imagecopy(
  1039. $image->resource(),
  1040. $this->resource(),
  1041. 0,
  1042. 0,
  1043. 0,
  1044. 0,
  1045. $this->width(),
  1046. $this->height()
  1047. );
  1048. return $image;
  1049. }
  1050. public function cropCopy(int $x, int $y, int $w, int $h): IImage {
  1051. $image = new OC_Image(null, $this->logger, $this->config);
  1052. $image->imageType = $this->imageType;
  1053. $image->mimeType = $this->mimeType;
  1054. $image->bitDepth = $this->bitDepth;
  1055. $image->resource = $this->cropNew($x, $y, $w, $h);
  1056. return $image;
  1057. }
  1058. public function preciseResizeCopy(int $width, int $height): IImage {
  1059. $image = new OC_Image(null, $this->logger, $this->config);
  1060. $image->imageType = $this->imageType;
  1061. $image->mimeType = $this->mimeType;
  1062. $image->bitDepth = $this->bitDepth;
  1063. $image->resource = $this->preciseResizeNew($width, $height);
  1064. return $image;
  1065. }
  1066. public function resizeCopy(int $maxSize): IImage {
  1067. $image = new OC_Image(null, $this->logger, $this->config);
  1068. $image->imageType = $this->imageType;
  1069. $image->mimeType = $this->mimeType;
  1070. $image->bitDepth = $this->bitDepth;
  1071. $image->resource = $this->resizeNew($maxSize);
  1072. return $image;
  1073. }
  1074. /**
  1075. * Resizes the image preserving ratio, returning a new copy
  1076. *
  1077. * @param integer $maxSize The maximum size of either the width or height.
  1078. * @return bool
  1079. */
  1080. public function copyResize($maxSize): IImage {
  1081. }
  1082. /**
  1083. * Destroys the current image and resets the object
  1084. */
  1085. public function destroy() {
  1086. if ($this->valid()) {
  1087. imagedestroy($this->resource);
  1088. }
  1089. $this->resource = null;
  1090. }
  1091. public function __destruct() {
  1092. $this->destroy();
  1093. }
  1094. }
  1095. if (!function_exists('imagebmp')) {
  1096. /**
  1097. * Output a BMP image to either the browser or a file
  1098. *
  1099. * @link http://www.ugia.cn/wp-data/imagebmp.php
  1100. * @author legend <legendsky@hotmail.com>
  1101. * @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm
  1102. * @author mgutt <marc@gutt.it>
  1103. * @version 1.00
  1104. * @param resource $im
  1105. * @param string $fileName [optional] <p>The path to save the file to.</p>
  1106. * @param int $bit [optional] <p>Bit depth, (default is 24).</p>
  1107. * @param int $compression [optional]
  1108. * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
  1109. */
  1110. function imagebmp($im, $fileName = '', $bit = 24, $compression = 0) {
  1111. if (!in_array($bit, [1, 4, 8, 16, 24, 32])) {
  1112. $bit = 24;
  1113. } elseif ($bit == 32) {
  1114. $bit = 24;
  1115. }
  1116. $bits = (int)pow(2, $bit);
  1117. imagetruecolortopalette($im, true, $bits);
  1118. $width = imagesx($im);
  1119. $height = imagesy($im);
  1120. $colorsNum = imagecolorstotal($im);
  1121. $rgbQuad = '';
  1122. if ($bit <= 8) {
  1123. for ($i = 0; $i < $colorsNum; $i++) {
  1124. $colors = imagecolorsforindex($im, $i);
  1125. $rgbQuad .= chr($colors['blue']) . chr($colors['green']) . chr($colors['red']) . "\0";
  1126. }
  1127. $bmpData = '';
  1128. if ($compression == 0 || $bit < 8) {
  1129. $compression = 0;
  1130. $extra = '';
  1131. $padding = 4 - ceil($width / (8 / $bit)) % 4;
  1132. if ($padding % 4 != 0) {
  1133. $extra = str_repeat("\0", $padding);
  1134. }
  1135. for ($j = $height - 1; $j >= 0; $j--) {
  1136. $i = 0;
  1137. while ($i < $width) {
  1138. $bin = 0;
  1139. $limit = $width - $i < 8 / $bit ? (8 / $bit - $width + $i) * $bit : 0;
  1140. for ($k = 8 - $bit; $k >= $limit; $k -= $bit) {
  1141. $index = imagecolorat($im, $i, $j);
  1142. $bin |= $index << $k;
  1143. $i++;
  1144. }
  1145. $bmpData .= chr($bin);
  1146. }
  1147. $bmpData .= $extra;
  1148. }
  1149. } // RLE8
  1150. elseif ($compression == 1 && $bit == 8) {
  1151. for ($j = $height - 1; $j >= 0; $j--) {
  1152. $lastIndex = null;
  1153. $sameNum = 0;
  1154. for ($i = 0; $i <= $width; $i++) {
  1155. $index = imagecolorat($im, $i, $j);
  1156. if ($index !== $lastIndex || $sameNum > 255) {
  1157. if ($sameNum != 0) {
  1158. $bmpData .= chr($sameNum) . chr($lastIndex);
  1159. }
  1160. $lastIndex = $index;
  1161. $sameNum = 1;
  1162. } else {
  1163. $sameNum++;
  1164. }
  1165. }
  1166. $bmpData .= "\0\0";
  1167. }
  1168. $bmpData .= "\0\1";
  1169. }
  1170. $sizeQuad = strlen($rgbQuad);
  1171. $sizeData = strlen($bmpData);
  1172. } else {
  1173. $extra = '';
  1174. $padding = 4 - ($width * ($bit / 8)) % 4;
  1175. if ($padding % 4 != 0) {
  1176. $extra = str_repeat("\0", $padding);
  1177. }
  1178. $bmpData = '';
  1179. for ($j = $height - 1; $j >= 0; $j--) {
  1180. for ($i = 0; $i < $width; $i++) {
  1181. $index = imagecolorat($im, $i, $j);
  1182. $colors = imagecolorsforindex($im, $index);
  1183. if ($bit == 16) {
  1184. $bin = 0 << $bit;
  1185. $bin |= ($colors['red'] >> 3) << 10;
  1186. $bin |= ($colors['green'] >> 3) << 5;
  1187. $bin |= $colors['blue'] >> 3;
  1188. $bmpData .= pack("v", $bin);
  1189. } else {
  1190. $bmpData .= pack("c*", $colors['blue'], $colors['green'], $colors['red']);
  1191. }
  1192. }
  1193. $bmpData .= $extra;
  1194. }
  1195. $sizeQuad = 0;
  1196. $sizeData = strlen($bmpData);
  1197. $colorsNum = 0;
  1198. }
  1199. $fileHeader = 'BM' . pack('V3', 54 + $sizeQuad + $sizeData, 0, 54 + $sizeQuad);
  1200. $infoHeader = pack('V3v2V*', 0x28, $width, $height, 1, $bit, $compression, $sizeData, 0, 0, $colorsNum, 0);
  1201. if ($fileName != '') {
  1202. $fp = fopen($fileName, 'wb');
  1203. fwrite($fp, $fileHeader . $infoHeader . $rgbQuad . $bmpData);
  1204. fclose($fp);
  1205. return true;
  1206. }
  1207. echo $fileHeader . $infoHeader . $rgbQuad . $bmpData;
  1208. return true;
  1209. }
  1210. }
  1211. if (!function_exists('exif_imagetype')) {
  1212. /**
  1213. * Workaround if exif_imagetype does not exist
  1214. *
  1215. * @link https://www.php.net/manual/en/function.exif-imagetype.php#80383
  1216. * @param string $fileName
  1217. * @return string|boolean
  1218. */
  1219. function exif_imagetype($fileName) {
  1220. if (($info = getimagesize($fileName)) !== false) {
  1221. return $info[2];
  1222. }
  1223. return false;
  1224. }
  1225. }