153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
import sharp from 'sharp'
|
|
import { sliceImage } from '@/lib/image-processor'
|
|
|
|
export const PERFORMANCE_SURVEY_IMAGE_LIMITS = {
|
|
maxSourceCount: 12,
|
|
maxSourceBytes: 20 * 1024 * 1024,
|
|
maxPixelsPerImage: 60_000_000,
|
|
maxWidth: 16_000,
|
|
sliceHeight: 1500,
|
|
maxSlicesPerImage: 24,
|
|
maxSlicesPerBatch: 48,
|
|
targetSliceSizeKB: 500,
|
|
} as const
|
|
|
|
export type PerformanceSurveyImageInput = {
|
|
buffer: Buffer
|
|
mimeType: string
|
|
sourceName: string
|
|
}
|
|
|
|
export type ProcessedPerformanceSurveyImage = {
|
|
buffer: Buffer
|
|
sourceIndex: number
|
|
sliceIndex: number
|
|
sliceCount: number
|
|
sourceName: string
|
|
}
|
|
|
|
type PerformanceSurveyImagePlan = {
|
|
input: PerformanceSurveyImageInput
|
|
sourceIndex: number
|
|
width: number
|
|
height: number
|
|
sliceCount: number
|
|
}
|
|
|
|
export class PerformanceSurveyImageProcessingError extends Error {}
|
|
|
|
export function getPerformanceSurveyImageSliceCount(height: number) {
|
|
return Math.ceil(height / PERFORMANCE_SURVEY_IMAGE_LIMITS.sliceHeight)
|
|
}
|
|
|
|
export function assertPerformanceSurveyImageDimensions(width: number, height: number, sourceName = '图片') {
|
|
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
|
|
throw new PerformanceSurveyImageProcessingError(`图片「${sourceName}」尺寸无效`)
|
|
}
|
|
if (width > PERFORMANCE_SURVEY_IMAGE_LIMITS.maxWidth) {
|
|
throw new PerformanceSurveyImageProcessingError(
|
|
`图片「${sourceName}」宽度超过 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxWidth}px`,
|
|
)
|
|
}
|
|
if (width * height > PERFORMANCE_SURVEY_IMAGE_LIMITS.maxPixelsPerImage) {
|
|
throw new PerformanceSurveyImageProcessingError(`图片「${sourceName}」像素总量过大`)
|
|
}
|
|
|
|
const sliceCount = getPerformanceSurveyImageSliceCount(height)
|
|
if (sliceCount > PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSlicesPerImage) {
|
|
throw new PerformanceSurveyImageProcessingError(
|
|
`图片「${sourceName}」过长,最多允许 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSlicesPerImage} 个切片`,
|
|
)
|
|
}
|
|
return sliceCount
|
|
}
|
|
|
|
async function buildPerformanceSurveyImagePlan(
|
|
input: PerformanceSurveyImageInput,
|
|
sourceIndex: number,
|
|
): Promise<PerformanceSurveyImagePlan> {
|
|
if (!input.mimeType.startsWith('image/')) {
|
|
throw new PerformanceSurveyImageProcessingError(`文件「${input.sourceName}」不是图片`)
|
|
}
|
|
if (input.buffer.length > PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceBytes) {
|
|
throw new PerformanceSurveyImageProcessingError(`图片「${input.sourceName}」超过 20MB`)
|
|
}
|
|
|
|
try {
|
|
const metadata = await sharp(input.buffer, {
|
|
limitInputPixels: PERFORMANCE_SURVEY_IMAGE_LIMITS.maxPixelsPerImage,
|
|
sequentialRead: true,
|
|
}).metadata()
|
|
if (!metadata.width || !metadata.height) {
|
|
throw new PerformanceSurveyImageProcessingError(`图片「${input.sourceName}」尺寸无效`)
|
|
}
|
|
|
|
return {
|
|
input,
|
|
sourceIndex,
|
|
width: metadata.width,
|
|
height: metadata.height,
|
|
sliceCount: assertPerformanceSurveyImageDimensions(metadata.width, metadata.height, input.sourceName),
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof PerformanceSurveyImageProcessingError) throw error
|
|
throw new PerformanceSurveyImageProcessingError(`图片「${input.sourceName}」无法读取或像素总量过大`)
|
|
}
|
|
}
|
|
|
|
export async function processPerformanceSurveyImages(
|
|
inputs: PerformanceSurveyImageInput[],
|
|
): Promise<ProcessedPerformanceSurveyImage[]> {
|
|
if (inputs.length === 0) {
|
|
throw new PerformanceSurveyImageProcessingError('每个案例至少上传 1 张图片')
|
|
}
|
|
if (inputs.length > PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount) {
|
|
throw new PerformanceSurveyImageProcessingError(
|
|
`单个案例最多上传 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount} 张图片`,
|
|
)
|
|
}
|
|
|
|
const plans: PerformanceSurveyImagePlan[] = []
|
|
let plannedSliceCount = 0
|
|
for (let sourceIndex = 0; sourceIndex < inputs.length; sourceIndex += 1) {
|
|
const plan = await buildPerformanceSurveyImagePlan(inputs[sourceIndex], sourceIndex)
|
|
plannedSliceCount += plan.sliceCount
|
|
if (plannedSliceCount > PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSlicesPerBatch) {
|
|
throw new PerformanceSurveyImageProcessingError(
|
|
`单个案例处理后最多生成 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSlicesPerBatch} 个切片`,
|
|
)
|
|
}
|
|
plans.push(plan)
|
|
}
|
|
|
|
const output: ProcessedPerformanceSurveyImage[] = []
|
|
for (const plan of plans) {
|
|
let slices: Buffer[]
|
|
try {
|
|
slices = await sliceImage(
|
|
plan.input.buffer,
|
|
PERFORMANCE_SURVEY_IMAGE_LIMITS.sliceHeight,
|
|
PERFORMANCE_SURVEY_IMAGE_LIMITS.targetSliceSizeKB,
|
|
)
|
|
} catch {
|
|
throw new PerformanceSurveyImageProcessingError(`图片「${plan.input.sourceName}」处理失败`)
|
|
}
|
|
|
|
if (slices.length === 0 || slices.length > plan.sliceCount) {
|
|
throw new PerformanceSurveyImageProcessingError(`图片「${plan.input.sourceName}」切片数量异常`)
|
|
}
|
|
|
|
slices.forEach((buffer, sliceIndex) => {
|
|
output.push({
|
|
buffer,
|
|
sourceIndex: plan.sourceIndex,
|
|
sliceIndex,
|
|
sliceCount: slices.length,
|
|
sourceName: plan.input.sourceName,
|
|
})
|
|
})
|
|
}
|
|
|
|
return output
|
|
}
|