Passed
Push — master ( edf8ce...eba372 )
by Roeland
13:54 queued 15s
created

OC_Image::preciseResizeCopy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
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 Christopher Schäpers <[email protected]>
10
 * @author Christoph Wurst <[email protected]>
11
 * @author Georg Ehrke <[email protected]>
12
 * @author j-ed <[email protected]>
13
 * @author Joas Schilling <[email protected]>
14
 * @author Johannes Willnecker <[email protected]>
15
 * @author Jörn Friedrich Dreyer <[email protected]>
16
 * @author Julius Härtl <[email protected]>
17
 * @author Lukas Reschke <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author Olivier Paroz <[email protected]>
20
 * @author Robin Appelman <[email protected]>
21
 * @author Roeland Jago Douma <[email protected]>
22
 * @author Samuel CHEMLA <[email protected]>
23
 * @author Thomas Müller <[email protected]>
24
 * @author Thomas Tanghus <[email protected]>
25
 *
26
 * @license AGPL-3.0
27
 *
28
 * This code is free software: you can redistribute it and/or modify
29
 * it under the terms of the GNU Affero General Public License, version 3,
30
 * as published by the Free Software Foundation.
31
 *
32
 * This program is distributed in the hope that it will be useful,
33
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35
 * GNU Affero General Public License for more details.
36
 *
37
 * You should have received a copy of the GNU Affero General Public License, version 3,
38
 * along with this program. If not, see <http://www.gnu.org/licenses/>
39
 *
40
 */
41
42
use OCP\IImage;
43
44
/**
45
 * Class for basic image manipulation
46
 */
47
class 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
		return is_resource($this->resource);
102
	}
103
104
	/**
105
	 * Returns the MIME type of the image or an empty string if no image is loaded.
106
	 *
107
	 * @return string
108
	 */
109
	public function mimeType() {
110
		return $this->valid() ? $this->mimeType : '';
111
	}
112
113
	/**
114
	 * Returns the width of the image or -1 if no image is loaded.
115
	 *
116
	 * @return int
117
	 */
118
	public function width() {
119
		return $this->valid() ? imagesx($this->resource) : -1;
120
	}
121
122
	/**
123
	 * Returns the height of the image or -1 if no image is loaded.
124
	 *
125
	 * @return int
126
	 */
127
	public function height() {
128
		return $this->valid() ? imagesy($this->resource) : -1;
129
	}
130
131
	/**
132
	 * Returns the width when the image orientation is top-left.
133
	 *
134
	 * @return int
135
	 */
136
	public function widthTopLeft() {
137
		$o = $this->getOrientation();
138
		$this->logger->debug('OC_Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']);
139
		switch ($o) {
140
			case -1:
141
			case 1:
142
			case 2: // Not tested
143
			case 3:
144
			case 4: // Not tested
145
				return $this->width();
146
			case 5: // Not tested
147
			case 6:
148
			case 7: // Not tested
149
			case 8:
150
				return $this->height();
151
		}
152
		return $this->width();
153
	}
154
155
	/**
156
	 * Returns the height when the image orientation is top-left.
157
	 *
158
	 * @return int
159
	 */
160
	public function heightTopLeft() {
161
		$o = $this->getOrientation();
162
		$this->logger->debug('OC_Image->heightTopLeft() 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->height();
170
			case 5: // Not tested
171
			case 6:
172
			case 7: // Not tested
173
			case 8:
174
				return $this->width();
175
		}
176
		return $this->height();
177
	}
178
179
	/**
180
	 * Outputs the image.
181
	 *
182
	 * @param string $mimeType
183
	 * @return bool
184
	 */
185
	public function show($mimeType = null) {
186
		if ($mimeType === null) {
187
			$mimeType = $this->mimeType();
188
		}
189
		header('Content-Type: ' . $mimeType);
190
		return $this->_output(null, $mimeType);
191
	}
192
193
	/**
194
	 * Saves the image.
195
	 *
196
	 * @param string $filePath
197
	 * @param string $mimeType
198
	 * @return bool
199
	 */
200
201
	public function save($filePath = null, $mimeType = null) {
202
		if ($mimeType === null) {
203
			$mimeType = $this->mimeType();
204
		}
205
		if ($filePath === null) {
206
			if ($this->filePath === null) {
207
				$this->logger->error(__METHOD__ . '(): called with no path.', ['app' => 'core']);
208
				return false;
209
			} else {
210
				$filePath = $this->filePath;
211
			}
212
		}
213
		return $this->_output($filePath, $mimeType);
214
	}
215
216
	/**
217
	 * Outputs/saves the image.
218
	 *
219
	 * @param string $filePath
220
	 * @param string $mimeType
221
	 * @return bool
222
	 * @throws Exception
223
	 */
224
	private function _output($filePath = null, $mimeType = null) {
225
		if ($filePath) {
226
			if (!file_exists(dirname($filePath))) {
227
				mkdir(dirname($filePath), 0777, true);
228
			}
229
			$isWritable = is_writable(dirname($filePath));
230
			if (!$isWritable) {
231
				$this->logger->error(__METHOD__ . '(): Directory \'' . dirname($filePath) . '\' is not writable.', ['app' => 'core']);
232
				return false;
233
			} elseif ($isWritable && 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, $this->getJpegQuality());
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);
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

288
				$retVal = imagebmp($this->resource, $filePath, /** @scrutinizer ignore-type */ $this->bitDepth);
Loading history...
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
	 * @param resource Returns the image resource in any.
0 ignored issues
show
Bug introduced by
The type Returns was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
305
	 * @throws \InvalidArgumentException in case the supplied resource does not have the type "gd"
306
	 */
307
	public function setResource($resource) {
308
		if (get_resource_type($resource) === 'gd') {
0 ignored issues
show
Bug introduced by
$resource of type Returns is incompatible with the type resource expected by parameter $handle of get_resource_type(). ( Ignorable by Annotation )

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

308
		if (get_resource_type(/** @scrutinizer ignore-type */ $resource) === 'gd') {
Loading history...
309
			$this->resource = $resource;
0 ignored issues
show
Documentation Bug introduced by
It seems like $resource of type Returns is incompatible with the declared type false|resource of property $resource.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
310
			return;
311
		}
312
		throw new \InvalidArgumentException('Supplied resource is not of type "gd".');
313
	}
314
315
	/**
316
	 * @return resource Returns the image resource in any.
317
	 */
318
	public function resource() {
319
		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 resource.
Loading history...
320
	}
321
322
	/**
323
	 * @return string Returns the mimetype of the data. Returns the empty string
324
	 * if the data is not valid.
325
	 */
326
	public function dataMimeType() {
327
		if (!$this->valid()) {
328
			return '';
329
		}
330
331
		switch ($this->mimeType) {
332
			case 'image/png':
333
			case 'image/jpeg':
334
			case 'image/gif':
335
				return $this->mimeType;
336
			default:
337
				return 'image/png';
338
		}
339
	}
340
341
	/**
342
	 * @return null|string Returns the raw image data.
343
	 */
344
	public function data() {
345
		if (!$this->valid()) {
346
			return null;
347
		}
348
		ob_start();
349
		switch ($this->mimeType) {
350
			case "image/png":
351
				$res = imagepng($this->resource);
352
				break;
353
			case "image/jpeg":
354
				$quality = $this->getJpegQuality();
355
				if ($quality !== null) {
356
					$res = imagejpeg($this->resource, null, $quality);
357
				} else {
358
					$res = imagejpeg($this->resource);
359
				}
360
				break;
361
			case "image/gif":
362
				$res = imagegif($this->resource);
363
				break;
364
			default:
365
				$res = imagepng($this->resource);
366
				$this->logger->info('OC_Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']);
367
				break;
368
		}
369
		if (!$res) {
370
			$this->logger->error('OC_Image->data. Error getting image data.', ['app' => 'core']);
371
		}
372
		return ob_get_clean();
373
	}
374
375
	/**
376
	 * @return string - base64 encoded, which is suitable for embedding in a VCard.
377
	 */
378
	public function __toString() {
379
		return base64_encode($this->data());
380
	}
381
382
	/**
383
	 * @return int|null
384
	 */
385
	protected function getJpegQuality() {
386
		$quality = $this->config->getAppValue('preview', 'jpeg_quality', 90);
387
		if ($quality !== null) {
0 ignored issues
show
introduced by
The condition $quality !== null is always true.
Loading history...
388
			$quality = min(100, max(10, (int) $quality));
389
		}
390
		return $quality;
391
	}
392
393
	/**
394
	 * (I'm open for suggestions on better method name ;)
395
	 * Get the orientation based on EXIF data.
396
	 *
397
	 * @return int The orientation or -1 if no EXIF data is available.
398
	 */
399
	public function getOrientation() {
400
		if ($this->exif !== null) {
401
			return $this->exif['Orientation'];
402
		}
403
404
		if ($this->imageType !== IMAGETYPE_JPEG) {
405
			$this->logger->debug('OC_Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']);
406
			return -1;
407
		}
408
		if (!is_callable('exif_read_data')) {
409
			$this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
410
			return -1;
411
		}
412
		if (!$this->valid()) {
413
			$this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']);
414
			return -1;
415
		}
416
		if (is_null($this->filePath) || !is_readable($this->filePath)) {
417
			$this->logger->debug('OC_Image->fixOrientation() No readable file path set.', ['app' => 'core']);
418
			return -1;
419
		}
420
		$exif = @exif_read_data($this->filePath, 'IFD0');
421
		if (!$exif) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exif 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...
422
			return -1;
423
		}
424
		if (!isset($exif['Orientation'])) {
425
			return -1;
426
		}
427
		$this->exif = $exif;
428
		return $exif['Orientation'];
429
	}
430
431
	public function readExif($data) {
432
		if (!is_callable('exif_read_data')) {
433
			$this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']);
434
			return;
435
		}
436
		if (!$this->valid()) {
437
			$this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']);
438
			return;
439
		}
440
441
		$exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($data));
442
		if (!$exif) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exif 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...
443
			return;
444
		}
445
		if (!isset($exif['Orientation'])) {
446
			return;
447
		}
448
		$this->exif = $exif;
449
	}
450
451
	/**
452
	 * (I'm open for suggestions on better method name ;)
453
	 * Fixes orientation based on EXIF data.
454
	 *
455
	 * @return bool
456
	 */
457
	public function fixOrientation() {
458
		$o = $this->getOrientation();
459
		$this->logger->debug('OC_Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']);
460
		$rotate = 0;
461
		$flip = false;
462
		switch ($o) {
463
			case -1:
464
				return false; //Nothing to fix
465
			case 1:
466
				$rotate = 0;
467
				break;
468
			case 2:
469
				$rotate = 0;
470
				$flip = true;
471
				break;
472
			case 3:
473
				$rotate = 180;
474
				break;
475
			case 4:
476
				$rotate = 180;
477
				$flip = true;
478
				break;
479
			case 5:
480
				$rotate = 90;
481
				$flip = true;
482
				break;
483
			case 6:
484
				$rotate = 270;
485
				break;
486
			case 7:
487
				$rotate = 270;
488
				$flip = true;
489
				break;
490
			case 8:
491
				$rotate = 90;
492
				break;
493
		}
494
		if($flip && function_exists('imageflip')) {
495
			imageflip($this->resource, IMG_FLIP_HORIZONTAL);
496
		}
497
		if ($rotate) {
498
			$res = imagerotate($this->resource, $rotate, 0);
499
			if ($res) {
0 ignored issues
show
introduced by
$res is of type false|resource, thus it always evaluated to false.
Loading history...
500
				if (imagealphablending($res, true)) {
501
					if (imagesavealpha($res, true)) {
502
						imagedestroy($this->resource);
503
						$this->resource = $res;
504
						return true;
505
					} else {
506
						$this->logger->debug('OC_Image->fixOrientation() Error during alpha-saving', ['app' => 'core']);
507
						return false;
508
					}
509
				} else {
510
					$this->logger->debug('OC_Image->fixOrientation() Error during alpha-blending', ['app' => 'core']);
511
					return false;
512
				}
513
			} else {
514
				$this->logger->debug('OC_Image->fixOrientation() Error during orientation fixing', ['app' => 'core']);
515
				return false;
516
			}
517
		}
518
		return false;
519
	}
520
521
	/**
522
	 * Loads an image from an open file handle.
523
	 * It is the responsibility of the caller to position the pointer at the correct place and to close the handle again.
524
	 *
525
	 * @param resource $handle
526
	 * @return resource|false An image resource or false on error
527
	 */
528
	public function loadFromFileHandle($handle) {
529
		$contents = stream_get_contents($handle);
530
		if ($this->loadFromData($contents)) {
531
			return $this->resource;
532
		}
533
		return false;
534
	}
535
536
	/**
537
	 * Loads an image from a local file.
538
	 *
539
	 * @param bool|string $imagePath The path to a local file.
540
	 * @return bool|resource An image resource or false on error
541
	 */
542
	public function loadFromFile($imagePath = false) {
543
		// exif_imagetype throws "read error!" if file is less than 12 byte
544
		if (!@is_file($imagePath) || !file_exists($imagePath) || filesize($imagePath) < 12 || !is_readable($imagePath)) {
545
			return false;
546
		}
547
		$iType = exif_imagetype($imagePath);
548
		switch ($iType) {
549
			case IMAGETYPE_GIF:
550
				if (imagetypes() & IMG_GIF) {
551
					$this->resource = imagecreatefromgif($imagePath);
552
					// Preserve transparency
553
					imagealphablending($this->resource, true);
554
					imagesavealpha($this->resource, true);
555
				} else {
556
					$this->logger->debug('OC_Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']);
557
				}
558
				break;
559
			case IMAGETYPE_JPEG:
560
				if (imagetypes() & IMG_JPG) {
561
					if (getimagesize($imagePath) !== false) {
562
						$this->resource = @imagecreatefromjpeg($imagePath);
563
					} else {
564
						$this->logger->debug('OC_Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']);
565
					}
566
				} else {
567
					$this->logger->debug('OC_Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']);
568
				}
569
				break;
570
			case IMAGETYPE_PNG:
571
				if (imagetypes() & IMG_PNG) {
572
					$this->resource = @imagecreatefrompng($imagePath);
573
					// Preserve transparency
574
					imagealphablending($this->resource, true);
575
					imagesavealpha($this->resource, true);
576
				} else {
577
					$this->logger->debug('OC_Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']);
578
				}
579
				break;
580
			case IMAGETYPE_XBM:
581
				if (imagetypes() & IMG_XPM) {
582
					$this->resource = @imagecreatefromxbm($imagePath);
583
				} else {
584
					$this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']);
585
				}
586
				break;
587
			case IMAGETYPE_WBMP:
588
				if (imagetypes() & IMG_WBMP) {
589
					$this->resource = @imagecreatefromwbmp($imagePath);
590
				} else {
591
					$this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']);
592
				}
593
				break;
594
			case IMAGETYPE_BMP:
595
				$this->resource = $this->imagecreatefrombmp($imagePath);
596
				break;
597
			/*
598
			case IMAGETYPE_TIFF_II: // (intel byte order)
599
				break;
600
			case IMAGETYPE_TIFF_MM: // (motorola byte order)
601
				break;
602
			case IMAGETYPE_JPC:
603
				break;
604
			case IMAGETYPE_JP2:
605
				break;
606
			case IMAGETYPE_JPX:
607
				break;
608
			case IMAGETYPE_JB2:
609
				break;
610
			case IMAGETYPE_SWC:
611
				break;
612
			case IMAGETYPE_IFF:
613
				break;
614
			case IMAGETYPE_ICO:
615
				break;
616
			case IMAGETYPE_SWF:
617
				break;
618
			case IMAGETYPE_PSD:
619
				break;
620
			*/
621
			default:
622
623
				// this is mostly file created from encrypted file
624
				$this->resource = imagecreatefromstring(\OC\Files\Filesystem::file_get_contents(\OC\Files\Filesystem::getLocalPath($imagePath)));
625
				$iType = IMAGETYPE_PNG;
626
				$this->logger->debug('OC_Image->loadFromFile, Default', ['app' => 'core']);
627
				break;
628
		}
629
		if ($this->valid()) {
630
			$this->imageType = $iType;
631
			$this->mimeType = image_type_to_mime_type($iType);
632
			$this->filePath = $imagePath;
0 ignored issues
show
Documentation Bug introduced by
It seems like $imagePath can also be of type boolean. However, the property $filePath is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
633
		}
634
		return $this->resource;
635
	}
636
637
	/**
638
	 * Loads an image from a string of data.
639
	 *
640
	 * @param string $str A string of image data as read from a file.
641
	 * @return bool|resource An image resource or false on error
642
	 */
643
	public function loadFromData($str) {
644
		if (is_resource($str)) {
0 ignored issues
show
introduced by
The condition is_resource($str) is always false.
Loading history...
645
			return false;
646
		}
647
		$this->resource = @imagecreatefromstring($str);
648
		if ($this->fileInfo) {
649
			$this->mimeType = $this->fileInfo->buffer($str);
650
		}
651
		if (is_resource($this->resource)) {
652
			imagealphablending($this->resource, false);
653
			imagesavealpha($this->resource, true);
654
		}
655
656
		if (!$this->resource) {
657
			$this->logger->debug('OC_Image->loadFromFile, could not load', ['app' => 'core']);
658
			return false;
659
		}
660
		return $this->resource;
661
	}
662
663
	/**
664
	 * Loads an image from a base64 encoded string.
665
	 *
666
	 * @param string $str A string base64 encoded string of image data.
667
	 * @return bool|resource An image resource or false on error
668
	 */
669
	public function loadFromBase64($str) {
670
		if (!is_string($str)) {
0 ignored issues
show
introduced by
The condition is_string($str) is always true.
Loading history...
671
			return false;
672
		}
673
		$data = base64_decode($str);
674
		if ($data) { // try to load from string data
675
			$this->resource = @imagecreatefromstring($data);
676
			if ($this->fileInfo) {
677
				$this->mimeType = $this->fileInfo->buffer($data);
678
			}
679
			if (!$this->resource) {
680
				$this->logger->debug('OC_Image->loadFromBase64, could not load', ['app' => 'core']);
681
				return false;
682
			}
683
			return $this->resource;
684
		} else {
685
			return false;
686
		}
687
	}
688
689
	/**
690
	 * Create a new image from file or URL
691
	 *
692
	 * @link http://www.programmierer-forum.de/function-imagecreatefrombmp-laeuft-mit-allen-bitraten-t143137.htm
693
	 * @version 1.00
694
	 * @param string $fileName <p>
695
	 * Path to the BMP image.
696
	 * </p>
697
	 * @return bool|resource an image resource identifier on success, <b>FALSE</b> on errors.
698
	 */
699
	private function imagecreatefrombmp($fileName) {
700
		if (!($fh = fopen($fileName, 'rb'))) {
701
			$this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName, ['app' => 'core']);
702
			return false;
703
		}
704
		// read file header
705
		$meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14));
706
		// check for bitmap
707
		if ($meta['type'] != 19778) {
708
			fclose($fh);
709
			$this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
710
			return false;
711
		}
712
		// read image header
713
		$meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40));
714
		// read additional 16bit header
715
		if ($meta['bits'] == 16) {
716
			$meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12));
717
		}
718
		// set bytes and padding
719
		$meta['bytes'] = $meta['bits'] / 8;
720
		$this->bitDepth = $meta['bits']; //remember the bit depth for the imagebmp call
721
		$meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4)));
722
		if ($meta['decal'] == 4) {
723
			$meta['decal'] = 0;
724
		}
725
		// obtain imagesize
726
		if ($meta['imagesize'] < 1) {
727
			$meta['imagesize'] = $meta['filesize'] - $meta['offset'];
728
			// in rare cases filesize is equal to offset so we need to read physical size
729
			if ($meta['imagesize'] < 1) {
730
				$meta['imagesize'] = @filesize($fileName) - $meta['offset'];
731
				if ($meta['imagesize'] < 1) {
732
					fclose($fh);
733
					$this->logger->warning('imagecreatefrombmp: Can not obtain file size of ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
734
					return false;
735
				}
736
			}
737
		}
738
		// calculate colors
739
		$meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors'];
740
		// read color palette
741
		$palette = [];
742
		if ($meta['bits'] < 16) {
743
			$palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4));
744
			// in rare cases the color value is signed
745
			if ($palette[1] < 0) {
746
				foreach ($palette as $i => $color) {
747
					$palette[$i] = $color + 16777216;
748
				}
749
			}
750
		}
751
		// create gd image
752
		$im = imagecreatetruecolor($meta['width'], $meta['height']);
753
		if ($im == false) {
754
			fclose($fh);
755
			$this->logger->warning(
756
				'imagecreatefrombmp: imagecreatetruecolor failed for file "' . $fileName . '" with dimensions ' . $meta['width'] . 'x' . $meta['height'],
757
				['app' => 'core']);
758
			return false;
759
		}
760
761
		$data = fread($fh, $meta['imagesize']);
762
		$p = 0;
763
		$vide = chr(0);
764
		$y = $meta['height'] - 1;
765
		$error = 'imagecreatefrombmp: ' . $fileName . ' has not enough data!';
766
		// loop through the image data beginning with the lower left corner
767
		while ($y >= 0) {
768
			$x = 0;
769
			while ($x < $meta['width']) {
770
				switch ($meta['bits']) {
771
					case 32:
772
					case 24:
773
						if (!($part = substr($data, $p, 3))) {
774
							$this->logger->warning($error, ['app' => 'core']);
775
							return $im;
776
						}
777
						$color = @unpack('V', $part . $vide);
778
						break;
779
					case 16:
780
						if (!($part = substr($data, $p, 2))) {
781
							fclose($fh);
782
							$this->logger->warning($error, ['app' => 'core']);
783
							return $im;
784
						}
785
						$color = @unpack('v', $part);
786
						$color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3);
787
						break;
788
					case 8:
789
						$color = @unpack('n', $vide . ($data[$p] ?? ''));
790
						$color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
791
						break;
792
					case 4:
793
						$color = @unpack('n', $vide . ($data[floor($p)] ?? ''));
794
						$color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F;
795
						$color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
796
						break;
797
					case 1:
798
						$color = @unpack('n', $vide . ($data[floor($p)] ?? ''));
799
						switch (($p * 8) % 8) {
800
							case 0:
801
								$color[1] = $color[1] >> 7;
802
								break;
803
							case 1:
804
								$color[1] = ($color[1] & 0x40) >> 6;
805
								break;
806
							case 2:
807
								$color[1] = ($color[1] & 0x20) >> 5;
808
								break;
809
							case 3:
810
								$color[1] = ($color[1] & 0x10) >> 4;
811
								break;
812
							case 4:
813
								$color[1] = ($color[1] & 0x8) >> 3;
814
								break;
815
							case 5:
816
								$color[1] = ($color[1] & 0x4) >> 2;
817
								break;
818
							case 6:
819
								$color[1] = ($color[1] & 0x2) >> 1;
820
								break;
821
							case 7:
822
								$color[1] = ($color[1] & 0x1);
823
								break;
824
						}
825
						$color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1];
826
						break;
827
					default:
828
						fclose($fh);
829
						$this->logger->warning('imagecreatefrombmp: ' . $fileName . ' has ' . $meta['bits'] . ' bits and this is not supported!', ['app' => 'core']);
830
						return false;
831
				}
832
				imagesetpixel($im, $x, $y, $color[1]);
833
				$x++;
834
				$p += $meta['bytes'];
835
			}
836
			$y--;
837
			$p += $meta['decal'];
838
		}
839
		fclose($fh);
840
		return $im;
841
	}
842
843
	/**
844
	 * Resizes the image preserving ratio.
845
	 *
846
	 * @param integer $maxSize The maximum size of either the width or height.
847
	 * @return bool
848
	 */
849
	public function resize($maxSize) {
850
		$result = $this->resizeNew($maxSize);
851
		imagedestroy($this->resource);
852
		$this->resource = $result;
853
		return is_resource($result);
854
	}
855
856
	/**
857
	 * @param $maxSize
858
	 * @return resource | bool
859
	 */
860
	private function resizeNew($maxSize) {
861
		if (!$this->valid()) {
862
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
863
			return false;
864
		}
865
		$widthOrig = imagesx($this->resource);
866
		$heightOrig = imagesy($this->resource);
867
		$ratioOrig = $widthOrig / $heightOrig;
868
869
		if ($ratioOrig > 1) {
870
			$newHeight = round($maxSize / $ratioOrig);
871
			$newWidth = $maxSize;
872
		} else {
873
			$newWidth = round($maxSize * $ratioOrig);
874
			$newHeight = $maxSize;
875
		}
876
877
		return $this->preciseResizeNew((int)round($newWidth), (int)round($newHeight));
878
	}
879
880
	/**
881
	 * @param int $width
882
	 * @param int $height
883
	 * @return bool
884
	 */
885
	public function preciseResize(int $width, int $height): bool {
886
		$result = $this->preciseResizeNew($width, $height);
887
		imagedestroy($this->resource);
888
		$this->resource = $result;
889
		return is_resource($result);
890
	}
891
892
893
	/**
894
	 * @param int $width
895
	 * @param int $height
896
	 * @return resource | bool
897
	 */
898
	public function preciseResizeNew(int $width, int $height) {
899
		if (!$this->valid()) {
900
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
901
			return false;
902
		}
903
		$widthOrig = imagesx($this->resource);
904
		$heightOrig = imagesy($this->resource);
905
		$process = imagecreatetruecolor($width, $height);
906
		if ($process === false) {
907
			$this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
908
			return false;
909
		}
910
911
		// preserve transparency
912
		if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
913
			imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
914
			imagealphablending($process, false);
915
			imagesavealpha($process, true);
916
		}
917
918
		$res = imagecopyresampled($process, $this->resource, 0, 0, 0, 0, $width, $height, $widthOrig, $heightOrig);
919
		if ($res === false) {
920
			$this->logger->error(__METHOD__ . '(): Error re-sampling process image', ['app' => 'core']);
921
			imagedestroy($process);
922
			return false;
923
		}
924
		return $process;
925
	}
926
927
	/**
928
	 * Crops the image to the middle square. If the image is already square it just returns.
929
	 *
930
	 * @param int $size maximum size for the result (optional)
931
	 * @return bool for success or failure
932
	 */
933
	public function centerCrop($size = 0) {
934
		if (!$this->valid()) {
935
			$this->logger->error('OC_Image->centerCrop, No image loaded', ['app' => 'core']);
936
			return false;
937
		}
938
		$widthOrig = imagesx($this->resource);
939
		$heightOrig = imagesy($this->resource);
940
		if ($widthOrig === $heightOrig and $size == 0) {
941
			return true;
942
		}
943
		$ratioOrig = $widthOrig / $heightOrig;
944
		$width = $height = min($widthOrig, $heightOrig);
945
946
		if ($ratioOrig > 1) {
947
			$x = ($widthOrig / 2) - ($width / 2);
948
			$y = 0;
949
		} else {
950
			$y = ($heightOrig / 2) - ($height / 2);
951
			$x = 0;
952
		}
953
		if ($size > 0) {
954
			$targetWidth = $size;
955
			$targetHeight = $size;
956
		} else {
957
			$targetWidth = $width;
958
			$targetHeight = $height;
959
		}
960
		$process = imagecreatetruecolor($targetWidth, $targetHeight);
961
		if ($process == false) {
962
			$this->logger->error('OC_Image->centerCrop, Error creating true color image', ['app' => 'core']);
963
			imagedestroy($process);
964
			return false;
965
		}
966
967
		// preserve transparency
968
		if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
969
			imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
970
			imagealphablending($process, false);
971
			imagesavealpha($process, true);
972
		}
973
974
		imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height);
975
		if ($process == false) {
976
			$this->logger->error('OC_Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']);
977
			imagedestroy($process);
978
			return false;
979
		}
980
		imagedestroy($this->resource);
981
		$this->resource = $process;
982
		return true;
983
	}
984
985
	/**
986
	 * Crops the image from point $x$y with dimension $wx$h.
987
	 *
988
	 * @param int $x Horizontal position
989
	 * @param int $y Vertical position
990
	 * @param int $w Width
991
	 * @param int $h Height
992
	 * @return bool for success or failure
993
	 */
994
	public function crop(int $x, int $y, int $w, int $h): bool {
995
		$result = $this->cropNew($x, $y, $w, $h);
996
		imagedestroy($this->resource);
997
		$this->resource = $result;
998
		return is_resource($result);
999
	}
1000
1001
	/**
1002
	 * Crops the image from point $x$y with dimension $wx$h.
1003
	 *
1004
	 * @param int $x Horizontal position
1005
	 * @param int $y Vertical position
1006
	 * @param int $w Width
1007
	 * @param int $h Height
1008
	 * @return resource | bool
1009
	 */
1010
	public function cropNew(int $x, int $y, int $w, int $h) {
1011
		if (!$this->valid()) {
1012
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1013
			return false;
1014
		}
1015
		$process = imagecreatetruecolor($w, $h);
1016
		if ($process == false) {
1017
			$this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']);
1018
			imagedestroy($process);
1019
			return false;
1020
		}
1021
1022
		// preserve transparency
1023
		if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) {
1024
			imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127));
1025
			imagealphablending($process, false);
1026
			imagesavealpha($process, true);
1027
		}
1028
1029
		imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $w, $h, $w, $h);
1030
		if ($process == false) {
1031
			$this->logger->error(__METHOD__ . '(): Error re-sampling process image ' . $w . 'x' . $h, ['app' => 'core']);
1032
			imagedestroy($process);
1033
			return false;
1034
		}
1035
		return $process;
1036
	}
1037
1038
	/**
1039
	 * Resizes the image to fit within a boundary while preserving ratio.
1040
	 *
1041
	 * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up
1042
	 *
1043
	 * @param integer $maxWidth
1044
	 * @param integer $maxHeight
1045
	 * @return bool
1046
	 */
1047
	public function fitIn($maxWidth, $maxHeight) {
1048
		if (!$this->valid()) {
1049
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1050
			return false;
1051
		}
1052
		$widthOrig = imagesx($this->resource);
1053
		$heightOrig = imagesy($this->resource);
1054
		$ratio = $widthOrig / $heightOrig;
1055
1056
		$newWidth = min($maxWidth, $ratio * $maxHeight);
1057
		$newHeight = min($maxHeight, $maxWidth / $ratio);
1058
1059
		$this->preciseResize((int)round($newWidth), (int)round($newHeight));
1060
		return true;
1061
	}
1062
1063
	/**
1064
	 * Shrinks larger images to fit within specified boundaries while preserving ratio.
1065
	 *
1066
	 * @param integer $maxWidth
1067
	 * @param integer $maxHeight
1068
	 * @return bool
1069
	 */
1070
	public function scaleDownToFit($maxWidth, $maxHeight) {
1071
		if (!$this->valid()) {
1072
			$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
1073
			return false;
1074
		}
1075
		$widthOrig = imagesx($this->resource);
1076
		$heightOrig = imagesy($this->resource);
1077
1078
		if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) {
1079
			return $this->fitIn($maxWidth, $maxHeight);
1080
		}
1081
1082
		return false;
1083
	}
1084
1085
	public function copy(): IImage {
1086
		$image = new OC_Image(null, $this->logger, $this->config);
1087
		$image->resource = imagecreatetruecolor($this->width(), $this->height());
1088
		imagecopy(
1089
			$image->resource(),
1090
			$this->resource(),
1091
			0,
1092
			0,
1093
			0,
1094
			0,
1095
			$this->width(),
1096
			$this->height()
1097
		);
1098
1099
		return $image;
1100
	}
1101
1102
	public function cropCopy(int $x, int $y, int $w, int $h): IImage {
1103
		$image = new OC_Image(null, $this->logger, $this->config);
1104
		$image->resource = $this->cropNew($x, $y, $w, $h);
1105
1106
		return $image;
1107
	}
1108
1109
	public function preciseResizeCopy(int $width, int $height): IImage {
1110
		$image = new OC_Image(null, $this->logger, $this->config);
1111
		$image->resource = $this->preciseResizeNew($width, $height);
1112
1113
		return $image;
1114
	}
1115
1116
	public function resizeCopy(int $maxSize): IImage {
1117
		$image = new OC_Image(null, $this->logger, $this->config);
1118
		$image->resource = $this->resizeNew($maxSize);
1119
1120
		return $image;
1121
	}
1122
1123
1124
	/**
1125
	 * Resizes the image preserving ratio, returning a new copy
1126
	 *
1127
	 * @param integer $maxSize The maximum size of either the width or height.
1128
	 * @return bool
1129
	 */
1130
	public function copyResize($maxSize): IImage {
0 ignored issues
show
Unused Code introduced by
The parameter $maxSize is not used and could be removed. ( Ignorable by Annotation )

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

1130
	public function copyResize(/** @scrutinizer ignore-unused */ $maxSize): IImage {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1131
1132
	}
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return OCP\IImage. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
1133
1134
	/**
1135
	 * Destroys the current image and resets the object
1136
	 */
1137
	public function destroy() {
1138
		if ($this->valid()) {
1139
			imagedestroy($this->resource);
1140
		}
1141
		$this->resource = null;
1142
	}
1143
1144
	public function __destruct() {
1145
		$this->destroy();
1146
	}
1147
}
1148
1149
if (!function_exists('imagebmp')) {
1150
	/**
1151
	 * Output a BMP image to either the browser or a file
1152
	 *
1153
	 * @link http://www.ugia.cn/wp-data/imagebmp.php
1154
	 * @author legend <[email protected]>
1155
	 * @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm
1156
	 * @author mgutt <[email protected]>
1157
	 * @version 1.00
1158
	 * @param resource $im
1159
	 * @param string $fileName [optional] <p>The path to save the file to.</p>
1160
	 * @param int $bit [optional] <p>Bit depth, (default is 24).</p>
1161
	 * @param int $compression [optional]
1162
	 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure.
1163
	 */
1164
	function imagebmp($im, $fileName = '', $bit = 24, $compression = 0) {
1165
		if (!in_array($bit, [1, 4, 8, 16, 24, 32])) {
1166
			$bit = 24;
1167
		} elseif ($bit == 32) {
1168
			$bit = 24;
1169
		}
1170
		$bits = pow(2, $bit);
1171
		imagetruecolortopalette($im, true, $bits);
1172
		$width = imagesx($im);
1173
		$height = imagesy($im);
1174
		$colorsNum = imagecolorstotal($im);
1175
		$rgbQuad = '';
1176
		if ($bit <= 8) {
1177
			for ($i = 0; $i < $colorsNum; $i++) {
1178
				$colors = imagecolorsforindex($im, $i);
1179
				$rgbQuad .= chr($colors['blue']) . chr($colors['green']) . chr($colors['red']) . "\0";
1180
			}
1181
			$bmpData = '';
1182
			if ($compression == 0 || $bit < 8) {
1183
				$compression = 0;
1184
				$extra = '';
1185
				$padding = 4 - ceil($width / (8 / $bit)) % 4;
1186
				if ($padding % 4 != 0) {
1187
					$extra = str_repeat("\0", $padding);
1188
				}
1189
				for ($j = $height - 1; $j >= 0; $j--) {
1190
					$i = 0;
1191
					while ($i < $width) {
1192
						$bin = 0;
1193
						$limit = $width - $i < 8 / $bit ? (8 / $bit - $width + $i) * $bit : 0;
1194
						for ($k = 8 - $bit; $k >= $limit; $k -= $bit) {
1195
							$index = imagecolorat($im, $i, $j);
1196
							$bin |= $index << $k;
1197
							$i++;
1198
						}
1199
						$bmpData .= chr($bin);
1200
					}
1201
					$bmpData .= $extra;
1202
				}
1203
			} // RLE8
1204
			elseif ($compression == 1 && $bit == 8) {
1205
				for ($j = $height - 1; $j >= 0; $j--) {
1206
					$lastIndex = "\0";
1207
					$sameNum = 0;
1208
					for ($i = 0; $i <= $width; $i++) {
1209
						$index = imagecolorat($im, $i, $j);
1210
						if ($index !== $lastIndex || $sameNum > 255) {
1211
							if ($sameNum != 0) {
1212
								$bmpData .= chr($sameNum) . chr($lastIndex);
0 ignored issues
show
Bug introduced by
$lastIndex of type string is incompatible with the type integer expected by parameter $ascii 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

1212
								$bmpData .= chr($sameNum) . chr(/** @scrutinizer ignore-type */ $lastIndex);
Loading history...
1213
							}
1214
							$lastIndex = $index;
1215
							$sameNum = 1;
1216
						} else {
1217
							$sameNum++;
1218
						}
1219
					}
1220
					$bmpData .= "\0\0";
1221
				}
1222
				$bmpData .= "\0\1";
1223
			}
1224
			$sizeQuad = strlen($rgbQuad);
1225
			$sizeData = strlen($bmpData);
1226
		} else {
1227
			$extra = '';
1228
			$padding = 4 - ($width * ($bit / 8)) % 4;
1229
			if ($padding % 4 != 0) {
1230
				$extra = str_repeat("\0", $padding);
1231
			}
1232
			$bmpData = '';
1233
			for ($j = $height - 1; $j >= 0; $j--) {
1234
				for ($i = 0; $i < $width; $i++) {
1235
					$index = imagecolorat($im, $i, $j);
1236
					$colors = imagecolorsforindex($im, $index);
1237
					if ($bit == 16) {
1238
						$bin = 0 << $bit;
1239
						$bin |= ($colors['red'] >> 3) << 10;
1240
						$bin |= ($colors['green'] >> 3) << 5;
1241
						$bin |= $colors['blue'] >> 3;
1242
						$bmpData .= pack("v", $bin);
1243
					} else {
1244
						$bmpData .= pack("c*", $colors['blue'], $colors['green'], $colors['red']);
1245
					}
1246
				}
1247
				$bmpData .= $extra;
1248
			}
1249
			$sizeQuad = 0;
1250
			$sizeData = strlen($bmpData);
1251
			$colorsNum = 0;
1252
		}
1253
		$fileHeader = 'BM' . pack('V3', 54 + $sizeQuad + $sizeData, 0, 54 + $sizeQuad);
1254
		$infoHeader = pack('V3v2V*', 0x28, $width, $height, 1, $bit, $compression, $sizeData, 0, 0, $colorsNum, 0);
1255
		if ($fileName != '') {
1256
			$fp = fopen($fileName, 'wb');
1257
			fwrite($fp, $fileHeader . $infoHeader . $rgbQuad . $bmpData);
1258
			fclose($fp);
1259
			return true;
1260
		}
1261
		echo $fileHeader . $infoHeader . $rgbQuad . $bmpData;
1262
		return true;
1263
	}
1264
}
1265
1266
if (!function_exists('exif_imagetype')) {
1267
	/**
1268
	 * Workaround if exif_imagetype does not exist
1269
	 *
1270
	 * @link http://www.php.net/manual/en/function.exif-imagetype.php#80383
1271
	 * @param string $fileName
1272
	 * @return string|boolean
1273
	 */
1274
	function exif_imagetype($fileName) {
1275
		if (($info = getimagesize($fileName)) !== false) {
1276
			return $info[2];
1277
		}
1278
		return false;
1279
	}
1280
}
1281