diff --git a/app/Services/Media/MediaOptimizer.php b/app/Services/Media/MediaOptimizer.php index 6c13c0fc..abbcb427 100644 --- a/app/Services/Media/MediaOptimizer.php +++ b/app/Services/Media/MediaOptimizer.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Log; use Intervention\Image\Drivers\Gd\Driver; use Intervention\Image\ImageManager; +use RuntimeException; class MediaOptimizer { @@ -113,6 +114,8 @@ public function maxWidthForPlatform(Platform $platform): ?int */ public function cropToAspectRatio(string $filePath, float $ratio): string { + $this->assertWithinMemoryBudget($filePath); + $image = $this->manager->decodePath($filePath); $width = $image->width(); @@ -156,6 +159,8 @@ public function cropToAspectRatio(string $filePath, float $ratio): string */ public function fitToCanvas(string $filePath, int $width, int $height): string { + $this->assertWithinMemoryBudget($filePath); + $foreground = $this->manager->decodePath($filePath); $canvasRatio = $width / $height; $imageRatio = $foreground->width() / $foreground->height(); @@ -181,6 +186,27 @@ public function fitToCanvas(string $filePath, int $width, int $height): string return $tempFile; } + /** + * Reject a source whose pixel dimensions would blow the GD memory budget, + * before it is decoded — a small-byte, huge-dimension image would otherwise + * exhaust memory with an uncatchable fatal. Transforms that can't fall back + * to the original (crop, fit) call this; `optimizeImage` skips instead. + */ + private function assertWithinMemoryBudget(string $filePath): void + { + $imageInfo = @getimagesize($filePath); + + if ($imageInfo === false) { + return; + } + + $estimatedMemory = $imageInfo[0] * $imageInfo[1] * ($imageInfo['channels'] ?? 4) * 1.5; + + if ($estimatedMemory > 256 * 1024 * 1024) { + throw new RuntimeException("Image dimensions ({$imageInfo[0]}x{$imageInfo[1]}) exceed the safe processing budget."); + } + } + /** * @return array{max_width: int, max_size: int, format: string, quality: int} */ diff --git a/app/Services/Social/Concerns/CropsImageForAspectRatio.php b/app/Services/Social/Concerns/CropsImageForAspectRatio.php index d2c67d9c..a6e5a632 100644 --- a/app/Services/Social/Concerns/CropsImageForAspectRatio.php +++ b/app/Services/Social/Concerns/CropsImageForAspectRatio.php @@ -37,14 +37,20 @@ protected function cropImageForAspectRatio(string $imageUrl, ?string $aspectRati throw $this->cropFailureException('Failed to download image for cropping'); } - $cropped = app(MediaOptimizer::class)->cropToAspectRatio($tempInput, $ratio); + try { + $cropped = app(MediaOptimizer::class)->cropToAspectRatio($tempInput, $ratio); + } catch (\Throwable) { + throw $this->cropFailureException('Failed to process image for cropping'); + } - $path = self::CROP_DIRECTORY.'/'.Str::uuid()->toString().'.jpg'; - Storage::put($path, file_get_contents($cropped)); + try { + $path = self::CROP_DIRECTORY.'/'.Str::uuid()->toString().'.jpg'; + Storage::put($path, file_get_contents($cropped)); - @unlink($cropped); - - return Storage::url($path); + return Storage::url($path); + } finally { + @unlink($cropped); + } } finally { @unlink($tempInput); } @@ -66,14 +72,20 @@ protected function fitImageToCanvas(string $imageUrl, int $width, int $height): throw $this->cropFailureException('Failed to download image for story fitting'); } - $fitted = app(MediaOptimizer::class)->fitToCanvas($tempInput, $width, $height); + try { + $fitted = app(MediaOptimizer::class)->fitToCanvas($tempInput, $width, $height); + } catch (\Throwable) { + throw $this->cropFailureException('Failed to process image for story fitting'); + } - $path = self::CROP_DIRECTORY.'/'.Str::uuid()->toString().'.jpg'; - Storage::put($path, file_get_contents($fitted)); + try { + $path = self::CROP_DIRECTORY.'/'.Str::uuid()->toString().'.jpg'; + Storage::put($path, file_get_contents($fitted)); - @unlink($fitted); - - return Storage::url($path); + return Storage::url($path); + } finally { + @unlink($fitted); + } } finally { @unlink($tempInput); } diff --git a/app/Services/Social/InstagramPublisher.php b/app/Services/Social/InstagramPublisher.php index 72f6adfc..8dcae63a 100644 --- a/app/Services/Social/InstagramPublisher.php +++ b/app/Services/Social/InstagramPublisher.php @@ -159,7 +159,7 @@ private function publishStory(string $instagramId, string $accessToken, $media): $params['video_url'] = $media->url; } else { $dimensions = ContentType::InstagramStory->aiImageDimensions(); - $params['image_url'] = $this->fitImageToCanvas($media->url, $dimensions['width'], $dimensions['height']); + $params['image_url'] = $this->fitImageToCanvas($media->url, data_get($dimensions, 'width'), data_get($dimensions, 'height')); } // Step 1: Create story container diff --git a/resources/js/components/posts/previews/InstagramPreview.vue b/resources/js/components/posts/previews/InstagramPreview.vue index 63ad2b0a..324a369a 100644 --- a/resources/js/components/posts/previews/InstagramPreview.vue +++ b/resources/js/components/posts/previews/InstagramPreview.vue @@ -168,7 +168,7 @@ const username = computed(() => props.socialAccount.username || props.socialAcco @@ -232,7 +232,7 @@ const username = computed(() => props.socialAccount.username || props.socialAcco diff --git a/resources/js/components/posts/previews/TikTokPreview.vue b/resources/js/components/posts/previews/TikTokPreview.vue index 23920184..c8c70d7e 100644 --- a/resources/js/components/posts/previews/TikTokPreview.vue +++ b/resources/js/components/posts/previews/TikTokPreview.vue @@ -2,7 +2,7 @@ import { IconPlus } from '@tabler/icons-vue'; import { computed } from 'vue'; -import VerticalMediaCanvas from "@/components/posts/previews/VerticalMediaCanvas.vue"; +import VerticalMediaCanvas from '@/components/posts/previews/VerticalMediaCanvas.vue'; import type { MediaItem } from '@/types/media'; interface SocialAccount { diff --git a/resources/js/components/posts/previews/YouTubePreview.vue b/resources/js/components/posts/previews/YouTubePreview.vue index 3c111e5d..985da55c 100644 --- a/resources/js/components/posts/previews/YouTubePreview.vue +++ b/resources/js/components/posts/previews/YouTubePreview.vue @@ -1,7 +1,7 @@