format('Y/m'), $article->id, substr(sha1($article->title.'|'.microtime(true)), 0, 10), ); $binary = $this->renderJpeg($article); Storage::disk($disk)->put($relative, $binary, ['visibility' => 'public']); Attachment::query()->create([ 'article_id' => $article->id, 'disk' => $disk, 'path' => $relative, 'filename' => basename($relative), 'mime' => 'image/jpeg', 'size' => strlen($binary), 'checksum' => hash('sha256', $binary), 'visibility' => Attachment::VISIBILITY_PUBLIC, 'synced_at' => now(), ]); return [ 'disk' => $disk, 'path' => $relative, 'source' => ArticleCoverService::SOURCE_GENERATED, ]; } protected function renderJpeg(Article $article): string { $manager = new ImageManager(new Driver); $image = $manager->create(1200, 630)->fill('#0f3d4c'); // Soft bands for atmosphere (avoid a single flat fill). $image->drawRectangle(0, 0, function ($rect): void { $rect->size(1200, 210)->background('#123f4f'); }); $image->drawRectangle(0, 420, function ($rect): void { $rect->size(1200, 210)->background('#16384a'); }); $image->drawRectangle(0, 0, function ($rect): void { $rect->size(1200, 12)->background('#2bb0a6'); }); $site = 'LaraBlog'; try { $site = (string) (app(GeneralSettings::class)->site_name ?: $site); } catch (\Throwable) { // } $title = $this->wrapText((string) $article->title, 18, 3); $summary = trim((string) ($article->ai_summary ?: $article->description ?: '')); if ($summary !== '') { $summary = $this->wrapText($summary, 32, 2); } $font = $this->fontFile(); if ($font !== null) { $image->text($site, 72, 110, function ($f) use ($font): void { $f->filename($font); $f->size(28); $f->color('#8fd9d2'); }); $image->text($title, 72, 220, function ($f) use ($font): void { $f->filename($font); $f->size(56); $f->color('#f4f7f8'); $f->lineHeight(1.25); }); if ($summary !== '') { $image->text($summary, 72, 430, function ($f) use ($font): void { $f->filename($font); $f->size(26); $f->color('#c5d4db'); $f->lineHeight(1.35); }); } } $encoded = $image->toJpeg(85)->toString(); if (! is_string($encoded) || $encoded === '') { throw new RuntimeException('Failed to encode generated cover JPEG.'); } return $encoded; } protected function wrapText(string $text, int $maxChars, int $maxLines): string { $text = trim(preg_replace('/\s+/u', ' ', $text) ?? ''); if ($text === '') { return ''; } $lines = []; while ($text !== '' && count($lines) < $maxLines) { if (mb_strlen($text) <= $maxChars) { $lines[] = $text; break; } $chunk = mb_substr($text, 0, $maxChars); $lines[] = $chunk; $text = ltrim(mb_substr($text, $maxChars)); } if ($text !== '' && $lines !== []) { $last = $lines[array_key_last($lines)]; $lines[array_key_last($lines)] = mb_substr($last, 0, max(1, $maxChars - 1)).'…'; } return implode("\n", $lines); } protected function fontFile(): ?string { $configured = config('larablog.cover_font'); $candidates = array_filter([ is_string($configured) && $configured !== '' ? $configured : null, resource_path('fonts/Cover.ttf'), resource_path('fonts/NotoSansSC-Regular.otf'), '/System/Library/Fonts/Supplemental/Arial Unicode.ttf', '/Library/Fonts/Arial Unicode.ttf', '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', ]); foreach ($candidates as $path) { if (is_string($path) && is_file($path)) { return $path; } } return null; } }