mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
- Consolidated thumbnail and blurhash processing into a dedicated data-processing module for better code organization. - Improved the logic for reusing existing thumbnails and blurhashes, ensuring efficient handling of image processing. - Updated the cache manager to validate cache data integrity and streamline the overall photo processing workflow. - Enhanced logging for better traceability during thumbnail generation and processing. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -11,9 +11,13 @@ export async function generateBlurhash(
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
// 复用缩略图的 Sharp 实例来生成 blurhash
|
||||
const { data, info } = await sharp(thumbnailBuffer).toBuffer({
|
||||
resolveWithObject: true,
|
||||
})
|
||||
// 确保转换为 raw RGBA 格式
|
||||
const { data, info } = await sharp(thumbnailBuffer)
|
||||
.raw()
|
||||
.ensureAlpha()
|
||||
.toBuffer({
|
||||
resolveWithObject: true,
|
||||
})
|
||||
|
||||
const xComponents = Math.min(Math.max(Math.round(info.width / 16), 3), 9)
|
||||
const yComponents = Math.min(Math.max(Math.round(info.height / 16), 3), 9)
|
||||
@@ -22,6 +26,15 @@ export async function generateBlurhash(
|
||||
`生成参数:原始 ${originalWidth}x${originalHeight}, 实际 ${info.width}x${info.height}, 组件 ${xComponents}x${yComponents}`,
|
||||
)
|
||||
|
||||
// 验证数据长度是否匹配
|
||||
const expectedLength = info.width * info.height * info.channels
|
||||
if (data.length !== expectedLength) {
|
||||
logger.blurhash.error(
|
||||
`数据长度不匹配:期望 ${expectedLength},实际 ${data.length}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// 生成 blurhash
|
||||
const blurhash = encode(
|
||||
new Uint8ClampedArray(data),
|
||||
|
||||
@@ -4,18 +4,55 @@ import path from 'node:path'
|
||||
import { workdir } from '@afilmory/builder/path.js'
|
||||
import sharp from 'sharp'
|
||||
|
||||
import type { Logger } from '../logger/index.js'
|
||||
import { getGlobalLoggers } from '../photo/logger-adapter.js'
|
||||
import type { ThumbnailResult } from '../types/photo.js'
|
||||
import { generateBlurhash } from './blurhash.js'
|
||||
|
||||
// 常量定义
|
||||
const THUMBNAIL_DIR = path.join(workdir, 'public/thumbnails')
|
||||
const THUMBNAIL_QUALITY = 100
|
||||
const THUMBNAIL_WIDTH = 600
|
||||
|
||||
// 获取缩略图路径信息
|
||||
function getThumbnailPaths(photoId: string) {
|
||||
const filename = `${photoId}.webp`
|
||||
const thumbnailPath = path.join(THUMBNAIL_DIR, filename)
|
||||
const thumbnailUrl = `/thumbnails/${filename}`
|
||||
|
||||
return { thumbnailPath, thumbnailUrl }
|
||||
}
|
||||
|
||||
// 创建失败结果
|
||||
function createFailureResult(): ThumbnailResult {
|
||||
return {
|
||||
thumbnailUrl: null,
|
||||
thumbnailBuffer: null,
|
||||
blurhash: null,
|
||||
}
|
||||
}
|
||||
|
||||
// 创建成功结果
|
||||
function createSuccessResult(
|
||||
thumbnailUrl: string,
|
||||
thumbnailBuffer: Buffer,
|
||||
blurhash: string | null,
|
||||
): ThumbnailResult {
|
||||
return {
|
||||
thumbnailUrl,
|
||||
thumbnailBuffer,
|
||||
blurhash,
|
||||
}
|
||||
}
|
||||
|
||||
// 确保缩略图目录存在
|
||||
async function ensureThumbnailDir(): Promise<void> {
|
||||
await fs.mkdir(THUMBNAIL_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
// 检查缩略图是否存在
|
||||
export async function thumbnailExists(photoId: string): Promise<boolean> {
|
||||
try {
|
||||
const thumbnailPath = path.join(
|
||||
workdir,
|
||||
'public/thumbnails',
|
||||
`${photoId}.webp`,
|
||||
)
|
||||
const { thumbnailPath } = getThumbnailPaths(photoId)
|
||||
await fs.access(thumbnailPath)
|
||||
return true
|
||||
} catch {
|
||||
@@ -23,6 +60,82 @@ export async function thumbnailExists(photoId: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
// 读取现有缩略图并生成 blurhash
|
||||
async function processExistingThumbnail(
|
||||
photoId: string,
|
||||
originalWidth: number,
|
||||
originalHeight: number,
|
||||
): Promise<ThumbnailResult | null> {
|
||||
const { thumbnailPath, thumbnailUrl } = getThumbnailPaths(photoId)
|
||||
|
||||
const thumbnailLog = getGlobalLoggers().thumbnail
|
||||
thumbnailLog.info(`复用现有缩略图:${photoId}`)
|
||||
|
||||
try {
|
||||
const existingBuffer = await fs.readFile(thumbnailPath)
|
||||
const blurhash = await generateBlurhash(
|
||||
existingBuffer,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
)
|
||||
|
||||
return createSuccessResult(thumbnailUrl, existingBuffer, blurhash)
|
||||
} catch (error) {
|
||||
thumbnailLog?.warn(`读取现有缩略图失败,重新生成:${photoId}`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 生成新的缩略图
|
||||
async function generateNewThumbnail(
|
||||
imageBuffer: Buffer,
|
||||
photoId: string,
|
||||
originalWidth: number,
|
||||
originalHeight: number,
|
||||
): Promise<ThumbnailResult> {
|
||||
const { thumbnailPath, thumbnailUrl } = getThumbnailPaths(photoId)
|
||||
|
||||
const log = getGlobalLoggers().thumbnail
|
||||
log.info(`生成缩略图:${photoId}`)
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
// 创建 Sharp 实例,复用于缩略图和 blurhash 生成
|
||||
const sharpInstance = sharp(imageBuffer).rotate() // 自动根据 EXIF 旋转
|
||||
|
||||
// 生成缩略图
|
||||
const thumbnailBuffer = await sharpInstance
|
||||
.clone() // 克隆实例用于缩略图生成
|
||||
.resize(THUMBNAIL_WIDTH, null, {
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp({
|
||||
quality: THUMBNAIL_QUALITY,
|
||||
})
|
||||
.toBuffer()
|
||||
|
||||
// 保存到文件
|
||||
await fs.writeFile(thumbnailPath, thumbnailBuffer)
|
||||
|
||||
// 记录生成信息
|
||||
const duration = Date.now() - startTime
|
||||
const sizeKB = Math.round(thumbnailBuffer.length / 1024)
|
||||
log.success(`生成完成:${photoId} (${sizeKB}KB, ${duration}ms)`)
|
||||
|
||||
// 基于生成的缩略图生成 blurhash
|
||||
const blurhash = await generateBlurhash(
|
||||
thumbnailBuffer,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
)
|
||||
|
||||
return createSuccessResult(thumbnailUrl, thumbnailBuffer, blurhash)
|
||||
} catch (error) {
|
||||
log.error(`生成失败:${photoId}`, error)
|
||||
return createFailureResult()
|
||||
}
|
||||
}
|
||||
|
||||
// 生成缩略图和 blurhash(复用 Sharp 实例)
|
||||
export async function generateThumbnailAndBlurhash(
|
||||
imageBuffer: Buffer,
|
||||
@@ -30,86 +143,35 @@ export async function generateThumbnailAndBlurhash(
|
||||
originalWidth: number,
|
||||
originalHeight: number,
|
||||
forceRegenerate = false,
|
||||
workerLogger?: {
|
||||
thumbnail: Logger['thumbnail']
|
||||
blurhash: Logger['blurhash']
|
||||
},
|
||||
): Promise<ThumbnailResult> {
|
||||
const thumbnailLog = workerLogger?.thumbnail
|
||||
const thumbnailLog = getGlobalLoggers().thumbnail
|
||||
|
||||
try {
|
||||
const thumbnailDir = path.join(workdir, 'public/thumbnails')
|
||||
await fs.mkdir(thumbnailDir, { recursive: true })
|
||||
await ensureThumbnailDir()
|
||||
|
||||
const thumbnailPath = path.join(thumbnailDir, `${photoId}.webp`)
|
||||
const thumbnailUrl = `/thumbnails/${photoId}.webp`
|
||||
|
||||
// 如果不是强制模式且缩略图已存在,读取现有文件
|
||||
// 如果不是强制模式且缩略图已存在,尝试复用现有文件
|
||||
if (!forceRegenerate && (await thumbnailExists(photoId))) {
|
||||
thumbnailLog?.info(`复用现有缩略图:${photoId}`)
|
||||
try {
|
||||
const existingBuffer = await fs.readFile(thumbnailPath)
|
||||
const existingResult = await processExistingThumbnail(
|
||||
photoId,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
)
|
||||
|
||||
// 基于现有缩略图生成 blurhash
|
||||
const blurhash = await generateBlurhash(
|
||||
existingBuffer,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
)
|
||||
|
||||
return {
|
||||
thumbnailUrl,
|
||||
thumbnailBuffer: existingBuffer,
|
||||
blurhash,
|
||||
}
|
||||
} catch (error) {
|
||||
thumbnailLog?.warn(`读取现有缩略图失败,重新生成:${photoId}`, error)
|
||||
// 继续执行生成逻辑
|
||||
if (existingResult) {
|
||||
return existingResult
|
||||
}
|
||||
// 如果处理现有缩略图失败,继续生成新的
|
||||
}
|
||||
|
||||
thumbnailLog?.info(`生成缩略图:${photoId}`)
|
||||
const startTime = Date.now()
|
||||
|
||||
// 创建 Sharp 实例,复用于缩略图和 blurhash 生成
|
||||
const sharpInstance = sharp(imageBuffer).rotate() // 自动根据 EXIF 旋转
|
||||
|
||||
// 生成缩略图
|
||||
const thumbnailBuffer = await sharpInstance
|
||||
.clone() // 克隆实例用于缩略图生成
|
||||
.resize(600, null, {
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp({
|
||||
quality: 100,
|
||||
})
|
||||
.toBuffer()
|
||||
|
||||
// 保存到文件
|
||||
await fs.writeFile(thumbnailPath, thumbnailBuffer)
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
const sizeKB = Math.round(thumbnailBuffer.length / 1024)
|
||||
thumbnailLog?.success(`生成完成:${photoId} (${sizeKB}KB, ${duration}ms)`)
|
||||
|
||||
// 基于生成的缩略图生成 blurhash
|
||||
const blurhash = await generateBlurhash(
|
||||
thumbnailBuffer,
|
||||
// 生成新的缩略图
|
||||
return await generateNewThumbnail(
|
||||
imageBuffer,
|
||||
photoId,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
)
|
||||
|
||||
return {
|
||||
thumbnailUrl,
|
||||
thumbnailBuffer,
|
||||
blurhash,
|
||||
}
|
||||
} catch (error) {
|
||||
thumbnailLog?.error(`生成失败:${photoId}`, error)
|
||||
return {
|
||||
thumbnailUrl: null,
|
||||
thumbnailBuffer: null,
|
||||
blurhash: null,
|
||||
}
|
||||
thumbnailLog.error(`处理失败:${photoId}`, error)
|
||||
return createFailureResult()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,6 @@
|
||||
// 主要构建器
|
||||
export { type BuilderOptions, defaultBuilder } from './builder/index.js'
|
||||
export type { StorageConfig } from './storage/interfaces.js'
|
||||
// 日志系统
|
||||
export { type Logger, logger, type WorkerLogger } from './logger/index.js'
|
||||
|
||||
// 类型定义
|
||||
export type {
|
||||
CompressedHistogramData,
|
||||
HistogramData,
|
||||
ImageMetadata,
|
||||
PhotoInfo,
|
||||
FujiRecipe,
|
||||
PhotoManifestItem,
|
||||
ProcessPhotoResult,
|
||||
ThumbnailResult,
|
||||
PickedExif,
|
||||
ToneAnalysis,
|
||||
ToneType,
|
||||
} from './types/photo.js'
|
||||
|
||||
// S3 操作
|
||||
export { generateBlurhash } from './image/blurhash.js'
|
||||
export {
|
||||
getImageMetadataWithSharp,
|
||||
preprocessImageBuffer,
|
||||
} from './image/processor.js'
|
||||
export {
|
||||
generateThumbnailAndBlurhash,
|
||||
thumbnailExists,
|
||||
} from './image/thumbnail.js'
|
||||
export { s3Client } from './s3/client.js'
|
||||
|
||||
// 照片处理
|
||||
export { extractPhotoInfo } from './photo/info-extractor.js'
|
||||
export { type PhotoProcessorOptions, processPhoto } from './photo/processor.js'
|
||||
|
||||
// Manifest 管理
|
||||
export {
|
||||
handleDeletedPhotos,
|
||||
loadExistingManifest,
|
||||
needsUpdate,
|
||||
saveManifest,
|
||||
} from './manifest/manager.js'
|
||||
export type { FujiRecipe, PickedExif } from './types/photo.js'
|
||||
// Worker 池
|
||||
export {
|
||||
type TaskFunction,
|
||||
WorkerPool,
|
||||
type WorkerPoolOptions,
|
||||
} from './worker/pool.js'
|
||||
|
||||
@@ -1,155 +1,17 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { workdir } from '@afilmory/builder/path.js'
|
||||
import type sharp from 'sharp'
|
||||
|
||||
import { HEIC_FORMATS } from '../constants/index.js'
|
||||
import { extractExifData } from '../image/exif.js'
|
||||
import { calculateHistogramAndAnalyzeTone } from '../image/histogram.js'
|
||||
import {
|
||||
generateThumbnailAndBlurhash,
|
||||
thumbnailExists,
|
||||
} from '../image/thumbnail.js'
|
||||
import type {
|
||||
PhotoManifestItem,
|
||||
PickedExif,
|
||||
ToneAnalysis,
|
||||
} from '../types/photo.js'
|
||||
import { getGlobalLoggers } from './logger-adapter.js'
|
||||
import { thumbnailExists } from '../image/thumbnail.js'
|
||||
import type { PhotoManifestItem } from '../types/photo.js'
|
||||
import type { PhotoProcessorOptions } from './processor.js'
|
||||
|
||||
export interface ThumbnailResult {
|
||||
thumbnailUrl: string
|
||||
thumbnailBuffer: Buffer
|
||||
blurhash: string
|
||||
}
|
||||
|
||||
export interface CacheableData {
|
||||
thumbnail?: ThumbnailResult
|
||||
exif?: PickedExif
|
||||
toneAnalysis?: ToneAnalysis
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理缩略图和 blurhash
|
||||
* 优先复用现有数据,如果不存在或需要强制更新则重新生成
|
||||
*/
|
||||
export async function processThumbnailAndBlurhash(
|
||||
imageBuffer: Buffer,
|
||||
photoId: string,
|
||||
width: number,
|
||||
height: number,
|
||||
existingItem: PhotoManifestItem | undefined,
|
||||
options: PhotoProcessorOptions,
|
||||
): Promise<ThumbnailResult> {
|
||||
const loggers = getGlobalLoggers()
|
||||
|
||||
// 检查是否可以复用现有数据
|
||||
if (
|
||||
!options.isForceMode &&
|
||||
!options.isForceThumbnails &&
|
||||
existingItem?.blurhash &&
|
||||
(await thumbnailExists(photoId))
|
||||
) {
|
||||
try {
|
||||
const fs = await import('node:fs/promises')
|
||||
const thumbnailPath = path.join(
|
||||
workdir,
|
||||
'public/thumbnails',
|
||||
`${photoId}.webp`,
|
||||
)
|
||||
const thumbnailBuffer = await fs.readFile(thumbnailPath)
|
||||
const thumbnailUrl = `/thumbnails/${photoId}.webp`
|
||||
|
||||
loggers.blurhash.info(`复用现有 blurhash: ${photoId}`)
|
||||
loggers.thumbnail.info(`复用现有缩略图:${photoId}`)
|
||||
|
||||
return {
|
||||
thumbnailUrl,
|
||||
thumbnailBuffer,
|
||||
blurhash: existingItem.blurhash,
|
||||
}
|
||||
} catch (error) {
|
||||
loggers.thumbnail.warn(`读取现有缩略图失败,重新生成:${photoId}`, error)
|
||||
// 继续执行生成逻辑
|
||||
}
|
||||
thumbnail?: {
|
||||
thumbnailUrl: string
|
||||
thumbnailBuffer: Buffer
|
||||
blurhash: string
|
||||
}
|
||||
|
||||
// 生成新的缩略图和 blurhash
|
||||
const result = await generateThumbnailAndBlurhash(
|
||||
imageBuffer,
|
||||
photoId,
|
||||
width,
|
||||
height,
|
||||
options.isForceMode || options.isForceThumbnails,
|
||||
{
|
||||
thumbnail: loggers.thumbnail.originalLogger,
|
||||
blurhash: loggers.blurhash.originalLogger,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
thumbnailUrl: result.thumbnailUrl!,
|
||||
thumbnailBuffer: result.thumbnailBuffer!,
|
||||
blurhash: result.blurhash!,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 EXIF 数据
|
||||
* 优先复用现有数据,如果不存在或需要强制更新则重新提取
|
||||
*/
|
||||
export async function processExifData(
|
||||
imageBuffer: Buffer,
|
||||
rawImageBuffer: Buffer | undefined,
|
||||
photoKey: string,
|
||||
existingItem: PhotoManifestItem | undefined,
|
||||
options: PhotoProcessorOptions,
|
||||
): Promise<PickedExif | null> {
|
||||
const loggers = getGlobalLoggers()
|
||||
|
||||
// 检查是否可以复用现有数据
|
||||
if (!options.isForceMode && !options.isForceManifest && existingItem?.exif) {
|
||||
const photoId = path.basename(photoKey, path.extname(photoKey))
|
||||
loggers.exif.info(`复用现有 EXIF 数据:${photoId}`)
|
||||
return existingItem.exif
|
||||
}
|
||||
|
||||
// 提取新的 EXIF 数据
|
||||
const ext = path.extname(photoKey).toLowerCase()
|
||||
const originalBuffer = HEIC_FORMATS.has(ext) ? rawImageBuffer : undefined
|
||||
|
||||
return await extractExifData(imageBuffer, originalBuffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理影调分析
|
||||
* 优先复用现有数据,如果不存在或需要强制更新则重新计算
|
||||
*/
|
||||
export async function processToneAnalysis(
|
||||
sharpInstance: sharp.Sharp,
|
||||
photoKey: string,
|
||||
existingItem: PhotoManifestItem | undefined,
|
||||
options: PhotoProcessorOptions,
|
||||
): Promise<ToneAnalysis | null> {
|
||||
const loggers = getGlobalLoggers()
|
||||
|
||||
// 检查是否可以复用现有数据
|
||||
if (
|
||||
!options.isForceMode &&
|
||||
!options.isForceManifest &&
|
||||
existingItem?.toneAnalysis
|
||||
) {
|
||||
const photoId = path.basename(photoKey, path.extname(photoKey))
|
||||
loggers.tone.info(`复用现有影调分析:${photoId}`)
|
||||
return existingItem.toneAnalysis
|
||||
}
|
||||
|
||||
// 计算新的影调分析
|
||||
return await calculateHistogramAndAnalyzeTone(
|
||||
sharpInstance,
|
||||
loggers.tone.originalLogger,
|
||||
)
|
||||
exif?: any
|
||||
toneAnalysis?: any
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,3 +58,36 @@ export async function shouldProcessPhoto(
|
||||
|
||||
return { shouldProcess: false, reason: '无需处理' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存数据的完整性
|
||||
*/
|
||||
export function validateCacheData(
|
||||
existingItem: PhotoManifestItem | undefined,
|
||||
options: PhotoProcessorOptions,
|
||||
): {
|
||||
needsThumbnail: boolean
|
||||
needsExif: boolean
|
||||
needsToneAnalysis: boolean
|
||||
} {
|
||||
if (!existingItem) {
|
||||
return {
|
||||
needsThumbnail: true,
|
||||
needsExif: true,
|
||||
needsToneAnalysis: true,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
needsThumbnail:
|
||||
options.isForceMode ||
|
||||
options.isForceThumbnails ||
|
||||
!existingItem.blurhash,
|
||||
needsExif:
|
||||
options.isForceMode || options.isForceManifest || !existingItem.exif,
|
||||
needsToneAnalysis:
|
||||
options.isForceMode ||
|
||||
options.isForceManifest ||
|
||||
!existingItem.toneAnalysis,
|
||||
}
|
||||
}
|
||||
|
||||
147
packages/builder/src/photo/data-processors.ts
Normal file
147
packages/builder/src/photo/data-processors.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
import type sharp from 'sharp'
|
||||
|
||||
import { HEIC_FORMATS } from '../constants/index.js'
|
||||
import { extractExifData } from '../image/exif.js'
|
||||
import { calculateHistogramAndAnalyzeTone } from '../image/histogram.js'
|
||||
import {
|
||||
generateThumbnailAndBlurhash,
|
||||
thumbnailExists,
|
||||
} from '../image/thumbnail.js'
|
||||
import { workdir } from '../path.js'
|
||||
import type {
|
||||
PhotoManifestItem,
|
||||
PickedExif,
|
||||
ToneAnalysis,
|
||||
} from '../types/photo.js'
|
||||
import { getGlobalLoggers } from './logger-adapter.js'
|
||||
import type { PhotoProcessorOptions } from './processor.js'
|
||||
|
||||
export interface ThumbnailResult {
|
||||
thumbnailUrl: string
|
||||
thumbnailBuffer: Buffer
|
||||
blurhash: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理缩略图和 blurhash
|
||||
* 优先复用现有数据,如果不存在或需要强制更新则重新生成
|
||||
*/
|
||||
export async function processThumbnailAndBlurhash(
|
||||
imageBuffer: Buffer,
|
||||
photoId: string,
|
||||
width: number,
|
||||
height: number,
|
||||
existingItem: PhotoManifestItem | undefined,
|
||||
options: PhotoProcessorOptions,
|
||||
): Promise<ThumbnailResult> {
|
||||
const loggers = getGlobalLoggers()
|
||||
|
||||
// 检查是否可以复用现有数据
|
||||
if (
|
||||
!options.isForceMode &&
|
||||
!options.isForceThumbnails &&
|
||||
existingItem?.blurhash &&
|
||||
(await thumbnailExists(photoId))
|
||||
) {
|
||||
try {
|
||||
const thumbnailPath = path.join(
|
||||
workdir,
|
||||
'public/thumbnails',
|
||||
`${photoId}.webp`,
|
||||
)
|
||||
const thumbnailBuffer = await fs.readFile(thumbnailPath)
|
||||
const thumbnailUrl = `/thumbnails/${photoId}.webp`
|
||||
|
||||
loggers.blurhash.info(`复用现有 blurhash: ${photoId}`)
|
||||
loggers.thumbnail.info(`复用现有缩略图:${photoId}`)
|
||||
|
||||
return {
|
||||
thumbnailUrl,
|
||||
thumbnailBuffer,
|
||||
blurhash: existingItem.blurhash,
|
||||
}
|
||||
} catch (error) {
|
||||
loggers.thumbnail.warn(`读取现有缩略图失败,重新生成:${photoId}`, error)
|
||||
// 继续执行生成逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 生成新的缩略图和 blurhash
|
||||
const result = await generateThumbnailAndBlurhash(
|
||||
imageBuffer,
|
||||
photoId,
|
||||
width,
|
||||
height,
|
||||
options.isForceMode || options.isForceThumbnails,
|
||||
{
|
||||
thumbnail: loggers.thumbnail.originalLogger,
|
||||
blurhash: loggers.blurhash.originalLogger,
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
thumbnailUrl: result.thumbnailUrl!,
|
||||
thumbnailBuffer: result.thumbnailBuffer!,
|
||||
blurhash: result.blurhash!,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 EXIF 数据
|
||||
* 优先复用现有数据,如果不存在或需要强制更新则重新提取
|
||||
*/
|
||||
export async function processExifData(
|
||||
imageBuffer: Buffer,
|
||||
rawImageBuffer: Buffer | undefined,
|
||||
photoKey: string,
|
||||
existingItem: PhotoManifestItem | undefined,
|
||||
options: PhotoProcessorOptions,
|
||||
): Promise<PickedExif | null> {
|
||||
const loggers = getGlobalLoggers()
|
||||
|
||||
// 检查是否可以复用现有数据
|
||||
if (!options.isForceMode && !options.isForceManifest && existingItem?.exif) {
|
||||
const photoId = path.basename(photoKey, path.extname(photoKey))
|
||||
loggers.exif.info(`复用现有 EXIF 数据:${photoId}`)
|
||||
return existingItem.exif
|
||||
}
|
||||
|
||||
// 提取新的 EXIF 数据
|
||||
const ext = path.extname(photoKey).toLowerCase()
|
||||
const originalBuffer = HEIC_FORMATS.has(ext) ? rawImageBuffer : undefined
|
||||
|
||||
return await extractExifData(imageBuffer, originalBuffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理影调分析
|
||||
* 优先复用现有数据,如果不存在或需要强制更新则重新计算
|
||||
*/
|
||||
export async function processToneAnalysis(
|
||||
sharpInstance: sharp.Sharp,
|
||||
photoKey: string,
|
||||
existingItem: PhotoManifestItem | undefined,
|
||||
options: PhotoProcessorOptions,
|
||||
): Promise<ToneAnalysis | null> {
|
||||
const loggers = getGlobalLoggers()
|
||||
|
||||
// 检查是否可以复用现有数据
|
||||
if (
|
||||
!options.isForceMode &&
|
||||
!options.isForceManifest &&
|
||||
existingItem?.toneAnalysis
|
||||
) {
|
||||
const photoId = path.basename(photoKey, path.extname(photoKey))
|
||||
loggers.tone.info(`复用现有影调分析:${photoId}`)
|
||||
return existingItem.toneAnalysis
|
||||
}
|
||||
|
||||
// 计算新的影调分析
|
||||
return await calculateHistogramAndAnalyzeTone(
|
||||
sharpInstance,
|
||||
loggers.tone.originalLogger,
|
||||
)
|
||||
}
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
preprocessImageBuffer,
|
||||
} from '../image/processor.js'
|
||||
import type { PhotoManifestItem } from '../types/photo.js'
|
||||
import { shouldProcessPhoto } from './cache-manager.js'
|
||||
import {
|
||||
processExifData,
|
||||
processThumbnailAndBlurhash,
|
||||
processToneAnalysis,
|
||||
shouldProcessPhoto,
|
||||
} from './cache-manager.js'
|
||||
} from './data-processors.js'
|
||||
import { extractPhotoInfo } from './info-extractor.js'
|
||||
import { processLivePhoto } from './live-photo-handler.js'
|
||||
import { getGlobalLoggers } from './logger-adapter.js'
|
||||
|
||||
@@ -3,13 +3,16 @@ export type { PhotoProcessorOptions } from './processor.js'
|
||||
export { processPhoto } from './processor.js'
|
||||
|
||||
// 缓存管理
|
||||
export type { CacheableData, ThumbnailResult } from './cache-manager.js'
|
||||
export type { CacheableData } from './cache-manager.js'
|
||||
export { shouldProcessPhoto } from './cache-manager.js'
|
||||
|
||||
// 数据处理器
|
||||
export type { ThumbnailResult } from './data-processors.js'
|
||||
export {
|
||||
processExifData,
|
||||
processThumbnailAndBlurhash,
|
||||
processToneAnalysis,
|
||||
shouldProcessPhoto,
|
||||
} from './cache-manager.js'
|
||||
} from './data-processors.js'
|
||||
|
||||
// Live Photo 处理
|
||||
export type { LivePhotoResult } from './live-photo-handler.js'
|
||||
|
||||
Reference in New Issue
Block a user