├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json ├── test.js └── test ├── car.jpg ├── css ├── deep │ └── sample.css ├── sample.css └── url.css ├── img └── girl.png └── white.jpg /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | 3 | node_modules/ 4 | npm-debug.log 5 | 6 | .travis.yml 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - stable 5 | - "8" 6 | - "6" 7 | - "4" 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## v0.2.0, v.0.2.1 - 20170929 6 | 7 | - Improve warning message when color is missing 8 | - Upgrade to PostCSS v6.0.11 9 | - Upgrade to AVA v0.22.0 10 | - Fix image path to be relative [#2](https://github.com/ismamz/postcss-get-color/issues/2) 11 | - Fix some typo issues 12 | - Add `test` folder and compress sample images 13 | - Break v0.12 of NodeJS 14 | 15 | ## v0.1.0 - 20160412 16 | 17 | - Initial release 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2017 Ismael Martínez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Get Color [![Build Status][ci-img]][ci] 2 | 3 | 4 | 5 | > [PostCSS] plugin to get the prominent colors from an image 6 | 7 | The plugin uses [Vibrant.js] and the [node port](https://github.com/akfish/node-vibrant) (node-vibrant). [Vibrant.js] is a javascript port of the awesome [Palette class](https://developer.android.com/reference/android/support/v7/graphics/Palette.html) in the Android support library. 8 | 9 | [Vibrant.js]: https://github.com/jariz/vibrant.js/ 10 | [PostCSS]: https://github.com/postcss/postcss 11 | [ci-img]: https://travis-ci.org/ismamz/postcss-get-color.svg 12 | [ci]: https://travis-ci.org/ismamz/postcss-get-color 13 | 14 | ## Material Design Example 15 | 16 | 17 | 18 | 19 | > Based on the [standards of material design](https://material.io/guidelines/style/color.html), the palette library extracts commonly used color profiles from an image. Each profile is defined by a Target, and colors extracted from the bitmap image are scored against each profile based on saturation, luminance, and population (number of pixels in the bitmap represented by the color). For each profile, the color with the best score defines that color profile for the given image. 20 | 21 | Source: [Android Developers - Extract Color Profiles](https://developer.android.com/training/material/palette-colors.html#extract-color-profiles) 22 | 23 | | Input | Output | 24 | |:-------------:|:-------------:| 25 | | | | 26 | | `color: get-color("../img/girl.png", Vibrant);` | `color: #e8ba3c;` | 27 | 28 | 29 | --- 30 | 31 | 32 | ### CSS Input 33 | 34 | ```css 35 | .foo { 36 | background-color: get-color("path/to/image.jpg", LightVibrant) url("path/to/image.jpg) no-repeat; 37 | } 38 | 39 | .bar { 40 | color: get-color("path/to/image.png"); 41 | } 42 | ``` 43 | 44 | ### CSS Output 45 | 46 | ```css 47 | .foo { 48 | background-color: #b9911b url("path/to/image.jpg") no-repeat; 49 | } 50 | 51 | .bar { 52 | color: #b9911b; 53 | } 54 | ``` 55 | 56 | ## Features 57 | 58 | ``` 59 | get-color(, [, ]) 60 | ``` 61 | 62 | **image-path** `string`: path to image relative to the CSS file (with quotes). 63 | 64 | **color-name** `string`: name (case sensitive) from the palette ([see available names](#vibrant-palette)).
_Default:_ first available color in the palette. 65 | 66 | **text-color** `[title|body]`: get the compatible foreground color. 67 | 68 | 69 | Use color format in `hex`, `rgb` or `rgba` ([see Options](#options)). 70 | 71 | 72 | ## Vibrant Palette 73 | 74 | See examples in [Vibrant.js Page](http://jariz.github.io/vibrant.js/). 75 | 76 | - Vibrant 77 | - DarkVibrant 78 | - LightVibrant 79 | - Muted 80 | - DarkMuted 81 | - LightMuted 82 | 83 | **Note:** colors are writing in `PascalCase`. 84 | 85 | You can get the title text color that works best with any **'title'** text that is used over this swatch's color, and the body text color that works best with any **'body'** text that is used over this swatch's color. 86 | 87 | #### After 88 | 89 | ```css 90 | .foo { 91 | color: get-color("path/to/image.jpg", LightVibrant, text); 92 | } 93 | ``` 94 | 95 | #### Before 96 | 97 | ```css 98 | .foo { 99 | color: #000; /* or #fff */ 100 | } 101 | ``` 102 | 103 | ## Usage 104 | 105 | ### Quick usage 106 | 107 | Using [PostCSS CLI](https://github.com/postcss/postcss-cli) you can do the following: 108 | 109 | First, install `postcss-cli` and the plugin on your project folder: 110 | 111 | ``` 112 | $ npm install postcss-cli postcss-get-color --save-dev 113 | ``` 114 | 115 | And finally add this script to your `package.json`: 116 | 117 | ```json 118 | "scripts": { 119 | "postcss": "postcss input.css -u postcss-get-color -o output.css -w" 120 | } 121 | ``` 122 | 123 | After this you can run `npm run postcss` and transform your `input.css` into `output.css`. Note that `-w` is for observe file system changes and recompile as source files change. 124 | 125 | ### For tasks runners and others enviroments 126 | 127 | ```js 128 | postcss([ require('postcss-get-color')({ /* options*/ }) ]) 129 | ``` 130 | 131 | See [PostCSS] docs for examples of your environment. 132 | 133 | ## Options 134 | 135 | ##### `format` 136 | 137 | Type: `string` 138 | 139 | Default: `hex` 140 | 141 | Select the color format between: `hex`, `rgb`, `rgba`. 142 | 143 | ## Contributing 144 | 145 | If you want to improve the plugin, [send a pull request](https://github.com/ismamz/postcss-get-color/pull/new/master) ;-) 146 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var postcss = require('postcss'); 3 | var parser = require('postcss-value-parser'); 4 | var vibrant = require('node-vibrant'); 5 | 6 | var colorNames = [ 7 | 'Vibrant', 8 | 'Muted', 9 | 'DarkVibrant', 10 | 'DarkMuted', 11 | 'LightVibrant', 12 | 'LightMuted' 13 | ]; 14 | 15 | function isUrl(string) { 16 | var matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/; 17 | return matcher.test(string); 18 | } 19 | 20 | function getAvailable(palette) { 21 | var i = 0; 22 | var availableColors = []; 23 | while (i < colorNames.length) { 24 | if (palette[colorNames[i]] !== null) { 25 | availableColors.push({ 26 | colorName: colorNames[i], 27 | colorValue: palette[colorNames[i]].getHex() 28 | }); 29 | } 30 | i++; 31 | } 32 | return availableColors; 33 | } 34 | 35 | function formatColor(format, palette, name) { 36 | var color, colorRgb; 37 | switch (format) { 38 | case 'hex': 39 | color = palette[name].getHex(); 40 | break; 41 | case 'rgb': 42 | colorRgb = palette[name].getRgb(); 43 | color = 'rgb(' + colorRgb[0] + ', ' + 44 | colorRgb[1] + ', ' + colorRgb[2] + ')'; 45 | break; 46 | case 'rgba': 47 | colorRgb = palette[name].getRgb(); 48 | color = 'rgba(' + colorRgb[0] + ', ' + 49 | colorRgb[1] + ', ' + colorRgb[2] + ', 1)'; 50 | break; 51 | default: 52 | color = palette[name].getHex(); 53 | break; 54 | } 55 | return color; 56 | } 57 | 58 | function getTextColor(palette, name, text) { 59 | var color; 60 | switch (text) { 61 | case 'title': 62 | color = palette[name].getTitleTextColor(); 63 | break; 64 | case 'body': 65 | color = palette[name].getBodyTextColor(); 66 | break; 67 | default: 68 | return false; 69 | } 70 | return color; 71 | } 72 | 73 | function procDecl(image, name, decl, nodes, text, format, result) { 74 | return new Promise(function (resolve, reject) { 75 | var imagePath; 76 | 77 | if (isUrl(image) || path.isAbsolute(image) || !decl.source.input.file) { 78 | imagePath = image; 79 | } else { 80 | imagePath = path.resolve(path.dirname(decl.source.input.file), image); 81 | } 82 | 83 | vibrant.from(imagePath).getPalette(function (err, palette) { 84 | var color; 85 | 86 | if (!err) { 87 | if (!(name in palette) || palette[name] !== null ) { 88 | 89 | if (text === 'title' || text === 'body') { 90 | color = getTextColor(palette, name, text); 91 | } else { 92 | color = formatColor(format, palette, name); 93 | } 94 | 95 | } else { 96 | var availableColors = getAvailable(palette); 97 | var newName = availableColors[0].colorName; 98 | 99 | if (text === 'title' || text === 'body') { 100 | color = getTextColor(palette, newName, text); 101 | } else { 102 | color = formatColor(format, palette, newName); 103 | } 104 | 105 | // Warning: color is not in the palette 106 | var availableColorsNames = availableColors.map(function (c) { 107 | return c.colorName; 108 | }); 109 | 110 | decl.warn( 111 | result, name + ' isn\'t available. ' + 112 | 'First available color was used: ' + 113 | availableColorsNames.join(', ') 114 | ); 115 | } 116 | 117 | nodes.walk(function (node) { 118 | if (node.type === 'function' && 119 | node.value === 'get-color') { 120 | node.type = 'word'; 121 | node.value = color; 122 | node.nodes = null; 123 | } 124 | }); 125 | 126 | decl.value = nodes.toString(); 127 | resolve(color); 128 | } else { 129 | reject(err); 130 | throw decl.error('Problem with "' + image + 131 | '" file.', { plugin: 'postcss-get-color' }); 132 | } 133 | }); 134 | }); 135 | } 136 | 137 | module.exports = postcss.plugin('postcss-get-color', function (opts) { 138 | return function (css, result) { 139 | opts = opts || {}; 140 | 141 | var format = 'format' in opts ? opts.format : 'hex'; // rgb or rgba 142 | 143 | const promises = []; 144 | 145 | css.walkRules(function (rule) { 146 | rule.walkDecls(function (decl) { 147 | var value = decl.value; 148 | 149 | var name, image, text; 150 | 151 | var str = parser(value); 152 | str.walk(function (node) { 153 | if (node.type === 'function' && 154 | node.value === 'get-color') { 155 | 156 | // Get the image path 157 | if (node.nodes[0].type === 'string') { 158 | image = node.nodes[0].value; 159 | } else { 160 | throw decl.error('Missed quotes in first argument'); 161 | } 162 | 163 | // Check if has 2 args and get the nameColor 164 | if (node.nodes.length > 2 && 165 | node.nodes[2].type === 'word') { 166 | name = node.nodes[2].value; 167 | } else { 168 | name = 'Vibrant'; 169 | } 170 | 171 | // Text Color as third arg 172 | if (node.nodes.length === 5 && 173 | node.nodes[4].type === 'word') { 174 | 175 | text = node.nodes[4].value; 176 | 177 | if (text !== 'title' && text !== 'body') { 178 | throw decl.error('Invalid text color ' + name); 179 | } 180 | } else { 181 | text = false; 182 | } 183 | 184 | // Check if is a valid name color before process 185 | if (colorNames.indexOf(name) > -1) { 186 | promises.push( 187 | procDecl(image, name, decl, str, text, format, result) 188 | ); 189 | } else { 190 | throw decl.error('Unknown name color: ' + name); 191 | } 192 | 193 | } 194 | }); 195 | }); 196 | }); 197 | 198 | return Promise.all(promises).then( 199 | function (response) { 200 | console.log('\n\npostcss-get-color processed ' + response.length + ' images\n'); 201 | }, 202 | function (err) { 203 | throw err; 204 | } 205 | ); 206 | }; 207 | }); 208 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-get-color", 3 | "version": "0.2.0", 4 | "description": "PostCSS plugin to get the prominent colors from an image", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "color", 10 | "dominant", 11 | "background-color" 12 | ], 13 | "author": "ismamz ", 14 | "license": "MIT", 15 | "repository": "ismamz/postcss-get-color", 16 | "bugs": { 17 | "url": "https://github.com/ismamz/postcss-get-color/issues" 18 | }, 19 | "homepage": "https://github.com/ismamz/postcss-get-color", 20 | "dependencies": { 21 | "node-vibrant": "^2.1.2", 22 | "path": "^0.12.7", 23 | "postcss": "^6.0.11", 24 | "postcss-value-parser": "^3.3.0" 25 | }, 26 | "devDependencies": { 27 | "ava": "^0.22.0", 28 | "eslint": "^1.10.3", 29 | "eslint-config-postcss": "^1.0.0", 30 | "fs": "0.0.1-security" 31 | }, 32 | "scripts": { 33 | "test": "ava && eslint *.js" 34 | }, 35 | "eslintConfig": { 36 | "extends": "postcss/es5", 37 | "rules": { 38 | "max-len": [ 39 | 2, 40 | 100 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import test from 'ava'; 3 | import fs from 'fs'; 4 | 5 | import plugin from './'; 6 | 7 | function run(t, input, output, opts = { }) { 8 | return postcss([ plugin(opts) ]).process(input) 9 | .then( result => { 10 | t.deepEqual(result.css, output); 11 | t.deepEqual(result.warnings().length, 0); 12 | }); 13 | } 14 | 15 | function runFile(t, input, output, opts = { }) { 16 | return postcss([ plugin(opts) ]).process(fs.readFileSync(input), { from: input }) 17 | .then( result => { 18 | t.deepEqual(result.css, output); 19 | t.deepEqual(result.warnings().length, 0); 20 | }); 21 | } 22 | 23 | function runWithWarn(t, input, output, opts = { }) { 24 | return postcss([ plugin(opts) ]).process(input) 25 | .then( result => { 26 | t.deepEqual(result.css, output); 27 | t.deepEqual(result.warnings().length, 1); 28 | }); 29 | } 30 | 31 | test('Always pass', t => { 32 | return run(t, 'a { }', 33 | 'a { }', { }); 34 | }); 35 | 36 | test('LightMuted twice', t => { 37 | return run(t, 'a { background: get-color("test/car.jpg", LightMuted); ' + 38 | 'color: get-color("test/white.jpg", LightMuted); }', 39 | 'a { background: #e1e1e1; color: #fcfcfc; }', { }); 40 | }); 41 | 42 | test('Vibrant by arg', t => { 43 | return run(t, 'a { background: get-color("test/img/girl.png", Vibrant); }', 44 | 'a { background: #e8ba3c; }', { }); 45 | }); 46 | 47 | var url = 'http://jariz.github.io/vibrant.js/examples/2.jpg'; 48 | test('Vibrant by Url', t => { 49 | return run(t, 'a { background: get-color("' + url + '", Vibrant); }', 50 | 'a { background: #b9911b; }', { }); 51 | }); 52 | 53 | test('Title color', t => { 54 | return run(t, 'a { background: get-color("test/car.jpg", Vibrant, title); }', 55 | 'a { background: #fff; }', { }); 56 | }); 57 | 58 | test('Body color with warning', t => { 59 | return runWithWarn(t, 'a { background: get-color("test/white.jpg", LightVibrant, body); }', 60 | 'a { background: #000; }', { }); 61 | }); 62 | 63 | test('Body color without warning', t => { 64 | return run(t, 'a { background: get-color("test/white.jpg", LightMuted, body); }', 65 | 'a { background: #000; }', { }); 66 | }); 67 | 68 | test('Vibrant by default', t => { 69 | return run(t, 'a { background: get-color("test/car.jpg"); }', 70 | 'a { background: #b79022; }', { }); 71 | }); 72 | 73 | test('Works with single quotes', t => { 74 | return run(t, 'a { background: get-color(\'test/car.jpg\'); }', 75 | 'a { background: #b79022; }', { }); 76 | }); 77 | 78 | test('White image take LighMuted by default', t => { 79 | return runWithWarn(t, 'a { background: get-color("test/white.jpg")' + 80 | ' url("car.jpg") no-repeat; }', 81 | 'a { background: #fcfcfc url("car.jpg") no-repeat; }', { }); 82 | }); 83 | 84 | test('Multiple value', t => { 85 | return runWithWarn(t, 'a { background: get-color("test/white.jpg")' + 86 | ' url("car.jpg") no-repeat; }', 87 | 'a { background: #fcfcfc url("car.jpg") no-repeat; }', { }); 88 | }); 89 | 90 | test('Relative path in file', t => { 91 | return runFile(t, './test/css/sample.css', 92 | '.bg { background: url("../img/girl.jpg") #e8ba3c; }\n', { }); 93 | }); 94 | 95 | test('Url in file', t => { 96 | return runFile(t, './test/css/url.css', 97 | '.url { color: #b9911b; }\n', { }); 98 | }); 99 | 100 | test('Relative path for deep file', t => { 101 | return runFile(t, './test/css/deep/sample.css', 102 | '.bg { background: url("../../img/girl.jpg") #e8ba3c; }\n', { }); 103 | }); 104 | 105 | -------------------------------------------------------------------------------- /test/car.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/postcss-get-color/89149e1b7a3d0d346c5f46678c7324e2adaba8df/test/car.jpg -------------------------------------------------------------------------------- /test/css/deep/sample.css: -------------------------------------------------------------------------------- 1 | .bg { background: url("../../img/girl.jpg") get-color("../../img/girl.png", Vibrant); } 2 | -------------------------------------------------------------------------------- /test/css/sample.css: -------------------------------------------------------------------------------- 1 | .bg { background: url("../img/girl.jpg") get-color("../img/girl.png", Vibrant); } 2 | -------------------------------------------------------------------------------- /test/css/url.css: -------------------------------------------------------------------------------- 1 | .url { color: get-color("http://jariz.github.io/vibrant.js/examples/2.jpg"); } 2 | -------------------------------------------------------------------------------- /test/img/girl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/postcss-get-color/89149e1b7a3d0d346c5f46678c7324e2adaba8df/test/img/girl.png -------------------------------------------------------------------------------- /test/white.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/postcss-get-color/89149e1b7a3d0d346c5f46678c7324e2adaba8df/test/white.jpg --------------------------------------------------------------------------------