diff --git a/app/Console/Commands/ReprocessMediaImages.php b/app/Console/Commands/ReprocessMediaImages.php new file mode 100644 index 00000000..299f0103 --- /dev/null +++ b/app/Console/Commands/ReprocessMediaImages.php @@ -0,0 +1,68 @@ +whereNotNull('image_widths') + ->where('image_widths', '>', 1000) + ->get(); + + $dryRun = $this->option('dry-run'); + + $this->info("Found {$media->count()} images to reprocess." . ($dryRun ? ' (dry run)' : '')); + + foreach ($media as $item) { + $path = $item->path; + + if (! Storage::disk('public')->exists($path)) { + $this->warn("{$path}: original not found on public disk, skipping."); + continue; + } + + if ($dryRun) { + $this->line("{$path} ({$item->image_widths}px wide)"); + continue; + } + + $this->info("Processing: {$path}"); + + try { + $file = Storage::disk('public')->get($path); + $image = $manager->read($file); + } catch (DecoderException) { + $this->warn(" Could not decode image, skipping."); + continue; + } + + $filenameParts = explode('.', $path); + $extension = array_pop($filenameParts); + $basename = trim(implode('.', $filenameParts), '.'); + + $medium = $image->scale(width: 1000); + Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); + + $small = $image->scale(width: 500); + Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode()); + + $this->info(" Done."); + } + + $this->info('Reprocessing complete.' . ($dryRun ? ' (dry run — no files were changed)' : '')); + } +} diff --git a/app/Jobs/ProcessMedia.php b/app/Jobs/ProcessMedia.php index e9e291a6..f9d8af50 100644 --- a/app/Jobs/ProcessMedia.php +++ b/app/Jobs/ProcessMedia.php @@ -56,10 +56,10 @@ class ProcessMedia implements ShouldQueue // foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar $basename = trim(implode('.', $filenameParts), '.'); - $medium = $image->resize(width: 1000); + $medium = $image->scale(width: 1000); Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); - $small = $image->resize(width: 500); + $small = $image->scale(width: 500); Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode()); } diff --git a/tests/Feature/Console/ReprocessMediaImagesTest.php b/tests/Feature/Console/ReprocessMediaImagesTest.php new file mode 100644 index 00000000..eeec5a6f --- /dev/null +++ b/tests/Feature/Console/ReprocessMediaImagesTest.php @@ -0,0 +1,105 @@ +put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg')); + + Media::factory()->create([ + 'path' => 'media/test-image.jpg', + 'image_widths' => 4592, + ]); + + $this->artisan('app:reprocess-media-images', ['--dry-run' => true]) + ->expectsOutputToContain('Found 1 images to reprocess. (dry run)') + ->expectsOutputToContain('media/test-image.jpg (4592px wide)') + ->assertSuccessful(); + + Storage::disk('public')->assertMissing('media/test-image-medium.jpg'); + Storage::disk('public')->assertMissing('media/test-image-small.jpg'); + + Storage::disk('public')->delete('media/test-image.jpg'); + } + + #[Test] + public function missing_original_file_is_skipped(): void + { + Media::factory()->create([ + 'path' => 'media/does-not-exist.jpg', + 'image_widths' => 2000, + ]); + + $this->artisan('app:reprocess-media-images') + ->expectsOutputToContain('media/does-not-exist.jpg: original not found on public disk, skipping.') + ->assertSuccessful(); + } + + #[Test] + public function non_decodable_image_is_skipped(): void + { + Storage::disk('public')->put('media/not-an-image.jpg', 'This is not an image'); + + Media::factory()->create([ + 'path' => 'media/not-an-image.jpg', + 'image_widths' => 2000, + ]); + + $this->artisan('app:reprocess-media-images') + ->expectsOutputToContain('Could not decode image, skipping.') + ->assertSuccessful(); + + Storage::disk('public')->assertMissing('media/not-an-image-medium.jpg'); + Storage::disk('public')->assertMissing('media/not-an-image-small.jpg'); + + Storage::disk('public')->delete('media/not-an-image.jpg'); + } + + #[Test] + public function large_images_are_rescaled_into_medium_and_small_variants(): void + { + Storage::disk('public')->put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg')); + + Media::factory()->create([ + 'path' => 'media/test-image.jpg', + 'image_widths' => 4592, + ]); + + $this->artisan('app:reprocess-media-images') + ->expectsOutputToContain('Processing: media/test-image.jpg') + ->assertSuccessful(); + + Storage::disk('public')->assertExists('media/test-image-medium.jpg'); + Storage::disk('public')->assertExists('media/test-image-small.jpg'); + + Storage::disk('public')->delete('media/test-image.jpg'); + Storage::disk('public')->delete('media/test-image-medium.jpg'); + Storage::disk('public')->delete('media/test-image-small.jpg'); + } + + #[Test] + public function images_with_width_at_or_below_threshold_are_not_processed(): void + { + Media::factory()->create([ + 'path' => 'media/small-image.jpg', + 'image_widths' => 1000, + ]); + + $this->artisan('app:reprocess-media-images') + ->expectsOutputToContain('Found 0 images to reprocess.') + ->assertSuccessful(); + } +}