165 lines
4.9 KiB
TypeScript
165 lines
4.9 KiB
TypeScript
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<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')
|
|
}
|
|
|
|
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}无法读取或像素总量过大`,
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|