114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
|
|
// services/worker/src/pipeline.ts —— sharp 处理流水线(Task 20)。
|
|||
|
|
// 输入原图 Buffer(jpeg/png/heic 等白名单格式),输出 thumb(≤480w, q75)与
|
|||
|
|
// browse(≤1920w, q82)两个 JPEG 变体 + 原图尺寸。核心约定:
|
|||
|
|
// · .rotate() 无参 = 按 EXIF orientation 自动转正(orientation 5-8 交换宽高);
|
|||
|
|
// · 不调用 withMetadata → 输出不含任何 EXIF(含 GPS)——公开变体不携带拍摄
|
|||
|
|
// 元数据(隐私),输出 metadata 的 orientation/exif 字段均为 undefined;
|
|||
|
|
// · resize withoutEnlargement → 小图不放大(300px 原图的 thumb/browse 均为原尺寸);
|
|||
|
|
// · 解码失败/尺寸缺失 → PipelineError('DECODE_FAILED')(processor 落
|
|||
|
|
// photos.errorCode 并驱动 BullMQ 重试/终败);格式不在白名单 →
|
|||
|
|
// PipelineError('UNSUPPORTED_TYPE')(与 api UPLOAD_CONTENT_TYPES 对齐,
|
|||
|
|
// 以 sharp format 名表示;G2 真机 HEIC 样本验证后收口)。
|
|||
|
|
import sharp from 'sharp';
|
|||
|
|
import type { Metadata } from 'sharp';
|
|||
|
|
|
|||
|
|
export type PipelineErrorCode = 'DECODE_FAILED' | 'UNSUPPORTED_TYPE';
|
|||
|
|
|
|||
|
|
export class PipelineError extends Error {
|
|||
|
|
constructor(
|
|||
|
|
readonly code: PipelineErrorCode,
|
|||
|
|
message: string,
|
|||
|
|
) {
|
|||
|
|
super(message);
|
|||
|
|
this.name = 'PipelineError';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface PipelineResult {
|
|||
|
|
thumb: Buffer;
|
|||
|
|
browse: Buffer;
|
|||
|
|
/** 原图(自动转向后)尺寸 → photos.width/height。 */
|
|||
|
|
width: number;
|
|||
|
|
height: number;
|
|||
|
|
/** 变体实际尺寸(withoutEnlargement 下小于等于上限)→ photo_variants.width/height。 */
|
|||
|
|
thumbWidth: number;
|
|||
|
|
thumbHeight: number;
|
|||
|
|
browseWidth: number;
|
|||
|
|
browseHeight: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const THUMB_MAX_WIDTH = 480;
|
|||
|
|
export const BROWSE_MAX_WIDTH = 1920;
|
|||
|
|
|
|||
|
|
/** 可解码格式白名单(sharp metadata.format 取值)。 */
|
|||
|
|
const SUPPORTED_FORMATS: ReadonlySet<string> = new Set([
|
|||
|
|
'jpeg',
|
|||
|
|
'png',
|
|||
|
|
'webp',
|
|||
|
|
'avif',
|
|||
|
|
'heif',
|
|||
|
|
'tiff',
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
export async function runPipeline(input: Buffer): Promise<PipelineResult> {
|
|||
|
|
let meta: Metadata;
|
|||
|
|
try {
|
|||
|
|
meta = await sharp(input).metadata();
|
|||
|
|
} catch (err) {
|
|||
|
|
// 头部/解码抛错一律 DECODE_FAILED(含魔数残缺、截断、随机字节拼接)。
|
|||
|
|
throw new PipelineError(
|
|||
|
|
'DECODE_FAILED',
|
|||
|
|
`原图解码失败: ${err instanceof Error ? err.message : String(err)}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
if (meta.format === undefined || !SUPPORTED_FORMATS.has(meta.format)) {
|
|||
|
|
throw new PipelineError(
|
|||
|
|
'UNSUPPORTED_TYPE',
|
|||
|
|
`不支持的图片格式: ${meta.format ?? 'unknown'}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
if (meta.width === undefined || meta.height === undefined) {
|
|||
|
|
throw new PipelineError('DECODE_FAILED', '无法解析原图尺寸');
|
|||
|
|
}
|
|||
|
|
// EXIF orientation 5-8(90°/270° 旋转):显示尺寸交换宽高;像素级转正由
|
|||
|
|
// 下方 .rotate() 在变体输出时完成(输出无 EXIF,尺寸即转向后尺寸)。
|
|||
|
|
const swap = (meta.orientation ?? 1) >= 5;
|
|||
|
|
const width = swap ? meta.height : meta.width;
|
|||
|
|
const height = swap ? meta.width : meta.height;
|
|||
|
|
|
|||
|
|
let thumb: Buffer;
|
|||
|
|
let browse: Buffer;
|
|||
|
|
try {
|
|||
|
|
thumb = await sharp(input)
|
|||
|
|
.rotate()
|
|||
|
|
.resize({ width: THUMB_MAX_WIDTH, withoutEnlargement: true })
|
|||
|
|
.jpeg({ quality: 75 })
|
|||
|
|
.toBuffer();
|
|||
|
|
browse = await sharp(input)
|
|||
|
|
.rotate()
|
|||
|
|
.resize({ width: BROWSE_MAX_WIDTH, withoutEnlargement: true })
|
|||
|
|
.jpeg({ quality: 82 })
|
|||
|
|
.toBuffer();
|
|||
|
|
} catch (err) {
|
|||
|
|
throw new PipelineError(
|
|||
|
|
'DECODE_FAILED',
|
|||
|
|
`变体生成失败: ${err instanceof Error ? err.message : String(err)}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
// 变体尺寸以输出头部为准(精确反映 withoutEnlargement/转向结果)。
|
|||
|
|
const [thumbMeta, browseMeta] = await Promise.all([
|
|||
|
|
sharp(thumb).metadata(),
|
|||
|
|
sharp(browse).metadata(),
|
|||
|
|
]);
|
|||
|
|
return {
|
|||
|
|
thumb,
|
|||
|
|
browse,
|
|||
|
|
width,
|
|||
|
|
height,
|
|||
|
|
thumbWidth: thumbMeta.width ?? 0,
|
|||
|
|
thumbHeight: thumbMeta.height ?? 0,
|
|||
|
|
browseWidth: browseMeta.width ?? 0,
|
|||
|
|
browseHeight: browseMeta.height ?? 0,
|
|||
|
|
};
|
|||
|
|
}
|