浏览代码

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.
Daniel Supernault 1 月之前
父节点
当前提交
ab9c13fe0d

+ 1 - 0
app/Console/Commands/CatchUnoptimizedMedia.php

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

+ 120 - 119
app/Console/Commands/FixMediaDriver.php

@@ -11,123 +11,124 @@ use App\Jobs\MediaPipeline\MediaFixLocalFilesystemCleanupPipeline;
 
 class FixMediaDriver extends Command
 {
-	/**
-	 * The name and signature of the console command.
-	 *
-	 * @var string
-	 */
-	protected $signature = 'media:fix-nonlocal-driver';
-
-	/**
-	 * The console command description.
-	 *
-	 * @var string
-	 */
-	protected $description = 'Fix filesystem when FILESYSTEM_DRIVER not set to local';
-
-	/**
-	 * Execute the console command.
-	 *
-	 * @return int
-	 */
-	public function handle()
-	{
-		if(config('filesystems.default') !== 'local') {
-			$this->error('Invalid default filesystem, set FILESYSTEM_DRIVER=local to proceed');
-			return Command::SUCCESS;
-		}
-
-		if((bool) config_cache('pixelfed.cloud_storage') == false) {
-			$this->error('Cloud storage not enabled, exiting...');
-			return Command::SUCCESS;
-		}
-
-		$this->info('       ____  _           ______         __  ');
-		$this->info('      / __ \(_)  _____  / / __/__  ____/ /  ');
-		$this->info('     / /_/ / / |/_/ _ \/ / /_/ _ \/ __  /   ');
-		$this->info('    / ____/ />  </  __/ / __/  __/ /_/ /    ');
-		$this->info('   /_/   /_/_/|_|\___/_/_/  \___/\__,_/     ');
-		$this->info(' ');
-		$this->info('   Media Filesystem Fix');
-		$this->info('   =====================');
-		$this->info('   Fix media that was created when FILESYSTEM_DRIVER=local');
-		$this->info('   was not properly set. This command will fix media urls');
-		$this->info('   and optionally optimize/generate thumbnails when applicable,');
-		$this->info('   clean up temporary local media files and clear the app cache');
-		$this->info('   to fix media paths/urls.');
-		$this->info(' ');
-		$this->error('   Remember, FILESYSTEM_DRIVER=local must remain set or you will break things!');
-
-		if(!$this->confirm('Are you sure you want to perform this command?')) {
-			$this->info('Exiting...');
-			return Command::SUCCESS;
-		}
-
-		$optimize = $this->choice(
-			'Do you want to optimize media and generate thumbnails? This will store s3 locally and re-upload optimized versions.',
-			['no', 'yes'],
-			1
-		);
-
-		$cloud = Storage::disk(config('filesystems.cloud'));
-		$mountManager = new MountManager([
-			's3' => $cloud->getDriver(),
-			'local' => Storage::disk('local')->getDriver(),
-		]);
-
-		$this->info('Fixing media, this may take a while...');
-		$this->line(' ');
-		$bar = $this->output->createProgressBar(Media::whereNotNull('status_id')->whereNull('cdn_url')->count());
-		$bar->start();
-
-		foreach(Media::whereNotNull('status_id')->whereNull('cdn_url')->lazyById(20) as $media) {
-			if($cloud->exists($media->media_path)) {
-				if($optimize === 'yes') {
-					$mountManager->copy(
-						's3://' . $media->media_path,
-						'local://' . $media->media_path
-					);
-					sleep(1);
-					if(empty($media->original_sha256)) {
-						$hash = \hash_file('sha256', Storage::disk('local')->path($media->media_path));
-						$media->original_sha256 = $hash;
-						$media->save();
-						sleep(1);
-					}
-					if(
-						$media->mime &&
-						in_array($media->mime, [
-							'image/jpeg',
-							'image/png',
-							'image/webp'
-						])
-					) {
-						ImageOptimize::dispatch($media);
-						sleep(3);
-					}
-				} else {
-					$media->cdn_url = $cloud->url($media->media_path);
-					$media->save();
-				}
-			}
-			$bar->advance();
-		}
-
-		$bar->finish();
-		$this->line(' ');
-		$this->line(' ');
-
-		$this->callSilently('cache:clear');
-
-		$this->info('Successfully fixed media paths and cleared cached!');
-
-		if($optimize === 'yes') {
-			MediaFixLocalFilesystemCleanupPipeline::dispatch()->delay(now()->addMinutes(15))->onQueue('default');
-			$this->line(' ');
-			$this->info('A cleanup job has been dispatched to delete media stored locally, it may take a few minutes to process!');
-		}
-
-		$this->line(' ');
-		return Command::SUCCESS;
-	}
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'media:fix-nonlocal-driver';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Fix filesystem when FILESYSTEM_DRIVER not set to local';
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        if(config('filesystems.default') !== 'local') {
+            $this->error('Invalid default filesystem, set FILESYSTEM_DRIVER=local to proceed');
+            return Command::SUCCESS;
+        }
+
+        if((bool) config_cache('pixelfed.cloud_storage') == false) {
+            $this->error('Cloud storage not enabled, exiting...');
+            return Command::SUCCESS;
+        }
+
+        $this->info('       ____  _           ______         __  ');
+        $this->info('      / __ \(_)  _____  / / __/__  ____/ /  ');
+        $this->info('     / /_/ / / |/_/ _ \/ / /_/ _ \/ __  /   ');
+        $this->info('    / ____/ />  </  __/ / __/  __/ /_/ /    ');
+        $this->info('   /_/   /_/_/|_|\___/_/_/  \___/\__,_/     ');
+        $this->info(' ');
+        $this->info('   Media Filesystem Fix');
+        $this->info('   =====================');
+        $this->info('   Fix media that was created when FILESYSTEM_DRIVER=local');
+        $this->info('   was not properly set. This command will fix media urls');
+        $this->info('   and optionally optimize/generate thumbnails when applicable,');
+        $this->info('   clean up temporary local media files and clear the app cache');
+        $this->info('   to fix media paths/urls.');
+        $this->info(' ');
+        $this->error('   Remember, FILESYSTEM_DRIVER=local must remain set or you will break things!');
+
+        if(!$this->confirm('Are you sure you want to perform this command?')) {
+            $this->info('Exiting...');
+            return Command::SUCCESS;
+        }
+
+        $optimize = $this->choice(
+            'Do you want to optimize media and generate thumbnails? This will store s3 locally and re-upload optimized versions.',
+            ['no', 'yes'],
+            1
+        );
+
+        $cloud = Storage::disk(config('filesystems.cloud'));
+        $mountManager = new MountManager([
+            's3' => $cloud->getDriver(),
+            'local' => Storage::disk('local')->getDriver(),
+        ]);
+
+        $this->info('Fixing media, this may take a while...');
+        $this->line(' ');
+        $bar = $this->output->createProgressBar(Media::whereNotNull('status_id')->whereNull('cdn_url')->count());
+        $bar->start();
+
+        foreach(Media::whereNotNull('status_id')->whereNull('cdn_url')->lazyById(20) as $media) {
+            if($cloud->exists($media->media_path)) {
+                if($optimize === 'yes') {
+                    $mountManager->copy(
+                        's3://' . $media->media_path,
+                        'local://' . $media->media_path
+                    );
+                    sleep(1);
+                    if(empty($media->original_sha256)) {
+                        $hash = \hash_file('sha256', Storage::disk('local')->path($media->media_path));
+                        $media->original_sha256 = $hash;
+                        $media->save();
+                        sleep(1);
+                    }
+                    if(
+                        $media->mime &&
+                        in_array($media->mime, [
+                            'image/jpg',
+                            'image/jpeg',
+                            'image/png',
+                            'image/webp'
+                        ])
+                    ) {
+                        ImageOptimize::dispatch($media);
+                        sleep(3);
+                    }
+                } else {
+                    $media->cdn_url = $cloud->url($media->media_path);
+                    $media->save();
+                }
+            }
+            $bar->advance();
+        }
+
+        $bar->finish();
+        $this->line(' ');
+        $this->line(' ');
+
+        $this->callSilently('cache:clear');
+
+        $this->info('Successfully fixed media paths and cleared cached!');
+
+        if($optimize === 'yes') {
+            MediaFixLocalFilesystemCleanupPipeline::dispatch()->delay(now()->addMinutes(15))->onQueue('default');
+            $this->line(' ');
+            $this->info('A cleanup job has been dispatched to delete media stored locally, it may take a few minutes to process!');
+        }
+
+        $this->line(' ');
+        return Command::SUCCESS;
+    }
 }

+ 1 - 1
app/Console/Commands/ImportEmojis.php

@@ -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);

+ 1 - 1
app/Console/Commands/RegenerateThumbnails.php

@@ -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);

+ 3 - 1
app/Http/Controllers/Api/ApiV1Controller.php

@@ -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':

+ 1 - 0
app/Http/Controllers/Api/ApiV1Dot1Controller.php

@@ -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');

+ 1 - 0
app/Http/Controllers/Api/ApiV2Controller.php

@@ -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;

+ 1 - 1
app/Http/Controllers/ImportPostController.php

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

+ 1 - 1
app/Http/Controllers/Stories/StoryApiV1Controller.php

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

+ 1 - 1
app/Http/Controllers/StoryComposeController.php

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

+ 3 - 0
app/Jobs/ImageOptimizePipeline/ImageOptimize.php

@@ -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;

+ 1 - 1
app/Media.php

@@ -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));

+ 5 - 2
app/Services/MediaStorageService.php

@@ -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);

+ 348 - 348
app/Status.php

@@ -15,104 +15,104 @@ use Illuminate\Support\Str;
 
 class Status extends Model
 {
-	use HasSnowflakePrimary, SoftDeletes;
-
-	/**
-	 * Indicates if the IDs are auto-incrementing.
-	 *
-	 * @var bool
-	 */
-	public $incrementing = false;
-
-	/**
-	 * The attributes that should be mutated to dates.
-	 *
-	 * @var array
-	 */
-	protected $casts = [
-		'deleted_at' => 'datetime',
-		'edited_at'  => 'datetime'
-	];
-
-	protected $guarded = [];
-
-	const STATUS_TYPES = [
-		'text',
-		'photo',
-		'photo:album',
-		'video',
-		'video:album',
-		'photo:video:album',
-		'share',
-		'reply',
-		'story',
-		'story:reply',
-		'story:reaction',
-		'story:live',
-		'loop'
-	];
-
-	const MAX_MENTIONS = 20;
-
-	const MAX_HASHTAGS = 60;
-
-	const MAX_LINKS = 5;
-
-	public function profile()
-	{
-		return $this->belongsTo(Profile::class);
-	}
-
-	public function media()
-	{
-		return $this->hasMany(Media::class);
-	}
-
-	public function firstMedia()
-	{
-		return $this->hasMany(Media::class)->orderBy('order', 'asc')->first();
-	}
-
-	public function viewType()
-	{
-		if($this->type) {
-			return $this->type;
-		}
-		return $this->setType();
-	}
-
-	public function setType()
-	{
-		if(in_array($this->type, self::STATUS_TYPES)) {
-			return $this->type;
-		}
-		$mimes = $this->media->pluck('mime')->toArray();
-		$type = StatusController::mimeTypeCheck($mimes);
-		if($type) {
-			$this->type = $type;
-			$this->save();
-			return $type;
-		}
-	}
-
-	public function thumb($showNsfw = false)
-	{
-		$entity = StatusService::get($this->id, false);
-
-		if(!$entity || !isset($entity['media_attachments']) || empty($entity['media_attachments'])) {
-			return url(Storage::url('public/no-preview.png'));
-		}
-
-		if((!isset($entity['sensitive']) || $entity['sensitive']) && !$showNsfw) {
-			return url(Storage::url('public/no-preview.png'));
-		}
+    use HasSnowflakePrimary, SoftDeletes;
+
+    /**
+     * Indicates if the IDs are auto-incrementing.
+     *
+     * @var bool
+     */
+    public $incrementing = false;
+
+    /**
+     * The attributes that should be mutated to dates.
+     *
+     * @var array
+     */
+    protected $casts = [
+        'deleted_at' => 'datetime',
+        'edited_at'  => 'datetime'
+    ];
+
+    protected $guarded = [];
+
+    const STATUS_TYPES = [
+        'text',
+        'photo',
+        'photo:album',
+        'video',
+        'video:album',
+        'photo:video:album',
+        'share',
+        'reply',
+        'story',
+        'story:reply',
+        'story:reaction',
+        'story:live',
+        'loop'
+    ];
+
+    const MAX_MENTIONS = 20;
+
+    const MAX_HASHTAGS = 60;
+
+    const MAX_LINKS = 5;
+
+    public function profile()
+    {
+        return $this->belongsTo(Profile::class);
+    }
+
+    public function media()
+    {
+        return $this->hasMany(Media::class);
+    }
+
+    public function firstMedia()
+    {
+        return $this->hasMany(Media::class)->orderBy('order', 'asc')->first();
+    }
+
+    public function viewType()
+    {
+        if($this->type) {
+            return $this->type;
+        }
+        return $this->setType();
+    }
+
+    public function setType()
+    {
+        if(in_array($this->type, self::STATUS_TYPES)) {
+            return $this->type;
+        }
+        $mimes = $this->media->pluck('mime')->toArray();
+        $type = StatusController::mimeTypeCheck($mimes);
+        if($type) {
+            $this->type = $type;
+            $this->save();
+            return $type;
+        }
+    }
+
+    public function thumb($showNsfw = false)
+    {
+        $entity = StatusService::get($this->id, false);
+
+        if(!$entity || !isset($entity['media_attachments']) || empty($entity['media_attachments'])) {
+            return url(Storage::url('public/no-preview.png'));
+        }
+
+        if((!isset($entity['sensitive']) || $entity['sensitive']) && !$showNsfw) {
+            return url(Storage::url('public/no-preview.png'));
+        }
 
         if(!isset($entity['visibility']) || !in_array($entity['visibility'], ['public', 'unlisted'])) {
             return url(Storage::url('public/no-preview.png'));
         }
 
-		return collect($entity['media_attachments'])
-            ->filter(fn($media) => $media['type'] == 'image' && in_array($media['mime'], ['image/jpeg', 'image/png']))
+        return collect($entity['media_attachments'])
+            ->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'];
@@ -121,259 +121,259 @@ class Status extends Model
                 return $media['url'];
             })
             ->first() ?? url(Storage::url('public/no-preview.png'));
-	}
-
-	public function url($forceLocal = false)
-	{
-		if($this->uri) {
-			return $forceLocal ? "/i/web/post/_/{$this->profile_id}/{$this->id}" : $this->uri;
-		} else {
-			$id = $this->id;
-			$account = AccountService::get($this->profile_id, true);
-			if(!$account || !isset($account['username'])) {
-				return '/404';
-			}
-			$path = url(config('app.url')."/p/{$account['username']}/{$id}");
-			return $path;
-		}
-	}
-
-	public function permalink($suffix = '/activity')
-	{
-		$id = $this->id;
-		$username = $this->profile->username;
-		$path = config('app.url')."/p/{$username}/{$id}{$suffix}";
-
-		return url($path);
-	}
-
-	public function editUrl()
-	{
-		return $this->url().'/edit';
-	}
-
-	public function mediaUrl()
-	{
-		$media = $this->firstMedia();
-		$path = $media->media_path;
-		$hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
-		$url = $media->cdn_url ? $media->cdn_url . "?v={$hash}" : url(Storage::url($path)."?v={$hash}");
-
-		return $url;
-	}
-
-	public function likes()
-	{
-		return $this->hasMany(Like::class);
-	}
-
-	public function liked() : bool
-	{
-		if(!Auth::check()) {
-			return false;
-		}
-
-		$pid = Auth::user()->profile_id;
-
-		return Like::select('status_id', 'profile_id')
-			->whereStatusId($this->id)
-			->whereProfileId($pid)
-			->exists();
-	}
-
-	public function likedBy()
-	{
-		return $this->hasManyThrough(
-			Profile::class,
-			Like::class,
-			'status_id',
-			'id',
-			'id',
-			'profile_id'
-		);
-	}
-
-	public function comments()
-	{
-		return $this->hasMany(self::class, 'in_reply_to_id');
-	}
-
-	public function bookmarked()
-	{
-		if (!Auth::check()) {
-			return false;
-		}
-		$profile = Auth::user()->profile;
-
-		return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
-	}
-
-	public function shares()
-	{
-		return $this->hasMany(self::class, 'reblog_of_id');
-	}
-
-	public function shared() : bool
-	{
-		if(!Auth::check()) {
-			return false;
-		}
-		$pid = Auth::user()->profile_id;
-
-		return $this->select('profile_id', 'reblog_of_id')
-			->whereProfileId($pid)
-			->whereReblogOfId($this->id)
-			->exists();
-	}
-
-	public function sharedBy()
-	{
-		return $this->hasManyThrough(
-			Profile::class,
-			Status::class,
-			'reblog_of_id',
-			'id',
-			'id',
-			'profile_id'
-		);
-	}
-
-	public function parent()
-	{
-		$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
-		if (!empty($parent)) {
-			return $this->findOrFail($parent);
-		} else {
-			return false;
-		}
-	}
-
-	public function conversation()
-	{
-		return $this->hasOne(Conversation::class);
-	}
-
-	public function hashtags()
-	{
-		return $this->hasManyThrough(
-		Hashtag::class,
-		StatusHashtag::class,
-		'status_id',
-		'id',
-		'id',
-		'hashtag_id'
-	  );
-	}
-
-	public function mentions()
-	{
-		return $this->hasManyThrough(
-		Profile::class,
-		Mention::class,
-		'status_id',
-		'id',
-		'id',
-		'profile_id'
-	  );
-	}
-
-	public function reportUrl()
-	{
-		return route('report.form')."?type=post&id={$this->id}";
-	}
-
-	public function toActivityStream()
-	{
-		$media = $this->media;
-		$mediaCollection = [];
-		foreach ($media as $image) {
-			$mediaCollection[] = [
-		  'type'      => 'Link',
-		  'href'      => $image->url(),
-		  'mediaType' => $image->mime,
-		];
-		}
-		$obj = [
-		'@context' => 'https://www.w3.org/ns/activitystreams',
-		'type'     => 'Image',
-		'name'     => null,
-		'url'      => $mediaCollection,
-	  ];
-
-		return $obj;
-	}
-
-	public function recentComments()
-	{
-		return $this->comments()->orderBy('created_at', 'desc')->take(3);
-	}
-
-	public function scopeToAudience($audience)
-	{
-		if(!in_array($audience, ['to', 'cc']) || $this->local == false) { 
-			return;
-		}
-		$res = [];
-		$res['to'] = [];
-		$res['cc'] = [];
-		$scope = $this->scope;
-		$mentions = $this->mentions->map(function ($mention) {
-			return $mention->permalink();
-		})->toArray();
-
-		if($this->in_reply_to_id != null) {
-			$parent = $this->parent();
-			if($parent) {
-				$mentions = array_merge([$parent->profile->permalink()], $mentions);
-			}
-		}
-
-		switch ($scope) {
-			case 'public':
-				$res['to'] = [
-					"https://www.w3.org/ns/activitystreams#Public"
-				];
-				$res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions);
-				break;
-
-			case 'unlisted':
-				$res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
-				$res['cc'] = [
-					"https://www.w3.org/ns/activitystreams#Public"
-				];
-				break;
-
-			case 'private':
-				$res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
-				$res['cc'] = [];
-				break;
-
-			// TODO: Update scope when DMs are supported
-			case 'direct':
-				$res['to'] = [];
-				$res['cc'] = [];
-				break;
-		}
-		return $res[$audience];
-	}
-
-	public function place()
-	{
-		return $this->belongsTo(Place::class);
-	}
-
-	public function directMessage()
-	{
-		return $this->hasOne(DirectMessage::class);
-	}
-
-	public function poll()
-	{
-		return $this->hasOne(Poll::class);
-	}
-
-	public function edits()
-	{
-		return $this->hasMany(StatusEdit::class);
-	}
+    }
+
+    public function url($forceLocal = false)
+    {
+        if($this->uri) {
+            return $forceLocal ? "/i/web/post/_/{$this->profile_id}/{$this->id}" : $this->uri;
+        } else {
+            $id = $this->id;
+            $account = AccountService::get($this->profile_id, true);
+            if(!$account || !isset($account['username'])) {
+                return '/404';
+            }
+            $path = url(config('app.url')."/p/{$account['username']}/{$id}");
+            return $path;
+        }
+    }
+
+    public function permalink($suffix = '/activity')
+    {
+        $id = $this->id;
+        $username = $this->profile->username;
+        $path = config('app.url')."/p/{$username}/{$id}{$suffix}";
+
+        return url($path);
+    }
+
+    public function editUrl()
+    {
+        return $this->url().'/edit';
+    }
+
+    public function mediaUrl()
+    {
+        $media = $this->firstMedia();
+        $path = $media->media_path;
+        $hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
+        $url = $media->cdn_url ? $media->cdn_url . "?v={$hash}" : url(Storage::url($path)."?v={$hash}");
+
+        return $url;
+    }
+
+    public function likes()
+    {
+        return $this->hasMany(Like::class);
+    }
+
+    public function liked() : bool
+    {
+        if(!Auth::check()) {
+            return false;
+        }
+
+        $pid = Auth::user()->profile_id;
+
+        return Like::select('status_id', 'profile_id')
+            ->whereStatusId($this->id)
+            ->whereProfileId($pid)
+            ->exists();
+    }
+
+    public function likedBy()
+    {
+        return $this->hasManyThrough(
+            Profile::class,
+            Like::class,
+            'status_id',
+            'id',
+            'id',
+            'profile_id'
+        );
+    }
+
+    public function comments()
+    {
+        return $this->hasMany(self::class, 'in_reply_to_id');
+    }
+
+    public function bookmarked()
+    {
+        if (!Auth::check()) {
+            return false;
+        }
+        $profile = Auth::user()->profile;
+
+        return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
+    }
+
+    public function shares()
+    {
+        return $this->hasMany(self::class, 'reblog_of_id');
+    }
+
+    public function shared() : bool
+    {
+        if(!Auth::check()) {
+            return false;
+        }
+        $pid = Auth::user()->profile_id;
+
+        return $this->select('profile_id', 'reblog_of_id')
+            ->whereProfileId($pid)
+            ->whereReblogOfId($this->id)
+            ->exists();
+    }
+
+    public function sharedBy()
+    {
+        return $this->hasManyThrough(
+            Profile::class,
+            Status::class,
+            'reblog_of_id',
+            'id',
+            'id',
+            'profile_id'
+        );
+    }
+
+    public function parent()
+    {
+        $parent = $this->in_reply_to_id ?? $this->reblog_of_id;
+        if (!empty($parent)) {
+            return $this->findOrFail($parent);
+        } else {
+            return false;
+        }
+    }
+
+    public function conversation()
+    {
+        return $this->hasOne(Conversation::class);
+    }
+
+    public function hashtags()
+    {
+        return $this->hasManyThrough(
+        Hashtag::class,
+        StatusHashtag::class,
+        'status_id',
+        'id',
+        'id',
+        'hashtag_id'
+      );
+    }
+
+    public function mentions()
+    {
+        return $this->hasManyThrough(
+        Profile::class,
+        Mention::class,
+        'status_id',
+        'id',
+        'id',
+        'profile_id'
+      );
+    }
+
+    public function reportUrl()
+    {
+        return route('report.form')."?type=post&id={$this->id}";
+    }
+
+    public function toActivityStream()
+    {
+        $media = $this->media;
+        $mediaCollection = [];
+        foreach ($media as $image) {
+            $mediaCollection[] = [
+          'type'      => 'Link',
+          'href'      => $image->url(),
+          'mediaType' => $image->mime,
+        ];
+        }
+        $obj = [
+        '@context' => 'https://www.w3.org/ns/activitystreams',
+        'type'     => 'Image',
+        'name'     => null,
+        'url'      => $mediaCollection,
+      ];
+
+        return $obj;
+    }
+
+    public function recentComments()
+    {
+        return $this->comments()->orderBy('created_at', 'desc')->take(3);
+    }
+
+    public function scopeToAudience($audience)
+    {
+        if(!in_array($audience, ['to', 'cc']) || $this->local == false) { 
+            return;
+        }
+        $res = [];
+        $res['to'] = [];
+        $res['cc'] = [];
+        $scope = $this->scope;
+        $mentions = $this->mentions->map(function ($mention) {
+            return $mention->permalink();
+        })->toArray();
+
+        if($this->in_reply_to_id != null) {
+            $parent = $this->parent();
+            if($parent) {
+                $mentions = array_merge([$parent->profile->permalink()], $mentions);
+            }
+        }
+
+        switch ($scope) {
+            case 'public':
+                $res['to'] = [
+                    "https://www.w3.org/ns/activitystreams#Public"
+                ];
+                $res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions);
+                break;
+
+            case 'unlisted':
+                $res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
+                $res['cc'] = [
+                    "https://www.w3.org/ns/activitystreams#Public"
+                ];
+                break;
+
+            case 'private':
+                $res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
+                $res['cc'] = [];
+                break;
+
+            // TODO: Update scope when DMs are supported
+            case 'direct':
+                $res['to'] = [];
+                $res['cc'] = [];
+                break;
+        }
+        return $res[$audience];
+    }
+
+    public function place()
+    {
+        return $this->belongsTo(Place::class);
+    }
+
+    public function directMessage()
+    {
+        return $this->hasOne(DirectMessage::class);
+    }
+
+    public function poll()
+    {
+        return $this->hasOne(Poll::class);
+    }
+
+    public function edits()
+    {
+        return $this->hasMany(StatusEdit::class);
+    }
 }

+ 47 - 48
app/Util/Media/Blurhash.php

@@ -7,53 +7,52 @@ use App\Media;
 
 class Blurhash {
 
-	const DEFAULT_HASH = 'U4Rfzst8?bt7ogayj[j[~pfQ9Goe%Mj[WBay';
-
-	public static function generate(Media $media)
-	{
-		if(!in_array($media->mime, ['image/png', 'image/jpeg', 'video/mp4'])) {
-			return self::DEFAULT_HASH;
-		}
-
-		if($media->thumbnail_path == null) {
-			return self::DEFAULT_HASH;
-		}
-
-		$file  = storage_path('app/' . $media->thumbnail_path);
-
-		if(!is_file($file)) {
-			return self::DEFAULT_HASH;
-		}
-
-		$image = imagecreatefromstring(file_get_contents($file));
-		if(!$image) {
-			return self::DEFAULT_HASH;
-		}
-		$width = imagesx($image);
-		$height = imagesy($image);
-
-		$pixels = [];
-		for ($y = 0; $y < $height; ++$y) {
-			$row = [];
-			for ($x = 0; $x < $width; ++$x) {
-				$index = imagecolorat($image, $x, $y);
-				$colors = imagecolorsforindex($image, $index);
-
-				$row[] = [$colors['red'], $colors['green'], $colors['blue']];
-			}
-			$pixels[] = $row;
-		}
-
-		// Free the allocated GdImage object from memory:
-		imagedestroy($image);
-
-		$components_x = 4;
-		$components_y = 4;
-		$blurhash = BlurhashEngine::encode($pixels, $components_x, $components_y);
-		if(strlen($blurhash) > 191) {
-			return self::DEFAULT_HASH;
-		}
-		return $blurhash;
-	}
+    const DEFAULT_HASH = 'U4Rfzst8?bt7ogayj[j[~pfQ9Goe%Mj[WBay';
+
+    public static function generate(Media $media)
+    {
+        if(!in_array($media->mime, ['image/png', 'image/jpeg', 'image/jpg', 'video/mp4'])) {
+            return self::DEFAULT_HASH;
+        }
+
+        if($media->thumbnail_path == null) {
+            return self::DEFAULT_HASH;
+        }
+
+        $file  = storage_path('app/' . $media->thumbnail_path);
+
+        if(!is_file($file)) {
+            return self::DEFAULT_HASH;
+        }
+
+        $image = imagecreatefromstring(file_get_contents($file));
+        if(!$image) {
+            return self::DEFAULT_HASH;
+        }
+        $width = imagesx($image);
+        $height = imagesy($image);
+
+        $pixels = [];
+        for ($y = 0; $y < $height; ++$y) {
+            $row = [];
+            for ($x = 0; $x < $width; ++$x) {
+                $index = imagecolorat($image, $x, $y);
+                $colors = imagecolorsforindex($image, $index);
+
+                $row[] = [$colors['red'], $colors['green'], $colors['blue']];
+            }
+            $pixels[] = $row;
+        }
+
+        imagedestroy($image);
+
+        $components_x = 4;
+        $components_y = 4;
+        $blurhash = BlurhashEngine::encode($pixels, $components_x, $components_y);
+        if(strlen($blurhash) > 191) {
+            return self::DEFAULT_HASH;
+        }
+        return $blurhash;
+    }
 
 }

+ 273 - 215
app/Util/Media/Image.php

@@ -3,223 +3,281 @@
 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
 {
-	public $square;
-	public $landscape;
-	public $portrait;
-	public $thumbnail;
-	public $orientation;
-	public $acceptedMimes = [
-		'image/png',
-		'image/jpeg',
-		'image/webp',
-		'image/avif',
-	];
-
-	public function __construct()
-	{
-		ini_set('memory_limit', config('pixelfed.memory_limit', '1024M'));
-
-		$this->square = $this->orientations()['square'];
-		$this->landscape = $this->orientations()['landscape'];
-		$this->portrait = $this->orientations()['portrait'];
-		$this->thumbnail = [
-			'width'  => 640,
-			'height' => 640,
-		];
-		$this->orientation = null;
-	}
-
-	public function orientations()
-	{
-		return [
-			'square' => [
-				'width'  => 1080,
-				'height' => 1080,
-			],
-			'landscape' => [
-				'width'  => 1920,
-				'height' => 1080,
-			],
-			'portrait' => [
-				'width'  => 1080,
-				'height' => 1350,
-			],
-		];
-	}
-
-	public function getAspectRatio($mediaPath, $thumbnail = false)
-	{
-		if (!is_file($mediaPath)) {
-			throw new \Exception('Invalid Media Path');
-		}
-		if ($thumbnail) {
-			return [
-				'dimensions'  => $this->thumbnail,
-				'orientation' => 'thumbnail',
-			];
-		}
-
-		list($width, $height) = getimagesize($mediaPath);
-		$aspect = $width / $height;
-		$orientation = $aspect === 1 ? 'square' :
-		($aspect > 1 ? 'landscape' : 'portrait');
-		$this->orientation = $orientation;
-
-		return [
-			'dimensions'  => $this->orientations()[$orientation],
-			'orientation' => $orientation,
-			'width_original' => $width,
-			'height_original' => $height,
-		];
-	}
-
-	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);
-	}
-
-	public function handleResizeImage(Media $media)
-	{
-		$this->handleImageTransform($media, false);
-	}
-
-	public function handleThumbnailImage(Media $media)
-	{
-		$this->handleImageTransform($media, true);
-	}
-
-	public function handleImageTransform(Media $media, $thumbnail = false)
-	{
-		$path = $media->media_path;
-		$file = storage_path('app/'.$path);
-		if (!in_array($media->mime, $this->acceptedMimes)) {
-			return;
-		}
-		$ratio = $this->getAspectRatio($file, $thumbnail);
-		$aspect = $ratio['dimensions'];
-		$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) {
-					$meta = [];
-					$keys = [
-						"COMPUTED",
-						"FileName",
-						"FileSize",
-						"FileType",
-						"Make",
-						"Model",
-						"MimeType",
-						"ColorSpace",
-						"ExifVersion",
-						"Orientation",
-						"UserComment",
-						"XResolution",
-						"YResolution",
-						"FileDateTime",
-						"SectionsFound",
-						"ExifImageWidth",
-						"ResolutionUnit",
-						"ExifImageLength",
-						"FlashPixVersion",
-						"Exif_IFD_Pointer",
-						"YCbCrPositioning",
-						"ComponentsConfiguration",
-						"ExposureTime",
-						"FNumber",
-						"ISOSpeedRatings",
-						"ShutterSpeedValue"
-					];
-					foreach ($metadata as $k => $v) {
-						if(in_array($k, $keys)) {
-							$meta[$k] = $v;
-						}
-					}
-					$media->metadata = json_encode($meta);
-				}
-
-				if (
-				    ($ratio['width_original'] > $aspect['width'])
-				    || ($ratio['height_original'] > $aspect['height'])
-				) {
-					$img->resize($aspect['width'], $aspect['height'], function ($constraint) {
-						$constraint->aspectRatio();
-					});
-				}
-			}
-			$converted = $this->setBaseName($path, $thumbnail, $img->extension);
-			$newPath = storage_path('app/'.$converted['path']);
-
-			$quality = config_cache('pixelfed.image_quality');
-			$img->save($newPath, $quality);
-
-			if ($thumbnail == true) {
-				$media->thumbnail_path = $converted['path'];
-				$media->thumbnail_url = url(Storage::url($converted['path']));
-			} else {
-				$media->width = $img->width();
-				$media->height = $img->height();
-				$media->orientation = $orientation;
-				$media->media_path = $converted['path'];
-				$media->mime = $img->mime;
-			}
-
-			$img->destroy();
-			$media->save();
-
-			if($thumbnail) {
-				$this->generateBlurhash($media);
-			}
-
-			Cache::forget('status:transformer:media:attachments:'.$media->status_id);
-			Cache::forget('status:thumb:'.$media->status_id);
-
-		} catch (Exception $e) {
-			$media->processed_at = now();
-			$media->save();
-			Log::info('MediaResizeException: Could not process media id: ' . $media->id);
-		}
-	}
-
-	public function setBaseName($basePath, $thumbnail, $extension)
-	{
-		$png = false;
-		$path = explode('.', $basePath);
-		$name = ($thumbnail == true) ? $path[0].'_thumb' : $path[0];
-		$ext = last($path);
-		$basePath = "{$name}.{$ext}";
-
-		return ['path' => $basePath, 'png' => $png];
-	}
-
-	protected function generateBlurhash($media)
-	{
-		$blurhash = Blurhash::generate($media);
-		if($blurhash) {
-			$media->blurhash = $blurhash;
-			$media->save();
-		}
-	}
+    public $square;
+    public $landscape;
+    public $portrait;
+    public $thumbnail;
+    public $orientation;
+    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'));
+
+        $this->square = $this->orientations()['square'];
+        $this->landscape = $this->orientations()['landscape'];
+        $this->portrait = $this->orientations()['portrait'];
+        $this->thumbnail = [
+            'width'  => 640,
+            '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()
+    {
+        return [
+            'square' => [
+                'width'  => 1080,
+                'height' => 1080,
+            ],
+            'landscape' => [
+                'width'  => 1920,
+                'height' => 1080,
+            ],
+            'portrait' => [
+                'width'  => 1080,
+                'height' => 1350,
+            ],
+        ];
+    }
+
+    public function getAspectRatio($mediaPath, $thumbnail = false)
+    {
+        if ($thumbnail) {
+            return [
+                'dimensions'  => $this->thumbnail,
+                'orientation' => 'thumbnail',
+            ];
+        }
+
+        if (!is_file($mediaPath)) {
+            throw new \Exception('Invalid Media Path');
+        }
+
+        list($width, $height) = getimagesize($mediaPath);
+        $aspect = $width / $height;
+        $orientation = $aspect === 1 ? 'square' :
+        ($aspect > 1 ? 'landscape' : 'portrait');
+        $this->orientation = $orientation;
+
+        return [
+            'dimensions'  => $this->orientations()[$orientation],
+            'orientation' => $orientation,
+            'width_original' => $width,
+            'height_original' => $height,
+        ];
+    }
+
+    public function resizeImage(Media $media)
+    {
+        $this->handleResizeImage($media);
+    }
+
+    public function resizeThumbnail(Media $media)
+    {
+        $this->handleThumbnailImage($media);
+    }
+
+    public function handleResizeImage(Media $media)
+    {
+        $this->handleImageTransform($media, false);
+    }
+
+    public function handleThumbnailImage(Media $media)
+    {
+        $this->handleImageTransform($media, true);
+    }
+
+    public function handleImageTransform(Media $media, $thumbnail = false)
+    {
+        $path = $media->media_path;
+        $file = storage_path('app/'.$path);
+        if (!in_array($media->mime, $this->acceptedMimes)) {
+            return;
+        }
+        $ratio = $this->getAspectRatio($file, $thumbnail);
+        $aspect = $ratio['dimensions'];
+        $orientation = $ratio['orientation'];
+
+        try {
+            $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 = [
+                            "FileName",
+                            "FileSize",
+                            "FileType",
+                            "Make",
+                            "Model",
+                            "MimeType",
+                            "ColorSpace",
+                            "ExifVersion",
+                            "Orientation",
+                            "UserComment",
+                            "XResolution",
+                            "YResolution",
+                            "FileDateTime",
+                            "SectionsFound",
+                            "ExifImageWidth",
+                            "ResolutionUnit",
+                            "ExifImageLength",
+                            "FlashPixVersion",
+                            "Exif_IFD_Pointer",
+                            "YCbCrPositioning",
+                            "ComponentsConfiguration",
+                            "ExposureTime",
+                            "FNumber",
+                            "ISOSpeedRatings",
+                            "ShutterSpeedValue"
+                        ];
+                        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 = $img->scaleDown(
+                        $aspect['width'],
+                        $aspect['height']
+                    );
+                }
+            }
+
+            $converted = $this->setBaseName($path, $thumbnail, $extension);
+            $newPath = storage_path('app/'.$converted['path']);
+
+            $quality = config_cache('pixelfed.image_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'];
+                $media->thumbnail_url = url(Storage::url($converted['path']));
+            } else {
+                $media->width = $img->width();
+                $media->height = $img->height();
+                $media->orientation = $orientation;
+                $media->media_path = $converted['path'];
+                $media->mime = 'image/' . $extension;
+            }
+
+            $media->save();
+
+            if ($thumbnail) {
+                $this->generateBlurhash($media);
+            }
+
+            Cache::forget('status:transformer:media:attachments:'.$media->status_id);
+            Cache::forget('status:thumb:'.$media->status_id);
+
+        } catch (\Exception $e) {
+            $media->processed_at = now();
+            $media->save();
+            Log::info('MediaResizeException: ' . $e->getMessage() . ' | Could not process media id: ' . $media->id);
+        }
+    }
+
+    public function setBaseName($basePath, $thumbnail, $extension)
+    {
+        $png = false;
+        $path = explode('.', $basePath);
+        $name = ($thumbnail == true) ? $path[0].'_thumb' : $path[0];
+        $ext = last($path);
+        $basePath = "{$name}.{$ext}";
+
+        return ['path' => $basePath, 'png' => $png];
+    }
+
+    protected function generateBlurhash($media)
+    {
+        $blurhash = Blurhash::generate($media);
+        if ($blurhash) {
+            $media->blurhash = $blurhash;
+            $media->save();
+        }
+    }
 }

+ 1 - 1
composer.json

@@ -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",

文件差异内容过多而无法显示
+ 238 - 174
composer.lock


+ 3 - 5
config/image.php

@@ -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'),
-
 ];

部分文件因为文件数量过多而无法显示