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

* Image handling reworked, new image formats added

* Updated messages.po

* The dot is now part of the file extension

* Added WebP in install documentation

* Handle unhandled mime types

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

View File

@ -44,7 +44,7 @@ For alternative server configurations (such as Nginx server and MariaDB database
### Optional ### Optional
* PHP ImageMagick extension (php-imagick) for animated GIF support. * PHP ImageMagick extension (php-imagick) for animated GIF and animated WebP support.
## Installation procedure ## Installation procedure

View File

@ -132,8 +132,6 @@ function photos_post(App $a)
throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.')); throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.'));
} }
$phototypes = Images::supportedTypes();
$can_post = false; $can_post = false;
$visitor = 0; $visitor = 0;
@ -337,7 +335,7 @@ function photos_post(App $a)
if (DBA::isResult($photos)) { if (DBA::isResult($photos)) {
$photo = $photos[0]; $photo = $photos[0];
$ext = $phototypes[$photo['type']]; $ext = Images::getExtensionByMimeType($photo['type']);
Photo::update( 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], ['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] ['resource-id' => $resource_id, 'uid' => $page_owner_uid]
@ -590,8 +588,6 @@ function photos_content(App $a)
$profile = Profile::getByUID($user['uid']); $profile = Profile::getByUID($user['uid']);
$phototypes = Images::supportedTypes();
$_SESSION['photo_return'] = DI::args()->getCommand(); $_SESSION['photo_return'] = DI::args()->getCommand();
// Parse arguments // Parse arguments
@ -844,7 +840,7 @@ function photos_content(App $a)
foreach ($r as $rr) { foreach ($r as $rr) {
$twist = !$twist; $twist = !$twist;
$ext = $phototypes[$rr['type']]; $ext = Images::getExtensionByMimeType($rr['type']);
$imgalt_e = $rr['filename']; $imgalt_e = $rr['filename'];
$desc_e = $rr['desc']; $desc_e = $rr['desc'];
@ -855,7 +851,7 @@ function photos_content(App $a)
'link' => 'photos/' . $user['nickname'] . '/image/' . $rr['resource-id'] 'link' => 'photos/' . $user['nickname'] . '/image/' . $rr['resource-id']
. ($order_field === 'created' ? '?order=created' : ''), . ($order_field === 'created' ? '?order=created' : ''),
'title' => DI::l10n()->t('View Photo'), '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, 'alt' => $imgalt_e,
'desc' => $desc_e, 'desc' => $desc_e,
'ext' => $ext, 'ext' => $ext,
@ -1013,9 +1009,9 @@ function photos_content(App $a)
} }
$photo = [ $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'), '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'], 'height' => $hires['height'],
'width' => $hires['width'], 'width' => $hires['width'],
'album' => $hires['album'], 'album' => $hires['album'],

View File

@ -150,7 +150,7 @@ HELP;
if ($valid) { if ($valid) {
$this->out('3', false); $this->out('3', false);
$image = new Image($imgdata, Images::getMimeTypeByData($imgdata)); $image = new Image($imgdata);
if (!$image->isValid()) { if (!$image->isValid()) {
$this->out(' ' . $this->l10n->t('invalid image for id %s', $resourceid) . ' ', false); $this->out(' ' . $this->l10n->t('invalid image for id %s', $resourceid) . ' ', false);
$valid = false; $valid = false;

View File

@ -80,13 +80,18 @@ class Avatar
return $fields; return $fields;
} }
if (!$fetchResult->isSuccess()) {
Logger::debug('Fetching was unsuccessful', ['avatar' => $avatar]);
return $fields;
}
$img_str = $fetchResult->getBodyString(); $img_str = $fetchResult->getBodyString();
if (empty($img_str)) { if (empty($img_str)) {
Logger::debug('Avatar is invalid', ['avatar' => $avatar]); Logger::debug('Avatar is invalid', ['avatar' => $avatar]);
return $fields; return $fields;
} }
$image = new Image($img_str, Images::getMimeTypeByData($img_str)); $image = new Image($img_str, $fetchResult->getContentType(), $avatar);
if (!$image->isValid()) { if (!$image->isValid()) {
Logger::debug('Avatar picture is invalid', ['avatar' => $avatar]); Logger::debug('Avatar picture is invalid', ['avatar' => $avatar]);
return $fields; return $fields;
@ -145,7 +150,7 @@ class Avatar
return ''; return '';
} }
$path = $filename . $size . '.' . $image->getExt(); $path = $filename . $size . $image->getExt();
$basepath = self::basePath(); $basepath = self::basePath();
if (empty($basepath)) { if (empty($basepath)) {

View File

@ -40,6 +40,7 @@ use Friendica\Model\Post;
use Friendica\Model\Tag; use Friendica\Model\Tag;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Util\Images;
use Friendica\Util\Map; use Friendica\Util\Map;
use Friendica\Util\Network; use Friendica\Util\Network;
use Friendica\Util\ParseUrl; use Friendica\Util\ParseUrl;
@ -1027,12 +1028,12 @@ class BBCode
if (is_null($text)) { if (is_null($text)) {
$curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]); $curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]);
if ($curlResult->isSuccess()) { if ($curlResult->isSuccess()) {
$mimetype = $curlResult->getHeader('Content-Type')[0] ?? ''; $mimetype = $curlResult->getContentType() ?? '';
} else { } else {
$mimetype = ''; $mimetype = '';
} }
if (substr($mimetype, 0, 6) == 'image/') { if (Images::isSupportedMimeType($mimetype)) {
$text = '[url=' . $match[1] . ']' . $match[1] . '[/url]'; $text = '[url=' . $match[1] . ']' . $match[1] . '[/url]';
} else { } else {
$text = '[url=' . $match[2] . ']' . $match[2] . '[/url]'; $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')]); $curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]);
if ($curlResult->isSuccess()) { if ($curlResult->isSuccess()) {
$mimetype = $curlResult->getHeader('Content-Type')[0] ?? ''; $mimetype = $curlResult->getContentType() ?? '';
} else { } else {
$mimetype = ''; $mimetype = '';
} }
// if its a link to a picture then embed this picture // 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]'; $text = '[img]' . $match[1] . '[/img]';
} else { } else {
if (!empty($match[3])) { if (!empty($match[3])) {

View File

@ -632,23 +632,10 @@ class Installer
*/ */
public function checkImagick() public function checkImagick()
{ {
$imagick = false; if (!class_exists('Imagick')) {
$gif = false; $this->addCheck(DI::l10n()->t('ImageMagick PHP extension is not installed'), false, 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, "");
} else { } else {
$this->addCheck(DI::l10n()->t('ImageMagick PHP extension is installed'), $imagick, false, ""); $this->addCheck(DI::l10n()->t('ImageMagick PHP extension is installed'), true, false, "");
if ($imagick) {
$this->addCheck(DI::l10n()->t('ImageMagick supports GIF'), $gif, false, "");
}
} }
// Imagick is not required // Imagick is not required

View File

@ -84,7 +84,7 @@ class Attachment extends BaseFactory
$type = 'audio'; $type = 'audio';
} elseif (($filetype == 'video') || ($attachment['type'] == Post\Media::VIDEO)) { } elseif (($filetype == 'video') || ($attachment['type'] == Post\Media::VIDEO)) {
$type = 'video'; $type = 'video';
} elseif ($attachment['mimetype'] == 'image/gif') { } elseif ($attachment['mimetype'] == image_type_to_mime_type(IMAGETYPE_GIF)) {
$type = 'gifv'; $type = 'gifv';
} elseif (($filetype == 'image') || ($attachment['type'] == Post\Media::IMAGE)) { } elseif (($filetype == 'image') || ($attachment['type'] == Post\Media::IMAGE)) {
$type = 'image'; $type = 'image';
@ -130,14 +130,13 @@ class Attachment extends BaseFactory
'blurhash' => $photo['blurhash'], 'blurhash' => $photo['blurhash'],
]; ];
$photoTypes = Images::supportedTypes(); $ext = Images::getExtensionByMimeType($photo['type']);
$ext = $photoTypes[$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']]); $preview = Photo::selectFirst(['scale'], ["`resource-id` = ? AND `uid` = ? AND `scale` > ?", $photo['resource-id'], $photo['uid'], 0], ['order' => ['scale']]);
if (!empty($preview)) { 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 { } else {
$preview_url = ''; $preview_url = '';
} }

View File

@ -842,7 +842,6 @@ class Contact
return false; return false;
} }
$file_suffix = 'jpg';
$url = DI::baseUrl() . '/profile/' . $user['nickname']; $url = DI::baseUrl() . '/profile/' . $user['nickname'];
$fields = [ $fields = [
@ -875,17 +874,11 @@ class Contact
$fields['avatar-date'] = DateTimeFormat::utcNow(); $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 // We are adding a timestamp value so that other systems won't use cached content
$timestamp = strtotime($fields['avatar-date']); $timestamp = strtotime($fields['avatar-date']);
$prefix = DI::baseUrl() . '/photo/' . $avatar['resource-id'] . '-'; $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['photo'] = $prefix . '4' . $suffix;
$fields['thumb'] = $prefix . '5' . $suffix; $fields['thumb'] = $prefix . '5' . $suffix;
@ -2313,8 +2306,8 @@ class Contact
$fetchResult = HTTPSignature::fetchRaw($avatar, 0, [HttpClientOptions::ACCEPT_CONTENT => [HttpClientAccept::IMAGE]]); $fetchResult = HTTPSignature::fetchRaw($avatar, 0, [HttpClientOptions::ACCEPT_CONTENT => [HttpClientAccept::IMAGE]]);
$img_str = $fetchResult->getBodyString(); $img_str = $fetchResult->getBodyString();
if (!empty($img_str)) { if ($fetchResult->isSuccess() && !empty($img_str)) {
$image = new Image($img_str, Images::getMimeTypeByData($img_str)); $image = new Image($img_str, $fetchResult->getContentType(), $avatar);
if ($image->isValid()) { if ($image->isValid()) {
$update_fields['blurhash'] = $image->getBlurHash(); $update_fields['blurhash'] = $image->getBlurHash();
} else { } else {

View File

@ -363,6 +363,7 @@ class Photo
$photo['backend-class'] = SystemResource::NAME; $photo['backend-class'] = SystemResource::NAME;
$photo['backend-ref'] = $filename; $photo['backend-ref'] = $filename;
$photo['type'] = $mimetype; $photo['type'] = $mimetype;
$photo['filename'] = basename($filename);
$photo['cacheable'] = false; $photo['cacheable'] = false;
return $photo; return $photo;
@ -394,6 +395,7 @@ class Photo
$photo['backend-class'] = ExternalResource::NAME; $photo['backend-class'] = ExternalResource::NAME;
$photo['backend-ref'] = json_encode(['url' => $url, 'uid' => $uid]); $photo['backend-ref'] = json_encode(['url' => $url, 'uid' => $uid]);
$photo['type'] = $mimetype; $photo['type'] = $mimetype;
$photo['filename'] = basename(parse_url($url, PHP_URL_PATH));
$photo['cacheable'] = true; $photo['cacheable'] = true;
$photo['blurhash'] = $blurhash; $photo['blurhash'] = $blurhash;
$photo['width'] = $width; $photo['width'] = $width;
@ -608,9 +610,7 @@ class Photo
return false; return false;
} }
$type = Images::getMimeTypeByData($img_str, $image_url, $type); $image = new Image($img_str, $type, $image_url);
$image = new Image($img_str, $type);
if ($image->isValid()) { if ($image->isValid()) {
$image->scaleToSquare(300); $image->scaleToSquare(300);
@ -619,9 +619,9 @@ class Photo
if ($maximagesize && ($filesize > $maximagesize)) { if ($maximagesize && ($filesize > $maximagesize)) {
Logger::info('Avatar exceeds image limit', ['uid' => $uid, 'cid' => $cid, 'maximagesize' => $maximagesize, 'size' => $filesize, 'type' => $image->getType()]); 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->toStatic();
$image = new Image($image->asString(), 'image/png'); $image = new Image($image->asString(), image_type_to_mime_type(IMAGETYPE_PNG));
$filesize = strlen($image->asString()); $filesize = strlen($image->asString());
Logger::info('Converted gif to a static png', ['uid' => $uid, 'cid' => $cid, 'size' => $filesize, 'type' => $image->getType()]); 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(); $suffix = '?ts=' . time();
$image_url = DI::baseUrl() . '/photo/' . $resource_id . '-4.' . $image->getExt() . $suffix; $image_url = DI::baseUrl() . '/photo/' . $resource_id . '-4' . $image->getExt() . $suffix;
$thumb = DI::baseUrl() . '/photo/' . $resource_id . '-5.' . $image->getExt() . $suffix; $thumb = DI::baseUrl() . '/photo/' . $resource_id . '-5' . $image->getExt() . $suffix;
$micro = DI::baseUrl() . '/photo/' . $resource_id . '-6.' . $image->getExt() . $suffix; $micro = DI::baseUrl() . '/photo/' . $resource_id . '-6' . $image->getExt() . $suffix;
} else { } else {
$photo_failure = true; $photo_failure = true;
} }
@ -1060,9 +1060,7 @@ class Photo
return []; return [];
} }
$type = Images::getMimeTypeByData($img_str, $image_url, $type); $image = new Image($img_str, $type, $image_url);
$image = new Image($img_str, $type);
$image = self::fitImageSize($image); $image = self::fitImageSize($image);
if (empty($image)) { if (empty($image)) {
@ -1132,12 +1130,10 @@ class Photo
return []; return [];
} }
$filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
Logger::info('File upload', ['src' => $src, 'filename' => $filename, 'size' => $filesize, 'type' => $filetype]); Logger::info('File upload', ['src' => $src, 'filename' => $filename, 'size' => $filesize, 'type' => $filetype]);
$imagedata = @file_get_contents($src); $imagedata = @file_get_contents($src);
$image = new Image($imagedata, $filetype); $image = new Image($imagedata, $filetype, $filename);
if (!$image->isValid()) { if (!$image->isValid()) {
Logger::notice('Image is unvalid', ['files' => $files]); Logger::notice('Image is unvalid', ['files' => $files]);
return []; return [];

View File

@ -134,15 +134,23 @@ class Link
Logger::notice('Error fetching url', ['url' => $url, 'exception' => $exception]); Logger::notice('Error fetching url', ['url' => $url, 'exception' => $exception]);
return []; return [];
} }
$fields = ['mimetype' => $curlResult->getHeader('Content-Type')[0]];
$img_str = $curlResult->getBodyString(); if (!$curlResult->isSuccess()) {
$image = new Image($img_str, Images::getMimeTypeByData($img_str)); Logger::notice('Fetching unsuccessful', ['url' => $url]);
if ($image->isValid()) { return [];
$fields['mimetype'] = $image->getType(); }
$fields['width'] = $image->getWidth();
$fields['height'] = $image->getHeight(); $fields = ['mimetype' => $curlResult->getContentType()];
$fields['blurhash'] = $image->getBlurHash();
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; return $fields;

View File

@ -196,7 +196,7 @@ class Media
if ($curlResult->isSuccess()) { if ($curlResult->isSuccess()) {
if (empty($media['mimetype'])) { if (empty($media['mimetype'])) {
$media['mimetype'] = $curlResult->getHeader('Content-Type')[0] ?? ''; $media['mimetype'] = $curlResult->getContentType() ?? '';
} }
if (empty($media['size'])) { if (empty($media['size'])) {
$media['size'] = (int)($curlResult->getHeader('Content-Length')[0] ?? 0); $media['size'] = (int)($curlResult->getHeader('Content-Length')[0] ?? 0);

View File

@ -1403,9 +1403,7 @@ class User
$type = ''; $type = '';
} }
$type = Images::getMimeTypeByData($img_str, $photo, $type); $image = new Image($img_str, $type, $photo);
$image = new Image($img_str, $type);
if ($image->isValid()) { if ($image->isValid()) {
$image->scaleToSquare(300); $image->scaleToSquare(300);

View File

@ -95,7 +95,7 @@ class Instance extends BaseApi
return new InstanceV2Entity\Configuration( return new InstanceV2Entity\Configuration(
$statuses_config, $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\Polls(),
new InstanceV2Entity\Accounts(), new InstanceV2Entity\Accounts(),
); );

View File

@ -131,7 +131,7 @@ class InstanceV2 extends BaseApi
return new InstanceEntity\Configuration( return new InstanceEntity\Configuration(
$statuses_config, $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\Polls(),
new InstanceEntity\Accounts(), new InstanceEntity\Accounts(),
); );

View 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']); Photo::setPermissionForResource($media[0]['resource-id'], $item['uid'], $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']);
$phototypes = Images::supportedTypes(); $ext = Images::getExtensionByMimeType($media[0]['type']);
$ext = $phototypes[$media[0]['type']];
$attachment = ['type' => Post\Media::IMAGE, 'mimetype' => $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'], 'size' => $media[0]['datasize'],
'name' => $media[0]['filename'] ?: $media[0]['resource-id'], 'name' => $media[0]['filename'] ?: $media[0]['resource-id'],
'description' => $media[0]['desc'] ?? '', 'description' => $media[0]['desc'] ?? '',
@ -415,7 +414,7 @@ class Statuses extends BaseApi
'height' => $media[0]['height']]; 'height' => $media[0]['height']];
if (count($media) > 1) { 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-width'] = $media[1]['width'];
$attachment['preview-height'] = $media[1]['height']; $attachment['preview-height'] = $media[1]['height'];
} }

View 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']); Photo::setPermissionForResource($media[0]['resource-id'], $uid, $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']);
$phototypes = Images::supportedTypes(); $ext = Images::getExtensionByMimeType($media[0]['type']);
$ext = $phototypes[$media[0]['type']];
$attachment = [ $attachment = [
'type' => Post\Media::IMAGE, 'type' => Post\Media::IMAGE,
'mimetype' => $media[0]['type'], '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'], 'size' => $media[0]['datasize'],
'name' => $media[0]['filename'] ?: $media[0]['resource-id'], 'name' => $media[0]['filename'] ?: $media[0]['resource-id'],
'description' => $media[0]['desc'] ?? '', 'description' => $media[0]['desc'] ?? '',
@ -170,7 +169,7 @@ class Update extends BaseApi
]; ];
if (count($media) > 1) { 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-width'] = $media[1]['width'];
$attachment['preview-height'] = $media[1]['height']; $attachment['preview-height'] = $media[1]['height'];
} }

View File

@ -99,8 +99,7 @@ class Browser extends BaseModule
protected function map_files(array $record): array protected function map_files(array $record): array
{ {
$types = Images::supportedTypes(); $ext = Images::getExtensionByMimeType($record['type']);
$ext = $types[$record['type']];
$filename_e = $record['filename']; $filename_e = $record['filename'];
// Take the largest picture that is smaller or equal 640 pixels // Take the largest picture that is smaller or equal 640 pixels
@ -118,7 +117,7 @@ class Browser extends BaseModule
return [ return [
sprintf('%s/photos/%s/image/%s', $this->baseUrl, $this->app->getLoggedInUserNickname(), $record['resource-id']), sprintf('%s/photos/%s/image/%s', $this->baseUrl, $this->app->getLoggedInUserNickname(), $record['resource-id']),
$filename_e, $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'], $record['desc'],
]; ];
} }

View File

@ -135,8 +135,6 @@ class Upload extends \Friendica\BaseModule
$this->return(401, $this->t('Invalid request.'), true); $this->return(401, $this->t('Invalid request.'), true);
} }
$filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
$this->logger->info('File upload:', [ $this->logger->info('File upload:', [
'src' => $src, 'src' => $src,
'filename' => $filename, 'filename' => $filename,
@ -145,7 +143,7 @@ class Upload extends \Friendica\BaseModule
]); ]);
$imagedata = @file_get_contents($src); $imagedata = @file_get_contents($src);
$image = new Image($imagedata, $filetype); $image = new Image($imagedata, $filetype, $filename);
if (!$image->isValid()) { if (!$image->isValid()) {
@unlink($src); @unlink($src);

View File

@ -167,14 +167,16 @@ class Photo extends BaseApi
$stamp = microtime(true); $stamp = microtime(true);
if (empty($request['blur']) || empty($photo['blurhash'])) { if (empty($request['blur']) || empty($photo['blurhash'])) {
$imgdata = MPhoto::getImageDataForPhoto($photo); $imgdata = MPhoto::getImageDataForPhoto($photo);
$mimetype = $photo['type'];
} }
if (empty($imgdata) && empty($photo['blurhash'])) { if (empty($imgdata) && empty($photo['blurhash'])) {
throw new HTTPException\NotFoundException(); throw new HTTPException\NotFoundException();
} elseif (empty($imgdata) && !empty($photo['blurhash'])) { } 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']); $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 // 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'])) { if (!empty($request['static'])) {
$img = new Image($imgdata, $photo['type']); $img = new Image($imgdata, $photo['type'], $photo['filename']);
$img->toStatic(); $img->toStatic();
$imgdata = $img->asString(); $imgdata = $img->asString();
$mimetype = $img->getType();
} }
// if customsize is set and image is not a gif, resize it // 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) { if ($photo['type'] !== image_type_to_mime_type(IMAGETYPE_GIF) && $customsize > 0 && $customsize <= Proxy::PIXEL_THUMB && $square_resize) {
$img = new Image($imgdata, $photo['type']); $img = new Image($imgdata, $photo['type'], $photo['filename']);
$img->scaleToSquare($customsize); $img->scaleToSquare($customsize);
$imgdata = $img->asString(); $imgdata = $img->asString();
} elseif ($photo['type'] !== 'image/gif' && $customsize > 0) { $mimetype = $img->getType();
$img = new Image($imgdata, $photo['type']); } elseif ($photo['type'] !== image_type_to_mime_type(IMAGETYPE_GIF) && $customsize > 0) {
$img = new Image($imgdata, $photo['type'], $photo['filename']);
$img->scaleDown($customsize); $img->scaleDown($customsize);
$imgdata = $img->asString(); $imgdata = $img->asString();
$mimetype = $img->getType();
} }
if (function_exists('header_remove')) { if (function_exists('header_remove')) {
@ -220,7 +225,7 @@ class Photo extends BaseApi
header_remove('pragma'); header_remove('pragma');
} }
header('Content-type: ' . $photo['type']); header('Content-type: ' . $mimetype);
$stamp = microtime(true); $stamp = microtime(true);
if (!$cacheable) { if (!$cacheable) {
@ -391,7 +396,7 @@ class Photo extends BaseApi
} }
} }
if (empty($mimetext) && !empty($contact['blurhash'])) { 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); $image->getFromBlurHash($contact['blurhash'], $customsize, $customsize);
return MPhoto::createPhotoForImageData($image->asString()); return MPhoto::createPhotoForImageData($image->asString());
} elseif (empty($mimetext)) { } elseif (empty($mimetext)) {

View File

@ -184,8 +184,6 @@ class Photos extends \Friendica\Module\BaseProfile
return; return;
} }
$type = Images::getMimeTypeBySource($src, $filename, $type);
$this->logger->info('photos: upload: received file: ' . $filename . ' as ' . $src . ' ('. $type . ') ' . $filesize . ' bytes'); $this->logger->info('photos: upload: received file: ' . $filename . ' as ' . $src . ' ('. $type . ') ' . $filesize . ' bytes');
$maximagesize = Strings::getBytesFromShorthand($this->config->get('system', 'maximagesize')); $maximagesize = Strings::getBytesFromShorthand($this->config->get('system', 'maximagesize'));
@ -210,7 +208,7 @@ class Photos extends \Friendica\Module\BaseProfile
$imagedata = @file_get_contents($src); $imagedata = @file_get_contents($src);
$image = new Image($imagedata, $type); $image = new Image($imagedata, $type, $filename);
if (!$image->isValid()) { if (!$image->isValid()) {
$this->logger->notice('unable to process image'); $this->logger->notice('unable to process image');
@ -341,14 +339,12 @@ class Photos extends \Friendica\Module\BaseProfile
$pager->getItemsPerPage() $pager->getItemsPerPage()
)); ));
$phototypes = Images::supportedTypes(); $photos = array_map(function ($photo){
$photos = array_map(function ($photo) use ($phototypes) {
return [ return [
'id' => $photo['id'], 'id' => $photo['id'],
'link' => 'photos/' . $this->owner['nickname'] . '/image/' . $photo['resource-id'], 'link' => 'photos/' . $this->owner['nickname'] . '/image/' . $photo['resource-id'],
'title' => $this->t('View Photo'), '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'], 'alt' => $photo['filename'],
'album' => [ 'album' => [
'link' => 'photos/' . $this->owner['nickname'] . '/album/' . bin2hex($photo['album']), 'link' => 'photos/' . $this->owner['nickname'] . '/album/' . bin2hex($photo['album']),

View 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']]); 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, $fetchResult->getContentType(), $request['url']);
$image = new Image($img_str, $mime);
if (!$image->isValid()) { 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(); self::responseError();
// stop. // stop.
} }
// reduce quality - if it isn't a GIF // reduce quality - if it isn't a GIF
if ($image->getType() != 'image/gif') { if ($image->getImageType() != IMAGETYPE_GIF) {
$image->scaleDown($request['size']); $image->scaleDown($request['size']);
} }

View File

@ -213,7 +213,7 @@ class Crop extends BaseSettings
DI::page()['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/profile/photo/crop_head.tpl'), []); 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'); $tpl = Renderer::getMarkupTemplate('settings/profile/photo/crop.tpl');
$o = Renderer::replaceMacros($tpl, [ $o = Renderer::replaceMacros($tpl, [
'$filename' => $filename, '$filename' => $filename,

View File

@ -52,8 +52,6 @@ class Index extends BaseSettings
$filesize = intval($_FILES['userfile']['size']); $filesize = intval($_FILES['userfile']['size']);
$filetype = $_FILES['userfile']['type']; $filetype = $_FILES['userfile']['type'];
$filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
$maximagesize = Strings::getBytesFromShorthand(DI::config()->get('system', 'maximagesize', 0)); $maximagesize = Strings::getBytesFromShorthand(DI::config()->get('system', 'maximagesize', 0));
if ($maximagesize && $filesize > $maximagesize) { if ($maximagesize && $filesize > $maximagesize) {
@ -63,7 +61,7 @@ class Index extends BaseSettings
} }
$imagedata = @file_get_contents($src); $imagedata = @file_get_contents($src);
$Image = new Image($imagedata, $filetype); $Image = new Image($imagedata, $filetype, $filename);
if (!$Image->isValid()) { if (!$Image->isValid()) {
DI::sysmsg()->addNotice(DI::l10n()->t('Unable to process image.')); DI::sysmsg()->addNotice(DI::l10n()->t('Unable to process image.'));

View File

@ -392,7 +392,7 @@ class Import extends \Friendica\BaseModule
$photo['data'] = hex2bin($photo['data']); $photo['data'] = hex2bin($photo['data']);
$r = Photo::store( $r = Photo::store(
new Image($photo['data'], $photo['type']), new Image($photo['data'], $photo['type'], $photo['filename']),
$photo['uid'], $photo['contact-id'], //0 $photo['uid'], $photo['contact-id'], //0
$photo['resource-id'], $photo['filename'], $photo['album'], $photo['scale'], $photo['profile'], //1 $photo['resource-id'], $photo['filename'], $photo['album'], $photo['scale'], $photo['profile'], //1
$photo['allow_cid'], $photo['allow_gid'], $photo['deny_cid'], $photo['deny_gid'] $photo['allow_cid'], $photo['allow_gid'], $photo['deny_cid'], $photo['deny_gid']

View File

@ -32,7 +32,7 @@ class HttpClientAccept
public const ATOM_XML = 'application/atom+xml,text/xml;q=0.9,*/*;q=0.8'; 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 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 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 JRD_JSON = 'application/jrd+json,application/json;q=0.9';
public const 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"'; public const JSON_AS = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';

View File

@ -45,25 +45,37 @@ class Image
private $width; private $width;
private $height; private $height;
private $valid; private $valid;
private $type; private $imageType;
private $types; private $filename;
/** /**
* Constructor * Constructor
* *
* @param string $data Image data * @param string $data Image data
* @param string $type optional, default null * @param string $type optional, default ''
* @param string $filename optional, default ''
* @param string $imagick optional, default 'true'
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException * @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->filename = $filename;
$this->types = Images::supportedTypes(); $type = Images::addMimeTypeByDataIfInvalid($type, $data);
if (!array_key_exists($type, $this->types)) { $type = Images::addMimeTypeByExtensionIfInvalid($type, $filename);
$type = 'image/jpeg';
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))) { if ($this->isImagick() && (empty($data) || $this->loadData($data))) {
$this->valid = !empty($data); $this->valid = !empty($data);
@ -75,6 +87,49 @@ class Image
$this->loadData($data); $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 * Destructor
* *
@ -118,28 +173,28 @@ class Image
$this->image->readImageBlob($data); $this->image->readImageBlob($data);
} catch (Exception $e) { } catch (Exception $e) {
// Imagick couldn't use the data // 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; return false;
} }
/* /*
* Setup the image to the format it will be saved to * Setup the image to the format it will be saved to
*/ */
$map = Images::getFormatsMap(); $this->image->setFormat(Images::getImagickFormatByImageType($this->imageType));
$format = $map[$this->type];
$this->image->setFormat($format);
// Always coalesce, if it is not a multi-frame image it won't hurt anyway // Always coalesce, if it is not a multi-frame image it won't hurt anyway
try { try {
$this->image = $this->image->coalesceImages(); $this->image = $this->image->coalesceImages();
} catch (Exception $e) { } 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; return false;
} }
/* /*
* setup the compression here, so we'll do it only once * setup the compression here, so we'll do it only once
*/ */
switch ($this->getType()) { switch ($this->getImageType()) {
case 'image/png': case IMAGETYPE_PNG:
$quality = DI::config()->get('system', 'png_quality'); $quality = DI::config()->get('system', 'png_quality');
/* /*
* From http://www.imagemagick.org/script/command-line-options.php#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' * unless the image has a color map, in which case it means compression level 7 with no PNG filtering'
*/ */
$quality = $quality * 10; $quality = $quality * 10;
$this->image->setCompressionQuality($quality); $this->image->setImageCompressionQuality($quality);
break; break;
case 'image/jpg': case IMAGETYPE_JPEG:
case 'image/jpeg':
$quality = DI::config()->get('system', 'jpeg_quality'); $quality = DI::config()->get('system', 'jpeg_quality');
$this->image->setCompressionQuality($quality); $this->image->setImageCompressionQuality($quality);
} }
$this->width = $this->image->getImageWidth(); $this->width = $this->image->getImageWidth();
@ -182,9 +236,9 @@ class Image
} catch (\Throwable $error) { } catch (\Throwable $error) {
/** @see https://github.com/php/doc-en/commit/d09a881a8e9059d11e756ee59d75bf404d6941ed */ /** @see https://github.com/php/doc-en/commit/d09a881a8e9059d11e756ee59d75bf404d6941ed */
if (strstr($error->getMessage(), "gd-webp cannot allocate temporary buffer")) { 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 { } 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 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 false;
} }
return $this->types[$this->getType()]; return Images::getExtensionByImageType($this->imageType);
} }
/** /**
@ -398,7 +464,7 @@ class Image
return false; return false;
} }
if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) { if ((!function_exists('exif_read_data')) || ($this->getImageType() !== IMAGETYPE_JPEG)) {
return; return;
} }
@ -545,7 +611,7 @@ class Image
imagealphablending($dest, false); imagealphablending($dest, false);
imagesavealpha($dest, true); 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 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
} }
@ -570,13 +636,13 @@ class Image
*/ */
public function toStatic() public function toStatic()
{ {
if ($this->type != 'image/gif') { if ($this->imageType != IMAGETYPE_GIF) {
return; return;
} }
if ($this->isImagick()) { if ($this->isImagick()) {
$this->type == 'image/png'; $this->imageType = IMAGETYPE_PNG;
$this->image->setFormat('png'); $this->image->setFormat(Images::getImagickFormatByImageType($this->imageType));
} }
} }
@ -614,7 +680,7 @@ class Image
imagealphablending($dest, false); imagealphablending($dest, false);
imagesavealpha($dest, true); 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 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); imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
@ -668,17 +734,28 @@ class Image
$stream = fopen('php://memory','r+'); $stream = fopen('php://memory','r+');
switch ($this->getType()) { switch ($this->getImageType()) {
case 'image/png': case IMAGETYPE_PNG:
$quality = DI::config()->get('system', 'png_quality'); $quality = DI::config()->get('system', 'png_quality');
imagepng($this->image, $stream, $quality); imagepng($this->image, $stream, $quality);
break; break;
case 'image/jpeg': case IMAGETYPE_JPEG:
case 'image/jpg':
$quality = DI::config()->get('system', 'jpeg_quality'); $quality = DI::config()->get('system', 'jpeg_quality');
imagejpeg($this->image, $stream, $quality); imagejpeg($this->image, $stream, $quality);
break; 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); rewind($stream);
return stream_get_contents($stream); return stream_get_contents($stream);
@ -692,7 +769,7 @@ class Image
*/ */
public function getBlurHash(): string public function getBlurHash(): string
{ {
$image = New Image($this->asString()); $image = New Image($this->asString(), $this->getType(), $this->filename, false);
if (empty($image) || !$this->isValid()) { if (empty($image) || !$this->isValid()) {
return ''; return '';
} }

View File

@ -304,10 +304,8 @@ class DFRN
$profilephotos = Photo::selectToArray(['resource-id', 'scale', 'type'], ['profile' => true, 'uid' => $uid], ['order' => ['scale']]); $profilephotos = Photo::selectToArray(['resource-id', 'scale', 'type'], ['profile' => true, 'uid' => $uid], ['order' => ['scale']]);
$photos = []; $photos = [];
$ext = Images::supportedTypes();
foreach ($profilephotos as $p) { 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'); $doc = new DOMDocument('1.0', 'utf-8');

View File

@ -43,6 +43,7 @@ use Friendica\Model\Tag;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Images;
use Friendica\Util\Network; use Friendica\Util\Network;
use Friendica\Util\ParseUrl; use Friendica\Util\ParseUrl;
use Friendica\Util\Proxy; use Friendica\Util\Proxy;
@ -573,7 +574,7 @@ class Feed
if (in_array($fetch_further_information, [LocalRelationship::FFI_INFORMATION, LocalRelationship::FFI_BOTH])) { if (in_array($fetch_further_information, [LocalRelationship::FFI_INFORMATION, LocalRelationship::FFI_BOTH])) {
// Handle enclosures and treat them as preview picture // Handle enclosures and treat them as preview picture
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if ($attachment['mimetype'] == 'image/jpeg') { if (Images::isSupportedMimeType($attachment['mimetype'])) {
$preview = $attachment['url']; $preview = $attachment['url'];
} }
} }

View File

@ -33,19 +33,107 @@ use Friendica\Object\Image;
*/ */
class Images 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 [ $formats = [
'image/jpeg' => 'JPG', // @todo add "IMAGETYPE_AVIF => 'AVIF'" once our minimal supported PHP version is 8.1.0
'image/jpg' => 'JPG', IMAGETYPE_WEBP => 'WEBP',
'image/png' => 'PNG', IMAGETYPE_PNG => 'PNG',
'image/gif' => 'GIF', 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 public static function getExtensionByMimeType(string $mimetype): string
{ {
switch ($mimetype) { if (empty($mimetype)) {
case 'image/png': return '';
$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;
} }
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 * @return array
*/ */
public static function supportedTypes(): array public static function supportedMimeTypes(): array
{ {
$types = [ $types = [];
'image/jpeg' => 'jpg',
'image/jpg' => 'jpg',
];
if (class_exists('Imagick')) { // @todo enable, once our lowest supported PHP version is 8.1.0
// Imagick::queryFormats won't help us a lot there... //if (imagetypes() & IMG_AVIF) {
// At least, not yet, other parts of friendica uses this array // $types[] = image_type_to_mime_type(IMAGETYPE_AVIF);
$types += [ //}
'image/png' => 'png', if (imagetypes() & IMG_WEBP) {
'image/gif' => 'gif' $types[] = image_type_to_mime_type(IMAGETYPE_WEBP);
]; }
} elseif (imagetypes() & IMG_PNG) { if (imagetypes() & IMG_PNG) {
$types += [ $types[] = image_type_to_mime_type(IMAGETYPE_PNG);
'image/png' => '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; return $types;
@ -115,45 +192,69 @@ class Images
* @return string MIME type * @return string MIME type
* @throws \Exception * @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); $image = @getimagesizefromstring($image_data);
if (!empty($image['mime'])) { if (!empty($image['mime'])) {
Logger::info('Mime type detected via data', ['filename' => $filename, 'default' => $default, 'mime' => $image['mime']]);
return $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 $mimetype
* @param string $filename File name (for guessing the type via the extension) * @return boolean
* @param string $default default MIME type
* @return string MIME type
* @throws \Exception
*/ */
public static function getMimeTypeBySource(string $sourcefile, string $filename = '', string $default = ''): string public static function isSupportedMimeType(string $mimetype): bool
{ {
if (substr($default, 0, 6) == 'image/') { if (substr($mimetype, 0, 6) != 'image/') {
Logger::info('Using default mime type', ['filename' => $filename, 'mime' => $default]); return false;
return $default;
} }
$image = @getimagesize($sourcefile); return in_array(self::sanitizeMimeType($mimetype), self::supportedMimeTypes());
if (!empty($image['mime'])) { }
Logger::info('Mime type detected via file', ['filename' => $filename, 'default' => $default, 'image' => $image]);
return $image['mime']; /**
* 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 public static function guessTypeByExtension(string $filename): string
{ {
$ext = pathinfo(parse_url($filename, PHP_URL_PATH), PATHINFO_EXTENSION); if (empty($filename)) {
$types = self::supportedTypes(); return '';
$type = 'image/jpeg'; }
foreach ($types as $m => $e) {
if ($ext == $e) { $ext = strtolower(pathinfo(parse_url($filename, PHP_URL_PATH), PATHINFO_EXTENSION));
$type = $m; $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]); Logger::debug('Unhandled extension', ['filename' => $filename, 'extension' => $ext]);
return $type; return '';
} }
/** /**
@ -256,7 +364,7 @@ class Images
return []; return [];
} }
$image = new Image($img_str); $image = new Image($img_str, '', $url);
if ($image->isValid()) { if ($image->isValid()) {
$data['blurhash'] = $image->getBlurHash(); $data['blurhash'] = $image->getBlurHash();
@ -344,7 +452,7 @@ class Images
{ {
return self::getBBCodeByUrl( return self::getBBCodeByUrl(
DI::baseUrl() . '/photos/' . $nickname . '/image/' . $resource_id, DI::baseUrl() . '/photos/' . $nickname . '/image/' . $resource_id,
DI::baseUrl() . '/photo/' . $resource_id . '-' . $preview. '.' . $ext, DI::baseUrl() . '/photo/' . $resource_id . '-' . $preview. $ext,
$description $description
); );
} }

View File

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

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 2024.03-rc\n" "Project-Id-Version: 2024.03-rc\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -45,7 +45,7 @@ msgid "Item not found."
msgstr "" msgstr ""
#: mod/item.php:457 mod/message.php:67 mod/message.php:113 mod/notes.php:45 #: 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/Attach.php:55 src/Module/BaseApi.php:103
#: src/Module/BaseNotifications.php:98 src/Module/BaseSettings.php:50 #: src/Module/BaseNotifications.php:98 src/Module/BaseSettings.php:50
#: src/Module/Calendar/Event/API.php:88 src/Module/Calendar/Event/Form.php:84 #: 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/Channels.php:135 src/Module/Settings/Delegation.php:90
#: src/Module/Settings/Display.php:90 src/Module/Settings/Display.php:199 #: 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/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/RemoveMe.php:119 src/Module/Settings/UserExport.php:80
#: src/Module/Settings/UserExport.php:114 #: src/Module/Settings/UserExport.php:114
#: src/Module/Settings/UserExport.php:215 #: src/Module/Settings/UserExport.php:215
@ -289,16 +289,16 @@ msgstr ""
msgid "Insert web link" msgid "Insert web link"
msgstr "" 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/Content/Conversation.php:401 src/Content/Conversation.php:1586
#: src/Module/Item/Compose.php:206 src/Module/Post/Edit.php:145 #: src/Module/Item/Compose.php:206 src/Module/Post/Edit.php:145
#: src/Object/Post.php:609 #: src/Object/Post.php:609
msgid "Please wait" msgid "Please wait"
msgstr "" msgstr ""
#: mod/message.php:202 mod/message.php:356 mod/photos.php:705 #: mod/message.php:202 mod/message.php:356 mod/photos.php:701
#: mod/photos.php:824 mod/photos.php:1101 mod/photos.php:1142 #: mod/photos.php:820 mod/photos.php:1097 mod/photos.php:1138
#: mod/photos.php:1198 mod/photos.php:1278 #: mod/photos.php:1194 mod/photos.php:1274
#: src/Module/Calendar/Event/Form.php:250 src/Module/Contact/Advanced.php:132 #: src/Module/Calendar/Event/Form.php:250 src/Module/Contact/Advanced.php:132
#: src/Module/Contact/Profile.php:364 #: src/Module/Contact/Profile.php:364
#: src/Module/Debug/ActivityPubConversion.php:140 #: src/Module/Debug/ActivityPubConversion.php:140
@ -386,7 +386,7 @@ msgstr ""
msgid "Save" msgid "Save"
msgstr "" 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/Model/Event.php:512 src/Model/Profile.php:233
#: src/Module/Calendar/Export.php:74 src/Module/Calendar/Show.php:74 #: 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 #: 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 "" msgstr ""
#: mod/photos.php:106 src/Module/BaseProfile.php:68 #: mod/photos.php:106 src/Module/BaseProfile.php:68
#: src/Module/Profile/Photos.php:379 #: src/Module/Profile/Photos.php:375
msgid "Photo Albums" msgid "Photo Albums"
msgstr "" msgstr ""
#: mod/photos.php:107 src/Module/Profile/Photos.php:380 #: mod/photos.php:107 src/Module/Profile/Photos.php:376
#: src/Module/Profile/Photos.php:400 #: src/Module/Profile/Photos.php:396
msgid "Recent Photos" msgid "Recent Photos"
msgstr "" msgstr ""
#: mod/photos.php:109 mod/photos.php:872 src/Module/Profile/Photos.php:382 #: mod/photos.php:109 mod/photos.php:868 src/Module/Profile/Photos.php:378
#: src/Module/Profile/Photos.php:402 #: src/Module/Profile/Photos.php:398
msgid "Upload New Photos" msgid "Upload New Photos"
msgstr "" msgstr ""
#: mod/photos.php:121 src/Module/BaseSettings.php:72 #: mod/photos.php:121 src/Module/BaseSettings.php:72
#: src/Module/Profile/Photos.php:363 #: src/Module/Profile/Photos.php:359
msgid "everybody" msgid "everybody"
msgstr "" msgstr ""
#: mod/photos.php:159 #: mod/photos.php:157
msgid "Contact information unavailable" msgid "Contact information unavailable"
msgstr "" msgstr ""
#: mod/photos.php:188 #: mod/photos.php:186
msgid "Album not found." msgid "Album not found."
msgstr "" msgstr ""
#: mod/photos.php:244 #: mod/photos.php:242
msgid "Album successfully deleted" msgid "Album successfully deleted"
msgstr "" msgstr ""
#: mod/photos.php:246 #: mod/photos.php:244
msgid "Album was empty." msgid "Album was empty."
msgstr "" msgstr ""
#: mod/photos.php:277 #: mod/photos.php:275
msgid "Failed to delete the photo." msgid "Failed to delete the photo."
msgstr "" msgstr ""
#: mod/photos.php:545 #: mod/photos.php:543
msgid "a photo" msgid "a photo"
msgstr "" msgstr ""
#: mod/photos.php:545 #: mod/photos.php:543
#, php-format #, php-format
msgid "%1$s was tagged in %2$s by %3$s" msgid "%1$s was tagged in %2$s by %3$s"
msgstr "" msgstr ""
#: mod/photos.php:582 src/Module/Conversation/Community.php:160 #: mod/photos.php:580 src/Module/Conversation/Community.php:160
#: src/Module/Directory.php:48 src/Module/Profile/Photos.php:295 #: src/Module/Directory.php:48 src/Module/Profile/Photos.php:293
#: src/Module/Search/Index.php:65 #: src/Module/Search/Index.php:65
msgid "Public access denied." msgid "Public access denied."
msgstr "" msgstr ""
#: mod/photos.php:587 #: mod/photos.php:585
msgid "No photos selected" msgid "No photos selected"
msgstr "" msgstr ""
#: mod/photos.php:721 #: mod/photos.php:717
#, php-format #, php-format
msgid "The maximum accepted image size is %s" msgid "The maximum accepted image size is %s"
msgstr "" msgstr ""
#: mod/photos.php:728 #: mod/photos.php:724
msgid "Upload Photos" msgid "Upload Photos"
msgstr "" msgstr ""
#: mod/photos.php:732 mod/photos.php:820 #: mod/photos.php:728 mod/photos.php:816
msgid "New album name: " msgid "New album name: "
msgstr "" msgstr ""
#: mod/photos.php:733 #: mod/photos.php:729
msgid "or select existing album:" msgid "or select existing album:"
msgstr "" msgstr ""
#: mod/photos.php:734 #: mod/photos.php:730
msgid "Do not show a status post for this upload" msgid "Do not show a status post for this upload"
msgstr "" 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 #: src/Module/Calendar/Event/Form.php:253 src/Module/Post/Edit.php:183
msgid "Permissions" msgid "Permissions"
msgstr "" msgstr ""
#: mod/photos.php:801 #: mod/photos.php:797
msgid "Do you really want to delete this photo album and all its photos?" msgid "Do you really want to delete this photo album and all its photos?"
msgstr "" msgstr ""
#: mod/photos.php:802 mod/photos.php:825 #: mod/photos.php:798 mod/photos.php:821
msgid "Delete Album" msgid "Delete Album"
msgstr "" 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/Follow.php:173 src/Module/Contact/Revoke.php:109
#: src/Module/Contact/Unfollow.php:126 #: src/Module/Contact/Unfollow.php:126
#: src/Module/Media/Attachment/Browser.php:77 #: src/Module/Media/Attachment/Browser.php:77
@ -501,132 +501,132 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: mod/photos.php:829 #: mod/photos.php:825
msgid "Edit Album" msgid "Edit Album"
msgstr "" msgstr ""
#: mod/photos.php:830 #: mod/photos.php:826
msgid "Drop Album" msgid "Drop Album"
msgstr "" msgstr ""
#: mod/photos.php:834 #: mod/photos.php:830
msgid "Show Newest First" msgid "Show Newest First"
msgstr "" msgstr ""
#: mod/photos.php:836 #: mod/photos.php:832
msgid "Show Oldest First" msgid "Show Oldest First"
msgstr "" msgstr ""
#: mod/photos.php:857 src/Module/Profile/Photos.php:350 #: mod/photos.php:853 src/Module/Profile/Photos.php:346
msgid "View Photo" msgid "View Photo"
msgstr "" msgstr ""
#: mod/photos.php:889 #: mod/photos.php:885
msgid "Permission denied. Access to this item may be restricted." msgid "Permission denied. Access to this item may be restricted."
msgstr "" msgstr ""
#: mod/photos.php:891 #: mod/photos.php:887
msgid "Photo not available" msgid "Photo not available"
msgstr "" msgstr ""
#: mod/photos.php:901 #: mod/photos.php:897
msgid "Do you really want to delete this photo?" msgid "Do you really want to delete this photo?"
msgstr "" msgstr ""
#: mod/photos.php:902 mod/photos.php:1102 #: mod/photos.php:898 mod/photos.php:1098
msgid "Delete Photo" msgid "Delete Photo"
msgstr "" msgstr ""
#: mod/photos.php:1000 #: mod/photos.php:996
msgid "View photo" msgid "View photo"
msgstr "" msgstr ""
#: mod/photos.php:1002 #: mod/photos.php:998
msgid "Edit photo" msgid "Edit photo"
msgstr "" msgstr ""
#: mod/photos.php:1003 #: mod/photos.php:999
msgid "Delete photo" msgid "Delete photo"
msgstr "" msgstr ""
#: mod/photos.php:1004 #: mod/photos.php:1000
msgid "Use as profile photo" msgid "Use as profile photo"
msgstr "" msgstr ""
#: mod/photos.php:1011 #: mod/photos.php:1007
msgid "Private Photo" msgid "Private Photo"
msgstr "" msgstr ""
#: mod/photos.php:1017 #: mod/photos.php:1013
msgid "View Full Size" msgid "View Full Size"
msgstr "" msgstr ""
#: mod/photos.php:1070 #: mod/photos.php:1066
msgid "Tags: " msgid "Tags: "
msgstr "" msgstr ""
#: mod/photos.php:1073 #: mod/photos.php:1069
msgid "[Select tags to remove]" msgid "[Select tags to remove]"
msgstr "" msgstr ""
#: mod/photos.php:1088 #: mod/photos.php:1084
msgid "New album name" msgid "New album name"
msgstr "" msgstr ""
#: mod/photos.php:1089 #: mod/photos.php:1085
msgid "Caption" msgid "Caption"
msgstr "" msgstr ""
#: mod/photos.php:1090 #: mod/photos.php:1086
msgid "Add a Tag" msgid "Add a Tag"
msgstr "" msgstr ""
#: mod/photos.php:1090 #: mod/photos.php:1086
msgid "Example: @bob, @Barbara_Jensen, @jim@example.com, #California, #camping" msgid "Example: @bob, @Barbara_Jensen, @jim@example.com, #California, #camping"
msgstr "" msgstr ""
#: mod/photos.php:1091 #: mod/photos.php:1087
msgid "Do not rotate" msgid "Do not rotate"
msgstr "" msgstr ""
#: mod/photos.php:1092 #: mod/photos.php:1088
msgid "Rotate CW (right)" msgid "Rotate CW (right)"
msgstr "" msgstr ""
#: mod/photos.php:1093 #: mod/photos.php:1089
msgid "Rotate CCW (left)" msgid "Rotate CCW (left)"
msgstr "" 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/Module/Contact.php:618 src/Module/Item/Compose.php:188
#: src/Object/Post.php:1151 #: src/Object/Post.php:1151
msgid "This is you" msgid "This is you"
msgstr "" 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/Module/Moderation/Reports.php:95 src/Object/Post.php:603
#: src/Object/Post.php:1153 #: src/Object/Post.php:1153
msgid "Comment" msgid "Comment"
msgstr "" 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/Content/Conversation.php:416 src/Module/Calendar/Event/Form.php:248
#: src/Module/Item/Compose.php:201 src/Module/Post/Edit.php:165 #: src/Module/Item/Compose.php:201 src/Module/Post/Edit.php:165
#: src/Object/Post.php:1167 #: src/Object/Post.php:1167
msgid "Preview" msgid "Preview"
msgstr "" 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 #: src/Module/Post/Edit.php:130 src/Object/Post.php:1155
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
#: mod/photos.php:1236 src/Content/Conversation.php:1501 #: mod/photos.php:1232 src/Content/Conversation.php:1501
#: src/Object/Post.php:261 #: src/Object/Post.php:261
msgid "Select" msgid "Select"
msgstr "" 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/Active.php:136
#: src/Module/Moderation/Users/Blocked.php:136 #: src/Module/Moderation/Users/Blocked.php:136
#: src/Module/Moderation/Users/Index.php:151 #: src/Module/Moderation/Users/Index.php:151
@ -635,23 +635,23 @@ msgstr ""
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
#: mod/photos.php:1298 src/Object/Post.php:426 #: mod/photos.php:1294 src/Object/Post.php:426
msgid "Like" msgid "Like"
msgstr "" msgstr ""
#: mod/photos.php:1299 src/Object/Post.php:426 #: mod/photos.php:1295 src/Object/Post.php:426
msgid "I like this (toggle)" msgid "I like this (toggle)"
msgstr "" msgstr ""
#: mod/photos.php:1300 src/Object/Post.php:427 #: mod/photos.php:1296 src/Object/Post.php:427
msgid "Dislike" msgid "Dislike"
msgstr "" 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)" msgid "I don't like this (toggle)"
msgstr "" msgstr ""
#: mod/photos.php:1324 #: mod/photos.php:1320
msgid "Map" msgid "Map"
msgstr "" msgstr ""
@ -1803,31 +1803,31 @@ msgstr ""
msgid "Follow Thread" msgid "Follow Thread"
msgstr "" msgstr ""
#: src/Content/Item.php:430 src/Model/Contact.php:1250 #: src/Content/Item.php:430 src/Model/Contact.php:1243
msgid "View Status" msgid "View Status"
msgstr "" msgstr ""
#: src/Content/Item.php:431 src/Content/Item.php:452 src/Model/Contact.php:1184 #: src/Content/Item.php:431 src/Content/Item.php:452 src/Model/Contact.php:1177
#: src/Model/Contact.php:1241 src/Model/Contact.php:1251 #: src/Model/Contact.php:1234 src/Model/Contact.php:1244
#: src/Module/Directory.php:157 src/Module/Settings/Profile/Index.php:259 #: src/Module/Directory.php:157 src/Module/Settings/Profile/Index.php:259
msgid "View Profile" msgid "View Profile"
msgstr "" msgstr ""
#: src/Content/Item.php:432 src/Model/Contact.php:1252 #: src/Content/Item.php:432 src/Model/Contact.php:1245
msgid "View Photos" msgid "View Photos"
msgstr "" 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 #: src/Model/Profile.php:468
msgid "Network Posts" msgid "Network Posts"
msgstr "" msgstr ""
#: src/Content/Item.php:434 src/Model/Contact.php:1243 #: src/Content/Item.php:434 src/Model/Contact.php:1236
#: src/Model/Contact.php:1254 #: src/Model/Contact.php:1247
msgid "View Contact" msgid "View Contact"
msgstr "" msgstr ""
#: src/Content/Item.php:435 src/Model/Contact.php:1255 #: src/Content/Item.php:435 src/Model/Contact.php:1248
msgid "Send PM" msgid "Send PM"
msgstr "" msgstr ""
@ -1863,7 +1863,7 @@ msgid "Languages"
msgstr "" msgstr ""
#: src/Content/Item.php:449 src/Content/Widget.php:80 #: 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 #: src/Module/Contact/Follow.php:167 view/theme/vier/theme.php:195
msgid "Connect/Follow" msgid "Connect/Follow"
msgstr "" msgstr ""
@ -2190,39 +2190,39 @@ msgstr ""
msgid "last" msgid "last"
msgstr "" msgstr ""
#: src/Content/Text/BBCode.php:766 src/Content/Text/BBCode.php:1727 #: src/Content/Text/BBCode.php:767 src/Content/Text/BBCode.php:1728
#: src/Content/Text/BBCode.php:1728 #: src/Content/Text/BBCode.php:1729
msgid "Image/photo" msgid "Image/photo"
msgstr "" msgstr ""
#: src/Content/Text/BBCode.php:984 #: src/Content/Text/BBCode.php:985
#, php-format #, php-format
msgid "" msgid ""
"<a href=\"%1$s\" target=\"_blank\" rel=\"noopener noreferrer\">%2$s</a> %3$s" "<a href=\"%1$s\" target=\"_blank\" rel=\"noopener noreferrer\">%2$s</a> %3$s"
msgstr "" 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 #: src/Model/Item.php:4005 src/Model/Item.php:4006
msgid "Link to source" msgid "Link to source"
msgstr "" 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" msgid "Click to open/close"
msgstr "" msgstr ""
#: src/Content/Text/BBCode.php:1667 #: src/Content/Text/BBCode.php:1668
msgid "$1 wrote:" msgid "$1 wrote:"
msgstr "" 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" msgid "Encrypted content"
msgstr "" msgstr ""
#: src/Content/Text/BBCode.php:1996 #: src/Content/Text/BBCode.php:1997
msgid "Invalid source protocol" msgid "Invalid source protocol"
msgstr "" msgstr ""
#: src/Content/Text/BBCode.php:2015 #: src/Content/Text/BBCode.php:2016
msgid "Invalid link protocol" msgid "Invalid link protocol"
msgstr "" msgstr ""
@ -2370,7 +2370,7 @@ msgstr ""
msgid "Organisations" msgid "Organisations"
msgstr "" msgstr ""
#: src/Content/Widget.php:536 src/Model/Contact.php:1746 #: src/Content/Widget.php:536 src/Model/Contact.php:1739
msgid "News" msgid "News"
msgstr "" msgstr ""
@ -2438,12 +2438,12 @@ msgstr[1] ""
msgid "More Trending Tags" msgid "More Trending Tags"
msgstr "" 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 #: src/Model/Profile.php:461
msgid "Post to group" msgid "Post to group"
msgstr "" 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 #: src/Model/Profile.php:466 src/Module/Moderation/Item/Source.php:85
msgid "Mention" msgid "Mention"
msgstr "" msgstr ""
@ -2471,13 +2471,13 @@ msgstr ""
msgid "Network:" msgid "Network:"
msgstr "" msgstr ""
#: src/Content/Widget/VCard.php:128 src/Model/Contact.php:1245 #: src/Content/Widget/VCard.php:128 src/Model/Contact.php:1238
#: src/Model/Contact.php:1257 src/Model/Profile.php:479 #: src/Model/Contact.php:1250 src/Model/Profile.php:479
#: src/Module/Contact/Profile.php:463 #: src/Module/Contact/Profile.php:463
msgid "Unfollow" msgid "Unfollow"
msgstr "" 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 #: src/Model/Profile.php:463
msgid "View group" msgid "View group"
msgstr "" msgstr ""
@ -2849,23 +2849,19 @@ msgstr ""
msgid "TLS detected" msgid "TLS detected"
msgstr "" msgstr ""
#: src/Core/Installer.php:646 #: src/Core/Installer.php:636
msgid "ImageMagick PHP extension is not installed" msgid "ImageMagick PHP extension is not installed"
msgstr "" msgstr ""
#: src/Core/Installer.php:648 #: src/Core/Installer.php:638
msgid "ImageMagick PHP extension is installed" msgid "ImageMagick PHP extension is installed"
msgstr "" msgstr ""
#: src/Core/Installer.php:650 #: src/Core/Installer.php:659
msgid "ImageMagick supports GIF"
msgstr ""
#: src/Core/Installer.php:672
msgid "Database already in use." msgid "Database already in use."
msgstr "" msgstr ""
#: src/Core/Installer.php:677 #: src/Core/Installer.php:664
msgid "Could not connect to database." msgid "Could not connect to database."
msgstr "" msgstr ""
@ -3247,90 +3243,90 @@ msgstr ""
msgid "Edit circles" msgid "Edit circles"
msgstr "" 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:132
#: src/Module/Notifications/Introductions.php:204 #: src/Module/Notifications/Introductions.php:204
msgid "Approve" msgid "Approve"
msgstr "" msgstr ""
#: src/Model/Contact.php:1742 #: src/Model/Contact.php:1735
msgid "Organisation" msgid "Organisation"
msgstr "" msgstr ""
#: src/Model/Contact.php:1750 #: src/Model/Contact.php:1743
msgid "Group" msgid "Group"
msgstr "" 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" msgid "Relay"
msgstr "" msgstr ""
#: src/Model/Contact.php:3057 #: src/Model/Contact.php:3050
msgid "Disallowed profile URL." msgid "Disallowed profile URL."
msgstr "" msgstr ""
#: src/Model/Contact.php:3062 src/Module/Friendica.php:101 #: src/Model/Contact.php:3055 src/Module/Friendica.php:101
msgid "Blocked domain" msgid "Blocked domain"
msgstr "" msgstr ""
#: src/Model/Contact.php:3067 #: src/Model/Contact.php:3060
msgid "Connect URL missing." msgid "Connect URL missing."
msgstr "" msgstr ""
#: src/Model/Contact.php:3076 #: src/Model/Contact.php:3069
msgid "" msgid ""
"The contact could not be added. Please check the relevant network " "The contact could not be added. Please check the relevant network "
"credentials in your Settings -> Social Networks page." "credentials in your Settings -> Social Networks page."
msgstr "" msgstr ""
#: src/Model/Contact.php:3094 #: src/Model/Contact.php:3087
#, php-format #, php-format
msgid "Expected network %s does not match actual network %s" msgid "Expected network %s does not match actual network %s"
msgstr "" 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." msgid "This seems to be a relay account. They can't be followed by users."
msgstr "" msgstr ""
#: src/Model/Contact.php:3118 #: src/Model/Contact.php:3111
msgid "The profile address specified does not provide adequate information." msgid "The profile address specified does not provide adequate information."
msgstr "" msgstr ""
#: src/Model/Contact.php:3120 #: src/Model/Contact.php:3113
msgid "No compatible communication protocols or feeds were discovered." msgid "No compatible communication protocols or feeds were discovered."
msgstr "" msgstr ""
#: src/Model/Contact.php:3123 #: src/Model/Contact.php:3116
msgid "An author or name was not found." msgid "An author or name was not found."
msgstr "" msgstr ""
#: src/Model/Contact.php:3126 #: src/Model/Contact.php:3119
msgid "No browser URL could be matched to this address." msgid "No browser URL could be matched to this address."
msgstr "" msgstr ""
#: src/Model/Contact.php:3129 #: src/Model/Contact.php:3122
msgid "" msgid ""
"Unable to match @-style Identity Address with a known protocol or email " "Unable to match @-style Identity Address with a known protocol or email "
"contact." "contact."
msgstr "" msgstr ""
#: src/Model/Contact.php:3130 #: src/Model/Contact.php:3123
msgid "Use mailto: in front of address to force email check." msgid "Use mailto: in front of address to force email check."
msgstr "" msgstr ""
#: src/Model/Contact.php:3136 #: src/Model/Contact.php:3129
msgid "" msgid ""
"The profile address specified belongs to a network which has been disabled " "The profile address specified belongs to a network which has been disabled "
"on this site." "on this site."
msgstr "" msgstr ""
#: src/Model/Contact.php:3141 #: src/Model/Contact.php:3134
msgid "" msgid ""
"Limited profile. This person will be unable to receive direct/personal " "Limited profile. This person will be unable to receive direct/personal "
"notifications from you." "notifications from you."
msgstr "" msgstr ""
#: src/Model/Contact.php:3207 #: src/Model/Contact.php:3200
msgid "Unable to retrieve contact information." msgid "Unable to retrieve contact information."
msgstr "" msgstr ""
@ -3527,7 +3523,7 @@ msgstr ""
msgid "[no subject]" msgid "[no subject]"
msgstr "" 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" msgid "Wall Photos"
msgstr "" msgstr ""
@ -3811,11 +3807,11 @@ msgid ""
"An error occurred creating your default contact circle. Please try again." "An error occurred creating your default contact circle. Please try again."
msgstr "" msgstr ""
#: src/Model/User.php:1415 #: src/Model/User.php:1413
msgid "Profile Photos" msgid "Profile Photos"
msgstr "" msgstr ""
#: src/Model/User.php:1597 #: src/Model/User.php:1595
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
@ -3823,7 +3819,7 @@ msgid ""
"\t\t\tthe administrator of %2$s has set up an account for you." "\t\t\tthe administrator of %2$s has set up an account for you."
msgstr "" msgstr ""
#: src/Model/User.php:1600 #: src/Model/User.php:1598
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
@ -3859,12 +3855,12 @@ msgid ""
"\t\tThank you and welcome to %4$s." "\t\tThank you and welcome to %4$s."
msgstr "" msgstr ""
#: src/Model/User.php:1632 src/Model/User.php:1738 #: src/Model/User.php:1630 src/Model/User.php:1736
#, php-format #, php-format
msgid "Registration details for %s" msgid "Registration details for %s"
msgstr "" msgstr ""
#: src/Model/User.php:1652 #: src/Model/User.php:1650
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
@ -3880,12 +3876,12 @@ msgid ""
"\t\t" "\t\t"
msgstr "" msgstr ""
#: src/Model/User.php:1671 #: src/Model/User.php:1669
#, php-format #, php-format
msgid "Registration at %s" msgid "Registration at %s"
msgstr "" msgstr ""
#: src/Model/User.php:1695 #: src/Model/User.php:1693
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
@ -3894,7 +3890,7 @@ msgid ""
"\t\t\t" "\t\t\t"
msgstr "" msgstr ""
#: src/Model/User.php:1703 #: src/Model/User.php:1701
#, php-format #, php-format
msgid "" msgid ""
"\n" "\n"
@ -3932,7 +3928,7 @@ msgid ""
"\t\t\tThank you and welcome to %2$s." "\t\t\tThank you and welcome to %2$s."
msgstr "" msgstr ""
#: src/Model/User.php:1765 #: src/Model/User.php:1763
msgid "" msgid ""
"User with delegates can't be removed, please remove delegate users first" "User with delegates can't be removed, please remove delegate users first"
msgstr "" msgstr ""
@ -7808,7 +7804,7 @@ msgstr ""
#: src/Module/Media/Attachment/Browser.php:79 #: src/Module/Media/Attachment/Browser.php:79
#: src/Module/Media/Photo/Browser.php:90 #: 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" msgid "Upload"
msgstr "" msgstr ""
@ -7829,14 +7825,14 @@ msgstr ""
msgid "File upload failed." msgid "File upload failed."
msgstr "" msgstr ""
#: src/Module/Media/Photo/Upload.php:152 src/Module/Media/Photo/Upload.php:153 #: src/Module/Media/Photo/Upload.php:150 src/Module/Media/Photo/Upload.php:151
#: src/Module/Profile/Photos.php:217 #: src/Module/Profile/Photos.php:215
#: src/Module/Settings/Profile/Photo/Index.php:69 #: src/Module/Settings/Profile/Photo/Index.php:67
msgid "Unable to process image." msgid "Unable to process image."
msgstr "" msgstr ""
#: src/Module/Media/Photo/Upload.php:178 src/Module/Profile/Photos.php:237 #: src/Module/Media/Photo/Upload.php:176 src/Module/Profile/Photos.php:235
#: src/Module/Settings/Profile/Photo/Index.php:96 #: src/Module/Settings/Profile/Photo/Index.php:94
msgid "Image upload failed." msgid "Image upload failed."
msgstr "" msgstr ""
@ -9075,27 +9071,27 @@ msgstr ""
#: src/Module/Profile/Conversations.php:106 #: src/Module/Profile/Conversations.php:106
#: src/Module/Profile/Conversations.php:109 src/Module/Profile/Profile.php:351 #: 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 #: src/Protocol/OStatus.php:1009
#, php-format #, php-format
msgid "%s's timeline" msgid "%s's timeline"
msgstr "" msgstr ""
#: src/Module/Profile/Conversations.php:107 src/Module/Profile/Profile.php:352 #: 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 #, php-format
msgid "%s's posts" msgid "%s's posts"
msgstr "" msgstr ""
#: src/Module/Profile/Conversations.php:108 src/Module/Profile/Profile.php:353 #: 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 #, php-format
msgid "%s's comments" msgid "%s's comments"
msgstr "" msgstr ""
#: src/Module/Profile/Photos.php:164 src/Module/Profile/Photos.php:167 #: src/Module/Profile/Photos.php:164 src/Module/Profile/Photos.php:167
#: src/Module/Profile/Photos.php:194 #: src/Module/Profile/Photos.php:192
#: src/Module/Settings/Profile/Photo/Index.php:60 #: src/Module/Settings/Profile/Photo/Index.php:58
#, php-format #, php-format
msgid "Image exceeds size limit of %s" msgid "Image exceeds size limit of %s"
msgstr "" msgstr ""
@ -9114,11 +9110,11 @@ msgid ""
"administrator" "administrator"
msgstr "" msgstr ""
#: src/Module/Profile/Photos.php:202 #: src/Module/Profile/Photos.php:200
msgid "Image file is empty." msgid "Image file is empty."
msgstr "" msgstr ""
#: src/Module/Profile/Photos.php:356 #: src/Module/Profile/Photos.php:352
msgid "View Album" msgid "View Album"
msgstr "" msgstr ""
@ -10929,7 +10925,7 @@ msgstr ""
#: src/Module/Settings/Profile/Photo/Crop.php:107 #: src/Module/Settings/Profile/Photo/Crop.php:107
#: src/Module/Settings/Profile/Photo/Crop.php:125 #: src/Module/Settings/Profile/Photo/Crop.php:125
#: src/Module/Settings/Profile/Photo/Crop.php:143 #: 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 #, php-format
msgid "Image size reduction [%s] failed." msgid "Image size reduction [%s] failed."
msgstr "" msgstr ""
@ -10969,31 +10965,31 @@ msgstr ""
msgid "Missing uploaded image." msgid "Missing uploaded image."
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Index.php:125 #: src/Module/Settings/Profile/Photo/Index.php:123
msgid "Profile Picture Settings" msgid "Profile Picture Settings"
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Index.php:126 #: src/Module/Settings/Profile/Photo/Index.php:124
msgid "Current Profile Picture" msgid "Current Profile Picture"
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Index.php:127 #: src/Module/Settings/Profile/Photo/Index.php:125
msgid "Upload Profile Picture" msgid "Upload Profile Picture"
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Index.php:128 #: src/Module/Settings/Profile/Photo/Index.php:126
msgid "Upload Picture:" msgid "Upload Picture:"
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Index.php:133 #: src/Module/Settings/Profile/Photo/Index.php:131
msgid "or" msgid "or"
msgstr "" msgstr ""
#: src/Module/Settings/Profile/Photo/Index.php:135 #: src/Module/Settings/Profile/Photo/Index.php:133
msgid "skip this step" msgid "skip this step"
msgstr "" 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" msgid "select a photo from your photo albums"
msgstr "" msgstr ""