├── .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 | 
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 | 
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 |