New supported formats, Preserve ICC Color Profiles, libvips support

Update image pipeline to handle avif, heic and webp and preserve ICC color profiles and added libvips support.
pull/5978/head
Daniel Supernault 6 months ago
parent c5e1f5fe5b
commit ab9c13fe0d
No known key found for this signature in database
GPG Key ID: 23740873EE6F76A1

@ -48,6 +48,7 @@ class CatchUnoptimizedMedia extends Command
->whereNotNull('status_id')
->whereNotNull('media_path')
->whereIn('mime', [
'image/jpg',
'image/jpeg',
'image/png',
])

@ -97,6 +97,7 @@ class FixMediaDriver extends Command
if(
$media->mime &&
in_array($media->mime, [
'image/jpg',
'image/jpeg',
'image/png',
'image/webp'

@ -110,7 +110,7 @@ class ImportEmojis extends Command
private function isEmoji($filename)
{
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/jpg'];
$mimeType = mime_content_type($filename);
return in_array($mimeType, $allowedMimeTypes);

@ -40,7 +40,7 @@ class RegenerateThumbnails extends Command
public function handle()
{
DB::transaction(function() {
Media::whereIn('mime', ['image/jpeg', 'image/png'])
Media::whereIn('mime', ['image/jpeg', 'image/png', 'image/jpg'])
->chunk(50, function($medias) {
foreach($medias as $media) {
\App\Jobs\ImageOptimizePipeline\ImageThumbnail::dispatch($media);

@ -243,7 +243,7 @@ class ApiV1Controller extends Controller
}
$this->validate($request, [
'avatar' => 'sometimes|mimetypes:image/jpeg,image/png|max:'.config('pixelfed.max_avatar_size'),
'avatar' => 'sometimes|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
'display_name' => 'nullable|string|max:30',
'note' => 'nullable|string|max:200',
'locked' => 'nullable',
@ -1907,6 +1907,7 @@ class ApiV1Controller extends Controller
$media->save();
switch ($media->mime) {
case 'image/jpg':
case 'image/jpeg':
case 'image/png':
case 'image/webp':
@ -2137,6 +2138,7 @@ class ApiV1Controller extends Controller
$media->save();
switch ($media->mime) {
case 'image/jpg':
case 'image/jpeg':
case 'image/png':
case 'image/webp':

@ -1307,6 +1307,7 @@ class ApiV1Dot1Controller extends Controller
$media->save();
switch ($media->mime) {
case 'image/jpg':
case 'image/jpeg':
case 'image/png':
ImageOptimize::dispatch($media)->onQueue('mmo');

@ -310,6 +310,7 @@ class ApiV2Controller extends Controller
switch ($media->mime) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
ImageOptimize::dispatch($media)->onQueue('mmo');
break;

@ -201,7 +201,7 @@ class ImportPostController extends Controller
$this->checkPermissions($request);
$allowedMimeTypes = ['image/png', 'image/jpeg'];
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/jpg'];
if (config('import.instagram.allow_image_webp') && str_contains(config_cache('pixelfed.media_types'), 'image/webp')) {
$allowedMimeTypes[] = 'image/webp';

@ -260,7 +260,7 @@ class StoryApiV1Controller extends Controller
'file' => function () {
return [
'required',
'mimetypes:image/jpeg,image/png,video/mp4',
'mimetypes:image/jpeg,image/jpg,image/png,video/mp4',
'max:'.config_cache('pixelfed.max_photo_size'),
];
},

@ -34,7 +34,7 @@ class StoryComposeController extends Controller
'file' => function () {
return [
'required',
'mimetypes:image/jpeg,image/png,video/mp4',
'mimetypes:image/jpeg,image/png,video/mp4,image/jpg',
'max:'.config_cache('pixelfed.max_photo_size'),
];
},

@ -40,6 +40,9 @@ class ImageOptimize implements ShouldQueue
public function handle()
{
$media = $this->media;
if(!$media) {
return;
}
$path = storage_path('app/'.$media->media_path);
if (!is_file($path) || $media->skip_optimize) {
return;

@ -64,7 +64,7 @@ class Media extends Model
return $this->cdn_url;
}
if ($this->media_path && $this->mime && in_array($this->mime, ['image/jpeg', 'image/png'])) {
if ($this->media_path && $this->mime && in_array($this->mime, ['image/jpeg', 'image/png', 'image/jpg'])) {
return $this->remote_media || Str::startsWith($this->media_path, 'http') ?
$this->media_path :
url(Storage::url($this->media_path));

@ -138,6 +138,7 @@ class MediaStorageService
}
$mimes = [
'image/jpg',
'image/jpeg',
'image/png',
'video/mp4',
@ -166,6 +167,7 @@ class MediaStorageService
$ext = '.gif';
break;
case 'image/jpg':
case 'image/jpeg':
$ext = '.jpg';
break;
@ -219,6 +221,7 @@ class MediaStorageService
$mimes = [
'application/octet-stream',
'image/jpg',
'image/jpeg',
'image/png',
];
@ -249,7 +252,7 @@ class MediaStorageService
}
$base = ($local ? 'public/cache/' : 'cache/').'avatars/'.$avatar->profile_id;
$ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png';
$ext = ($head['mime'] == 'image/png') ? 'png' : 'jpg';
$path = 'avatar_'.strtolower(Str::random(random_int(3, 6))).'.'.$ext;
$tmpBase = storage_path('app/remcache/');
$tmpPath = 'avatar_'.$avatar->profile_id.'-'.$path;
@ -262,7 +265,7 @@ class MediaStorageService
$mimeCheck = Storage::mimeType('remcache/'.$tmpPath);
if (! $mimeCheck || ! in_array($mimeCheck, ['image/png', 'image/jpeg'])) {
if (! $mimeCheck || ! in_array($mimeCheck, ['image/png', 'image/jpeg', 'image/jpg'])) {
$avatar->last_fetched_at = now();
$avatar->save();
unlink($tmpName);

@ -112,7 +112,7 @@ class Status extends Model
}
return collect($entity['media_attachments'])
->filter(fn($media) => $media['type'] == 'image' && in_array($media['mime'], ['image/jpeg', 'image/png']))
->filter(fn($media) => $media['type'] == 'image' && in_array($media['mime'], ['image/jpeg', 'image/png', 'image/jpg']))
->map(function($media) {
if(!Str::endsWith($media['preview_url'], ['no-preview.png', 'no-preview.jpg'])) {
return $media['preview_url'];

@ -11,7 +11,7 @@ class Blurhash {
public static function generate(Media $media)
{
if(!in_array($media->mime, ['image/png', 'image/jpeg', 'video/mp4'])) {
if(!in_array($media->mime, ['image/png', 'image/jpeg', 'image/jpg', 'video/mp4'])) {
return self::DEFAULT_HASH;
}
@ -44,7 +44,6 @@ class Blurhash {
$pixels[] = $row;
}
// Free the allocated GdImage object from memory:
imagedestroy($image);
$components_x = 4;

@ -3,8 +3,13 @@
namespace App\Util\Media;
use App\Media;
use Image as Intervention;
use Intervention\Image\ImageManager;
use Intervention\Image\Encoders\JpegEncoder;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Encoders\AvifEncoder;
use Intervention\Image\Encoders\PngEncoder;
use Cache, Log, Storage;
use App\Util\Media\Blurhash;
class Image
{
@ -16,10 +21,14 @@ class Image
public $acceptedMimes = [
'image/png',
'image/jpeg',
'image/jpg',
'image/webp',
'image/avif',
'image/heic',
];
protected $imageManager;
public function __construct()
{
ini_set('memory_limit', config('pixelfed.memory_limit', '1024M'));
@ -32,6 +41,20 @@ class Image
'height' => 640,
];
$this->orientation = null;
$driver = match(config('image.driver')) {
'imagick' => new \Intervention\Image\Drivers\Imagick\Driver(),
'vips' => new \Intervention\Image\Drivers\Vips\Driver(),
default => new \Intervention\Image\Drivers\Gd\Driver()
};
$this->imageManager = new ImageManager(
$driver,
autoOrientation: true,
decodeAnimation: true,
blendingColor: 'ffffff',
strip: true
);
}
public function orientations()
@ -54,9 +77,6 @@ class Image
public function getAspectRatio($mediaPath, $thumbnail = false)
{
if (!is_file($mediaPath)) {
throw new \Exception('Invalid Media Path');
}
if ($thumbnail) {
return [
'dimensions' => $this->thumbnail,
@ -64,6 +84,10 @@ class Image
];
}
if (!is_file($mediaPath)) {
throw new \Exception('Invalid Media Path');
}
list($width, $height) = getimagesize($mediaPath);
$aspect = $width / $height;
$orientation = $aspect === 1 ? 'square' :
@ -80,15 +104,11 @@ class Image
public function resizeImage(Media $media)
{
$basePath = storage_path('app/'.$media->media_path);
$this->handleResizeImage($media);
}
public function resizeThumbnail(Media $media)
{
$basePath = storage_path('app/'.$media->media_path);
$this->handleThumbnailImage($media);
}
@ -114,18 +134,16 @@ class Image
$orientation = $ratio['orientation'];
try {
$img = Intervention::make($file);
$metadata = $img->exif();
$img->orientate();
if($thumbnail) {
$img->resize($aspect['width'], $aspect['height'], function ($constraint) {
$constraint->aspectRatio();
});
} else {
if(config('media.exif.database', false) == true && $metadata) {
$fileInfo = pathinfo($file);
$extension = strtolower($fileInfo['extension'] ?? 'jpg');
$metadata = null;
if (!$thumbnail && config('media.exif.database', false) == true) {
try {
$exif = @exif_read_data($file);
if ($exif) {
$meta = [];
$keys = [
"COMPUTED",
"FileName",
"FileSize",
"FileType",
@ -152,28 +170,69 @@ class Image
"ISOSpeedRatings",
"ShutterSpeedValue"
];
foreach ($metadata as $k => $v) {
foreach ($exif as $k => $v) {
if (in_array($k, $keys)) {
$meta[$k] = $v;
}
}
$media->metadata = json_encode($meta);
}
} catch (\Exception $e) {
Log::info('EXIF extraction failed: ' . $e->getMessage());
}
}
$img = $this->imageManager->read($file);
if ($thumbnail) {
$img = $img->coverDown(
$aspect['width'],
$aspect['height']
);
} else {
if (
($ratio['width_original'] > $aspect['width'])
|| ($ratio['height_original'] > $aspect['height'])
) {
$img->resize($aspect['width'], $aspect['height'], function ($constraint) {
$constraint->aspectRatio();
});
$img = $img->scaleDown(
$aspect['width'],
$aspect['height']
);
}
}
$converted = $this->setBaseName($path, $thumbnail, $img->extension);
$converted = $this->setBaseName($path, $thumbnail, $extension);
$newPath = storage_path('app/'.$converted['path']);
$quality = config_cache('pixelfed.image_quality');
$img->save($newPath, $quality);
$encoder = null;
switch ($extension) {
case 'jpeg':
case 'jpg':
$encoder = new JpegEncoder($quality);
break;
case 'png':
$encoder = new PngEncoder();
break;
case 'webp':
$encoder = new WebpEncoder($quality);
break;
case 'avif':
$encoder = new AvifEncoder($quality);
break;
case 'heic':
$encoder = new JpegEncoder($quality);
$extension = 'jpg';
break;
default:
$encoder = new JpegEncoder($quality);
$extension = 'jpg';
}
$encoded = $encoder->encode($img);
file_put_contents($newPath, $encoded->toString());
if ($thumbnail == true) {
$media->thumbnail_path = $converted['path'];
@ -183,10 +242,9 @@ class Image
$media->height = $img->height();
$media->orientation = $orientation;
$media->media_path = $converted['path'];
$media->mime = $img->mime;
$media->mime = 'image/' . $extension;
}
$img->destroy();
$media->save();
if ($thumbnail) {
@ -196,10 +254,10 @@ class Image
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
Cache::forget('status:thumb:'.$media->status_id);
} catch (Exception $e) {
} catch (\Exception $e) {
$media->processed_at = now();
$media->save();
Log::info('MediaResizeException: Could not process media id: ' . $media->id);
Log::info('MediaResizeException: ' . $e->getMessage() . ' | Could not process media id: ' . $media->id);
}
}

@ -18,7 +18,7 @@
"buzz/laravel-h-captcha": "^1.0.4",
"doctrine/dbal": "^3.0",
"endroid/qr-code": "^6.0",
"intervention/image": "^2.4",
"intervention/image": "^3.11.2",
"jenssegers/agent": "^2.6",
"laravel-notification-channels/expo": "^2.0.0",
"laravel-notification-channels/webpush": "^10.2",

876
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -7,14 +7,12 @@ return [
| Image Driver
|--------------------------------------------------------------------------
|
| Intervention Image supports "GD Library" and "Imagick" to process images
| internally. You may choose one of them according to your PHP
| Intervention Image supports "GD Library", "Imagick" and "libvips" to process
| images internally. You may choose one of them according to your PHP
| configuration. By default PHP's "GD Library" implementation is used.
|
| Supported: "gd", "imagick"
| Supported: "gd", "imagick", "libvips"
|
*/
'driver' => env('IMAGE_DRIVER', 'gd'),
];

Loading…
Cancel
Save