From 14e5b060299e7f0444c363d52f33b161175c1dd6 Mon Sep 17 00:00:00 2001 From: Michael Vogel Date: Sat, 17 Feb 2024 07:45:41 +0100 Subject: [PATCH] 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 --- doc/Install.md | 2 +- mod/photos.php | 14 +- src/Console/MoveToAvatarCache.php | 2 +- src/Contact/Avatar.php | 9 +- src/Content/Text/BBCode.php | 9 +- src/Core/Installer.php | 19 +- src/Factory/Api/Mastodon/Attachment.php | 9 +- src/Model/Contact.php | 13 +- src/Model/Photo.php | 24 +- src/Model/Post/Link.php | 24 +- src/Model/Post/Media.php | 2 +- src/Model/User.php | 4 +- src/Module/Api/Mastodon/Instance.php | 2 +- src/Module/Api/Mastodon/InstanceV2.php | 2 +- src/Module/Api/Mastodon/Statuses.php | 7 +- src/Module/Api/Twitter/Statuses/Update.php | 7 +- src/Module/Media/Photo/Browser.php | 5 +- src/Module/Media/Photo/Upload.php | 4 +- src/Module/Photo.php | 31 +- src/Module/Profile/Photos.php | 10 +- src/Module/Proxy.php | 8 +- src/Module/Settings/Profile/Photo/Crop.php | 2 +- src/Module/Settings/Profile/Photo/Index.php | 4 +- src/Module/User/Import.php | 2 +- .../HTTPClient/Client/HttpClientAccept.php | 2 +- src/Object/Image.php | 145 +++++++-- src/Protocol/DFRN.php | 4 +- src/Protocol/Feed.php | 3 +- src/Util/Images.php | 260 ++++++++++----- src/Util/ParseUrl.php | 2 +- view/lang/C/messages.po | 296 +++++++++--------- 31 files changed, 541 insertions(+), 386 deletions(-) diff --git a/doc/Install.md b/doc/Install.md index 4715c27233..c50854aaf2 100644 --- a/doc/Install.md +++ b/doc/Install.md @@ -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 diff --git a/mod/photos.php b/mod/photos.php index 322ddd1599..a8a62e8964 100644 --- a/mod/photos.php +++ b/mod/photos.php @@ -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'], diff --git a/src/Console/MoveToAvatarCache.php b/src/Console/MoveToAvatarCache.php index f5b067ffc6..e8c0823381 100644 --- a/src/Console/MoveToAvatarCache.php +++ b/src/Console/MoveToAvatarCache.php @@ -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; diff --git a/src/Contact/Avatar.php b/src/Contact/Avatar.php index 371e645232..f165e1d3a1 100644 --- a/src/Contact/Avatar.php +++ b/src/Contact/Avatar.php @@ -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)) { diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index 4c2a3a6b46..f5bba8ce42 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -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])) { diff --git a/src/Core/Installer.php b/src/Core/Installer.php index efbcad5ee1..1c07f325ed 100644 --- a/src/Core/Installer.php +++ b/src/Core/Installer.php @@ -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 diff --git a/src/Factory/Api/Mastodon/Attachment.php b/src/Factory/Api/Mastodon/Attachment.php index eb93a3d620..727b77630a 100644 --- a/src/Factory/Api/Mastodon/Attachment.php +++ b/src/Factory/Api/Mastodon/Attachment.php @@ -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 = ''; } diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 7ab726ad41..e673822ef2 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -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 { diff --git a/src/Model/Photo.php b/src/Model/Photo.php index 4700db6f50..360ee16220 100644 --- a/src/Model/Photo.php +++ b/src/Model/Photo.php @@ -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 []; diff --git a/src/Model/Post/Link.php b/src/Model/Post/Link.php index be2f7fd2da..6c70d8b624 100644 --- a/src/Model/Post/Link.php +++ b/src/Model/Post/Link.php @@ -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; diff --git a/src/Model/Post/Media.php b/src/Model/Post/Media.php index cbbfdb97ec..346a6a1d00 100644 --- a/src/Model/Post/Media.php +++ b/src/Model/Post/Media.php @@ -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); diff --git a/src/Model/User.php b/src/Model/User.php index 13587a342a..9472407715 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -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); diff --git a/src/Module/Api/Mastodon/Instance.php b/src/Module/Api/Mastodon/Instance.php index 882f873408..8faa356dd6 100644 --- a/src/Module/Api/Mastodon/Instance.php +++ b/src/Module/Api/Mastodon/Instance.php @@ -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(), ); diff --git a/src/Module/Api/Mastodon/InstanceV2.php b/src/Module/Api/Mastodon/InstanceV2.php index 638f329867..a3d268311e 100644 --- a/src/Module/Api/Mastodon/InstanceV2.php +++ b/src/Module/Api/Mastodon/InstanceV2.php @@ -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(), ); diff --git a/src/Module/Api/Mastodon/Statuses.php b/src/Module/Api/Mastodon/Statuses.php index 33b9b3b830..9add05376c 100644 --- a/src/Module/Api/Mastodon/Statuses.php +++ b/src/Module/Api/Mastodon/Statuses.php @@ -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']; } diff --git a/src/Module/Api/Twitter/Statuses/Update.php b/src/Module/Api/Twitter/Statuses/Update.php index da9d30422c..8e5cc976d8 100644 --- a/src/Module/Api/Twitter/Statuses/Update.php +++ b/src/Module/Api/Twitter/Statuses/Update.php @@ -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']; } diff --git a/src/Module/Media/Photo/Browser.php b/src/Module/Media/Photo/Browser.php index e667e02ba7..06b2b7107c 100644 --- a/src/Module/Media/Photo/Browser.php +++ b/src/Module/Media/Photo/Browser.php @@ -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'], ]; } diff --git a/src/Module/Media/Photo/Upload.php b/src/Module/Media/Photo/Upload.php index d5d01290cd..6594b453f8 100644 --- a/src/Module/Media/Photo/Upload.php +++ b/src/Module/Media/Photo/Upload.php @@ -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); diff --git a/src/Module/Photo.php b/src/Module/Photo.php index 1ab228b7c3..3a064c48ac 100644 --- a/src/Module/Photo.php +++ b/src/Module/Photo.php @@ -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)) { diff --git a/src/Module/Profile/Photos.php b/src/Module/Profile/Photos.php index 8b915a4eba..1987cdc821 100644 --- a/src/Module/Profile/Photos.php +++ b/src/Module/Profile/Photos.php @@ -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']), diff --git a/src/Module/Proxy.php b/src/Module/Proxy.php index 0eb95d8a3f..12f58f3a84 100644 --- a/src/Module/Proxy.php +++ b/src/Module/Proxy.php @@ -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']); } diff --git a/src/Module/Settings/Profile/Photo/Crop.php b/src/Module/Settings/Profile/Photo/Crop.php index ef621b1823..a9988d2830 100644 --- a/src/Module/Settings/Profile/Photo/Crop.php +++ b/src/Module/Settings/Profile/Photo/Crop.php @@ -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, diff --git a/src/Module/Settings/Profile/Photo/Index.php b/src/Module/Settings/Profile/Photo/Index.php index 3f44de357e..90cbc9f886 100644 --- a/src/Module/Settings/Profile/Photo/Index.php +++ b/src/Module/Settings/Profile/Photo/Index.php @@ -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.')); diff --git a/src/Module/User/Import.php b/src/Module/User/Import.php index 252649819b..4d03e3996d 100644 --- a/src/Module/User/Import.php +++ b/src/Module/User/Import.php @@ -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'] diff --git a/src/Network/HTTPClient/Client/HttpClientAccept.php b/src/Network/HTTPClient/Client/HttpClientAccept.php index 76eb736fe2..11c317acb4 100644 --- a/src/Network/HTTPClient/Client/HttpClientAccept.php +++ b/src/Network/HTTPClient/Client/HttpClientAccept.php @@ -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"'; diff --git a/src/Object/Image.php b/src/Object/Image.php index 5987e26d6f..d938643751 100644 --- a/src/Object/Image.php +++ b/src/Object/Image.php @@ -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 ''; } diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index 4d44b8a40f..b58faed679 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -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'); diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index c67894494c..e0ab3dd49a 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -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']; } } diff --git a/src/Util/Images.php b/src/Util/Images.php index e5b8afbb54..33bae87a79 100644 --- a/src/Util/Images.php +++ b/src/Util/Images.php @@ -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 ); } diff --git a/src/Util/ParseUrl.php b/src/Util/ParseUrl.php index cd9669624a..1287160576 100644 --- a/src/Util/ParseUrl.php +++ b/src/Util/ParseUrl.php @@ -87,7 +87,7 @@ class ParseUrl return []; } - $contenttype = $curlResult->getHeader('Content-Type')[0] ?? ''; + $contenttype = $curlResult->getContentType(); if (empty($contenttype)) { return ['application', 'octet-stream']; } diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index 2d0345b1d8..21146e801a 100644 --- a/view/lang/C/messages.po +++ b/view/lang/C/messages.po @@ -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 \n" "Language-Team: LANGUAGE \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 "" "%2$s %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 ""