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