import sharp from "sharp"; export type AlibabaImageInputRole = | "product" | "model" | "supplement" | "layout"; export type PreparedAlibabaImageInput = { buffer: Buffer; extension: "png" | "jpg"; transformed: boolean; transparentProductTrimmed: boolean; }; function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } export function selectAlibabaInputImageUrls( imageUrls: string[], maxImages = 3, ) { const normalized = Array.from( new Set( imageUrls .map((url) => String(url || "").trim()) .filter((url) => /^https?:\/\//i.test(url)), ), ); const limit = Math.max(1, Math.floor(maxImages)); if (normalized.length <= limit) return normalized; return [...normalized.slice(0, limit - 1), normalized.at(-1)!]; } async function trimTransparentProduct( buffer: Buffer, maxSide: number, jpegQuality: number, ) { const trimmed = await sharp(buffer, { animated: false, failOn: "none", }) .rotate() .trim({ background: { r: 0, g: 0, b: 0, alpha: 0 }, threshold: 8, }) .toBuffer({ resolveWithObject: true }); const padding = clamp( Math.round(Math.max(trimmed.info.width, trimmed.info.height) * 0.04), 24, 160, ); return sharp(trimmed.data, { failOn: "none" }) .flatten({ background: "#ffffff" }) .extend({ top: padding, bottom: padding, left: padding, right: padding, background: "#ffffff", }) .resize(maxSide, maxSide, { fit: "inside", withoutEnlargement: true, }) .jpeg({ quality: jpegQuality, mozjpeg: true }) .toBuffer(); } export async function prepareAlibabaImageInputBuffer(params: { buffer: Buffer; contentType?: string | null; imageUrl?: string | null; inputRole?: AlibabaImageInputRole | null; maxInputBytes: number; maxSide: number; modelId: string; }): Promise { const metadata = await sharp(params.buffer, { failOn: "none" }).metadata(); const format = String(metadata.format || "").toLowerCase(); const contentType = String(params.contentType || "").toLowerCase(); const cleanUrl = String(params.imageUrl || "") .split("?")[0] ?.toLowerCase(); const isGif = format === "gif" || contentType.includes("gif") || cleanUrl.endsWith(".gif"); const isTooLarge = params.buffer.length > params.maxInputBytes; const isTooWide = Number(metadata.width || 0) > params.maxSide; const isTooTall = Number(metadata.height || 0) > params.maxSide; const isQwenImage = /qwen-image/i.test(params.modelId); const shouldTrimTransparentProduct = Boolean( isQwenImage && params.inputRole === "product" && metadata.hasAlpha, ); if (shouldTrimTransparentProduct) { const prepared = await trimTransparentProduct(params.buffer, params.maxSide, 90); if (prepared.length > params.maxInputBytes) { const compressed = await sharp(prepared, { failOn: "none" }) .resize(3072, 3072, { fit: "inside", withoutEnlargement: true }) .jpeg({ quality: 82, mozjpeg: true }) .toBuffer(); return { buffer: compressed, extension: "jpg", transformed: true, transparentProductTrimmed: true, }; } return { buffer: prepared, extension: "jpg", transformed: true, transparentProductTrimmed: true, }; } if (!isGif && !isTooLarge && !isTooWide && !isTooTall) { return { buffer: params.buffer, extension: format === "jpeg" || format === "jpg" ? "jpg" : "png", transformed: false, transparentProductTrimmed: false, }; } const resizeTo = async (side: number, quality?: number) => { const pipeline = sharp(params.buffer, { animated: false, failOn: "none", }) .rotate() .resize(side, side, { fit: "inside", withoutEnlargement: true }); return quality ? pipeline.jpeg({ quality, mozjpeg: true }).toBuffer() : pipeline.png({ compressionLevel: 8 }).toBuffer(); }; let prepared = await resizeTo(params.maxSide); let extension: "png" | "jpg" = "png"; if (prepared.length > params.maxInputBytes) { prepared = await resizeTo(params.maxSide, 88); extension = "jpg"; } if (prepared.length > params.maxInputBytes) { prepared = await resizeTo(3072, 82); extension = "jpg"; } if (prepared.length > params.maxInputBytes) { prepared = await resizeTo(2048, 76); extension = "jpg"; } return { buffer: prepared, extension, transformed: true, transparentProductTrimmed: false, }; }