feat: 支持高清大图上传与处理
This commit is contained in:
@@ -3,7 +3,7 @@ import { mkdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { extname } from 'node:path'
|
||||
import sharp from 'sharp'
|
||||
import { z } from 'zod'
|
||||
import { MAX_IMAGE_BYTES, MAX_IMAGE_HEIGHT, MAX_IMAGE_PIXELS, MAX_IMAGE_WIDTH, isSharpPixelLimitError } from '../../utils/image-validation'
|
||||
import { LARGE_IMAGE_EDGE, MAX_IMAGE_BYTES, MAX_IMAGE_HEIGHT, MAX_IMAGE_MB, MAX_IMAGE_PIXELS, MAX_IMAGE_WIDTH, isSharpPixelLimitError } from '../../utils/image-validation'
|
||||
import { mediaUrl, safeUploadPath, serializeMedia, type MediaAssetRow } from '../../utils/media'
|
||||
import { useDatabase } from '../../utils/db'
|
||||
|
||||
@@ -19,6 +19,7 @@ const metadataSchema = z.object({
|
||||
copyright: z.string().trim().max(200).default(''),
|
||||
authorizationStatus: z.enum(['pending', 'authorized', 'restricted']).default('pending'),
|
||||
})
|
||||
const pixelLimitLabel = `${MAX_IMAGE_PIXELS / 10_000} 万像素`
|
||||
|
||||
function detectImageType(data: Buffer) {
|
||||
if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) return 'image/jpeg'
|
||||
@@ -31,7 +32,7 @@ export default defineEventHandler(async (event) => {
|
||||
const parts = await readMultipartFormData(event)
|
||||
const file = parts?.find(part => part.name === 'file' && part.filename)
|
||||
if (!file?.data?.length) throw createError({ statusCode: 400, statusMessage: '请选择图片文件' })
|
||||
if (file.data.length > MAX_IMAGE_BYTES) throw createError({ statusCode: 413, statusMessage: '单张图片不能超过 8MB' })
|
||||
if (file.data.length > MAX_IMAGE_BYTES) throw createError({ statusCode: 413, statusMessage: `单张图片不能超过 ${MAX_IMAGE_MB}MB` })
|
||||
|
||||
const detectedType = detectImageType(file.data)
|
||||
if (!detectedType || !(detectedType in allowedTypes)) throw createError({ statusCode: 415, statusMessage: '仅支持 JPG、PNG、WebP 图片' })
|
||||
@@ -43,14 +44,14 @@ export default defineEventHandler(async (event) => {
|
||||
try { imageMetadata = await sharp(file.data, { limitInputPixels: MAX_IMAGE_PIXELS }).metadata() }
|
||||
catch (error) {
|
||||
if (isSharpPixelLimitError(error)) {
|
||||
throw createError({ statusCode: 413, statusMessage: '图片像素过大,请压缩至 10000×10000 且不超过 4000 万像素后上传' })
|
||||
throw createError({ statusCode: 413, statusMessage: `图片像素过大,请压缩至 ${MAX_IMAGE_WIDTH}×${MAX_IMAGE_HEIGHT} 且不超过 ${pixelLimitLabel} 后上传` })
|
||||
}
|
||||
throw createError({ statusCode: 415, statusMessage: '图片内容损坏或无法解析' })
|
||||
}
|
||||
const width = imageMetadata.width || 0
|
||||
const height = imageMetadata.height || 0
|
||||
if (!width || !height || width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT || width * height > MAX_IMAGE_PIXELS) {
|
||||
throw createError({ statusCode: 413, statusMessage: '图片尺寸不能超过 10000×10000 或 4000 万像素' })
|
||||
throw createError({ statusCode: 413, statusMessage: `图片尺寸不能超过 ${MAX_IMAGE_WIDTH}×${MAX_IMAGE_HEIGHT} 或 ${pixelLimitLabel}` })
|
||||
}
|
||||
|
||||
const fields = Object.fromEntries((parts || []).filter(part => !part.filename && part.name).map(part => [part.name!, part.data.toString('utf8')]))
|
||||
@@ -66,8 +67,8 @@ export default defineEventHandler(async (event) => {
|
||||
await Promise.all(writtenPaths.map(path => mkdir(safeUploadPath(path).replace(/\/[^/]+$/, ''), { recursive: true })))
|
||||
try {
|
||||
await writeFile(safeUploadPath(originalPath), file.data, { flag: 'wx' })
|
||||
await sharp(file.data).rotate().resize({ width: 2400, height: 2400, fit: 'inside', withoutEnlargement: true }).webp({ quality: 84 }).toFile(safeUploadPath(derivedPath))
|
||||
await sharp(file.data).rotate().resize({ width: 480, height: 480, fit: 'inside', withoutEnlargement: true }).webp({ quality: 78 }).toFile(safeUploadPath(thumbnailPath))
|
||||
await sharp(file.data, { limitInputPixels: MAX_IMAGE_PIXELS }).rotate().resize({ width: LARGE_IMAGE_EDGE, height: LARGE_IMAGE_EDGE, fit: 'inside', withoutEnlargement: true }).webp({ quality: 88 }).toFile(safeUploadPath(derivedPath))
|
||||
await sharp(file.data, { limitInputPixels: MAX_IMAGE_PIXELS }).rotate().resize({ width: 480, height: 480, fit: 'inside', withoutEnlargement: true }).webp({ quality: 80 }).toFile(safeUploadPath(thumbnailPath))
|
||||
|
||||
const timestamp = now.toISOString()
|
||||
const db = useDatabase()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export const MAX_IMAGE_BYTES = 8 * 1024 * 1024
|
||||
export const MAX_IMAGE_WIDTH = 10_000
|
||||
export const MAX_IMAGE_HEIGHT = 10_000
|
||||
export const MAX_IMAGE_PIXELS = 40_000_000
|
||||
export const MAX_IMAGE_MB = 20
|
||||
export const MAX_IMAGE_BYTES = MAX_IMAGE_MB * 1024 * 1024
|
||||
export const MAX_IMAGE_WIDTH = 12_000
|
||||
export const MAX_IMAGE_HEIGHT = 12_000
|
||||
export const MAX_IMAGE_PIXELS = 64_000_000
|
||||
export const LARGE_IMAGE_EDGE = 3_200
|
||||
|
||||
export function isSharpPixelLimitError(error: unknown) {
|
||||
return error instanceof Error && /exceeds pixel limit/i.test(error.message)
|
||||
|
||||
Reference in New Issue
Block a user