gongxiangp2/lib/standard-model-white-background-upload-guard.ts

158 lines
4.6 KiB
TypeScript

import sharp from 'sharp'
import type {
StandardModelWhiteBgKey,
} from '@/lib/standard-model-white-background'
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,
} 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<Record<StandardModelWhiteBgKey, File | null>>
otherImages: File[]
}): Promise<void> {
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')
}
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}无法读取或像素总量过大`,
)
}
}
}