3 * @file src/Object/Image.php
4 * This file contains the Image class for image processing
6 namespace Friendica\Object;
9 use Friendica\Core\Config;
10 use Friendica\Core\System;
12 use Friendica\Util\Images;
17 * Class to handle images
21 /** @var Imagick|resource */
25 * Put back gd stuff, not everybody have Imagick
37 * @param boolean $type optional, default null
38 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
39 * @throws \ImagickException
41 public function __construct($data, $type = null)
43 $this->imagick = class_exists('Imagick');
44 $this->types = Images::supportedTypes();
45 if (!array_key_exists($type, $this->types)) {
50 if ($this->isImagick() && $this->loadData($data)) {
53 // Failed to load with Imagick, fallback
54 $this->imagick = false;
56 return $this->loadData($data);
64 public function __destruct()
67 if ($this->isImagick()) {
68 $this->image->clear();
69 $this->image->destroy();
72 if (is_resource($this->image)) {
73 imagedestroy($this->image);
81 public function isImagick()
83 return $this->imagick;
87 * @param string $data data
89 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
90 * @throws \ImagickException
92 private function loadData($data)
94 if ($this->isImagick()) {
95 $this->image = new Imagick();
97 $this->image->readImageBlob($data);
98 } catch (Exception $e) {
99 // Imagick couldn't use the data
104 * Setup the image to the format it will be saved to
106 $map = Images::getFormatsMap();
107 $format = $map[$this->type];
108 $this->image->setFormat($format);
110 // Always coalesce, if it is not a multi-frame image it won't hurt anyway
111 $this->image = $this->image->coalesceImages();
114 * setup the compression here, so we'll do it only once
116 switch ($this->getType()) {
118 $quality = Config::get('system', 'png_quality');
119 if ((! $quality) || ($quality > 9)) {
120 $quality = PNG_QUALITY;
123 * From http://www.imagemagick.org/script/command-line-options.php#quality:
125 * 'For the MNG and PNG image formats, the quality value sets
126 * the zlib compression level (quality / 10) and filter-type (quality % 10).
127 * The default PNG "quality" is 75, which means compression level 7 with adaptive PNG filtering,
128 * unless the image has a color map, in which case it means compression level 7 with no PNG filtering'
130 $quality = $quality * 10;
131 $this->image->setCompressionQuality($quality);
134 $quality = Config::get('system', 'jpeg_quality');
135 if ((! $quality) || ($quality > 100)) {
136 $quality = JPEG_QUALITY;
138 $this->image->setCompressionQuality($quality);
141 // The 'width' and 'height' properties are only used by non-Imagick routines.
142 $this->width = $this->image->getImageWidth();
143 $this->height = $this->image->getImageHeight();
149 $this->valid = false;
150 $this->image = @imagecreatefromstring($data);
151 if ($this->image !== false) {
152 $this->width = imagesx($this->image);
153 $this->height = imagesy($this->image);
155 imagealphablending($this->image, false);
156 imagesavealpha($this->image, true);
167 public function isValid()
169 if ($this->isImagick()) {
170 return ($this->image !== false);
178 public function getWidth()
180 if (!$this->isValid()) {
184 if ($this->isImagick()) {
185 return $this->image->getImageWidth();
193 public function getHeight()
195 if (!$this->isValid()) {
199 if ($this->isImagick()) {
200 return $this->image->getImageHeight();
202 return $this->height;
208 public function getImage()
210 if (!$this->isValid()) {
214 if ($this->isImagick()) {
216 $this->image = $this->image->deconstructImages();
225 public function getType()
227 if (!$this->isValid()) {
237 public function getExt()
239 if (!$this->isValid()) {
243 return $this->types[$this->getType()];
247 * @param integer $max max dimension
250 public function scaleDown($max)
252 if (!$this->isValid()) {
256 $width = $this->getWidth();
257 $height = $this->getHeight();
259 if ((! $width)|| (! $height)) {
263 if ($width > $max && $height > $max) {
264 // very tall image (greater than 16:9)
265 // constrain the width - let the height float.
267 if ((($height * 9) / 16) > $width) {
269 $dest_height = intval(($height * $max) / $width);
270 } elseif ($width > $height) {
271 // else constrain both dimensions
273 $dest_height = intval(($height * $max) / $width);
275 $dest_width = intval(($width * $max) / $height);
281 $dest_height = intval(($height * $max) / $width);
283 if ($height > $max) {
284 // very tall image (greater than 16:9)
285 // but width is OK - don't do anything
287 if ((($height * 9) / 16) > $width) {
288 $dest_width = $width;
289 $dest_height = $height;
291 $dest_width = intval(($width * $max) / $height);
295 $dest_width = $width;
296 $dest_height = $height;
301 return $this->scale($dest_width, $dest_height);
305 * @param integer $degrees degrees to rotate image
308 public function rotate($degrees)
310 if (!$this->isValid()) {
314 if ($this->isImagick()) {
315 $this->image->setFirstIterator();
317 $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate()
318 } while ($this->image->nextImage());
322 // if script dies at this point check memory_limit setting in php.ini
323 $this->image = imagerotate($this->image, $degrees, 0);
324 $this->width = imagesx($this->image);
325 $this->height = imagesy($this->image);
329 * @param boolean $horiz optional, default true
330 * @param boolean $vert optional, default false
333 public function flip($horiz = true, $vert = false)
335 if (!$this->isValid()) {
339 if ($this->isImagick()) {
340 $this->image->setFirstIterator();
343 $this->image->flipImage();
346 $this->image->flopImage();
348 } while ($this->image->nextImage());
352 $w = imagesx($this->image);
353 $h = imagesy($this->image);
354 $flipped = imagecreate($w, $h);
356 for ($x = 0; $x < $w; $x++) {
357 imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
361 for ($y = 0; $y < $h; $y++) {
362 imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
365 $this->image = $flipped;
369 * @param string $filename filename
372 public function orient($filename)
374 if ($this->isImagick()) {
375 // based off comment on http://php.net/manual/en/imagick.getimageorientation.php
376 $orientation = $this->image->getImageOrientation();
377 switch ($orientation) {
378 case Imagick::ORIENTATION_BOTTOMRIGHT:
379 $this->image->rotateimage("#000", 180);
381 case Imagick::ORIENTATION_RIGHTTOP:
382 $this->image->rotateimage("#000", 90);
384 case Imagick::ORIENTATION_LEFTBOTTOM:
385 $this->image->rotateimage("#000", -90);
389 $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
392 // based off comment on http://php.net/manual/en/function.imagerotate.php
394 if (!$this->isValid()) {
398 if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) {
402 $exif = @exif_read_data($filename, null, true);
407 $ort = isset($exif['IFD0']['Orientation']) ? $exif['IFD0']['Orientation'] : 1;
413 case 2: // horizontal flip
417 case 3: // 180 rotate left
421 case 4: // vertical flip
422 $this->flip(false, true);
425 case 5: // vertical flip + 90 rotate right
426 $this->flip(false, true);
430 case 6: // 90 rotate right
434 case 7: // horizontal flip + 90 rotate right
439 case 8: // 90 rotate left
444 // Logger::log('exif: ' . print_r($exif,true));
449 * @param integer $min minimum dimension
452 public function scaleUp($min)
454 if (!$this->isValid()) {
458 $width = $this->getWidth();
459 $height = $this->getHeight();
461 if ((!$width)|| (!$height)) {
465 if ($width < $min && $height < $min) {
466 if ($width > $height) {
468 $dest_height = intval(($height * $min) / $width);
470 $dest_width = intval(($width * $min) / $height);
476 $dest_height = intval(($height * $min) / $width);
478 if ($height < $min) {
479 $dest_width = intval(($width * $min) / $height);
482 $dest_width = $width;
483 $dest_height = $height;
488 return $this->scale($dest_width, $dest_height);
492 * @param integer $dim dimension
495 public function scaleToSquare($dim)
497 if (!$this->isValid()) {
501 return $this->scale($dim, $dim);
505 * Scale image to target dimensions
507 * @param int $dest_width
508 * @param int $dest_height
511 private function scale($dest_width, $dest_height)
513 if (!$this->isValid()) {
517 if ($this->isImagick()) {
519 * If it is not animated, there will be only one iteration here,
520 * so don't bother checking
522 // Don't forget to go back to the first frame
523 $this->image->setFirstIterator();
525 // FIXME - implement horizontal bias for scaling as in following GD functions
526 // to allow very tall images to be constrained only horizontally.
527 $this->image->scaleImage($dest_width, $dest_height);
528 } while ($this->image->nextImage());
530 // These may not be necessary anymore
531 $this->width = $this->image->getImageWidth();
532 $this->height = $this->image->getImageHeight();
534 $dest = imagecreatetruecolor($dest_width, $dest_height);
535 imagealphablending($dest, false);
536 imagesavealpha($dest, true);
538 if ($this->type=='image/png') {
539 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
542 imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $this->width, $this->height);
545 imagedestroy($this->image);
548 $this->image = $dest;
549 $this->width = imagesx($this->image);
550 $this->height = imagesy($this->image);
557 * @param integer $max maximum
558 * @param integer $x x coordinate
559 * @param integer $y y coordinate
560 * @param integer $w width
561 * @param integer $h height
564 public function crop($max, $x, $y, $w, $h)
566 if (!$this->isValid()) {
570 if ($this->isImagick()) {
571 $this->image->setFirstIterator();
573 $this->image->cropImage($w, $h, $x, $y);
575 * We need to remove the canva,
576 * or the image is not resized to the crop:
577 * http://php.net/manual/en/imagick.cropimage.php#97232
579 $this->image->setImagePage(0, 0, 0, 0);
580 } while ($this->image->nextImage());
581 return $this->scaleDown($max);
584 $dest = imagecreatetruecolor($max, $max);
585 imagealphablending($dest, false);
586 imagesavealpha($dest, true);
587 if ($this->type=='image/png') {
588 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
590 imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
592 imagedestroy($this->image);
594 $this->image = $dest;
595 $this->width = imagesx($this->image);
596 $this->height = imagesy($this->image);
600 * @param string $path file path
602 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
604 public function saveToFilePath($path)
606 if (!$this->isValid()) {
610 $string = $this->asString();
612 $stamp1 = microtime(true);
613 file_put_contents($path, $string);
614 DI::profiler()->saveTimestamp($stamp1, "file", System::callstack());
618 * Magic method allowing string casting of an Image object
620 * Ex: $data = $Image->asString();
622 * $data = (string) $Image;
625 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
627 public function __toString() {
628 return $this->asString();
633 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
635 public function asString()
637 if (!$this->isValid()) {
641 if ($this->isImagick()) {
643 $this->image = $this->image->deconstructImages();
644 $string = $this->image->getImagesBlob();
650 // Enable interlacing
651 imageinterlace($this->image, true);
653 switch ($this->getType()) {
655 $quality = Config::get('system', 'png_quality');
656 if ((!$quality) || ($quality > 9)) {
657 $quality = PNG_QUALITY;
659 imagepng($this->image, null, $quality);
662 $quality = Config::get('system', 'jpeg_quality');
663 if ((!$quality) || ($quality > 100)) {
664 $quality = JPEG_QUALITY;
666 imagejpeg($this->image, null, $quality);
668 $string = ob_get_contents();
675 * supported mimetypes and corresponding file extensions
678 * @deprecated in version 2019.12 please use Util\Images::supportedTypes() instead.
680 public static function supportedTypes()
682 return Images::supportedTypes();
686 * Maps Mime types to Imagick formats
688 * @return array With with image formats (mime type as key)
689 * @deprecated in version 2019.12 please use Util\Images::getFormatsMap() instead.
691 public static function getFormatsMap()
693 return Images::getFormatsMap();
697 * Guess image mimetype from filename or from Content-Type header
699 * @param string $filename Image filename
700 * @param boolean $fromcurl Check Content-Type header from curl request
701 * @param string $header passed headers to take into account
703 * @return string|null
705 * @deprecated in version 2019.12 please use Util\Images::guessType() instead.
707 public static function guessType($filename, $fromcurl = false, $header = '')
709 return Images::guessType($filename, $fromcurl, $header);
713 * @param string $url url
715 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
716 * @deprecated in version 2019.12 please use Util\Images::getInfoFromURLCached() instead.
718 public static function getInfoFromURL($url)
720 return Images::getInfoFromURLCached($url);
724 * @param integer $width width
725 * @param integer $height height
726 * @param integer $max max
728 * @deprecated in version 2019.12 please use Util\Images::getScalingDimensions() instead.
730 public static function getScalingDimensions($width, $height, $max)
732 return Images::getScalingDimensions($width, $height, $max);