diff --git a/database/migrations/2026_09_22_000000_optimize_onticket_images.php b/database/migrations/2026_09_22_000000_optimize_onticket_images.php new file mode 100644 index 0000000..fe9faeb --- /dev/null +++ b/database/migrations/2026_09_22_000000_optimize_onticket_images.php @@ -0,0 +1,23 @@ +environment('testing')) { + Storage::fake('s3'); + } + + app(OptimizeOnticketImagesSeeder::class)->run(); + } + + public function down(): void + { + // The optimized attachments may already be in active use. Keep this + // data migration irreversible instead of restoring heavier assets. + } +}; diff --git a/database/seeders/OnTicketTenantSeeder.php b/database/seeders/OnTicketTenantSeeder.php index 9ad1e98..8d2b0d3 100644 --- a/database/seeders/OnTicketTenantSeeder.php +++ b/database/seeders/OnTicketTenantSeeder.php @@ -14,6 +14,8 @@ use RuntimeException; class OnTicketTenantSeeder extends Seeder { + private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado'; + public function __construct( private readonly TenantProvisioningService $tenantProvisioningService, private readonly WebsiteExtraService $websiteExtraService, @@ -49,7 +51,7 @@ class OnTicketTenantSeeder extends Seeder 'header_logo' => $this->uploadedImage('onticket_logo.png'), 'footer_logo' => $this->uploadedImage('onticket_footer_logo.png'), 'favicon' => $this->uploadedImage('onticket_favicon.svg'), - 'header_bg_image' => $this->uploadedImage('onticket_header_background.png'), + 'header_bg_image' => $this->uploadedImage('onticket_header_background.avif'), ]); } else { $tenant->update([ @@ -64,7 +66,7 @@ class OnTicketTenantSeeder extends Seeder 'header_logo' => ['relation' => 'headerLogo', 'filename' => 'onticket_logo.png'], 'footer_logo' => ['relation' => 'footerLogo', 'filename' => 'onticket_footer_logo.png'], 'favicon' => ['relation' => 'favicon', 'filename' => 'onticket_favicon.svg'], - 'header_bg_image' => ['relation' => 'headerBackgroundImage', 'filename' => 'onticket_header_background.png'], + 'header_bg_image' => ['relation' => 'headerBackgroundImage', 'filename' => 'onticket_header_background.avif'], ] as $field => $image) { $attachment = $tenant->{$image['relation']}; @@ -98,7 +100,7 @@ class OnTicketTenantSeeder extends Seeder private function uploadedImage(string $filename): UploadedFile { - $path = public_path("images/tennants/onticket/{$filename}"); + $path = public_path(self::IMAGE_DIRECTORY."/{$filename}"); if (! is_file($path)) { throw new RuntimeException("Image not found at path: {$path}"); @@ -106,6 +108,7 @@ class OnTicketTenantSeeder extends Seeder $mimeType = match (strtolower(pathinfo($filename, PATHINFO_EXTENSION))) { 'svg' => 'image/svg+xml', + 'avif' => 'image/avif', default => 'image/png', }; diff --git a/database/seeders/OnticketImmersiveHeroCarouselSeeder.php b/database/seeders/OnticketImmersiveHeroCarouselSeeder.php index 23058cb..888f0c4 100644 --- a/database/seeders/OnticketImmersiveHeroCarouselSeeder.php +++ b/database/seeders/OnticketImmersiveHeroCarouselSeeder.php @@ -10,13 +10,15 @@ use RuntimeException; class OnticketImmersiveHeroCarouselSeeder extends Seeder { + private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel'; + private const IMAGES = [ - 'onticket_hero_carousel_1.png', - 'onticket_hero_carousel_2.png', + 'onticket_hero_carousel_1.avif', + 'onticket_hero_carousel_2.avif', 'onticket_hero_carousel_3.avif', - 'onticket_hero_carousel_4.jfif', - 'onticket_hero_carousel_5.jfif', - 'onticket_hero_carousel_6.jfif', + 'onticket_hero_carousel_4.avif', + 'onticket_hero_carousel_5.avif', + 'onticket_hero_carousel_6.avif', 'onticket_hero_carousel_7.avif', 'onticket_hero_carousel_8.avif', ]; @@ -39,7 +41,7 @@ class OnticketImmersiveHeroCarouselSeeder extends Seeder } $images = array_map(function (string $filename): UploadedFile { - $path = public_path("images/tennants/onticket/onticket_tennant_hero_carousel/{$filename}"); + $path = public_path(self::IMAGE_DIRECTORY."/{$filename}"); if (! is_file($path)) { throw new RuntimeException("Image not found at path: {$path}"); diff --git a/database/seeders/OptimizeOnticketImagesSeeder.php b/database/seeders/OptimizeOnticketImagesSeeder.php new file mode 100644 index 0000000..ccd1cc5 --- /dev/null +++ b/database/seeders/OptimizeOnticketImagesSeeder.php @@ -0,0 +1,155 @@ + 'live-music.avif', + '[Prueba] Festival de Sabores' => 'food-festival.avif', + '[Prueba] Stand Up en el Centro' => 'stand-up.avif', + ]; + + public function run(): void + { + $tenant = Tenant::query()->where('codigo', 'onticket')->first(); + + if ($tenant === null) { + return; + } + + $this->replaceAttachment( + $tenant->headerBackgroundImage, + 'onticket_header_background.avif', + ); + + $carousel = $tenant->websiteExtras() + ->whereHas( + 'websiteTypeExtra', + fn ($query) => $query->where('codigo', 'immersiveHeroCarousel') + ) + ->first(); + $carouselIds = is_array($carousel?->config) ? array_values($carousel->config) : []; + + foreach (self::CAROUSEL_IMAGES as $index => $filename) { + $attachmentId = $carouselIds[$index] ?? null; + + if (is_int($attachmentId)) { + $this->replaceAttachment( + Attachment::query()->find($attachmentId), + 'onticket_tennant_hero_carousel/'.$filename, + ); + } + } + + foreach (self::EVENT_IMAGES as $title => $filename) { + $event = $tenant->events()->where('title', $title)->first(); + + $this->replaceAttachment( + $event?->attachment, + 'test-events/'.$filename, + ); + } + } + + private function replaceAttachment(?Attachment $attachment, string $relativePath): void + { + if ($attachment === null) { + return; + } + + $sourcePath = public_path(self::IMAGE_DIRECTORY.'/'.$relativePath); + + if (! is_file($sourcePath)) { + throw new RuntimeException("Image not found at path: {$sourcePath}"); + } + + $contents = file_get_contents($sourcePath); + + if (! is_string($contents) || $contents === '') { + throw new RuntimeException("Could not read image at path: {$sourcePath}"); + } + + $filename = basename($relativePath); + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $mimeType = match ($extension) { + 'avif' => 'image/avif', + 'svg' => 'image/svg+xml', + default => 'image/png', + }; + $directory = trim(str_replace('\\', '/', dirname($attachment->path)), './'); + $storedFilename = $attachment->key.($extension === '' ? '' : '.'.$extension); + $newPath = $directory === '' ? $storedFilename : $directory.'/'.$storedFilename; + $disk = Storage::disk('s3'); + + if ( + $attachment->path === $newPath + && $attachment->filename === $filename + && $attachment->mime_type === $mimeType + && $attachment->size === strlen($contents) + && $disk->exists($newPath) + ) { + return; + } + + $oldPath = $attachment->path; + $oldContents = $disk->exists($oldPath) ? $disk->get($oldPath) : null; + + if (! $disk->put($newPath, $contents)) { + throw new RuntimeException("Could not store optimized image at path: {$newPath}"); + } + + try { + DB::transaction(function () use ( + $attachment, + $extension, + $filename, + $mimeType, + $newPath, + $contents, + ): void { + $attachment->update([ + 'path' => $newPath, + 'filename' => $filename, + 'type' => 'image', + 'mime_type' => $mimeType, + 'extension' => $extension, + 'size' => strlen($contents), + ]); + }); + } catch (Throwable $throwable) { + if ($newPath === $oldPath && is_string($oldContents)) { + $disk->put($oldPath, $oldContents); + } else { + $disk->delete($newPath); + } + + throw $throwable; + } + + if ($oldPath !== $newPath && $disk->exists($oldPath)) { + $disk->delete($oldPath); + } + } +} diff --git a/database/seeders/TestEventsSeeder.php b/database/seeders/TestEventsSeeder.php index b2d2173..7de99e7 100644 --- a/database/seeders/TestEventsSeeder.php +++ b/database/seeders/TestEventsSeeder.php @@ -11,6 +11,8 @@ use RuntimeException; class TestEventsSeeder extends Seeder { + private const IMAGE_DIRECTORY = 'images/tennants/onticket_otpimizado/test-events'; + public function __construct(private readonly AttachmentService $attachmentService) {} public function run(): void @@ -24,7 +26,7 @@ class TestEventsSeeder extends Seeder 'subtitle' => 'Una noche para cantar y bailar', 'description' => 'Evento de prueba para explorar la cartelera de OnTicket.', 'location' => 'Teatro Broadway, Rosario', - 'image' => 'live-music.png', + 'image' => 'live-music.avif', 'weeks_from_now' => [2], 'time_start' => '21:00:00', 'time_end' => '23:30:00', @@ -35,7 +37,7 @@ class TestEventsSeeder extends Seeder 'subtitle' => 'Gastronomía y música para toda la familia', 'description' => 'Evento de prueba con dos jornadas disponibles.', 'location' => 'Parque de España, Rosario', - 'image' => 'food-festival.png', + 'image' => 'food-festival.avif', 'weeks_from_now' => [3, 3], 'day_offsets' => [0, 1], 'time_start' => '12:00:00', @@ -47,7 +49,7 @@ class TestEventsSeeder extends Seeder 'subtitle' => 'Humor para compartir', 'description' => 'Evento de prueba para visualizar distintas propuestas.', 'location' => 'Centro Cultural La Comedia, Rosario', - 'image' => 'stand-up.png', + 'image' => 'stand-up.avif', 'weeks_from_now' => [4], 'time_start' => '20:30:00', 'time_end' => '22:00:00', @@ -74,14 +76,14 @@ class TestEventsSeeder extends Seeder } if ($event->attachment_id === null) { - $path = public_path('images/tennants/onticket/test-events/'.$definition['image']); + $path = public_path(self::IMAGE_DIRECTORY.'/'.$definition['image']); if (! is_file($path)) { throw new RuntimeException("Image not found at path: {$path}"); } $attachment = $this->attachmentService->store( - new UploadedFile($path, $definition['image'], 'image/png', null, true), + new UploadedFile($path, $definition['image'], 'image/avif', null, true), 'tenants/onticket/events', ); $event->update(['attachment_id' => $attachment->id]); diff --git a/public/images/tennants/onticket_otpimizado/onticket_favicon.svg b/public/images/tennants/onticket_otpimizado/onticket_favicon.svg new file mode 100644 index 0000000..30502c6 --- /dev/null +++ b/public/images/tennants/onticket_otpimizado/onticket_favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/tennants/onticket_otpimizado/onticket_footer_logo.png b/public/images/tennants/onticket_otpimizado/onticket_footer_logo.png new file mode 100644 index 0000000..dd86921 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_footer_logo.png differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_header_background.avif b/public/images/tennants/onticket_otpimizado/onticket_header_background.avif new file mode 100644 index 0000000..1332b8e Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_header_background.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_logo.png b/public/images/tennants/onticket_otpimizado/onticket_logo.png new file mode 100644 index 0000000..12cd594 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_logo.png differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_1.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_1.avif new file mode 100644 index 0000000..4b1ad34 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_1.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_2.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_2.avif new file mode 100644 index 0000000..6b7809f Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_2.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_3.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_3.avif new file mode 100644 index 0000000..fcb3f2b Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_3.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_4.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_4.avif new file mode 100644 index 0000000..cef2f75 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_4.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_5.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_5.avif new file mode 100644 index 0000000..6cf82df Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_5.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_6.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_6.avif new file mode 100644 index 0000000..ddb764f Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_6.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_7.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_7.avif new file mode 100644 index 0000000..cdb0017 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_7.avif differ diff --git a/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_8.avif b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_8.avif new file mode 100644 index 0000000..b2bbad9 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/onticket_tennant_hero_carousel/onticket_hero_carousel_8.avif differ diff --git a/public/images/tennants/onticket_otpimizado/test-events/food-festival.avif b/public/images/tennants/onticket_otpimizado/test-events/food-festival.avif new file mode 100644 index 0000000..68d9e5a Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/test-events/food-festival.avif differ diff --git a/public/images/tennants/onticket_otpimizado/test-events/live-music.avif b/public/images/tennants/onticket_otpimizado/test-events/live-music.avif new file mode 100644 index 0000000..c7b4246 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/test-events/live-music.avif differ diff --git a/public/images/tennants/onticket_otpimizado/test-events/stand-up.avif b/public/images/tennants/onticket_otpimizado/test-events/stand-up.avif new file mode 100644 index 0000000..9e78288 Binary files /dev/null and b/public/images/tennants/onticket_otpimizado/test-events/stand-up.avif differ diff --git a/tests/Feature/Seeders/TenantSeederTest.php b/tests/Feature/Seeders/TenantSeederTest.php index df9157c..8847f80 100644 --- a/tests/Feature/Seeders/TenantSeederTest.php +++ b/tests/Feature/Seeders/TenantSeederTest.php @@ -44,7 +44,7 @@ class TenantSeederTest extends TestCase $this->assertSame('onticket_logo.png', $tenant->headerLogo->filename); $this->assertSame('onticket_footer_logo.png', $tenant->footerLogo->filename); $this->assertSame('onticket_favicon.svg', $tenant->favicon->filename); - $this->assertSame('onticket_header_background.png', $tenant->headerBackgroundImage->filename); + $this->assertSame('onticket_header_background.avif', $tenant->headerBackgroundImage->filename); foreach ([$tenant->headerLogo, $tenant->footerLogo, $tenant->favicon, $tenant->headerBackgroundImage] as $attachment) { Storage::disk('s3')->assertExists($attachment->path); @@ -56,12 +56,12 @@ class TenantSeederTest extends TestCase $imageIds = $carousel->config; $this->assertCount(8, $imageIds); $this->assertSame([ - 'onticket_hero_carousel_1.png', - 'onticket_hero_carousel_2.png', + 'onticket_hero_carousel_1.avif', + 'onticket_hero_carousel_2.avif', 'onticket_hero_carousel_3.avif', - 'onticket_hero_carousel_4.jfif', - 'onticket_hero_carousel_5.jfif', - 'onticket_hero_carousel_6.jfif', + 'onticket_hero_carousel_4.avif', + 'onticket_hero_carousel_5.avif', + 'onticket_hero_carousel_6.avif', 'onticket_hero_carousel_7.avif', 'onticket_hero_carousel_8.avif', ], array_map(fn (int $id): string => Attachment::query()->findOrFail($id)->filename, $imageIds)); diff --git a/tests/Feature/Seeders/TestEventsSeederTest.php b/tests/Feature/Seeders/TestEventsSeederTest.php index 5e98013..ef9b81d 100644 --- a/tests/Feature/Seeders/TestEventsSeederTest.php +++ b/tests/Feature/Seeders/TestEventsSeederTest.php @@ -29,11 +29,16 @@ class TestEventsSeederTest extends TestCase $events = $tenant->events()->with('dates.validityTime')->get(); $this->assertCount(3, $events); $this->assertSame(4, $events->sum(fn ($event): int => $event->dates->count())); + $this->assertEqualsCanonicalizing( + ['live-music.avif', 'food-festival.avif', 'stand-up.avif'], + $events->map(fn ($event): string => $event->attachment->filename)->all(), + ); foreach ($events as $event) { $this->assertTrue($event->published_at->isPast()); $this->assertNotNull($event->date_text); $this->assertNotNull($event->attachment_id); + $this->assertSame('image/avif', $event->attachment->mime_type); Storage::disk('s3')->assertExists($event->attachment->path); foreach ($event->dates as $date) {