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 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
+ }
@@ -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
+ };