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.

1288 lines
36 KiB

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