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