Image handling reworked, new image formats added (#13900)

* Image handling reworked, new image formats added

* Updated messages.po

* The dot is now part of the file extension

* Added WebP in install documentation

* Handle unhandled mime types

* Fixed animated picture detected
This commit is contained in:
Michael Vogel 2024-02-17 07:45:41 +01:00 committed by GitHub
parent 1ea8a4042d
commit 14e5b06029
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 541 additions and 386 deletions

View file

@ -45,25 +45,37 @@ class Image
private $width;
private $height;
private $valid;
private $type;
private $types;
private $imageType;
private $filename;
/**
* Constructor
*
* @param string $data Image data
* @param string $type optional, default null
* @param string $data Image data
* @param string $type optional, default ''
* @param string $filename optional, default ''
* @param string $imagick optional, default 'true'
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public function __construct(string $data, string $type = null)
public function __construct(string $data, string $type = '', string $filename = '', bool $imagick = true)
{
$this->imagick = class_exists('Imagick');
$this->types = Images::supportedTypes();
if (!array_key_exists($type, $this->types)) {
$type = 'image/jpeg';
$this->filename = $filename;
$type = Images::addMimeTypeByDataIfInvalid($type, $data);
$type = Images::addMimeTypeByExtensionIfInvalid($type, $filename);
if (Images::isSupportedMimeType($type)) {
$this->imageType = Images::getImageTypeByMimeType($type);
} elseif (($type == '') || substr($type, 0, 6) != 'image/' || substr($type, 0, 12) != ' application/') {
$this->imageType = IMAGETYPE_WEBP;
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;
}
$this->type = $type;
$this->imagick = $imagick && $this->useImagick($data);
if ($this->isImagick() && (empty($data) || $this->loadData($data))) {
$this->valid = !empty($data);
@ -75,6 +87,49 @@ class Image
$this->loadData($data);
}
/**
* Check if Imagick will be used
*
* @param string $data
* @return boolean
*/
private function useImagick(string $data): bool
{
if (!class_exists('Imagick')) {
return false;
}
if ($this->imageType == IMAGETYPE_GIF) {
$count = preg_match_all("#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s", $data);
return ($count > 0);
}
return (($this->imageType == IMAGETYPE_WEBP) && $this->isAnimatedWebP(substr($data, 0, 90)));
}
/**
* 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';
$header = unpack($header_format, $data);
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;
}
/**
* Destructor
*
@ -118,28 +173,28 @@ class Image
$this->image->readImageBlob($data);
} catch (Exception $e) {
// Imagick couldn't use the data
DI::logger()->debug('Error during readImageBlob', ['message' => $e->getMessage(), 'code' => $e->getCode(), 'trace' => $e->getTraceAsString(), 'previous' => $e->getPrevious(), 'file' => $this->filename]);
return false;
}
/*
* Setup the image to the format it will be saved to
*/
$map = Images::getFormatsMap();
$format = $map[$this->type];
$this->image->setFormat($format);
$this->image->setFormat(Images::getImagickFormatByImageType($this->imageType));
// Always coalesce, if it is not a multi-frame image it won't hurt anyway
try {
$this->image = $this->image->coalesceImages();
} catch (Exception $e) {
DI::logger()->debug('Error during coalesceImages', ['message' => $e->getMessage(), 'code' => $e->getCode(), 'trace' => $e->getTraceAsString(), 'previous' => $e->getPrevious(), 'file' => $this->filename]);
return false;
}
/*
* setup the compression here, so we'll do it only once
*/
switch ($this->getType()) {
case 'image/png':
switch ($this->getImageType()) {
case IMAGETYPE_PNG:
$quality = DI::config()->get('system', 'png_quality');
/*
* From http://www.imagemagick.org/script/command-line-options.php#quality:
@ -150,13 +205,12 @@ class Image
* unless the image has a color map, in which case it means compression level 7 with no PNG filtering'
*/
$quality = $quality * 10;
$this->image->setCompressionQuality($quality);
$this->image->setImageCompressionQuality($quality);
break;
case 'image/jpg':
case 'image/jpeg':
case IMAGETYPE_JPEG:
$quality = DI::config()->get('system', 'jpeg_quality');
$this->image->setCompressionQuality($quality);
$this->image->setImageCompressionQuality($quality);
}
$this->width = $this->image->getImageWidth();
@ -182,9 +236,9 @@ class Image
} catch (\Throwable $error) {
/** @see https://github.com/php/doc-en/commit/d09a881a8e9059d11e756ee59d75bf404d6941ed */
if (strstr($error->getMessage(), "gd-webp cannot allocate temporary buffer")) {
DI::logger()->notice('Image is probably animated and therefore unsupported', ['error' => $error]);
DI::logger()->notice('Image is probably animated and therefore unsupported', ['message' => $error->getMessage(), 'code' => $error->getCode(), 'trace' => $error->getTraceAsString(), 'file' => $this->filename]);
} else {
DI::logger()->warning('Unexpected throwable.', ['error' => $error]);
DI::logger()->warning('Unexpected throwable.', ['message' => $error->getMessage(), 'code' => $error->getCode(), 'trace' => $error->getTraceAsString(), 'file' => $this->filename]);
}
}
@ -256,7 +310,19 @@ class Image
return false;
}
return $this->type;
return image_type_to_mime_type($this->imageType);
}
/**
* @return mixed
*/
public function getImageType()
{
if (!$this->isValid()) {
return false;
}
return $this->imageType;
}
/**
@ -268,7 +334,7 @@ class Image
return false;
}
return $this->types[$this->getType()];
return Images::getExtensionByImageType($this->imageType);
}
/**
@ -398,7 +464,7 @@ class Image
return false;
}
if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) {
if ((!function_exists('exif_read_data')) || ($this->getImageType() !== IMAGETYPE_JPEG)) {
return;
}
@ -545,7 +611,7 @@ class Image
imagealphablending($dest, false);
imagesavealpha($dest, true);
if ($this->type=='image/png') {
if ($this->imageType == IMAGETYPE_PNG) {
imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
}
@ -570,13 +636,13 @@ class Image
*/
public function toStatic()
{
if ($this->type != 'image/gif') {
if ($this->imageType != IMAGETYPE_GIF) {
return;
}
if ($this->isImagick()) {
$this->type == 'image/png';
$this->image->setFormat('png');
$this->imageType = IMAGETYPE_PNG;
$this->image->setFormat(Images::getImagickFormatByImageType($this->imageType));
}
}
@ -614,7 +680,7 @@ class Image
imagealphablending($dest, false);
imagesavealpha($dest, true);
if ($this->type=='image/png') {
if ($this->imageType == IMAGETYPE_PNG) {
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);
@ -668,17 +734,28 @@ class Image
$stream = fopen('php://memory','r+');
switch ($this->getType()) {
case 'image/png':
switch ($this->getImageType()) {
case IMAGETYPE_PNG:
$quality = DI::config()->get('system', 'png_quality');
imagepng($this->image, $stream, $quality);
break;
case 'image/jpeg':
case 'image/jpg':
case IMAGETYPE_JPEG:
$quality = DI::config()->get('system', 'jpeg_quality');
imagejpeg($this->image, $stream, $quality);
break;
case IMAGETYPE_GIF:
imagegif($this->image, $stream);
break;
case IMAGETYPE_WEBP:
imagewebp($this->image, $stream, DI::config()->get('system', 'jpeg_quality'));
break;
case IMAGETYPE_BMP:
imagebmp($this->image, $stream);
break;
}
rewind($stream);
return stream_get_contents($stream);
@ -692,7 +769,7 @@ class Image
*/
public function getBlurHash(): string
{
$image = New Image($this->asString());
$image = New Image($this->asString(), $this->getType(), $this->filename, false);
if (empty($image) || !$this->isValid()) {
return '';
}