sizeFromAspect($aspect); $urls = []; for ($variant = 1; $variant <= $count; $variant++) { $image = imagecreatetruecolor($width, $height); if ($image === false) { throw new RuntimeException('Gagal menyiapkan canvas poster image.'); } if (function_exists('imageantialias')) { @imageantialias($image, true); } $palette = $this->paletteFromPrompt($prompt, $variant); $this->drawBackground($image, $width, $height, $palette); $this->drawGlassPanels($image, $width, $height, $palette); $this->drawHeadline($image, $width, $height, $prompt); ob_start(); imagepng($image); $binary = ob_get_clean(); imagedestroy($image); if (! is_string($binary) || $binary === '') { throw new RuntimeException('Gagal mengekspor poster image ke PNG.'); } $filename = 'poster_'.now()->format('Ymd_His').'_'.Str::lower(Str::random(8)).'.png'; $path = 'images/'.$filename; Storage::disk('public')->put($path, $binary); $urls[] = $this->offloadPublicPath($path); } return $urls; } 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 sizeFromAspect(string $aspect): array { return match ($aspect) { 'square' => [1080, 1080], 'landscape' => [1536, 1024], default => [1080, 1920], }; } private function paletteFromPrompt(string $prompt, int $variant): array { $seed = abs(crc32(Str::lower($prompt).'|'.$variant)); $palettes = [ ['#07111a', '#102338', '#43d9bd', '#f5b96b'], ['#10131f', '#1b2940', '#8de2ff', '#ff7a59'], ['#0b1220', '#1e1730', '#9d8cff', '#43d9bd'], ['#120e19', '#27324b', '#f07cab', '#f5b96b'], ]; return $palettes[$seed % count($palettes)]; } private function drawBackground($image, int $width, int $height, array $palette): void { [$top, $bottom, $accentA, $accentB] = $palette; for ($y = 0; $y < $height; $y++) { $ratio = $height > 1 ? $y / ($height - 1) : 0; [$r, $g, $b] = $this->mixHex($top, $bottom, $ratio); imageline($image, 0, $y, $width, $y, imagecolorallocate($image, $r, $g, $b)); } $this->drawGlow($image, (int) ($width * 0.2), (int) ($height * 0.14), (int) ($width * 0.38), $accentA, 88); $this->drawGlow($image, (int) ($width * 0.82), (int) ($height * 0.18), (int) ($width * 0.3), $accentB, 72); $this->drawGlow($image, (int) ($width * 0.76), (int) ($height * 0.78), (int) ($width * 0.35), '#ffffff', 24); } private function drawGlassPanels($image, int $width, int $height, array $palette): void { $stroke = imagecolorallocatealpha($image, 255, 255, 255, 90); $glass = imagecolorallocatealpha($image, 255, 255, 255, 112); $accent = $this->allocateHex($image, $palette[2], 45); $soft = imagecolorallocatealpha($image, 255, 255, 255, 118); $this->roundedRectangle($image, 72, 96, $width - 72, $height - 136, 42, $glass, true); $this->roundedRectangle($image, 72, 96, $width - 72, $height - 136, 42, $stroke, false); $this->roundedRectangle($image, 118, 162, $width - 118, (int) ($height * 0.55), 36, $soft, true); $this->roundedRectangle($image, 118, 162, $width - 118, (int) ($height * 0.55), 36, $stroke, false); $this->roundedRectangle($image, 118, (int) ($height * 0.64), (int) ($width * 0.7), (int) ($height * 0.73), 28, $accent, true); $this->roundedRectangle($image, 118, (int) ($height * 0.78), (int) ($width * 0.58), (int) ($height * 0.845), 22, $soft, true); imagefilledellipse( $image, (int) ($width * 0.24), (int) ($height * 0.34), (int) ($width * 0.22), (int) ($width * 0.22), $this->allocateHex($image, $palette[3], 28) ); } private function drawHeadline($image, int $width, int $height, string $prompt): void { $textColor = imagecolorallocate($image, 246, 251, 255); $muted = imagecolorallocate($image, 194, 207, 225); $fontBold = $this->resolveFont([ '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', '/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf', ]); $fontRegular = $this->resolveFont([ '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', '/usr/share/fonts/truetype/liberation2/LiberationSans-Regular.ttf', ]); $title = Str::title(Str::limit(trim($prompt), 90, '')); $subtitle = 'VERTICAL STORY ASSET 9:16'; if ($fontBold !== null && $fontRegular !== null && function_exists('imagettftext')) { $lines = $this->wrapText($title, $fontBold, 64, $width - 260, 4); $y = (int) ($height * 0.64); foreach ($lines as $line) { imagettftext($image, 64, 0, 120, $y, $textColor, $fontBold, $line); $y += 82; } imagettftext($image, 28, 0, 120, $height - 150, $muted, $fontRegular, $subtitle); imagettftext($image, 20, 0, 120, $height - 112, $muted, $fontRegular, 'Generated locally as premium fallback media for social automation.'); return; } imagestring($image, 5, 120, (int) ($height * 0.65), Str::limit($title, 36, ''), $textColor); imagestring($image, 3, 120, $height - 140, $subtitle, $muted); } private function wrapText(string $text, string $font, int $size, int $maxWidth, int $maxLines): array { $words = preg_split('/\s+/', trim($text)) ?: []; $lines = []; $current = ''; $hasFreeType = function_exists('imagettfbbox'); foreach ($words as $word) { $candidate = trim($current.' '.$word); if ($hasFreeType) { $box = imagettfbbox($size, 0, $font, $candidate); $lineWidth = is_array($box) ? abs($box[2] - $box[0]) : 0; } else { $lineWidth = (int) (strlen($candidate) * $size * 0.55); } if ($current !== '' && $lineWidth > $maxWidth) { $lines[] = $current; $current = $word; } else { $current = $candidate; } if (count($lines) >= $maxLines - 1) { break; } } if ($current !== '') { $remaining = array_slice($words, count(explode(' ', trim(implode(' ', $lines).' '.$current)))); if ($remaining !== []) { $current = trim($current.' '.implode(' ', $remaining)); } $lines[] = Str::limit($current, 42, '...'); } return array_slice($lines, 0, $maxLines); } private function roundedRectangle($image, int $x1, int $y1, int $x2, int $y2, int $radius, int $color, bool $filled): void { $diameter = $radius * 2; if ($filled) { imagefilledrectangle($image, $x1 + $radius, $y1, $x2 - $radius, $y2, $color); imagefilledrectangle($image, $x1, $y1 + $radius, $x2, $y2 - $radius, $color); imagefilledellipse($image, $x1 + $radius, $y1 + $radius, $diameter, $diameter, $color); imagefilledellipse($image, $x2 - $radius, $y1 + $radius, $diameter, $diameter, $color); imagefilledellipse($image, $x1 + $radius, $y2 - $radius, $diameter, $diameter, $color); imagefilledellipse($image, $x2 - $radius, $y2 - $radius, $diameter, $diameter, $color); return; } imageline($image, $x1 + $radius, $y1, $x2 - $radius, $y1, $color); imageline($image, $x1 + $radius, $y2, $x2 - $radius, $y2, $color); imageline($image, $x1, $y1 + $radius, $x1, $y2 - $radius, $color); imageline($image, $x2, $y1 + $radius, $x2, $y2 - $radius, $color); imagearc($image, $x1 + $radius, $y1 + $radius, $diameter, $diameter, 180, 270, $color); imagearc($image, $x2 - $radius, $y1 + $radius, $diameter, $diameter, 270, 360, $color); imagearc($image, $x1 + $radius, $y2 - $radius, $diameter, $diameter, 90, 180, $color); imagearc($image, $x2 - $radius, $y2 - $radius, $diameter, $diameter, 0, 90, $color); } private function drawGlow($image, int $centerX, int $centerY, int $radius, string $hex, int $maxAlpha): void { for ($i = $radius; $i > 0; $i -= max(6, (int) ($radius / 12))) { $alpha = max(0, min(127, (int) ($maxAlpha + (($radius - $i) / max(1, $radius)) * 34))); imagefilledellipse( $image, $centerX, $centerY, $i * 2, $i * 2, $this->allocateHex($image, $hex, $alpha) ); } } private function mixHex(string $from, string $to, float $ratio): array { $ratio = max(0, min(1, $ratio)); [$fr, $fg, $fb] = $this->hexToRgb($from); [$tr, $tg, $tb] = $this->hexToRgb($to); return [ (int) round($fr + (($tr - $fr) * $ratio)), (int) round($fg + (($tg - $fg) * $ratio)), (int) round($fb + (($tb - $fb) * $ratio)), ]; } private function allocateHex($image, string $hex, int $alpha = 0): int { [$r, $g, $b] = $this->hexToRgb($hex); return imagecolorallocatealpha($image, $r, $g, $b, max(0, min(127, $alpha))); } private function hexToRgb(string $hex): array { $value = ltrim($hex, '#'); if (strlen($value) === 3) { $value = preg_replace('/(.)/', '$1$1', $value) ?? $value; } return [ hexdec(substr($value, 0, 2)), hexdec(substr($value, 2, 2)), hexdec(substr($value, 4, 2)), ]; } private function resolveFont(array $candidates): ?string { foreach ($candidates as $candidate) { if (is_file($candidate)) { return $candidate; } } return null; } }