├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── color_extractor.php └── src ├── ColorExtractorModifier.php └── ColorExtractorServiceProvider.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: aryehraber 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | node_modules/ 4 | vendor 5 | package.json 6 | mix-manifest.json 7 | webpack.config.js 8 | webpack.mix.js 9 | gulpfile.js 10 | yarn.lock 11 | composer.lock 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aryeh Raber 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Color Extractor 2 | 3 | **Extract colors from images.** 4 | 5 | This addon provides a new Modifier which takes an image asset and returns its dominant (or average) color as a HEX value. 6 | 7 | ![Color Extractor](https://user-images.githubusercontent.com/5065331/79727966-7b8e3a00-82ed-11ea-870a-8a5f4e0d05e8.jpg) 8 | 9 | ## Installation 10 | 11 | Install the addon via composer: 12 | 13 | ``` 14 | composer require aryehraber/statamic-color-extractor 15 | ``` 16 | 17 | Publish the config file: 18 | 19 | ``` 20 | php artisan vendor:publish --tag=color_extractor-config 21 | ``` 22 | 23 | ## Usage 24 | 25 | Simply add the `color` modifier to an image asset to output the HEX color value: 26 | 27 | ```html 28 | {{ image | color }} 29 | ``` 30 | 31 | **Example** 32 | 33 | ```html 34 | --- 35 | image: my-colorful-image.jpg 36 | --- 37 | 38 |
39 | 40 |
41 | 42 | // OR 43 | 44 | {{ image }} 45 |
46 | 47 |
48 | {{ /image }} 49 | ``` 50 | 51 | By default, the underlying color extractor tries to find the most dominant color in the image, however, results can vary (see example screenshot below). Therefore, an `average` param can be passed in to instead find the average color found in the image: 52 | 53 | ```html 54 | {{ image | color:average }} 55 | ``` 56 | 57 | The default type can be changed to `average` instead via the config file, which opens up a `dominant` param: 58 | 59 | ```html 60 | {{ image | color:dominant }} 61 | ``` 62 | 63 | The `contrast` parameter will try to find a color from the image palette with the most contrast to the dominant color: 64 | 65 | ```html 66 | {{ image | color:contrast }} 67 | ``` 68 | 69 | ### Dominant vs. Average vs. Contrast 70 | 71 | Example screenshot to demonstrate the difference between the color extraction strategies: 72 | 73 | ![Color Extractor Diff](https://user-images.githubusercontent.com/5065331/98469153-3c1e3100-21de-11eb-9b04-3a82baa19bd4.jpg) 74 | 75 | ### Manually Editing Colors 76 | 77 | Whenever a color is extracted from an image, it's added to the asset's meta data. This means you can manually override it by adding the following fields to your `assets.yaml` blueprint: 78 | 79 | ```yaml 80 | title: Asset 81 | fields: 82 | # existing fields 83 | - 84 | handle: color_dominant 85 | field: 86 | display: Dominant Color 87 | type: color 88 | - 89 | handle: color_average 90 | field: 91 | display: Average Color 92 | type: color 93 | - 94 | handle: color_contrast 95 | field: 96 | display: Contrast Color 97 | type: color 98 | ``` 99 | 100 | ## Credits 101 | 102 | Inspiration: https://github.com/sylvainjule/kirby-colorextractor 103 | 104 | Color Extractor: https://github.com/thephpleague/color-extractor 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aryehraber/statamic-color-extractor", 3 | "type": "statamic-addon", 4 | "description": "Extract colors from images.", 5 | "keywords": [ 6 | "statamic", 7 | "image color", 8 | "color extractor" 9 | ], 10 | "homepage": "https://github.com/aryehraber/statamic-color-extractor", 11 | "license": "MIT", 12 | "require": { 13 | "jonaskohl/color-extractor": "^0.4", 14 | "davidgorges/color-contrast": "^1.0", 15 | "statamic/cms": "^4.0 || ^5.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "AryehRaber\\ColorExtractor\\": "src" 20 | } 21 | }, 22 | "authors": [ 23 | { 24 | "name": "Aryeh Raber", 25 | "email": "aryeh.raber@gmail.com", 26 | "homepage": "https://aryeh.dev", 27 | "role": "Developer" 28 | } 29 | ], 30 | "extra": { 31 | "statamic": { 32 | "name": "ColorExtractor", 33 | "slug": "color_extractor", 34 | "description": "Extract colors from images." 35 | }, 36 | "laravel": { 37 | "providers": [ 38 | "AryehRaber\\ColorExtractor\\ColorExtractorServiceProvider" 39 | ] 40 | } 41 | }, 42 | "minimum-stability": "dev", 43 | "prefer-stable": true, 44 | "config": { 45 | "allow-plugins": { 46 | "pixelfear/composer-dist-plugin": true 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /config/color_extractor.php: -------------------------------------------------------------------------------- 1 | 500, 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Fallback Color 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Transparent images require a fallback color since it isn't possible 23 | | to extract from a color that has an alpha value. 24 | | 25 | */ 26 | 'fallback' => '#000000', 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Default Color Extraction Strategy 31 | |-------------------------------------------------------------------------- 32 | | 33 | | The are three color extraction strategies: 34 | | 35 | | - "dominant" (used by default) analyses all pixels in the image and calculates 36 | | the most dominant color 37 | | - "contrast" will try to find a color from palette with the most contrast to 38 | | the dominant color 39 | | - "average" reduces the image down to a tiny size and extracts its color 40 | | 41 | | Supported: "dominant", "contrast", "average" 42 | | 43 | */ 44 | 'default_type' => 'dominant', 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Temp. Storage Directory 49 | |-------------------------------------------------------------------------- 50 | | 51 | | During image processing, the image needs to temporarily be stored on 52 | | the filesystem, once done it will automatically be removed. 53 | | 54 | */ 55 | 'temp_dir' => storage_path('color_extractor'), 56 | 57 | ]; 58 | -------------------------------------------------------------------------------- /src/ColorExtractorModifier.php: -------------------------------------------------------------------------------- 1 | init($value, $params)) return; 33 | 34 | if ($color = Arr::get($this->asset->meta(), "data.color_{$this->type}")) { 35 | return $color; 36 | } 37 | 38 | $color = $this->getColor(); 39 | 40 | $this->updateAssetMeta($color); 41 | 42 | $this->cleanUp(); 43 | 44 | return $color; 45 | } 46 | 47 | protected function init($value, $params) 48 | { 49 | $this->asset = Asset::find($value); 50 | 51 | $this->type = in_array(Arr::get($params, 0), self::$strategies) 52 | ? Arr::get($params, 0) 53 | : config('color_extractor.default_type'); 54 | 55 | if (! $this->asset instanceof Asset) { 56 | return false; 57 | } 58 | 59 | if (! $this->asset->isImage()) { 60 | return false; 61 | } 62 | 63 | return true; 64 | } 65 | 66 | protected function getColor() 67 | { 68 | $this->img = $this->processImage($this->asset); 69 | 70 | $palette = Palette::fromFilename($this->tempImgPath()); 71 | 72 | $extractor = new ColorExtractor($palette, Color::fromHexToInt(config('color_extractor.fallback'))); 73 | 74 | $strategyMethod = sprintf('get%sColor', Str::title($this->type)); 75 | 76 | return $this->{$strategyMethod}($extractor); 77 | } 78 | 79 | public function getDominantColor($extractor) 80 | { 81 | return Color::fromIntToHex($extractor->extract(1)[0]); 82 | } 83 | 84 | public function getAverageColor($extractor) 85 | { 86 | return Color::fromIntToHex($extractor->extract(1)[0]); 87 | } 88 | 89 | public function getContrastColor($extractor) 90 | { 91 | $colors = collect($extractor->extract(5))->map(function ($int) { 92 | return Color::fromIntToHex($int); 93 | }); 94 | 95 | $dominant = $colors->get(0); 96 | 97 | $contrast = new ColorContrast(); 98 | $contrast->addColors($colors->toArray()); 99 | 100 | $combinations = $contrast->getCombinations(ColorContrast::MIN_CONTRAST_AA); 101 | $complementary = collect($combinations) 102 | ->filter(function ($combination) use ($dominant) { 103 | return in_array($dominant, [ 104 | '#'.$combination->getBackground(), 105 | ]); 106 | }) 107 | ->sortByDesc(function ($combination) { 108 | return $combination->getContrast(); 109 | }) 110 | ->get(0); 111 | 112 | return $complementary ? '#'.$complementary->getForeground() : $colors->get(1); 113 | } 114 | 115 | protected function processImage() 116 | { 117 | if (! Folder::exists($tempDir = config('color_extractor.temp_dir'))) { 118 | Folder::makeDirectory($tempDir); 119 | } 120 | 121 | $path = strpos($this->asset->url(), 'http') === 0 122 | ? $this->asset->absoluteUrl() 123 | : $this->asset->resolvedPath(); 124 | 125 | $image = Image::make($path); 126 | [$width, $height] = $this->resizeDimensions(); 127 | 128 | $image->resize($width, $height, function ($constraint) { 129 | $constraint->aspectRatio(); 130 | }); 131 | 132 | return $image->save("{$tempDir}/{$this->asset->basename()}"); 133 | } 134 | 135 | protected function tempImgPath() 136 | { 137 | if (! $this->img) { 138 | return null; 139 | } 140 | 141 | return "{$this->img->dirname}/{$this->img->basename}"; 142 | } 143 | 144 | protected function resizeDimensions() 145 | { 146 | $size = $this->type === 'average' ? 2 : config('color_extractor.accuracy'); 147 | 148 | if ($this->asset->orientation() === 'square') { 149 | return [$size, $size]; 150 | } 151 | 152 | return [ 153 | $this->asset->orientation() === 'landscape' ? $size : null, // width 154 | $this->asset->orientation() === 'portrait' ? $size : null, // height 155 | ]; 156 | } 157 | 158 | protected function updateAssetMeta($color) 159 | { 160 | $meta = $this->asset->meta(); 161 | 162 | Arr::set($meta, "data.color_{$this->type}", $color); 163 | 164 | $this->asset->writeMeta($meta); 165 | 166 | Cache::forget($this->asset->metaCacheKey()); 167 | } 168 | 169 | protected function cleanUp() 170 | { 171 | if (File::exists($this->tempImgPath())) { 172 | File::delete($this->tempImgPath()); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/ColorExtractorServiceProvider.php: -------------------------------------------------------------------------------- 1 |