import sharp from 'sharp' import type { StandardModelWhiteBgKey, } from '@/lib/standard-model-white-background' import { withImageProcessingSlot } from '@/lib/image-processing-limiter' import { configureSharpRuntime } from '@/lib/sharp-runtime' configureSharpRuntime() export const STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS = { maxOtherImages: 12, maxTotalImages: 20, maxBytesPerImage: 20 * 1024 * 1024, maxTotalBytes: 100 * 1024 * 1024, maxPixelsPerImage: 40_000_000, maxTotalPixels: 160_000_000, maxRequestBytes: 105 * 1024 * 1024, } as const const ALLOWED_IMAGE_MIME_TYPES = new Set([ 'image/avif', 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', ]) const ALLOWED_IMAGE_FORMATS = new Set([ 'avif', 'jpeg', 'png', 'webp', ]) export class WhiteBackgroundUploadValidationError extends Error { constructor(message: string) { super(message) this.name = 'WhiteBackgroundUploadValidationError' } } export function assertWhiteBackgroundImageDimensions( width: number, height: number, label = '图片', ): number { if ( !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0 ) { throw new WhiteBackgroundUploadValidationError(`${label}尺寸无效`) } const pixels = width * height if (pixels > STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS.maxPixelsPerImage) { throw new WhiteBackgroundUploadValidationError(`${label}像素总量过大`) } return pixels } function assertUploadFile(value: File, label: string): void { if ( !value || typeof value.name !== 'string' || typeof value.arrayBuffer !== 'function' || !Number.isFinite(value.size) || value.size <= 0 ) { throw new WhiteBackgroundUploadValidationError(`${label}不是有效图片文件`) } if (!ALLOWED_IMAGE_MIME_TYPES.has(String(value.type || '').toLowerCase())) { throw new WhiteBackgroundUploadValidationError( `${label}格式不支持,仅允许 JPG、PNG、WebP 或 AVIF`, ) } if (value.size > STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS.maxBytesPerImage) { throw new WhiteBackgroundUploadValidationError(`${label}不能超过 20MB`) } } export async function assertWhiteBackgroundUploadFiles(input: { fixedImages: Partial> otherImages: File[] }): Promise { if (input.otherImages.length > STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS.maxOtherImages) { throw new WhiteBackgroundUploadValidationError( `其他图片最多上传 ${STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS.maxOtherImages} 张`, ) } const files = [ ...Object.values(input.fixedImages).filter((file): file is File => Boolean(file)), ...input.otherImages, ] if (files.length === 0) { throw new WhiteBackgroundUploadValidationError('请至少上传 1 张白底图') } if (files.length > STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS.maxTotalImages) { throw new WhiteBackgroundUploadValidationError( `单次最多上传 ${STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS.maxTotalImages} 张白底图`, ) } let totalBytes = 0 for (let index = 0; index < files.length; index++) { const file = files[index] const label = `图片「${file.name || index + 1}」` assertUploadFile(file, label) totalBytes += file.size } if (totalBytes > STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS.maxTotalBytes) { throw new WhiteBackgroundUploadValidationError('单次上传白底图总体积不能超过 100MB') } await withImageProcessingSlot(async () => { let totalPixels = 0 for (let index = 0; index < files.length; index++) { const file = files[index] const label = `图片「${file.name || index + 1}」` try { const buffer = Buffer.from(await file.arrayBuffer()) const metadata = await sharp(buffer, { limitInputPixels: STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS.maxPixelsPerImage, sequentialRead: true, failOn: 'error', }).metadata() if ( !metadata.width || !metadata.height || !metadata.format || !ALLOWED_IMAGE_FORMATS.has(metadata.format) ) { throw new WhiteBackgroundUploadValidationError(`${label}不是支持的图片`) } if (Number(metadata.pages || 1) > 1) { throw new WhiteBackgroundUploadValidationError(`${label}不支持动图或多页图片`) } const pixels = assertWhiteBackgroundImageDimensions( metadata.width, metadata.height, label, ) totalPixels += pixels if (totalPixels > STANDARD_MODEL_WHITE_BG_UPLOAD_LIMITS.maxTotalPixels) { throw new WhiteBackgroundUploadValidationError('单次上传白底图总像素量过大') } } catch (error) { if (error instanceof WhiteBackgroundUploadValidationError) { throw error } throw new WhiteBackgroundUploadValidationError( `${label}无法读取或像素总量过大`, ) } } }) }