const PUZZLE_REFERENCE_IMAGE_MAX_EDGE = 1536; const PUZZLE_REFERENCE_IMAGE_COMPRESS_TRIGGER_BYTES = 1536 * 1024; export const PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH = 10 * 1024 * 1024; type PuzzleReferenceImageSize = { width: number; height: number; }; function readFileAsDataUrl(file: File) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error('参考图读取失败,请重试。')); reader.onload = () => { if (typeof reader.result !== 'string') { reject(new Error('参考图读取失败,请重试。')); return; } resolve(reader.result); }; reader.readAsDataURL(file); }); } function ensureReferenceImageWithinLimit(dataUrl: string) { if (dataUrl.length > PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH) { throw new Error('参考图过大,请换一张尺寸更小的图片。'); } return dataUrl; } function loadReferenceImage(dataUrl: string) { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error('参考图读取失败,请重试。')); image.src = dataUrl; }); } function resolveCompressedImageSize( image: HTMLImageElement, ): PuzzleReferenceImageSize { const sourceWidth = image.naturalWidth || image.width; const sourceHeight = image.naturalHeight || image.height; if (sourceWidth <= 0 || sourceHeight <= 0) { throw new Error('参考图读取失败,请重试。'); } const scale = Math.min( 1, PUZZLE_REFERENCE_IMAGE_MAX_EDGE / Math.max(sourceWidth, sourceHeight), ); return { width: Math.max(1, Math.round(sourceWidth * scale)), height: Math.max(1, Math.round(sourceHeight * scale)), }; } function shouldCompressReferenceImage(file: File, dataUrl: string) { return ( file.size > PUZZLE_REFERENCE_IMAGE_COMPRESS_TRIGGER_BYTES || dataUrl.length > PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH ); } async function compressReferenceImageDataUrl(file: File, dataUrl: string) { if ( typeof document === 'undefined' || typeof Image === 'undefined' || !shouldCompressReferenceImage(file, dataUrl) ) { return dataUrl; } const image = await loadReferenceImage(dataUrl); const size = resolveCompressedImageSize(image); const canvas = document.createElement('canvas'); canvas.width = size.width; canvas.height = size.height; const context = canvas.getContext('2d'); if (!context) { return dataUrl; } // 中文注释:参考图只作为生成提示,不需要保留手机原图体积;压到单边 1536 内给 JSON body 留余量。 context.imageSmoothingEnabled = true; context.imageSmoothingQuality = 'high'; context.fillStyle = '#ffffff'; context.fillRect(0, 0, size.width, size.height); context.drawImage(image, 0, 0, size.width, size.height); const candidates = [0.84, 0.76, 0.68].map((quality) => canvas.toDataURL('image/jpeg', quality), ); return candidates.reduce((best, current) => current.length < best.length ? current : best, ); } export async function readPuzzleReferenceImageAsDataUrl(file: File) { const dataUrl = await readFileAsDataUrl(file); try { const compressedDataUrl = await compressReferenceImageDataUrl( file, dataUrl, ); return ensureReferenceImageWithinLimit( compressedDataUrl.length < dataUrl.length ? compressedDataUrl : dataUrl, ); } catch (error) { if (dataUrl.length <= PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH) { return dataUrl; } throw error; } }