2010-07-02 01:48:07 +02:00
< ? php
2016-11-04 16:44:49 +01:00
/**
2024-01-02 21:57:26 +01:00
* @ copyright Copyright ( C ) 2010 - 2024 , the Friendica project
2020-02-09 16:18:46 +01:00
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*
2016-11-04 16:44:49 +01:00
*/
2020-02-09 16:18:46 +01:00
2017-11-29 13:52:27 +01:00
namespace Friendica\Object ;
2016-11-04 16:44:49 +01:00
2019-02-16 23:11:30 +01:00
use Exception ;
2019-12-15 23:50:35 +01:00
use Friendica\DI ;
2019-10-18 03:26:15 +02:00
use Friendica\Util\Images ;
2017-11-29 23:29:11 +01:00
use Imagick ;
2022-12-05 07:53:19 +01:00
use ImagickDraw ;
2017-11-29 23:29:11 +01:00
use ImagickPixel ;
2022-12-04 14:29:21 +01:00
use GDImage ;
use kornrunner\Blurhash\Blurhash ;
2017-04-30 06:07:00 +02:00
2017-11-29 18:17:12 +01:00
/**
2017-12-07 14:56:11 +01:00
* Class to handle images
2017-11-29 18:17:12 +01:00
*/
2017-12-07 14:56:11 +01:00
class Image
2017-11-29 18:17:12 +01:00
{
2022-12-04 14:29:21 +01:00
/** @var GDImage|Imagick|resource */
2016-11-04 16:44:49 +01:00
private $image ;
/*
* Put back gd stuff , not everybody have Imagick
*/
private $imagick ;
private $width ;
private $height ;
private $valid ;
2024-02-25 09:52:52 +01:00
private $outputType ;
private $originType ;
2024-02-17 07:45:41 +01:00
private $filename ;
2016-11-04 16:44:49 +01:00
2017-11-29 18:17:12 +01:00
/**
2020-01-19 07:05:23 +01:00
* Constructor
2022-06-21 23:34:14 +02:00
*
2024-02-17 07:45:41 +01:00
* @ param string $data Image data
* @ param string $type optional , default ''
2024-02-18 04:27:37 +01:00
* @ param string $filename optional , default ''
2024-02-17 07:45:41 +01:00
* @ param string $imagick optional , default 'true'
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2017-11-29 18:17:12 +01:00
*/
2024-02-17 07:45:41 +01:00
public function __construct ( string $data , string $type = '' , string $filename = '' , bool $imagick = true )
2017-11-29 13:52:27 +01:00
{
2024-02-17 07:45:41 +01:00
$this -> filename = $filename ;
$type = Images :: addMimeTypeByDataIfInvalid ( $type , $data );
$type = Images :: addMimeTypeByExtensionIfInvalid ( $type , $filename );
if ( Images :: isSupportedMimeType ( $type )) {
2024-02-25 09:52:52 +01:00
$this -> originType = $this -> outputType = Images :: getImageTypeByMimeType ( $type );
2024-02-17 15:46:48 +01:00
} elseif (( $type == '' ) || substr ( $type , 0 , 6 ) == 'image/' || substr ( $type , 0 , 12 ) == ' application/' ) {
2024-02-25 09:52:52 +01:00
$this -> originType = IMAGETYPE_UNKNOWN ;
$this -> outputType = IMAGETYPE_WEBP ;
2024-02-17 07:45:41 +01:00
DI :: logger () -> debug ( 'Unhandled image mime type, use WebP instead' , [ 'type' => $type , 'filename' => $filename , 'size' => strlen ( $data )]);
} else {
DI :: logger () -> debug ( 'Unhandled mime type' , [ 'type' => $type , 'filename' => $filename , 'size' => strlen ( $data )]);
$this -> valid = false ;
return ;
2016-11-04 16:44:49 +01:00
}
2024-02-17 07:45:41 +01:00
$this -> imagick = $imagick && $this -> useImagick ( $data );
2012-07-21 17:17:19 +02:00
2022-12-05 07:53:19 +01:00
if ( $this -> isImagick () && ( empty ( $data ) || $this -> loadData ( $data ))) {
2022-12-20 07:22:11 +01:00
$this -> valid = ! empty ( $data );
2022-06-21 23:34:14 +02:00
return ;
2016-11-04 19:26:28 +01:00
} else {
// Failed to load with Imagick, fallback
$this -> imagick = false ;
}
2022-06-21 23:34:14 +02:00
$this -> loadData ( $data );
2014-06-16 21:49:45 +02:00
}
2024-02-17 07:45:41 +01:00
/**
* Check if Imagick will be used
*
* @ param string $data
* @ return boolean
*/
private function useImagick ( string $data ) : bool
{
if ( ! class_exists ( 'Imagick' )) {
return false ;
}
2024-02-25 09:52:52 +01:00
if ( $this -> outputType == IMAGETYPE_PNG ) {
return true ;
}
if ( $this -> originType == IMAGETYPE_GIF ) {
2024-02-18 04:27:37 +01:00
$count = preg_match_all ( " # \\ x00 \\ x21 \\ xF9 \\ x04. { 4} \\ x00[ \\ x2C \\ x21]#s " , $data );
2024-02-17 07:45:41 +01:00
return ( $count > 0 );
}
2024-02-25 09:52:52 +01:00
return (( $this -> originType == IMAGETYPE_WEBP ) && $this -> isAnimatedWebP ( substr ( $data , 0 , 90 )));
2024-02-17 07:45:41 +01:00
}
/**
* Detect if a WebP image is animated .
* @ see https :// www . php . net / manual / en / function . imagecreatefromwebp . php #126269
* @ param string $data
* @ return boolean
*/
private function isAnimatedWebP ( string $data ) {
$header_format = 'A4Riff/I1Filesize/A4Webp/A4Vp/A74Chunk' ;
2024-02-17 15:46:48 +01:00
$header = @ unpack ( $header_format , $data );
2024-02-17 07:45:41 +01:00
if ( ! isset ( $header [ 'Riff' ]) || strtoupper ( $header [ 'Riff' ]) !== 'RIFF' ) {
return false ;
}
if ( ! isset ( $header [ 'Webp' ]) || strtoupper ( $header [ 'Webp' ]) !== 'WEBP' ) {
return false ;
}
if ( ! isset ( $header [ 'Vp' ]) || strpos ( strtoupper ( $header [ 'Vp' ]), 'VP8' ) === false ) {
return false ;
}
return strpos ( strtoupper ( $header [ 'Chunk' ]), 'ANIM' ) !== false || strpos ( strtoupper ( $header [ 'Chunk' ]), 'ANMF' ) !== false ;
}
2017-11-29 18:17:12 +01:00
/**
2020-01-19 07:05:23 +01:00
* Destructor
2020-01-19 10:51:37 +01:00
*
2017-11-29 18:17:12 +01:00
* @ return void
*/
public function __destruct ()
{
2016-11-04 16:44:49 +01:00
if ( $this -> image ) {
2017-11-29 18:17:12 +01:00
if ( $this -> isImagick ()) {
2016-11-04 16:44:49 +01:00
$this -> image -> clear ();
$this -> image -> destroy ();
return ;
}
2017-05-01 22:16:22 +02:00
if ( is_resource ( $this -> image )) {
2017-05-01 16:38:39 +02:00
imagedestroy ( $this -> image );
2017-05-01 22:16:22 +02:00
}
2016-11-04 16:44:49 +01:00
}
}
2017-11-29 18:17:12 +01:00
/**
* @ return boolean
*/
public function isImagick ()
2017-11-29 13:52:27 +01:00
{
2016-11-04 16:44:49 +01:00
return $this -> imagick ;
2014-06-16 21:49:45 +02:00
}
2012-09-12 11:55:09 +02:00
2017-11-29 18:17:12 +01:00
/**
2022-06-21 23:34:14 +02:00
* Loads image data into handler class
*
* @ param string $data Image data
* @ return boolean Success
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2017-11-29 18:17:12 +01:00
*/
2022-06-21 23:34:14 +02:00
private function loadData ( string $data ) : bool
2017-11-29 18:17:12 +01:00
{
if ( $this -> isImagick ()) {
2016-11-04 19:26:28 +01:00
$this -> image = new Imagick ();
2016-11-04 16:44:49 +01:00
try {
2012-09-12 11:55:09 +02:00
$this -> image -> readImageBlob ( $data );
2016-11-04 19:26:28 +01:00
} catch ( Exception $e ) {
2012-09-12 11:55:09 +02:00
// Imagick couldn't use the data
2024-02-17 07:45:41 +01:00
DI :: logger () -> debug ( 'Error during readImageBlob' , [ 'message' => $e -> getMessage (), 'code' => $e -> getCode (), 'trace' => $e -> getTraceAsString (), 'previous' => $e -> getPrevious (), 'file' => $this -> filename ]);
2012-09-12 11:55:09 +02:00
return false ;
}
2012-07-22 13:43:46 +02:00
2016-11-04 16:44:49 +01:00
/*
* Setup the image to the format it will be saved to
*/
2024-02-25 09:52:52 +01:00
$this -> image -> setFormat ( Images :: getImagickFormatByImageType ( $this -> outputType ));
2016-11-04 16:44:49 +01:00
// Always coalesce, if it is not a multi-frame image it won't hurt anyway
2020-10-02 22:58:14 +02:00
try {
$this -> image = $this -> image -> coalesceImages ();
} catch ( Exception $e ) {
2024-02-17 07:45:41 +01:00
DI :: logger () -> debug ( 'Error during coalesceImages' , [ 'message' => $e -> getMessage (), 'code' => $e -> getCode (), 'trace' => $e -> getTraceAsString (), 'previous' => $e -> getPrevious (), 'file' => $this -> filename ]);
2020-10-02 22:58:14 +02:00
return false ;
}
2016-11-04 16:44:49 +01:00
/*
* setup the compression here , so we ' ll do it only once
*/
2024-02-17 07:45:41 +01:00
switch ( $this -> getImageType ()) {
case IMAGETYPE_PNG :
2020-01-19 21:21:13 +01:00
$quality = DI :: config () -> get ( 'system' , 'png_quality' );
2016-11-04 16:44:49 +01:00
/*
* From http :// www . imagemagick . org / script / command - line - options . php #quality:
*
* ' For the MNG and PNG image formats , the quality value sets
* the zlib compression level ( quality / 10 ) and filter - type ( quality % 10 ) .
* The default PNG " quality " is 75 , which means compression level 7 with adaptive PNG filtering ,
* unless the image has a color map , in which case it means compression level 7 with no PNG filtering '
*/
$quality = $quality * 10 ;
2024-02-17 07:45:41 +01:00
$this -> image -> setImageCompressionQuality ( $quality );
2016-11-04 16:44:49 +01:00
break ;
2022-06-21 23:34:14 +02:00
2024-02-17 07:45:41 +01:00
case IMAGETYPE_JPEG :
2020-01-19 21:21:13 +01:00
$quality = DI :: config () -> get ( 'system' , 'jpeg_quality' );
2024-02-17 07:45:41 +01:00
$this -> image -> setImageCompressionQuality ( $quality );
2016-11-04 16:44:49 +01:00
}
2012-07-21 17:17:19 +02:00
2012-10-09 17:35:32 +02:00
$this -> width = $this -> image -> getImageWidth ();
2012-09-12 11:55:09 +02:00
$this -> height = $this -> image -> getImageHeight ();
2022-12-20 07:22:11 +01:00
$this -> valid = ! empty ( $this -> image );
2012-07-21 17:17:19 +02:00
2022-12-20 07:22:11 +01:00
return $this -> valid ;
2012-09-12 11:55:09 +02:00
}
2012-07-22 13:43:46 +02:00
2012-09-12 11:55:09 +02:00
$this -> valid = false ;
2022-05-21 18:44:03 +02:00
try {
$this -> image = @ imagecreatefromstring ( $data );
if ( $this -> image !== false ) {
$this -> width = imagesx ( $this -> image );
$this -> height = imagesy ( $this -> image );
$this -> valid = true ;
imagealphablending ( $this -> image , false );
imagesavealpha ( $this -> image , true );
2024-02-05 19:16:47 +01:00
imageinterlace ( $this -> image , true );
2022-05-21 18:44:03 +02:00
return true ;
}
} catch ( \Throwable $error ) {
/** @see https://github.com/php/doc-en/commit/d09a881a8e9059d11e756ee59d75bf404d6941ed */
if ( strstr ( $error -> getMessage (), " gd-webp cannot allocate temporary buffer " )) {
2024-02-17 07:45:41 +01:00
DI :: logger () -> notice ( 'Image is probably animated and therefore unsupported' , [ 'message' => $error -> getMessage (), 'code' => $error -> getCode (), 'trace' => $error -> getTraceAsString (), 'file' => $this -> filename ]);
2022-05-21 18:44:03 +02:00
} else {
2024-02-17 07:45:41 +01:00
DI :: logger () -> warning ( 'Unexpected throwable.' , [ 'message' => $error -> getMessage (), 'code' => $error -> getCode (), 'trace' => $error -> getTraceAsString (), 'file' => $this -> filename ]);
2022-05-21 18:44:03 +02:00
}
2012-09-12 11:55:09 +02:00
}
2016-11-04 16:44:49 +01:00
2012-09-12 11:55:09 +02:00
return false ;
}
2012-07-22 17:09:18 +02:00
2017-11-29 18:17:12 +01:00
/**
* @ return boolean
*/
2022-06-21 23:34:14 +02:00
public function isValid () : bool
2017-11-29 18:17:12 +01:00
{
if ( $this -> isImagick ()) {
2023-01-09 16:29:56 +01:00
return ! empty ( $this -> image );
2016-11-04 16:44:49 +01:00
}
return $this -> valid ;
2014-06-16 21:49:45 +02:00
}
2012-07-21 17:17:19 +02:00
2017-11-29 18:17:12 +01:00
/**
* @ return mixed
*/
public function getWidth ()
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
return $this -> width ;
}
2012-07-21 17:17:19 +02:00
2017-11-29 18:17:12 +01:00
/**
* @ return mixed
*/
public function getHeight ()
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
return $this -> height ;
}
2012-07-21 17:17:19 +02:00
2017-11-29 18:17:12 +01:00
/**
* @ return mixed
*/
public function getImage ()
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2012-07-21 17:17:19 +02:00
2017-11-29 18:17:12 +01:00
if ( $this -> isImagick ()) {
2021-04-12 14:37:11 +02:00
try {
/* Clean it */
$this -> image = $this -> image -> deconstructImages ();
return $this -> image ;
} catch ( Exception $e ) {
return false ;
}
2016-11-04 16:44:49 +01:00
}
return $this -> image ;
}
2012-07-22 13:43:46 +02:00
2017-11-29 18:17:12 +01:00
/**
* @ return mixed
*/
public function getType ()
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2012-07-22 13:43:46 +02:00
2024-02-25 09:52:52 +01:00
return image_type_to_mime_type ( $this -> outputType );
2024-02-17 07:45:41 +01:00
}
/**
* @ return mixed
*/
public function getImageType ()
{
if ( ! $this -> isValid ()) {
return false ;
}
2024-02-25 09:52:52 +01:00
return $this -> outputType ;
2016-11-04 16:44:49 +01:00
}
2012-07-22 13:43:46 +02:00
2017-11-29 18:17:12 +01:00
/**
* @ return mixed
*/
public function getExt ()
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2024-02-25 09:52:52 +01:00
return Images :: getExtensionByImageType ( $this -> outputType );
2016-11-04 16:44:49 +01:00
}
2017-11-29 18:17:12 +01:00
/**
2022-06-21 23:34:14 +02:00
* Scales image down
*
2017-11-29 18:17:12 +01:00
* @ param integer $max max dimension
* @ return mixed
*/
2022-06-21 23:34:14 +02:00
public function scaleDown ( int $max )
2017-11-29 18:17:12 +01:00
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
$width = $this -> getWidth ();
$height = $this -> getHeight ();
2022-12-28 16:43:47 +01:00
$scale = Images :: getScalingDimensions ( $width , $height , $max );
2022-12-28 16:42:38 +01:00
if ( $scale ) {
return $this -> scale ( $scale [ 'width' ], $scale [ 'height' ]);
} else {
2016-11-04 16:44:49 +01:00
return false ;
}
2014-06-16 21:49:45 +02:00
}
2017-11-29 18:17:12 +01:00
/**
2022-06-21 23:34:14 +02:00
* Rotates image
*
2017-11-29 18:17:12 +01:00
* @ param integer $degrees degrees to rotate image
* @ return mixed
*/
2022-06-21 23:34:14 +02:00
public function rotate ( int $degrees )
2017-11-29 18:17:12 +01:00
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2014-06-16 21:49:45 +02:00
2017-11-29 18:17:12 +01:00
if ( $this -> isImagick ()) {
2016-11-04 16:44:49 +01:00
$this -> image -> setFirstIterator ();
do {
$this -> image -> rotateImage ( new ImagickPixel (), - $degrees ); // ImageMagick rotates in the opposite direction of imagerotate()
} while ( $this -> image -> nextImage ());
2022-12-20 07:22:11 +01:00
$this -> width = $this -> image -> getImageWidth ();
$this -> height = $this -> image -> getImageHeight ();
2016-11-04 16:44:49 +01:00
return ;
}
2017-05-01 16:38:39 +02:00
// if script dies at this point check memory_limit setting in php.ini
2017-11-29 18:17:12 +01:00
$this -> image = imagerotate ( $this -> image , $degrees , 0 );
2016-11-04 16:44:49 +01:00
$this -> width = imagesx ( $this -> image );
$this -> height = imagesy ( $this -> image );
2014-06-16 21:49:45 +02:00
}
2016-11-04 16:44:49 +01:00
2017-11-29 18:17:12 +01:00
/**
2022-06-21 23:34:14 +02:00
* Flips image
*
2017-11-29 18:17:12 +01:00
* @ param boolean $horiz optional , default true
* @ param boolean $vert optional , default false
* @ return mixed
*/
2022-06-21 23:34:14 +02:00
public function flip ( bool $horiz = true , bool $vert = false )
2017-11-29 18:17:12 +01:00
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2017-11-29 18:17:12 +01:00
if ( $this -> isImagick ()) {
2016-11-04 16:44:49 +01:00
$this -> image -> setFirstIterator ();
do {
if ( $horiz ) {
$this -> image -> flipImage ();
}
if ( $vert ) {
$this -> image -> flopImage ();
}
} while ( $this -> image -> nextImage ());
return ;
}
$w = imagesx ( $this -> image );
$h = imagesy ( $this -> image );
$flipped = imagecreate ( $w , $h );
if ( $horiz ) {
for ( $x = 0 ; $x < $w ; $x ++ ) {
imagecopy ( $flipped , $this -> image , $x , 0 , $w - $x - 1 , 0 , 1 , $h );
}
}
if ( $vert ) {
for ( $y = 0 ; $y < $h ; $y ++ ) {
imagecopy ( $flipped , $this -> image , 0 , $y , 0 , $h - $y - 1 , $w , 1 );
}
}
$this -> image = $flipped ;
2015-09-30 00:19:54 +02:00
}
2012-07-21 17:17:19 +02:00
2017-11-29 18:17:12 +01:00
/**
2022-06-21 23:34:14 +02:00
* Fixes orientation and maybe returns EXIF data ( ? )
*
* @ param string $filename Filename
2017-11-29 18:17:12 +01:00
* @ return mixed
*/
2022-06-21 23:34:14 +02:00
public function orient ( string $filename )
2017-11-29 18:17:12 +01:00
{
if ( $this -> isImagick ()) {
2016-11-04 16:44:49 +01:00
// based off comment on http://php.net/manual/en/imagick.getimageorientation.php
$orientation = $this -> image -> getImageOrientation ();
switch ( $orientation ) {
2017-11-29 23:29:11 +01:00
case Imagick :: ORIENTATION_BOTTOMRIGHT :
2022-12-28 23:19:53 +01:00
$this -> rotate ( 180 );
2017-11-29 18:17:12 +01:00
break ;
2017-11-29 23:29:11 +01:00
case Imagick :: ORIENTATION_RIGHTTOP :
2022-12-28 23:19:48 +01:00
$this -> rotate ( - 90 );
2017-11-29 18:17:12 +01:00
break ;
2017-11-29 23:29:11 +01:00
case Imagick :: ORIENTATION_LEFTBOTTOM :
2022-12-28 21:43:34 +01:00
$this -> rotate ( 90 );
2017-11-29 18:17:12 +01:00
break ;
2016-11-04 16:44:49 +01:00
}
2012-07-21 17:17:19 +02:00
2017-11-29 23:29:11 +01:00
$this -> image -> setImageOrientation ( Imagick :: ORIENTATION_TOPLEFT );
2016-11-04 16:44:49 +01:00
return true ;
}
// based off comment on http://php.net/manual/en/function.imagerotate.php
2017-11-29 18:17:12 +01:00
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2024-02-17 07:45:41 +01:00
if (( ! function_exists ( 'exif_read_data' )) || ( $this -> getImageType () !== IMAGETYPE_JPEG )) {
2016-11-04 16:44:49 +01:00
return ;
}
2017-11-29 18:17:12 +01:00
$exif = @ exif_read_data ( $filename , null , true );
2016-11-04 19:26:28 +01:00
if ( ! $exif ) {
2016-11-04 16:44:49 +01:00
return ;
}
2012-09-05 13:02:30 +02:00
2019-03-09 04:09:41 +01:00
$ort = isset ( $exif [ 'IFD0' ][ 'Orientation' ]) ? $exif [ 'IFD0' ][ 'Orientation' ] : 1 ;
2012-09-05 13:02:30 +02:00
2017-11-29 18:17:12 +01:00
switch ( $ort ) {
2016-11-04 16:44:49 +01:00
case 1 : // nothing
break ;
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
case 2 : // horizontal flip
$this -> flip ();
break ;
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
case 3 : // 180 rotate left
$this -> rotate ( 180 );
break ;
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
case 4 : // vertical flip
$this -> flip ( false , true );
break ;
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
case 5 : // vertical flip + 90 rotate right
$this -> flip ( false , true );
$this -> rotate ( - 90 );
break ;
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
case 6 : // 90 rotate right
$this -> rotate ( - 90 );
break ;
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
case 7 : // horizontal flip + 90 rotate right
$this -> flip ();
$this -> rotate ( - 90 );
break ;
case 8 : // 90 rotate left
$this -> rotate ( 90 );
break ;
}
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
return $exif ;
2014-06-16 21:49:45 +02:00
}
2015-09-30 00:19:54 +02:00
2017-11-29 18:17:12 +01:00
/**
2022-06-21 23:34:14 +02:00
* Rescales image to minimum size
*
* @ param integer $min Minimum dimension
2017-11-29 18:17:12 +01:00
* @ return mixed
*/
2022-06-21 23:34:14 +02:00
public function scaleUp ( int $min )
2017-11-29 18:17:12 +01:00
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2012-07-21 17:17:19 +02:00
2016-11-04 16:44:49 +01:00
$width = $this -> getWidth ();
$height = $this -> getHeight ();
2014-06-16 21:49:45 +02:00
2016-11-04 19:26:28 +01:00
if (( ! $width ) || ( ! $height )) {
2016-11-04 16:44:49 +01:00
return false ;
}
2014-06-16 21:49:45 +02:00
2016-11-04 16:44:49 +01:00
if ( $width < $min && $height < $min ) {
if ( $width > $height ) {
$dest_width = $min ;
2016-11-04 19:26:28 +01:00
$dest_height = intval (( $height * $min ) / $width );
2016-11-04 16:44:49 +01:00
} else {
2016-11-04 19:26:28 +01:00
$dest_width = intval (( $width * $min ) / $height );
2016-11-04 16:44:49 +01:00
$dest_height = $min ;
}
} else {
2016-11-04 19:26:28 +01:00
if ( $width < $min ) {
2016-11-04 16:44:49 +01:00
$dest_width = $min ;
2016-11-04 19:26:28 +01:00
$dest_height = intval (( $height * $min ) / $width );
2016-11-04 16:44:49 +01:00
} else {
2016-11-04 19:26:28 +01:00
if ( $height < $min ) {
$dest_width = intval (( $width * $min ) / $height );
2016-11-04 16:44:49 +01:00
$dest_height = $min ;
} else {
$dest_width = $width ;
$dest_height = $height ;
}
}
}
2014-06-16 21:49:45 +02:00
2017-12-17 21:35:07 +01:00
return $this -> scale ( $dest_width , $dest_height );
2014-06-16 21:49:45 +02:00
}
2017-11-29 18:17:12 +01:00
/**
2022-06-21 23:34:14 +02:00
* Scales image to square
*
* @ param integer $dim Dimension
2017-11-29 18:17:12 +01:00
* @ return mixed
*/
2022-06-21 23:34:14 +02:00
public function scaleToSquare ( int $dim )
2017-11-29 18:17:12 +01:00
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2017-12-17 21:35:07 +01:00
return $this -> scale ( $dim , $dim );
}
/**
2020-01-19 07:05:23 +01:00
* Scale image to target dimensions
2017-12-17 21:35:07 +01:00
*
2022-06-21 23:34:14 +02:00
* @ param int $dest_width Destination width
* @ param int $dest_height Destination height
* @ return boolean Success
2017-12-17 21:35:07 +01:00
*/
2022-06-21 23:34:14 +02:00
private function scale ( int $dest_width , int $dest_height ) : bool
2017-12-17 21:35:07 +01:00
{
if ( ! $this -> isValid ()) {
return false ;
}
2017-11-29 18:17:12 +01:00
if ( $this -> isImagick ()) {
2017-12-17 21:35:07 +01:00
/*
* If it is not animated , there will be only one iteration here ,
* so don ' t bother checking
*/
// Don't forget to go back to the first frame
2016-11-04 16:44:49 +01:00
$this -> image -> setFirstIterator ();
do {
2017-12-17 21:35:07 +01:00
// FIXME - implement horizontal bias for scaling as in following GD functions
// to allow very tall images to be constrained only horizontally.
2021-03-23 21:01:32 +01:00
try {
$this -> image -> scaleImage ( $dest_width , $dest_height );
} catch ( Exception $e ) {
// Imagick couldn't use the data
return false ;
}
2016-11-04 16:44:49 +01:00
} while ( $this -> image -> nextImage ());
2017-12-17 21:35:07 +01:00
$this -> width = $this -> image -> getImageWidth ();
$this -> height = $this -> image -> getImageHeight ();
} else {
$dest = imagecreatetruecolor ( $dest_width , $dest_height );
imagealphablending ( $dest , false );
imagesavealpha ( $dest , true );
2024-02-25 09:52:52 +01:00
if ( $this -> outputType == IMAGETYPE_PNG ) {
2017-12-17 21:35:07 +01:00
imagefill ( $dest , 0 , 0 , imagecolorallocatealpha ( $dest , 0 , 0 , 0 , 127 )); // fill with alpha
}
imagecopyresampled ( $dest , $this -> image , 0 , 0 , 0 , 0 , $dest_width , $dest_height , $this -> width , $this -> height );
if ( $this -> image ) {
imagedestroy ( $this -> image );
}
$this -> image = $dest ;
$this -> width = imagesx ( $this -> image );
$this -> height = imagesy ( $this -> image );
2016-11-04 16:44:49 +01:00
}
2017-12-17 21:35:07 +01:00
return true ;
2016-11-04 16:44:49 +01:00
}
2021-03-26 07:56:08 +01:00
/**
* Convert a GIF to a PNG to make it static
2022-06-21 23:34:14 +02:00
*
* @ return void
2021-03-26 07:56:08 +01:00
*/
public function toStatic ()
{
2024-02-25 09:52:52 +01:00
if ( $this -> outputType != IMAGETYPE_GIF ) {
2021-03-26 07:56:08 +01:00
return ;
}
if ( $this -> isImagick ()) {
2024-02-25 09:52:52 +01:00
$this -> outputType = IMAGETYPE_PNG ;
$this -> image -> setFormat ( Images :: getImagickFormatByImageType ( $this -> outputType ));
2021-03-26 07:56:08 +01:00
}
}
2017-11-29 18:17:12 +01:00
/**
2022-06-21 23:34:14 +02:00
* Crops image
*
2017-11-29 18:17:12 +01:00
* @ param integer $max maximum
* @ param integer $x x coordinate
* @ param integer $y y coordinate
* @ param integer $w width
* @ param integer $h height
* @ return mixed
*/
2022-06-21 23:34:14 +02:00
public function crop ( int $max , int $x , int $y , int $w , int $h )
2017-11-29 18:17:12 +01:00
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2012-07-21 17:17:19 +02:00
2017-11-29 18:17:12 +01:00
if ( $this -> isImagick ()) {
2012-10-09 17:35:32 +02:00
$this -> image -> setFirstIterator ();
do {
$this -> image -> cropImage ( $w , $h , $x , $y );
2016-11-04 16:44:49 +01:00
/*
2023-03-27 00:03:04 +02:00
* We need to remove the canvas ,
2012-10-09 17:35:32 +02:00
* or the image is not resized to the crop :
* http :// php . net / manual / en / imagick . cropimage . php #97232
*/
$this -> image -> setImagePage ( 0 , 0 , 0 , 0 );
} while ( $this -> image -> nextImage ());
2017-12-07 14:56:11 +01:00
return $this -> scaleDown ( $max );
2012-10-09 17:35:32 +02:00
}
2012-07-22 13:43:46 +02:00
2016-11-04 19:26:28 +01:00
$dest = imagecreatetruecolor ( $max , $max );
2016-11-04 16:44:49 +01:00
imagealphablending ( $dest , false );
imagesavealpha ( $dest , true );
2023-11-28 15:57:11 +01:00
2024-02-25 09:52:52 +01:00
if ( $this -> outputType == IMAGETYPE_PNG ) {
2016-11-04 16:44:49 +01:00
imagefill ( $dest , 0 , 0 , imagecolorallocatealpha ( $dest , 0 , 0 , 0 , 127 )); // fill with alpha
}
imagecopyresampled ( $dest , $this -> image , 0 , 0 , $x , $y , $max , $max , $w , $h );
if ( $this -> image ) {
imagedestroy ( $this -> image );
}
2022-12-20 07:22:11 +01:00
$this -> image = $dest ;
2016-11-04 16:44:49 +01:00
$this -> width = imagesx ( $this -> image );
$this -> height = imagesy ( $this -> image );
2022-06-21 23:34:14 +02:00
// All successful
return true ;
2016-11-04 16:44:49 +01:00
}
2012-07-21 17:17:19 +02:00
2017-12-07 14:56:11 +01:00
/**
2020-01-19 07:05:23 +01:00
* Magic method allowing string casting of an Image object
2017-12-07 14:56:11 +01:00
*
* Ex : $data = $Image -> asString ();
* can be replaced by
* $data = ( string ) $Image ;
*
* @ return string
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2017-12-07 14:56:11 +01:00
*/
2022-06-21 23:34:14 +02:00
public function __toString () : string
{
return ( string ) $this -> asString ();
2017-12-07 14:56:11 +01:00
}
2017-11-29 18:17:12 +01:00
/**
2022-06-21 23:34:14 +02:00
* Returns image as string or false on failure
*
2017-11-29 18:17:12 +01:00
* @ return mixed
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2017-11-29 18:17:12 +01:00
*/
2017-12-07 14:56:11 +01:00
public function asString ()
2017-11-29 18:17:12 +01:00
{
if ( ! $this -> isValid ()) {
2016-11-04 16:44:49 +01:00
return false ;
}
2014-06-16 21:49:45 +02:00
2017-11-29 18:17:12 +01:00
if ( $this -> isImagick ()) {
2021-04-12 14:37:11 +02:00
try {
/* Clean it */
$this -> image = $this -> image -> deconstructImages ();
2022-12-04 14:29:21 +01:00
return $this -> image -> getImagesBlob ();
2021-04-12 14:37:11 +02:00
} catch ( Exception $e ) {
return false ;
}
2016-11-04 16:44:49 +01:00
}
2012-07-21 17:17:19 +02:00
2022-12-04 14:29:21 +01:00
$stream = fopen ( 'php://memory' , 'r+' );
2012-07-21 17:17:19 +02:00
2024-02-17 07:45:41 +01:00
switch ( $this -> getImageType ()) {
case IMAGETYPE_PNG :
2020-01-19 21:21:13 +01:00
$quality = DI :: config () -> get ( 'system' , 'png_quality' );
2022-12-04 14:29:21 +01:00
imagepng ( $this -> image , $stream , $quality );
2016-11-04 16:44:49 +01:00
break ;
2022-06-21 23:34:14 +02:00
2024-02-17 07:45:41 +01:00
case IMAGETYPE_JPEG :
2020-01-19 21:21:13 +01:00
$quality = DI :: config () -> get ( 'system' , 'jpeg_quality' );
2022-12-04 14:29:21 +01:00
imagejpeg ( $this -> image , $stream , $quality );
2022-06-21 23:34:14 +02:00
break ;
2024-02-17 07:45:41 +01:00
case IMAGETYPE_GIF :
imagegif ( $this -> image , $stream );
break ;
2024-02-18 04:27:37 +01:00
2024-02-17 07:45:41 +01:00
case IMAGETYPE_WEBP :
2024-02-25 09:52:52 +01:00
@ imagewebp ( $this -> image , $stream , DI :: config () -> get ( 'system' , 'jpeg_quality' ));
2024-02-17 07:45:41 +01:00
break ;
case IMAGETYPE_BMP :
imagebmp ( $this -> image , $stream );
break ;
2016-11-04 16:44:49 +01:00
}
2022-12-04 14:29:21 +01:00
rewind ( $stream );
return stream_get_contents ( $stream );
}
/**
* Create a blurhash out of a given image string
*
* @ param string $img_str
* @ return string
*/
public function getBlurHash () : string
{
2024-02-17 07:45:41 +01:00
$image = New Image ( $this -> asString (), $this -> getType (), $this -> filename , false );
2022-12-20 07:22:11 +01:00
if ( empty ( $image ) || ! $this -> isValid ()) {
2022-12-08 06:49:25 +01:00
return '' ;
}
2022-12-05 21:38:21 +01:00
2022-12-20 23:32:24 +01:00
$width = $image -> getWidth ();
2022-12-05 21:38:21 +01:00
$height = $image -> getHeight ();
2022-12-04 14:29:21 +01:00
if ( max ( $width , $height ) > 90 ) {
2022-12-05 21:38:21 +01:00
$image -> scaleDown ( 90 );
2022-12-20 23:32:24 +01:00
$width = $image -> getWidth ();
2022-12-05 21:38:21 +01:00
$height = $image -> getHeight ();
2022-12-04 14:29:21 +01:00
}
2022-12-20 23:32:24 +01:00
if ( empty ( $width ) || empty ( $height )) {
return '' ;
}
2022-12-04 14:29:21 +01:00
$pixels = [];
for ( $y = 0 ; $y < $height ; ++ $y ) {
$row = [];
for ( $x = 0 ; $x < $width ; ++ $x ) {
2022-12-05 21:38:21 +01:00
if ( $image -> isImagick ()) {
2022-12-08 06:49:25 +01:00
try {
$colors = $image -> image -> getImagePixelColor ( $x , $y ) -> getColor ();
2023-02-26 23:43:45 +01:00
} catch ( \Exception $exception ) {
2022-12-08 06:49:25 +01:00
return '' ;
}
2022-12-05 07:53:19 +01:00
$row [] = [ $colors [ 'r' ], $colors [ 'g' ], $colors [ 'b' ]];
} else {
2022-12-05 21:38:21 +01:00
$index = imagecolorat ( $image -> image , $x , $y );
$colors = @ imagecolorsforindex ( $image -> image , $index );
2022-12-05 07:53:19 +01:00
$row [] = [ $colors [ 'red' ], $colors [ 'green' ], $colors [ 'blue' ]];
}
2022-12-04 14:29:21 +01:00
}
$pixels [] = $row ;
}
// The components define the amount of details (1 to 9).
$components_x = 9 ;
$components_y = 9 ;
return Blurhash :: encode ( $pixels , $components_x , $components_y );
}
/**
* Create an image out of a blurhash
*
* @ param string $blurhash
* @ param integer $width
* @ param integer $height
* @ return void
*/
public function getFromBlurHash ( string $blurhash , int $width , int $height )
{
2022-12-04 15:58:53 +01:00
$scaled = Images :: getScalingDimensions ( $width , $height , 90 );
$pixels = Blurhash :: decode ( $blurhash , $scaled [ 'width' ], $scaled [ 'height' ]);
2022-12-05 07:53:19 +01:00
if ( $this -> isImagick ()) {
$this -> image = new Imagick ();
$draw = new ImagickDraw ();
$this -> image -> newImage ( $scaled [ 'width' ], $scaled [ 'height' ], '' , 'png' );
} else {
$this -> image = imagecreatetruecolor ( $scaled [ 'width' ], $scaled [ 'height' ]);
}
2022-12-04 15:58:53 +01:00
for ( $y = 0 ; $y < $scaled [ 'height' ]; ++ $y ) {
for ( $x = 0 ; $x < $scaled [ 'width' ]; ++ $x ) {
2022-12-04 14:29:21 +01:00
[ $r , $g , $b ] = $pixels [ $y ][ $x ];
2022-12-05 07:53:19 +01:00
if ( $this -> isImagick ()) {
$draw -> setFillColor ( " rgb( $r , $g , $b ) " );
$draw -> point ( $x , $y );
} else {
imagesetpixel ( $this -> image , $x , $y , imagecolorallocate ( $this -> image , $r , $g , $b ));
}
2022-12-04 14:29:21 +01:00
}
}
2022-12-04 15:58:53 +01:00
2022-12-05 13:53:21 +01:00
if ( $this -> isImagick ()) {
$this -> image -> drawImage ( $draw );
2022-12-20 07:22:11 +01:00
$this -> width = $this -> image -> getImageWidth ();
$this -> height = $this -> image -> getImageHeight ();
2022-12-05 13:53:21 +01:00
} else {
2022-12-05 07:53:19 +01:00
$this -> width = imagesx ( $this -> image );
$this -> height = imagesy ( $this -> image );
}
2022-12-20 07:22:11 +01:00
$this -> valid = ! empty ( $this -> image );
2022-12-04 15:58:53 +01:00
$this -> scaleUp ( min ( $width , $height ));
2016-11-04 16:44:49 +01:00
}
2014-07-15 08:49:41 +02:00
}