Browse Source

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 tháng trước cách đây
mục cha
commit
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",

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 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'),
-
 ];

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác