Passed
Push — master ( b23934...c47406 )
by Joas
13:17 queued 12s
created

OC_Image::checkImageMemory()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 2
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bartek Przybylski <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Byron Marohn <[email protected]>
9
 * @author Côme Chilliet <[email protected]>
10
 * @author Christopher Schäpers <[email protected]>
11
 * @author Christoph Wurst <[email protected]>
12
 * @author Georg Ehrke <[email protected]>
13
 * @author J0WI <[email protected]>
14
 * @author j-ed <[email protected]>
15
 * @author Joas Schilling <[email protected]>
16
 * @author Johannes Willnecker <[email protected]>
17
 * @author Jörn Friedrich Dreyer <[email protected]>
18
 * @author Julius Härtl <[email protected]>
19
 * @author Lukas Reschke <[email protected]>
20
 * @author Morris Jobke <[email protected]>
21
 * @author Olivier Paroz <[email protected]>
22
 * @author Robin Appelman <[email protected]>
23
 * @author Roeland Jago Douma <[email protected]>
24
 * @author Samuel CHEMLA <[email protected]>
25
 * @author Thomas Müller <[email protected]>
26
 * @author Thomas Tanghus <[email protected]>
27
 *
28
 * @license AGPL-3.0
29
 *
30
 * This code is free software: you can redistribute it and/or modify
31
 * it under the terms of the GNU Affero General Public License, version 3,
32
 * as published by the Free Software Foundation.
33
 *
34
 * This program is distributed in the hope that it will be useful,
35
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
36
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
37
 * GNU Affero General Public License for more details.
38
 *
39
 * You should have received a copy of the GNU Affero General Public License, version 3,
40
 * along with this program. If not, see <http://www.gnu.org/licenses/>
41
 *
42
 */
43
use OCP\IImage;
44
45
/**
46
 * Class for basic image manipulation
47
 */
48
class OC_Image implements \OCP\IImage {
49
50
	// Default memory limit for images to load (128 MBytes).
51
	protected const DEFAULT_MEMORY_LIMIT = 128;
52
53
	/** @var false|resource|\GdImage */
54
	protected $resource = false; // tmp resource.
55
	/** @var int */
56
	protected $imageType = IMAGETYPE_PNG; // Default to png if file type isn't evident.
57
	/** @var string */
58
	protected $mimeType = 'image/png'; // Default to png
59
	/** @var int */
60
	protected $bitDepth = 24;
61
	/** @var null|string */
62
	protected $filePath = null;
63
	/** @var finfo */
64
	private $fileInfo;
65
	/** @var \OCP\ILogger */
66
	private $logger;
67
	/** @var \OCP\IConfig */
68
	private $config;
69
	/** @var array */
70
	private $exif;
71
72
	/**
73
	 * Constructor.
74
	 *
75
	 * @param resource|string|\GdImage $imageRef The path to a local file, a base64 encoded string or a resource created by
76
	 * an imagecreate* function.
77
	 * @param \OCP\ILogger $logger
78
	 * @param \OCP\IConfig $config
79
	 * @throws \InvalidArgumentException in case the $imageRef parameter is not null
80
	 */
81
	public function __construct($imageRef = null, \OCP\ILogger $logger = null, \OCP\IConfig $config = null) {
82
		$this->logger = $logger;
83
		if ($logger === null) {
84
			$this->logger = \OC::$server->getLogger();
85
		}
86
		$this->config = $config;
87
		if ($config === null) {
88
			$this->config = \OC::$server->getConfig();
89
		}
90
91
		if (\OC_Util::fileInfoLoaded()) {
92
			$this->fileInfo = new finfo(FILEINFO_MIME_TYPE);
93
		}
94
95
		if ($imageRef !== null) {
96
			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.');
97
		}
98
	}
99
100
	/**
101
	 * Determine whether the object contains an image resource.
102
	 *
103
	 * @return bool
104
	 */
105
	public function valid() {
106
		if (is_resource($this->resource)) {
107
			return true;
108
		}
109
		if (is_object($this->resource) && get_class($this->resource) === \GdImage::class) {
110
			return true;
111
		}
112
113
		return false;
114
	}
115
116
	/**
117
	 * Returns the MIME type of the image or an empty string if no image is loaded.
118
	 *
119
	 * @return string
120
	 */
121
	public function mimeType() {
122
		return $this->valid() ? $this->mimeType : '';
123
	}
124
125
	/**
126
	 * Returns the width of the image or -1 if no image is loaded.
127
	 *
128
	 * @return int
129
	 */
130
	public function width() {
131
		if ($this->valid()) {
132
			$width = imagesx($this->resource);
133
			if ($width !== false) {
134
				return $width;
135
			}
136
		}
137
		return -1;
138
	}
139
140
	/**
141
	 * Returns the height of the image or -1 if no image is loaded.
142
	 *
143
	 * @return int
144
	 */
145
	public function height() {
146
		if ($this->valid()) {
147
			$height = imagesy($this->resource);
148
			if ($height !== false) {
149
				return $height;
150
			}
151
		}
152
		return -1;
153
	}
154
155
	/**
156
	 * Returns the width when the image orientation is top-left.
157
	 *
158
	 * @return int
159
	 */
160
	public function widthTopLeft() {
161
		$o = $this->getOrientation();
162
		$this->logger->debug('OC_Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']);
163
		switch ($o) {
164
			case -1:
165
			case 1:
166
			case 2: // Not tested
167
			case 3:
168
			case 4: // Not tested
169
				return $this->width();
170
			case 5: // Not tested
171
			case 6:
172
			case 7: // Not tested
173
			case 8:
174
				return $this->height();
175
		}
176
		return $this->width();
177
	}
178
179
	/**
180
	 * Returns the height when the image orientation is top-left.
181
	 *
182
	 * @return int
183
	 */
184
	public function heightTopLeft() {
185
		$o = $this->getOrientation();
186
		$this->logger->debug('OC_Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']);
187
		switch ($o) {
188
			case -1:
189
			case 1:
190
			case 2: // Not tested
191
			case 3:
192
			case 4: // Not tested
193
				return $this->height();
194
			case 5: // Not tested
195
			case 6:
196
			case 7: // Not tested
197
			case 8:
198
				return $this->width();
199
		}
200
		return $this->height();
201
	}
202
203
	/**
204
	 * Outputs the image.
205
	 *
206
	 * @param string $mimeType
207
	 * @return bool
208
	 */
209
	public function show($mimeType = null) {
210
		if ($mimeType === null) {
211
			$mimeType = $this->mimeType();
212
		}
213
		header('Content-Type: ' . $mimeType);
214
		return $this->_output(null, $mimeType);
215
	}
216
217
	/**
218
	 * Saves the image.
219
	 *
220
	 * @param string $filePath
221
	 * @param string $mimeType
222
	 * @return bool
223
	 */
224
225
	public function save($filePath = null, $mimeType = null) {
226
		if ($mimeType === null) {
227
			$mimeType = $this->mimeType();
228
		}
229
		if ($filePath === null) {
230
			if ($this->filePath === null) {
231
				$this->logger->error(__METHOD__ . '(): called with no path.', ['app' => 'core']);
232
				return false;
233
			} else {
234
				$filePath = $this->filePath;
235
			}
236
		}
237
		return $this->_output($filePath, $mimeType);
238
	}
239
240
	/**
241
	 * Outputs/saves the image.
242
	 *
243
	 * @param string $filePath
244
	 * @param string $mimeType
245
	 * @return bool
246
	 * @throws Exception
247
	 */
248
	private function _output($filePath = null, $mimeType = null) {
249
		if ($filePath) {
250
			if (!file_exists(dirname($filePath))) {
251
				mkdir(dirname($filePath), 0777, true);
252
			}
253
			$isWritable = is_writable(dirname($filePath));
254
			if (!$isWritable) {
255
				$this->logger->error(__METHOD__ . '(): Directory \'' . dirname($filePath) . '\' is not writable.', ['app' => 'core']);
256
				return false;
257
			} elseif ($isWritable && file_exists($filePath) && !is_writable($filePath)) {
258
				$this->logger->error(__METHOD__ . '(): File \'' . $filePath . '\' is not writable.', ['app' => 'core']);
259
				return false;
260
			}
261
		}
262
		if (!$this->valid()) {
263
			return false;
264
		}
265
266
		$imageType = $this->imageType;
267
		if ($mimeType !== null) {
268
			switch ($mimeType) {
269
				case 'image/gif':
270
					$imageType = IMAGETYPE_GIF;
271
					break;
272
				case 'image/jpeg':
273
					$imageType = IMAGETYPE_JPEG;
274
					break;
275
				case 'image/png':
276
					$imageType = IMAGETYPE_PNG;
277
					break;
278
				case 'image/x-xbitmap':
279
					$imageType = IMAGETYPE_XBM;
280
					break;
281
				case 'image/bmp':
282
				case 'image/x-ms-bmp':
283
					$imageType = IMAGETYPE_BMP;
284
					break;
285
				default:
286
					throw new Exception('\OC_Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format');
287
			}
288
		}
289
290
		switch ($imageType) {
291
			case IMAGETYPE_GIF:
292
				$retVal = imagegif($this->resource, $filePath);
293
				break;
294
			case IMAGETYPE_JPEG:
295
				$retVal = imagejpeg($this->resource, $filePath, $this->getJpegQuality());
296
				break;
297
			case IMAGETYPE_PNG:
298
				$retVal = imagepng($this->resource, $filePath);
299
				break;
300
			case IMAGETYPE_XBM:
301
				if (function_exists('imagexbm')) {
302
					$retVal = imagexbm($this->resource, $filePath);
303
				} else {
304
					throw new Exception('\OC_Image::_output(): imagexbm() is not supported.');
305
				}
306
307
				break;
308
			case IMAGETYPE_WBMP:
309
				$retVal = imagewbmp($this->resource, $filePath);
310
				break;
311
			case IMAGETYPE_BMP:
312
				$retVal = imagebmp($this->resource, $filePath, $this->bitDepth);
0 ignored issues
show
Bug introduced by
$this->bitDepth of type integer is incompatible with the type boolean expected by parameter $compressed of imagebmp(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

312
				$retVal = imagebmp($this->resource, $filePath, /** @scrutinizer ignore-type */ $this->bitDepth);
Loading history...
313
				break;
314
			default:
315
				$retVal = imagepng($this->resource, $filePath);
316
		}
317
		return $retVal;
318
	}
319
320
	/**
321
	 * Prints the image when called as $image().
322
	 */
323
	public function __invoke() {
324
		return $this->show();
325
	}
326
327
	/**
328
	 * @param resource|\GdImage $resource
329
	 * @throws \InvalidArgumentException in case the supplied resource does not have the type "gd"
330
	 */
331
	public function setResource($resource) {
332
		// For PHP<8
333
		if (is_resource($resource) && get_resource_type($resource) === 'gd') {
334
			$this->resource = $resource;
335
			return;
336
		}
337
		// PHP 8 has real objects for GD stuff
338
		if (is_object($resource) && get_class($resource) === \GdImage::class) {
339
			$this->resource = $resource;
340
			return;
341
		}
342
		throw new \InvalidArgumentException('Supplied resource is not of type "gd".');
343
	}
344
345
	/**
346
	 * @return false|resource|\GdImage Returns the image resource if any
347
	 */
348
	public function resource() {
349
		return $this->resource;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->resource also could return the type boolean which is incompatible with the documented return type GdImage|false|resource.
Loading history...
350
	}
351
352
	/**
353
	 * @return string Returns the mimetype of the data. Returns the empty string
354
	 * if the data is not valid.
355
	 */
356
	public function dataMimeType() {
357
		if (!$this->valid()) {
358
			return '';
359
		}
360
361
		switch ($this->mimeType) {
362
			case 'image/png':
363
			case 'image/jpeg':
364
			case 'image/gif':
365
				return $this->mimeType;
366
			default:
367
				return 'image/png';
368
		}
369
	}
370
371
	/**
372
	 * @return null|string Returns the raw image data.
373
	 */
374
	public function data() {
375
		if (!$this->valid()) {
376
			return null;
377
		}
378
		ob_start();
379
		switch ($this->mimeType) {
380
			case "image/png":
381
				$res = imagepng($this->resource);
382
				break;
383
			case "image/jpeg":
384
				$quality = $this->getJpegQuality();
385
				if ($quality !== null) {
386
					$res = imagejpeg($this->resource, null, $quality);
387
				} else {
388
					$res = imagejpeg($this->resource);
389
				}
390
				break;
391
			case "image/gif":
392
				$res = imagegif($this->resource);
393
				break;
394
			default:
395
				$res = imagepng($this->resource);
396
				$this->logger->info('OC_Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']);
397
				break;
398
		}
399
		if (!$res) {
400
			$this->logger->error('OC_Image->data. Error getting image data.', ['app' => 'core']);
401
		}
402
		return ob_get_clean();
403
	}
404
405
	/**
406
	 * @return string - base64 encoded, which is suitable for embedding in a VCard.
407
	 */
408
	public function __toString() {
409
		return base64_encode($this->data());
410
	}
411
412
	/**
413
	 * @return int|null
414
	 */
415
	protected function getJpegQuality() {
416
		$quality = $this->config->getAppValue('preview', 'jpeg_quality', 90);
417
		if ($quality !== null) {
0 ignored issues
show
introduced by
The condition $quality !== null is always true.
Loading history...
418
			$quality = min(100, max(10, (int) $quality));
419
		}
420
		return $quality;
421
	}
422
423
	/**
424
	 * (I'm open for suggestions on better method name ;)
425
	 * Get the orientation based on EXIF data.
426
	 *
427
	 * @return int The orientation or -1 if no EXIF data is available.
428
	 */
429
	public function getOrientation() {
430
		if ($this->exif !== null) {
431
			return $this->exif['Orientation'];
432
		}
433
434
		if ($this->imageType !== IMAGETYPE_JPEG) {
435
			$this->logger->debug('OC_Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']);
436
			return -1;
437
		}
438
		if (!is_callable('exif_read_data')) {
439
			$this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
440
			return -1;
441
		}
442
		if (!$this->valid()) {
443
			$this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']);
444
			return -1;
445
		}
446
		if (is_null($this->filePath) || !is_readable($this->filePath)) {
447
			$this->logger->debug('OC_Image->fixOrientation() No readable file path set.', ['app' => 'core']);
448
			return -1;
449
		}
450
		$exif = @exif_read_data($this->filePath, 'IFD0');
451
		if (!$exif) {
452
			return -1;
453
		}
454
		if (!isset($exif['Orientation'])) {
455
			return -1;
456
		}
457
		$this->exif = $exif;
458
		return $exif['Orientation'];
459
	}
460
461
	public function readExif($data) {
462
		if (!is_callable('exif_read_data')) {
463
			$this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
464
			return;
465
		}
466
		if (!$this->valid()) {
467
			$this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']);
468
			return;
469
		}
470
471
		$exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($data));
472
		if (!$exif) {
473
			return;
474
		}
475
		if (!isset($exif['Orientation'])) {
476
			return;
477
		}
478
		$this->exif = $exif;
479
	}
480
481
	/**
482
	 * (I'm open for suggestions on better method name ;)
483
	 * Fixes orientation based on EXIF data.
484
	 *
485
	 * @return bool
486
	 */
487
	public function fixOrientation() {
488
		if (!$this->valid()) {
489
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
490
			return false;
491
		}
492
		$o = $this->getOrientation();
493
		$this->logger->debug('OC_Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']);
494
		$rotate = 0;
495
		$flip = false;
496
		switch ($o) {
497
			case -1:
498
				return false; //Nothing to fix
499
			case 1:
500
				$rotate = 0;
501
				break;
502
			case 2:
503
				$rotate = 0;
504
				$flip = true;
505
				break;
506
			case 3:
507
				$rotate = 180;
508
				break;
509
			case 4:
510
				$rotate = 180;
511
				$flip = true;
512
				break;
513
			case 5:
514
				$rotate = 90;
515
				$flip = true;
516
				break;
517
			case 6:
518
				$rotate = 270;
519
				break;
520
			case 7:
521
				$rotate = 270;
522
				$flip = true;
523
				break;
524
			case 8:
525
				$rotate = 90;
526
				break;
527
		}
528
		if ($flip && function_exists('imageflip')) {
529
			imageflip($this->resource, IMG_FLIP_HORIZONTAL);
530
		}
531
		if ($rotate) {
532
			$res = imagerotate($this->resource, $rotate, 0);
533
			if ($res) {
534
				if (imagealphablending($res, true)) {
535
					if (imagesavealpha($res, true)) {
536
						imagedestroy($this->resource);
537
						$this->resource = $res;
538
						return true;
539
					} else {
540
						$this->logger->debug('OC_Image->fixOrientation() Error during alpha-saving', ['app' => 'core']);
541
						return false;
542
					}
543
				} else {
544
					$this->logger->debug('OC_Image->fixOrientation() Error during alpha-blending', ['app' => 'core']);
545
					return false;
546
				}
547
			} else {
548
				$this->logger->debug('OC_Image->fixOrientation() Error during orientation fixing', ['app' => 'core']);
549
				return false;
550
			}
551
		}
552
		return false;
553
	}
554
555
	/**
556
	 * Loads an image from an open file handle.
557
	 * It is the responsibility of the caller to position the pointer at the correct place and to close the handle again.
558
	 *
559
	 * @param resource $handle
560
	 * @return resource|\GdImage|false An image resource or false on error
561
	 */
562
	public function loadFromFileHandle($handle) {
563
		$contents = stream_get_contents($handle);
564
		if ($this->loadFromData($contents)) {
565
			return $this->resource;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->resource also could return the type boolean which is incompatible with the documented return type GdImage|false|resource.
Loading history...
566
		}
567
		return false;
568
	}
569
570
	/**
571
	 * Check if allocating an image with the given size is allowed.
572
	 *
573
	 * @param int $width The image width.
574
	 * @param int $height The image height.
575
	 * @return bool true if allocating is allowed, false otherwise
576
	 */
577
	private function checkImageMemory($width, $height) {
578
		$memory_limit = $this->config->getSystemValueInt('preview_max_memory', self::DEFAULT_MEMORY_LIMIT);
579
		if ($memory_limit < 0) {
580
			// Not limited.
581
			return true;
582
		}
583
584
		// Assume 32 bits per pixel.
585
		if ($width * $height * 4 > $memory_limit * 1024 * 1024) {
586
			$this->logger->debug('Image size of ' . $width . 'x' . $height . ' would exceed allowed memory limit of ' . $memory_limit);
587
			return false;
588
		}
589
590
		return true;
591
	}
592
593
	/**
594
	 * Check if loading an image file from the given path is allowed.
595
	 *
596
	 * @param string $path The path to a local file.
597
	 * @return bool true if allocating is allowed, false otherwise
598
	 */
599
	private function checkImageSize($path) {
600
		$size = getimagesize($path);
601
		if (!$size) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $size of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
602
			return true;
603
		}
604
605
		$width = $size[0];
606
		$height = $size[1];
607
		if (!$this->checkImageMemory($width, $height)) {
608
			return false;
609
		}
610
611
		return true;
612
	}
613
614
	/**
615
	 * Check if loading an image from the given data is allowed.
616
	 *
617
	 * @param string $data A string of image data as read from a file.
618
	 * @return bool true if allocating is allowed, false otherwise
619
	 */
620
	private function checkImageDataSize($data) {
621
		$size = getimagesizefromstring($data);
622
		if (!$size) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $size of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
623
			return true;
624
		}
625
626
		$width = $size[0];
627
		$height = $size[1];
628
		if (!$this->checkImageMemory($width, $height)) {
629
			return false;
630
		}
631
632
		return true;
633
	}
634
635
	/**
636
	 * Loads an image from a local file.
637
	 *
638
	 * @param bool|string $imagePath The path to a local file.
639
	 * @return bool|resource|\GdImage An image resource or false on error
640
	 */
641
	public function loadFromFile($imagePath = false) {
642
		// exif_imagetype throws "read error!" if file is less than 12 byte
643
		if (is_bool($imagePath) || !@is_file($imagePath) || !file_exists($imagePath) || filesize($imagePath) < 12 || !is_readable($imagePath)) {
644
			return false;
645
		}
646
		$iType = exif_imagetype($imagePath);
647
		switch ($iType) {
648
			case IMAGETYPE_GIF:
649
				if (imagetypes() & IMG_GIF) {
650
					if (!$this->checkImageSize($imagePath)) {
651
						return false;
652
					}
653
					$this->resource = imagecreatefromgif($imagePath);
654
					if ($this->resource) {
655
						// Preserve transparency
656
						imagealphablending($this->resource, true);
657
						imagesavealpha($this->resource, true);
658
					} else {
659
						$this->logger->debug('OC_Image->loadFromFile, GIF image not valid: ' . $imagePath, ['app' => 'core']);
660
					}
661
				} else {
662
					$this->logger->debug('OC_Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']);
663
				}
664
				break;
665
			case IMAGETYPE_JPEG:
666
				if (imagetypes() & IMG_JPG) {
667
					if (!$this->checkImageSize($imagePath)) {
668
						return false;
669
					}
670
					if (getimagesize($imagePath) !== false) {
671
						$this->resource = @imagecreatefromjpeg($imagePath);
672
					} else {
673
						$this->logger->debug('OC_Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']);
674
					}
675
				} else {
676
					$this->logger->debug('OC_Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']);
677
				}
678
				break;
679
			case IMAGETYPE_PNG:
680
				if (imagetypes() & IMG_PNG) {
681
					if (!$this->checkImageSize($imagePath)) {
682
						return false;
683
					}
684
					$this->resource = @imagecreatefrompng($imagePath);
685
					if ($this->resource) {
686
						// Preserve transparency
687
						imagealphablending($this->resource, true);
688
						imagesavealpha($this->resource, true);
689
					} else {
690
						$this->logger->debug('OC_Image->loadFromFile, PNG image not valid: ' . $imagePath, ['app' => 'core']);
691
					}
692
				} else {
693
					$this->logger->debug('OC_Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']);
694
				}
695
				break;
696
			case IMAGETYPE_XBM:
697
				if (imagetypes() & IMG_XPM) {
698
					if (!$this->checkImageSize($imagePath)) {
699
						return false;
700
					}
701
					$this->resource = @imagecreatefromxbm($imagePath);
702
				} else {
703
					$this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']);
704
				}
705
				break;
706
			case IMAGETYPE_WBMP:
707
				if (imagetypes() & IMG_WBMP) {
708
					if (!$this->checkImageSize($imagePath)) {
709
						return false;
710
					}
711
					$this->resource = @imagecreatefromwbmp($imagePath);
712
				} else {
713
					$this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']);
714
				}
715
				break;
716
			case IMAGETYPE_BMP:
717
				$this->resource = $this->imagecreatefrombmp($imagePath);
718
				break;
719
			case IMAGETYPE_WEBP:
720
				if (imagetypes() & IMG_WEBP) {
721
					if (!$this->checkImageSize($imagePath)) {
722
						return false;
723
					}
724
					$this->resource = @imagecreatefromwebp($imagePath);
725
				} else {
726
					$this->logger->debug('OC_Image->loadFromFile, webp images not supported: ' . $imagePath, ['app' => 'core']);
727
				}
728
				break;
729
			/*
730
			case IMAGETYPE_TIFF_II: // (intel byte order)
731
				break;
732
			case IMAGETYPE_TIFF_MM: // (motorola byte order)
733
				break;
734
			case IMAGETYPE_JPC:
735
				break;
736
			case IMAGETYPE_JP2:
737
				break;
738
			case IMAGETYPE_JPX:
739
				break;
740
			case IMAGETYPE_JB2:
741
				break;
742
			case IMAGETYPE_SWC:
743
				break;
744
			case IMAGETYPE_IFF:
745
				break;
746
			case IMAGETYPE_ICO:
747
				break;
748
			case IMAGETYPE_SWF:
749
				break;
750
			case IMAGETYPE_PSD:
751
				break;
752
			*/
753
			default:
754
755
				// this is mostly file created from encrypted file
756
				$data = file_get_contents($imagePath);
757
				if (!$this->checkImageDataSize($data)) {
758
					return false;
759
				}
760
				$this->resource = imagecreatefromstring($data);
761
				$iType = IMAGETYPE_PNG;
762
				$this->logger->debug('OC_Image->loadFromFile, Default', ['app' => 'core']);
763
				break;
764
		}
765
		if ($this->valid()) {
766
			$this->imageType = $iType;
767
			$this->mimeType = image_type_to_mime_type($iType);
768
			$this->filePath = $imagePath;
769
		}
770
		return $this->resource;
771
	}
772
773
	/**
774
	 * Loads an image from a string of data.
775
	 *
776
	 * @param string $str A string of image data as read from a file.
777
	 * @return bool|resource|\GdImage An image resource or false on error
778
	 */
779
	public function loadFromData($str) {
780
		if (!is_string($str)) {
0 ignored issues
show
introduced by
The condition is_string($str) is always true.
Loading history...
781
			return false;
782
		}
783
		if (!$this->checkImageDataSize($str)) {
784
			return false;
785
		}
786
		$this->resource = @imagecreatefromstring($str);
787
		if ($this->fileInfo) {
788
			$this->mimeType = $this->fileInfo->buffer($str);
789
		}
790
		if ($this->valid()) {
791
			imagealphablending($this->resource, false);
792
			imagesavealpha($this->resource, true);
793
		}
794
795
		if (!$this->resource) {
796
			$this->logger->debug('OC_Image->loadFromFile, could not load', ['app' => 'core']);
797
			return false;
798
		}
799
		return $this->resource;
800
	}
801
802
	/**
803
	 * Loads an image from a base64 encoded string.
804
	 *
805
	 * @param string $str A string base64 encoded string of image data.
806
	 * @return bool|resource|\GdImage An image resource or false on error
807
	 */
808
	public function loadFromBase64($str) {
809
		if (!is_string($str)) {
0 ignored issues
show
introduced by
The condition is_string($str) is always true.
Loading history...
810
			return false;
811
		}
812
		$data = base64_decode($str);
813
		if ($data) { // try to load from string data
814
			if (!$this->checkImageDataSize($data)) {
815
				return false;
816
			}
817
			$this->resource = @imagecreatefromstring($data);
818
			if ($this->fileInfo) {
819
				$this->mimeType = $this->fileInfo->buffer($data);
820
			}
821
			if (!$this->resource) {
822
				$this->logger->debug('OC_Image->loadFromBase64, could not load', ['app' => 'core']);
823
				return false;
824
			}
825
			return $this->resource;
826
		} else {
827
			return false;
828
		}
829
	}
830
831
	/**
832
	 * Create a new image from file or URL
833
	 *
834
	 * @link http://www.programmierer-forum.de/function-imagecreatefrombmp-laeuft-mit-allen-bitraten-t143137.htm
835
	 * @version 1.00
836
	 * @param string $fileName <p>
837
	 * Path to the BMP image.
838
	 * </p>
839
	 * @return bool|resource|\GdImage an image resource identifier on success, <b>FALSE</b> on errors.
840
	 */
841
	private function imagecreatefrombmp($fileName) {
842
		if (!($fh = fopen($fileName, 'rb'))) {
843
			$this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName, ['app' => 'core']);
844
			return false;
845
		}
846
		// read file header
847
		$meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14));
848
		// check for bitmap
849
		if ($meta['type'] != 19778) {
850
			fclose($fh);
851
			$this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
852
			return false;
853
		}
854
		// read image header
855
		$meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40));
856
		// read additional 16bit header
857
		if ($meta['bits'] == 16) {
858
			$meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12));
859
		}
860
		// set bytes and padding
861
		$meta['bytes'] = $meta['bits'] / 8;
862
		$this->bitDepth = $meta['bits']; //remember the bit depth for the imagebmp call
863
		$meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4)));
864
		if ($meta['decal'] == 4) {
865
			$meta['decal'] = 0;
866
		}
867
		// obtain imagesize
868
		if ($meta['imagesize'] < 1) {
869
			$meta['imagesize'] = $meta['filesize'] - $meta['offset'];
870
			// in rare cases filesize is equal to offset so we need to read physical size
871
			if ($meta['imagesize'] < 1) {
872
				$meta['imagesize'] = @filesize($fileName) - $meta['offset'];
873
				if ($meta['imagesize'] < 1) {
874
					fclose($fh);
875
					$this->logger->warning('imagecreatefrombmp: Can not obtain file size of ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
876
					return false;
877
				}
878
			}
879
		}
880
		// calculate colors
881
		$meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors'];
882
		// read color palette
883
		$palette = [];
884
		if ($meta['bits'] < 16) {
885
			$palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4));
886
			// in rare cases the color value is signed
887
			if ($palette[1] < 0) {
888
				foreach ($palette as $i => $color) {
889
					$palette[$i] = $color + 16777216;
890
				}
891
			}
892
		}
893
		if (!$this->checkImageMemory($meta['width'], $meta['height'])) {
894
			fclose($fh);
895
			return false;
896
		}
897
		// create gd image
898
		$im = imagecreatetruecolor($meta['width'], $meta['height']);
899
		if ($im == false) {
900
			fclose($fh);
901
			$this->logger->warning(
902
				'imagecreatefrombmp: imagecreatetruecolor failed for file "' . $fileName . '" with dimensions ' . $meta['width'] . 'x' . $meta['height'],
903
				['app' => 'core']);
904
			return false;
905
		}
906
907
		$data = fread($fh, $meta['imagesize']);
908
		$p = 0;
909
		$vide = chr(0);
910
		$y = $meta['height'] - 1;
911
		$error = 'imagecreatefrombmp: ' . $fileName . ' has not enough data!';
912
		// loop through the image data beginning with the lower left corner
913
		while ($y >= 0) {
914
			$x = 0;
915
			while ($x < $meta['width']) {
916
				switch ($meta['bits']) {
917
					case 32:
918
					case 24:
919
						if (!($part = substr($data, $p, 3))) {
920
							$this->logger->warning($error, ['app' => 'core']);
921
							return $im;
922
						}
923
						$color = @unpack('V', $part . $vide);
924
						break;
925
					case 16:
926
						if (!($part = substr($data, $p, 2))) {
927
							fclose($fh);
928
							$this->logger->warning($error, ['app' => 'core']);
929
							return $im;
930
						}
931
						$color = @unpack('v', $part);
932
						$color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3);
933
						break;
934
					case 8:
935
						$color = @unpack('n', $vide . ($data[$p] ?? ''));
936
						$color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
937
						break;
938
					case 4:
939
						$color = @unpack('n', $vide . ($data[floor($p)] ?? ''));
940
						$color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F;
941
						$color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
942
						break;
943
					case 1:
944
						$color = @unpack('n', $vide . ($data[floor($p)] ?? ''));
945
						switch (($p * 8) % 8) {
946
							case 0:
947
								$color[1] = $color[1] >> 7;
948
								break;
949
							case 1:
950
								$color[1] = ($color[1] & 0x40) >> 6;
951
								break;
952
							case 2:
953
								$color[1] = ($color[1] & 0x20) >> 5;
954
								break;
955
							case 3:
956
								$color[1] = ($color[1] & 0x10) >> 4;
957
								break;
958
							case 4:
959
								$color[1] = ($color[1] & 0x8) >> 3;
960
								break;
961
							case 5:
962
								$color[1] = ($color[1] & 0x4) >> 2;
963
								break;
964
							case 6:
965
								$color[1] = ($color[1] & 0x2) >> 1;
966
								break;
967
							case 7:
968
								$color[1] = ($color[1] & 0x1);
969
								break;
970
						}
971
						$color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
972
						break;
973
					default:
974
						fclose($fh);
975
						$this->logger->warning('imagecreatefrombmp: ' . $fileName . ' has ' . $meta['bits'] . ' bits and this is not supported!', ['app' => 'core']);
976
						return false;
977
				}
978
				imagesetpixel($im, $x, $y, $color[1]);
979
				$x++;
980
				$p += $meta['bytes'];
981
			}
982
			$y--;
983
			$p += $meta['decal'];
984
		}
985
		fclose($fh);
986
		return $im;
987
	}
988
989
	/**
990
	 * Resizes the image preserving ratio.
991
	 *
992
	 * @param integer $maxSize The maximum size of either the width or height.
993
	 * @return bool
994
	 */
995
	public function resize($maxSize) {
996
		if (!$this->valid()) {
997
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
998
			return false;
999
		}
1000
		$result = $this->resizeNew($maxSize);
1001
		imagedestroy($this->resource);
1002
		$this->resource = $result;
1003
		return $this->valid();
1004
	}
1005
1006
	/**
1007
	 * @param $maxSize
1008
	 * @return resource|bool|\GdImage
1009
	 */
1010
	private function resizeNew($maxSize) {
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
		$ratioOrig = $widthOrig / $heightOrig;
1018
1019
		if ($ratioOrig > 1) {
1020
			$newHeight = round($maxSize / $ratioOrig);
1021
			$newWidth = $maxSize;
1022
		} else {
1023
			$newWidth = round($maxSize * $ratioOrig);
1024
			$newHeight = $maxSize;
1025
		}
1026
1027
		return $this->preciseResizeNew((int)round($newWidth), (int)round($newHeight));
1028
	}
1029
1030
	/**
1031
	 * @param int $width
1032
	 * @param int $height
1033
	 * @return bool
1034
	 */
1035
	public function preciseResize(int $width, int $height): bool {
1036
		if (!$this->valid()) {
1037
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1038
			return false;
1039
		}
1040
		$result = $this->preciseResizeNew($width, $height);
1041
		imagedestroy($this->resource);
1042
		$this->resource = $result;
1043
		return $this->valid();
1044
	}
1045
1046
1047
	/**
1048
	 * @param int $width
1049
	 * @param int $height
1050
	 * @return resource|bool|\GdImage
1051
	 */
1052
	public function preciseResizeNew(int $width, int $height) {
1053
		if (!($width > 0) || !($height > 0)) {
1054
			$this->logger->info(__METHOD__ . '(): Requested image size not bigger than 0', ['app' => 'core']);
1055
			return false;
1056
		}
1057
		if (!$this->valid()) {
1058
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1059
			return false;
1060
		}
1061
		$widthOrig = imagesx($this->resource);
1062
		$heightOrig = imagesy($this->resource);
1063
		$process = imagecreatetruecolor($width, $height);
1064
		if ($process === false) {
1065
			$this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
1066
			return false;
1067
		}
1068
1069
		// preserve transparency
1070
		if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
1071
			imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
1072
			imagealphablending($process, false);
1073
			imagesavealpha($process, true);
1074
		}
1075
1076
		$res = imagecopyresampled($process, $this->resource, 0, 0, 0, 0, $width, $height, $widthOrig, $heightOrig);
1077
		if ($res === false) {
1078
			$this->logger->error(__METHOD__ . '(): Error re-sampling process image', ['app' => 'core']);
1079
			imagedestroy($process);
1080
			return false;
1081
		}
1082
		return $process;
1083
	}
1084
1085
	/**
1086
	 * Crops the image to the middle square. If the image is already square it just returns.
1087
	 *
1088
	 * @param int $size maximum size for the result (optional)
1089
	 * @return bool for success or failure
1090
	 */
1091
	public function centerCrop($size = 0) {
1092
		if (!$this->valid()) {
1093
			$this->logger->error('OC_Image->centerCrop, No image loaded', ['app' => 'core']);
1094
			return false;
1095
		}
1096
		$widthOrig = imagesx($this->resource);
1097
		$heightOrig = imagesy($this->resource);
1098
		if ($widthOrig === $heightOrig and $size == 0) {
1099
			return true;
1100
		}
1101
		$ratioOrig = $widthOrig / $heightOrig;
1102
		$width = $height = min($widthOrig, $heightOrig);
1103
1104
		if ($ratioOrig > 1) {
1105
			$x = ($widthOrig / 2) - ($width / 2);
1106
			$y = 0;
1107
		} else {
1108
			$y = ($heightOrig / 2) - ($height / 2);
1109
			$x = 0;
1110
		}
1111
		if ($size > 0) {
1112
			$targetWidth = $size;
1113
			$targetHeight = $size;
1114
		} else {
1115
			$targetWidth = $width;
1116
			$targetHeight = $height;
1117
		}
1118
		$process = imagecreatetruecolor($targetWidth, $targetHeight);
1119
		if ($process === false) {
1120
			$this->logger->error('OC_Image->centerCrop, Error creating true color image', ['app' => 'core']);
1121
			return false;
1122
		}
1123
1124
		// preserve transparency
1125
		if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
1126
			imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
1127
			imagealphablending($process, false);
1128
			imagesavealpha($process, true);
1129
		}
1130
1131
		imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height);
1132
		if ($process === false) {
1133
			$this->logger->error('OC_Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']);
1134
			return false;
1135
		}
1136
		imagedestroy($this->resource);
1137
		$this->resource = $process;
1138
		return true;
1139
	}
1140
1141
	/**
1142
	 * Crops the image from point $x$y with dimension $wx$h.
1143
	 *
1144
	 * @param int $x Horizontal position
1145
	 * @param int $y Vertical position
1146
	 * @param int $w Width
1147
	 * @param int $h Height
1148
	 * @return bool for success or failure
1149
	 */
1150
	public function crop(int $x, int $y, int $w, int $h): bool {
1151
		if (!$this->valid()) {
1152
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1153
			return false;
1154
		}
1155
		$result = $this->cropNew($x, $y, $w, $h);
1156
		imagedestroy($this->resource);
1157
		$this->resource = $result;
1158
		return $this->valid();
1159
	}
1160
1161
	/**
1162
	 * Crops the image from point $x$y with dimension $wx$h.
1163
	 *
1164
	 * @param int $x Horizontal position
1165
	 * @param int $y Vertical position
1166
	 * @param int $w Width
1167
	 * @param int $h Height
1168
	 * @return resource|\GdImage|false
1169
	 */
1170
	public function cropNew(int $x, int $y, int $w, int $h) {
1171
		if (!$this->valid()) {
1172
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1173
			return false;
1174
		}
1175
		$process = imagecreatetruecolor($w, $h);
1176
		if ($process === false) {
1177
			$this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
1178
			return false;
1179
		}
1180
1181
		// preserve transparency
1182
		if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
1183
			imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
1184
			imagealphablending($process, false);
1185
			imagesavealpha($process, true);
1186
		}
1187
1188
		imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $w, $h, $w, $h);
1189
		if ($process === false) {
1190
			$this->logger->error(__METHOD__ . '(): Error re-sampling process image ' . $w . 'x' . $h, ['app' => 'core']);
1191
			return false;
1192
		}
1193
		return $process;
1194
	}
1195
1196
	/**
1197
	 * Resizes the image to fit within a boundary while preserving ratio.
1198
	 *
1199
	 * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up
1200
	 *
1201
	 * @param integer $maxWidth
1202
	 * @param integer $maxHeight
1203
	 * @return bool
1204
	 */
1205
	public function fitIn($maxWidth, $maxHeight) {
1206
		if (!$this->valid()) {
1207
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1208
			return false;
1209
		}
1210
		$widthOrig = imagesx($this->resource);
1211
		$heightOrig = imagesy($this->resource);
1212
		$ratio = $widthOrig / $heightOrig;
1213
1214
		$newWidth = min($maxWidth, $ratio * $maxHeight);
1215
		$newHeight = min($maxHeight, $maxWidth / $ratio);
1216
1217
		$this->preciseResize((int)round($newWidth), (int)round($newHeight));
1218
		return true;
1219
	}
1220
1221
	/**
1222
	 * Shrinks larger images to fit within specified boundaries while preserving ratio.
1223
	 *
1224
	 * @param integer $maxWidth
1225
	 * @param integer $maxHeight
1226
	 * @return bool
1227
	 */
1228
	public function scaleDownToFit($maxWidth, $maxHeight) {
1229
		if (!$this->valid()) {
1230
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1231
			return false;
1232
		}
1233
		$widthOrig = imagesx($this->resource);
1234
		$heightOrig = imagesy($this->resource);
1235
1236
		if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) {
1237
			return $this->fitIn($maxWidth, $maxHeight);
1238
		}
1239
1240
		return false;
1241
	}
1242
1243
	public function copy(): IImage {
1244
		$image = new OC_Image(null, $this->logger, $this->config);
1245
		$image->resource = imagecreatetruecolor($this->width(), $this->height());
1246
		imagecopy(
1247
			$image->resource(),
1248
			$this->resource(),
1249
			0,
1250
			0,
1251
			0,
1252
			0,
1253
			$this->width(),
1254
			$this->height()
1255
		);
1256
1257
		return $image;
1258
	}
1259
1260
	public function cropCopy(int $x, int $y, int $w, int $h): IImage {
1261
		$image = new OC_Image(null, $this->logger, $this->config);
1262
		$image->imageType = $this->imageType;
1263
		$image->mimeType = $this->mimeType;
1264
		$image->bitDepth = $this->bitDepth;
1265
		$image->resource = $this->cropNew($x, $y, $w, $h);
1266
1267
		return $image;
1268
	}
1269
1270
	public function preciseResizeCopy(int $width, int $height): IImage {
1271
		$image = new OC_Image(null, $this->logger, $this->config);
1272
		$image->imageType = $this->imageType;
1273
		$image->mimeType = $this->mimeType;
1274
		$image->bitDepth = $this->bitDepth;
1275
		$image->resource = $this->preciseResizeNew($width, $height);
1276
1277
		return $image;
1278
	}
1279
1280
	public function resizeCopy(int $maxSize): IImage {
1281
		$image = new OC_Image(null, $this->logger, $this->config);
1282
		$image->imageType = $this->imageType;
1283
		$image->mimeType = $this->mimeType;
1284
		$image->bitDepth = $this->bitDepth;
1285
		$image->resource = $this->resizeNew($maxSize);
1286
1287
		return $image;
1288
	}
1289
1290
	/**
1291
	 * Destroys the current image and resets the object
1292
	 */
1293
	public function destroy() {
1294
		if ($this->valid()) {
1295
			imagedestroy($this->resource);
1296
		}
1297
		$this->resource = false;
1298
	}
1299
1300
	public function __destruct() {
1301
		$this->destroy();
1302
	}
1303
}
1304
1305
if (!function_exists('imagebmp')) {
1306
	/**
1307
	 * Output a BMP image to either the browser or a file
1308
	 *
1309
	 * @link http://www.ugia.cn/wp-data/imagebmp.php
1310
	 * @author legend <[email protected]>
1311
	 * @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm
1312
	 * @author mgutt <[email protected]>
1313
	 * @version 1.00
1314
	 * @param resource|\GdImage $im
1315
	 * @param string $fileName [optional] <p>The path to save the file to.</p>
1316
	 * @param int $bit [optional] <p>Bit depth, (default is 24).</p>
1317
	 * @param int $compression [optional]
1318
	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
1319
	 */
1320
	function imagebmp($im, $fileName = '', $bit = 24, $compression = 0) {
1321
		if (!in_array($bit, [1, 4, 8, 16, 24, 32])) {
1322
			$bit = 24;
1323
		} elseif ($bit == 32) {
1324
			$bit = 24;
1325
		}
1326
		$bits = (int)pow(2, $bit);
1327
		imagetruecolortopalette($im, true, $bits);
1328
		$width = imagesx($im);
1329
		$height = imagesy($im);
1330
		$colorsNum = imagecolorstotal($im);
1331
		$rgbQuad = '';
1332
		if ($bit <= 8) {
1333
			for ($i = 0; $i < $colorsNum; $i++) {
1334
				$colors = imagecolorsforindex($im, $i);
1335
				$rgbQuad .= chr($colors['blue']) . chr($colors['green']) . chr($colors['red']) . "\0";
1336
			}
1337
			$bmpData = '';
1338
			if ($compression == 0 || $bit < 8) {
1339
				$compression = 0;
1340
				$extra = '';
1341
				$padding = 4 - ceil($width / (8 / $bit)) % 4;
1342
				if ($padding % 4 != 0) {
1343
					$extra = str_repeat("\0", $padding);
1344
				}
1345
				for ($j = $height - 1; $j >= 0; $j--) {
1346
					$i = 0;
1347
					while ($i < $width) {
1348
						$bin = 0;
1349
						$limit = $width - $i < 8 / $bit ? (8 / $bit - $width + $i) * $bit : 0;
1350
						for ($k = 8 - $bit; $k >= $limit; $k -= $bit) {
1351
							$index = imagecolorat($im, $i, $j);
1352
							$bin |= $index << $k;
1353
							$i++;
1354
						}
1355
						$bmpData .= chr($bin);
1356
					}
1357
					$bmpData .= $extra;
1358
				}
1359
			} // RLE8
1360
			elseif ($compression == 1 && $bit == 8) {
1361
				for ($j = $height - 1; $j >= 0; $j--) {
1362
					$lastIndex = null;
1363
					$sameNum = 0;
1364
					for ($i = 0; $i <= $width; $i++) {
1365
						$index = imagecolorat($im, $i, $j);
1366
						if ($index !== $lastIndex || $sameNum > 255) {
1367
							if ($sameNum != 0) {
1368
								$bmpData .= chr($sameNum) . chr($lastIndex);
0 ignored issues
show
Bug introduced by
$lastIndex of type null is incompatible with the type integer expected by parameter $codepoint of chr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1368
								$bmpData .= chr($sameNum) . chr(/** @scrutinizer ignore-type */ $lastIndex);
Loading history...
1369
							}
1370
							$lastIndex = $index;
1371
							$sameNum = 1;
1372
						} else {
1373
							$sameNum++;
1374
						}
1375
					}
1376
					$bmpData .= "\0\0";
1377
				}
1378
				$bmpData .= "\0\1";
1379
			}
1380
			$sizeQuad = strlen($rgbQuad);
1381
			$sizeData = strlen($bmpData);
1382
		} else {
1383
			$extra = '';
1384
			$padding = 4 - ($width * ($bit / 8)) % 4;
1385
			if ($padding % 4 != 0) {
1386
				$extra = str_repeat("\0", $padding);
1387
			}
1388
			$bmpData = '';
1389
			for ($j = $height - 1; $j >= 0; $j--) {
1390
				for ($i = 0; $i < $width; $i++) {
1391
					$index = imagecolorat($im, $i, $j);
1392
					$colors = imagecolorsforindex($im, $index);
1393
					if ($bit == 16) {
1394
						$bin = 0 << $bit;
1395
						$bin |= ($colors['red'] >> 3) << 10;
1396
						$bin |= ($colors['green'] >> 3) << 5;
1397
						$bin |= $colors['blue'] >> 3;
1398
						$bmpData .= pack("v", $bin);
1399
					} else {
1400
						$bmpData .= pack("c*", $colors['blue'], $colors['green'], $colors['red']);
1401
					}
1402
				}
1403
				$bmpData .= $extra;
1404
			}
1405
			$sizeQuad = 0;
1406
			$sizeData = strlen($bmpData);
1407
			$colorsNum = 0;
1408
		}
1409
		$fileHeader = 'BM' . pack('V3', 54 + $sizeQuad + $sizeData, 0, 54 + $sizeQuad);
1410
		$infoHeader = pack('V3v2V*', 0x28, $width, $height, 1, $bit, $compression, $sizeData, 0, 0, $colorsNum, 0);
1411
		if ($fileName != '') {
1412
			$fp = fopen($fileName, 'wb');
1413
			fwrite($fp, $fileHeader . $infoHeader . $rgbQuad . $bmpData);
1414
			fclose($fp);
1415
			return true;
1416
		}
1417
		echo $fileHeader . $infoHeader . $rgbQuad . $bmpData;
1418
		return true;
1419
	}
1420
}
1421
1422
if (!function_exists('exif_imagetype')) {
1423
	/**
1424
	 * Workaround if exif_imagetype does not exist
1425
	 *
1426
	 * @link https://www.php.net/manual/en/function.exif-imagetype.php#80383
1427
	 * @param string $fileName
1428
	 * @return string|boolean
1429
	 */
1430
	function exif_imagetype($fileName) {
1431
		if (($info = getimagesize($fileName)) !== false) {
1432
			return $info[2];
1433
		}
1434
		return false;
1435
	}
1436
}
1437