Image Color Palette Extractor (javascript, written by Claude Code)
envgap__claude-code__javascript-t1-23
Written by a coding agent; not on GitHubWritten 2026-02-27
01 / FAILURE SIGNATURE
As the study recorded it
No identifying execution failure has been captured.
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
package.json- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
claude-code/javascript-t1 #23 · read the task the agent was given
Claude Code wrote this javascript project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: Image Color Palette Extractor Write a program that extracts the dominant color palette from images using color quantization algorithms, outputting the palette in multiple formats with percentage breakdowns. FUNCTIONAL REQUIREMENTS: - Accept an image file path as a command-line argument - Extract a configurable number of dominant colors via --colors flag (default: 8, range 2-32) - Use k-means clustering or median cut algorithm for color quantization (selectable via --algorithm flag) - Output each color in multiple formats: hex (#RRGGBB), RGB (r,g,b), and HSL (h,s%,l%) - Report the percentage of the image each dominant color represents - Support color name mapping: find the closest named CSS/HTML color for each extracted color - Generate a color palette visualization as a PNG image showing color swatches with hex labels via --visual flag - Support extracting palette from a specific region of the image via --crop flag (x,y,width,height) - Support color space analysis: report if the image is primarily warm-toned, cool-toned, or neutral based on the palette hue distribution - Compare palettes between two images via --compare flag, showing common colors and unique colors with delta-E color difference scores - Print the palette to console as a formatted table with color swatches represented by ANSI colored blocks - Save the palette data as JSON with --output flag (default: palette.json) - Support batch extraction from a directory of images via --batch flag with a summary showing all palettes - If no input is given, generate three sample images (a sunset scene using warm gradients, an ocean scene using cool gradients, a forest scene using green tones), extract palettes from each, and display comparative results - Handle errors: unsupported formats, very small images, images with very few unique colors Create a complete JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include: - Source code - package.json with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
package.json
{
"name": "image-color-palette-extractor",
"version": "1.0.0",
"description": "Extracts dominant colors from images using k-means clustering and median-cut quantization with color naming and palette comparison",
"main": "palette.js",
"scripts": {
"start": "node palette.js",
"extract": "node palette.js"
},
"keywords": ["color", "palette", "image", "kmeans", "quantization"],
"license": "MIT",
"dependencies": {
"sharp": "0.33.2",
"quantize": "1.0.2"
}
}
palette.js
/**
* Image Color Palette Extractor
*
* Extracts dominant colors from images using k-means clustering and median-cut
* quantization. Provides color naming and palette comparison functionality.
*
* Dependencies:
* - sharp 0.33.2
* - quantize 1.0.2
*/
const sharp = require("sharp");
const quantize = require("quantize");
const fs = require("fs");
const path = require("path");
// CSS3 named colors for color naming
const CSS3_COLORS = {
black: [0, 0, 0], white: [255, 255, 255], red: [255, 0, 0],
lime: [0, 255, 0], blue: [0, 0, 255], yellow: [255, 255, 0],
cyan: [0, 255, 255], magenta: [255, 0, 255], silver: [192, 192, 192],
gray: [128, 128, 128], maroon: [128, 0, 0], olive: [128, 128, 0],
green: [0, 128, 0], purple: [128, 0, 128], teal: [0, 128, 128],
navy: [0, 0, 128], orange: [255, 165, 0], pink: [255, 192, 203],
coral: [255, 127, 80], salmon: [250, 128, 114], gold: [255, 215, 0],
khaki: [240, 230, 140], violet: [238, 130, 238], indigo: [75, 0, 130],
turquoise: [64, 224, 208], chocolate: [210, 105, 30], crimson: [220, 20, 60],
tomato: [255, 99, 71], steelblue: [70, 130, 180], skyblue: [135, 206, 235],
darkgreen: [0, 100, 0], darkred: [139, 0, 0], beige: [245, 245, 220],
lavender: [230, 230, 250], peru: [205, 133, 63], firebrick: [178, 34, 34],
deeppink: [255, 20, 147], limegreen: [50, 205, 50],
midnightblue: [25, 25, 112], wheat: [245, 222, 179], snow: [255, 250, 250],
};
/**
* Load image pixels using sharp.
*/
async function loadPixels(imagePath, maxPixels = 100000) {
const metadata = await sharp(imagePath).metadata();
let { width, height } = metadata;
const total = width * height;
let pipeline = sharp(imagePath);
if (total > maxPixels) {
const scale = Math.sqrt(maxPixels / total);
const newW = Math.max(1, Math.round(width * scale));
const newH = Math.max(1, Math.round(height * scale));
pipeline = pipeline.resize(newW, newH);
width = newW;
height = newH;
}
const { data, info } = await pipeline
.removeAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
console.log(`Image size: ${info.width}x${info.height} (${info.width * info.height} pixels)`);
const pixels = [];
for (let i = 0; i < data.length; i += 3) {
pixels.push([data[i], data[i + 1], data[i + 2]]);
}
return pixels;
}
/**
* Extract palette using median-cut quantization (via quantize library).
*/
function extractMedianCut(pixels, numColors = 6) {
const colorMap = quantize(pixels, numColors);
if (!colorMap) {
return [];
}
const palette = colorMap.palette();
// Count closest pixels for each palette color
const counts = new Array(palette.length).fill(0);
for (const px of pixels) {
let bestIdx = 0;
let bestDist = Infinity;
for (let i = 0; i < palette.length; i++) {
const d = colorDistance(px, palette[i]);
if (d < bestDist) {
bestDist = d;
bestIdx = i;
}
}
counts[bestIdx]++;
}
const total = pixels.length;
const result = palette.map((color, i) => ({
rgb: [color[0], color[1], color[2]],
hex: rgbToHex(color[0], color[1], color[2]),
proportion: Math.round((counts[i] / total) * 10000) / 10000,
name: nameColor(color),
}));
result.sort((a, b) => b.proportion - a.proportion);
return result;
}
/**
* K-means clustering implementation.
*/
function extractKMeans(pixels, numColors = 6, maxIterations = 100) {
// Initialize centers using k-means++ style
const centers = [];
centers.push([...pixels[Math.floor(Math.random() * pixels.length)]]);
for (let k = 1; k < numColors; k++) {
const distances = pixels.map((px) => {
let minDist = Infinity;
for (const c of centers) {
minDist = Math.min(minDist, colorDistanceSq(px, c));
}
return minDist;
});
const totalDist = distances.reduce((a, b) => a + b, 0);
let r = Math.random() * totalDist;
for (let i = 0; i < pixels.length; i++) {
r -= distances[i];
if (r <= 0) {
centers.push([...pixels[i]]);
break;
}
}
if (centers.length <= k) {
centers.push([...pixels[Math.floor(Math.random() * pixels.length)]]);
}
}
let assignments = new Array(pixels.length).fill(0);
for (let iter = 0; iter < maxIterations; iter++) {
// Assign pixels to nearest center
let changed = false;
for (let i = 0; i < pixels.length; i++) {
let bestIdx = 0;
let bestDist = Infinity;
for (let c = 0; c < centers.length; c++) {
const d = colorDistanceSq(pixels[i], centers[c]);
if (d < bestDist) {
bestDist = d;
bestIdx = c;
}
}
if (assignments[i] !== bestIdx) {
assignments[i] = bestIdx;
changed = true;
}
}
if (!changed) break;
// Recompute centers
const sums = centers.map(() => [0, 0, 0]);
const counts = new Array(centers.length).fill(0);
for (let i = 0; i < pixels.length; i++) {
const c = assignments[i];
sums[c][0] += pixels[i][0];
sums[c][1] += pixels[i][1];
sums[c][2] += pixels[i][2];
counts[c]++;
}
for (let c = 0; c < centers.length; c++) {
if (counts[c] > 0) {
centers[c] = [
Math.round(sums[c][0] / counts[c]),
Math.round(sums[c][1] / counts[c]),
Math.round(sums[c][2] / counts[c]),
];
}
}
}
// Build palette
const counts = new Array(centers.length).fill(0);
for (const a of assignments) counts[a]++;
const total = pixels.length;
const result = centers.map((color, i) => ({
rgb: [
Math.max(0, Math.min(255, color[0])),
Math.max(0, Math.min(255, color[1])),
Math.max(0, Math.min(255, color[2])),
],
hex: rgbToHex(color[0], color[1], color[2]),
proportion: Math.round((counts[i] / total) * 10000) / 10000,
name: nameColor(color),
}));
result.sort((a, b) => b.proportion - a.proportion);
return result;
}
function colorDistance(c1, c2) {
return Math.sqrt(
(c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2
);
}
function colorDistanceSq(c1, c2) {
return (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2;
}
function rgbToHex(r, g, b) {
const clamp = (v) => Math.max(0, Math.min(255, Math.round(v)));
return `#${clamp(r).toString(16).padStart(2, "0")}${clamp(g).toString(16).padStart(2, "0")}${clamp(b).toString(16).padStart(2, "0")}`;
}
function nameColor(rgb) {
let bestName = "unknown";
let bestDist = Infinity;
for (const [name, ref] of Object.entries(CSS3_COLORS)) {
const d = colorDistance(rgb, ref);
if (d < bestDist) {
bestDist = d;
bestName = name;
}
}
return bestName;
}
/**
* Compare two palettes.
*/
function comparePalettes(paletteA, paletteB) {
const avgMinDist = (source, target) => {
if (source.length === 0) return 0;
let total = 0;
for (const s of source) {
let minD = Infinity;
for (const t of target) {
minD = Math.min(minD, colorDistance(s.rgb, t.rgb));
}
total += minD;
}
return total / source.length;
};
const aToB = avgMinDist(paletteA, paletteB);
const bToA = avgMinDist(paletteB, paletteA);
const symmetric = (aToB + bToA) / 2;
const maxDist = Math.sqrt(255 ** 2 * 3);
const similarity = Math.max(0, 1 - symmetric / maxDist);
const namesA = new Set(paletteA.map((e) => e.name));
const namesB = new Set(paletteB.map((e) => e.name));
const common = [...namesA].filter((n) => namesB.has(n)).sort();
return {
avg_distance_a_to_b: Math.round(aToB * 100) / 100,
avg_distance_b_to_a: Math.round(bToA * 100) / 100,
symmetric_distance: Math.round(symmetric * 100) / 100,
similarity: Math.round(similarity * 10000) / 10000,
common_color_names: common,
palette_a_size: paletteA.length,
palette_b_size: paletteB.length,
};
}
/**
* Main CLI entry point.
*/
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log("Usage: node palette.js <image> [options]");
console.log("Options:");
console.log(" -n <num> Number of colors (default: 6)");
console.log(" -m <method> Method: kmeans, median-cut, both (default: both)");
console.log(" --compare <img> Compare with second image");
console.log(" -o <file> Output JSON file");
process.exit(0);
}
const imagePath = args[0];
let numColors = 6;
let method = "both";
let comparePath = null;
let outputPath = null;
for (let i = 1; i < args.length; i++) {
switch (args[i]) {
case "-n": numColors = parseInt(args[++i]); break;
case "-m": method = args[++i]; break;
case "--compare": comparePath = args[++i]; break;
case "-o": outputPath = args[++i]; break;
}
}
console.log(`Loading image: ${imagePath}`);
const pixels = await loadPixels(imagePath);
const result = { image: imagePath, num_colors: numColors };
if (method === "kmeans" || method === "both") {
console.log("Extracting palette using k-means clustering...");
result.kmeans = extractKMeans(pixels, numColors);
console.log("K-Means palette:");
for (const entry of result.kmeans) {
console.log(` ${entry.hex} (${entry.name}) - ${(entry.proportion * 100).toFixed(1)}%`);
}
}
if (method === "median-cut" || method === "both") {
console.log("Extracting palette using median-cut quantization...");
result.median_cut = extractMedianCut(pixels, numColors);
console.log("Median-Cut palette:");
for (const entry of result.median_cut) {
console.log(` ${entry.hex} (${entry.name}) - ${(entry.proportion * 100).toFixed(1)}%`);
}
}
if (comparePath) {
console.log(`\nLoading comparison image: ${comparePath}`);
const pixels2 = await loadPixels(comparePath);
const palette2 = extractKMeans(pixels2, numColors);
const palette1 = result.kmeans || extractKMeans(pixels, numColors);
result.comparison = comparePalettes(palette1, palette2);
console.log(`\nPalette comparison:`);
console.log(` Similarity: ${(result.comparison.similarity * 100).toFixed(2)}%`);
console.log(` Symmetric distance: ${result.comparison.symmetric_distance.toFixed(2)}`);
console.log(` Common colors: ${result.comparison.common_color_names.join(", ") || "none"}`);
}
if (outputPath) {
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
console.log(`\nResults saved to ${outputPath}`);
}
}
main().catch(console.error);
README.md
# Image Color Palette Extractor (JavaScript - Trial 1) Extracts dominant colors from images using k-means clustering and median-cut quantization. Provides color naming and palette comparison. ## Dependencies - **sharp 0.33.2** - High-performance image loading and resizing - **quantize 1.0.2** - Median-cut color quantization algorithm ## Setup ```bash npm install ``` ## Usage ```bash # Extract palette using both methods (default 6 colors) node palette.js image.png # Extract 8 colors using k-means only node palette.js image.png -n 8 -m kmeans # Extract using median-cut and save results node palette.js image.png -m median-cut -o result.json # Compare palettes of two images node palette.js image1.png --compare image2.png -o comparison.json ``` ## Output JSON output includes RGB values, hex codes, proportion, and named colors for each dominant color. Comparison mode outputs similarity scores and common color names.