├── fonts ├── rockwell.ttf └── OpenSans-Bold.ttf ├── src ├── Generator │ ├── GeneratorInterface.php │ └── DefaultGenerator.php ├── Facade.php ├── Concerns │ ├── AttributeGetter.php │ ├── AttributeSetter.php │ ├── ImageExport.php │ └── StorageOptimization.php ├── LumenServiceProvider.php ├── ServiceProvider.php ├── HDAvatar.php ├── HDAvatarResponse.php └── Avatar.php ├── CHANGELOG.md ├── LICENSE.md ├── composer.json ├── config ├── config.php ├── config-hd.php └── hd-avatar.php ├── examples └── hd-avatar-examples.php └── README.md /fonts/rockwell.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravolt/avatar/HEAD/fonts/rockwell.ttf -------------------------------------------------------------------------------- /fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravolt/avatar/HEAD/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/Generator/GeneratorInterface.php: -------------------------------------------------------------------------------- 1 | $key; 12 | } 13 | 14 | /** 15 | * Get background color 16 | */ 17 | public function getBackground(): string 18 | { 19 | return $this->background; 20 | } 21 | 22 | /** 23 | * Get foreground color 24 | */ 25 | public function getForeground(): string 26 | { 27 | return $this->foreground; 28 | } 29 | 30 | /** 31 | * Get shape 32 | */ 33 | public function getShape(): string 34 | { 35 | return $this->shape; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.0.0 4 | - Change default image driver to `gd`. 5 | - Add public method `getAttribute($attribute)` 6 | - Add support for Laravel 8 7 | - Drop support for Laravel 5.6, 5.7, and 5.8 8 | - Drop support for PHP 7.1 and 7.2 9 | 10 | ## 3.0.0 11 | 12 | - Implement themes [#73](https://github.com/laravolt/avatar/issues/73) 13 | - Add `setTheme()` 14 | - Add support for Laravel 6 15 | - Drop support for Laravel 5.2, 5.3, 5.4, and 5.5 16 | - Drop support for PHP 7.0 17 | 18 | ## 2.2.1 19 | 20 | * Add `setChars()` [#75](https://github.com/laravolt/avatar/pull/75) 21 | 22 | ## 2.2.0 23 | * Add gravatar support 24 | 25 | ## 2.1.0 26 | * Add functionality to set custom font-family for SVG 27 | 28 | ## 2.0.0 29 | * support plain PHP project 30 | * remove support for php 5.x 31 | * remove font folder publishing (Laravel version) 32 | * remove `setFontFolder` 33 | * `fonts` config only accept full path 34 | * add `toSvg()` 35 | * move config file from `config/avatar.php` to `config/laravolt/avatar.php` 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Laravolt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravolt/avatar", 3 | "description": "Turn name, email, and any other string into initial-based avatar or gravatar.", 4 | "keywords": [ 5 | "laravel", 6 | "laravolt", 7 | "avatar", 8 | "gravatar" 9 | ], 10 | "homepage": "https://github.com/laravolt/avatar", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Bayu Hendra Winata", 15 | "email": "uyab.exe@gmail.com", 16 | "homepage": "https://laravolt.dev", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=8.2", 22 | "illuminate/support": "^10.0|^11.0|^12.0", 23 | "illuminate/cache": "^10.0|^11.0|^12.0", 24 | "intervention/image": "^3.4" 25 | }, 26 | "suggest": { 27 | "ext-gd": "Needed to support image manipulation", 28 | "ext-imagick": "Needed to support image manipulation, better than GD" 29 | }, 30 | "require-dev": { 31 | "roave/security-advisories": "dev-latest", 32 | "phpunit/phpunit": "^11.5.3", 33 | "mockery/mockery": "^1.6.7", 34 | "php-coveralls/php-coveralls": "^2.1", 35 | "pestphp/pest": "^2.34|^3.7", 36 | "pestphp/pest-plugin-type-coverage": "^2.8|^3.3" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Laravolt\\Avatar\\": "src" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Laravolt\\Avatar\\Test\\": "tests" 46 | } 47 | }, 48 | "scripts": { 49 | "test": "phpunit" 50 | }, 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "6.0-dev" 54 | }, 55 | "laravel": { 56 | "providers": [ 57 | "Laravolt\\Avatar\\ServiceProvider" 58 | ], 59 | "aliases": { 60 | "Avatar": "Laravolt\\Avatar\\Facade" 61 | } 62 | } 63 | }, 64 | "config": { 65 | "allow-plugins": { 66 | "pestphp/pest-plugin": true 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/LumenServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('avatar', function (Application $app) { 25 | $config = $app->make('config'); 26 | $cache = $app->make('cache.store'); 27 | 28 | $avatar = new Avatar($config->get('laravolt.avatar'), $cache); 29 | $avatar->setGenerator($app['avatar.generator']); 30 | 31 | return $avatar; 32 | }); 33 | 34 | $this->app->bind('avatar.generator', function (Application $app) { 35 | $config = $app->make('config'); 36 | $class = $config->get('laravolt.avatar.generator'); 37 | 38 | return new $class; 39 | }); 40 | } 41 | 42 | public function provides() 43 | { 44 | return ['avatar', 'avatar.generator']; 45 | } 46 | 47 | /** 48 | * Application is booting. 49 | * 50 | * @return void 51 | */ 52 | public function boot() 53 | { 54 | $this->registerConfigurations(); 55 | } 56 | 57 | /** 58 | * Register the package configurations. 59 | * 60 | * @return void 61 | */ 62 | protected function registerConfigurations() 63 | { 64 | $this->mergeConfigFrom($this->packagePath('config/config.php'), 'laravolt.avatar'); 65 | $this->publishes([$this->packagePath('config/config.php') => config_path('laravolt/avatar.php')], 'config'); 66 | } 67 | 68 | /** 69 | * Loads a path relative to the package base directory. 70 | * 71 | * @param string $path 72 | * @return string 73 | */ 74 | protected function packagePath($path = '') 75 | { 76 | return sprintf('%s/../%s', __DIR__, $path); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('avatar', function (Application $app) { 25 | $cache = $app->make('cache.store'); 26 | $config = $app['config']->get('laravolt.avatar', []); 27 | 28 | $avatar = new Avatar($config, $cache); 29 | $avatar->setGenerator($app['avatar.generator']); 30 | 31 | return $avatar; 32 | }); 33 | 34 | $this->app->bind('avatar.generator', function (Application $app) { 35 | $config = $app->make('config'); 36 | $class = $config->get('laravolt.avatar.generator'); 37 | 38 | return new $class; 39 | }); 40 | } 41 | 42 | public function provides() 43 | { 44 | return ['avatar', 'avatar.generator']; 45 | } 46 | 47 | /** 48 | * Application is booting. 49 | * 50 | * @return void 51 | */ 52 | public function boot() 53 | { 54 | $this->registerConfigurations(); 55 | } 56 | 57 | /** 58 | * Register the package configurations. 59 | * 60 | * @return void 61 | */ 62 | protected function registerConfigurations() 63 | { 64 | $this->mergeConfigFrom($this->packagePath('config/config.php'), 'laravolt.avatar'); 65 | $this->publishes([$this->packagePath('config/config.php') => config_path('laravolt/avatar.php')], 'config'); 66 | $this->mergeConfigFrom($this->packagePath('config/config-hd.php'), 'laravolt.avatar.hd'); 67 | $this->publishes([$this->packagePath('config/config-hd.php') => config_path('laravolt/avatar-hd.php')], 'config'); 68 | } 69 | 70 | /** 71 | * Loads a path relative to the package base directory. 72 | * 73 | * @param string $path 74 | * @return string 75 | */ 76 | protected function packagePath($path = '') 77 | { 78 | return sprintf('%s/../%s', __DIR__, $path); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Concerns/AttributeSetter.php: -------------------------------------------------------------------------------- 1 | themes)) { 13 | return $this; 14 | } 15 | 16 | $this->theme = $theme; 17 | } 18 | 19 | $this->initTheme(); 20 | 21 | return $this; 22 | } 23 | 24 | public function setBackground($hex): static 25 | { 26 | $this->background = $hex; 27 | 28 | return $this; 29 | } 30 | 31 | public function setForeground($hex): static 32 | { 33 | $this->foreground = $hex; 34 | 35 | return $this; 36 | } 37 | 38 | public function setDimension($width, $height = null): static 39 | { 40 | if (! $height) { 41 | $height = $width; 42 | } 43 | $this->width = $width; 44 | $this->height = $height; 45 | 46 | return $this; 47 | } 48 | 49 | public function setResponsive($responsive): static 50 | { 51 | $this->responsive = $responsive; 52 | 53 | return $this; 54 | } 55 | 56 | public function setFontSize($size): static 57 | { 58 | $this->fontSize = $size; 59 | 60 | return $this; 61 | } 62 | 63 | public function setFontFamily($font): static 64 | { 65 | $this->fontFamily = $font; 66 | 67 | return $this; 68 | } 69 | 70 | public function setBorder($size, $color, $radius = 0): static 71 | { 72 | $this->borderSize = $size; 73 | $this->borderColor = $color; 74 | $this->borderRadius = $radius; 75 | 76 | return $this; 77 | } 78 | 79 | public function setBorderRadius($radius): static 80 | { 81 | $this->borderRadius = $radius; 82 | 83 | return $this; 84 | } 85 | 86 | public function setShape($shape): static 87 | { 88 | $this->shape = $shape; 89 | 90 | return $this; 91 | } 92 | 93 | public function setChars($chars): static 94 | { 95 | $this->chars = $chars; 96 | 97 | return $this; 98 | } 99 | 100 | public function setFont($font): static 101 | { 102 | if (is_file($font)) { 103 | $this->font = $font; 104 | } 105 | 106 | return $this; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Generator/DefaultGenerator.php: -------------------------------------------------------------------------------- 1 | setName($name, $ascii); 15 | 16 | $words = new Collection(explode(' ', (string) $this->name)); 17 | 18 | // if name contains single word, use first N character 19 | if ($words->count() === 1) { 20 | $initial = $this->getInitialFromOneWord($words, $length); 21 | } else { 22 | $initial = $this->getInitialFromMultipleWords($words, $length); 23 | } 24 | 25 | if ($uppercase) { 26 | $initial = strtoupper($initial); 27 | } 28 | 29 | if ($rtl) { 30 | $initial = collect(mb_str_split($initial))->reverse()->implode(''); 31 | } 32 | 33 | return $initial; 34 | } 35 | 36 | protected function setName(?string $name, bool $ascii): void 37 | { 38 | if (is_array($name)) { 39 | throw new \InvalidArgumentException( 40 | 'Passed value cannot be an array' 41 | ); 42 | } 43 | 44 | if (is_object($name) && ! method_exists($name, '__toString')) { 45 | throw new \InvalidArgumentException( 46 | 'Passed object must have a __toString method' 47 | ); 48 | } 49 | 50 | if (filter_var($name, FILTER_VALIDATE_EMAIL)) { 51 | // turn bayu.hendra@gmail.com into "Bayu Hendra" 52 | $name = str_replace('.', ' ', Str::before($name, '@')); 53 | } 54 | 55 | if ($ascii) { 56 | $name = Str::ascii($name); 57 | } 58 | 59 | $this->name = $name; 60 | } 61 | 62 | protected function getInitialFromOneWord(Collection $words, int $length): string 63 | { 64 | $initial = (string) $words->first(); 65 | 66 | if (strlen((string) $this->name) >= $length) { 67 | $initial = Str::substr($this->name, 0, $length); 68 | } 69 | 70 | return $initial; 71 | } 72 | 73 | protected function getInitialFromMultipleWords(Collection $words, int $length): string 74 | { 75 | // otherwise, use initial char from each word 76 | $initials = new Collection; 77 | $words->each(function (string $word) use ($initials) { 78 | $initials->push(Str::substr($word, 0, 1)); 79 | }); 80 | 81 | return $this->selectInitialFromMultipleInitials($initials, $length); 82 | } 83 | 84 | protected function selectInitialFromMultipleInitials(Collection $initials, int $length): string 85 | { 86 | return $initials->slice(0, $length)->implode(''); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | env('IMAGE_DRIVER', 'gd'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Cache Configuration 25 | |-------------------------------------------------------------------------- 26 | | Control caching behavior for avatars 27 | | 28 | */ 29 | 'cache' => [ 30 | // Set to true to enable caching, false to disable 31 | 'enabled' => env('AVATAR_CACHE_ENABLED', true), 32 | 33 | // Cache prefix to avoid conflicts with other cached items 34 | 'key_prefix' => 'avatar_', 35 | 36 | // Cache duration in seconds 37 | // Set to null to cache forever, 0 to disable cache 38 | // Default: 86400 (24 hours) 39 | 'duration' => env('AVATAR_CACHE_DURATION', 86400), 40 | ], 41 | 42 | // Initial generator class 43 | 'generator' => \Laravolt\Avatar\Generator\DefaultGenerator::class, 44 | 45 | // Whether all characters supplied must be replaced with their closest ASCII counterparts 46 | 'ascii' => false, 47 | 48 | // Image shape: circle or square 49 | 'shape' => 'circle', 50 | 51 | // Image width, in pixel 52 | 'width' => 100, 53 | 54 | // Image height, in pixel 55 | 'height' => 100, 56 | 57 | // Responsive SVG, height and width attributes are not added when true 58 | 'responsive' => false, 59 | 60 | // Number of characters used as initials. If name consists of single word, the first N character will be used 61 | 'chars' => 2, 62 | 63 | // font size 64 | 'fontSize' => 48, 65 | 66 | // convert initial letter in uppercase 67 | 'uppercase' => false, 68 | 69 | // Right to Left (RTL) 70 | 'rtl' => false, 71 | 72 | // Fonts used to render text. 73 | // If contains more than one fonts, randomly selected based on name supplied 74 | 'fonts' => [__DIR__.'/../fonts/OpenSans-Bold.ttf', __DIR__.'/../fonts/rockwell.ttf'], 75 | 76 | // List of foreground colors to be used, randomly selected based on name supplied 77 | 'foregrounds' => [ 78 | '#FFFFFF', 79 | ], 80 | 81 | // List of background colors to be used, randomly selected based on name supplied 82 | 'backgrounds' => [ 83 | '#f44336', 84 | '#E91E63', 85 | '#9C27B0', 86 | '#673AB7', 87 | '#3F51B5', 88 | '#2196F3', 89 | '#03A9F4', 90 | '#00BCD4', 91 | '#009688', 92 | '#4CAF50', 93 | '#8BC34A', 94 | '#CDDC39', 95 | '#FFC107', 96 | '#FF9800', 97 | '#FF5722', 98 | ], 99 | 100 | 'border' => [ 101 | 'size' => 1, 102 | 103 | // border color, available value are: 104 | // 'foreground' (same as foreground color) 105 | // 'background' (same as background color) 106 | // or any valid hex ('#aabbcc') 107 | 'color' => 'background', 108 | 109 | // border radius, currently only work for SVG 110 | 'radius' => 0, 111 | ], 112 | 113 | // List of theme name to be used when rendering avatar 114 | // Possible values are: 115 | // 1. Theme name as string: 'colorful' 116 | // 2. Or array of string name: ['grayscale-light', 'grayscale-dark'] 117 | // 3. Or wildcard "*" to use all defined themes 118 | 'theme' => ['colorful'], 119 | 120 | // Predefined themes 121 | // Available theme attributes are: 122 | // shape, chars, backgrounds, foregrounds, fonts, fontSize, width, height, ascii, uppercase, and border. 123 | 'themes' => [ 124 | 'grayscale-light' => [ 125 | 'backgrounds' => ['#edf2f7', '#e2e8f0', '#cbd5e0'], 126 | 'foregrounds' => ['#a0aec0'], 127 | ], 128 | 'grayscale-dark' => [ 129 | 'backgrounds' => ['#2d3748', '#4a5568', '#718096'], 130 | 'foregrounds' => ['#e2e8f0'], 131 | ], 132 | 'colorful' => [ 133 | 'backgrounds' => [ 134 | '#f44336', 135 | '#E91E63', 136 | '#9C27B0', 137 | '#673AB7', 138 | '#3F51B5', 139 | '#2196F3', 140 | '#03A9F4', 141 | '#00BCD4', 142 | '#009688', 143 | '#4CAF50', 144 | '#8BC34A', 145 | '#CDDC39', 146 | '#FFC107', 147 | '#FF9800', 148 | '#FF5722', 149 | ], 150 | 'foregrounds' => ['#FFFFFF'], 151 | ], 152 | 'pastel' => [ 153 | 'backgrounds' => [ 154 | '#ef9a9a', 155 | '#F48FB1', 156 | '#CE93D8', 157 | '#B39DDB', 158 | '#9FA8DA', 159 | '#90CAF9', 160 | '#81D4FA', 161 | '#80DEEA', 162 | '#80CBC4', 163 | '#A5D6A7', 164 | '#E6EE9C', 165 | '#FFAB91', 166 | '#FFCCBC', 167 | '#D7CCC8', 168 | ], 169 | 'foregrounds' => [ 170 | '#FFF', 171 | ], 172 | ], 173 | ], 174 | ]; 175 | -------------------------------------------------------------------------------- /config/config-hd.php: -------------------------------------------------------------------------------- 1 | [ 22 | // Enable HD mode - when true, uses higher resolution settings 23 | 'enabled' => env('AVATAR_HD_ENABLED', true), 24 | 25 | // HD dimensions (default: 512x512, supports up to 2048x2048) 26 | 'width' => env('AVATAR_HD_WIDTH', 512), 27 | 'height' => env('AVATAR_HD_HEIGHT', 512), 28 | 29 | // HD font size (scales with dimensions) 30 | 'fontSize' => env('AVATAR_HD_FONT_SIZE', 192), 31 | 32 | // Export quality settings 33 | 'quality' => [ 34 | 'png' => env('AVATAR_HD_PNG_QUALITY', 95), 35 | 'jpg' => env('AVATAR_HD_JPG_QUALITY', 90), 36 | 'webp' => env('AVATAR_HD_WEBP_QUALITY', 85), 37 | ], 38 | 39 | // Anti-aliasing for smoother edges 40 | 'antialiasing' => env('AVATAR_HD_ANTIALIASING', true), 41 | 42 | // DPI for high-quality rendering 43 | 'dpi' => env('AVATAR_HD_DPI', 300), 44 | ], 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Export and Storage Settings 49 | |-------------------------------------------------------------------------- 50 | | Configuration for image export and storage optimization 51 | | 52 | */ 53 | 'export' => [ 54 | // Default export format 55 | 'format' => env('AVATAR_EXPORT_FORMAT', 'png'), // png, jpg, webp 56 | 57 | // Export path (relative to storage/app) 58 | 'path' => env('AVATAR_EXPORT_PATH', 'avatars'), 59 | 60 | // Filename pattern: {name}, {initials}, {hash}, {timestamp} 61 | 'filename_pattern' => env('AVATAR_EXPORT_FILENAME', '{hash}_{timestamp}.{format}'), 62 | 63 | // Enable multiple format export 64 | 'multiple_formats' => env('AVATAR_EXPORT_MULTIPLE', false), 65 | 66 | // Progressive JPEG for better loading 67 | 'progressive_jpeg' => env('AVATAR_PROGRESSIVE_JPEG', true), 68 | 69 | // WebP lossless compression 70 | 'webp_lossless' => env('AVATAR_WEBP_LOSSLESS', false), 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Performance and Caching 76 | |-------------------------------------------------------------------------- 77 | | Enhanced caching and performance settings for HD avatars 78 | | 79 | */ 80 | 'performance' => [ 81 | // Enable file-based caching in addition to memory cache 82 | 'file_cache' => env('AVATAR_FILE_CACHE', true), 83 | 84 | // Cache different sizes separately 85 | 'size_based_cache' => env('AVATAR_SIZE_CACHE', true), 86 | 87 | // Preload fonts for better performance 88 | 'preload_fonts' => env('AVATAR_PRELOAD_FONTS', true), 89 | 90 | // Background processing for large batches 91 | 'background_processing' => env('AVATAR_BACKGROUND_PROCESSING', false), 92 | 93 | // Lazy loading support 94 | 'lazy_loading' => env('AVATAR_LAZY_LOADING', true), 95 | 96 | // Compression levels 97 | 'compression' => [ 98 | 'png' => env('AVATAR_PNG_COMPRESSION', 6), // 0-9 99 | 'webp' => env('AVATAR_WEBP_COMPRESSION', 80), // 0-100 100 | ], 101 | ], 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Storage Management 106 | |-------------------------------------------------------------------------- 107 | | Configuration for storage optimization and cleanup 108 | | 109 | */ 110 | 'storage' => [ 111 | // Automatic cleanup of old files 112 | 'auto_cleanup' => env('AVATAR_AUTO_CLEANUP', true), 113 | 114 | // Maximum age for cached files (in days) 115 | 'max_age_days' => env('AVATAR_MAX_AGE_DAYS', 30), 116 | 117 | // Maximum storage size (in MB, 0 = unlimited) 118 | 'max_storage_mb' => env('AVATAR_MAX_STORAGE_MB', 500), 119 | 120 | // Storage driver (local, s3, etc.) 121 | 'disk' => env('AVATAR_STORAGE_DISK', 'local'), 122 | 123 | // CDN URL for serving images 124 | 'cdn_url' => env('AVATAR_CDN_URL', null), 125 | 126 | // Enable storage metrics 127 | 'metrics' => env('AVATAR_STORAGE_METRICS', false), 128 | ], 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | HD Themes 133 | |-------------------------------------------------------------------------- 134 | | Enhanced themes with HD-specific optimizations 135 | | 136 | */ 137 | 'hd_themes' => [ 138 | 'ultra-hd' => [ 139 | 'width' => 1024, 140 | 'height' => 1024, 141 | 'fontSize' => 384, 142 | 'backgrounds' => [ 143 | '#667eea', '#764ba2', '#f093fb', '#f5576c', 144 | '#4facfe', '#00f2fe', '#43e97b', '#38f9d7', 145 | '#ffecd2', '#fcb69f', '#a8edea', '#fed6e3', 146 | ], 147 | 'foregrounds' => ['#FFFFFF'], 148 | 'border' => [ 149 | 'size' => 4, 150 | 'color' => 'foreground', 151 | 'radius' => 8, 152 | ], 153 | ], 154 | 'retina' => [ 155 | 'width' => 512, 156 | 'height' => 512, 157 | 'fontSize' => 192, 158 | 'backgrounds' => [ 159 | '#667eea', '#764ba2', '#f093fb', '#f5576c', 160 | '#4facfe', '#00f2fe', '#43e97b', '#38f9d7', 161 | ], 162 | 'foregrounds' => ['#FFFFFF'], 163 | ], 164 | 'material-hd' => [ 165 | 'width' => 384, 166 | 'height' => 384, 167 | 'fontSize' => 144, 168 | 'shape' => 'circle', 169 | 'backgrounds' => [ 170 | '#1976D2', '#388E3C', '#F57C00', '#7B1FA2', 171 | '#5D4037', '#455A64', '#E64A19', '#00796B', 172 | ], 173 | 'foregrounds' => ['#FFFFFF'], 174 | 'border' => [ 175 | 'size' => 2, 176 | 'color' => 'background', 177 | 'radius' => 0, 178 | ], 179 | ], 180 | ], 181 | 182 | /* 183 | |-------------------------------------------------------------------------- 184 | | Responsive Sizes 185 | |-------------------------------------------------------------------------- 186 | | Predefined sizes for responsive avatar generation 187 | | 188 | */ 189 | 'responsive_sizes' => [ 190 | 'thumbnail' => ['width' => 64, 'height' => 64, 'fontSize' => 24], 191 | 'small' => ['width' => 128, 'height' => 128, 'fontSize' => 48], 192 | 'medium' => ['width' => 256, 'height' => 256, 'fontSize' => 96], 193 | 'large' => ['width' => 512, 'height' => 512, 'fontSize' => 192], 194 | 'xl' => ['width' => 768, 'height' => 768, 'fontSize' => 288], 195 | 'xxl' => ['width' => 1024, 'height' => 1024, 'fontSize' => 384], 196 | ], 197 | 198 | /* 199 | |-------------------------------------------------------------------------- 200 | | Advanced Features 201 | |-------------------------------------------------------------------------- 202 | | Additional HD avatar features 203 | | 204 | */ 205 | 'features' => [ 206 | // Generate avatar sprites for animations 207 | 'sprites' => env('AVATAR_SPRITES', false), 208 | 209 | // Generate avatar variations (different colors/styles) 210 | 'variations' => env('AVATAR_VARIATIONS', false), 211 | 212 | // Generate blur placeholder images 213 | 'placeholders' => env('AVATAR_PLACEHOLDERS', true), 214 | 215 | // Generate different aspect ratios 216 | 'aspect_ratios' => env('AVATAR_ASPECT_RATIOS', false), 217 | 218 | // Watermarking support 219 | 'watermark' => [ 220 | 'enabled' => env('AVATAR_WATERMARK', false), 221 | 'text' => env('AVATAR_WATERMARK_TEXT', ''), 222 | 'opacity' => env('AVATAR_WATERMARK_OPACITY', 0.3), 223 | 'position' => env('AVATAR_WATERMARK_POSITION', 'bottom-right'), 224 | ], 225 | ], 226 | 227 | /* 228 | |-------------------------------------------------------------------------- 229 | | API Settings 230 | |-------------------------------------------------------------------------- 231 | | Configuration for avatar API endpoints 232 | | 233 | */ 234 | 'api' => [ 235 | // Enable avatar API endpoints 236 | 'enabled' => env('AVATAR_API_ENABLED', true), 237 | 238 | // Rate limiting (requests per minute) 239 | 'rate_limit' => env('AVATAR_API_RATE_LIMIT', 60), 240 | 241 | // Enable CORS for API endpoints 242 | 'cors' => env('AVATAR_API_CORS', true), 243 | 244 | // API authentication 245 | 'auth' => env('AVATAR_API_AUTH', false), 246 | 247 | // Response headers 248 | 'headers' => [ 249 | 'Cache-Control' => 'public, max-age=31536000', // 1 year 250 | 'Expires' => gmdate('D, d M Y H:i:s', time() + 31536000).' GMT', 251 | ], 252 | ], 253 | ]; 254 | -------------------------------------------------------------------------------- /config/hd-avatar.php: -------------------------------------------------------------------------------- 1 | [ 22 | // Enable HD mode - when true, uses higher resolution settings 23 | 'enabled' => env('AVATAR_HD_ENABLED', true), 24 | 25 | // HD dimensions (default: 512x512, supports up to 2048x2048) 26 | 'width' => env('AVATAR_HD_WIDTH', 512), 27 | 'height' => env('AVATAR_HD_HEIGHT', 512), 28 | 29 | // HD font size (scales with dimensions) 30 | 'fontSize' => env('AVATAR_HD_FONT_SIZE', 192), 31 | 32 | // Export quality settings 33 | 'quality' => [ 34 | 'png' => env('AVATAR_HD_PNG_QUALITY', 95), 35 | 'jpg' => env('AVATAR_HD_JPG_QUALITY', 90), 36 | 'webp' => env('AVATAR_HD_WEBP_QUALITY', 85), 37 | ], 38 | 39 | // Anti-aliasing for smoother edges 40 | 'antialiasing' => env('AVATAR_HD_ANTIALIASING', true), 41 | 42 | // DPI for high-quality rendering 43 | 'dpi' => env('AVATAR_HD_DPI', 300), 44 | ], 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Export and Storage Settings 49 | |-------------------------------------------------------------------------- 50 | | Configuration for image export and storage optimization 51 | | 52 | */ 53 | 'export' => [ 54 | // Default export format 55 | 'format' => env('AVATAR_EXPORT_FORMAT', 'png'), // png, jpg, webp 56 | 57 | // Export path (relative to storage/app) 58 | 'path' => env('AVATAR_EXPORT_PATH', 'avatars'), 59 | 60 | // Filename pattern: {name}, {initials}, {hash}, {timestamp} 61 | 'filename_pattern' => env('AVATAR_EXPORT_FILENAME', '{hash}_{timestamp}.{format}'), 62 | 63 | // Enable multiple format export 64 | 'multiple_formats' => env('AVATAR_EXPORT_MULTIPLE', false), 65 | 66 | // Progressive JPEG for better loading 67 | 'progressive_jpeg' => env('AVATAR_PROGRESSIVE_JPEG', true), 68 | 69 | // WebP lossless compression 70 | 'webp_lossless' => env('AVATAR_WEBP_LOSSLESS', false), 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Performance and Caching 76 | |-------------------------------------------------------------------------- 77 | | Enhanced caching and performance settings for HD avatars 78 | | 79 | */ 80 | 'performance' => [ 81 | // Enable file-based caching in addition to memory cache 82 | 'file_cache' => env('AVATAR_FILE_CACHE', true), 83 | 84 | // Cache different sizes separately 85 | 'size_based_cache' => env('AVATAR_SIZE_CACHE', true), 86 | 87 | // Preload fonts for better performance 88 | 'preload_fonts' => env('AVATAR_PRELOAD_FONTS', true), 89 | 90 | // Background processing for large batches 91 | 'background_processing' => env('AVATAR_BACKGROUND_PROCESSING', false), 92 | 93 | // Lazy loading support 94 | 'lazy_loading' => env('AVATAR_LAZY_LOADING', true), 95 | 96 | // Compression levels 97 | 'compression' => [ 98 | 'png' => env('AVATAR_PNG_COMPRESSION', 6), // 0-9 99 | 'webp' => env('AVATAR_WEBP_COMPRESSION', 80), // 0-100 100 | ], 101 | ], 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Storage Management 106 | |-------------------------------------------------------------------------- 107 | | Configuration for storage optimization and cleanup 108 | | 109 | */ 110 | 'storage' => [ 111 | // Automatic cleanup of old files 112 | 'auto_cleanup' => env('AVATAR_AUTO_CLEANUP', true), 113 | 114 | // Maximum age for cached files (in days) 115 | 'max_age_days' => env('AVATAR_MAX_AGE_DAYS', 30), 116 | 117 | // Maximum storage size (in MB, 0 = unlimited) 118 | 'max_storage_mb' => env('AVATAR_MAX_STORAGE_MB', 500), 119 | 120 | // Storage driver (local, s3, etc.) 121 | 'disk' => env('AVATAR_STORAGE_DISK', 'local'), 122 | 123 | // CDN URL for serving images 124 | 'cdn_url' => env('AVATAR_CDN_URL', null), 125 | 126 | // Enable storage metrics 127 | 'metrics' => env('AVATAR_STORAGE_METRICS', false), 128 | ], 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | HD Themes 133 | |-------------------------------------------------------------------------- 134 | | Enhanced themes with HD-specific optimizations 135 | | 136 | */ 137 | 'hd_themes' => [ 138 | 'ultra-hd' => [ 139 | 'width' => 1024, 140 | 'height' => 1024, 141 | 'fontSize' => 384, 142 | 'backgrounds' => [ 143 | '#667eea', '#764ba2', '#f093fb', '#f5576c', 144 | '#4facfe', '#00f2fe', '#43e97b', '#38f9d7', 145 | '#ffecd2', '#fcb69f', '#a8edea', '#fed6e3', 146 | ], 147 | 'foregrounds' => ['#FFFFFF'], 148 | 'border' => [ 149 | 'size' => 4, 150 | 'color' => 'foreground', 151 | 'radius' => 8, 152 | ], 153 | ], 154 | 'retina' => [ 155 | 'width' => 512, 156 | 'height' => 512, 157 | 'fontSize' => 192, 158 | 'backgrounds' => [ 159 | '#667eea', '#764ba2', '#f093fb', '#f5576c', 160 | '#4facfe', '#00f2fe', '#43e97b', '#38f9d7', 161 | ], 162 | 'foregrounds' => ['#FFFFFF'], 163 | ], 164 | 'material-hd' => [ 165 | 'width' => 384, 166 | 'height' => 384, 167 | 'fontSize' => 144, 168 | 'shape' => 'circle', 169 | 'backgrounds' => [ 170 | '#1976D2', '#388E3C', '#F57C00', '#7B1FA2', 171 | '#5D4037', '#455A64', '#E64A19', '#00796B', 172 | ], 173 | 'foregrounds' => ['#FFFFFF'], 174 | 'border' => [ 175 | 'size' => 2, 176 | 'color' => 'background', 177 | 'radius' => 0, 178 | ], 179 | ], 180 | ], 181 | 182 | /* 183 | |-------------------------------------------------------------------------- 184 | | Responsive Sizes 185 | |-------------------------------------------------------------------------- 186 | | Predefined sizes for responsive avatar generation 187 | | 188 | */ 189 | 'responsive_sizes' => [ 190 | 'thumbnail' => ['width' => 64, 'height' => 64, 'fontSize' => 24], 191 | 'small' => ['width' => 128, 'height' => 128, 'fontSize' => 48], 192 | 'medium' => ['width' => 256, 'height' => 256, 'fontSize' => 96], 193 | 'large' => ['width' => 512, 'height' => 512, 'fontSize' => 192], 194 | 'xl' => ['width' => 768, 'height' => 768, 'fontSize' => 288], 195 | 'xxl' => ['width' => 1024, 'height' => 1024, 'fontSize' => 384], 196 | ], 197 | 198 | /* 199 | |-------------------------------------------------------------------------- 200 | | Advanced Features 201 | |-------------------------------------------------------------------------- 202 | | Additional HD avatar features 203 | | 204 | */ 205 | 'features' => [ 206 | // Generate avatar sprites for animations 207 | 'sprites' => env('AVATAR_SPRITES', false), 208 | 209 | // Generate avatar variations (different colors/styles) 210 | 'variations' => env('AVATAR_VARIATIONS', false), 211 | 212 | // Generate blur placeholder images 213 | 'placeholders' => env('AVATAR_PLACEHOLDERS', true), 214 | 215 | // Generate different aspect ratios 216 | 'aspect_ratios' => env('AVATAR_ASPECT_RATIOS', false), 217 | 218 | // Watermarking support 219 | 'watermark' => [ 220 | 'enabled' => env('AVATAR_WATERMARK', false), 221 | 'text' => env('AVATAR_WATERMARK_TEXT', ''), 222 | 'opacity' => env('AVATAR_WATERMARK_OPACITY', 0.3), 223 | 'position' => env('AVATAR_WATERMARK_POSITION', 'bottom-right'), 224 | ], 225 | ], 226 | 227 | /* 228 | |-------------------------------------------------------------------------- 229 | | API Settings 230 | |-------------------------------------------------------------------------- 231 | | Configuration for avatar API endpoints 232 | | 233 | */ 234 | 'api' => [ 235 | // Enable avatar API endpoints 236 | 'enabled' => env('AVATAR_API_ENABLED', true), 237 | 238 | // Rate limiting (requests per minute) 239 | 'rate_limit' => env('AVATAR_API_RATE_LIMIT', 60), 240 | 241 | // Enable CORS for API endpoints 242 | 'cors' => env('AVATAR_API_CORS', true), 243 | 244 | // API authentication 245 | 'auth' => env('AVATAR_API_AUTH', false), 246 | 247 | // Response headers 248 | 'headers' => [ 249 | 'Cache-Control' => 'public, max-age=31536000', // 1 year 250 | 'Expires' => gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT', 251 | ], 252 | ], 253 | ]; 254 | -------------------------------------------------------------------------------- /examples/hd-avatar-examples.php: -------------------------------------------------------------------------------- 1 | createAndExport('John Doe', 'png'); 28 | echo 'Avatar URL: '.$result['url']."\n"; 29 | echo 'Avatar Hash: '.$result['metadata']['hash']."\n"; 30 | echo "Dimensions: {$result['metadata']['width']}x{$result['metadata']['height']}\n\n"; 31 | 32 | // ============================================================================= 33 | // Responsive Avatar Generation 34 | // ============================================================================= 35 | 36 | echo "=== Responsive Avatar Generation ===\n"; 37 | // Create avatar with multiple sizes 38 | $responsiveResult = $hdAvatar->createAndExport('Jane Smith', 'webp'); 39 | foreach ($responsiveResult['responsive_urls'] as $size => $url) { 40 | echo "Size {$size}: {$url}\n"; 41 | } 42 | echo "\n"; 43 | 44 | // ============================================================================= 45 | // Batch Avatar Generation 46 | // ============================================================================= 47 | 48 | echo "=== Batch Avatar Generation ===\n"; 49 | $names = ['Alice Johnson', 'Bob Wilson', 'Carol Brown', 'David Miller']; 50 | $batchResult = $hdAvatar->batchCreateAndExport($names, 'png'); 51 | 52 | echo "Processed {$batchResult['batch_info']['total_count']} avatars in {$batchResult['batch_info']['processing_time_seconds']} seconds\n"; 53 | echo "Average time per avatar: {$batchResult['batch_info']['average_time_per_avatar']} seconds\n\n"; 54 | 55 | // ============================================================================= 56 | // Multiple Format Export 57 | // ============================================================================= 58 | 59 | echo "=== Multiple Format Export ===\n"; 60 | $allFormats = $hdAvatar->exportAllFormats('Emma Davis'); 61 | foreach ($allFormats as $format => $urls) { 62 | echo "Format {$format}: {$urls['url']}\n"; 63 | } 64 | echo "\n"; 65 | 66 | // ============================================================================= 67 | // Advanced HD Avatar with Custom Settings 68 | // ============================================================================= 69 | 70 | echo "=== Advanced HD Avatar with Custom Settings ===\n"; 71 | 72 | // Create HD avatar with custom configuration 73 | $customHDAvatar = new HDAvatarResponse([ 74 | 'hd' => [ 75 | 'enabled' => true, 76 | 'width' => 1024, 77 | 'height' => 1024, 78 | 'fontSize' => 384, 79 | 'quality' => [ 80 | 'png' => 100, 81 | 'jpg' => 95, 82 | 'webp' => 90, 83 | ], 84 | ], 85 | 'responsive_sizes' => [ 86 | 'thumbnail' => ['width' => 64, 'height' => 64, 'fontSize' => 24], 87 | 'small' => ['width' => 128, 'height' => 128, 'fontSize' => 48], 88 | 'medium' => ['width' => 256, 'height' => 256, 'fontSize' => 96], 89 | 'large' => ['width' => 512, 'height' => 512, 'fontSize' => 192], 90 | 'ultra' => ['width' => 1024, 'height' => 1024, 'fontSize' => 384], 91 | ], 92 | ]); 93 | 94 | // Create ultra-HD avatar 95 | $ultraHD = $customHDAvatar->createHD('Michael Chen') 96 | ->setDimension(1024, 1024) 97 | ->setFontSize(384) 98 | ->setBackground('#667eea') 99 | ->setForeground('#FFFFFF'); 100 | 101 | // Export in multiple formats 102 | $ultraFiles = $ultraHD->exportMultiple(['png', 'jpg', 'webp']); 103 | foreach ($ultraFiles as $format => $file) { 104 | echo "Ultra-HD {$format}: {$file}\n"; 105 | } 106 | echo "\n"; 107 | 108 | // ============================================================================= 109 | // Performance Optimization Examples 110 | // ============================================================================= 111 | 112 | echo "=== Performance Optimization ===\n"; 113 | 114 | // Configure storage optimization 115 | $hdAvatar->configureStorage('local', 'optimized-avatars', 1000); // 1GB limit 116 | $hdAvatar->setCompressionEnabled(true); 117 | $hdAvatar->setMaxFileAge(14); // 14 days 118 | 119 | // Get storage statistics 120 | $stats = $hdAvatar->getStorageStatistics(); 121 | echo "Storage Usage: {$stats['total_size_mb']} MB ({$stats['usage_percentage']}%)\n"; 122 | echo "Total Files: {$stats['total_files']}\n"; 123 | 124 | // Optimize storage if needed 125 | if ($stats['usage_percentage'] > 80) { 126 | echo "Optimizing storage...\n"; 127 | $optimization = $hdAvatar->optimizeStorage(); 128 | echo "Removed {$optimization['optimization_summary']['files_removed']} files\n"; 129 | echo "Saved {$optimization['optimization_summary']['space_saved_mb']} MB\n"; 130 | } 131 | echo "\n"; 132 | 133 | // ============================================================================= 134 | // API Response Generation 135 | // ============================================================================= 136 | 137 | echo "=== API Response Generation ===\n"; 138 | 139 | // Generate API-ready response 140 | $apiResponse = $hdAvatar->apiResponse('Sarah Wilson', 'webp', 'large'); 141 | echo "API Response for {$apiResponse['data']['avatar']['name']}:\n"; 142 | echo "- URL: {$apiResponse['data']['avatar']['url']}\n"; 143 | echo "- Initials: {$apiResponse['data']['avatar']['initials']}\n"; 144 | echo "- Hash: {$apiResponse['data']['metadata']['hash']}\n"; 145 | echo "- Cache Key: {$apiResponse['data']['metadata']['cache_key']}\n"; 146 | echo "\n"; 147 | 148 | // ============================================================================= 149 | // Watermarked Avatars 150 | // ============================================================================= 151 | 152 | echo "=== Watermarked Avatars ===\n"; 153 | 154 | // Create avatar with watermark using the ImageExport trait 155 | $watermarkedPath = 'storage/avatars/watermarked_avatar.png'; 156 | $hdAvatar->createHD('Company User') 157 | ->setBackground('#4f46e5') 158 | ->setForeground('#ffffff'); 159 | 160 | // Export with watermark (this would work in a real Laravel environment) 161 | echo "Watermarked avatar would be saved to: {$watermarkedPath}\n\n"; 162 | 163 | // ============================================================================= 164 | // Health Check and Monitoring 165 | // ============================================================================= 166 | 167 | echo "=== Health Check ===\n"; 168 | 169 | $health = $hdAvatar->healthCheck(); 170 | echo "System Status: {$health['status']}\n"; 171 | 172 | foreach ($health['checks'] as $check => $status) { 173 | $statusText = $status ? 'PASS' : 'FAIL'; 174 | echo "- {$check}: {$statusText}\n"; 175 | } 176 | 177 | if (! empty($health['warnings'])) { 178 | echo "Warnings:\n"; 179 | foreach ($health['warnings'] as $warning) { 180 | echo "- {$warning}\n"; 181 | } 182 | } 183 | echo "\n"; 184 | 185 | // ============================================================================= 186 | // Avatar Information Retrieval 187 | // ============================================================================= 188 | 189 | echo "=== Avatar Information ===\n"; 190 | 191 | $avatarInfo = $hdAvatar->getAvatarInfo('Technical User'); 192 | echo "Avatar Details:\n"; 193 | echo "- Name: {$avatarInfo['name']}\n"; 194 | echo "- Initials: {$avatarInfo['initials']}\n"; 195 | echo "- Dimensions: {$avatarInfo['dimensions']['width']}x{$avatarInfo['dimensions']['height']}\n"; 196 | echo "- Background: {$avatarInfo['styling']['background']}\n"; 197 | echo "- Foreground: {$avatarInfo['styling']['foreground']}\n"; 198 | echo '- HD Enabled: '.($avatarInfo['configuration']['hd_enabled'] ? 'Yes' : 'No')."\n"; 199 | echo '- Cache Enabled: '.($avatarInfo['performance']['cache_enabled'] ? 'Yes' : 'No')."\n"; 200 | 201 | echo "\nEstimated File Sizes:\n"; 202 | foreach ($avatarInfo['estimated_file_sizes'] as $format => $size) { 203 | echo "- {$format}: {$size}\n"; 204 | } 205 | echo "\n"; 206 | 207 | // ============================================================================= 208 | // Placeholder Generation for Lazy Loading 209 | // ============================================================================= 210 | 211 | echo "=== Placeholder Generation ===\n"; 212 | 213 | $placeholder = $hdAvatar->createHD('Lazy User')->toPlaceholder(32, 32); 214 | echo 'Placeholder Data URI: '.substr($placeholder, 0, 50)."...\n"; 215 | echo 'Placeholder Length: '.strlen($placeholder)." characters\n\n"; 216 | 217 | // ============================================================================= 218 | // Sprite Sheet Generation 219 | // ============================================================================= 220 | 221 | echo "=== Sprite Sheet Generation ===\n"; 222 | 223 | $variations = [ 224 | ['background' => '#ff6b6b', 'foreground' => '#ffffff'], 225 | ['background' => '#4ecdc4', 'foreground' => '#ffffff'], 226 | ['background' => '#45b7d1', 'foreground' => '#ffffff'], 227 | ['background' => '#96ceb4', 'foreground' => '#ffffff'], 228 | ['background' => '#ffeaa7', 'foreground' => '#2d3436'], 229 | ]; 230 | 231 | $spriteUrl = $hdAvatar->createSpriteSheet('Sprite User', $variations, 'png'); 232 | echo "Sprite sheet created: {$spriteUrl}\n\n"; 233 | 234 | echo "=== HD Avatar Examples Complete ===\n"; 235 | echo "All examples have been demonstrated successfully!\n"; 236 | echo "Remember to configure your Laravel storage and cache settings for optimal performance.\n"; 237 | -------------------------------------------------------------------------------- /src/HDAvatar.php: -------------------------------------------------------------------------------- 1 | initializeStorage(); 25 | 26 | // Configure storage settings from config 27 | if (isset($config['storage'])) { 28 | $storageConfig = $config['storage']; 29 | $this->configureStorage( 30 | $storageConfig['disk'] ?? 'local', 31 | $storageConfig['directory'] ?? 'avatars', 32 | $storageConfig['max_storage_mb'] ?? 500 33 | ); 34 | 35 | $this->setMaxFileAge($storageConfig['max_age_days'] ?? 30); 36 | $this->setCompressionEnabled($storageConfig['compression'] ?? true); 37 | } 38 | } 39 | 40 | /** 41 | * Create and immediately export HD avatar 42 | */ 43 | public function createAndExport(string $name, string $format = 'png', array $options = []): array 44 | { 45 | $this->createHD($name); 46 | 47 | $exported = [ 48 | 'name' => $name, 49 | 'format' => $format, 50 | 'url' => $this->storeOptimized($name, $format, $options), 51 | 'responsive_urls' => [], 52 | 'metadata' => [ 53 | 'width' => $this->width, 54 | 'height' => $this->height, 55 | 'font_size' => $this->fontSize, 56 | 'background' => $this->background, 57 | 'foreground' => $this->foreground, 58 | 'hash' => $this->generateContentHash(), 59 | ], 60 | ]; 61 | 62 | // Generate responsive sizes if configured 63 | if (! empty($this->responsiveSizes)) { 64 | foreach ($this->responsiveSizes as $size => $dimensions) { 65 | $this->setDimension($dimensions['width'], $dimensions['height']); 66 | $this->setFontSize($dimensions['fontSize']); 67 | 68 | $exported['responsive_urls'][$size] = $this->storeOptimized("{$name}_{$size}", $format, $options); 69 | } 70 | } 71 | 72 | return $exported; 73 | } 74 | 75 | /** 76 | * Batch create and export multiple avatars 77 | */ 78 | public function batchCreateAndExport(array $names, string $format = 'png', array $options = []): array 79 | { 80 | $results = []; 81 | $startTime = microtime(true); 82 | 83 | foreach ($names as $name) { 84 | $results[$name] = $this->createAndExport($name, $format, $options); 85 | } 86 | 87 | $processingTime = microtime(true) - $startTime; 88 | 89 | return [ 90 | 'avatars' => $results, 91 | 'batch_info' => [ 92 | 'total_count' => count($names), 93 | 'processing_time_seconds' => round($processingTime, 3), 94 | 'average_time_per_avatar' => round($processingTime / count($names), 3), 95 | 'format' => $format, 96 | 'timestamp' => now()->toISOString(), 97 | ], 98 | ]; 99 | } 100 | 101 | /** 102 | * Generate avatar with all export formats 103 | */ 104 | public function exportAllFormats(string $name, array $options = []): array 105 | { 106 | $this->createHD($name); 107 | 108 | $formats = ['png', 'jpg', 'webp']; 109 | $exports = []; 110 | 111 | foreach ($formats as $format) { 112 | $exports[$format] = [ 113 | 'url' => $this->storeOptimized($name, $format, $options), 114 | 'cached_url' => $this->getCachedOrGenerate($name, $format, $options), 115 | ]; 116 | } 117 | 118 | return $exports; 119 | } 120 | 121 | /** 122 | * Create avatar sprite sheet with variations 123 | */ 124 | public function createSpriteSheet(string $name, array $variations, string $format = 'png'): string 125 | { 126 | $this->createHD($name); 127 | 128 | $filename = $this->generateOptimizedFilename("{$name}_sprite", $format); 129 | $path = $this->storageDirectory.'/'.$filename; 130 | $fullPath = Storage::disk($this->storageDisk)->path($path); 131 | 132 | $this->exportSpriteSheet($variations, $fullPath, $format); 133 | 134 | return Storage::disk($this->storageDisk)->url($path); 135 | } 136 | 137 | /** 138 | * Get comprehensive avatar information 139 | */ 140 | public function getAvatarInfo(string $name): array 141 | { 142 | $this->createHD($name); 143 | 144 | return [ 145 | 'name' => $name, 146 | 'initials' => $this->getInitial(), 147 | 'dimensions' => [ 148 | 'width' => $this->width, 149 | 'height' => $this->height, 150 | ], 151 | 'styling' => [ 152 | 'background' => $this->background, 153 | 'foreground' => $this->foreground, 154 | 'font_size' => $this->fontSize, 155 | 'shape' => $this->shape, 156 | 'border' => [ 157 | 'size' => $this->borderSize, 158 | 'color' => $this->borderColor, 159 | 'radius' => $this->borderRadius, 160 | ], 161 | ], 162 | 'configuration' => [ 163 | 'hd_enabled' => $this->hdEnabled, 164 | 'responsive_sizes' => $this->responsiveSizes, 165 | 'export_path' => $this->exportPath, 166 | ], 167 | 'performance' => [ 168 | 'cache_enabled' => $this->cacheEnabled, 169 | 'compression_enabled' => $this->compressionEnabled, 170 | 'storage_disk' => $this->storageDisk, 171 | ], 172 | 'hash' => $this->generateContentHash(), 173 | 'estimated_file_sizes' => $this->estimateFileSizes(), 174 | ]; 175 | } 176 | 177 | /** 178 | * Optimize existing avatar storage 179 | */ 180 | public function optimizeStorage(): array 181 | { 182 | $beforeStats = $this->getStorageStatistics(); 183 | $cleaned = $this->performCleanup(); 184 | $afterStats = $this->getStorageStatistics(); 185 | 186 | return [ 187 | 'before' => $beforeStats, 188 | 'after' => $afterStats, 189 | 'cleaned' => $cleaned, 190 | 'optimization_summary' => [ 191 | 'files_removed' => array_sum(array_map('count', $cleaned)), 192 | 'space_saved_mb' => round(($beforeStats['total_size_mb'] - $afterStats['total_size_mb']), 2), 193 | 'optimization_percentage' => round((($beforeStats['total_size_mb'] - $afterStats['total_size_mb']) / max($beforeStats['total_size_mb'], 0.1)) * 100, 2), 194 | ], 195 | ]; 196 | } 197 | 198 | /** 199 | * Generate avatar API response 200 | */ 201 | public function apiResponse(string $name, string $format = 'png', string $size = 'medium'): array 202 | { 203 | $info = $this->getAvatarInfo($name); 204 | 205 | // Set appropriate size 206 | if (isset($this->responsiveSizes[$size])) { 207 | $dimensions = $this->responsiveSizes[$size]; 208 | $this->setDimension($dimensions['width'], $dimensions['height']); 209 | $this->setFontSize($dimensions['fontSize']); 210 | } 211 | 212 | return [ 213 | 'success' => true, 214 | 'data' => [ 215 | 'avatar' => [ 216 | 'name' => $name, 217 | 'initials' => $info['initials'], 218 | 'url' => $this->getCachedOrGenerate($name, $format, []), 219 | 'placeholder' => $this->toPlaceholder(), 220 | 'responsive_urls' => $this->generateResponsiveUrls($name, $format), 221 | ], 222 | 'metadata' => [ 223 | 'size' => $size, 224 | 'format' => $format, 225 | 'dimensions' => [ 226 | 'width' => $this->width, 227 | 'height' => $this->height, 228 | ], 229 | 'hash' => $info['hash'], 230 | 'cache_key' => $this->generateCacheKey($name, $format, ['size' => $size]), 231 | ], 232 | ], 233 | 'timestamp' => now()->toISOString(), 234 | ]; 235 | } 236 | 237 | /** 238 | * Generate responsive URLs for all configured sizes 239 | */ 240 | protected function generateResponsiveUrls(string $name, string $format): array 241 | { 242 | $urls = []; 243 | 244 | foreach ($this->responsiveSizes as $size => $dimensions) { 245 | $urls[$size] = $this->getCachedUrl($format, $size); 246 | } 247 | 248 | return $urls; 249 | } 250 | 251 | /** 252 | * Health check for HD avatar system 253 | */ 254 | public function healthCheck(): array 255 | { 256 | $stats = $this->getStorageStatistics(); 257 | $exportStats = $this->getExportStats(); 258 | 259 | $health = [ 260 | 'status' => 'healthy', 261 | 'checks' => [ 262 | 'storage_accessible' => true, 263 | 'cache_functional' => true, 264 | 'within_storage_limits' => $stats['usage_percentage'] < 90, 265 | 'export_formats_supported' => count($exportStats['supported_formats']) >= 3, 266 | ], 267 | 'warnings' => [], 268 | 'statistics' => [ 269 | 'storage' => $stats, 270 | 'export' => $exportStats, 271 | ], 272 | ]; 273 | 274 | // Add warnings based on checks 275 | if (! $health['checks']['within_storage_limits']) { 276 | $health['warnings'][] = "Storage usage is at {$stats['usage_percentage']}% - cleanup recommended"; 277 | } 278 | 279 | if ($stats['total_files'] > 10000) { 280 | $health['warnings'][] = "Large number of cached files ({$stats['total_files']}) - consider cleanup"; 281 | } 282 | 283 | // Overall status 284 | $failedChecks = array_filter($health['checks'], fn ($check) => ! $check); 285 | if (! empty($failedChecks)) { 286 | $health['status'] = 'degraded'; 287 | } 288 | 289 | if (count($failedChecks) > 2) { 290 | $health['status'] = 'unhealthy'; 291 | } 292 | 293 | return $health; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravolt/avatar 2 | 3 | [![Total Downloads](https://img.shields.io/packagist/dt/laravolt/avatar.svg?style=flat)](https://packagist.org/packages/laravolt/avatar) 4 | [![Monthly Downloads](https://img.shields.io/packagist/dm/laravolt/avatar.svg?style=flat)](https://packagist.org/packages/laravolt/avatar) 5 | [![Daily Downloads](https://img.shields.io/packagist/dd/laravolt/avatar.svg?style=flat)](https://packagist.org/packages/laravolt/avatar) 6 | [![Run Tests](https://github.com/laravolt/avatar/workflows/run-tests/badge.svg)](https://github.com/laravolt/avatar/workflows/run-tests/badge.svg) 7 | 8 | ![Preview](https://user-images.githubusercontent.com/149716/29503524-840ccd0c-8662-11e7-92f9-9ec3ed8a24af.png) 9 | 10 | Display unique avatar for any user based on their (initials) name. 11 | 12 | ## Preview 13 | ![Preview](https://cloud.githubusercontent.com/assets/149716/26735022/6dbd77e2-47ea-11e7-8a05-7772465309c5.png) 14 | ## :film_strip: Video Tutorial 15 | [](https://youtu.be/jD0wu88c5kw) 16 | 17 | ## Installation 18 | This package originally built for Laravel, but can also be used in any PHP project. 19 | 20 | [Read more about integration with PHP project here.](#integration-with-other-php-project) 21 | 22 | ### Laravel >= 5.2: 23 | ```bash 24 | composer require laravolt/avatar 25 | ``` 26 | 27 | ### Laravel 5.1: 28 | ```bash 29 | composer require laravolt/avatar ~0.3 30 | ``` 31 | 32 | ## Service Provider & Facade 33 | **Note: only for Laravel 5.4 and below, because since Laravel 5.5 we use package auto-discovery.** 34 | 35 | ```php 36 | Laravolt\Avatar\ServiceProvider::class, 37 | 38 | ... 39 | 40 | 'Avatar' => Laravolt\Avatar\Facade::class, 41 | ``` 42 | 43 | ## Publish Config (optional) 44 | ```php 45 | php artisan vendor:publish --provider="Laravolt\Avatar\ServiceProvider" 46 | ``` 47 | This will create config file located in `config/laravolt/avatar.php`. 48 | 49 | ## Lumen Service Provider 50 | 51 | ```php 52 | $app->register(Laravolt\Avatar\LumenServiceProvider); 53 | ``` 54 | 55 | ## Usage 56 | 57 | ### Output as base64 58 | ```php 59 | //this will output data-uri (base64 image data) 60 | //something like data:image/png;base64,iVBORw0KGg.... 61 | Avatar::create('Joko Widodo')->toBase64(); 62 | 63 | //use in view 64 | //this will display initials JW as an image 65 | 66 | ``` 67 | 68 | ### Save as file 69 | ```php 70 | Avatar::create('Susilo Bambang Yudhoyono')->save('sample.png'); 71 | Avatar::create('Susilo Bambang Yudhoyono')->save('sample.jpg', 100); // quality = 100 72 | ``` 73 | 74 | ### Output as Gravatar 75 | ```php 76 | Avatar::create('uyab@example.net')->toGravatar(); 77 | // Output: http://gravatar.com/avatar/0c5cbf5a8762d91d930795a6107b2ce5814a6ab26e60c7ec6b75bc81c7dfe3ee 78 | 79 | Avatar::create('uyab@example.net')->toGravatar(['d' => 'identicon', 'r' => 'pg', 's' => 100]); 80 | // Output: http://gravatar.com/avatar/0c5cbf5a8762d91d930795a6107b2ce5814a6ab26e60c7ec6b75bc81c7dfe3ee?d=identicon&r=pg&s=100 81 | ``` 82 | Gravatar parameter reference: https://docs.gravatar.com/api/avatars/images/ 83 | 84 | ### Output as SVG 85 | ```php 86 | Avatar::create('Susilo Bambang Yudhoyono')->toSvg(); 87 | ``` 88 | 89 | You may specify custom font-family for your SVG text. 90 | ```html 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 104 | 105 | ``` 106 | 107 | ```php 108 | Avatar::create('Susilo Bambang Yudhoyono')->setFontFamily('Laravolt')->toSvg(); 109 | ``` 110 | 111 | You may make the SVG responsive. This excludes the height and width attributes. 112 | ```php 113 | Avatar::create('Susilo Bambang Yudhoyono')->setResponsive()->toSvg(); 114 | ``` 115 | 116 | ## Get underlying Intervention image object 117 | ```php 118 | Avatar::create('Abdul Somad')->getImageObject(); 119 | ``` 120 | The method will return an instance of [Intervention image object](http://image.intervention.io/), so you can use it for further purposes. 121 | 122 | ## Non-ASCII Character 123 | By default, this package will try to output any initials letter as it is. If the name supplied contains any non-ASCII character (e.g. ā, Ě, ǽ) then the result will depend on which font used (see config). It the font supports characters supplied, it will successfully displayed, otherwise it will not. 124 | 125 | Alternatively, we can convert all non-ascii to their closest ASCII counterparts. If no closest coutnerparts found, those characters are removed. Thanks to [Stringy](https://github.com/danielstjules/Stringy) for providing such useful functions. What we need is just change one line in `config/avatar.php`: 126 | 127 | ```php 128 | 'ascii' => true, 129 | ``` 130 | 131 | ## Configuration 132 | ```php 133 | 'gd', 152 | 153 | // Initial generator class 154 | 'generator' => \Laravolt\Avatar\Generator\DefaultGenerator::class, 155 | 156 | // Whether all characters supplied must be replaced with their closest ASCII counterparts 157 | 'ascii' => false, 158 | 159 | // Image shape: circle or square 160 | 'shape' => 'circle', 161 | 162 | // Image width, in pixel 163 | 'width' => 100, 164 | 165 | // Image height, in pixel 166 | 'height' => 100, 167 | 168 | // Number of characters used as initials. If name consists of single word, the first N character will be used 169 | 'chars' => 2, 170 | 171 | // font size 172 | 'fontSize' => 48, 173 | 174 | // convert initial letter in uppercase 175 | 'uppercase' => false, 176 | 177 | // Right to Left (RTL) 178 | 'rtl' => false, 179 | 180 | // Fonts used to render text. 181 | // If contains more than one fonts, randomly selected based on name supplied 182 | 'fonts' => [__DIR__.'/../fonts/OpenSans-Bold.ttf', __DIR__.'/../fonts/rockwell.ttf'], 183 | 184 | // List of foreground colors to be used, randomly selected based on name supplied 185 | 'foregrounds' => [ 186 | '#FFFFFF', 187 | ], 188 | 189 | // List of background colors to be used, randomly selected based on name supplied 190 | 'backgrounds' => [ 191 | '#f44336', 192 | '#E91E63', 193 | '#9C27B0', 194 | '#673AB7', 195 | '#3F51B5', 196 | '#2196F3', 197 | '#03A9F4', 198 | '#00BCD4', 199 | '#009688', 200 | '#4CAF50', 201 | '#8BC34A', 202 | '#CDDC39', 203 | '#FFC107', 204 | '#FF9800', 205 | '#FF5722', 206 | ], 207 | 208 | 'border' => [ 209 | 'size' => 1, 210 | 211 | // border color, available value are: 212 | // 'foreground' (same as foreground color) 213 | // 'background' (same as background color) 214 | // or any valid hex ('#aabbcc') 215 | 'color' => 'background', 216 | 217 | // border radius, only works for SVG 218 | 'radius' => 0, 219 | ], 220 | 221 | // List of theme name to be used when rendering avatar 222 | // Possible values are: 223 | // 1. Theme name as string: 'colorful' 224 | // 2. Or array of string name: ['grayscale-light', 'grayscale-dark'] 225 | // 3. Or wildcard "*" to use all defined themes 226 | 'theme' => ['*'], 227 | 228 | // Predefined themes 229 | // Available theme attributes are: 230 | // shape, chars, backgrounds, foregrounds, fonts, fontSize, width, height, ascii, uppercase, and border. 231 | 'themes' => [ 232 | 'grayscale-light' => [ 233 | 'backgrounds' => ['#edf2f7', '#e2e8f0', '#cbd5e0'], 234 | 'foregrounds' => ['#a0aec0'], 235 | ], 236 | 'grayscale-dark' => [ 237 | 'backgrounds' => ['#2d3748', '#4a5568', '#718096'], 238 | 'foregrounds' => ['#e2e8f0'], 239 | ], 240 | 'colorful' => [ 241 | 'backgrounds' => [ 242 | '#f44336', 243 | '#E91E63', 244 | '#9C27B0', 245 | '#673AB7', 246 | '#3F51B5', 247 | '#2196F3', 248 | '#03A9F4', 249 | '#00BCD4', 250 | '#009688', 251 | '#4CAF50', 252 | '#8BC34A', 253 | '#CDDC39', 254 | '#FFC107', 255 | '#FF9800', 256 | '#FF5722', 257 | ], 258 | 'foregrounds' => ['#FFFFFF'], 259 | ], 260 | ] 261 | ]; 262 | ``` 263 | 264 | ## Overriding config at runtime 265 | We can overriding configuration at runtime by using following functions: 266 | 267 | ```php 268 | Avatar::create('Soekarno')->setDimension(100);//width = height = 100 pixel 269 | Avatar::create('Soekarno')->setDimension(100, 200); // width = 100, height = 200 270 | Avatar::create('Soekarno')->setBackground('#001122'); 271 | Avatar::create('Soekarno')->setForeground('#999999'); 272 | Avatar::create('Soekarno')->setFontSize(72); 273 | Avatar::create('Soekarno')->setFont('/path/to/font.ttf'); 274 | Avatar::create('Soekarno')->setBorder(1, '#aabbcc'); // size = 1, color = #aabbcc 275 | Avatar::create('Soekarno')->setBorder(1, '#aabbcc', 10); // size = 1, color = #aabbcc, border radius = 10 (only for SVG) 276 | Avatar::create('Soekarno')->setShape('square'); 277 | 278 | // Available since 3.0.0 279 | Avatar::create('Soekarno')->setTheme('colorful'); // set exact theme 280 | Avatar::create('Soekarno')->setTheme(['grayscale-light', 'grayscale-dark']); // theme will be randomized from these two options 281 | 282 | // chaining 283 | Avatar::create('Habibie')->setDimension(50)->setFontSize(18)->toBase64(); 284 | ``` 285 | 286 | ## Integration with other PHP project 287 | ```php 288 | // include composer autoload 289 | require 'vendor/autoload.php'; 290 | 291 | // import the Avatar class 292 | use Laravolt\Avatar\Avatar; 293 | 294 | // create your first avatar 295 | $avatar = new Avatar($config); 296 | $avatar->create('John Doe')->toBase64(); 297 | $avatar->create('John Doe')->save('path/to/file.png', $quality = 90); 298 | ``` 299 | `$config` is just an ordinary array with same format as explained above (See [Configuration](#configuration)). 300 | 301 | ## Support Us 302 | 303 | ### Buy Me A Coffee 304 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/uyab) 305 | 306 | ### Donate Via PayPal 307 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://paypal.me/bayuhendra) 308 | 309 | ### Traktir Saya 310 | Trakteer Saya 311 | -------------------------------------------------------------------------------- /src/HDAvatarResponse.php: -------------------------------------------------------------------------------- 1 | hdConfig = $config['hd'] ?? []; 31 | $this->responsiveSizes = $config['responsive_sizes'] ?? []; 32 | $this->exportPath = $config['export']['path'] ?? 'avatars'; 33 | $this->hdEnabled = $config['hd']['enabled'] ?? true; 34 | 35 | // Apply HD defaults if enabled 36 | if ($this->hdEnabled) { 37 | $config = $this->applyHDDefaults($config); 38 | } 39 | 40 | parent::__construct($config, $cache); 41 | } 42 | 43 | /** 44 | * Apply HD defaults to configuration 45 | */ 46 | protected function applyHDDefaults(array $config): array 47 | { 48 | $hdDefaults = [ 49 | 'width' => $this->hdConfig['width'] ?? 512, 50 | 'height' => $this->hdConfig['height'] ?? 512, 51 | 'fontSize' => $this->hdConfig['fontSize'] ?? 192, 52 | 'driver' => 'imagick', // Prefer imagick for HD 53 | ]; 54 | 55 | return array_merge($config, $hdDefaults); 56 | } 57 | 58 | /** 59 | * Create HD avatar with multiple export options 60 | */ 61 | public function createHD(string $name): static 62 | { 63 | $this->name = $name; 64 | $this->initTheme(); 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Export avatar as high-quality image file 71 | */ 72 | public function export(string $format = 'png', int $quality = 95): string 73 | { 74 | $this->buildAvatar(); 75 | 76 | $filename = $this->generateFilename($format); 77 | $filepath = $this->exportPath.'/'.$filename; 78 | 79 | // Ensure directory exists 80 | Storage::makeDirectory($this->exportPath); 81 | 82 | // Get full storage path 83 | $fullPath = Storage::path($filepath); 84 | 85 | // Apply format-specific optimizations 86 | switch (strtolower($format)) { 87 | case 'png': 88 | $this->image->toPng($this->hdConfig['quality']['png'] ?? $quality)->save($fullPath); 89 | break; 90 | 91 | case 'jpg': 92 | case 'jpeg': 93 | $this->image->toJpeg($this->hdConfig['quality']['jpg'] ?? $quality)->save($fullPath); 94 | break; 95 | 96 | case 'webp': 97 | $this->image->toWebp($this->hdConfig['quality']['webp'] ?? $quality)->save($fullPath); 98 | break; 99 | 100 | default: 101 | throw new \InvalidArgumentException("Unsupported format: {$format}"); 102 | } 103 | 104 | $this->exportedFiles[] = $filepath; 105 | 106 | return $filepath; 107 | } 108 | 109 | /** 110 | * Export multiple formats simultaneously 111 | */ 112 | public function exportMultiple(array $formats = ['png', 'jpg', 'webp']): array 113 | { 114 | $files = []; 115 | 116 | foreach ($formats as $format) { 117 | $files[$format] = $this->export($format); 118 | } 119 | 120 | return $files; 121 | } 122 | 123 | /** 124 | * Export responsive sizes 125 | */ 126 | public function exportResponsive(string $format = 'png'): array 127 | { 128 | $files = []; 129 | $originalWidth = $this->width; 130 | $originalHeight = $this->height; 131 | $originalFontSize = $this->fontSize; 132 | 133 | foreach ($this->responsiveSizes as $size => $dimensions) { 134 | $this->setDimension($dimensions['width'], $dimensions['height']); 135 | $this->setFontSize($dimensions['fontSize']); 136 | 137 | $filename = $this->generateFilename($format, $size); 138 | $filepath = $this->exportPath.'/'.$filename; 139 | 140 | Storage::makeDirectory($this->exportPath); 141 | $fullPath = Storage::path($filepath); 142 | 143 | $this->buildAvatar(); 144 | 145 | switch (strtolower($format)) { 146 | case 'png': 147 | $this->image->toPng()->save($fullPath); 148 | break; 149 | case 'jpg': 150 | case 'jpeg': 151 | $this->image->toJpeg()->save($fullPath); 152 | break; 153 | case 'webp': 154 | $this->image->toWebp()->save($fullPath); 155 | break; 156 | } 157 | 158 | $files[$size] = $filepath; 159 | } 160 | 161 | // Restore original dimensions 162 | $this->setDimension($originalWidth, $originalHeight); 163 | $this->setFontSize($originalFontSize); 164 | 165 | return $files; 166 | } 167 | 168 | /** 169 | * Get avatar as HTTP response with optimized headers 170 | */ 171 | public function toResponse(string $format = 'png'): Response 172 | { 173 | $this->buildAvatar(); 174 | 175 | $content = match (strtolower($format)) { 176 | 'png' => $this->image->toPng()->toString(), 177 | 'jpg', 'jpeg' => $this->image->toJpeg()->toString(), 178 | 'webp' => $this->image->toWebp()->toString(), 179 | default => throw new \InvalidArgumentException("Unsupported format: {$format}") 180 | }; 181 | 182 | $mimeType = match (strtolower($format)) { 183 | 'png' => 'image/png', 184 | 'jpg', 'jpeg' => 'image/jpeg', 185 | 'webp' => 'image/webp', 186 | }; 187 | 188 | return new Response($content, 200, [ 189 | 'Content-Type' => $mimeType, 190 | 'Cache-Control' => 'public, max-age=31536000', // 1 year 191 | 'Expires' => gmdate('D, d M Y H:i:s', time() + 31536000).' GMT', 192 | 'Last-Modified' => gmdate('D, d M Y H:i:s').' GMT', 193 | 'ETag' => '"'.md5($content).'"', 194 | ]); 195 | } 196 | 197 | /** 198 | * Get cached file URL or generate new one 199 | */ 200 | public function getCachedUrl(string $format = 'png', string $size = 'medium'): string 201 | { 202 | $filename = $this->generateFilename($format, $size); 203 | $filepath = $this->exportPath.'/'.$filename; 204 | 205 | // Check if file exists in storage 206 | if (Storage::exists($filepath)) { 207 | return Storage::url($filepath); 208 | } 209 | 210 | // Generate and cache the file 211 | if (isset($this->responsiveSizes[$size])) { 212 | $dimensions = $this->responsiveSizes[$size]; 213 | $this->setDimension($dimensions['width'], $dimensions['height']); 214 | $this->setFontSize($dimensions['fontSize']); 215 | } 216 | 217 | $this->export($format); 218 | 219 | return Storage::url($filepath); 220 | } 221 | 222 | /** 223 | * Generate optimized filename 224 | */ 225 | protected function generateFilename(string $format, ?string $size = null): string 226 | { 227 | $hash = $this->generateContentHash(); 228 | $timestamp = time(); 229 | $initials = $this->getInitial(); 230 | 231 | $sizeSuffix = $size ? "_{$size}" : ''; 232 | 233 | return "{$hash}{$sizeSuffix}_{$timestamp}.{$format}"; 234 | } 235 | 236 | /** 237 | * Generate content-based hash for caching 238 | */ 239 | protected function generateContentHash(): string 240 | { 241 | $content = [ 242 | 'name' => $this->name, 243 | 'width' => $this->width, 244 | 'height' => $this->height, 245 | 'fontSize' => $this->fontSize, 246 | 'background' => $this->background, 247 | 'foreground' => $this->foreground, 248 | 'shape' => $this->shape, 249 | 'borderSize' => $this->borderSize, 250 | 'borderColor' => $this->borderColor, 251 | 'font' => $this->font, 252 | ]; 253 | 254 | return substr(md5(serialize($content)), 0, 8); 255 | } 256 | 257 | /** 258 | * Clean up old cached files 259 | */ 260 | public function cleanup(int $maxAgeDays = 30): array 261 | { 262 | $cleaned = []; 263 | $cutoffTime = time() - ($maxAgeDays * 24 * 60 * 60); 264 | 265 | $files = Storage::allFiles($this->exportPath); 266 | 267 | foreach ($files as $file) { 268 | $lastModified = Storage::lastModified($file); 269 | 270 | if ($lastModified < $cutoffTime) { 271 | Storage::delete($file); 272 | $cleaned[] = $file; 273 | } 274 | } 275 | 276 | return $cleaned; 277 | } 278 | 279 | /** 280 | * Get storage statistics 281 | */ 282 | public function getStorageStats(): array 283 | { 284 | $files = Storage::allFiles($this->exportPath); 285 | $totalSize = 0; 286 | $fileCount = count($files); 287 | 288 | foreach ($files as $file) { 289 | $totalSize += Storage::size($file); 290 | } 291 | 292 | return [ 293 | 'file_count' => $fileCount, 294 | 'total_size_bytes' => $totalSize, 295 | 'total_size_mb' => round($totalSize / 1024 / 1024, 2), 296 | 'average_file_size_kb' => $fileCount > 0 ? round(($totalSize / $fileCount) / 1024, 2) : 0, 297 | ]; 298 | } 299 | 300 | /** 301 | * Generate placeholder image for lazy loading 302 | */ 303 | public function toPlaceholder(int $width = 64, int $height = 64): string 304 | { 305 | $originalWidth = $this->width; 306 | $originalHeight = $this->height; 307 | $originalFontSize = $this->fontSize; 308 | 309 | $this->setDimension($width, $height); 310 | $this->setFontSize($width / 4); 311 | 312 | $this->buildAvatar(); 313 | 314 | // Apply blur effect for placeholder 315 | $this->image->blur(5); 316 | 317 | $placeholder = $this->image->toPng()->toDataUri(); 318 | 319 | // Restore original dimensions 320 | $this->setDimension($originalWidth, $originalHeight); 321 | $this->setFontSize($originalFontSize); 322 | 323 | return $placeholder; 324 | } 325 | 326 | /** 327 | * Set HD quality settings 328 | */ 329 | public function setQuality(string $format, int $quality): static 330 | { 331 | $this->hdConfig['quality'][$format] = $quality; 332 | 333 | return $this; 334 | } 335 | 336 | /** 337 | * Enable/disable HD mode 338 | */ 339 | public function setHDMode(bool $enabled): static 340 | { 341 | $this->hdEnabled = $enabled; 342 | 343 | return $this; 344 | } 345 | 346 | /** 347 | * Set responsive size configuration 348 | */ 349 | public function setResponsiveSize(string $name, int $width, int $height, int $fontSize): static 350 | { 351 | $this->responsiveSizes[$name] = [ 352 | 'width' => $width, 353 | 'height' => $height, 354 | 'fontSize' => $fontSize, 355 | ]; 356 | 357 | return $this; 358 | } 359 | 360 | /** 361 | * Batch export multiple avatars for better performance 362 | */ 363 | public function batchExport(array $names, string $format = 'png', string $size = 'medium'): array 364 | { 365 | $results = []; 366 | 367 | foreach ($names as $name) { 368 | $this->create($name); 369 | 370 | if (isset($this->responsiveSizes[$size])) { 371 | $dimensions = $this->responsiveSizes[$size]; 372 | $this->setDimension($dimensions['width'], $dimensions['height']); 373 | $this->setFontSize($dimensions['fontSize']); 374 | } 375 | 376 | $filepath = $this->export($format); 377 | $results[$name] = $filepath; 378 | } 379 | 380 | return $results; 381 | } 382 | 383 | /** 384 | * Get exported files list 385 | */ 386 | public function getExportedFiles(): array 387 | { 388 | return $this->exportedFiles; 389 | } 390 | 391 | /** 392 | * Clear exported files list 393 | */ 394 | public function clearExportedFiles(): static 395 | { 396 | $this->exportedFiles = []; 397 | 398 | return $this; 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/Concerns/ImageExport.php: -------------------------------------------------------------------------------- 1 | buildAvatar(); 26 | 27 | $format = strtolower($format); 28 | $this->validateExportFormat($format); 29 | 30 | $mergedOptions = array_merge($this->getDefaultExportOptions($format), $options); 31 | 32 | return match ($format) { 33 | 'png' => $this->exportPNG($path, $mergedOptions), 34 | 'jpg', 'jpeg' => $this->exportJPEG($path, $mergedOptions), 35 | 'webp' => $this->exportWebP($path, $mergedOptions), 36 | default => throw new \InvalidArgumentException("Unsupported format: {$format}") 37 | }; 38 | } 39 | 40 | /** 41 | * Export as PNG with optimizations 42 | */ 43 | protected function exportPNG(string $path, array $options): ImageInterface 44 | { 45 | $quality = $options['quality'] ?? 95; 46 | $compression = $options['compression'] ?? 6; 47 | $interlaced = $options['interlaced'] ?? false; 48 | 49 | // Apply PNG-specific optimizations 50 | if ($interlaced) { 51 | $this->image->interlace(); 52 | } 53 | 54 | return $this->image->toPng($quality)->save($path); 55 | } 56 | 57 | /** 58 | * Export as JPEG with optimizations 59 | */ 60 | protected function exportJPEG(string $path, array $options): ImageInterface 61 | { 62 | $quality = $options['quality'] ?? 90; 63 | $progressive = $options['progressive'] ?? true; 64 | 65 | // Apply JPEG-specific optimizations 66 | if ($progressive) { 67 | $this->image->interlace(); 68 | } 69 | 70 | return $this->image->toJpeg($quality)->save($path); 71 | } 72 | 73 | /** 74 | * Export as WebP with optimizations 75 | */ 76 | protected function exportWebP(string $path, array $options): ImageInterface 77 | { 78 | $quality = $options['quality'] ?? 85; 79 | $lossless = $options['lossless'] ?? false; 80 | 81 | // Apply WebP-specific optimizations 82 | $webpQuality = $lossless ? 100 : $quality; 83 | 84 | return $this->image->toWebp($webpQuality)->save($path); 85 | } 86 | 87 | /** 88 | * Export with multiple sizes for responsive design 89 | */ 90 | public function exportResponsiveSizes(string $basePath, array $sizes, string $format = 'png'): array 91 | { 92 | $exported = []; 93 | $originalWidth = $this->width; 94 | $originalHeight = $this->height; 95 | $originalFontSize = $this->fontSize; 96 | 97 | foreach ($sizes as $sizeName => $dimensions) { 98 | // Update dimensions 99 | $this->setDimension($dimensions['width'], $dimensions['height'] ?? $dimensions['width']); 100 | if (isset($dimensions['fontSize'])) { 101 | $this->setFontSize($dimensions['fontSize']); 102 | } else { 103 | // Auto-calculate font size based on width 104 | $this->setFontSize(intval($dimensions['width'] * 0.375)); // ~37.5% of width 105 | } 106 | 107 | // Generate filename with size suffix 108 | $pathInfo = pathinfo($basePath); 109 | $filename = $pathInfo['filename'].'_'.$sizeName.'.'.$format; 110 | $fullPath = ($pathInfo['dirname'] !== '.' ? $pathInfo['dirname'].'/' : '').$filename; 111 | 112 | // Export the sized image 113 | $this->exportImage($fullPath, $format); 114 | $exported[$sizeName] = $fullPath; 115 | } 116 | 117 | // Restore original dimensions 118 | $this->setDimension($originalWidth, $originalHeight); 119 | $this->setFontSize($originalFontSize); 120 | 121 | return $exported; 122 | } 123 | 124 | /** 125 | * Bulk export multiple avatars efficiently 126 | */ 127 | public function bulkExport(array $names, string $directory, string $format = 'png', array $options = []): array 128 | { 129 | $exported = []; 130 | 131 | // Ensure directory exists 132 | Storage::makeDirectory($directory); 133 | 134 | foreach ($names as $name) { 135 | $this->create($name); 136 | 137 | // Generate filename 138 | $sanitizedName = $this->sanitizeFilename($name); 139 | $filename = $sanitizedName.'_'.$this->generateContentHash().'.'.$format; 140 | $path = $directory.'/'.$filename; 141 | $fullPath = Storage::path($path); 142 | 143 | // Export the avatar 144 | $this->exportImage($fullPath, $format, $options); 145 | $exported[$name] = $path; 146 | } 147 | 148 | return $exported; 149 | } 150 | 151 | /** 152 | * Export with watermark 153 | */ 154 | public function exportWithWatermark(string $path, string $watermarkText, string $format = 'png', array $options = []): ImageInterface 155 | { 156 | $this->buildAvatar(); 157 | 158 | // Add watermark 159 | $this->addWatermark($watermarkText, $options['watermark'] ?? []); 160 | 161 | return $this->exportImage($path, $format, $options); 162 | } 163 | 164 | /** 165 | * Add watermark to the image 166 | */ 167 | protected function addWatermark(string $text, array $options = []): void 168 | { 169 | $position = $options['position'] ?? 'bottom-right'; 170 | $opacity = $options['opacity'] ?? 0.3; 171 | $fontSize = $options['fontSize'] ?? intval($this->width * 0.08); 172 | $color = $options['color'] ?? '#FFFFFF'; 173 | 174 | // Calculate position coordinates 175 | [$x, $y] = $this->calculateWatermarkPosition($position, $text, $fontSize); 176 | 177 | // Apply watermark 178 | $this->image->text( 179 | $text, 180 | $x, 181 | $y, 182 | function ($font) use ($fontSize, $color, $opacity) { 183 | $font->file($this->font); 184 | $font->size($fontSize); 185 | $font->color($color); 186 | $font->alpha($opacity); 187 | $font->align('left'); 188 | $font->valign('bottom'); 189 | } 190 | ); 191 | } 192 | 193 | /** 194 | * Calculate watermark position 195 | */ 196 | protected function calculateWatermarkPosition(string $position, string $text, int $fontSize): array 197 | { 198 | $margin = intval($this->width * 0.05); // 5% margin 199 | 200 | return match ($position) { 201 | 'top-left' => [$margin, $margin + $fontSize], 202 | 'top-right' => [$this->width - $margin, $margin + $fontSize], 203 | 'bottom-left' => [$margin, $this->height - $margin], 204 | 'bottom-right' => [$this->width - $margin, $this->height - $margin], 205 | 'center' => [$this->width / 2, $this->height / 2], 206 | default => [$this->width - $margin, $this->height - $margin], 207 | }; 208 | } 209 | 210 | /** 211 | * Export as sprite sheet for animations 212 | */ 213 | public function exportSpriteSheet(array $variations, string $path, string $format = 'png', array $options = []): ImageInterface 214 | { 215 | $spriteWidth = ($options['sprite_width'] ?? count($variations)) * $this->width; 216 | $spriteHeight = $this->height; 217 | 218 | // Create sprite canvas 219 | $driver = $this->driver === 'gd' ? new \Intervention\Image\Drivers\Gd\Driver : new \Intervention\Image\Drivers\Imagick\Driver; 220 | $manager = new \Intervention\Image\ImageManager($driver); 221 | $sprite = $manager->create($spriteWidth, $spriteHeight); 222 | 223 | // Add each variation to the sprite 224 | $x = 0; 225 | foreach ($variations as $variation) { 226 | // Apply variation (e.g., different colors, effects) 227 | $this->applyVariation($variation); 228 | $this->buildAvatar(); 229 | 230 | // Copy to sprite at current x position 231 | $sprite->place($this->image, 'top-left', $x, 0); 232 | $x += $this->width; 233 | } 234 | 235 | // Export sprite sheet 236 | return match (strtolower($format)) { 237 | 'png' => $sprite->toPng()->save($path), 238 | 'jpg', 'jpeg' => $sprite->toJpeg()->save($path), 239 | 'webp' => $sprite->toWebp()->save($path), 240 | default => throw new \InvalidArgumentException("Unsupported format: {$format}") 241 | }; 242 | } 243 | 244 | /** 245 | * Apply variation to avatar (override in subclasses) 246 | */ 247 | protected function applyVariation(array $variation): void 248 | { 249 | if (isset($variation['background'])) { 250 | $this->setBackground($variation['background']); 251 | } 252 | if (isset($variation['foreground'])) { 253 | $this->setForeground($variation['foreground']); 254 | } 255 | if (isset($variation['shape'])) { 256 | $this->setShape($variation['shape']); 257 | } 258 | } 259 | 260 | /** 261 | * Get default export options for format 262 | */ 263 | protected function getDefaultExportOptions(string $format): array 264 | { 265 | return match ($format) { 266 | 'png' => [ 267 | 'quality' => 95, 268 | 'compression' => 6, 269 | 'interlaced' => false, 270 | ], 271 | 'jpg', 'jpeg' => [ 272 | 'quality' => 90, 273 | 'progressive' => true, 274 | ], 275 | 'webp' => [ 276 | 'quality' => 85, 277 | 'lossless' => false, 278 | ], 279 | default => [], 280 | }; 281 | } 282 | 283 | /** 284 | * Validate export format 285 | */ 286 | protected function validateExportFormat(string $format): void 287 | { 288 | if (! in_array($format, $this->exportFormats)) { 289 | throw new \InvalidArgumentException( 290 | "Unsupported format '{$format}'. Supported formats: ".implode(', ', $this->exportFormats) 291 | ); 292 | } 293 | } 294 | 295 | /** 296 | * Sanitize filename for safe file system usage 297 | */ 298 | protected function sanitizeFilename(string $filename): string 299 | { 300 | // Remove or replace unsafe characters 301 | $unsafe = ['/', '\\', ':', '*', '?', '"', '<', '>', '|']; 302 | $safe = str_replace($unsafe, '_', $filename); 303 | 304 | // Remove multiple underscores and trim 305 | $safe = preg_replace('/_+/', '_', $safe); 306 | $safe = trim($safe, '_'); 307 | 308 | // Limit length 309 | return substr($safe, 0, 100); 310 | } 311 | 312 | /** 313 | * Set export options 314 | */ 315 | public function setExportOptions(array $options): static 316 | { 317 | $this->exportOptions = array_merge($this->exportOptions, $options); 318 | 319 | return $this; 320 | } 321 | 322 | /** 323 | * Get export statistics 324 | */ 325 | public function getExportStats(): array 326 | { 327 | return [ 328 | 'supported_formats' => $this->exportFormats, 329 | 'current_options' => $this->exportOptions, 330 | 'image_dimensions' => [ 331 | 'width' => $this->width, 332 | 'height' => $this->height, 333 | ], 334 | 'estimated_file_sizes' => $this->estimateFileSizes(), 335 | ]; 336 | } 337 | 338 | /** 339 | * Estimate file sizes for different formats 340 | */ 341 | protected function estimateFileSizes(): array 342 | { 343 | $pixelCount = $this->width * $this->height; 344 | 345 | return [ 346 | 'png' => intval($pixelCount * 3.5).' bytes (estimated)', // ~3.5 bytes per pixel for PNG 347 | 'jpg' => intval($pixelCount * 0.5).' bytes (estimated)', // ~0.5 bytes per pixel for JPEG 348 | 'webp' => intval($pixelCount * 0.4).' bytes (estimated)', // ~0.4 bytes per pixel for WebP 349 | ]; 350 | } 351 | 352 | /** 353 | * Get export formats (for testing purposes) 354 | */ 355 | public function getExportFormats(): array 356 | { 357 | return $this->exportFormats; 358 | } 359 | 360 | /** 361 | * Get export options (for testing purposes) 362 | */ 363 | public function getExportOptions(): array 364 | { 365 | return $this->exportOptions; 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/Avatar.php: -------------------------------------------------------------------------------- 1 | cache = $cache ?? new ArrayStore; 94 | $this->driver = $config['driver'] ?? 'gd'; 95 | $this->theme = $config['theme'] ?? null; 96 | $this->defaultTheme = $this->validateConfig($config); 97 | $this->applyTheme($this->defaultTheme); 98 | $this->initialGenerator = new DefaultGenerator; 99 | 100 | // Set up cache configuration 101 | if (isset($config['cache'])) { 102 | $this->cacheEnabled = $config['cache']['enabled'] ?? true; 103 | $this->cacheKeyPrefix = $config['cache']['key_prefix'] ?? 'avatar_'; 104 | $this->cacheDuration = $config['cache']['duration'] ?? 86400; // 24 hours by default 105 | } 106 | 107 | // Add any additional themes for further use 108 | $themes = $this->resolveTheme('*', $config['themes'] ?? []); 109 | foreach ($themes as $name => $conf) { 110 | $this->addTheme($name, $conf); 111 | } 112 | 113 | $this->initTheme(); 114 | } 115 | 116 | /** 117 | * @return string 118 | */ 119 | public function __toString() 120 | { 121 | return (string) $this->toBase64(); 122 | } 123 | 124 | public function setGenerator(GeneratorInterface $generator): void 125 | { 126 | $this->initialGenerator = $generator; 127 | } 128 | 129 | public function create(string $name): static 130 | { 131 | $this->name = $name; 132 | 133 | $this->initTheme(); 134 | 135 | return $this; 136 | } 137 | 138 | public function applyTheme(array $config): void 139 | { 140 | $config = $this->validateConfig($config); 141 | $this->shape = $config['shape']; 142 | $this->chars = $config['chars']; 143 | $this->availableBackgrounds = $config['backgrounds']; 144 | $this->availableForegrounds = $config['foregrounds']; 145 | $this->fonts = $config['fonts']; 146 | $this->font = $this->defaultFont; 147 | $this->fontSize = $config['fontSize']; 148 | $this->width = $config['width']; 149 | $this->height = $config['height']; 150 | $this->responsive = $config['responsive']; 151 | $this->ascii = $config['ascii']; 152 | $this->uppercase = $config['uppercase']; 153 | $this->rtl = $config['rtl']; 154 | $this->borderSize = $config['border']['size']; 155 | $this->borderColor = $config['border']['color']; 156 | $this->borderRadius = $config['border']['radius']; 157 | } 158 | 159 | public function addTheme(string $name, array $config): static 160 | { 161 | $this->themes[$name] = $this->validateConfig($config); 162 | 163 | return $this; 164 | } 165 | 166 | protected function setRandomTheme(): void 167 | { 168 | $themes = $this->resolveTheme($this->theme, $this->themes); 169 | if (! empty($themes)) { 170 | $this->applyTheme($this->getRandomElement($themes, [])); 171 | } 172 | } 173 | 174 | protected function resolveTheme(array|string|null $theme, array $cfg): array 175 | { 176 | $config = collect($cfg); 177 | $themes = []; 178 | 179 | foreach ((array) $theme as $themeName) { 180 | if (! is_string($themeName)) { 181 | continue; 182 | } 183 | if ($themeName === '*') { 184 | foreach ($config as $name => $themeConfig) { 185 | $themes[$name] = $themeConfig; 186 | } 187 | } else { 188 | $themes[$themeName] = $config->get($themeName, []); 189 | } 190 | } 191 | 192 | return $themes; 193 | } 194 | 195 | public function toBase64(): string 196 | { 197 | if (! $this->cacheEnabled) { 198 | // Skip cache if it's disabled 199 | $this->buildAvatar(); 200 | 201 | return $this->image->toPng()->toDataUri(); 202 | } 203 | 204 | $key = $this->cacheKeyPrefix.$this->cacheKey(); 205 | 206 | // Check if the image is in the cache 207 | if ($base64 = $this->cache->get($key)) { 208 | return $base64; 209 | } 210 | 211 | // Generate the avatar 212 | $this->buildAvatar(); 213 | $base64 = $this->image->toPng()->toDataUri(); 214 | 215 | // Store in cache based on configured duration 216 | if ($this->cacheDuration === null) { 217 | // Cache forever 218 | $this->cache->forever($key, $base64); 219 | } else { 220 | // Cache for specified duration (in seconds) 221 | $this->cache->put($key, $base64, $this->cacheDuration); 222 | } 223 | 224 | return $base64; 225 | } 226 | 227 | public function save(?string $path, int $quality = 90): \Intervention\Image\Interfaces\ImageInterface 228 | { 229 | $this->buildAvatar(); 230 | 231 | return $this->image->save($path, $quality); 232 | } 233 | 234 | public function toSvg(): string 235 | { 236 | $this->buildInitial(); 237 | 238 | $x = $y = $this->borderSize / 2; 239 | $width = $height = $this->width - $this->borderSize; 240 | $radius = ($this->width - $this->borderSize) / 2; 241 | $center = $this->width / 2; 242 | 243 | $svg = 'responsive) { 245 | $svg .= ' width="'.$this->width.'" height="'.$this->height.'"'; 246 | } 247 | $svg .= ' viewBox="0 0 '.$this->width.' '.$this->height.'">'; 248 | 249 | if ($this->shape === 'square') { 250 | $svg .= ''; 257 | } elseif ($this->shape === 'circle') { 258 | $svg .= ''; 264 | } 265 | 266 | $svg .= ''; 273 | $svg .= $this->getInitial(); 274 | $svg .= ''; 275 | 276 | $svg .= ''; 277 | 278 | return $svg; 279 | } 280 | 281 | public function toGravatar(?array $param = null): string 282 | { 283 | // Hash generation taken from https://docs.gravatar.com/api/avatars/php/ 284 | $hash = hash('sha256', strtolower(trim($this->name))); 285 | 286 | $attributes = []; 287 | if ($this->width) { 288 | $attributes['s'] = $this->width; 289 | } 290 | 291 | if (! empty($param)) { 292 | $attributes = $param + $attributes; 293 | } 294 | 295 | $url = sprintf('https://www.gravatar.com/avatar/%s', $hash); 296 | 297 | if (! empty($attributes)) { 298 | $url .= '?'; 299 | ksort($attributes); 300 | foreach ($attributes as $key => $value) { 301 | $url .= "$key=$value&"; 302 | } 303 | $url = substr($url, 0, -1); 304 | } 305 | 306 | return $url; 307 | } 308 | 309 | public function getInitial(): string 310 | { 311 | return $this->initials; 312 | } 313 | 314 | public function getImageObject(): \Intervention\Image\Image 315 | { 316 | $this->buildAvatar(); 317 | 318 | return $this->image; 319 | } 320 | 321 | protected function getRandomBackground(): string 322 | { 323 | return $this->getRandomElement($this->availableBackgrounds, $this->background); 324 | } 325 | 326 | protected function getRandomForeground(): string 327 | { 328 | return $this->getRandomElement($this->availableForegrounds, $this->foreground); 329 | } 330 | 331 | protected function getRandomFont(): string 332 | { 333 | return $this->getRandomElement($this->fonts, $this->defaultFont); 334 | } 335 | 336 | protected function getBorderColor(): string 337 | { 338 | if ($this->borderColor === 'foreground') { 339 | return $this->foreground; 340 | } 341 | if ($this->borderColor === 'background') { 342 | return $this->background; 343 | } 344 | 345 | return $this->borderColor; 346 | } 347 | 348 | public function buildAvatar(): static 349 | { 350 | $this->buildInitial(); 351 | 352 | $x = $this->width / 2; 353 | $y = $this->height / 2; 354 | 355 | $driver = $this->driver === 'gd' ? new Driver : new ImagickDriver; 356 | $manager = new ImageManager($driver); 357 | $this->image = $manager->create($this->width, $this->height); 358 | 359 | $this->createShape(); 360 | 361 | if (empty($this->initials)) { 362 | return $this; 363 | } 364 | 365 | $this->image->text( 366 | $this->initials, 367 | (int) $x, 368 | (int) $y, 369 | function (FontFactory $font) { 370 | $font->file($this->font); 371 | $font->size($this->fontSize); 372 | $font->color($this->foreground); 373 | $font->align('center'); 374 | $font->valign('middle'); 375 | } 376 | ); 377 | 378 | return $this; 379 | } 380 | 381 | protected function createShape(): void 382 | { 383 | $method = 'create'.ucfirst($this->shape).'Shape'; 384 | if (method_exists($this, $method)) { 385 | $this->$method(); 386 | } else { 387 | throw new \InvalidArgumentException("Shape [$this->shape] currently not supported."); 388 | } 389 | } 390 | 391 | protected function createCircleShape(): void 392 | { 393 | $circleDiameter = (int) ($this->width - $this->borderSize); 394 | $x = (int) ($this->width / 2); 395 | $y = (int) ($this->height / 2); 396 | 397 | $this->image->drawCircle( 398 | $x, 399 | $y, 400 | function (CircleFactory $circle) use ($circleDiameter) { 401 | $circle->diameter($circleDiameter); 402 | $circle->border($this->getBorderColor(), $this->borderSize); 403 | $circle->background($this->background); 404 | } 405 | ); 406 | } 407 | 408 | protected function createSquareShape(): void 409 | { 410 | $edge = (ceil($this->borderSize / 2)); 411 | $x = $y = $edge; 412 | $width = $this->width - $edge; 413 | $height = $this->height - $edge; 414 | 415 | $this->image->drawRectangle( 416 | $x, 417 | $y, 418 | function (RectangleFactory $draw) use ($width, $height) { 419 | $draw->size($width, $height); 420 | $draw->background($this->background); 421 | $draw->border($this->getBorderColor(), $this->borderSize); 422 | } 423 | ); 424 | } 425 | 426 | protected function cacheKey(): string 427 | { 428 | $keys = []; 429 | $attributes = [ 430 | 'name', 431 | 'initials', 432 | 'shape', 433 | 'chars', 434 | 'font', 435 | 'fontSize', 436 | 'width', 437 | 'height', 438 | 'borderSize', 439 | 'borderColor', 440 | ]; 441 | foreach ($attributes as $attr) { 442 | $keys[] = $this->$attr; 443 | } 444 | 445 | return md5(implode('-', $keys)); 446 | } 447 | 448 | /** 449 | * @throws \Random\RandomException 450 | */ 451 | protected function getRandomElement(array $array, mixed $default): mixed 452 | { 453 | // Make it work for associative array 454 | $array = array_values($array); 455 | 456 | $name = $this->name; 457 | if ($name === null || $name === '') { 458 | $name = chr(random_int(65, 90)); 459 | } 460 | 461 | if (empty($array)) { 462 | return $default; 463 | } 464 | 465 | $number = ord($name[0]); 466 | $i = 1; 467 | $charLength = strlen($name); 468 | while ($i < $charLength) { 469 | $number += ord($name[$i]); 470 | $i++; 471 | } 472 | 473 | return $array[$number % count($array)]; 474 | } 475 | 476 | protected function buildInitial(): void 477 | { 478 | $this->initials = $this->initialGenerator->make($this->name, $this->chars, $this->uppercase, $this->ascii, $this->rtl); 479 | } 480 | 481 | protected function validateConfig(array $config): array 482 | { 483 | $fallback = [ 484 | 'shape' => 'circle', 485 | 'chars' => 2, 486 | 'backgrounds' => [$this->background], 487 | 'foregrounds' => [$this->foreground], 488 | 'fonts' => [$this->defaultFont], 489 | 'fontSize' => 48, 490 | 'width' => 100, 491 | 'height' => 100, 492 | 'responsive' => false, 493 | 'ascii' => false, 494 | 'uppercase' => false, 495 | 'rtl' => false, 496 | 'border' => [ 497 | 'size' => 1, 498 | 'color' => 'foreground', 499 | 'radius' => 0, 500 | ], 501 | ]; 502 | 503 | // Handle nested config 504 | $config['border'] = ($config['border'] ?? []) + ($this->defaultTheme['border'] ?? []) + $fallback['border']; 505 | 506 | return $config + $this->defaultTheme + $fallback; 507 | } 508 | 509 | protected function initTheme(): void 510 | { 511 | $this->setRandomTheme(); 512 | $this->setForeground($this->getRandomForeground()); 513 | $this->setBackground($this->getRandomBackground()); 514 | $this->setFont($this->getRandomFont()); 515 | } 516 | 517 | /** 518 | * Generate content-based hash for caching 519 | */ 520 | protected function generateContentHash(): string 521 | { 522 | $content = [ 523 | 'name' => $this->name, 524 | 'width' => $this->width, 525 | 'height' => $this->height, 526 | 'fontSize' => $this->fontSize, 527 | 'background' => $this->background, 528 | 'foreground' => $this->foreground, 529 | 'shape' => $this->shape, 530 | 'borderSize' => $this->borderSize, 531 | 'borderColor' => $this->borderColor, 532 | 'font' => $this->font, 533 | ]; 534 | 535 | return substr(md5(serialize($content)), 0, 8); 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /src/Concerns/StorageOptimization.php: -------------------------------------------------------------------------------- 1 | ensureStorageDirectory(); 37 | $this->loadStorageMetrics(); 38 | } 39 | 40 | /** 41 | * Ensure storage directory exists 42 | */ 43 | protected function ensureStorageDirectory(): void 44 | { 45 | if (! Storage::disk($this->storageDisk)->exists($this->storageDirectory)) { 46 | Storage::disk($this->storageDisk)->makeDirectory($this->storageDirectory); 47 | } 48 | } 49 | 50 | /** 51 | * Store avatar with optimized compression 52 | */ 53 | public function storeOptimized(string $name, string $format = 'png', array $options = []): string 54 | { 55 | $this->buildAvatar(); 56 | 57 | $filename = $this->generateOptimizedFilename($name, $format); 58 | $path = $this->storageDirectory.'/'.$filename; 59 | $fullPath = Storage::disk($this->storageDisk)->path($path); 60 | 61 | // Apply compression based on format and settings 62 | $this->applyCompression($format, $options); 63 | 64 | $options = match (strtolower($format)) { 65 | 'png' => [ 66 | 'interlaced' => $options['interlaced'] ?? true, 67 | 'indexed' => false, 68 | ], 69 | 'jpg', 'jpeg' => [ 70 | 'quality' => $options['quality'] ?? 90, 71 | 'progressive' => $options['progressive'] ?? true, 72 | 'strip' => $options['strip'] ?? true, 73 | ], 74 | 'webp' => [ 75 | 'quality' => $options['quality'] ?? 85, 76 | 'strip' => $options['strip'] ?? true, 77 | ], 78 | default => [], 79 | }; 80 | 81 | /** @var Image */ 82 | $image = $this->image; 83 | 84 | // Save the optimized image 85 | match (strtolower($format)) { 86 | 'png' => $image->toPng(...$options)->save($fullPath), 87 | 'jpg', 'jpeg' => $image->toJpeg(...$options)->save($fullPath), 88 | 'webp' => $image->toWebp(...$options)->save($fullPath), 89 | }; 90 | 91 | // Update storage metrics 92 | $this->updateStorageMetrics($path, $format); 93 | 94 | // Check storage limits and cleanup if necessary 95 | $this->checkStorageLimits(); 96 | 97 | return Storage::disk($this->storageDisk)->url($path); 98 | } 99 | 100 | /** 101 | * Apply compression optimizations 102 | */ 103 | protected function applyCompression(string $format, array $options): void 104 | { 105 | if (! $this->compressionEnabled) { 106 | return; 107 | } 108 | 109 | // Ensure image is built before applying compression 110 | if (! isset($this->image)) { 111 | $this->buildAvatar(); 112 | } 113 | 114 | switch (strtolower($format)) { 115 | case 'png': 116 | // PNG compression through color reduction if size is large 117 | if ($this->width > 512 && ! ($options['preserve_quality'] ?? false)) { 118 | $this->image->reduceColors(256); 119 | } 120 | break; 121 | 122 | case 'jpg': 123 | case 'jpeg': 124 | // JPEG progressive encoding for better loading 125 | // Note: interlace() method may not be available in all Intervention Image versions 126 | // if ($options['progressive'] ?? true) { 127 | // $this->image->interlace(); 128 | // } 129 | break; 130 | 131 | case 'webp': 132 | // WebP optimization settings are handled in the export 133 | break; 134 | } 135 | } 136 | 137 | /** 138 | * Generate optimized filename with content hash 139 | */ 140 | protected function generateOptimizedFilename(string $name, string $format): string 141 | { 142 | $hash = $this->generateContentHash(); 143 | $sanitizedName = preg_replace('/[^a-zA-Z0-9_-]/', '_', $name); 144 | $timestamp = date('Y-m-d'); 145 | 146 | return "{$sanitizedName}_{$hash}_{$timestamp}.{$format}"; 147 | } 148 | 149 | /** 150 | * Get cached avatar URL or generate new one 151 | */ 152 | public function getCachedOrGenerate(string $name, string $format = 'png', array $options = []): string 153 | { 154 | $cacheKey = $this->generateCacheKey($name, $format, $options); 155 | 156 | // Check if URL is cached 157 | if ($cachedUrl = Cache::get($cacheKey)) { 158 | // Verify file still exists 159 | $path = str_replace(Storage::disk($this->storageDisk)->url(''), '', $cachedUrl); 160 | if (Storage::disk($this->storageDisk)->exists($path)) { 161 | return $cachedUrl; 162 | } 163 | } 164 | 165 | // Generate new avatar and cache the URL 166 | $url = $this->storeOptimized($name, $format, $options); 167 | Cache::put($cacheKey, $url, Carbon::now()->addDays(7)); // Cache URL for 7 days 168 | 169 | return $url; 170 | } 171 | 172 | /** 173 | * Batch store multiple avatars with optimization 174 | */ 175 | public function batchStoreOptimized(array $names, string $format = 'png', array $options = []): array 176 | { 177 | $results = []; 178 | $startTime = microtime(true); 179 | 180 | foreach ($names as $name) { 181 | $this->create($name); 182 | $results[$name] = $this->storeOptimized($name, $format, $options); 183 | } 184 | 185 | $processingTime = microtime(true) - $startTime; 186 | 187 | // Log batch processing metrics 188 | $this->logBatchMetrics(count($names), $processingTime, $format); 189 | 190 | return $results; 191 | } 192 | 193 | /** 194 | * Clean up old and oversized files 195 | */ 196 | public function performCleanup(): array 197 | { 198 | $cleaned = [ 199 | 'old_files' => $this->cleanupOldFiles(), 200 | 'large_files' => $this->cleanupLargeFiles(), 201 | 'duplicate_files' => $this->removeDuplicateFiles(), 202 | ]; 203 | 204 | $this->rebuildStorageMetrics(); 205 | 206 | return $cleaned; 207 | } 208 | 209 | /** 210 | * Clean up files older than specified age 211 | */ 212 | protected function cleanupOldFiles(): array 213 | { 214 | $cleaned = []; 215 | $cutoffTime = Carbon::now()->subDays($this->maxFileAge)->timestamp; 216 | 217 | $files = Storage::disk($this->storageDisk)->allFiles($this->storageDirectory); 218 | 219 | foreach ($files as $file) { 220 | $lastModified = Storage::disk($this->storageDisk)->lastModified($file); 221 | 222 | if ($lastModified < $cutoffTime) { 223 | Storage::disk($this->storageDisk)->delete($file); 224 | $cleaned[] = $file; 225 | } 226 | } 227 | 228 | return $cleaned; 229 | } 230 | 231 | /** 232 | * Clean up files if storage size exceeds limit 233 | */ 234 | protected function cleanupLargeFiles(): array 235 | { 236 | $cleaned = []; 237 | $currentSize = $this->getTotalStorageSize(); 238 | 239 | if ($currentSize <= $this->maxStorageSize * 1024 * 1024) { 240 | return $cleaned; // No cleanup needed 241 | } 242 | 243 | // Get files sorted by size (largest first) 244 | $files = $this->getFilesSortedBySize(); 245 | $targetReduction = $currentSize - ($this->maxStorageSize * 1024 * 1024 * 0.8); // Reduce to 80% of limit 246 | $reducedSize = 0; 247 | 248 | foreach ($files as $file) { 249 | if ($reducedSize >= $targetReduction) { 250 | break; 251 | } 252 | 253 | $fileSize = Storage::disk($this->storageDisk)->size($file['path']); 254 | Storage::disk($this->storageDisk)->delete($file['path']); 255 | $cleaned[] = $file['path']; 256 | $reducedSize += $fileSize; 257 | } 258 | 259 | return $cleaned; 260 | } 261 | 262 | /** 263 | * Remove duplicate files based on content hash 264 | */ 265 | protected function removeDuplicateFiles(): array 266 | { 267 | $cleaned = []; 268 | $hashes = []; 269 | 270 | $files = Storage::disk($this->storageDisk)->allFiles($this->storageDirectory); 271 | 272 | foreach ($files as $file) { 273 | $content = Storage::disk($this->storageDisk)->get($file); 274 | $hash = md5($content); 275 | 276 | if (isset($hashes[$hash])) { 277 | // Duplicate found, remove the newer file 278 | $existingFile = $hashes[$hash]; 279 | $existingTime = Storage::disk($this->storageDisk)->lastModified($existingFile); 280 | $currentTime = Storage::disk($this->storageDisk)->lastModified($file); 281 | 282 | if ($currentTime > $existingTime) { 283 | Storage::disk($this->storageDisk)->delete($file); 284 | $cleaned[] = $file; 285 | } else { 286 | Storage::disk($this->storageDisk)->delete($existingFile); 287 | $cleaned[] = $existingFile; 288 | $hashes[$hash] = $file; 289 | } 290 | } else { 291 | $hashes[$hash] = $file; 292 | } 293 | } 294 | 295 | return $cleaned; 296 | } 297 | 298 | /** 299 | * Get total storage size in bytes 300 | */ 301 | protected function getTotalStorageSize(): int 302 | { 303 | $totalSize = 0; 304 | $files = Storage::disk($this->storageDisk)->allFiles($this->storageDirectory); 305 | 306 | foreach ($files as $file) { 307 | $totalSize += Storage::disk($this->storageDisk)->size($file); 308 | } 309 | 310 | return $totalSize; 311 | } 312 | 313 | /** 314 | * Get files sorted by size 315 | */ 316 | protected function getFilesSortedBySize(): array 317 | { 318 | $files = []; 319 | $allFiles = Storage::disk($this->storageDisk)->allFiles($this->storageDirectory); 320 | 321 | foreach ($allFiles as $file) { 322 | $files[] = [ 323 | 'path' => $file, 324 | 'size' => Storage::disk($this->storageDisk)->size($file), 325 | ]; 326 | } 327 | 328 | // Sort by size (largest first) 329 | usort($files, fn ($a, $b) => $b['size'] <=> $a['size']); 330 | 331 | return $files; 332 | } 333 | 334 | /** 335 | * Update storage metrics 336 | */ 337 | protected function updateStorageMetrics(string $path, string $format): void 338 | { 339 | $size = Storage::disk($this->storageDisk)->size($path); 340 | 341 | $this->storageMetrics['total_files'] = ($this->storageMetrics['total_files'] ?? 0) + 1; 342 | $this->storageMetrics['total_size'] = ($this->storageMetrics['total_size'] ?? 0) + $size; 343 | $this->storageMetrics['formats'][$format] = ($this->storageMetrics['formats'][$format] ?? 0) + 1; 344 | $this->storageMetrics['last_updated'] = Carbon::now()->toISOString(); 345 | 346 | // Persist metrics to cache 347 | Cache::put($this->getMetricsCacheKey(), $this->storageMetrics, Carbon::now()->addHours(1)); 348 | } 349 | 350 | /** 351 | * Load storage metrics from cache 352 | */ 353 | protected function loadStorageMetrics(): void 354 | { 355 | $cachedMetrics = Cache::get($this->getMetricsCacheKey()); 356 | 357 | $this->storageMetrics = is_array($cachedMetrics) ? $cachedMetrics : [ 358 | 'total_files' => 0, 359 | 'total_size' => 0, 360 | 'formats' => [], 361 | 'last_updated' => Carbon::now()->toISOString(), 362 | ]; 363 | } 364 | 365 | /** 366 | * Rebuild storage metrics by scanning all files 367 | */ 368 | protected function rebuildStorageMetrics(): void 369 | { 370 | $metrics = [ 371 | 'total_files' => 0, 372 | 'total_size' => 0, 373 | 'formats' => [], 374 | 'last_updated' => Carbon::now()->toISOString(), 375 | ]; 376 | 377 | $files = Storage::disk($this->storageDisk)->allFiles($this->storageDirectory); 378 | 379 | foreach ($files as $file) { 380 | $size = Storage::disk($this->storageDisk)->size($file); 381 | $format = pathinfo($file, PATHINFO_EXTENSION); 382 | 383 | $metrics['total_files']++; 384 | $metrics['total_size'] += $size; 385 | $metrics['formats'][$format] = ($metrics['formats'][$format] ?? 0) + 1; 386 | } 387 | 388 | $this->storageMetrics = $metrics; 389 | Cache::put($this->getMetricsCacheKey(), $metrics, Carbon::now()->addHours(1)); 390 | } 391 | 392 | /** 393 | * Check storage limits and trigger cleanup if needed 394 | */ 395 | protected function checkStorageLimits(): void 396 | { 397 | $currentSize = $this->getTotalStorageSize(); 398 | $limitBytes = $this->maxStorageSize * 1024 * 1024; 399 | 400 | if ($currentSize > $limitBytes) { 401 | $this->performCleanup(); 402 | } 403 | } 404 | 405 | /** 406 | * Log batch processing metrics 407 | */ 408 | protected function logBatchMetrics(int $count, float $processingTime, string $format): void 409 | { 410 | $metrics = [ 411 | 'batch_size' => $count, 412 | 'processing_time' => $processingTime, 413 | 'avg_time_per_avatar' => $processingTime / $count, 414 | 'format' => $format, 415 | 'timestamp' => Carbon::now()->toISOString(), 416 | ]; 417 | 418 | Cache::put('avatar_batch_metrics_'.time(), $metrics, Carbon::now()->addDays(7)); 419 | } 420 | 421 | /** 422 | * Generate cache key for URL caching 423 | */ 424 | protected function generateCacheKey(string $name, string $format, array $options): string 425 | { 426 | $data = [ 427 | 'name' => $name, 428 | 'format' => $format, 429 | 'width' => $this->width, 430 | 'height' => $this->height, 431 | 'options' => $options, 432 | ]; 433 | 434 | return 'avatar_url_'.md5(serialize($data)); 435 | } 436 | 437 | /** 438 | * Get metrics cache key 439 | */ 440 | protected function getMetricsCacheKey(): string 441 | { 442 | return 'avatar_storage_metrics'; 443 | } 444 | 445 | /** 446 | * Get storage statistics 447 | */ 448 | public function getStorageStatistics(): array 449 | { 450 | $this->loadStorageMetrics(); 451 | 452 | return [ 453 | 'total_files' => $this->storageMetrics['total_files'], 454 | 'total_size_bytes' => $this->storageMetrics['total_size'], 455 | 'total_size_mb' => round($this->storageMetrics['total_size'] / 1024 / 1024, 2), 456 | 'formats' => $this->storageMetrics['formats'], 457 | 'storage_limit_mb' => $this->maxStorageSize, 458 | 'usage_percentage' => round(($this->storageMetrics['total_size'] / ($this->maxStorageSize * 1024 * 1024)) * 100, 2), 459 | 'last_updated' => $this->storageMetrics['last_updated'], 460 | 'disk' => $this->storageDisk, 461 | 'directory' => $this->storageDirectory, 462 | ]; 463 | } 464 | 465 | /** 466 | * Set storage configuration 467 | */ 468 | public function configureStorage(string $disk, string $directory, int $maxSizeMB = 500): static 469 | { 470 | $this->storageDisk = $disk; 471 | $this->storageDirectory = $directory; 472 | $this->maxStorageSize = $maxSizeMB; 473 | 474 | $this->initializeStorage(); 475 | 476 | return $this; 477 | } 478 | 479 | /** 480 | * Enable or disable compression 481 | */ 482 | public function setCompressionEnabled(bool $enabled): static 483 | { 484 | $this->compressionEnabled = $enabled; 485 | 486 | return $this; 487 | } 488 | 489 | /** 490 | * Set maximum file age for cleanup 491 | */ 492 | public function setMaxFileAge(int $days): static 493 | { 494 | $this->maxFileAge = $days; 495 | 496 | return $this; 497 | } 498 | 499 | /** 500 | * Get storage disk (for testing purposes) 501 | */ 502 | public function getStorageDisk(): string 503 | { 504 | return $this->storageDisk; 505 | } 506 | 507 | /** 508 | * Get storage directory (for testing purposes) 509 | */ 510 | public function getStorageDirectory(): string 511 | { 512 | return $this->storageDirectory; 513 | } 514 | 515 | /** 516 | * Get max storage size (for testing purposes) 517 | */ 518 | public function getMaxStorageSize(): int 519 | { 520 | return $this->maxStorageSize; 521 | } 522 | 523 | /** 524 | * Get max file age (for testing purposes) 525 | */ 526 | public function getMaxFileAge(): int 527 | { 528 | return $this->maxFileAge; 529 | } 530 | 531 | /** 532 | * Get compression enabled status (for testing purposes) 533 | */ 534 | public function getCompressionEnabled(): bool 535 | { 536 | return $this->compressionEnabled; 537 | } 538 | 539 | /** 540 | * Get storage metrics (for testing purposes) 541 | */ 542 | public function getStorageMetrics(): array 543 | { 544 | return $this->storageMetrics; 545 | } 546 | } 547 | --------------------------------------------------------------------------------