teknolis / app /Services /StoryImageNormalizerService.php
adwandw's picture
Fix: Space Restarting loop
d4d07bc
Raw
History Blame Contribute Delete
6.01 kB
<?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();
}
}