OC_Image.php 40 KB

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