image-color-analyst 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +23 -0
- package/bin/cli.js +143 -0
- package/examples/server.js +91 -0
- package/package.json +56 -0
- package/src/analyzer.js +175 -0
- package/src/color-utils.js +121 -0
- package/src/index.js +50 -0
package/LICENSE
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Image Color Analyzer šØ
|
|
2
|
+
|
|
3
|
+
A Node.js package to analyze images and extract dominant colors, color palettes, and color statistics.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- šÆ Extract dominant color from images
|
|
8
|
+
- š Get top N colors with percentages
|
|
9
|
+
- š Color statistics and analysis
|
|
10
|
+
- šØ Generate color palettes
|
|
11
|
+
- š
CSS variable generation
|
|
12
|
+
- š„ļø CLI interface
|
|
13
|
+
- š Express.js API server
|
|
14
|
+
- š Fast processing with Sharp
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install globally for CLI usage
|
|
20
|
+
npm install -g image-color-analyzer
|
|
21
|
+
|
|
22
|
+
# Install as dependency for your project
|
|
23
|
+
npm install image-color-analyzer
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// Use ES module imports
|
|
7
|
+
import { program } from 'commander';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
// For local modules, still use require
|
|
13
|
+
const { analyze, getDominantColor, getColorPalette } = require('../src/index');
|
|
14
|
+
|
|
15
|
+
// Display banner
|
|
16
|
+
console.log(
|
|
17
|
+
chalk.cyan(
|
|
18
|
+
'='.repeat(60) + '\n' +
|
|
19
|
+
'šØ IMAGE COLOR ANALYZER šØ\n' +
|
|
20
|
+
'='.repeat(60)
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('color-analyzer')
|
|
26
|
+
.description('CLI tool to analyze dominant colors in images')
|
|
27
|
+
.version('1.0.0');
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('analyze <image-path>')
|
|
31
|
+
.description('Analyze an image and get color information')
|
|
32
|
+
.option('-t, --top <number>', 'Number of top colors to show', '5')
|
|
33
|
+
.option('-o, --output <format>', 'Output format (json, table, simple)', 'table')
|
|
34
|
+
.option('-s, --save <filename>', 'Save results to JSON file')
|
|
35
|
+
.action(async (imagePath, options) => {
|
|
36
|
+
try {
|
|
37
|
+
// Check if file exists
|
|
38
|
+
if (!fs.existsSync(imagePath)) {
|
|
39
|
+
console.error(chalk.red(`Error: File not found - ${imagePath}`));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(chalk.blue(`š· Analyzing ${imagePath}...\n`));
|
|
44
|
+
|
|
45
|
+
const result = await analyze(imagePath, {
|
|
46
|
+
topColorsCount: parseInt(options.top)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (options.output === 'json') {
|
|
50
|
+
console.log(JSON.stringify(result, null, 2));
|
|
51
|
+
} else if (options.output === 'simple') {
|
|
52
|
+
console.log(chalk.bold('šØ Dominant Color:'));
|
|
53
|
+
console.log(` ${result.dominantColor.hex} - ${result.dominantColor.name}`);
|
|
54
|
+
console.log(` Percentage: ${result.dominantColor.percentage}%\n`);
|
|
55
|
+
|
|
56
|
+
console.log(chalk.bold('š Top Colors:'));
|
|
57
|
+
result.topColors.forEach((color, index) => {
|
|
58
|
+
console.log(` ${index + 1}. ${color.hex} - ${color.name} (${color.percentage}%)`);
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
// Table format (default)
|
|
62
|
+
console.log(chalk.bold('š Image Information:'));
|
|
63
|
+
console.log(` Dimensions: ${result.imageInfo.width} Ć ${result.imageInfo.height}`);
|
|
64
|
+
console.log(` Format: ${result.imageInfo.format}`);
|
|
65
|
+
console.log(` Processing Time: ${result.processingTime}ms\n`);
|
|
66
|
+
|
|
67
|
+
console.log(chalk.bold('šØ Dominant Color:'));
|
|
68
|
+
console.log(chalk.bgHex(result.dominantColor.hex)(' '),
|
|
69
|
+
` ${result.dominantColor.hex} - ${result.dominantColor.name}`);
|
|
70
|
+
console.log(` RGB: ${result.dominantColor.rgb}`);
|
|
71
|
+
console.log(` Percentage: ${result.dominantColor.percentage}%\n`);
|
|
72
|
+
|
|
73
|
+
console.log(chalk.bold('š Top Colors:'));
|
|
74
|
+
console.log(chalk.cyan(' Rank Color Name Percentage'));
|
|
75
|
+
console.log(chalk.cyan(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
76
|
+
|
|
77
|
+
result.topColors.forEach((color, index) => {
|
|
78
|
+
const rank = (index + 1).toString().padEnd(5);
|
|
79
|
+
const colorBlock = chalk.bgHex(color.hex)(' ');
|
|
80
|
+
const hex = color.hex.padEnd(10);
|
|
81
|
+
const name = color.name.padEnd(12);
|
|
82
|
+
const percentage = color.percentage.toFixed(2).padEnd(8);
|
|
83
|
+
|
|
84
|
+
console.log(` ${rank} ${colorBlock} ${hex} ${name} ${percentage}%`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log(`\nš Total unique colors: ${result.colorStats.totalColors}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Save to file if requested
|
|
91
|
+
if (options.save) {
|
|
92
|
+
fs.writeFileSync(options.save, JSON.stringify(result, null, 2));
|
|
93
|
+
console.log(chalk.green(`\nā
Results saved to ${options.save}`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command('dominant <image-path>')
|
|
104
|
+
.description('Get only the dominant color of an image')
|
|
105
|
+
.action(async (imagePath) => {
|
|
106
|
+
try {
|
|
107
|
+
const dominant = await getDominantColor(imagePath);
|
|
108
|
+
console.log(chalk.bgHex(dominant.hex)(' '),
|
|
109
|
+
chalk.bold(` ${dominant.hex} - ${dominant.name}`));
|
|
110
|
+
console.log(`RGB: ${dominant.rgb}`);
|
|
111
|
+
console.log(`Percentage: ${dominant.percentage}%`);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
program
|
|
118
|
+
.command('palette <image-path>')
|
|
119
|
+
.description('Extract color palette from image')
|
|
120
|
+
.option('-c, --colors <number>', 'Number of colors in palette', '5')
|
|
121
|
+
.action(async (imagePath, options) => {
|
|
122
|
+
try {
|
|
123
|
+
const palette = await getColorPalette(imagePath, parseInt(options.colors));
|
|
124
|
+
|
|
125
|
+
console.log(chalk.bold(`šØ Color Palette (${options.colors} colors):\n`));
|
|
126
|
+
|
|
127
|
+
palette.forEach((color, index) => {
|
|
128
|
+
const swatch = chalk.bgHex(color.hex)(' ');
|
|
129
|
+
console.log(`${swatch} ${color.hex.padEnd(10)} ${color.name.padEnd(12)} ${color.percentage.toFixed(2)}%`);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Generate CSS
|
|
133
|
+
console.log(chalk.bold('\nš
CSS Variables:'));
|
|
134
|
+
palette.forEach((color, index) => {
|
|
135
|
+
console.log(`--color-${index + 1}: ${color.hex};`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
program.parse();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const multer = require('multer');
|
|
3
|
+
const { analyze } = require('../src/index');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
const PORT = process.env.PORT || 3000;
|
|
9
|
+
|
|
10
|
+
// Create uploads directory
|
|
11
|
+
const uploadDir = 'uploads';
|
|
12
|
+
if (!fs.existsSync(uploadDir)) {
|
|
13
|
+
fs.mkdirSync(uploadDir);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Configure multer
|
|
17
|
+
const storage = multer.diskStorage({
|
|
18
|
+
destination: (req, file, cb) => {
|
|
19
|
+
cb(null, uploadDir);
|
|
20
|
+
},
|
|
21
|
+
filename: (req, file, cb) => {
|
|
22
|
+
cb(null, Date.now() + path.extname(file.originalname));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const upload = multer({
|
|
27
|
+
storage,
|
|
28
|
+
limits: { fileSize: 10 * 1024 * 1024 },
|
|
29
|
+
fileFilter: (req, file, cb) => {
|
|
30
|
+
if (!file.originalname.match(/\.(jpg|jpeg|png|gif|webp|bmp)$/)) {
|
|
31
|
+
return cb(new Error('Only image files are allowed!'), false);
|
|
32
|
+
}
|
|
33
|
+
cb(null, true);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Middleware
|
|
38
|
+
app.use(express.json());
|
|
39
|
+
app.use(express.urlencoded({ extended: true }));
|
|
40
|
+
|
|
41
|
+
// Routes
|
|
42
|
+
app.get('/', (req, res) => {
|
|
43
|
+
res.json({
|
|
44
|
+
message: 'Image Color Analyzer API',
|
|
45
|
+
endpoints: {
|
|
46
|
+
analyze: 'POST /analyze',
|
|
47
|
+
health: 'GET /health'
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
app.get('/health', (req, res) => {
|
|
53
|
+
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
app.post('/analyze', upload.single('image'), async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
if (!req.file) {
|
|
59
|
+
return res.status(400).json({
|
|
60
|
+
success: false,
|
|
61
|
+
error: 'No image file provided'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await analyze(req.file.path, {
|
|
66
|
+
topColorsCount: 10,
|
|
67
|
+
includeNames: true
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Add file info
|
|
71
|
+
result.fileInfo = {
|
|
72
|
+
filename: req.file.filename,
|
|
73
|
+
originalname: req.file.originalname,
|
|
74
|
+
size: req.file.size,
|
|
75
|
+
mimetype: req.file.mimetype
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
res.json(result);
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
res.status(500).json({
|
|
82
|
+
success: false,
|
|
83
|
+
error: error.message
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
app.listen(PORT, () => {
|
|
89
|
+
console.log(`š Server running on http://localhost:${PORT}`);
|
|
90
|
+
console.log(`š Upload endpoint: POST http://localhost:${PORT}/analyze`);
|
|
91
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "image-color-analyst",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Analyze images to find dominant colors and color distribution",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"color-analyzer": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"dev": "node examples/server.js",
|
|
13
|
+
"test": "jest --passWithNoTests",
|
|
14
|
+
"lint": "eslint src/",
|
|
15
|
+
"format": "prettier --write \"src/**/*.js\"",
|
|
16
|
+
"prepublishOnly": "npm test"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"image",
|
|
20
|
+
"color",
|
|
21
|
+
"analyzer",
|
|
22
|
+
"dominant-color",
|
|
23
|
+
"palette",
|
|
24
|
+
"color-analysis",
|
|
25
|
+
"image-processing"
|
|
26
|
+
],
|
|
27
|
+
"author": "Krishna Pada Mandal",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/KrishnaPadaMandal/image-color-analyzer.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/KrishnaPadaMandal/image-color-analyzer#readme",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"chalk": "^4.1.2",
|
|
36
|
+
"commander": "^11.1.0",
|
|
37
|
+
"figlet": "^1.7.0",
|
|
38
|
+
"sharp": "^0.33.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"eslint": "^8.52.0",
|
|
42
|
+
"jest": "^29.7.0",
|
|
43
|
+
"nodemon": "^3.0.1",
|
|
44
|
+
"prettier": "^3.0.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=14.0.0"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"src/",
|
|
51
|
+
"bin/",
|
|
52
|
+
"examples/",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
]
|
|
56
|
+
}
|
package/src/analyzer.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const sharp = require('sharp');
|
|
2
|
+
const { rgbToHex, getColorName } = require('./color-utils');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Analyze an image to find dominant colors
|
|
6
|
+
* @param {string} imagePath - Path to the image file
|
|
7
|
+
* @param {Object} options - Analysis options
|
|
8
|
+
* @returns {Promise<Object>} Analysis results
|
|
9
|
+
*/
|
|
10
|
+
async function analyzeImageColors(imagePath, options = {}) {
|
|
11
|
+
const {
|
|
12
|
+
maxDimension = 200,
|
|
13
|
+
topColorsCount = 10,
|
|
14
|
+
colorQuantization = 10,
|
|
15
|
+
includeNames = true,
|
|
16
|
+
includeStats = true
|
|
17
|
+
} = options;
|
|
18
|
+
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Read image metadata
|
|
23
|
+
const metadata = await sharp(imagePath).metadata();
|
|
24
|
+
|
|
25
|
+
// Resize image for processing
|
|
26
|
+
let width = metadata.width;
|
|
27
|
+
let height = metadata.height;
|
|
28
|
+
|
|
29
|
+
if (width > maxDimension || height > maxDimension) {
|
|
30
|
+
if (width > height) {
|
|
31
|
+
height = Math.round((height * maxDimension) / width);
|
|
32
|
+
width = maxDimension;
|
|
33
|
+
} else {
|
|
34
|
+
width = Math.round((width * maxDimension) / height);
|
|
35
|
+
height = maxDimension;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get pixel data
|
|
40
|
+
const { data, info } = await sharp(imagePath)
|
|
41
|
+
.resize(width, height)
|
|
42
|
+
.raw()
|
|
43
|
+
.toBuffer({ resolveWithObject: true });
|
|
44
|
+
|
|
45
|
+
const pixelData = new Uint8Array(data);
|
|
46
|
+
const colorMap = new Map();
|
|
47
|
+
const totalPixels = info.width * info.height;
|
|
48
|
+
|
|
49
|
+
// Process each pixel
|
|
50
|
+
for (let i = 0; i < pixelData.length; i += info.channels) {
|
|
51
|
+
const r = pixelData[i];
|
|
52
|
+
const g = pixelData[i + 1];
|
|
53
|
+
const b = pixelData[i + 2];
|
|
54
|
+
|
|
55
|
+
// Quantize colors
|
|
56
|
+
const quantizedR = Math.floor(r / colorQuantization) * colorQuantization;
|
|
57
|
+
const quantizedG = Math.floor(g / colorQuantization) * colorQuantization;
|
|
58
|
+
const quantizedB = Math.floor(b / colorQuantization) * colorQuantization;
|
|
59
|
+
|
|
60
|
+
const colorKey = `${quantizedR},${quantizedG},${quantizedB}`;
|
|
61
|
+
|
|
62
|
+
if (colorMap.has(colorKey)) {
|
|
63
|
+
colorMap.set(colorKey, colorMap.get(colorKey) + 1);
|
|
64
|
+
} else {
|
|
65
|
+
colorMap.set(colorKey, 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Convert to array and sort
|
|
70
|
+
const colorArray = Array.from(colorMap, ([colorKey, count]) => {
|
|
71
|
+
const [r, g, b] = colorKey.split(',').map(Number);
|
|
72
|
+
const hex = rgbToHex(r, g, b);
|
|
73
|
+
const percentage = (count / totalPixels) * 100;
|
|
74
|
+
|
|
75
|
+
const colorObj = {
|
|
76
|
+
rgb: `rgb(${r}, ${g}, ${b})`,
|
|
77
|
+
hex,
|
|
78
|
+
r, g, b,
|
|
79
|
+
count,
|
|
80
|
+
percentage: parseFloat(percentage.toFixed(2))
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Add color name if requested
|
|
84
|
+
if (includeNames) {
|
|
85
|
+
colorObj.name = getColorName(r, g, b);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return colorObj;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Sort by frequency
|
|
92
|
+
colorArray.sort((a, b) => b.count - a.count);
|
|
93
|
+
|
|
94
|
+
// Get top colors
|
|
95
|
+
const topColors = colorArray.slice(0, topColorsCount);
|
|
96
|
+
const dominantColor = topColors[0] || null;
|
|
97
|
+
|
|
98
|
+
// Prepare results
|
|
99
|
+
const result = {
|
|
100
|
+
success: true,
|
|
101
|
+
dominantColor,
|
|
102
|
+
topColors,
|
|
103
|
+
imageInfo: {
|
|
104
|
+
width: metadata.width,
|
|
105
|
+
height: metadata.height,
|
|
106
|
+
format: metadata.format,
|
|
107
|
+
channels: metadata.channels,
|
|
108
|
+
size: metadata.size || 0
|
|
109
|
+
},
|
|
110
|
+
processingTime: Date.now() - startTime
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Add stats if requested
|
|
114
|
+
if (includeStats) {
|
|
115
|
+
result.colorStats = {
|
|
116
|
+
totalColors: colorArray.length,
|
|
117
|
+
totalPixels: totalPixels,
|
|
118
|
+
processedPixels: totalPixels
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(`Image analysis failed: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get color statistics from analysis results
|
|
131
|
+
* @param {Array} colors - Array of color objects
|
|
132
|
+
* @returns {Object} Color statistics
|
|
133
|
+
*/
|
|
134
|
+
function getColorStats(colors) {
|
|
135
|
+
if (!colors || !Array.isArray(colors) || colors.length === 0) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const stats = {
|
|
140
|
+
totalColors: colors.length,
|
|
141
|
+
colorDistribution: {},
|
|
142
|
+
averageSaturation: 0,
|
|
143
|
+
averageLightness: 0
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
let totalSaturation = 0;
|
|
147
|
+
let totalLightness = 0;
|
|
148
|
+
|
|
149
|
+
colors.forEach(color => {
|
|
150
|
+
// Calculate HSL values
|
|
151
|
+
const max = Math.max(color.r, color.g, color.b);
|
|
152
|
+
const min = Math.min(color.r, color.g, color.b);
|
|
153
|
+
const lightness = ((max + min) / 2 / 255) * 100;
|
|
154
|
+
const delta = (max - min) / 255;
|
|
155
|
+
const saturation = lightness === 0 || lightness === 1 ? 0 :
|
|
156
|
+
delta / (1 - Math.abs(2 * lightness - 1)) * 100;
|
|
157
|
+
|
|
158
|
+
totalSaturation += saturation;
|
|
159
|
+
totalLightness += lightness;
|
|
160
|
+
|
|
161
|
+
// Track color distribution by name
|
|
162
|
+
const name = color.name || getColorName(color.r, color.g, color.b);
|
|
163
|
+
stats.colorDistribution[name] = (stats.colorDistribution[name] || 0) + color.percentage;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
stats.averageSaturation = parseFloat((totalSaturation / colors.length).toFixed(2));
|
|
167
|
+
stats.averageLightness = parseFloat((totalLightness / colors.length).toFixed(2));
|
|
168
|
+
|
|
169
|
+
return stats;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
analyzeImageColors,
|
|
174
|
+
getColorStats
|
|
175
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert RGB to HEX
|
|
3
|
+
* @param {number} r - Red (0-255)
|
|
4
|
+
* @param {number} g - Green (0-255)
|
|
5
|
+
* @param {number} b - Blue (0-255)
|
|
6
|
+
* @returns {string} HEX color code
|
|
7
|
+
*/
|
|
8
|
+
function rgbToHex(r, g, b) {
|
|
9
|
+
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert HEX to RGB
|
|
14
|
+
* @param {string} hex - HEX color code
|
|
15
|
+
* @returns {Object} RGB object
|
|
16
|
+
*/
|
|
17
|
+
function hexToRgb(hex) {
|
|
18
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
19
|
+
return result ? {
|
|
20
|
+
r: parseInt(result[1], 16),
|
|
21
|
+
g: parseInt(result[2], 16),
|
|
22
|
+
b: parseInt(result[3], 16)
|
|
23
|
+
} : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get color name from RGB values
|
|
28
|
+
* @param {number} r - Red (0-255)
|
|
29
|
+
* @param {number} g - Green (0-255)
|
|
30
|
+
* @param {number} b - Blue (0-255)
|
|
31
|
+
* @returns {string} Color name
|
|
32
|
+
*/
|
|
33
|
+
function getColorName(r, g, b) {
|
|
34
|
+
const hue = getHue(r, g, b);
|
|
35
|
+
const saturation = getSaturation(r, g, b);
|
|
36
|
+
const lightness = getLightness(r, g, b);
|
|
37
|
+
|
|
38
|
+
if (lightness < 20) return "Black";
|
|
39
|
+
if (lightness > 85) return "White";
|
|
40
|
+
if (saturation < 15) return "Gray";
|
|
41
|
+
|
|
42
|
+
if (hue >= 0 && hue < 15) return "Red";
|
|
43
|
+
if (hue >= 15 && hue < 45) return "Orange";
|
|
44
|
+
if (hue >= 45 && hue < 75) return "Yellow";
|
|
45
|
+
if (hue >= 75 && hue < 165) return "Green";
|
|
46
|
+
if (hue >= 165 && hue < 195) return "Cyan";
|
|
47
|
+
if (hue >= 195 && hue < 255) return "Blue";
|
|
48
|
+
if (hue >= 255 && hue < 285) return "Purple";
|
|
49
|
+
if (hue >= 285 && hue < 330) return "Pink";
|
|
50
|
+
return "Red";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate hue from RGB
|
|
55
|
+
* @param {number} r - Red
|
|
56
|
+
* @param {number} g - Green
|
|
57
|
+
* @param {number} b - Blue
|
|
58
|
+
* @returns {number} Hue value (0-360)
|
|
59
|
+
*/
|
|
60
|
+
function getHue(r, g, b) {
|
|
61
|
+
const max = Math.max(r, g, b);
|
|
62
|
+
const min = Math.min(r, g, b);
|
|
63
|
+
let hue = 0;
|
|
64
|
+
|
|
65
|
+
if (max === min) {
|
|
66
|
+
hue = 0;
|
|
67
|
+
} else {
|
|
68
|
+
const delta = max - min;
|
|
69
|
+
if (max === r) {
|
|
70
|
+
hue = ((g - b) / delta) % 6;
|
|
71
|
+
} else if (max === g) {
|
|
72
|
+
hue = (b - r) / delta + 2;
|
|
73
|
+
} else {
|
|
74
|
+
hue = (r - g) / delta + 4;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
hue = Math.round(hue * 60);
|
|
78
|
+
if (hue < 0) hue += 360;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return hue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Calculate saturation from RGB
|
|
86
|
+
* @param {number} r - Red
|
|
87
|
+
* @param {number} g - Green
|
|
88
|
+
* @param {number} b - Blue
|
|
89
|
+
* @returns {number} Saturation percentage
|
|
90
|
+
*/
|
|
91
|
+
function getSaturation(r, g, b) {
|
|
92
|
+
const max = Math.max(r, g, b);
|
|
93
|
+
const min = Math.min(r, g, b);
|
|
94
|
+
const lightness = (max + min) / 2 / 255;
|
|
95
|
+
const delta = (max - min) / 255;
|
|
96
|
+
|
|
97
|
+
if (lightness === 0 || lightness === 1) return 0;
|
|
98
|
+
return parseFloat((delta / (1 - Math.abs(2 * lightness - 1)) * 100).toFixed(2));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Calculate lightness from RGB
|
|
103
|
+
* @param {number} r - Red
|
|
104
|
+
* @param {number} g - Green
|
|
105
|
+
* @param {number} b - Blue
|
|
106
|
+
* @returns {number} Lightness percentage
|
|
107
|
+
*/
|
|
108
|
+
function getLightness(r, g, b) {
|
|
109
|
+
const max = Math.max(r, g, b);
|
|
110
|
+
const min = Math.min(r, g, b);
|
|
111
|
+
return parseFloat((((max + min) / 2 / 255) * 100).toFixed(2));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
rgbToHex,
|
|
116
|
+
hexToRgb,
|
|
117
|
+
getColorName,
|
|
118
|
+
getHue,
|
|
119
|
+
getSaturation,
|
|
120
|
+
getLightness
|
|
121
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { analyzeImageColors, getColorStats } = require('./analyzer');
|
|
2
|
+
const { rgbToHex, getColorName, hexToRgb } = require('./color-utils');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
// Core functions
|
|
6
|
+
analyzeImageColors,
|
|
7
|
+
getColorStats,
|
|
8
|
+
|
|
9
|
+
// Color utilities
|
|
10
|
+
rgbToHex,
|
|
11
|
+
getColorName,
|
|
12
|
+
hexToRgb,
|
|
13
|
+
|
|
14
|
+
// Main analyzer function with options
|
|
15
|
+
analyze: async (imagePath, options = {}) => {
|
|
16
|
+
const defaultOptions = {
|
|
17
|
+
maxDimension: 200,
|
|
18
|
+
topColorsCount: 10,
|
|
19
|
+
colorQuantization: 10,
|
|
20
|
+
includeNames: true,
|
|
21
|
+
includeStats: true
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const mergedOptions = { ...defaultOptions, ...options };
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = await analyzeImageColors(imagePath, mergedOptions);
|
|
28
|
+
return result;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`Color analysis failed: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Quick analyze for dominant color only
|
|
35
|
+
getDominantColor: async (imagePath, options = {}) => {
|
|
36
|
+
const result = await analyzeImageColors(imagePath, {
|
|
37
|
+
...options,
|
|
38
|
+
topColorsCount: 1
|
|
39
|
+
});
|
|
40
|
+
return result.dominantColor;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Get color palette
|
|
44
|
+
getColorPalette: async (imagePath, count = 5) => {
|
|
45
|
+
const result = await analyzeImageColors(imagePath, {
|
|
46
|
+
topColorsCount: count
|
|
47
|
+
});
|
|
48
|
+
return result.topColors;
|
|
49
|
+
}
|
|
50
|
+
};
|