Image handling reworked, new image formats added

This commit is contained in:
Michael 2024-02-16 02:09:14 +00:00
parent 1ea8a4042d
commit 8b1ac7115d
27 changed files with 345 additions and 216 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = '';
}

View File

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

View File

@ -363,6 +363,7 @@ class Photo
$photo['backend-class'] = SystemResource::NAME;
$photo['backend-ref'] = $filename;
$photo['type'] = $mimetype;
$photo['filename'] = basename($filename);
$photo['cacheable'] = false;
return $photo;
@ -394,6 +395,7 @@ class Photo
$photo['backend-class'] = ExternalResource::NAME;
$photo['backend-ref'] = json_encode(['url' => $url, 'uid' => $uid]);
$photo['type'] = $mimetype;
$photo['filename'] = basename(parse_url($url, PHP_URL_PATH));
$photo['cacheable'] = true;
$photo['blurhash'] = $blurhash;
$photo['width'] = $width;
@ -608,9 +610,7 @@ class Photo
return false;
}
$type = Images::getMimeTypeByData($img_str, $image_url, $type);
$image = new Image($img_str, $type);
$image = new Image($img_str, $type, $image_url);
if ($image->isValid()) {
$image->scaleToSquare(300);
@ -619,7 +619,7 @@ 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');
@ -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 [];

View File

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

View File

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

View File

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

View File

@ -95,7 +95,7 @@ class Instance extends BaseApi
return new InstanceV2Entity\Configuration(
$statuses_config,
new InstanceV2Entity\MediaAttachmentsConfig(array_keys(Images::supportedTypes()), $image_size_limit, $image_matrix_limit),
new InstanceV2Entity\MediaAttachmentsConfig(Images::supportedMimeTypes(), $image_size_limit, $image_matrix_limit),
new InstanceV2Entity\Polls(),
new InstanceV2Entity\Accounts(),
);

View File

@ -131,7 +131,7 @@ class InstanceV2 extends BaseApi
return new InstanceEntity\Configuration(
$statuses_config,
new InstanceEntity\MediaAttachmentsConfig(array_keys(Images::supportedTypes()), $image_size_limit, $image_matrix_limit),
new InstanceEntity\MediaAttachmentsConfig(Images::supportedMimeTypes(), $image_size_limit, $image_matrix_limit),
new InstanceEntity\Polls(),
new InstanceEntity\Accounts(),
);

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']);
$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'];
}

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']);
$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'];
}

View File

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

View File

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

View File

@ -199,18 +199,18 @@ 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();
}
// 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']);
$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']);
$img = new Image($imgdata, $photo['type'], $photo['filename']);
$img->scaleDown($customsize);
$imgdata = $img->asString();
}

View File

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

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']]);
$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']);
}

View File

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

View File

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

View File

@ -45,25 +45,34 @@ 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);
} 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 +84,27 @@ class Image
$this->loadData($data);
}
/**
* Check if Imagick will be used
*
* @param string $data
* @return boolean
*/
private function useImagick(string $data): bool
{
if (!$this->isImagick()) {
return false;
}
if ($this->imageType == IMAGETYPE_GIF) {
$count = preg_match_all("#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s", $data);
return ($count > 0);
}
// @todo add check for WebP
return ($this->imageType == IMAGETYPE_WEBP);
}
/**
* Destructor
*
@ -118,28 +148,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 +180,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 +211,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 +285,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 +309,7 @@ class Image
return false;
}
return $this->types[$this->getType()];
return Images::getExtensionByImageType($this->imageType);
}
/**
@ -398,7 +439,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 +586,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 +611,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 +655,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 +709,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 +744,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 '';
}

View File

@ -304,10 +304,8 @@ class DFRN
$profilephotos = Photo::selectToArray(['resource-id', 'scale', 'type'], ['profile' => true, 'uid' => $uid], ['order' => ['scale']]);
$photos = [];
$ext = Images::supportedTypes();
foreach ($profilephotos as $p) {
$photos[$p['scale']] = DI::baseUrl() . '/photo/' . $p['resource-id'] . '-' . $p['scale'] . '.' . $ext[$p['type']];
$photos[$p['scale']] = DI::baseUrl() . '/photo/' . $p['resource-id'] . '-' . $p['scale'] . Images::getExtensionByMimeType($p['type']);
}
$doc = new DOMDocument('1.0', 'utf-8');

View File

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

View File

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

View File

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