Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
AI-powered brand identity skill for generating cohesive brand guidelines, color palettes, and visual identity.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/extract-colors.cjs
1#!/usr/bin/env node2/**3* extract-colors.cjs4*5* Extract dominant colors from an image and compare against brand palette.6* Uses pure Node.js without external image processing dependencies.7*8* For full color extraction from images, integrate with ai-multimodal skill9* or use ImageMagick via shell commands.10*11* Usage:12* node extract-colors.cjs <image-path>13* node extract-colors.cjs <image-path> --brand-file <path>14* node extract-colors.cjs --palette # Show brand palette from guidelines15*16* Integration:17* For image color analysis, use: ai-multimodal skill or ImageMagick18* magick <image> -colors 10 -depth 8 -format "%c" histogram:info:19*/2021const fs = require("fs");22const path = require("path");2324// Default brand guidelines path25const DEFAULT_GUIDELINES_PATH = "docs/brand-guidelines.md";2627/**28* Extract hex colors from markdown content29*/30function extractHexColors(text) {31const hexPattern = /#[0-9A-Fa-f]{6}\b/g;32return [...new Set(text.match(hexPattern) || [])];33}3435/**36* Parse brand guidelines for color palette37*/38function parseBrandColors(guidelinesPath) {39const resolvedPath = path.isAbsolute(guidelinesPath)40? guidelinesPath41: path.join(process.cwd(), guidelinesPath);4243if (!fs.existsSync(resolvedPath)) {44return null;45}4647const content = fs.readFileSync(resolvedPath, "utf-8");4849const palette = {50primary: [],51secondary: [],52neutral: [],53semantic: [],54all: [],55};5657// Extract colors from different sections58const sections = [59{ name: "primary", regex: /### Primary[\s\S]*?(?=###|##|$)/i },60{ name: "secondary", regex: /### Secondary[\s\S]*?(?=###|##|$)/i },61{ name: "neutral", regex: /### Neutral[\s\S]*?(?=###|##|$)/i },62{ name: "semantic", regex: /### Semantic[\s\S]*?(?=###|##|$)/i },63];6465sections.forEach(({ name, regex }) => {66const match = content.match(regex);67if (match) {68const colors = extractHexColors(match[0]);69palette[name] = colors;70palette.all.push(...colors);71}72});7374// Dedupe all75palette.all = [...new Set(palette.all)];7677return palette;78}7980/**81* Convert hex to RGB82*/83function hexToRgb(hex) {84const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);85return result86? {87r: parseInt(result[1], 16),88g: parseInt(result[2], 16),89b: parseInt(result[3], 16),90}91: null;92}9394/**95* Convert RGB to hex96*/97function rgbToHex(r, g, b) {98return (99"#" +100[r, g, b]101.map((x) => {102const hex = Math.round(x).toString(16);103return hex.length === 1 ? "0" + hex : hex;104})105.join("")106.toUpperCase()107);108}109110/**111* Calculate color distance (Euclidean in RGB space)112*/113function colorDistance(color1, color2) {114const rgb1 = typeof color1 === "string" ? hexToRgb(color1) : color1;115const rgb2 = typeof color2 === "string" ? hexToRgb(color2) : color2;116117if (!rgb1 || !rgb2) return Infinity;118119return Math.sqrt(120Math.pow(rgb1.r - rgb2.r, 2) +121Math.pow(rgb1.g - rgb2.g, 2) +122Math.pow(rgb1.b - rgb2.b, 2)123);124}125126/**127* Find nearest brand color128*/129function findNearestBrandColor(color, brandColors) {130let nearest = null;131let minDistance = Infinity;132133brandColors.forEach((brandColor) => {134const distance = colorDistance(color, brandColor);135if (distance < minDistance) {136minDistance = distance;137nearest = brandColor;138}139});140141return { color: nearest, distance: minDistance };142}143144/**145* Calculate brand compliance percentage146* Distance threshold: 50 (out of max ~441 for RGB)147*/148function calculateCompliance(extractedColors, brandColors, threshold = 50) {149if (!extractedColors || extractedColors.length === 0) return 100;150if (!brandColors || brandColors.length === 0) return 0;151152let matchCount = 0;153154extractedColors.forEach((color) => {155const nearest = findNearestBrandColor(color, brandColors);156if (nearest.distance <= threshold) {157matchCount++;158}159});160161return Math.round((matchCount / extractedColors.length) * 100);162}163164/**165* Generate ImageMagick command for color extraction166*/167function generateImageMagickCommand(imagePath, numColors = 10) {168return `magick "${imagePath}" -colors ${numColors} -depth 8 -format "%c" histogram:info:`;169}170171/**172* Parse ImageMagick histogram output to extract colors173*/174function parseImageMagickOutput(output) {175const colors = [];176const lines = output.trim().split("\n");177178lines.forEach((line) => {179// Match pattern like: 12345: (255,128,64) #FF8040 srgb(255,128,64)180const hexMatch = line.match(/#([0-9A-Fa-f]{6})/);181const countMatch = line.match(/^\s*(\d+):/);182183if (hexMatch) {184colors.push({185hex: "#" + hexMatch[1].toUpperCase(),186count: countMatch ? parseInt(countMatch[1]) : 0,187});188}189});190191// Sort by count (most common first)192colors.sort((a, b) => b.count - a.count);193194return colors;195}196197/**198* Display brand palette199*/200function displayPalette(palette) {201console.log("\n" + "=".repeat(50));202console.log("BRAND COLOR PALETTE");203console.log("=".repeat(50));204205if (palette.primary.length > 0) {206console.log("\nPrimary Colors:");207palette.primary.forEach((c) => console.log(` ${c}`));208}209210if (palette.secondary.length > 0) {211console.log("\nSecondary Colors:");212palette.secondary.forEach((c) => console.log(` ${c}`));213}214215if (palette.neutral.length > 0) {216console.log("\nNeutral Colors:");217palette.neutral.forEach((c) => console.log(` ${c}`));218}219220if (palette.semantic.length > 0) {221console.log("\nSemantic Colors:");222palette.semantic.forEach((c) => console.log(` ${c}`));223}224225console.log("\n" + "=".repeat(50));226console.log(`Total: ${palette.all.length} colors in brand palette`);227console.log("=".repeat(50) + "\n");228}229230/**231* Main function232*/233function main() {234const args = process.argv.slice(2);235const jsonOutput = args.includes("--json");236const showPalette = args.includes("--palette");237const brandFileIdx = args.indexOf("--brand-file");238const brandFile =239brandFileIdx !== -1 ? args[brandFileIdx + 1] : DEFAULT_GUIDELINES_PATH;240const brandFileValue = brandFileIdx !== -1 ? args[brandFileIdx + 1] : null;241const imagePath = args.find(242(a) => !a.startsWith("--") && a !== brandFileValue243);244245// Load brand palette246const brandPalette = parseBrandColors(brandFile);247248if (!brandPalette) {249console.error(`Brand guidelines not found at: ${brandFile}`);250console.error(`Create brand guidelines or specify path with --brand-file`);251process.exit(1);252}253254// Show palette mode255if (showPalette || !imagePath) {256if (jsonOutput) {257console.log(JSON.stringify(brandPalette, null, 2));258} else {259displayPalette(brandPalette);260261if (!imagePath) {262console.log("To extract colors from an image:");263console.log(" node extract-colors.cjs <image-path>");264console.log("\nOr use ImageMagick directly:");265console.log(' magick image.png -colors 10 -depth 8 -format "%c" histogram:info:');266}267}268return;269}270271// Resolve image path272const resolvedPath = path.isAbsolute(imagePath)273? imagePath274: path.join(process.cwd(), imagePath);275276if (!fs.existsSync(resolvedPath)) {277console.error(`Image not found: ${resolvedPath}`);278process.exit(1);279}280281// Generate extraction instructions282const result = {283image: resolvedPath,284brandPalette: brandPalette,285extractionCommand: generateImageMagickCommand(resolvedPath),286instructions: [287"1. Run the ImageMagick command to extract colors:",288` ${generateImageMagickCommand(resolvedPath)}`,289"",290"2. Or use the ai-multimodal skill:",291` python .claude/skills/ai-multimodal/scripts/gemini_batch_process.py \\`,292` --files "${resolvedPath}" \\`,293` --task analyze \\`,294` --prompt "Extract the 10 most dominant colors as hex values"`,295"",296"3. Then compare extracted colors against brand palette",297],298complianceCheck: {299threshold: 50,300description:301"Colors within distance 50 (RGB space) are considered brand-compliant",302brandColors: brandPalette.all,303},304};305306if (jsonOutput) {307console.log(JSON.stringify(result, null, 2));308} else {309console.log("\n" + "=".repeat(60));310console.log("COLOR EXTRACTION HELPER");311console.log("=".repeat(60));312console.log(`\nImage: ${result.image}`);313console.log(`\nBrand Colors: ${brandPalette.all.length} colors loaded`);314console.log("\nTo extract colors from this image:\n");315result.instructions.forEach((line) => console.log(line));316console.log("\n" + "=".repeat(60));317318// Show brand palette for reference319console.log("\nBrand Palette Reference:");320console.log(` Primary: ${brandPalette.primary.join(", ") || "none"}`);321console.log(` Secondary: ${brandPalette.secondary.join(", ") || "none"}`);322console.log(` Neutral: ${brandPalette.neutral.join(", ") || "none"}`);323console.log("=".repeat(60) + "\n");324}325}326327// Export functions for use as module328module.exports = {329parseBrandColors,330hexToRgb,331rgbToHex,332colorDistance,333findNearestBrandColor,334calculateCompliance,335parseImageMagickOutput,336};337338// Run if called directly339if (require.main === module) {340main();341}342