From 3f21435271347eb6ebdf86f991707d9b11b2cf16 Mon Sep 17 00:00:00 2001 From: Innei Date: Sun, 29 Jun 2025 11:28:23 +0800 Subject: [PATCH] refactor: update ErrorElement and HistogramChart components for improved styling and functionality - Enhanced the ErrorElement component by adjusting the layout for better centering and added select-text class for improved text selection. - Modified the HistogramChart component to accept a thumbnail URL instead of tone analysis, streamlining the data flow. - Refactored the histogram calculation logic to improve performance and clarity, including compression of histogram data. - Updated the manifest version from 'v2' to 'v3' across multiple files to reflect the new data structure. Signed-off-by: Innei --- .../src/components/common/ErrorElement.tsx | 6 +- .../components/ui/photo-viewer/ExifPanel.tsx | 2 +- .../ui/photo-viewer/HistogramChart.tsx | 717 ++++++------------ apps/web/src/pages/(data)/manifest.tsx | 4 +- locales/app/en.json | 10 - locales/app/jp.json | 16 +- locales/app/ko.json | 16 +- locales/app/zh-CN.json | 10 - locales/app/zh-HK.json | 14 +- locales/app/zh-TW.json | 10 - packages/builder/src/builder/builder.ts | 2 +- packages/builder/src/image/histogram.ts | 61 +- packages/builder/src/manifest/manager.ts | 20 +- packages/builder/src/photo/data-processors.ts | 9 +- packages/builder/src/types/manifest.ts | 2 +- packages/builder/src/types/photo.ts | 1 - 16 files changed, 277 insertions(+), 623 deletions(-) diff --git a/apps/web/src/components/common/ErrorElement.tsx b/apps/web/src/components/common/ErrorElement.tsx index 76dd3228..937d3d12 100644 --- a/apps/web/src/components/common/ErrorElement.tsx +++ b/apps/web/src/components/common/ErrorElement.tsx @@ -36,11 +36,11 @@ export function ErrorElement() { return (
-
+
-

{t('error.title')}

+

{t('error.title')}

-

{message}

+

{message}

{import.meta.env.DEV && stack ? (
{attachOpenInEditor(stack)} diff --git a/apps/web/src/components/ui/photo-viewer/ExifPanel.tsx b/apps/web/src/components/ui/photo-viewer/ExifPanel.tsx index d2393fa9..42baff08 100644 --- a/apps/web/src/components/ui/photo-viewer/ExifPanel.tsx +++ b/apps/web/src/components/ui/photo-viewer/ExifPanel.tsx @@ -285,7 +285,7 @@ export const ExifPanel: FC<{
{t('exif.histogram')}
- +
diff --git a/apps/web/src/components/ui/photo-viewer/HistogramChart.tsx b/apps/web/src/components/ui/photo-viewer/HistogramChart.tsx index d2edb7a1..45eb643a 100644 --- a/apps/web/src/components/ui/photo-viewer/HistogramChart.tsx +++ b/apps/web/src/components/ui/photo-viewer/HistogramChart.tsx @@ -1,520 +1,281 @@ -import type { ToneAnalysis } from '@afilmory/builder' -import { AnimatePresence, m } from 'motion/react' import type { FC } from 'react' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { decompressHistogram } from '~/lib/histogram' +import { cx } from '~/lib/cn' -interface HistogramChartProps { - toneAnalysis: ToneAnalysis - className?: string +interface CompressedHistogramData { + red: number[] + green: number[] + blue: number[] + luminance: number[] } -interface ChannelVisibility { - red: boolean - green: boolean - blue: boolean - luminance: boolean +interface HistogramData { + red: number[] + green: number[] + blue: number[] + luminance: number[] } -interface HoverInfo { - x: number - value: number - channels: { - red: number - green: number - blue: number - luminance: number +const calculateHistogram = (imageData: ImageData): CompressedHistogramData => { + const histogram: HistogramData = { + red: Array.from({ length: 256 }).fill(0) as number[], + green: Array.from({ length: 256 }).fill(0) as number[], + blue: Array.from({ length: 256 }).fill(0) as number[], + luminance: Array.from({ length: 256 }).fill(0) as number[], + } + + const { data } = imageData + for (let i = 0; i < data.length; i += 4) { + const r = data[i] + const g = data[i + 1] + const b = data[i + 2] + histogram.red[r]++ + histogram.green[g]++ + histogram.blue[b]++ + const luminance = Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b) + histogram.luminance[luminance]++ + } + + const compress = (channelData: number[]): number[] => { + const compressed = Array.from({ length: 128 }).fill(0) as number[] + for (let i = 0; i < 256; i++) { + compressed[Math.floor(i / 2)] += channelData[i] + } + return compressed + } + + return { + red: compress(histogram.red), + green: compress(histogram.green), + blue: compress(histogram.blue), + luminance: compress(histogram.luminance), } } -export const HistogramChart: FC = ({ - toneAnalysis, - className = '', -}) => { - const { t } = useTranslation() - const canvasRef = useRef(null) - const [channelVisibility, setChannelVisibility] = useState( - { - red: true, - green: true, - blue: true, - luminance: true, - }, +const drawHistogram = ( + canvas: HTMLCanvasElement, + histogram: CompressedHistogramData, +) => { + const ctx = canvas.getContext('2d') + if (!ctx) return + + // 获取 Canvas 的实际显示尺寸 + const rect = canvas.getBoundingClientRect() + const { width } = rect + const { height } = rect + const dpr = window.devicePixelRatio || 1 + + // 设置高分辨率 + canvas.width = width * dpr + canvas.height = height * dpr + ctx.scale(dpr, dpr) + canvas.style.width = `${width}px` + canvas.style.height = `${height}px` + + // 清空画布 + ctx.clearRect(0, 0, width, height) + + // 找到最大值用于归一化 + const maxVal = Math.max( + ...histogram.luminance, + ...histogram.red, + ...histogram.green, + ...histogram.blue, ) - const [hoverInfo, setHoverInfo] = useState(null) - const [showStats, setShowStats] = useState(false) - const toggleChannel = useCallback((channel: keyof ChannelVisibility) => { - setChannelVisibility((prev) => ({ - ...prev, - [channel]: !prev[channel], - })) - }, []) + if (maxVal === 0) return - const handleMouseMove = useCallback( - (event: React.MouseEvent) => { - const canvas = canvasRef.current - if (!canvas) return + const padding = 0 + const chartWidth = width - padding * 2 + const chartHeight = height - padding * 2 - const rect = canvas.getBoundingClientRect() - const x = event.clientX - rect.left - const padding = 8 - const drawWidth = rect.width - padding * 2 + // Apple 风格的颜色定义 + const colors = { + red: 'rgb(255, 105, 97)', + green: 'rgb(52, 199, 89)', + blue: 'rgb(64, 156, 255)', + luminance: 'rgba(255, 255, 255, 0.6)', + background: 'rgba(28, 28, 30, 0.95)', + grid: 'rgba(255, 255, 255, 0.04)', + border: 'rgba(255, 255, 255, 0.08)', + } - // 计算对应的直方图索引 - const histogramIndex = Math.floor(((x - padding) / drawWidth) * 256) + // 绘制背景 + ctx.fillStyle = colors.background + ctx.fillRect(0, 0, width, height) - if (histogramIndex >= 0 && histogramIndex < 256) { - const decompressedHistogram = decompressHistogram( - toneAnalysis.histogram, - ) - const { red, green, blue, luminance } = decompressedHistogram + // 绘制极简网格 + ctx.strokeStyle = colors.grid + ctx.lineWidth = 0.5 - setHoverInfo({ - x: histogramIndex, - value: histogramIndex, - channels: { - red: red[histogramIndex] || 0, - green: green[histogramIndex] || 0, - blue: blue[histogramIndex] || 0, - luminance: luminance[histogramIndex] || 0, - }, - }) + // 只绘制几条关键的网格线 + for (let i = 1; i <= 3; i++) { + const y = padding + (chartHeight / 4) * i + ctx.beginPath() + ctx.moveTo(padding, y) + ctx.lineTo(width - padding, y) + ctx.stroke() + } + + // 绘制柱状图函数 + const drawBars = (data: number[], color: string, alpha = 1) => { + const barWidth = chartWidth / data.length + + for (const [i, datum] of data.entries()) { + const barHeight = (datum / maxVal) * chartHeight + const x = padding + i * barWidth + const y = height - padding - barHeight + + // 创建渐变 + const gradient = ctx.createLinearGradient(0, y, 0, height - padding) + + // 正确处理颜色字符串转换 + let topColor: string + let bottomColor: string + + if (color.startsWith('rgba')) { + // 如果已经是 rgba 格式,替换最后的透明度值 + topColor = color.replace(/[\d.]+\)$/, `${alpha})`) + bottomColor = color.replace(/[\d.]+\)$/, `${alpha * 0.1})`) + } else if (color.startsWith('rgb')) { + // 如果是 rgb 格式,转换为 rgba + topColor = color.replace('rgb', 'rgba').replace(')', `, ${alpha})`) + bottomColor = color + .replace('rgb', 'rgba') + .replace(')', `, ${alpha * 0.1})`) + } else { + // 其他格式直接使用 + topColor = color + bottomColor = color } - }, - [toneAnalysis], - ) - const handleMouseLeave = useCallback(() => { - setHoverInfo(null) - }, []) + gradient.addColorStop(0, topColor) + gradient.addColorStop(1, bottomColor) + + ctx.fillStyle = gradient + ctx.fillRect(x, y, barWidth * 0.8, barHeight) + } + } + + // 先绘制亮度通道作为背景 + drawBars(histogram.luminance, colors.luminance, 0.3) + + // 设置混合模式 + ctx.globalCompositeOperation = 'screen' + + // 绘制 RGB 通道 + drawBars(histogram.red, colors.red, 0.7) + drawBars(histogram.green, colors.green, 0.7) + drawBars(histogram.blue, colors.blue, 0.7) + + // 重置混合模式 + ctx.globalCompositeOperation = 'source-over' + + // 绘制边框 + ctx.strokeStyle = colors.border + ctx.lineWidth = 1 + ctx.strokeRect(padding - 0.5, padding - 0.5, chartWidth + 1, chartHeight + 1) + + // 添加顶部高光 + const highlightGradient = ctx.createLinearGradient(0, 0, 0, height * 0.2) + highlightGradient.addColorStop(0, 'rgba(255, 255, 255, 0.03)') + highlightGradient.addColorStop(1, 'rgba(255, 255, 255, 0)') + + ctx.fillStyle = highlightGradient + ctx.fillRect(0, 0, width, height * 0.2) +} + +export const HistogramChart: FC<{ + thumbnailUrl: string + className?: string +}> = ({ thumbnailUrl, className = '' }) => { + const canvasRef = useRef(null) + const [histogram, setHistogram] = useState( + null, + ) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + const { t } = useTranslation() useEffect(() => { - const canvas = canvasRef.current - if (!canvas) return + setLoading(true) + setError(false) + setHistogram(null) - const ctx = canvas.getContext('2d') - if (!ctx) return + const img = new Image() + img.crossOrigin = 'Anonymous' + img.src = thumbnailUrl - // 解压缩直方图数据用于渲染 - const decompressedHistogram = decompressHistogram(toneAnalysis.histogram) - const { red, green, blue, luminance } = decompressedHistogram - - // 获取设备像素比,提高画布分辨率 - const devicePixelRatio = window.devicePixelRatio || 1 - const rect = canvas.getBoundingClientRect() - - // 设置画布内部分辨率 - canvas.width = rect.width * devicePixelRatio - canvas.height = rect.height * devicePixelRatio - - // 缩放上下文以匹配设备像素比 - ctx.scale(devicePixelRatio, devicePixelRatio) - - // 设置画布样式尺寸 - canvas.style.width = `${rect.width}px` - canvas.style.height = `${rect.height}px` - - // 启用抗锯齿和平滑渲染 - ctx.imageSmoothingEnabled = true - ctx.imageSmoothingQuality = 'high' - ctx.lineJoin = 'round' - ctx.lineCap = 'round' - - const { width } = rect - const { height } = rect - const padding = 8 - - // 清空画布 - ctx.clearRect(0, 0, width, height) - - // 计算最大值用于归一化 - const maxRed = Math.max(...red) - const maxGreen = Math.max(...green) - const maxBlue = Math.max(...blue) - const maxLuminance = Math.max(...luminance) - const globalMax = Math.max(maxRed, maxGreen, maxBlue, maxLuminance) - - if (globalMax === 0) return - - const drawWidth = width - padding * 2 - const drawHeight = height - padding * 2 - const barWidth = drawWidth / 256 - - // 绘制背景网格 - ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)' - ctx.lineWidth = 0.5 - - // 垂直网格线 - for (let i = 0; i <= 4; i++) { - const x = padding + (i * drawWidth) / 4 - ctx.beginPath() - ctx.moveTo(x, padding) - ctx.lineTo(x, height - padding) - ctx.stroke() - } - - // 水平网格线 - for (let i = 0; i <= 4; i++) { - const y = padding + (i * drawHeight) / 4 - ctx.beginPath() - ctx.moveTo(padding, y) - ctx.lineTo(width - padding, y) - ctx.stroke() - } - - // 绘制直方图函数 - const drawHistogram = (data: number[], color: string, alpha = 0.6) => { - ctx.globalAlpha = alpha - - // 使用路径绘制更平滑的直方图 - ctx.beginPath() - ctx.moveTo(padding, height - padding) - - for (const [i, datum] of data.entries()) { - const x = padding + i * barWidth - const barHeight = (datum / globalMax) * drawHeight - const y = height - padding - barHeight - - ctx.lineTo(x, y) + img.onload = () => { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d', { willReadFrequently: true }) + if (!ctx) { + setError(true) + setLoading(false) + return } - ctx.lineTo(padding + drawWidth, height - padding) - ctx.closePath() + // 为了更好的性能,缩放图片到合适的大小 + const maxSize = 300 + const scale = Math.min( + maxSize / img.naturalWidth, + maxSize / img.naturalHeight, + ) + const scaledWidth = Math.floor(img.naturalWidth * scale) + const scaledHeight = Math.floor(img.naturalHeight * scale) - ctx.fillStyle = color - ctx.fill() + canvas.width = scaledWidth + canvas.height = scaledHeight + ctx.drawImage(img, 0, 0, scaledWidth, scaledHeight) - // 添加描边以增强视觉效果 - ctx.strokeStyle = color - ctx.lineWidth = 0.5 - ctx.globalAlpha = alpha * 1.5 - ctx.stroke() - } - - // 根据可见性绘制 RGB 直方图 - if (channelVisibility.red) { - drawHistogram(red, 'rgba(255, 99, 99, 0.6)', 0.6) - } - if (channelVisibility.green) { - drawHistogram(green, 'rgba(99, 255, 99, 0.6)', 0.6) - } - if (channelVisibility.blue) { - drawHistogram(blue, 'rgba(99, 99, 255, 0.6)', 0.6) - } - - // 绘制亮度直方图 - if (channelVisibility.luminance) { - ctx.globalAlpha = 0.8 - - // 使用路径绘制更平滑的亮度直方图 - ctx.beginPath() - ctx.moveTo(padding, height - padding) - - for (const [i, element] of luminance.entries()) { - const x = padding + i * barWidth - const barHeight = (element / globalMax) * drawHeight - const y = height - padding - barHeight - - ctx.lineTo(x, y) + try { + const imageData = ctx.getImageData(0, 0, scaledWidth, scaledHeight) + const calculatedHistogram = calculateHistogram(imageData) + setHistogram(calculatedHistogram) + } catch (e) { + console.error('Error calculating histogram:', e) + setError(true) + } finally { + setLoading(false) } - - ctx.lineTo(padding + drawWidth, height - padding) - ctx.closePath() - - ctx.fillStyle = 'rgba(255, 255, 255, 0.8)' - ctx.fill() - - // 添加描边 - ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)' - ctx.lineWidth = 0.5 - ctx.globalAlpha = 0.9 - ctx.stroke() } - // 重置透明度 - ctx.globalAlpha = 1 - - // 绘制阴影和高光区域标记 - ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)' - ctx.lineWidth = 1 - ctx.setLineDash([2, 2]) - - // 阴影区域 (0-85) - const shadowEnd = padding + (85 * drawWidth) / 255 - ctx.beginPath() - ctx.moveTo(shadowEnd, padding) - ctx.lineTo(shadowEnd, height - padding) - ctx.stroke() - - // 高光区域 (170-255) - const highlightStart = padding + (170 * drawWidth) / 255 - ctx.beginPath() - ctx.moveTo(highlightStart, padding) - ctx.lineTo(highlightStart, height - padding) - ctx.stroke() - - ctx.setLineDash([]) - - // 绘制悬停指示线 - if (hoverInfo) { - const barWidth = drawWidth / 256 - const hoverX = padding + hoverInfo.x * barWidth - ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)' - ctx.lineWidth = 1 - ctx.setLineDash([]) - ctx.beginPath() - ctx.moveTo(hoverX, padding) - ctx.lineTo(hoverX, height - padding) - ctx.stroke() + img.onerror = () => { + setError(true) + setLoading(false) } - }, [toneAnalysis, channelVisibility, hoverInfo]) + }, [thumbnailUrl]) - // 计算统计信息 - const getStatistics = () => { - const decompressedHistogram = decompressHistogram(toneAnalysis.histogram) - const { red, green, blue, luminance } = decompressedHistogram - - const calculateStats = (data: number[]) => { - const total = data.reduce((sum, val) => sum + val, 0) - if (total === 0) return { mean: 0, median: 0, mode: 0 } - - let weightedSum = 0 - let count = 0 - for (const [i, datum] of data.entries()) { - weightedSum += i * datum - count += datum - } - const mean = Math.round(weightedSum / count) - - // 找到中位数 - let cumulative = 0 - let median = 0 - for (const [i, datum] of data.entries()) { - cumulative += datum - if (cumulative >= count / 2) { - median = i - break - } - } - - // 找到众数 - const maxValue = Math.max(...data) - const mode = data.indexOf(maxValue) - - return { mean, median, mode } + useEffect(() => { + if (histogram && canvasRef.current) { + drawHistogram(canvasRef.current, histogram) } - - return { - luminance: calculateStats(luminance), - red: calculateStats(red), - green: calculateStats(green), - blue: calculateStats(blue), - } - } - - const stats = getStatistics() + }, [histogram]) return ( -
- {/* 主画布 */} -
- - - {/* 悬停信息 */} - {hoverInfo && ( - -
- {t('histogram.value')}: {hoverInfo.value} +
+ {loading && ( +
+
+
+ )} + {error && ( +
+
+
+ {t('photo.error.loading')}
- {channelVisibility.red && ( -
- R: {hoverInfo.channels.red.toFixed(2)} -
- )} - {channelVisibility.green && ( -
- G: {hoverInfo.channels.green.toFixed(2)} -
- )} - {channelVisibility.blue && ( -
- B: {hoverInfo.channels.blue.toFixed(2)} -
- )} - {channelVisibility.luminance && ( -
- {t('histogram.luminance')}:{' '} - {hoverInfo.channels.luminance.toFixed(2)} -
- )} - - )} -
- - {/* 控制面板 */} -
- {/* 通道切换 */} -
-
- - - - -
- -
-
- - {/* 统计信息 */} - - {showStats && ( - -
-
-
- {t('histogram.channel')} -
-
- {channelVisibility.luminance && ( -
- {t('histogram.luminance')} -
- )} - {channelVisibility.red && ( -
{t('histogram.red')}
- )} - {channelVisibility.green && ( -
- {t('histogram.green')} -
- )} - {channelVisibility.blue && ( -
{t('histogram.blue')}
- )} -
-
-
-
- {t('histogram.mean')} -
-
- {channelVisibility.luminance && ( -
{stats.luminance.mean}
- )} - {channelVisibility.red &&
{stats.red.mean}
} - {channelVisibility.green &&
{stats.green.mean}
} - {channelVisibility.blue &&
{stats.blue.mean}
} -
-
-
-
- {t('histogram.median')} -
-
- {channelVisibility.luminance && ( -
{stats.luminance.median}
- )} - {channelVisibility.red &&
{stats.red.median}
} - {channelVisibility.green &&
{stats.green.median}
} - {channelVisibility.blue &&
{stats.blue.median}
} -
-
-
-
- {t('histogram.mode')} -
-
- {channelVisibility.luminance && ( -
{stats.luminance.mode}
- )} - {channelVisibility.red &&
{stats.red.mode}
} - {channelVisibility.green &&
{stats.green.mode}
} - {channelVisibility.blue &&
{stats.blue.mode}
} -
-
-
-
- )} -
-
+ )} + {histogram && ( + + )}
) } diff --git a/apps/web/src/pages/(data)/manifest.tsx b/apps/web/src/pages/(data)/manifest.tsx index d6a46439..c419b29e 100644 --- a/apps/web/src/pages/(data)/manifest.tsx +++ b/apps/web/src/pages/(data)/manifest.tsx @@ -192,7 +192,7 @@ export const Component = () => { const photos = photoLoader.getPhotos() const manifestData = { - version: 'v2', + version: 'v3', data: photos, } @@ -355,7 +355,7 @@ export const Component = () => { diff --git a/locales/app/en.json b/locales/app/en.json index 42692c9d..f5db3748 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -209,16 +209,6 @@ "gallery.built.at": "Built at ", "gallery.photos_one": "{{count}} photo", "gallery.photos_other": "{{count}} photos", - "histogram.blue": "Blue", - "histogram.channel": "Channel", - "histogram.green": "Green", - "histogram.luminance": "Lu", - "histogram.mean": "Mean", - "histogram.median": "Median", - "histogram.mode": "Mode", - "histogram.red": "Red", - "histogram.statistics": "Statistics", - "histogram.value": "Value", "loading.converting": "Converting...", "loading.default": "Loading", "loading.heic.converting": "Converting HEIC/HEIF image format...", diff --git a/locales/app/jp.json b/locales/app/jp.json index c66efdfe..5fe2d3fa 100644 --- a/locales/app/jp.json +++ b/locales/app/jp.json @@ -208,16 +208,6 @@ "gallery.built.at": "ビルド日時 ", "gallery.photos_one": "写真{{count}}枚", "gallery.photos_other": "写真{{count}}枚", - "histogram.blue": "青", - "histogram.channel": "チャンネル", - "histogram.green": "緑", - "histogram.luminance": "輝度", - "histogram.mean": "平均値", - "histogram.median": "中央値", - "histogram.mode": "最頻値", - "histogram.red": "赤", - "histogram.statistics": "統計", - "histogram.value": "値", "loading.converting": "変換中...", "loading.default": "読み込み中", "loading.heic.converting": "HEIC/HEIF 画像フォーマットを変換中...", @@ -233,10 +223,10 @@ "photo.error.loading": "画像の読み込みに失敗しました", "photo.live.badge": "Live", "photo.live.converting.detail": "{{method}}を使用してビデオ形式を変換しています...", - "photo.live.converting.video": "Live Photoのビデオを変換中", + "photo.live.converting.video": "Live Photo のビデオを変換中", "photo.live.playing": "ライブ写真を再生中", - "photo.live.tooltip.desktop.main": "ホバーしてLive Photoを再生", - "photo.live.tooltip.desktop.zoom": "ホバーしてLive Photoを再生 / ダブルクリックしてズーム", + "photo.live.tooltip.desktop.main": "ホバーして Live Photo を再生", + "photo.live.tooltip.desktop.zoom": "ホバーして Live Photo を再生 / ダブルクリックしてズーム", "photo.live.tooltip.mobile.main": "長押ししてライブフォトを再生", "photo.live.tooltip.mobile.zoom": "長押ししてライブフォトを再生/ダブルタップしてズーム", "photo.share.actions": "アクション", diff --git a/locales/app/ko.json b/locales/app/ko.json index 66b1dcd3..d987fdbb 100644 --- a/locales/app/ko.json +++ b/locales/app/ko.json @@ -172,10 +172,10 @@ "exif.sensing.method.color.sequential.linear": "컬러 순차 선형 센서", "exif.sensing.method.color.sequential.main": "컬러 순차 영역 센서", "exif.sensing.method.one-chip-color-area": "1 칩 컬러 영역 센서", - "exif.sensing.method.one.chip": "1칩 컬러 영역 센서", - "exif.sensing.method.three.chip": "3칩 컬러 영역 센서", + "exif.sensing.method.one.chip": "1 칩 컬러 영역 센서", + "exif.sensing.method.three.chip": "3 칩 컬러 영역 센서", "exif.sensing.method.trilinear": "삼선형 센서", - "exif.sensing.method.two.chip": "2칩 컬러 영역 센서", + "exif.sensing.method.two.chip": "2 칩 컬러 영역 센서", "exif.sensing.method.type": "감지 방식", "exif.sensing.method.undefined": "정의되지 않음", "exif.shadow.ratio": "섀도우 비율", @@ -208,16 +208,6 @@ "gallery.built.at": "빌드 날짜 ", "gallery.photos_one": "사진 {{count}}장", "gallery.photos_other": "사진 {{count}}장", - "histogram.blue": "파랑", - "histogram.channel": "채널", - "histogram.green": "초록", - "histogram.luminance": "휘도", - "histogram.mean": "평균", - "histogram.median": "중간값", - "histogram.mode": "최빈값", - "histogram.red": "빨강", - "histogram.statistics": "통계", - "histogram.value": "값", "loading.converting": "변환 중...", "loading.default": "로딩 중", "loading.heic.converting": "HEIC/HEIF 이미지 형식 변환 중...", diff --git a/locales/app/zh-CN.json b/locales/app/zh-CN.json index 2ae025f4..f72b3631 100644 --- a/locales/app/zh-CN.json +++ b/locales/app/zh-CN.json @@ -209,16 +209,6 @@ "gallery.built.at": "构建于 ", "gallery.photos_one": "{{count}} 张照片", "gallery.photos_other": "{{count}} 张照片", - "histogram.blue": "蓝", - "histogram.channel": "通道", - "histogram.green": "绿", - "histogram.luminance": "亮度", - "histogram.mean": "平均值", - "histogram.median": "中位数", - "histogram.mode": "众数", - "histogram.red": "红", - "histogram.statistics": "统计", - "histogram.value": "值", "loading.converting": "转换中...", "loading.default": "加载中", "loading.heic.converting": "正在转换 HEIC/HEIF 图像格式...", diff --git a/locales/app/zh-HK.json b/locales/app/zh-HK.json index ac74b543..fca09894 100644 --- a/locales/app/zh-HK.json +++ b/locales/app/zh-HK.json @@ -202,22 +202,12 @@ "exif.white.balance.kelvin": "手動色溫", "exif.white.balance.manual": "手動", "exif.white.balance.red": "紅色", - "exif.white.balance.shift.ab": "白平衡偏移 (琥珀色-藍色)", - "exif.white.balance.shift.gm": "白平衡偏移 (綠色-洋紅色)", + "exif.white.balance.shift.ab": "白平衡偏移 (琥珀色 - 藍色)", + "exif.white.balance.shift.gm": "白平衡偏移 (綠色 - 洋紅色)", "exif.white.balance.title": "白平衡", "gallery.built.at": "建置於 ", "gallery.photos_one": "{{count}} 張照片", "gallery.photos_other": "{{count}} 張照片", - "histogram.blue": "藍", - "histogram.channel": "通道", - "histogram.green": "綠", - "histogram.luminance": "亮度", - "histogram.mean": "平均值", - "histogram.median": "中位數", - "histogram.mode": "眾數", - "histogram.red": "紅", - "histogram.statistics": "統計", - "histogram.value": "值", "loading.converting": "轉換中...", "loading.default": "載入中", "loading.heic.converting": "正在轉換 HEIC/HEIF 圖像格式...", diff --git a/locales/app/zh-TW.json b/locales/app/zh-TW.json index cbb022b1..956571ec 100644 --- a/locales/app/zh-TW.json +++ b/locales/app/zh-TW.json @@ -208,16 +208,6 @@ "gallery.built.at": "建置於 ", "gallery.photos_one": "{{count}} 張照片", "gallery.photos_other": "{{count}} 張照片", - "histogram.blue": "藍", - "histogram.channel": "通道", - "histogram.green": "綠", - "histogram.luminance": "亮度", - "histogram.mean": "平均值", - "histogram.median": "中位數", - "histogram.mode": "眾數", - "histogram.red": "紅", - "histogram.statistics": "統計", - "histogram.value": "值", "loading.converting": "轉換中...", "loading.default": "載入中", "loading.heic.converting": "正在轉換 HEIC/HEIF 圖像格式...", diff --git a/packages/builder/src/builder/builder.ts b/packages/builder/src/builder/builder.ts index bade4119..1ce256d6 100644 --- a/packages/builder/src/builder/builder.ts +++ b/packages/builder/src/builder/builder.ts @@ -312,7 +312,7 @@ class PhotoGalleryBuilder { ): Promise { return options.isForceMode || options.isForceManifest ? { - version: 'v2', + version: 'v3', data: [], } : await loadExistingManifest() diff --git a/packages/builder/src/image/histogram.ts b/packages/builder/src/image/histogram.ts index c9c7c743..fb66d619 100644 --- a/packages/builder/src/image/histogram.ts +++ b/packages/builder/src/image/histogram.ts @@ -1,44 +1,11 @@ -import type { Logger } from '@afilmory/builder/logger/index.js' import type { - CompressedHistogramData, HistogramData, ToneAnalysis, ToneType, } from '@afilmory/builder/types/photo.js' import type sharp from 'sharp' -/** - * 将 256 点位直方图压缩到 64 点位 - * @param histogram 原始直方图数据 - * @returns 压缩后的直方图数据 - */ -function compressHistogram(histogram: HistogramData): CompressedHistogramData { - const compressChannel = (data: number[]): number[] => { - const compressed: number[] = [] - const groupSize = 4 // 256 / 64 = 4,每 4 个点合并为 1 个点 - - for (let i = 0; i < 64; i++) { - let sum = 0 - for (let j = 0; j < groupSize; j++) { - const index = i * groupSize + j - if (index < data.length) { - sum += data[index] - } - } - // 量化为整数 (0-10000),保留4位小数精度 - compressed[i] = Math.round(sum * 10000) - } - - return compressed - } - - return { - red: compressChannel(histogram.red), - green: compressChannel(histogram.green), - blue: compressChannel(histogram.blue), - luminance: compressChannel(histogram.luminance), - } -} +import { getGlobalLoggers } from '../photo' /** * 计算图片的直方图 @@ -48,9 +15,8 @@ function compressHistogram(histogram: HistogramData): CompressedHistogramData { */ async function calculateHistogram( sharpInstance: sharp.Sharp, - imageLogger?: Logger['image'], ): Promise { - const log = imageLogger + const log = getGlobalLoggers().image try { log?.info('开始计算图片直方图') @@ -114,11 +80,8 @@ async function calculateHistogram( * @param imageLogger 日志记录器 * @returns 影调分析结果 */ -function analyzeTone( - histogram: HistogramData, - imageLogger?: Logger['image'], -): ToneAnalysis { - const log = imageLogger +function analyzeTone(histogram: HistogramData): ToneAnalysis { + const log = getGlobalLoggers().image try { log?.info('开始分析图片影调') @@ -180,7 +143,6 @@ function analyzeTone( contrast, shadowRatio: Math.round(shadowRatio * 100) / 100, highlightRatio: Math.round(highlightRatio * 100) / 100, - histogram: compressHistogram(histogram), } log?.success( @@ -189,21 +151,15 @@ function analyzeTone( return result } catch (error) { - log?.error('分析影调失败:', error) + log.error('分析影调失败:', error) // 返回默认值 - const defaultHistogram: CompressedHistogramData = { - red: Array.from({ length: 64 }).fill(0) as number[], - green: Array.from({ length: 64 }).fill(0) as number[], - blue: Array.from({ length: 64 }).fill(0) as number[], - luminance: Array.from({ length: 64 }).fill(0) as number[], - } + return { toneType: 'normal', brightness: 50, contrast: 50, shadowRatio: 0.33, highlightRatio: 0.33, - histogram: defaultHistogram, } } } @@ -216,12 +172,11 @@ function analyzeTone( */ export async function calculateHistogramAndAnalyzeTone( sharpInstance: sharp.Sharp, - imageLogger?: Logger['image'], ): Promise { - const histogram = await calculateHistogram(sharpInstance, imageLogger) + const histogram = await calculateHistogram(sharpInstance) if (!histogram) { return null } - return analyzeTone(histogram, imageLogger) + return analyzeTone(histogram) } diff --git a/packages/builder/src/manifest/manager.ts b/packages/builder/src/manifest/manager.ts index e104adbb..7fc69c3c 100644 --- a/packages/builder/src/manifest/manager.ts +++ b/packages/builder/src/manifest/manager.ts @@ -11,22 +11,28 @@ import type { PhotoManifestItem } from '../types/photo.js' const manifestPath = path.join(workdir, 'src/data/photos-manifest.json') export async function loadExistingManifest(): Promise { + let manifest: AfilmoryManifest try { const manifestContent = await fs.readFile(manifestPath, 'utf-8') - const manifest = JSON.parse(manifestContent) as AfilmoryManifest - if (manifest.version !== 'v2') { - throw new Error('Invalid manifest version') - } - return manifest + manifest = JSON.parse(manifestContent) as AfilmoryManifest } catch { logger.fs.error( '🔍 未找到 manifest 文件/解析失败,创建新的 manifest 文件...', ) return { - version: 'v2', + version: 'v3', data: [], } } + + if (manifest.version !== 'v3') { + logger.fs.error('🔍 无效的 manifest 版本,创建新的 manifest 文件...') + return { + version: 'v3', + data: [], + } + } + return manifest } // 检查照片是否需要更新(基于最后修改时间) @@ -55,7 +61,7 @@ export async function saveManifest(items: PhotoManifestItem[]): Promise { manifestPath, JSON.stringify( { - version: 'v2', + version: 'v3', data: sortedManifest, } as AfilmoryManifest, null, diff --git a/packages/builder/src/photo/data-processors.ts b/packages/builder/src/photo/data-processors.ts index d8da982a..e6fa7e73 100644 --- a/packages/builder/src/photo/data-processors.ts +++ b/packages/builder/src/photo/data-processors.ts @@ -76,10 +76,6 @@ export async function processThumbnailAndBlurhash( width, height, options.isForceMode || options.isForceThumbnails, - { - thumbnail: loggers.thumbnail.originalLogger, - blurhash: loggers.blurhash.originalLogger, - }, ) return { @@ -140,8 +136,5 @@ export async function processToneAnalysis( } // 计算新的影调分析 - return await calculateHistogramAndAnalyzeTone( - sharpInstance, - loggers.tone.originalLogger, - ) + return await calculateHistogramAndAnalyzeTone(sharpInstance) } diff --git a/packages/builder/src/types/manifest.ts b/packages/builder/src/types/manifest.ts index d6533440..b35401f5 100644 --- a/packages/builder/src/types/manifest.ts +++ b/packages/builder/src/types/manifest.ts @@ -1,6 +1,6 @@ import type { PhotoManifestItem } from './photo' export type AfilmoryManifest = { - version: 'v2' + version: 'v3' data: PhotoManifestItem[] } diff --git a/packages/builder/src/types/photo.ts b/packages/builder/src/types/photo.ts index 81686211..a4020d0e 100644 --- a/packages/builder/src/types/photo.ts +++ b/packages/builder/src/types/photo.ts @@ -26,7 +26,6 @@ export interface ToneAnalysis { contrast: number // 0-100,对比度 shadowRatio: number // 0-1,阴影区域占比 highlightRatio: number // 0-1,高光区域占比 - histogram: CompressedHistogramData // 压缩的直方图数据 } export interface PhotoInfo {