From e305fa7ae5e18f65ae893a287802532b8780afd6 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 12 Apr 2026 19:56:21 +0200 Subject: [PATCH] feat(29-01): add dominant color extraction via Sharp extractDominantColor() resizes image to 1x1 pixel for weighted average color. Integrated into fetchImageFromUrl to return dominantColor. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/server/services/image.service.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/server/services/image.service.ts b/src/server/services/image.service.ts index 249f2d9..1369307 100644 --- a/src/server/services/image.service.ts +++ b/src/server/services/image.service.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import sharp from "sharp"; import { uploadImage } from "./storage.service"; const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]; @@ -8,6 +9,7 @@ const FETCH_TIMEOUT = 10_000; // 10 seconds interface FetchImageResult { filename: string; sourceUrl: string; + dominantColor: string | null; } export async function fetchImageFromUrl( @@ -75,5 +77,29 @@ export async function fetchImageFromUrl( // Upload to object storage await uploadImage(Buffer.from(buffer), filename, contentType); - return { filename, sourceUrl: url }; + const dominantColor = await extractDominantColor(buffer); + + return { filename, sourceUrl: url, dominantColor }; +} + +/** + * Extract the dominant color from an image buffer. + * Resizes to 1x1 pixel for a perceptually weighted average. + * Returns hex string like '#a3b2c1' or null on failure. + */ +export async function extractDominantColor( + buffer: Buffer | ArrayBuffer, +): Promise { + try { + const { data } = await sharp(Buffer.from(buffer)) + .resize(1, 1) + .raw() + .toBuffer({ resolveWithObject: true }); + const r = data[0]; + const g = data[1]; + const b = data[2]; + return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; + } catch { + return null; + } }