Created
October 6, 2025 15:31
-
-
Save mariusbolik/dd0df305d5c85bba7ededc795cc5773b to your computer and use it in GitHub Desktop.
Extract Main Color from Image (TypeScript)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Jimp, intToRGBA } from "jimp"; | |
| type RGB = { r: number; g: number; b: number }; | |
| type DominantColor = RGB & { hex: string }; | |
| interface Options { | |
| /** Downscale before sampling (faster, negligible accuracy loss). */ | |
| sampleSize?: number; | |
| /** Ignore pixels with alpha below this (0–255). */ | |
| alphaThreshold?: number; | |
| /** Ignore near-white pixels (helps when backgrounds are white). */ | |
| ignoreNearWhite?: boolean; | |
| /** Ignore near-black pixels. */ | |
| ignoreNearBlack?: boolean; | |
| /** Quantization step per channel; 16 => 4-bit (0–15) buckets. */ | |
| step?: number; | |
| } | |
| /** | |
| * Returns the dominant color of an image as RGB + hex. | |
| */ | |
| async function getDominantColor( | |
| source: string | Buffer, | |
| opts: Options = {} | |
| ): Promise<DominantColor> { | |
| const { | |
| sampleSize = 64, | |
| alphaThreshold = 10, | |
| ignoreNearWhite = false, | |
| ignoreNearBlack = false, | |
| step = 16, | |
| } = opts; | |
| const image = await Jimp.read(source); | |
| // downscale for speed (preserve aspect ratio) | |
| const w = image.bitmap.width; | |
| const h = image.bitmap.height; | |
| const scale = Math.max(w, h) > sampleSize ? sampleSize / Math.max(w, h) : 1; | |
| if (scale < 1) { | |
| image.resize({ | |
| w: Math.max(1, Math.round(w * scale)), | |
| h: Math.max(1, Math.round(h * scale)) | |
| }); | |
| } | |
| const { width, height, data } = image.bitmap; | |
| const bucketKey = (r: number, g: number, b: number) => | |
| `${Math.floor(r / step)}_${Math.floor(g / step)}_${Math.floor(b / step)}`; | |
| type Bucket = { count: number; rSum: number; gSum: number; bSum: number }; | |
| const buckets = new Map<string, Bucket>(); | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| const idx = (y * width + x) * 4; | |
| const r = data[idx + 0]!; | |
| const g = data[idx + 1]!; | |
| const b = data[idx + 2]!; | |
| const a = data[idx + 3]!; | |
| if (a <= alphaThreshold) continue; | |
| if (ignoreNearWhite && r > 240 && g > 240 && b > 240) continue; | |
| if (ignoreNearBlack && r < 15 && g < 15 && b < 15) continue; | |
| const key = bucketKey(r, g, b); | |
| const bucket = buckets.get(key) ?? { count: 0, rSum: 0, gSum: 0, bSum: 0 }; | |
| bucket.count++; | |
| bucket.rSum += r; | |
| bucket.gSum += g; | |
| bucket.bSum += b; | |
| buckets.set(key, bucket); | |
| } | |
| } | |
| if (buckets.size === 0) { | |
| const { r, g, b } = await averageColor(image); | |
| return { r, g, b, hex: rgbToHex(r, g, b) }; | |
| } | |
| let bestKey = ""; | |
| let bestCount = -1; | |
| for (const [key, bucket] of buckets) { | |
| if (bucket.count > bestCount) { | |
| bestCount = bucket.count; | |
| bestKey = key; | |
| } | |
| } | |
| const best = buckets.get(bestKey)!; | |
| const r = Math.round(best.rSum / best.count); | |
| const g = Math.round(best.gSum / best.count); | |
| const b = Math.round(best.bSum / best.count); | |
| return { r, g, b, hex: rgbToHex(r, g, b) }; | |
| } | |
| function rgbToHex(r: number, g: number, b: number): string { | |
| return `#${[r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("")}`; | |
| } | |
| async function averageColor(image: any): Promise<RGB> { | |
| const clone = image.clone().resize({ w: 1, h: 1 }); | |
| const { r, g, b } = intToRGBA(clone.getPixelColor(0, 0)); | |
| return { r, g, b }; | |
| } | |
| // Dominant color extraction using Washington Post's image proxy to convert WebP to JPEG | |
| const imageUrl = "https://revenuecat.wpenginepowered.com/wp-content/uploads/2025/09/image-2.png"; | |
| const proxyUrl = `https://www.washingtonpost.com/wp-apps/imrs.php?src=${encodeURIComponent(imageUrl)}&w=400&h=400`; // Smaller for faster processing | |
| // Usage | |
| const color = await getDominantColor(proxyUrl, { | |
| ignoreNearWhite: true, | |
| ignoreNearBlack: true, | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment