]> git.mxchange.org Git - friendica.git/commitdiff
Image handling reworked, new image formats added (#13900)
authorMichael Vogel <icarus@dabo.de>
Sat, 17 Feb 2024 06:45:41 +0000 (07:45 +0100)
committerGitHub <noreply@github.com>
Sat, 17 Feb 2024 06:45:41 +0000 (07:45 +0100)
* 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

31 files changed:
doc/Install.md
mod/photos.php
src/Console/MoveToAvatarCache.php
src/Contact/Avatar.php
src/Content/Text/BBCode.php
src/Core/Installer.php
src/Factory/Api/Mastodon/Attachment.php
src/Model/Contact.php
src/Model/Photo.php
src/Model/Post/Link.php
src/Model/Post/Media.php
src/Model/User.php
src/Module/Api/Mastodon/Instance.php
src/Module/Api/Mastodon/InstanceV2.php
src/Module/Api/Mastodon/Statuses.php
src/Module/Api/Twitter/Statuses/Update.php
src/Module/Media/Photo/Browser.php
src/Module/Media/Photo/Upload.php
src/Module/Photo.php
src/Module/Profile/Photos.php
src/Module/Proxy.php
src/Module/Settings/Profile/Photo/Crop.php
src/Module/Settings/Profile/Photo/Index.php
src/Module/User/Import.php
src/Network/HTTPClient/Client/HttpClientAccept.php
src/Object/Image.php
src/Protocol/DFRN.php
src/Protocol/Feed.php
src/Util/Images.php
src/Util/ParseUrl.php
view/lang/C/messages.po

index 4715c27233ef1bfdc86b1d81617171d91b507442..c50854aaf2128163d54fb4c0f538fff4d95c8032 100644 (file)
@@ -44,7 +44,7 @@ For alternative server configurations (such as Nginx server and MariaDB database
 
 ### Optional
 
-* PHP ImageMagick extension (php-imagick) for animated GIF support.
+* PHP ImageMagick extension (php-imagick) for animated GIF and animated WebP support.
 
 ## Installation procedure
 
index 322ddd15999a99859420acec1d77f44c10ffc515..a8a62e8964bacaca417e94ab55f8ca83617ea1f2 100644 (file)
@@ -132,8 +132,6 @@ function photos_post(App $a)
                throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.'));
        }
 
-       $phototypes = Images::supportedTypes();
-
        $can_post  = false;
        $visitor   = 0;
 
@@ -337,7 +335,7 @@ function photos_post(App $a)
 
                if (DBA::isResult($photos)) {
                        $photo = $photos[0];
-                       $ext = $phototypes[$photo['type']];
+                       $ext = Images::getExtensionByMimeType($photo['type']);
                        Photo::update(
                                ['desc' => $desc, 'album' => $albname, 'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow, 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny],
                                ['resource-id' => $resource_id, 'uid' => $page_owner_uid]
@@ -590,8 +588,6 @@ function photos_content(App $a)
 
        $profile = Profile::getByUID($user['uid']);
 
-       $phototypes = Images::supportedTypes();
-
        $_SESSION['photo_return'] = DI::args()->getCommand();
 
        // Parse arguments
@@ -844,7 +840,7 @@ function photos_content(App $a)
                        foreach ($r as $rr) {
                                $twist = !$twist;
 
-                               $ext = $phototypes[$rr['type']];
+                               $ext = Images::getExtensionByMimeType($rr['type']);
 
                                $imgalt_e = $rr['filename'];
                                $desc_e = $rr['desc'];
@@ -855,7 +851,7 @@ function photos_content(App $a)
                                        'link'  => 'photos/' . $user['nickname'] . '/image/' . $rr['resource-id']
                                                . ($order_field === 'created' ? '?order=created' : ''),
                                        'title' => DI::l10n()->t('View Photo'),
-                                       'src'   => 'photo/' . $rr['resource-id'] . '-' . $rr['scale'] . '.' . $ext,
+                                       'src'   => 'photo/' . $rr['resource-id'] . '-' . $rr['scale'] . $ext,
                                        'alt'   => $imgalt_e,
                                        'desc'  => $desc_e,
                                        'ext'   => $ext,
@@ -1013,9 +1009,9 @@ function photos_content(App $a)
                }
 
                $photo = [
-                       'href'     => 'photo/' . $hires['resource-id'] . '-' . $hires['scale'] . '.' . $phototypes[$hires['type']],
+                       'href'     => 'photo/' . $hires['resource-id'] . '-' . $hires['scale'] . Images::getExtensionByMimeType($hires['type']),
                        'title'    => DI::l10n()->t('View Full Size'),
-                       'src'      => 'photo/' . $lores['resource-id'] . '-' . $lores['scale'] . '.' . $phototypes[$lores['type']] . '?_u=' . DateTimeFormat::utcNow('ymdhis'),
+                       'src'      => 'photo/' . $lores['resource-id'] . '-' . $lores['scale'] . Images::getExtensionByMimeType($lores['type']) . '?_u=' . DateTimeFormat::utcNow('ymdhis'),
                        'height'   => $hires['height'],
                        'width'    => $hires['width'],
                        'album'    => $hires['album'],
index f5b067ffc68583a757b0a09027f966cc529370ff..e8c082338169fd0e85c2087cdedc85f8e4978973 100644 (file)
@@ -150,7 +150,7 @@ HELP;
 
                if ($valid) {
                        $this->out('3', false);
-                       $image = new Image($imgdata, Images::getMimeTypeByData($imgdata));
+                       $image = new Image($imgdata);
                        if (!$image->isValid()) {
                                $this->out(' ' . $this->l10n->t('invalid image for id %s', $resourceid) . ' ', false);
                                $valid = false;
index 371e6452329066853fdcd73110e253e645cd4de0..f165e1d3a17317bcfb8394c58bc5f1988ab687e3 100644 (file)
@@ -80,13 +80,18 @@ class Avatar
                        return $fields;
                }
 
+               if (!$fetchResult->isSuccess()) {
+                       Logger::debug('Fetching was unsuccessful', ['avatar' => $avatar]);
+                       return $fields;
+               }
+
                $img_str = $fetchResult->getBodyString();
                if (empty($img_str)) {
                        Logger::debug('Avatar is invalid', ['avatar' => $avatar]);
                        return $fields;
                }
 
-               $image = new Image($img_str, Images::getMimeTypeByData($img_str));
+               $image = new Image($img_str, $fetchResult->getContentType(), $avatar);
                if (!$image->isValid()) {
                        Logger::debug('Avatar picture is invalid', ['avatar' => $avatar]);
                        return $fields;
@@ -145,7 +150,7 @@ class Avatar
                        return '';
                }
 
-               $path = $filename . $size . '.' . $image->getExt();
+               $path = $filename . $size . $image->getExt();
 
                $basepath = self::basePath();
                if (empty($basepath)) {
index 4c2a3a6b4606ce7197a81018fdc5c7afcb71168c..f5bba8ce42dfc84d6483e77aff1faee64f4bbc8c 100644 (file)
@@ -40,6 +40,7 @@ use Friendica\Model\Post;
 use Friendica\Model\Tag;
 use Friendica\Network\HTTPClient\Client\HttpClientAccept;
 use Friendica\Network\HTTPClient\Client\HttpClientOptions;
+use Friendica\Util\Images;
 use Friendica\Util\Map;
 use Friendica\Util\Network;
 use Friendica\Util\ParseUrl;
@@ -1027,12 +1028,12 @@ class BBCode
                if (is_null($text)) {
                        $curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]);
                        if ($curlResult->isSuccess()) {
-                               $mimetype = $curlResult->getHeader('Content-Type')[0] ?? '';
+                               $mimetype = $curlResult->getContentType() ?? '';
                        } else {
                                $mimetype = '';
                        }
 
-                       if (substr($mimetype, 0, 6) == 'image/') {
+                       if (Images::isSupportedMimeType($mimetype)) {
                                $text = '[url=' . $match[1] . ']' . $match[1] . '[/url]';
                        } else {
                                $text = '[url=' . $match[2] . ']' . $match[2] . '[/url]';
@@ -1125,13 +1126,13 @@ class BBCode
 
                $curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]);
                if ($curlResult->isSuccess()) {
-                       $mimetype = $curlResult->getHeader('Content-Type')[0] ?? '';
+                       $mimetype = $curlResult->getContentType() ?? '';
                } else {
                        $mimetype = '';
                }
 
                // if its a link to a picture then embed this picture
-               if (substr($mimetype, 0, 6) == 'image/') {
+               if (Images::isSupportedMimeType($mimetype)) {
                        $text = '[img]' . $match[1] . '[/img]';
                } else {
                        if (!empty($match[3])) {
index efbcad5ee166a7d35b4394da59faed85cb55a9dd..1c07f325ed66dea99e2cf45dab1ec28236d59b0b 100644 (file)
@@ -632,23 +632,10 @@ class Installer
         */
        public function checkImagick()
        {
-               $imagick = false;
-               $gif = false;
-
-               if (class_exists('Imagick')) {
-                       $imagick = true;
-                       $supported = Images::supportedTypes();
-                       if (array_key_exists('image/gif', $supported)) {
-                               $gif = true;
-                       }
-               }
-               if (!$imagick) {
-                       $this->addCheck(DI::l10n()->t('ImageMagick PHP extension is not installed'), $imagick, false, "");
+               if (!class_exists('Imagick')) {
+                       $this->addCheck(DI::l10n()->t('ImageMagick PHP extension is not installed'), false, false, "");
                } else {
-                       $this->addCheck(DI::l10n()->t('ImageMagick PHP extension is installed'), $imagick, false, "");
-                       if ($imagick) {
-                               $this->addCheck(DI::l10n()->t('ImageMagick supports GIF'), $gif, false, "");
-                       }
+                       $this->addCheck(DI::l10n()->t('ImageMagick PHP extension is installed'), true, false, "");
                }
 
                // Imagick is not required
index eb93a3d6207d5c227a521dd6018e081d29485a82..727b77630a70f899f4d94b20358b942bc21a48ed 100644 (file)
@@ -84,7 +84,7 @@ class Attachment extends BaseFactory
                        $type = 'audio';
                } elseif (($filetype == 'video') || ($attachment['type'] == Post\Media::VIDEO)) {
                        $type = 'video';
-               } elseif ($attachment['mimetype'] == 'image/gif') {
+               } elseif ($attachment['mimetype'] == image_type_to_mime_type(IMAGETYPE_GIF)) {
                        $type = 'gifv';
                } elseif (($filetype == 'image') || ($attachment['type'] == Post\Media::IMAGE)) {
                        $type = 'image';
@@ -130,14 +130,13 @@ class Attachment extends BaseFactory
                        'blurhash'    => $photo['blurhash'],
                ];
 
-               $photoTypes = Images::supportedTypes();
-               $ext        = $photoTypes[$photo['type']];
+               $ext = Images::getExtensionByMimeType($photo['type']);
 
-               $url = $this->baseUrl . '/photo/' . $photo['resource-id'] . '-0.' . $ext;
+               $url = $this->baseUrl . '/photo/' . $photo['resource-id'] . '-0' . $ext;
 
                $preview = Photo::selectFirst(['scale'], ["`resource-id` = ? AND `uid` = ? AND `scale` > ?", $photo['resource-id'], $photo['uid'], 0], ['order' => ['scale']]);
                if (!empty($preview)) {
-                       $preview_url = $this->baseUrl . '/photo/' . $photo['resource-id'] . '-' . $preview['scale'] . '.' . $ext;
+                       $preview_url = $this->baseUrl . '/photo/' . $photo['resource-id'] . '-' . $preview['scale'] . $ext;
                } else {
                        $preview_url = '';
                }
index 7ab726ad413cb08aed260a79edb3c1651f4df913..e673822ef271b0b29f6e0dec1e07d94f1d41b82b 100644 (file)
@@ -842,7 +842,6 @@ class Contact
                        return false;
                }
 
-               $file_suffix = 'jpg';
                $url = DI::baseUrl() . '/profile/' . $user['nickname'];
 
                $fields = [
@@ -875,17 +874,11 @@ class Contact
                                $fields['avatar-date'] = DateTimeFormat::utcNow();
                        }
 
-                       // Creating the path to the avatar, beginning with the file suffix
-                       $types = Images::supportedTypes();
-                       if (isset($types[$avatar['type']])) {
-                               $file_suffix = $types[$avatar['type']];
-                       }
-
                        // We are adding a timestamp value so that other systems won't use cached content
                        $timestamp = strtotime($fields['avatar-date']);
 
                        $prefix = DI::baseUrl() . '/photo/' . $avatar['resource-id'] . '-';
-                       $suffix = '.' . $file_suffix . '?ts=' . $timestamp;
+                       $suffix = Images::getExtensionByMimeType($avatar['type']) . '?ts=' . $timestamp;
 
                        $fields['photo'] = $prefix . '4' . $suffix;
                        $fields['thumb'] = $prefix . '5' . $suffix;
@@ -2313,8 +2306,8 @@ class Contact
                                                $fetchResult = HTTPSignature::fetchRaw($avatar, 0, [HttpClientOptions::ACCEPT_CONTENT => [HttpClientAccept::IMAGE]]);
 
                                                $img_str = $fetchResult->getBodyString();
-                                               if (!empty($img_str)) {
-                                                       $image = new Image($img_str, Images::getMimeTypeByData($img_str));
+                                               if ($fetchResult->isSuccess() && !empty($img_str)) {
+                                                       $image = new Image($img_str, $fetchResult->getContentType(), $avatar);
                                                        if ($image->isValid()) {
                                                                $update_fields['blurhash'] = $image->getBlurHash();
                                                        } else {
index 4700db6f50a20b0514f79dca629225ccc7165d26..360ee162206e1ee088501c7113eae39b301b4ca7 100644 (file)
@@ -363,6 +363,7 @@ class Photo
                $photo['backend-class'] = SystemResource::NAME;
                $photo['backend-ref']   = $filename;
                $photo['type']          = $mimetype;
+               $photo['filename']      = basename($filename);
                $photo['cacheable']     = false;
 
                return $photo;
@@ -394,6 +395,7 @@ class Photo
                $photo['backend-class'] = ExternalResource::NAME;
                $photo['backend-ref']   = json_encode(['url' => $url, 'uid' => $uid]);
                $photo['type']          = $mimetype;
+               $photo['filename']          = basename(parse_url($url, PHP_URL_PATH));
                $photo['cacheable']     = true;
                $photo['blurhash']      = $blurhash;
                $photo['width']         = $width;
@@ -608,9 +610,7 @@ class Photo
                        return false;
                }
 
-               $type = Images::getMimeTypeByData($img_str, $image_url, $type);
-
-               $image = new Image($img_str, $type);
+               $image = new Image($img_str, $type, $image_url);
                if ($image->isValid()) {
                        $image->scaleToSquare(300);
 
@@ -619,9 +619,9 @@ class Photo
 
                        if ($maximagesize && ($filesize > $maximagesize)) {
                                Logger::info('Avatar exceeds image limit', ['uid' => $uid, 'cid' => $cid, 'maximagesize' => $maximagesize, 'size' => $filesize, 'type' => $image->getType()]);
-                               if ($image->getType() == 'image/gif') {
+                               if ($image->getImageType() == IMAGETYPE_GIF) {
                                        $image->toStatic();
-                                       $image = new Image($image->asString(), 'image/png');
+                                       $image = new Image($image->asString(), image_type_to_mime_type(IMAGETYPE_PNG));
 
                                        $filesize = strlen($image->asString());
                                        Logger::info('Converted gif to a static png', ['uid' => $uid, 'cid' => $cid, 'size' => $filesize, 'type' => $image->getType()]);
@@ -662,9 +662,9 @@ class Photo
 
                        $suffix = '?ts=' . time();
 
-                       $image_url = DI::baseUrl() . '/photo/' . $resource_id . '-4.' . $image->getExt() . $suffix;
-                       $thumb = DI::baseUrl() . '/photo/' . $resource_id . '-5.' . $image->getExt() . $suffix;
-                       $micro = DI::baseUrl() . '/photo/' . $resource_id . '-6.' . $image->getExt() . $suffix;
+                       $image_url = DI::baseUrl() . '/photo/' . $resource_id . '-4' . $image->getExt() . $suffix;
+                       $thumb = DI::baseUrl() . '/photo/' . $resource_id . '-5' . $image->getExt() . $suffix;
+                       $micro = DI::baseUrl() . '/photo/' . $resource_id . '-6' . $image->getExt() . $suffix;
                } else {
                        $photo_failure = true;
                }
@@ -1060,9 +1060,7 @@ class Photo
                        return [];
                }
 
-               $type = Images::getMimeTypeByData($img_str, $image_url, $type);
-
-               $image = new Image($img_str, $type);
+               $image = new Image($img_str, $type, $image_url);
 
                $image = self::fitImageSize($image);
                if (empty($image)) {
@@ -1132,12 +1130,10 @@ class Photo
                        return [];
                }
 
-               $filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
-
                Logger::info('File upload', ['src' => $src, 'filename' => $filename, 'size' => $filesize, 'type' => $filetype]);
 
                $imagedata = @file_get_contents($src);
-               $image = new Image($imagedata, $filetype);
+               $image = new Image($imagedata, $filetype, $filename);
                if (!$image->isValid()) {
                        Logger::notice('Image is unvalid', ['files' => $files]);
                        return [];
index be2f7fd2da5959f64b115938361c79bd6797609a..6c70d8b624e7466fcd58dadca1f98c108ce5e5d7 100644 (file)
@@ -134,15 +134,23 @@ class Link
                        Logger::notice('Error fetching url', ['url' => $url, 'exception' => $exception]);
                        return [];
                }
-               $fields = ['mimetype' => $curlResult->getHeader('Content-Type')[0]];
-
-               $img_str = $curlResult->getBodyString();
-               $image = new Image($img_str, Images::getMimeTypeByData($img_str));
-               if ($image->isValid()) {
-                       $fields['mimetype'] = $image->getType();
-                       $fields['width']    = $image->getWidth();
-                       $fields['height']   = $image->getHeight();
-                       $fields['blurhash'] = $image->getBlurHash();
+
+               if (!$curlResult->isSuccess()) {
+                       Logger::notice('Fetching unsuccessful', ['url' => $url]);
+                       return [];
+               }
+
+               $fields = ['mimetype' => $curlResult->getContentType()];
+
+               if (Images::isSupportedMimeType($fields['mimetype'])) {
+                       $img_str = $curlResult->getBodyString();
+                       $image = new Image($img_str, $fields['mimetype'], $url);
+                       if ($image->isValid()) {
+                               $fields['mimetype'] = $image->getType();
+                               $fields['width']    = $image->getWidth();
+                               $fields['height']   = $image->getHeight();
+                               $fields['blurhash'] = $image->getBlurHash();
+                       }
                }
 
                return $fields;
index cbbfdb97ec6727f5be4d4d27d950f0f1648befde..346a6a1d00807e1173213b8323a55fac5c818732 100644 (file)
@@ -196,7 +196,7 @@ class Media
 
                        if ($curlResult->isSuccess()) {
                                if (empty($media['mimetype'])) {
-                                       $media['mimetype'] = $curlResult->getHeader('Content-Type')[0] ?? '';
+                                       $media['mimetype'] = $curlResult->getContentType() ?? '';
                                }
                                if (empty($media['size'])) {
                                        $media['size'] = (int)($curlResult->getHeader('Content-Length')[0] ?? 0);
index 13587a342a18a424d216eec4d20bad3ce5afda75..9472407715a6158d55d68240da264ad1467e1fd8 100644 (file)
@@ -1403,9 +1403,7 @@ class User
                                $type = '';
                        }
 
-                       $type = Images::getMimeTypeByData($img_str, $photo, $type);
-
-                       $image = new Image($img_str, $type);
+                       $image = new Image($img_str, $type, $photo);
                        if ($image->isValid()) {
                                $image->scaleToSquare(300);
 
index 882f873408a0349b1684f05ad4cb24a68792f2ad..8faa356dd60f57dc31390115d39c1de46045b5a1 100644 (file)
@@ -95,7 +95,7 @@ class Instance extends BaseApi
 
                return new InstanceV2Entity\Configuration(
                        $statuses_config,
-                       new InstanceV2Entity\MediaAttachmentsConfig(array_keys(Images::supportedTypes()), $image_size_limit, $image_matrix_limit),
+                       new InstanceV2Entity\MediaAttachmentsConfig(Images::supportedMimeTypes(), $image_size_limit, $image_matrix_limit),
                        new InstanceV2Entity\Polls(),
                        new InstanceV2Entity\Accounts(),
                );
index 638f329867c7654a9af35793f181273d0d86adf6..a3d268311e7e24ebb51559eb150f30f0587ef267 100644 (file)
@@ -131,7 +131,7 @@ class InstanceV2 extends BaseApi
 
                return new InstanceEntity\Configuration(
                        $statuses_config,
-                       new InstanceEntity\MediaAttachmentsConfig(array_keys(Images::supportedTypes()), $image_size_limit, $image_matrix_limit),
+                       new InstanceEntity\MediaAttachmentsConfig(Images::supportedMimeTypes(), $image_size_limit, $image_matrix_limit),
                        new InstanceEntity\Polls(),
                        new InstanceEntity\Accounts(),
                );
index 33b9b3b83048236ecca892e6ef5297f33495a4fb..9add05376c3a111f4c2e0eaee76510c852a3679b 100644 (file)
@@ -403,11 +403,10 @@ class Statuses extends BaseApi
 
                        Photo::setPermissionForResource($media[0]['resource-id'], $item['uid'], $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']);
 
-                       $phototypes = Images::supportedTypes();
-                       $ext = $phototypes[$media[0]['type']];
+                       $ext = Images::getExtensionByMimeType($media[0]['type']);
 
                        $attachment = ['type' => Post\Media::IMAGE, 'mimetype' => $media[0]['type'],
-                               'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . '.' . $ext,
+                               'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . $ext,
                                'size' => $media[0]['datasize'],
                                'name' => $media[0]['filename'] ?: $media[0]['resource-id'],
                                'description' => $media[0]['desc'] ?? '',
@@ -415,7 +414,7 @@ class Statuses extends BaseApi
                                'height' => $media[0]['height']];
 
                        if (count($media) > 1) {
-                               $attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . '.' . $ext;
+                               $attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . $ext;
                                $attachment['preview-width'] = $media[1]['width'];
                                $attachment['preview-height'] = $media[1]['height'];
                        }
index da9d30422ca03b4acbce567ee628a92638cc490a..8e5cc976d879ea1a11fe26dff2cdcdbdf129a271 100644 (file)
@@ -155,13 +155,12 @@ class Update extends BaseApi
 
                                Photo::setPermissionForResource($media[0]['resource-id'], $uid, $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']);
 
-                               $phototypes = Images::supportedTypes();
-                               $ext        = $phototypes[$media[0]['type']];
+                               $ext = Images::getExtensionByMimeType($media[0]['type']);
 
                                $attachment = [
                                        'type'        => Post\Media::IMAGE,
                                        'mimetype'    => $media[0]['type'],
-                                       'url'         => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . '.' . $ext,
+                                       'url'         => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . $ext,
                                        'size'        => $media[0]['datasize'],
                                        'name'        => $media[0]['filename'] ?: $media[0]['resource-id'],
                                        'description' => $media[0]['desc'] ?? '',
@@ -170,7 +169,7 @@ class Update extends BaseApi
                                ];
 
                                if (count($media) > 1) {
-                                       $attachment['preview']        = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . '.' . $ext;
+                                       $attachment['preview']        = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . $ext;
                                        $attachment['preview-width']  = $media[1]['width'];
                                        $attachment['preview-height'] = $media[1]['height'];
                                }
index e667e02ba7f465e56fe5461a59fa53d38508d129..06b2b7107c0007554caaaa08d5e061c18966643b 100644 (file)
@@ -99,8 +99,7 @@ class Browser extends BaseModule
 
        protected function map_files(array $record): array
        {
-               $types      = Images::supportedTypes();
-               $ext        = $types[$record['type']];
+               $ext        = Images::getExtensionByMimeType($record['type']);
                $filename_e = $record['filename'];
 
                // Take the largest picture that is smaller or equal 640 pixels
@@ -118,7 +117,7 @@ class Browser extends BaseModule
                return [
                        sprintf('%s/photos/%s/image/%s', $this->baseUrl, $this->app->getLoggedInUserNickname(), $record['resource-id']),
                        $filename_e,
-                       sprintf('%s/photo/%s-%s.%s', $this->baseUrl, $record['resource-id'], $scale, $ext),
+                       sprintf('%s/photo/%s-%s%s', $this->baseUrl, $record['resource-id'], $scale, $ext),
                        $record['desc'],
                ];
        }
index d5d01290cdc44467e8b75641140ede43332c096d..6594b453f8d85d68eb014665a3a55ff386fe8536 100644 (file)
@@ -135,8 +135,6 @@ class Upload extends \Friendica\BaseModule
                        $this->return(401, $this->t('Invalid request.'), true);
                }
 
-               $filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
-
                $this->logger->info('File upload:', [
                        'src'      => $src,
                        'filename' => $filename,
@@ -145,7 +143,7 @@ class Upload extends \Friendica\BaseModule
                ]);
 
                $imagedata = @file_get_contents($src);
-               $image     = new Image($imagedata, $filetype);
+               $image     = new Image($imagedata, $filetype, $filename);
 
                if (!$image->isValid()) {
                        @unlink($src);
index 1ab228b7c33a4ccb032fc31d992fbf3b75a0962e..3a064c48ac5f699d797a616bb5d29092fb216cad 100644 (file)
@@ -167,14 +167,16 @@ class Photo extends BaseApi
                $stamp = microtime(true);
 
                if (empty($request['blur']) || empty($photo['blurhash'])) {
-                       $imgdata = MPhoto::getImageDataForPhoto($photo);
+                       $imgdata  = MPhoto::getImageDataForPhoto($photo);
+                       $mimetype = $photo['type'];
                }
                if (empty($imgdata) && empty($photo['blurhash'])) {
                        throw new HTTPException\NotFoundException();
                } elseif (empty($imgdata) && !empty($photo['blurhash'])) {
-                       $image = New Image('', 'image/png');
+                       $image = New Image('', image_type_to_mime_type(IMAGETYPE_WEBP));
                        $image->getFromBlurHash($photo['blurhash'], $photo['width'], $photo['height']);
-                       $imgdata = $image->asString();
+                       $imgdata  = $image->asString();
+                       $mimetype = $image->getType();
                }
 
                // The mimetype for an external or system resource can only be known reliably after it had been fetched
@@ -199,20 +201,23 @@ class Photo extends BaseApi
                }
 
                if (!empty($request['static'])) {
-                       $img = new Image($imgdata, $photo['type']);
+                       $img = new Image($imgdata, $photo['type'], $photo['filename']);
                        $img->toStatic();
-                       $imgdata = $img->asString();
+                       $imgdata  = $img->asString();
+                       $mimetype = $img->getType();
                }
 
                // if customsize is set and image is not a gif, resize it
-               if ($photo['type'] !== 'image/gif' && $customsize > 0 && $customsize <= Proxy::PIXEL_THUMB && $square_resize) {
-                       $img = new Image($imgdata, $photo['type']);
+               if ($photo['type'] !== image_type_to_mime_type(IMAGETYPE_GIF) && $customsize > 0 && $customsize <= Proxy::PIXEL_THUMB && $square_resize) {
+                       $img = new Image($imgdata, $photo['type'], $photo['filename']);
                        $img->scaleToSquare($customsize);
-                       $imgdata = $img->asString();
-               } elseif ($photo['type'] !== 'image/gif' && $customsize > 0) {
-                       $img = new Image($imgdata, $photo['type']);
+                       $imgdata  = $img->asString();
+                       $mimetype = $img->getType();
+               } elseif ($photo['type'] !== image_type_to_mime_type(IMAGETYPE_GIF) && $customsize > 0) {
+                       $img = new Image($imgdata, $photo['type'], $photo['filename']);
                        $img->scaleDown($customsize);
-                       $imgdata = $img->asString();
+                       $imgdata  = $img->asString();
+                       $mimetype = $img->getType();
                }
 
                if (function_exists('header_remove')) {
@@ -220,7 +225,7 @@ class Photo extends BaseApi
                        header_remove('pragma');
                }
 
-               header('Content-type: ' . $photo['type']);
+               header('Content-type: ' . $mimetype);
 
                $stamp = microtime(true);
                if (!$cacheable) {
@@ -391,7 +396,7 @@ class Photo extends BaseApi
                                        }
                                }
                                if (empty($mimetext) && !empty($contact['blurhash'])) {
-                                       $image = New Image('', 'image/png');
+                                       $image = New Image('', image_type_to_mime_type(IMAGETYPE_WEBP));
                                        $image->getFromBlurHash($contact['blurhash'], $customsize, $customsize);
                                        return MPhoto::createPhotoForImageData($image->asString());
                                } elseif (empty($mimetext)) {
index 8b915a4eba38429792c7e859ca308b7f3467a596..1987cdc8211c6d097fb518544e88e956795dee10 100644 (file)
@@ -184,8 +184,6 @@ class Photos extends \Friendica\Module\BaseProfile
                        return;
                }
 
-               $type = Images::getMimeTypeBySource($src, $filename, $type);
-
                $this->logger->info('photos: upload: received file: ' . $filename . ' as ' . $src . ' ('. $type . ') ' . $filesize . ' bytes');
 
                $maximagesize = Strings::getBytesFromShorthand($this->config->get('system', 'maximagesize'));
@@ -210,7 +208,7 @@ class Photos extends \Friendica\Module\BaseProfile
 
                $imagedata = @file_get_contents($src);
 
-               $image = new Image($imagedata, $type);
+               $image = new Image($imagedata, $type, $filename);
 
                if (!$image->isValid()) {
                        $this->logger->notice('unable to process image');
@@ -341,14 +339,12 @@ class Photos extends \Friendica\Module\BaseProfile
                        $pager->getItemsPerPage()
                ));
 
-               $phototypes = Images::supportedTypes();
-
-               $photos = array_map(function ($photo) use ($phototypes) {
+               $photos = array_map(function ($photo){
                        return [
                                'id'    => $photo['id'],
                                'link'  => 'photos/' . $this->owner['nickname'] . '/image/' . $photo['resource-id'],
                                'title' => $this->t('View Photo'),
-                               'src'   => 'photo/' . $photo['resource-id'] . '-' . ((($photo['scale']) == 6) ? 4 : $photo['scale']) . '.' . $phototypes[$photo['type']],
+                               'src'   => 'photo/' . $photo['resource-id'] . '-' . ((($photo['scale']) == 6) ? 4 : $photo['scale']) . Images::getExtensionByMimeType($photo['type']),
                                'alt'   => $photo['filename'],
                                'album' => [
                                        'link' => 'photos/' . $this->owner['nickname'] . '/album/' . bin2hex($photo['album']),
index 0eb95d8a3fb1a6f2dd615227e8f29e3ac1e39030..12f58f3a844dfacfadb99feb2e5e5f0cbae24dd1 100644 (file)
@@ -99,17 +99,15 @@ class Proxy extends BaseModule
 
                Logger::debug('Got picture', ['Content-Type' => $fetchResult->getHeader('Content-Type'), 'uid' => DI::userSession()->getLocalUserId(), 'image' => $request['url']]);
 
-               $mime = Images::getMimeTypeByData($img_str);
-
-               $image = new Image($img_str, $mime);
+               $image = new Image($img_str, $fetchResult->getContentType(), $request['url']);
                if (!$image->isValid()) {
-                       Logger::notice('The image is invalid', ['image' => $request['url'], 'mime' => $mime]);
+                       Logger::notice('The image is invalid', ['image' => $request['url'], 'mime' => $fetchResult->getContentType()]);
                        self::responseError();
                        // stop.
                }
 
                // reduce quality - if it isn't a GIF
-               if ($image->getType() != 'image/gif') {
+               if ($image->getImageType() != IMAGETYPE_GIF) {
                        $image->scaleDown($request['size']);
                }
 
index ef621b1823ea91eac2c6c0623ee4ea1b8208a839..a9988d283007c7949888f33296c3aaa72f83031a 100644 (file)
@@ -213,7 +213,7 @@ class Crop extends BaseSettings
 
                DI::page()['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/profile/photo/crop_head.tpl'), []);
 
-               $filename = $imagecrop['resource-id'] . '-' . $imagecrop['scale'] . '.' . $imagecrop['ext'];
+               $filename = $imagecrop['resource-id'] . '-' . $imagecrop['scale'] . $imagecrop['ext'];
                $tpl = Renderer::getMarkupTemplate('settings/profile/photo/crop.tpl');
                $o = Renderer::replaceMacros($tpl, [
                        '$filename'  => $filename,
index 3f44de357ed8df13f3358880fcc3e5a55a78e295..90cbc9f886288307ea6eb567c8a94661c2f89d99 100644 (file)
@@ -52,8 +52,6 @@ class Index extends BaseSettings
                $filesize = intval($_FILES['userfile']['size']);
                $filetype = $_FILES['userfile']['type'];
 
-               $filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
-
                $maximagesize = Strings::getBytesFromShorthand(DI::config()->get('system', 'maximagesize', 0));
 
                if ($maximagesize && $filesize > $maximagesize) {
@@ -63,7 +61,7 @@ class Index extends BaseSettings
                }
 
                $imagedata = @file_get_contents($src);
-               $Image = new Image($imagedata, $filetype);
+               $Image = new Image($imagedata, $filetype, $filename);
 
                if (!$Image->isValid()) {
                        DI::sysmsg()->addNotice(DI::l10n()->t('Unable to process image.'));
index 252649819b3cb459ae38d9bfbee6da79c21c4ee3..4d03e3996df1aa38eead5590003a4414f8a3c5ef 100644 (file)
@@ -392,7 +392,7 @@ class Import extends \Friendica\BaseModule
                        $photo['data'] = hex2bin($photo['data']);
 
                        $r = Photo::store(
-                               new Image($photo['data'], $photo['type']),
+                               new Image($photo['data'], $photo['type'], $photo['filename']),
                                $photo['uid'], $photo['contact-id'], //0
                                $photo['resource-id'], $photo['filename'], $photo['album'], $photo['scale'], $photo['profile'], //1
                                $photo['allow_cid'], $photo['allow_gid'], $photo['deny_cid'], $photo['deny_gid']
index 76eb736fe2453d3009ec6a666ea4c7357738a5a0..11c317acb4bbfa8e32b1a35c15670352e17a9e12 100644 (file)
@@ -32,7 +32,7 @@ class HttpClientAccept
        public const ATOM_XML  = 'application/atom+xml,text/xml;q=0.9,*/*;q=0.8';
        public const FEED_XML  = 'application/atom+xml,application/rss+xml;q=0.9,application/rdf+xml;q=0.8,text/xml;q=0.7,*/*;q=0.6';
        public const HTML      = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
-       public const IMAGE     = 'image/png,image/jpeg,image/gif,image/*;q=0.9,*/*;q=0.8';
+       public const IMAGE     = 'image/webp,image/png,image/jpeg,image/gif,image/*;q=0.9,*/*;q=0.8'; // @todo add image/avif once our minimal supported PHP version is 8.1.0
        public const JRD_JSON  = 'application/jrd+json,application/json;q=0.9';
        public const JSON      = 'application/json,*/*;q=0.9';
        public const JSON_AS   = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
index 5987e26d6fdb95254efe0928620dd3ead6f7748f..d938643751f8e276e538b5862d7cfba6ee7bcc3e 100644 (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 '';
                }
index 4d44b8a40fe3f4f17b6f3ef1eb18bd17bbd76fba..b58faed679b8effc7120c67fd6c0b528f61f60de 100644 (file)
@@ -304,10 +304,8 @@ class DFRN
                $profilephotos = Photo::selectToArray(['resource-id', 'scale', 'type'], ['profile' => true, 'uid' => $uid], ['order' => ['scale']]);
 
                $photos = [];
-               $ext = Images::supportedTypes();
-
                foreach ($profilephotos as $p) {
-                       $photos[$p['scale']] = DI::baseUrl() . '/photo/' . $p['resource-id'] . '-' . $p['scale'] . '.' . $ext[$p['type']];
+                       $photos[$p['scale']] = DI::baseUrl() . '/photo/' . $p['resource-id'] . '-' . $p['scale'] . Images::getExtensionByMimeType($p['type']);
                }
 
                $doc = new DOMDocument('1.0', 'utf-8');
index c67894494c6bea23a17486545196773cd2282510..e0ab3dd49a34ad7af0249057b1f6cb01ab2d5c36 100644 (file)
@@ -43,6 +43,7 @@ use Friendica\Model\Tag;
 use Friendica\Model\User;
 use Friendica\Network\HTTPException;
 use Friendica\Util\DateTimeFormat;
+use Friendica\Util\Images;
 use Friendica\Util\Network;
 use Friendica\Util\ParseUrl;
 use Friendica\Util\Proxy;
@@ -573,7 +574,7 @@ class Feed
                        if (in_array($fetch_further_information, [LocalRelationship::FFI_INFORMATION, LocalRelationship::FFI_BOTH])) {
                                // Handle enclosures and treat them as preview picture
                                foreach ($attachments as $attachment) {
-                                       if ($attachment['mimetype'] == 'image/jpeg') {
+                                       if (Images::isSupportedMimeType($attachment['mimetype'])) {
                                                $preview = $attachment['url'];
                                        }
                                }
index e5b8afbb545cb24fda69f574468d07ffb75dda80..33bae87a794b9665ce8d6787be4b53a59e24a577 100644 (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',
-               ];
-
-               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'
-                       ];
+               $types = [];
+
+               // @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;
+               }
+
+               $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;
                }
 
-               return self::guessTypeByExtension($filename);
+               $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
                );
        }
index cd9669624afd1fda214cc8bf0acce4802a529f5f..128716057620649555e99a5889e205ab71e28dd5 100644 (file)
@@ -87,7 +87,7 @@ class ParseUrl
                        return [];
                }
 
-               $contenttype =  $curlResult->getHeader('Content-Type')[0] ?? '';
+               $contenttype =  $curlResult->getContentType();
                if (empty($contenttype)) {
                        return ['application', 'octet-stream'];
                }
index 2d0345b1d89cb0bf9b6ad75f6c13ba1fe523ea16..21146e801a1bad4d88de91651e25bb80006397fb 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: 2024.03-rc\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-13 05:20+0000\n"
+"POT-Creation-Date: 2024-02-16 02:33+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -45,7 +45,7 @@ msgid "Item not found."
 msgstr ""
 
 #: mod/item.php:457 mod/message.php:67 mod/message.php:113 mod/notes.php:45
-#: mod/photos.php:152 mod/photos.php:670 src/Model/Event.php:520
+#: mod/photos.php:150 mod/photos.php:666 src/Model/Event.php:520
 #: src/Module/Attach.php:55 src/Module/BaseApi.php:103
 #: src/Module/BaseNotifications.php:98 src/Module/BaseSettings.php:50
 #: src/Module/Calendar/Event/API.php:88 src/Module/Calendar/Event/Form.php:84
@@ -70,7 +70,7 @@ msgstr ""
 #: src/Module/Settings/Channels.php:135 src/Module/Settings/Delegation.php:90
 #: src/Module/Settings/Display.php:90 src/Module/Settings/Display.php:199
 #: src/Module/Settings/Profile/Photo/Crop.php:165
-#: src/Module/Settings/Profile/Photo/Index.php:112
+#: src/Module/Settings/Profile/Photo/Index.php:110
 #: src/Module/Settings/RemoveMe.php:119 src/Module/Settings/UserExport.php:80
 #: src/Module/Settings/UserExport.php:114
 #: src/Module/Settings/UserExport.php:215
@@ -289,16 +289,16 @@ msgstr ""
 msgid "Insert web link"
 msgstr ""
 
-#: mod/message.php:201 mod/message.php:357 mod/photos.php:1301
+#: mod/message.php:201 mod/message.php:357 mod/photos.php:1297
 #: src/Content/Conversation.php:401 src/Content/Conversation.php:1586
 #: src/Module/Item/Compose.php:206 src/Module/Post/Edit.php:145
 #: src/Object/Post.php:609
 msgid "Please wait"
 msgstr ""
 
-#: mod/message.php:202 mod/message.php:356 mod/photos.php:705
-#: mod/photos.php:824 mod/photos.php:1101 mod/photos.php:1142
-#: mod/photos.php:1198 mod/photos.php:1278
+#: mod/message.php:202 mod/message.php:356 mod/photos.php:701
+#: mod/photos.php:820 mod/photos.php:1097 mod/photos.php:1138
+#: mod/photos.php:1194 mod/photos.php:1274
 #: src/Module/Calendar/Event/Form.php:250 src/Module/Contact/Advanced.php:132
 #: src/Module/Contact/Profile.php:364
 #: src/Module/Debug/ActivityPubConversion.php:140
@@ -386,7 +386,7 @@ msgstr ""
 msgid "Save"
 msgstr ""
 
-#: mod/photos.php:67 mod/photos.php:132 mod/photos.php:578
+#: mod/photos.php:67 mod/photos.php:132 mod/photos.php:576
 #: src/Model/Event.php:512 src/Model/Profile.php:233
 #: src/Module/Calendar/Export.php:74 src/Module/Calendar/Show.php:74
 #: src/Module/DFRN/Poll.php:43 src/Module/Feed.php:65 src/Module/HCard.php:51
@@ -399,99 +399,99 @@ msgid "User not found."
 msgstr ""
 
 #: mod/photos.php:106 src/Module/BaseProfile.php:68
-#: src/Module/Profile/Photos.php:379
+#: src/Module/Profile/Photos.php:375
 msgid "Photo Albums"
 msgstr ""
 
-#: mod/photos.php:107 src/Module/Profile/Photos.php:380
-#: src/Module/Profile/Photos.php:400
+#: mod/photos.php:107 src/Module/Profile/Photos.php:376
+#: src/Module/Profile/Photos.php:396
 msgid "Recent Photos"
 msgstr ""
 
-#: mod/photos.php:109 mod/photos.php:872 src/Module/Profile/Photos.php:382
-#: src/Module/Profile/Photos.php:402
+#: mod/photos.php:109 mod/photos.php:868 src/Module/Profile/Photos.php:378
+#: src/Module/Profile/Photos.php:398
 msgid "Upload New Photos"
 msgstr ""
 
 #: mod/photos.php:121 src/Module/BaseSettings.php:72
-#: src/Module/Profile/Photos.php:363
+#: src/Module/Profile/Photos.php:359
 msgid "everybody"
 msgstr ""
 
-#: mod/photos.php:159
+#: mod/photos.php:157
 msgid "Contact information unavailable"
 msgstr ""
 
-#: mod/photos.php:188
+#: mod/photos.php:186
 msgid "Album not found."
 msgstr ""
 
-#: mod/photos.php:244
+#: mod/photos.php:242
 msgid "Album successfully deleted"
 msgstr ""
 
-#: mod/photos.php:246
+#: mod/photos.php:244
 msgid "Album was empty."
 msgstr ""
 
-#: mod/photos.php:277
+#: mod/photos.php:275
 msgid "Failed to delete the photo."
 msgstr ""
 
-#: mod/photos.php:545
+#: mod/photos.php:543
 msgid "a photo"
 msgstr ""
 
-#: mod/photos.php:545
+#: mod/photos.php:543
 #, php-format
 msgid "%1$s was tagged in %2$s by %3$s"
 msgstr ""
 
-#: mod/photos.php:582 src/Module/Conversation/Community.php:160
-#: src/Module/Directory.php:48 src/Module/Profile/Photos.php:295
+#: mod/photos.php:580 src/Module/Conversation/Community.php:160
+#: src/Module/Directory.php:48 src/Module/Profile/Photos.php:293
 #: src/Module/Search/Index.php:65
 msgid "Public access denied."
 msgstr ""
 
-#: mod/photos.php:587
+#: mod/photos.php:585
 msgid "No photos selected"
 msgstr ""
 
-#: mod/photos.php:721
+#: mod/photos.php:717
 #, php-format
 msgid "The maximum accepted image size is %s"
 msgstr ""
 
-#: mod/photos.php:728
+#: mod/photos.php:724
 msgid "Upload Photos"
 msgstr ""
 
-#: mod/photos.php:732 mod/photos.php:820
+#: mod/photos.php:728 mod/photos.php:816
 msgid "New album name: "
 msgstr ""
 
-#: mod/photos.php:733
+#: mod/photos.php:729
 msgid "or select existing album:"
 msgstr ""
 
-#: mod/photos.php:734
+#: mod/photos.php:730
 msgid "Do not show a status post for this upload"
 msgstr ""
 
-#: mod/photos.php:736 mod/photos.php:1097 src/Content/Conversation.php:403
+#: mod/photos.php:732 mod/photos.php:1093 src/Content/Conversation.php:403
 #: src/Module/Calendar/Event/Form.php:253 src/Module/Post/Edit.php:183
 msgid "Permissions"
 msgstr ""
 
-#: mod/photos.php:801
+#: mod/photos.php:797
 msgid "Do you really want to delete this photo album and all its photos?"
 msgstr ""
 
-#: mod/photos.php:802 mod/photos.php:825
+#: mod/photos.php:798 mod/photos.php:821
 msgid "Delete Album"
 msgstr ""
 
-#: mod/photos.php:803 mod/photos.php:903 src/Content/Conversation.php:419
+#: mod/photos.php:799 mod/photos.php:899 src/Content/Conversation.php:419
 #: src/Module/Contact/Follow.php:173 src/Module/Contact/Revoke.php:109
 #: src/Module/Contact/Unfollow.php:126
 #: src/Module/Media/Attachment/Browser.php:77
@@ -501,132 +501,132 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
-#: mod/photos.php:829
+#: mod/photos.php:825
 msgid "Edit Album"
 msgstr ""
 
-#: mod/photos.php:830
+#: mod/photos.php:826
 msgid "Drop Album"
 msgstr ""
 
-#: mod/photos.php:834
+#: mod/photos.php:830
 msgid "Show Newest First"
 msgstr ""
 
-#: mod/photos.php:836
+#: mod/photos.php:832
 msgid "Show Oldest First"
 msgstr ""
 
-#: mod/photos.php:857 src/Module/Profile/Photos.php:350
+#: mod/photos.php:853 src/Module/Profile/Photos.php:346
 msgid "View Photo"
 msgstr ""
 
-#: mod/photos.php:889
+#: mod/photos.php:885
 msgid "Permission denied. Access to this item may be restricted."
 msgstr ""
 
-#: mod/photos.php:891
+#: mod/photos.php:887
 msgid "Photo not available"
 msgstr ""
 
-#: mod/photos.php:901
+#: mod/photos.php:897
 msgid "Do you really want to delete this photo?"
 msgstr ""
 
-#: mod/photos.php:902 mod/photos.php:1102
+#: mod/photos.php:898 mod/photos.php:1098
 msgid "Delete Photo"
 msgstr ""
 
-#: mod/photos.php:1000
+#: mod/photos.php:996
 msgid "View photo"
 msgstr ""
 
-#: mod/photos.php:1002
+#: mod/photos.php:998
 msgid "Edit photo"
 msgstr ""
 
-#: mod/photos.php:1003
+#: mod/photos.php:999
 msgid "Delete photo"
 msgstr ""
 
-#: mod/photos.php:1004
+#: mod/photos.php:1000
 msgid "Use as profile photo"
 msgstr ""
 
-#: mod/photos.php:1011
+#: mod/photos.php:1007
 msgid "Private Photo"
 msgstr ""
 
-#: mod/photos.php:1017
+#: mod/photos.php:1013
 msgid "View Full Size"
 msgstr ""
 
-#: mod/photos.php:1070
+#: mod/photos.php:1066
 msgid "Tags: "
 msgstr ""
 
-#: mod/photos.php:1073
+#: mod/photos.php:1069
 msgid "[Select tags to remove]"
 msgstr ""
 
-#: mod/photos.php:1088
+#: mod/photos.php:1084
 msgid "New album name"
 msgstr ""
 
-#: mod/photos.php:1089
+#: mod/photos.php:1085
 msgid "Caption"
 msgstr ""
 
-#: mod/photos.php:1090
+#: mod/photos.php:1086
 msgid "Add a Tag"
 msgstr ""
 
-#: mod/photos.php:1090
+#: mod/photos.php:1086
 msgid "Example: @bob, @Barbara_Jensen, @jim@example.com, #California, #camping"
 msgstr ""
 
-#: mod/photos.php:1091
+#: mod/photos.php:1087
 msgid "Do not rotate"
 msgstr ""
 
-#: mod/photos.php:1092
+#: mod/photos.php:1088
 msgid "Rotate CW (right)"
 msgstr ""
 
-#: mod/photos.php:1093
+#: mod/photos.php:1089
 msgid "Rotate CCW (left)"
 msgstr ""
 
-#: mod/photos.php:1139 mod/photos.php:1195 mod/photos.php:1275
+#: mod/photos.php:1135 mod/photos.php:1191 mod/photos.php:1271
 #: src/Module/Contact.php:618 src/Module/Item/Compose.php:188
 #: src/Object/Post.php:1151
 msgid "This is you"
 msgstr ""
 
-#: mod/photos.php:1141 mod/photos.php:1197 mod/photos.php:1277
+#: mod/photos.php:1137 mod/photos.php:1193 mod/photos.php:1273
 #: src/Module/Moderation/Reports.php:95 src/Object/Post.php:603
 #: src/Object/Post.php:1153
 msgid "Comment"
 msgstr ""
 
-#: mod/photos.php:1143 mod/photos.php:1199 mod/photos.php:1279
+#: mod/photos.php:1139 mod/photos.php:1195 mod/photos.php:1275
 #: src/Content/Conversation.php:416 src/Module/Calendar/Event/Form.php:248
 #: src/Module/Item/Compose.php:201 src/Module/Post/Edit.php:165
 #: src/Object/Post.php:1167
 msgid "Preview"
 msgstr ""
 
-#: mod/photos.php:1144 src/Content/Conversation.php:369
+#: mod/photos.php:1140 src/Content/Conversation.php:369
 #: src/Module/Post/Edit.php:130 src/Object/Post.php:1155
 msgid "Loading..."
 msgstr ""
 
-#: mod/photos.php:1236 src/Content/Conversation.php:1501
+#: mod/photos.php:1232 src/Content/Conversation.php:1501
 #: src/Object/Post.php:261
 msgid "Select"
 msgstr ""
 
-#: mod/photos.php:1237 src/Content/Conversation.php:1502
+#: mod/photos.php:1233 src/Content/Conversation.php:1502
 #: src/Module/Moderation/Users/Active.php:136
 #: src/Module/Moderation/Users/Blocked.php:136
 #: src/Module/Moderation/Users/Index.php:151
@@ -635,23 +635,23 @@ msgstr ""
 msgid "Delete"
 msgstr ""
 
-#: mod/photos.php:1298 src/Object/Post.php:426
+#: mod/photos.php:1294 src/Object/Post.php:426
 msgid "Like"
 msgstr ""
 
-#: mod/photos.php:1299 src/Object/Post.php:426
+#: mod/photos.php:1295 src/Object/Post.php:426
 msgid "I like this (toggle)"
 msgstr ""
 
-#: mod/photos.php:1300 src/Object/Post.php:427
+#: mod/photos.php:1296 src/Object/Post.php:427
 msgid "Dislike"
 msgstr ""
 
-#: mod/photos.php:1302 src/Object/Post.php:427
+#: mod/photos.php:1298 src/Object/Post.php:427
 msgid "I don't like this (toggle)"
 msgstr ""
 
-#: mod/photos.php:1324
+#: mod/photos.php:1320
 msgid "Map"
 msgstr ""
 
@@ -1803,31 +1803,31 @@ msgstr ""
 msgid "Follow Thread"
 msgstr ""
 
-#: src/Content/Item.php:430 src/Model/Contact.php:1250
+#: src/Content/Item.php:430 src/Model/Contact.php:1243
 msgid "View Status"
 msgstr ""
 
-#: src/Content/Item.php:431 src/Content/Item.php:452 src/Model/Contact.php:1184
-#: src/Model/Contact.php:1241 src/Model/Contact.php:1251
+#: src/Content/Item.php:431 src/Content/Item.php:452 src/Model/Contact.php:1177
+#: src/Model/Contact.php:1234 src/Model/Contact.php:1244
 #: src/Module/Directory.php:157 src/Module/Settings/Profile/Index.php:259
 msgid "View Profile"
 msgstr ""
 
-#: src/Content/Item.php:432 src/Model/Contact.php:1252
+#: src/Content/Item.php:432 src/Model/Contact.php:1245
 msgid "View Photos"
 msgstr ""
 
-#: src/Content/Item.php:433 src/Model/Contact.php:1219
+#: src/Content/Item.php:433 src/Model/Contact.php:1212
 #: src/Model/Profile.php:468
 msgid "Network Posts"
 msgstr ""
 
-#: src/Content/Item.php:434 src/Model/Contact.php:1243
-#: src/Model/Contact.php:1254
+#: src/Content/Item.php:434 src/Model/Contact.php:1236
+#: src/Model/Contact.php:1247
 msgid "View Contact"
 msgstr ""
 
-#: src/Content/Item.php:435 src/Model/Contact.php:1255
+#: src/Content/Item.php:435 src/Model/Contact.php:1248
 msgid "Send PM"
 msgstr ""
 
@@ -1863,7 +1863,7 @@ msgid "Languages"
 msgstr ""
 
 #: src/Content/Item.php:449 src/Content/Widget.php:80
-#: src/Model/Contact.php:1244 src/Model/Contact.php:1256
+#: src/Model/Contact.php:1237 src/Model/Contact.php:1249
 #: src/Module/Contact/Follow.php:167 view/theme/vier/theme.php:195
 msgid "Connect/Follow"
 msgstr ""
@@ -2190,39 +2190,39 @@ msgstr ""
 msgid "last"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:766 src/Content/Text/BBCode.php:1727
-#: src/Content/Text/BBCode.php:1728
+#: src/Content/Text/BBCode.php:767 src/Content/Text/BBCode.php:1728
+#: src/Content/Text/BBCode.php:1729
 msgid "Image/photo"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:984
+#: src/Content/Text/BBCode.php:985
 #, php-format
 msgid ""
 "<a href=\"%1$s\" target=\"_blank\" rel=\"noopener noreferrer\">%2$s</a> %3$s"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:1009 src/Model/Item.php:3999
+#: src/Content/Text/BBCode.php:1010 src/Model/Item.php:3999
 #: src/Model/Item.php:4005 src/Model/Item.php:4006
 msgid "Link to source"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:1634 src/Content/Text/HTML.php:905
+#: src/Content/Text/BBCode.php:1635 src/Content/Text/HTML.php:905
 msgid "Click to open/close"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:1667
+#: src/Content/Text/BBCode.php:1668
 msgid "$1 wrote:"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:1732 src/Content/Text/BBCode.php:1733
+#: src/Content/Text/BBCode.php:1733 src/Content/Text/BBCode.php:1734
 msgid "Encrypted content"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:1996
+#: src/Content/Text/BBCode.php:1997
 msgid "Invalid source protocol"
 msgstr ""
 
-#: src/Content/Text/BBCode.php:2015
+#: src/Content/Text/BBCode.php:2016
 msgid "Invalid link protocol"
 msgstr ""
 
@@ -2370,7 +2370,7 @@ msgstr ""
 msgid "Organisations"
 msgstr ""
 
-#: src/Content/Widget.php:536 src/Model/Contact.php:1746
+#: src/Content/Widget.php:536 src/Model/Contact.php:1739
 msgid "News"
 msgstr ""
 
@@ -2438,12 +2438,12 @@ msgstr[1] ""
 msgid "More Trending Tags"
 msgstr ""
 
-#: src/Content/Widget/VCard.php:104 src/Model/Contact.php:1212
+#: src/Content/Widget/VCard.php:104 src/Model/Contact.php:1205
 #: src/Model/Profile.php:461
 msgid "Post to group"
 msgstr ""
 
-#: src/Content/Widget/VCard.php:109 src/Model/Contact.php:1217
+#: src/Content/Widget/VCard.php:109 src/Model/Contact.php:1210
 #: src/Model/Profile.php:466 src/Module/Moderation/Item/Source.php:85
 msgid "Mention"
 msgstr ""
@@ -2471,13 +2471,13 @@ msgstr ""
 msgid "Network:"
 msgstr ""
 
-#: src/Content/Widget/VCard.php:128 src/Model/Contact.php:1245
-#: src/Model/Contact.php:1257 src/Model/Profile.php:479
+#: src/Content/Widget/VCard.php:128 src/Model/Contact.php:1238
+#: src/Model/Contact.php:1250 src/Model/Profile.php:479
 #: src/Module/Contact/Profile.php:463
 msgid "Unfollow"
 msgstr ""
 
-#: src/Content/Widget/VCard.php:134 src/Model/Contact.php:1214
+#: src/Content/Widget/VCard.php:134 src/Model/Contact.php:1207
 #: src/Model/Profile.php:463
 msgid "View group"
 msgstr ""
@@ -2849,23 +2849,19 @@ msgstr ""
 msgid "TLS detected"
 msgstr ""
 
-#: src/Core/Installer.php:646
+#: src/Core/Installer.php:636
 msgid "ImageMagick PHP extension is not installed"
 msgstr ""
 
-#: src/Core/Installer.php:648
+#: src/Core/Installer.php:638
 msgid "ImageMagick PHP extension is installed"
 msgstr ""
 
-#: src/Core/Installer.php:650
-msgid "ImageMagick supports GIF"
-msgstr ""
-
-#: src/Core/Installer.php:672
+#: src/Core/Installer.php:659
 msgid "Database already in use."
 msgstr ""
 
-#: src/Core/Installer.php:677
+#: src/Core/Installer.php:664
 msgid "Could not connect to database."
 msgstr ""
 
@@ -3247,90 +3243,90 @@ msgstr ""
 msgid "Edit circles"
 msgstr ""
 
-#: src/Model/Contact.php:1264 src/Module/Moderation/Users/Pending.php:102
+#: src/Model/Contact.php:1257 src/Module/Moderation/Users/Pending.php:102
 #: src/Module/Notifications/Introductions.php:132
 #: src/Module/Notifications/Introductions.php:204
 msgid "Approve"
 msgstr ""
 
-#: src/Model/Contact.php:1742
+#: src/Model/Contact.php:1735
 msgid "Organisation"
 msgstr ""
 
-#: src/Model/Contact.php:1750
+#: src/Model/Contact.php:1743
 msgid "Group"
 msgstr ""
 
-#: src/Model/Contact.php:1754 src/Module/Moderation/BaseUsers.php:130
+#: src/Model/Contact.php:1747 src/Module/Moderation/BaseUsers.php:130
 msgid "Relay"
 msgstr ""
 
-#: src/Model/Contact.php:3057
+#: src/Model/Contact.php:3050
 msgid "Disallowed profile URL."
 msgstr ""
 
-#: src/Model/Contact.php:3062 src/Module/Friendica.php:101
+#: src/Model/Contact.php:3055 src/Module/Friendica.php:101
 msgid "Blocked domain"
 msgstr ""
 
-#: src/Model/Contact.php:3067
+#: src/Model/Contact.php:3060
 msgid "Connect URL missing."
 msgstr ""
 
-#: src/Model/Contact.php:3076
+#: src/Model/Contact.php:3069
 msgid ""
 "The contact could not be added. Please check the relevant network "
 "credentials in your Settings -> Social Networks page."
 msgstr ""
 
-#: src/Model/Contact.php:3094
+#: src/Model/Contact.php:3087
 #, php-format
 msgid "Expected network %s does not match actual network %s"
 msgstr ""
 
-#: src/Model/Contact.php:3111
+#: src/Model/Contact.php:3104
 msgid "This seems to be a relay account. They can't be followed by users."
 msgstr ""
 
-#: src/Model/Contact.php:3118
+#: src/Model/Contact.php:3111
 msgid "The profile address specified does not provide adequate information."
 msgstr ""
 
-#: src/Model/Contact.php:3120
+#: src/Model/Contact.php:3113
 msgid "No compatible communication protocols or feeds were discovered."
 msgstr ""
 
-#: src/Model/Contact.php:3123
+#: src/Model/Contact.php:3116
 msgid "An author or name was not found."
 msgstr ""
 
-#: src/Model/Contact.php:3126
+#: src/Model/Contact.php:3119
 msgid "No browser URL could be matched to this address."
 msgstr ""
 
-#: src/Model/Contact.php:3129
+#: src/Model/Contact.php:3122
 msgid ""
 "Unable to match @-style Identity Address with a known protocol or email "
 "contact."
 msgstr ""
 
-#: src/Model/Contact.php:3130
+#: src/Model/Contact.php:3123
 msgid "Use mailto: in front of address to force email check."
 msgstr ""
 
-#: src/Model/Contact.php:3136
+#: src/Model/Contact.php:3129
 msgid ""
 "The profile address specified belongs to a network which has been disabled "
 "on this site."
 msgstr ""
 
-#: src/Model/Contact.php:3141
+#: src/Model/Contact.php:3134
 msgid ""
 "Limited profile. This person will be unable to receive direct/personal "
 "notifications from you."
 msgstr ""
 
-#: src/Model/Contact.php:3207
+#: src/Model/Contact.php:3200
 msgid "Unable to retrieve contact information."
 msgstr ""
 
@@ -3527,7 +3523,7 @@ msgstr ""
 msgid "[no subject]"
 msgstr ""
 
-#: src/Model/Photo.php:1191 src/Module/Media/Photo/Upload.php:170
+#: src/Model/Photo.php:1187 src/Module/Media/Photo/Upload.php:168
 msgid "Wall Photos"
 msgstr ""
 
@@ -3811,11 +3807,11 @@ msgid ""
 "An error occurred creating your default contact circle. Please try again."
 msgstr ""
 
-#: src/Model/User.php:1415
+#: src/Model/User.php:1413
 msgid "Profile Photos"
 msgstr ""
 
-#: src/Model/User.php:1597
+#: src/Model/User.php:1595
 #, php-format
 msgid ""
 "\n"
@@ -3823,7 +3819,7 @@ msgid ""
 "\t\t\tthe administrator of %2$s has set up an account for you."
 msgstr ""
 
-#: src/Model/User.php:1600
+#: src/Model/User.php:1598
 #, php-format
 msgid ""
 "\n"
@@ -3859,12 +3855,12 @@ msgid ""
 "\t\tThank you and welcome to %4$s."
 msgstr ""
 
-#: src/Model/User.php:1632 src/Model/User.php:1738
+#: src/Model/User.php:1630 src/Model/User.php:1736
 #, php-format
 msgid "Registration details for %s"
 msgstr ""
 
-#: src/Model/User.php:1652
+#: src/Model/User.php:1650
 #, php-format
 msgid ""
 "\n"
@@ -3880,12 +3876,12 @@ msgid ""
 "\t\t"
 msgstr ""
 
-#: src/Model/User.php:1671
+#: src/Model/User.php:1669
 #, php-format
 msgid "Registration at %s"
 msgstr ""
 
-#: src/Model/User.php:1695
+#: src/Model/User.php:1693
 #, php-format
 msgid ""
 "\n"
@@ -3894,7 +3890,7 @@ msgid ""
 "\t\t\t"
 msgstr ""
 
-#: src/Model/User.php:1703
+#: src/Model/User.php:1701
 #, php-format
 msgid ""
 "\n"
@@ -3932,7 +3928,7 @@ msgid ""
 "\t\t\tThank you and welcome to %2$s."
 msgstr ""
 
-#: src/Model/User.php:1765
+#: src/Model/User.php:1763
 msgid ""
 "User with delegates can't be removed, please remove delegate users first"
 msgstr ""
@@ -7808,7 +7804,7 @@ msgstr ""
 
 #: src/Module/Media/Attachment/Browser.php:79
 #: src/Module/Media/Photo/Browser.php:90
-#: src/Module/Settings/Profile/Photo/Index.php:129
+#: src/Module/Settings/Profile/Photo/Index.php:127
 msgid "Upload"
 msgstr ""
 
@@ -7829,14 +7825,14 @@ msgstr ""
 msgid "File upload failed."
 msgstr ""
 
-#: src/Module/Media/Photo/Upload.php:152 src/Module/Media/Photo/Upload.php:153
-#: src/Module/Profile/Photos.php:217
-#: src/Module/Settings/Profile/Photo/Index.php:69
+#: src/Module/Media/Photo/Upload.php:150 src/Module/Media/Photo/Upload.php:151
+#: src/Module/Profile/Photos.php:215
+#: src/Module/Settings/Profile/Photo/Index.php:67
 msgid "Unable to process image."
 msgstr ""
 
-#: src/Module/Media/Photo/Upload.php:178 src/Module/Profile/Photos.php:237
-#: src/Module/Settings/Profile/Photo/Index.php:96
+#: src/Module/Media/Photo/Upload.php:176 src/Module/Profile/Photos.php:235
+#: src/Module/Settings/Profile/Photo/Index.php:94
 msgid "Image upload failed."
 msgstr ""
 
@@ -9075,27 +9071,27 @@ msgstr ""
 
 #: src/Module/Profile/Conversations.php:106
 #: src/Module/Profile/Conversations.php:109 src/Module/Profile/Profile.php:351
-#: src/Module/Profile/Profile.php:354 src/Protocol/Feed.php:1098
+#: src/Module/Profile/Profile.php:354 src/Protocol/Feed.php:1099
 #: src/Protocol/OStatus.php:1009
 #, php-format
 msgid "%s's timeline"
 msgstr ""
 
 #: src/Module/Profile/Conversations.php:107 src/Module/Profile/Profile.php:352
-#: src/Protocol/Feed.php:1102 src/Protocol/OStatus.php:1014
+#: src/Protocol/Feed.php:1103 src/Protocol/OStatus.php:1014
 #, php-format
 msgid "%s's posts"
 msgstr ""
 
 #: src/Module/Profile/Conversations.php:108 src/Module/Profile/Profile.php:353
-#: src/Protocol/Feed.php:1105 src/Protocol/OStatus.php:1018
+#: src/Protocol/Feed.php:1106 src/Protocol/OStatus.php:1018
 #, php-format
 msgid "%s's comments"
 msgstr ""
 
 #: src/Module/Profile/Photos.php:164 src/Module/Profile/Photos.php:167
-#: src/Module/Profile/Photos.php:194
-#: src/Module/Settings/Profile/Photo/Index.php:60
+#: src/Module/Profile/Photos.php:192
+#: src/Module/Settings/Profile/Photo/Index.php:58
 #, php-format
 msgid "Image exceeds size limit of %s"
 msgstr ""
@@ -9114,11 +9110,11 @@ msgid ""
 "administrator"
 msgstr ""
 
-#: src/Module/Profile/Photos.php:202
+#: src/Module/Profile/Photos.php:200
 msgid "Image file is empty."
 msgstr ""
 
-#: src/Module/Profile/Photos.php:356
+#: src/Module/Profile/Photos.php:352
 msgid "View Album"
 msgstr ""
 
@@ -10929,7 +10925,7 @@ msgstr ""
 #: src/Module/Settings/Profile/Photo/Crop.php:107
 #: src/Module/Settings/Profile/Photo/Crop.php:125
 #: src/Module/Settings/Profile/Photo/Crop.php:143
-#: src/Module/Settings/Profile/Photo/Index.php:102
+#: src/Module/Settings/Profile/Photo/Index.php:100
 #, php-format
 msgid "Image size reduction [%s] failed."
 msgstr ""
@@ -10969,31 +10965,31 @@ msgstr ""
 msgid "Missing uploaded image."
 msgstr ""
 
-#: src/Module/Settings/Profile/Photo/Index.php:125
+#: src/Module/Settings/Profile/Photo/Index.php:123
 msgid "Profile Picture Settings"
 msgstr ""
 
-#: src/Module/Settings/Profile/Photo/Index.php:126
+#: src/Module/Settings/Profile/Photo/Index.php:124
 msgid "Current Profile Picture"
 msgstr ""
 
-#: src/Module/Settings/Profile/Photo/Index.php:127
+#: src/Module/Settings/Profile/Photo/Index.php:125
 msgid "Upload Profile Picture"
 msgstr ""
 
-#: src/Module/Settings/Profile/Photo/Index.php:128
+#: src/Module/Settings/Profile/Photo/Index.php:126
 msgid "Upload Picture:"
 msgstr ""
 
-#: src/Module/Settings/Profile/Photo/Index.php:133
+#: src/Module/Settings/Profile/Photo/Index.php:131
 msgid "or"
 msgstr ""
 
-#: src/Module/Settings/Profile/Photo/Index.php:135
+#: src/Module/Settings/Profile/Photo/Index.php:133
 msgid "skip this step"
 msgstr ""
 
-#: src/Module/Settings/Profile/Photo/Index.php:137
+#: src/Module/Settings/Profile/Photo/Index.php:135
 msgid "select a photo from your photo albums"
 msgstr ""