import { browserImageCodec } from './browser-image-codec'; export const AGENT_AVATAR_MAX_DIMENSION = 256; export const AGENT_AVATAR_MAX_SOURCE_BYTES = 32 * 1024 * 1024; export const AGENT_AVATAR_WEBP_QUALITY = 0.8; export const AGENT_AVATAR_SUPPORTED_MIME_TYPES = [ 'image/png', 'image/jpeg', 'image/webp', ] as const; export type AgentAvatarMimeType = typeof AGENT_AVATAR_SUPPORTED_MIME_TYPES[number]; export type PreparedAgentAvatar = { fileName: string; mimeType: AgentAvatarMimeType; previewUrl: string; width: number; height: number; bytes: number; }; function isSupportedMimeType(value: string): value is AgentAvatarMimeType { return (AGENT_AVATAR_SUPPORTED_MIME_TYPES as readonly string[]).includes(value); } function fileExtension(mimeType: AgentAvatarMimeType): string { if (mimeType === 'image/jpeg') return 'jpg'; if (mimeType === 'image/png') return 'png'; return 'webp'; } function bytesToBase64(bytes: Uint8Array): string { let result = ''; const chunkSize = 0x8000; for (let offset = 0; offset < bytes.length; offset += chunkSize) { result += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)); } return btoa(result); } async function blobToDataUrl(blob: Blob, mimeType: AgentAvatarMimeType): Promise { const dataBase64 = bytesToBase64(new Uint8Array(await blob.arrayBuffer())); return `data:${mimeType};base64,${dataBase64}`; } function encodeCanvas( canvas: HTMLCanvasElement, mimeType: AgentAvatarMimeType, quality?: number, ): Promise { return new Promise((resolve, reject) => { try { canvas.toBlob(resolve, mimeType, quality); } catch (error) { reject(error); } }); } async function encodeAvatarCanvas(canvas: HTMLCanvasElement): Promise<{ blob: Blob; mimeType: AgentAvatarMimeType; }> { const webp = await encodeCanvas(canvas, 'image/webp', AGENT_AVATAR_WEBP_QUALITY); if (webp?.type.trim().toLowerCase() === 'image/webp') { return { blob: webp, mimeType: 'image/webp' }; } const png = await encodeCanvas(canvas, 'image/png'); if (png) return { blob: png, mimeType: 'image/png' }; throw new Error('头像图片编码失败'); } export async function prepareAgentAvatar(file: File): Promise { const mimeType = file.type.trim().toLowerCase(); if (!isSupportedMimeType(mimeType)) { throw new Error('智能体头像仅支持 PNG、JPEG 或 WebP 图片'); } if (file.size > AGENT_AVATAR_MAX_SOURCE_BYTES) { throw new Error('智能体头像图片不能超过 32 MB'); } const frameCount = await browserImageCodec.inspectFrameCount(file, mimeType); if (frameCount !== 1) { throw new Error(frameCount && frameCount > 1 ? '智能体头像不支持动态图片' : '无法确认智能体头像为静态图片'); } const decoded = await browserImageCodec.decode(file, { imageOrientation: 'from-image', }); const canvas = document.createElement('canvas'); const side = Math.min(decoded.width, decoded.height); const targetSize = Math.max(1, Math.min(AGENT_AVATAR_MAX_DIMENSION, Math.floor(side))); const sourceX = Math.max(0, (decoded.width - side) / 2); const sourceY = Math.max(0, (decoded.height - side) / 2); canvas.width = targetSize; canvas.height = targetSize; try { const context = canvas.getContext('2d'); if (!context) throw new Error('智能体头像图片处理失败'); context.imageSmoothingEnabled = true; context.imageSmoothingQuality = 'high'; context.clearRect(0, 0, targetSize, targetSize); context.drawImage( decoded.source, sourceX, sourceY, side, side, 0, 0, targetSize, targetSize, ); const encoded = await encodeAvatarCanvas(canvas); return { fileName: `avatar.${fileExtension(encoded.mimeType)}`, mimeType: encoded.mimeType, previewUrl: await blobToDataUrl(encoded.blob, encoded.mimeType), width: targetSize, height: targetSize, bytes: encoded.blob.size, }; } finally { decoded.dispose(); canvas.width = 1; canvas.height = 1; } }