[MTM] Fix images on mobile #97
3 changed files with 175 additions and 2 deletions
Add command to reprocess existing media with correct aspect-ratio scaling
ProcessMedia was using resize(), which distorts images that aren't the target aspect ratio; scale() preserves it. Existing medium/small variants generated before this fix need regenerating, so add an artisan command (with --dry-run) to do that, plus feature tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
commit
4569abc351
68
app/Console/Commands/ReprocessMediaImages.php
Normal file
68
app/Console/Commands/ReprocessMediaImages.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Media;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Exceptions\DecoderException;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class ReprocessMediaImages extends Command
|
||||
{
|
||||
protected $signature = 'app:reprocess-media-images {--dry-run : List images that would be processed without making changes}';
|
||||
|
||||
protected $description = 'Regenerate medium and small image variants using correct aspect-ratio scaling';
|
||||
|
||||
public function handle(ImageManager $manager): void
|
||||
{
|
||||
$media = Media::where('type', 'image')
|
||||
->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)' : ''));
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
105
tests/Feature/Console/ReprocessMediaImagesTest.php
Normal file
105
tests/Feature/Console/ReprocessMediaImagesTest.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Console;
|
||||
|
||||
use App\Models\Media;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ReprocessMediaImagesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
#[Test]
|
||||
public function dry_run_lists_images_without_modifying_files(): 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', ['--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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue