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) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 19:56:21 +02:00
parent b637b105fb
commit e305fa7ae5

View File

@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import sharp from "sharp";
import { uploadImage } from "./storage.service"; import { uploadImage } from "./storage.service";
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]; const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
@@ -8,6 +9,7 @@ const FETCH_TIMEOUT = 10_000; // 10 seconds
interface FetchImageResult { interface FetchImageResult {
filename: string; filename: string;
sourceUrl: string; sourceUrl: string;
dominantColor: string | null;
} }
export async function fetchImageFromUrl( export async function fetchImageFromUrl(
@@ -75,5 +77,29 @@ export async function fetchImageFromUrl(
// Upload to object storage // Upload to object storage
await uploadImage(Buffer.from(buffer), filename, contentType); 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<string | null> {
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;
}
} }