|
2 | 2 |
|
3 | 3 | namespace App\Services; |
4 | 4 |
|
| 5 | +use Exception; |
| 6 | +use Illuminate\Support\Facades\Log; |
5 | 7 | use Illuminate\Support\Facades\Storage; |
| 8 | +use Illuminate\Support\Str; |
6 | 9 | use Intervention\Image\Drivers\Gd\Driver as GdDriver; |
| 10 | +use Intervention\Image\Encoders\WebpEncoder; |
7 | 11 | use Intervention\Image\ImageManager; |
8 | 12 | use Intervention\Image\Typography\FontFactory; |
9 | 13 |
|
10 | | - |
11 | 14 | class AvatarService |
12 | 15 | { |
13 | | - final public function generateInitialsAvatar(string $firstName, string $lastName, string $userId, int $size = 200): string |
14 | | - { |
15 | | - $firstInitial = mb_substr($firstName, 0, 1); |
16 | | - $lastInitial = !empty($lastName) ? mb_substr($lastName, 0, 1) : ''; |
17 | | - $initials = mb_strtoupper($firstInitial . $lastInitial); |
18 | | - |
19 | | - $manager = new ImageManager(new GdDriver()); |
20 | | - |
21 | | - $hash = md5($userId); |
22 | | - $hue = hexdec(substr($hash, 0, 2)) % 360; |
23 | | - $backgroundColor = $this->hsvToRgb($hue, 0.7, 0.9); |
24 | | - |
25 | | - $image = $manager->create($size, $size)->fill($backgroundColor); |
26 | | - |
27 | | - $image->text($initials, $size / 2, $size / 2, function (FontFactory $font) use ($size) { |
28 | | - $font->file(public_path('fonts/poppins.ttf')); |
29 | | - $font->size($size * 0.4); |
30 | | - $font->color('#ffffff'); |
31 | | - $font->align('center'); |
32 | | - $font->valign('middle'); |
33 | | - }); |
34 | | - |
35 | | - $path = 'profile_pictures/initial_' . $userId . '.png'; |
36 | | - |
37 | | - $encodedImage = $image->toPng()->toString(); |
| 16 | + private const AVATAR_SIZE = 200; |
| 17 | + private const FONT_SIZE_RATIO = 0.45; |
| 18 | + private const IMAGE_QUALITY = 85; |
38 | 19 |
|
39 | | - Storage::disk('public')->put($path, $encodedImage); |
| 20 | + final public function generateInitialsAvatar(string $firstName, string $lastName = '', string $userId): string |
| 21 | + { |
| 22 | + try { |
| 23 | + $firstInitial = !empty($firstName) ? mb_strtoupper(mb_substr($firstName, 0, 1, 'UTF-8')) : ''; |
| 24 | + $lastInitial = !empty($lastName) ? mb_strtoupper(mb_substr($lastName, 0, 1, 'UTF-8')) : ''; |
| 25 | + $initials = $firstInitial . $lastInitial; |
| 26 | + |
| 27 | + if (empty(trim($initials))) { |
| 28 | + $initials = '?'; |
| 29 | + } |
| 30 | + |
| 31 | + $fontPath = storage_path('app/fonts/NotoSans-Regular.ttf'); |
| 32 | + if (!file_exists($fontPath)) { |
| 33 | + Log::error('AvatarService: Font not found at path: ' . $fontPath); |
| 34 | + return 'images/avatars/default.png'; |
| 35 | + } |
| 36 | + |
| 37 | + $manager = new ImageManager(new GdDriver()); |
| 38 | + $image = $manager->create(self::AVATAR_SIZE, self::AVATAR_SIZE); |
| 39 | + |
| 40 | + $backgroundColor = $this->generateBackgroundColor($userId); |
| 41 | + $image->fill($backgroundColor); |
| 42 | + |
| 43 | + $image->text($initials, self::AVATAR_SIZE / 2, self::AVATAR_SIZE / 2, function (FontFactory $font) use ($fontPath) { |
| 44 | + $font->file($fontPath); |
| 45 | + $font->size(self::AVATAR_SIZE * self::FONT_SIZE_RATIO); |
| 46 | + $font->color('#FFFFFF'); |
| 47 | + $font->align('center'); |
| 48 | + $font->valign('middle'); |
| 49 | + }); |
| 50 | + |
| 51 | + $path = 'profile_pictures/initials_' . $userId . '_' . Str::random(5) . '.webp'; |
| 52 | + $encodedImage = $image->encode(new WebpEncoder(quality: self::IMAGE_QUALITY)); |
| 53 | + Storage::disk('public')->put($path, $encodedImage); |
| 54 | + |
| 55 | + return $path; |
| 56 | + |
| 57 | + } catch (Exception $e) { |
| 58 | + Log::error('Failed to generate initials avatar for user ' . $userId, [ |
| 59 | + 'error' => $e->getMessage(), |
| 60 | + 'trace' => $e->getTraceAsString(), |
| 61 | + ]); |
| 62 | + return 'images/avatars/default.png'; |
| 63 | + } |
| 64 | + } |
40 | 65 |
|
41 | | - return $path; |
| 66 | + private function generateBackgroundColor(string $seed): string |
| 67 | + { |
| 68 | + $hash = crc32($seed); |
| 69 | + $hue = $hash % 360; |
| 70 | + $saturation = 0.5; |
| 71 | + $value = 0.8; |
| 72 | + return $this->hsvToRgbString($hue, $saturation, $value); |
42 | 73 | } |
43 | 74 |
|
44 | | - private function hsvToRgb(float $h, float $s, float $v): string |
| 75 | + private function hsvToRgbString(float $h, float $s, float $v): string |
45 | 76 | { |
46 | 77 | $h_i = floor($h / 60) % 6; |
47 | 78 | $f = $h / 60 - $h_i; |
48 | 79 | $p = $v * (1 - $s); |
49 | 80 | $q = $v * (1 - $f * $s); |
50 | 81 | $t = $v * (1 - (1 - $f) * $s); |
51 | | - |
52 | | - $r_float = 0.0; |
53 | | - $g_float = 0.0; |
54 | | - $b_float = 0.0; |
55 | | - |
56 | 82 | switch ($h_i) { |
57 | 83 | case 0: |
58 | | - [$r_float, $g_float, $b_float] = [$v, $t, $p]; |
| 84 | + list($r, $g, $b) = [$v, $t, $p]; |
59 | 85 | break; |
60 | 86 | case 1: |
61 | | - [$r_float, $g_float, $b_float] = [$q, $v, $p]; |
| 87 | + list($r, $g, $b) = [$q, $v, $p]; |
62 | 88 | break; |
63 | 89 | case 2: |
64 | | - [$r_float, $g_float, $b_float] = [$p, $v, $t]; |
| 90 | + list($r, $g, $b) = [$p, $v, $t]; |
65 | 91 | break; |
66 | 92 | case 3: |
67 | | - [$r_float, $g_float, $b_float] = [$p, $q, $v]; |
| 93 | + list($r, $g, $b) = [$p, $q, $v]; |
68 | 94 | break; |
69 | 95 | case 4: |
70 | | - [$r_float, $g_float, $b_float] = [$t, $p, $v]; |
| 96 | + list($r, $g, $b) = [$t, $p, $v]; |
71 | 97 | break; |
72 | | - case 5: |
73 | 98 | default: |
74 | | - [$r_float, $g_float, $b_float] = [$v, $p, $q]; |
| 99 | + list($r, $g, $b) = [$v, $p, $q]; |
75 | 100 | break; |
76 | 101 | } |
77 | | - |
78 | | - $r = round($r_float * 255); |
79 | | - $g = round($g_float * 255); |
80 | | - $b = round($b_float * 255); |
81 | | - |
82 | | - return "rgba($r, $g, $b, 1)"; |
| 102 | + return sprintf("#%02x%02x%02x", round($r * 255), round($g * 255), round($b * 255)); |
83 | 103 | } |
84 | 104 | } |
0 commit comments