Project Files
src / helpers / drawBboxesOnImage.ts
// Parse a CSS color name or hex string to an RGBA integer (Jimp format: 0xRRGGBBAA).
function cssColorToRgbaInt(color: string): number {
const named: Record<string, [number, number, number]> = {
pink: [0xff, 0x69, 0xb4],
red: [0xff, 0x00, 0x00],
green: [0x00, 0x80, 0x00],
lime: [0x00, 0xff, 0x00],
blue: [0x00, 0x00, 0xff],
yellow: [0xff, 0xff, 0x00],
cyan: [0x00, 0xff, 0xff],
magenta: [0xff, 0x00, 0xff],
white: [0xff, 0xff, 0xff],
black: [0x00, 0x00, 0x00],
orange: [0xff, 0xa5, 0x00],
purple: [0x80, 0x00, 0x80],
};
const lower = color.trim().toLowerCase();
if (named[lower]) {
const [r, g, b] = named[lower];
return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | 0xff) >>> 0;
}
const rgb3 = lower.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
if (rgb3) {
const r = parseInt(rgb3[1] + rgb3[1], 16);
const g = parseInt(rgb3[2] + rgb3[2], 16);
const b = parseInt(rgb3[3] + rgb3[3], 16);
return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | 0xff) >>> 0;
}
const rgb6 = lower.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
if (rgb6) {
const r = parseInt(rgb6[1], 16);
const g = parseInt(rgb6[2], 16);
const b = parseInt(rgb6[3], 16);
return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | 0xff) >>> 0;
}
return (((0xff & 0xff) << 24) | ((0x69 & 0xff) << 16) | ((0xb4 & 0xff) << 8) | 0xff) >>> 0;
}
export async function drawBboxesOnImage(
buffer: Buffer,
bboxes: [number, number, number, number][],
options?: {
sourceDims?: { width: number; height: number };
color?: string;
lineWeight?: number;
palette?: boolean;
}
): Promise<Buffer> {
const sourceDims = options?.sourceDims;
const usePalette = options?.palette !== false;
const lineWeight = options?.lineWeight ?? 2;
const singleColorInt = usePalette ? 0 : cssColorToRgbaInt(options?.color ?? "pink");
const requireFn = typeof require !== "undefined" ? require : (await import("module")).createRequire(__filename);
const jimpMod: any = requireFn("jimp");
const Jimp: any = jimpMod.Jimp ?? jimpMod.default ?? jimpMod;
if (!Jimp || typeof Jimp.read !== "function") {
throw new Error("drawBboxesOnImage: Jimp.read not available");
}
const img: any = await Jimp.read(buffer);
const imgW: number =
typeof img.getWidth === "function"
? img.getWidth()
: typeof img.width === "number"
? img.width
: img.bitmap?.width || 0;
const imgH: number =
typeof img.getHeight === "function"
? img.getHeight()
: typeof img.height === "number"
? img.height
: img.bitmap?.height || 0;
const scaleX = sourceDims && sourceDims.width > 0 ? imgW / sourceDims.width : 1;
const scaleY = sourceDims && sourceDims.height > 0 ? imgH / sourceDims.height : 1;
const palette: [number, number, number, number][] = [
[0xff, 0x3b, 0x30, 0xff],
[0x34, 0xc7, 0x59, 0xff],
[0x00, 0x7a, 0xff, 0xff],
[0xff, 0x9f, 0x0a, 0xff],
[0xbf, 0x5a, 0xf2, 0xff],
[0xff, 0xd6, 0x0a, 0xff],
];
for (let bi = 0; bi < bboxes.length; bi++) {
let colorInt: number;
if (usePalette) {
const [r, g, b, a] = palette[bi % palette.length];
colorInt = (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)) >>> 0;
} else {
colorInt = singleColorInt;
}
const [bx1, by1, bx2, by2] = bboxes[bi];
const x1 = Math.max(0, Math.min(imgW - 1, Math.round(bx1 * scaleX)));
const y1 = Math.max(0, Math.min(imgH - 1, Math.round(by1 * scaleY)));
const x2 = Math.max(0, Math.min(imgW - 1, Math.round(bx2 * scaleX)));
const y2 = Math.max(0, Math.min(imgH - 1, Math.round(by2 * scaleY)));
for (let t = 0; t < lineWeight; t++) {
for (let x = x1; x <= x2; x++) {
if (y1 + t < imgH) img.setPixelColor(colorInt, x, y1 + t);
if (y2 - t >= 0) img.setPixelColor(colorInt, x, y2 - t);
}
for (let y = y1; y <= y2; y++) {
if (x1 + t < imgW) img.setPixelColor(colorInt, x1 + t, y);
if (x2 - t >= 0) img.setPixelColor(colorInt, x2 - t, y);
}
}
}
const bufResult: any =
typeof img.getBufferAsync === "function"
? img.getBufferAsync("image/png")
: img.getBuffer("image/png");
if (bufResult && typeof bufResult.then === "function") {
return bufResult as Promise<Buffer>;
}
return new Promise<Buffer>((resolve, reject) =>
img.getBuffer("image/png", (err: any, data: Buffer) => (err ? reject(err) : resolve(data)))
);
}