| <?php |
|
|
| namespace App\Services; |
|
|
| use Illuminate\Support\Facades\Http; |
| use Illuminate\Support\Facades\Storage; |
| use Illuminate\Support\Str; |
| use RuntimeException; |
| use function imagecreatefromstring; |
| use function imagedestroy; |
| use function imagesx; |
| use function imagesy; |
| use function imagecreatetruecolor; |
| use function imagealphablending; |
| use function imagesavealpha; |
| use function imagecolorallocate; |
| use function imagefilledrectangle; |
| use function imagecopyresampled; |
| use function imagecopy; |
| use function imagejpeg; |
|
|
| class StoryImageNormalizerService |
| { |
| public function __construct(private ?HuggingFaceMediaStorageService $hfMediaStorage = null) |
| { |
| } |
|
|
| public function normalizeMany(array $sourceUrls): array |
| { |
| $variants = []; |
|
|
| foreach ($sourceUrls as $sourceUrl) { |
| if (! is_string($sourceUrl) || trim($sourceUrl) === '') { |
| continue; |
| } |
|
|
| $variants[] = $this->normalizeVariant(trim($sourceUrl)); |
| } |
|
|
| if ($variants === []) { |
| throw new RuntimeException('Tidak ada image source yang bisa dinormalisasi ke format story.'); |
| } |
|
|
| return $variants; |
| } |
|
|
| public function normalizeVariant(string $sourceUrl): array |
| { |
| if (! extension_loaded('gd')) { |
| throw new RuntimeException('GD extension belum aktif, jadi normalisasi image story belum bisa dijalankan.'); |
| } |
|
|
| $binary = $this->loadImageBinary($sourceUrl); |
| $resource = @imagecreatefromstring($binary); |
|
|
| if ($resource === false) { |
| throw new RuntimeException('Gagal membaca image source untuk dinormalisasi ke story post.'); |
| } |
|
|
| $storyCanvas = $this->renderCanvas($resource, 1080, 1920); |
| $thumbCanvas = $this->renderCanvas($resource, 270, 480); |
| imagedestroy($resource); |
|
|
| $storyRelative = 'images/story_'.now()->format('Ymd_His').'_'.Str::lower(Str::random(8)).'.jpg'; |
| $thumbRelative = 'images/story_thumb_'.now()->format('Ymd_His').'_'.Str::lower(Str::random(8)).'.jpg'; |
|
|
| Storage::disk('public')->put($storyRelative, $this->jpegBinary($storyCanvas, 92)); |
| Storage::disk('public')->put($thumbRelative, $this->jpegBinary($thumbCanvas, 82)); |
|
|
| imagedestroy($storyCanvas); |
| imagedestroy($thumbCanvas); |
|
|
| return [ |
| 'url' => $this->offloadPublicPath($storyRelative), |
| 'thumbnail_url' => $this->offloadPublicPath($thumbRelative), |
| 'width' => 1080, |
| 'height' => 1920, |
| ]; |
| } |
|
|
| private function offloadPublicPath(string $relativePath): string |
| { |
| try { |
| return $this->hfMedia()->offloadPublicRelativePath($relativePath); |
| } catch (\Throwable) { |
| return Storage::url($relativePath); |
| } |
| } |
|
|
| private function hfMedia(): HuggingFaceMediaStorageService |
| { |
| if ($this->hfMediaStorage instanceof HuggingFaceMediaStorageService) { |
| return $this->hfMediaStorage; |
| } |
|
|
| $service = app(HuggingFaceMediaStorageService::class); |
| if (! $service instanceof HuggingFaceMediaStorageService) { |
| throw new RuntimeException('Service HuggingFaceMediaStorageService tidak tersedia.'); |
| } |
|
|
| $this->hfMediaStorage = $service; |
|
|
| return $this->hfMediaStorage; |
| } |
|
|
| private function renderCanvas($source, int $targetWidth, int $targetHeight) |
| { |
| $sourceWidth = imagesx($source); |
| $sourceHeight = imagesy($source); |
|
|
| $scale = max($targetWidth / max(1, $sourceWidth), $targetHeight / max(1, $sourceHeight)); |
| $resizedWidth = (int) ceil($sourceWidth * $scale); |
| $resizedHeight = (int) ceil($sourceHeight * $scale); |
| $offsetX = (int) floor(($resizedWidth - $targetWidth) / 2); |
| $offsetY = (int) floor(($resizedHeight - $targetHeight) / 2); |
|
|
| $resized = imagecreatetruecolor($resizedWidth, $resizedHeight); |
| $canvas = imagecreatetruecolor($targetWidth, $targetHeight); |
|
|
| if ($resized === false || $canvas === false) { |
| throw new RuntimeException('Gagal menyiapkan canvas image story.'); |
| } |
|
|
| imagealphablending($resized, true); |
| imagesavealpha($resized, true); |
| imagealphablending($canvas, true); |
| imagesavealpha($canvas, true); |
|
|
| $background = imagecolorallocate($resized, 11, 19, 31); |
| imagefilledrectangle($resized, 0, 0, $resizedWidth, $resizedHeight, $background); |
| imagecopyresampled($resized, $source, 0, 0, 0, 0, $resizedWidth, $resizedHeight, $sourceWidth, $sourceHeight); |
|
|
| $canvasBg = imagecolorallocate($canvas, 11, 19, 31); |
| imagefilledrectangle($canvas, 0, 0, $targetWidth, $targetHeight, $canvasBg); |
| imagecopy($canvas, $resized, 0, 0, $offsetX, $offsetY, $targetWidth, $targetHeight); |
| imagedestroy($resized); |
|
|
| return $canvas; |
| } |
|
|
| private function jpegBinary($canvas, int $quality): string |
| { |
| ob_start(); |
| imagejpeg($canvas, null, $quality); |
| $binary = ob_get_clean(); |
|
|
| if (! is_string($binary) || $binary === '') { |
| throw new RuntimeException('Gagal mengekspor image story ke JPEG.'); |
| } |
|
|
| return $binary; |
| } |
|
|
| private function loadImageBinary(string $imageUrl): string |
| { |
| $path = parse_url($imageUrl, PHP_URL_PATH) ?: $imageUrl; |
|
|
| if (is_string($path) && str_starts_with($path, '/storage/')) { |
| $relative = ltrim(substr($path, strlen('/storage/')), '/'); |
| if (Storage::disk('public')->exists($relative)) { |
| $binary = Storage::disk('public')->get($relative); |
| if ($binary !== '') { |
| return $binary; |
| } |
| } |
| } |
|
|
| $response = Http::timeout(90)->get($imageUrl); |
| if (! $response->successful()) { |
| throw new RuntimeException('Gagal mengambil image source untuk story post.'); |
| } |
|
|
| return (string) $response->body(); |
| } |
| } |
|
|