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

@ -33,19 +33,107 @@ use Friendica\Object\Image;
*/
class Images
{
// @todo add IMAGETYPE_AVIF once our minimal supported PHP version is 8.1.0
const IMAGETYPES = [IMAGETYPE_WEBP, IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_BMP];
/**
* Maps Mime types to Imagick formats
* Get the Imagick format for the given image type
*
* @return array Format map
* @param int $imagetype
* @return string
*/
public static function getFormatsMap()
public static function getImagickFormatByImageType(int $imagetype): string
{
return [
'image/jpeg' => 'JPG',
'image/jpg' => 'JPG',
'image/png' => 'PNG',
'image/gif' => 'GIF',
$formats = [
// @todo add "IMAGETYPE_AVIF => 'AVIF'" once our minimal supported PHP version is 8.1.0
IMAGETYPE_WEBP => 'WEBP',
IMAGETYPE_PNG => 'PNG',
IMAGETYPE_JPEG => 'JPEG',
IMAGETYPE_GIF => 'GIF',
IMAGETYPE_BMP => 'BMP',
];
if (empty($formats[$imagetype])) {
return '';
}
return $formats[$imagetype];
}
/**
* Sanitize the provided mime type, replace invalid mime types with valid ones.
*
* @param string $mimetype
* @return string
*/
private static function sanitizeMimeType(string $mimetype): string
{
$mimetype = current(explode(';', $mimetype));
if ($mimetype == 'image/jpg') {
$mimetype = image_type_to_mime_type(IMAGETYPE_JPEG);
} elseif (in_array($mimetype, ['image/vnd.mozilla.apng', 'image/apng'])) {
$mimetype = image_type_to_mime_type(IMAGETYPE_PNG);
} elseif (in_array($mimetype, ['image/x-ms-bmp', 'image/x-bmp'])) {
$mimetype = image_type_to_mime_type(IMAGETYPE_BMP);
}
return $mimetype;
}
/**
* Replace invalid extensions with valid ones.
*
* @param string $extension
* @return string
*/
private static function sanitizeExtensions(string $extension): string
{
if (in_array($extension, ['jpg', 'jpe', 'jfif'])) {
$extension = image_type_to_extension(IMAGETYPE_JPEG, false);
} elseif ($extension == 'apng') {
$extension = image_type_to_extension(IMAGETYPE_PNG, false);
} elseif ($extension == 'dib') {
$extension = image_type_to_extension(IMAGETYPE_BMP, false);
}
return $extension;
}
/**
* Get the image type for the given mime type
*
* @param string $mimetype
* @return integer
*/
public static function getImageTypeByMimeType(string $mimetype): int
{
$mimetype = self::sanitizeMimeType($mimetype);
foreach (self::IMAGETYPES as $type) {
if ($mimetype == image_type_to_mime_type($type)) {
return $type;
}
}
Logger::debug('Undetected mimetype', ['mimetype' => $mimetype]);
return 0;
}
/**
* Get the extension for the given image type
*
* @param integer $type
* @return string
*/
public static function getExtensionByImageType(int $type): string
{
if (empty($type)) {
Logger::debug('Invalid image type', ['type' => $type]);
return '';
}
return image_type_to_extension($type);
}
/**
@ -56,51 +144,40 @@ class Images
*/
public static function getExtensionByMimeType(string $mimetype): string
{
switch ($mimetype) {
case 'image/png':
$imagetype = IMAGETYPE_PNG;
break;
case 'image/gif':
$imagetype = IMAGETYPE_GIF;
break;
case 'image/jpeg':
case 'image/jpg':
$imagetype = IMAGETYPE_JPEG;
break;
default: // Unknown type must be a blob then
return 'blob';
break;
if (empty($mimetype)) {
return '';
}
return image_type_to_extension($imagetype);
return self::getExtensionByImageType(self::getImageTypeByMimeType($mimetype));
}
/**
* Returns supported image mimetypes and corresponding file extensions
* Returns supported image mimetypes
*
* @return array
*/
public static function supportedTypes(): array
public static function supportedMimeTypes(): array
{
$types = [
'image/jpeg' => 'jpg',
'image/jpg' => 'jpg',
];
$types = [];
if (class_exists('Imagick')) {
// Imagick::queryFormats won't help us a lot there...
// At least, not yet, other parts of friendica uses this array
$types += [
'image/png' => 'png',
'image/gif' => 'gif'
];
} elseif (imagetypes() & IMG_PNG) {
$types += [
'image/png' => 'png'
];
// @todo enable, once our lowest supported PHP version is 8.1.0
//if (imagetypes() & IMG_AVIF) {
// $types[] = image_type_to_mime_type(IMAGETYPE_AVIF);
//}
if (imagetypes() & IMG_WEBP) {
$types[] = image_type_to_mime_type(IMAGETYPE_WEBP);
}
if (imagetypes() & IMG_PNG) {
$types[] = image_type_to_mime_type(IMAGETYPE_PNG);
}
if (imagetypes() & IMG_JPG) {
$types[] = image_type_to_mime_type(IMAGETYPE_JPEG);
}
if (imagetypes() & IMG_GIF) {
$types[] = image_type_to_mime_type(IMAGETYPE_GIF);
}
if (imagetypes() & IMG_BMP) {
$types[] = image_type_to_mime_type(IMAGETYPE_BMP);
}
return $types;
@ -115,45 +192,69 @@ class Images
* @return string MIME type
* @throws \Exception
*/
public static function getMimeTypeByData(string $image_data, string $filename = '', string $default = ''): string
public static function getMimeTypeByData(string $image_data): string
{
if (substr($default, 0, 6) == 'image/') {
Logger::info('Using default mime type', ['filename' => $filename, 'mime' => $default]);
return $default;
}
$image = @getimagesizefromstring($image_data);
if (!empty($image['mime'])) {
Logger::info('Mime type detected via data', ['filename' => $filename, 'default' => $default, 'mime' => $image['mime']]);
return $image['mime'];
}
return self::guessTypeByExtension($filename);
Logger::debug('Undetected mime type', ['image' => $image, 'size' => strlen($image_data)]);
return '';
}
/**
* Fetch image mimetype from the image data or guessing from the file name
* Checks if the provided mime type is supported by the system
*
* @param string $sourcefile Source file of the image
* @param string $filename File name (for guessing the type via the extension)
* @param string $default default MIME type
* @return string MIME type
* @throws \Exception
* @param string $mimetype
* @return boolean
*/
public static function getMimeTypeBySource(string $sourcefile, string $filename = '', string $default = ''): string
public static function isSupportedMimeType(string $mimetype): bool
{
if (substr($default, 0, 6) == 'image/') {
Logger::info('Using default mime type', ['filename' => $filename, 'mime' => $default]);
return $default;
if (substr($mimetype, 0, 6) != 'image/') {
return false;
}
$image = @getimagesize($sourcefile);
if (!empty($image['mime'])) {
Logger::info('Mime type detected via file', ['filename' => $filename, 'default' => $default, 'image' => $image]);
return $image['mime'];
return in_array(self::sanitizeMimeType($mimetype), self::supportedMimeTypes());
}
/**
* Checks if the provided mime type is supported. If not, it is fetched from the provided image data.
*
* @param string $mimetype
* @param string $image_data
* @return string
*/
public static function addMimeTypeByDataIfInvalid(string $mimetype, string $image_data): string
{
$mimetype = self::sanitizeMimeType($mimetype);
if (($image_data == '') || self::isSupportedMimeType($mimetype)) {
return $mimetype;
}
return self::guessTypeByExtension($filename);
$alternative = self::getMimeTypeByData($image_data);
return $alternative ?: $mimetype;
}
/**
* Checks if the provided mime type is supported. If not, it is fetched from the provided file name.
*
* @param string $mimetype
* @param string $filename
* @return string
*/
public static function addMimeTypeByExtensionIfInvalid(string $mimetype, string $filename): string
{
$mimetype = self::sanitizeMimeType($mimetype);
if (($filename == '') || self::isSupportedMimeType($mimetype)) {
return $mimetype;
}
$alternative = self::guessTypeByExtension($filename);
return $alternative ?: $mimetype;
}
/**
@ -165,17 +266,24 @@ class Images
*/
public static function guessTypeByExtension(string $filename): string
{
$ext = pathinfo(parse_url($filename, PHP_URL_PATH), PATHINFO_EXTENSION);
$types = self::supportedTypes();
$type = 'image/jpeg';
foreach ($types as $m => $e) {
if ($ext == $e) {
$type = $m;
if (empty($filename)) {
return '';
}
$ext = strtolower(pathinfo(parse_url($filename, PHP_URL_PATH), PATHINFO_EXTENSION));
$ext = self::sanitizeExtensions($ext);
if ($ext == '') {
return '';
}
foreach (self::IMAGETYPES as $type) {
if ($ext == image_type_to_extension($type, false)) {
return image_type_to_mime_type($type);
}
}
Logger::info('Mime type guessed via extension', ['filename' => $filename, 'type' => $type]);
return $type;
Logger::debug('Unhandled extension', ['filename' => $filename, 'extension' => $ext]);
return '';
}
/**
@ -256,7 +364,7 @@ class Images
return [];
}
$image = new Image($img_str);
$image = new Image($img_str, '', $url);
if ($image->isValid()) {
$data['blurhash'] = $image->getBlurHash();
@ -344,7 +452,7 @@ class Images
{
return self::getBBCodeByUrl(
DI::baseUrl() . '/photos/' . $nickname . '/image/' . $resource_id,
DI::baseUrl() . '/photo/' . $resource_id . '-' . $preview. '.' . $ext,
DI::baseUrl() . '/photo/' . $resource_id . '-' . $preview. $ext,
$description
);
}

View file

@ -87,7 +87,7 @@ class ParseUrl
return [];
}
$contenttype = $curlResult->getHeader('Content-Type')[0] ?? '';
$contenttype = $curlResult->getContentType();
if (empty($contenttype)) {
return ['application', 'octet-stream'];
}