This reference implementation assumes helper functions such as medianCut(), rgbToHex(), and generatePalette() are available in the execution environment, mirroring the browser-side logic used by the UI.
async function main(args) {
const {
input_base64,
max_colors = 8,
include_tints_shades = false,
} = args;
if (!input_base64) {
return { success: false, result: [], error: "input_base64 is required" };
}
try {
const base64Data = input_base64.replace(/^data:[^;]+;base64,/, "");
const binaryString = atob(base64Data);
const bytes = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
const blob = new Blob([bytes]);
const image = await createImageBitmap(blob);
const maxDim = 256;
const scale = Math.min(maxDim / image.width, maxDim / image.height, 1);
const width = Math.max(1, Math.round(image.width * scale));
const height = Math.max(1, Math.round(image.height * scale));
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d", { willReadFrequently: true });
if (!ctx) {
throw new Error("Canvas context unavailable");
}
ctx.drawImage(image, 0, 0, width, height);
const { data } = ctx.getImageData(0, 0, width, height);
const pixels = [];
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3];
if (alpha < 128) continue;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const luma = 0.299 * r + 0.587 * g + 0.114 * b;
if (luma < 5 || luma > 250) continue;
pixels.push({ r, g, b });
}
if (pixels.length === 0) {
for (let i = 0; i < data.length; i += 4) {
pixels.push({ r: data[i], g: data[i + 1], b: data[i + 2] });
}
}
const boxes = medianCut(pixels, max_colors);
const totalPixels = pixels.length;
const result = boxes
.map((box) => {
const count = box.pixels.length;
if (!count) return null;
const avgR = Math.round(box.pixels.reduce((sum, pixel) => sum + pixel.r, 0) / count);
const avgG = Math.round(box.pixels.reduce((sum, pixel) => sum + pixel.g, 0) / count);
const avgB = Math.round(box.pixels.reduce((sum, pixel) => sum + pixel.b, 0) / count);
const hex = rgbToHex(avgR, avgG, avgB);
const item = {
hex,
r: avgR,
g: avgG,
b: avgB,
percentage: Math.round((count / totalPixels) * 100),
};
if (!include_tints_shades) return item;
return {
...item,
palette: generatePalette(hex, 4),
};
})
.filter(Boolean)
.sort((a, b) => b.percentage - a.percentage);
return { success: true, result };
} catch (error) {
return {
success: false,
result: [],
error: "Palette extraction failed: " + (error instanceof Error ? error.message : String(error)),
};
}
}