feat: migrate from exif-reader to exiftool-vendored and update EXIF handling

- Replaced `exif-reader` with `exiftool-vendored` for improved EXIF data processing.
- Updated related components to utilize the new `PickedExif` type for better type safety.
- Refactored EXIF data extraction and formatting logic to align with the new library's structure.
- Removed deprecated dependencies and cleaned up related code for better maintainability.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-06-14 17:19:28 +08:00
parent b14dd85b28
commit ece05078b1
24 changed files with 1384 additions and 1276 deletions

View File

@@ -39,9 +39,7 @@
"consola": "3.4.2",
"dotenv": "16.5.0",
"es-toolkit": "1.39.3",
"exif-reader": "2.0.2",
"foxact": "0.2.49",
"fuji-recipes": "1.0.2",
"heic-to": "1.1.14",
"i18next": "25.2.1",
"i18next-browser-languagedetector": "8.2.0",

View File

@@ -1,6 +1,7 @@
import './PhotoViewer.css'
import type { Exif } from 'exif-reader'
import type { PickedExif } from '@afilmory/data'
import { isNil } from 'es-toolkit/compat'
import { m } from 'motion/react'
import type { FC } from 'react'
import { Fragment } from 'react'
@@ -8,7 +9,6 @@ import { useTranslation } from 'react-i18next'
import { ScrollArea } from '~/components/ui/scroll-areas/ScrollArea'
import { useMobile } from '~/hooks/useMobile'
import { i18nAtom } from '~/i18n'
import {
CarbonIsoOutline,
MaterialSymbolsExposure,
@@ -17,22 +17,21 @@ import {
TablerAperture,
} from '~/icons'
import { getImageFormat } from '~/lib/image-utils'
import { jotaiStore } from '~/lib/jotai'
import { Spring } from '~/lib/spring'
import type { PhotoManifest } from '~/types/photo'
import { MotionButtonBase } from '../button'
import { EllipsisHorizontalTextWithTooltip } from '../typography'
import { formatExifData, Row } from './formatExifData'
export const ExifPanel: FC<{
currentPhoto: PhotoManifest
exifData: Exif | null
exifData: PickedExif | null
onClose?: () => void
}> = ({ currentPhoto, exifData, onClose }) => {
const { t } = useTranslation()
const isMobile = useMobile()
const formattedExifData = formatExifData(exifData, t)
const formattedExifData = formatExifData(exifData)
// 使用通用的图片格式提取函数
const imageFormat = getImageFormat(
@@ -126,10 +125,10 @@ export const ExifPanel: FC<{
{/* 标签信息 - 移到基本信息 section 内 */}
{currentPhoto.tags && currentPhoto.tags.length > 0 && (
<div className="mt-3">
<div className="mb-2 text-sm text-white/80">
<h4 className="mb-2 text-sm font-medium text-white/80">
{t('exif.tags')}
</div>
<div className="flex flex-wrap gap-1.5">
</h4>
<div className="-ml-1 flex flex-wrap gap-1.5">
{currentPhoto.tags.map((tag) => (
<MotionButtonBase
type="button"
@@ -190,12 +189,6 @@ export const ExifPanel: FC<{
value={`f/${formattedExifData.maxAperture}`}
/>
)}
{formattedExifData.digitalZoom && (
<Row
label={t('exif.digital.zoom')}
value={`${formattedExifData.digitalZoom.toFixed(2)}x`}
/>
)}
</div>
</div>
)}
@@ -254,6 +247,7 @@ export const ExifPanel: FC<{
{/* 新增:拍摄模式信息 */}
{(formattedExifData.exposureMode ||
formattedExifData.exposureProgram ||
formattedExifData.meteringMode ||
formattedExifData.whiteBalance ||
formattedExifData.lightSource ||
@@ -263,85 +257,62 @@ export const ExifPanel: FC<{
{t('exif.capture.mode')}
</h4>
<div className="space-y-1 text-sm">
{formattedExifData.exposureMode && (
{!isNil(formattedExifData.exposureProgram) && (
<Row
label={t('exif.exposureprogram.title')}
value={formattedExifData.exposureProgram}
/>
)}
{!isNil(formattedExifData.exposureMode) && (
<Row
label={t('exif.exposure.mode.title')}
value={formattedExifData.exposureMode}
/>
)}
{formattedExifData.meteringMode && (
{!isNil(formattedExifData.meteringMode) && (
<Row
label={t('exif.metering.mode.type')}
value={formattedExifData.meteringMode}
/>
)}
{formattedExifData.whiteBalance && (
{!isNil(formattedExifData.whiteBalance) && (
<Row
label={t('exif.white.balance.title')}
value={formattedExifData.whiteBalance}
/>
)}
{formattedExifData.whiteBalanceBias && (
{!isNil(formattedExifData.whiteBalanceBias) && (
<Row
label={t('exif.white.balance.bias')}
value={`${formattedExifData.whiteBalanceBias} Mired`}
/>
)}
{formattedExifData.wbShiftAB && (
{!isNil(formattedExifData.wbShiftAB) && (
<Row
label={t('exif.white.balance.shift.ab')}
value={formattedExifData.wbShiftAB}
/>
)}
{formattedExifData.wbShiftGM && (
{!isNil(formattedExifData.wbShiftGM) && (
<Row
label={t('exif.white.balance.shift.gm')}
value={formattedExifData.wbShiftGM}
/>
)}
{formattedExifData.whiteBalanceFineTune && (
{!isNil(formattedExifData.whiteBalanceFineTune) && (
<Row
label={t('exif.white.balance.fine.tune')}
value={formattedExifData.whiteBalanceFineTune}
/>
)}
{formattedExifData.wbGRBLevels && (
<Row
label={t('exif.white.balance.grb')}
value={
Array.isArray(formattedExifData.wbGRBLevels)
? formattedExifData.wbGRBLevels.join(' ')
: formattedExifData.wbGRBLevels
}
/>
)}
{formattedExifData.wbGRBLevelsStandard && (
<Row
label={t('exif.standard.white.balance.grb')}
value={
Array.isArray(formattedExifData.wbGRBLevelsStandard)
? formattedExifData.wbGRBLevelsStandard.join(' ')
: formattedExifData.wbGRBLevelsStandard
}
/>
)}
{formattedExifData.wbGRBLevelsAuto && (
<Row
label={t('exif.auto.white.balance.grb')}
value={
Array.isArray(formattedExifData.wbGRBLevelsAuto)
? formattedExifData.wbGRBLevelsAuto.join(' ')
: formattedExifData.wbGRBLevelsAuto
}
/>
)}
{formattedExifData.flash && (
{!isNil(formattedExifData.flash) && (
<Row
label={t('exif.flash.title')}
value={formattedExifData.flash}
/>
)}
{formattedExifData.lightSource && (
{!isNil(formattedExifData.lightSource) && (
<Row
label={t('exif.light.source.type')}
value={formattedExifData.lightSource}
@@ -363,68 +334,80 @@ export const ExifPanel: FC<{
value={formattedExifData.fujiRecipe.FilmMode}
/>
)}
{formattedExifData.fujiRecipe.DynamicRange && (
{!isNil(formattedExifData.fujiRecipe.DynamicRange) && (
<Row
label={t('exif.dynamic.range')}
value={formattedExifData.fujiRecipe.DynamicRange}
/>
)}
{formattedExifData.fujiRecipe.WhiteBalance && (
{!isNil(formattedExifData.fujiRecipe.WhiteBalance) && (
<Row
label={t('exif.white.balance.title')}
value={formattedExifData.fujiRecipe.WhiteBalance}
/>
)}
{formattedExifData.fujiRecipe.HighlightTone && (
{!isNil(formattedExifData.fujiRecipe.HighlightTone) && (
<Row
label={t('exif.highlight.tone')}
value={formattedExifData.fujiRecipe.HighlightTone}
/>
)}
{formattedExifData.fujiRecipe.ShadowTone && (
{!isNil(formattedExifData.fujiRecipe.ShadowTone) && (
<Row
label={t('exif.shadow.tone')}
value={formattedExifData.fujiRecipe.ShadowTone}
/>
)}
{formattedExifData.fujiRecipe.Saturation && (
{!isNil(formattedExifData.fujiRecipe.Saturation) && (
<Row
label={t('exif.saturation')}
value={formattedExifData.fujiRecipe.Saturation}
/>
)}
{formattedExifData.fujiRecipe.Sharpness && (
{!isNil(formattedExifData.fujiRecipe.Sharpness) && (
<Row
label={t('exif.sharpness')}
value={formattedExifData.fujiRecipe.Sharpness}
/>
)}
{formattedExifData.fujiRecipe.NoiseReduction && (
{!isNil(formattedExifData.fujiRecipe.NoiseReduction) && (
<Row
label={t('exif.noise.reduction')}
value={formattedExifData.fujiRecipe.NoiseReduction}
/>
)}
{formattedExifData.fujiRecipe.Clarity && (
{!isNil(formattedExifData.fujiRecipe.Clarity) && (
<Row
label={t('exif.clarity')}
value={formattedExifData.fujiRecipe.Clarity}
/>
)}
{formattedExifData.fujiRecipe.ColorChromeEffect && (
{!isNil(formattedExifData.fujiRecipe.ColorChromeEffect) && (
<Row
label={t('exif.color.effect')}
value={formattedExifData.fujiRecipe.ColorChromeEffect}
/>
)}
{formattedExifData.fujiRecipe.ColorChromeFxBlue && (
{!isNil(formattedExifData.fujiRecipe.ColorChromeFxBlue) && (
<Row
label={t('exif.blue.color.effect')}
value={formattedExifData.fujiRecipe.ColorChromeFxBlue}
/>
)}
{(formattedExifData.fujiRecipe.GrainEffectRoughness ||
formattedExifData.fujiRecipe.GrainEffectSize) && (
{!isNil(
formattedExifData.fujiRecipe.WhiteBalanceFineTune,
) && (
<Row
label={t('exif.white.balance.fine.tune')}
value={
formattedExifData.fujiRecipe.WhiteBalanceFineTune
}
/>
)}
{(!isNil(
formattedExifData.fujiRecipe.GrainEffectRoughness,
) ||
!isNil(formattedExifData.fujiRecipe.GrainEffectSize)) && (
<>
{formattedExifData.fujiRecipe.GrainEffectRoughness && (
<Row
@@ -434,7 +417,9 @@ export const ExifPanel: FC<{
}
/>
)}
{formattedExifData.fujiRecipe.GrainEffectSize && (
{!isNil(
formattedExifData.fujiRecipe.GrainEffectSize,
) && (
<Row
label={t('exif.grain.effect.size')}
value={formattedExifData.fujiRecipe.GrainEffectSize}
@@ -442,57 +427,6 @@ export const ExifPanel: FC<{
)}
</>
)}
{(formattedExifData.fujiRecipe.Red ||
formattedExifData.fujiRecipe.Blue) && (
<>
{formattedExifData.fujiRecipe.Red && (
<Row
label={t('exif.red.adjustment')}
value={formattedExifData.fujiRecipe.Red}
/>
)}
{formattedExifData.fujiRecipe.Blue && (
<Row
label={t('exif.blue.adjustment')}
value={formattedExifData.fujiRecipe.Blue}
/>
)}
</>
)}
</div>
</div>
)}
{formattedExifData.gps && (
<div>
<h4 className="my-2 text-sm font-medium text-white/80">
{t('exif.gps.location.info')}
</h4>
<div className="space-y-1 text-sm">
<Row
label={t('exif.gps.latitude')}
value={formattedExifData.gps.latitude}
/>
<Row
label={t('exif.gps.longitude')}
value={formattedExifData.gps.longitude}
/>
{formattedExifData.gps.altitude && (
<Row
label={t('exif.gps.altitude')}
value={`${formattedExifData.gps.altitude}m`}
/>
)}
<div className="mt-2 text-right">
<a
href={`https://uri.amap.com/marker?position=${formattedExifData.gps.longitude},${formattedExifData.gps.latitude}&name=${encodeURIComponent(t('exif.gps.location.name'))}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue inline-flex items-center gap-1 text-xs underline transition-colors hover:text-blue-300"
>
{t('exif.gps.view.map')}
<i className="i-mingcute-external-link-line" />
</a>
</div>
</div>
</div>
)}
@@ -502,7 +436,6 @@ export const ExifPanel: FC<{
formattedExifData.shutterSpeedValue ||
formattedExifData.apertureValue ||
formattedExifData.sensingMethod ||
formattedExifData.customRendered ||
formattedExifData.focalPlaneXResolution ||
formattedExifData.focalPlaneYResolution) && (
<div>
@@ -534,17 +467,12 @@ export const ExifPanel: FC<{
value={formattedExifData.sensingMethod}
/>
)}
{formattedExifData.customRendered && (
<Row
label={t('exif.custom.rendered.type')}
value={formattedExifData.customRendered}
/>
)}
{(formattedExifData.focalPlaneXResolution ||
formattedExifData.focalPlaneYResolution) && (
<Row
label={t('exif.focal.plane.resolution')}
value={`${formattedExifData.focalPlaneXResolution || 'N/A'} × ${formattedExifData.focalPlaneYResolution || 'N/A'}${formattedExifData.focalPlaneResolutionUnit ? ` (${formattedExifData.focalPlaneResolutionUnit})` : ''}`}
value={`${formattedExifData.focalPlaneXResolution || 'N/A'} × ${formattedExifData.focalPlaneYResolution || 'N/A'}`}
/>
)}
</div>
@@ -557,396 +485,3 @@ export const ExifPanel: FC<{
</m.div>
)
}
const formatExifData = (exif: Exif | null, t: any) => {
if (!exif) return null
const photo = exif.Photo || {}
const image = exif.Image || {}
const gps = exif.GPSInfo || {}
// 等效焦距 (35mm)
const focalLength35mm = photo.FocalLengthIn35mmFilm
? Math.round(photo.FocalLengthIn35mmFilm)
: null
// 实际焦距
const focalLength = photo.FocalLength ? Math.round(photo.FocalLength) : null
// ISO
const iso = photo.ISOSpeedRatings || image.ISOSpeedRatings
// 快门速度
const exposureTime = photo.ExposureTime
const shutterSpeed = exposureTime
? exposureTime >= 1
? `${exposureTime}s`
: `1/${Math.round(1 / exposureTime)}`
: null
// 光圈
const aperture = photo.FNumber ? `f/${photo.FNumber}` : null
// 最大光圈
const maxAperture = photo.MaxApertureValue
? `${Math.round(Math.pow(Math.sqrt(2), photo.MaxApertureValue) * 10) / 10}`
: null
// 相机信息
const camera =
image.Make && image.Model ? `${image.Make} ${image.Model}` : null
// 镜头信息
const lens =
photo.LensModel || photo.LensSpecification || photo.LensMake || null
// 软件信息
const software = image.Software || null
const offsetTimeOriginal = photo.OffsetTimeOriginal || photo.OffsetTime
// 拍摄时间
const dateTime: string | null = (() => {
const originalDateTimeStr =
(photo.DateTimeOriginal as unknown as string) ||
(photo.DateTime as unknown as string)
if (!originalDateTimeStr) return null
const date = new Date(originalDateTimeStr)
if (offsetTimeOriginal) {
// 解析时区偏移,例如 "+08:00" 或 "-05:00"
const offsetMatch = offsetTimeOriginal.match(/([+-])(\d{2}):(\d{2})/)
if (offsetMatch) {
const [, sign, hours, minutes] = offsetMatch
const offsetMinutes =
(Number.parseInt(hours) * 60 + Number.parseInt(minutes)) *
(sign === '+' ? 1 : -1)
// 减去偏移量,将本地时间转换为 UTC 时间
const utcTime = new Date(date.getTime() - offsetMinutes * 60 * 1000)
return formatDateTime(utcTime)
}
return formatDateTime(date)
}
return formatDateTime(date)
})()
// 曝光模式
const exposureModeMap: Record<number, string> = {
0: t('exif.exposure.mode.auto'),
1: t('exif.exposure.mode.manual'),
2: t('exif.exposure.mode.bracket'),
}
const exposureMode =
photo.ExposureMode !== undefined
? exposureModeMap[photo.ExposureMode] ||
`${t('exif.unknown')} (${photo.ExposureMode})`
: null
// 测光模式
const meteringModeMap: Record<number, string> = {
0: t('exif.metering.mode.unknown'),
1: t('exif.metering.mode.average'),
2: t('exif.metering.mode.center'),
3: t('exif.metering.mode.spot'),
4: t('exif.metering.mode.multi'),
5: t('exif.metering.mode.pattern'),
6: t('exif.metering.mode.partial'),
}
const meteringMode =
photo.MeteringMode !== undefined
? meteringModeMap[photo.MeteringMode] ||
`${t('exif.unknown')} (${photo.MeteringMode})`
: null
// 白平衡
const whiteBalanceMap: Record<number, string> = {
0: t('exif.white.balance.auto'),
1: t('exif.white.balance.manual'),
}
const whiteBalance =
photo.WhiteBalance !== undefined
? whiteBalanceMap[photo.WhiteBalance] ||
`${t('exif.unknown')} (${photo.WhiteBalance})`
: null
// 闪光灯
const flashMap: Record<number, string> = {
0: t('exif.flash.disabled'),
1: t('exif.flash.enabled'),
5: t('exif.flash.disabled.return.detected'),
7: t('exif.flash.return.detected'),
9: t('exif.flash.forced.mode'),
13: t('exif.flash.forced.disabled.return.detected'),
15: t('exif.flash.forced.return.detected'),
16: t('exif.flash.off.mode'),
24: t('exif.flash.auto.no'),
25: t('exif.flash.auto.yes'),
29: t('exif.flash.auto.no.return'),
31: t('exif.flash.auto.return'),
32: t('exif.flash.unavailable'),
}
const flash =
photo.Flash !== undefined
? flashMap[photo.Flash] || `${t('exif.unknown')} (${photo.Flash})`
: null
// 数字变焦
const digitalZoom = photo.DigitalZoomRatio || null
// 曝光补偿
const exposureBias = photo.ExposureBiasValue
? `${photo.ExposureBiasValue > 0 ? '+' : ''}${photo.ExposureBiasValue.toFixed(1)} EV`
: null
// 亮度值
const brightnessValue = photo.BrightnessValue
? `${photo.BrightnessValue.toFixed(1)} EV`
: null
// 快门速度值
const shutterSpeedValue = photo.ShutterSpeedValue
? `${photo.ShutterSpeedValue.toFixed(1)} EV`
: null
// 光圈值
const apertureValue = photo.ApertureValue
? `${photo.ApertureValue.toFixed(1)} EV`
: null
// 光源类型
const lightSourceMap: Record<number, string> = {
0: t('exif.light.source.auto'),
1: t('exif.light.source.daylight.main'),
2: t('exif.light.source.fluorescent'),
3: t('exif.light.source.tungsten'),
4: t('exif.light.source.flash'),
9: t('exif.light.source.fine.weather'),
10: t('exif.light.source.cloudy'),
11: t('exif.light.source.shade'),
12: t('exif.light.source.daylight.fluorescent'),
13: t('exif.light.source.day.white.fluorescent'),
14: t('exif.light.source.cool.white.fluorescent'),
15: t('exif.light.source.white.fluorescent'),
17: t('exif.light.source.standard.a'),
18: t('exif.light.source.standard.b'),
19: t('exif.light.source.standard.c'),
20: t('exif.light.source.d55'),
21: t('exif.light.source.d65'),
22: t('exif.light.source.d75'),
23: t('exif.light.source.d50'),
24: t('exif.light.source.iso.tungsten'),
255: t('exif.light.source.other'),
}
const lightSource =
photo.LightSource !== undefined
? lightSourceMap[photo.LightSource] ||
`${t('exif.unknown')} (${photo.LightSource})`
: null
// 白平衡偏移/微调相关字段
const whiteBalanceBias = (photo as any).WhiteBalanceBias || null
const wbShiftAB = (photo as any).WBShiftAB || null
const wbShiftGM = (photo as any).WBShiftGM || null
const whiteBalanceFineTune = (photo as any).WhiteBalanceFineTune || null
// 富士相机特有的白平衡字段
const wbGRBLevels =
(photo as any).WBGRBLevels || (photo as any)['WB GRB Levels'] || null
const wbGRBLevelsStandard =
(photo as any).WBGRBLevelsStandard ||
(photo as any)['WB GRB Levels Standard'] ||
null
const wbGRBLevelsAuto =
(photo as any).WBGRBLevelsAuto ||
(photo as any)['WB GRB Levels Auto'] ||
null
// 感光方法
const sensingMethodMap: Record<number, string> = {
1: t('exif.sensing.method.undefined'),
2: t('exif.sensing.method.one.chip'),
3: t('exif.sensing.method.two.chip'),
4: t('exif.sensing.method.three.chip'),
5: t('exif.sensing.method.color.sequential.main'),
7: t('exif.sensing.method.trilinear'),
8: t('exif.sensing.method.color.sequential.linear'),
}
const sensingMethod =
photo.SensingMethod !== undefined
? sensingMethodMap[photo.SensingMethod] ||
`${t('exif.unknown')} (${photo.SensingMethod})`
: null
// 自定义渲染
const customRenderedMap: Record<number, string> = {
0: t('exif.custom.rendered.normal'),
1: t('exif.custom.rendered.special'),
}
const customRendered =
photo.CustomRendered !== undefined
? customRenderedMap[photo.CustomRendered] ||
`${t('exif.unknown')} (${photo.CustomRendered})`
: null
// 焦平面分辨率
const focalPlaneXResolution = photo.FocalPlaneXResolution
? Math.round(photo.FocalPlaneXResolution)
: null
const focalPlaneYResolution = photo.FocalPlaneYResolution
? Math.round(photo.FocalPlaneYResolution)
: null
// 焦平面分辨率单位
const focalPlaneResolutionUnitMap: Record<number, string> = {
1: t('exif.resolution.unit.none'),
2: t('exif.resolution.unit.inches'),
3: t('exif.resolution.unit.cm'),
}
const focalPlaneResolutionUnit =
photo.FocalPlaneResolutionUnit !== undefined
? focalPlaneResolutionUnitMap[photo.FocalPlaneResolutionUnit] ||
`${t('exif.unknown')} (${photo.FocalPlaneResolutionUnit})`
: null
// 像素信息
const pixelXDimension = photo.PixelXDimension || null
const pixelYDimension = photo.PixelYDimension || null
const totalPixels =
pixelXDimension && pixelYDimension
? pixelXDimension * pixelYDimension
: null
const megaPixels = totalPixels
? `${(totalPixels / 1000000).toFixed(1)}MP`
: null
// 色彩空间
const colorSpaceMap: Record<number, string> = {
1: 'sRGB',
65535: 'Adobe RGB',
}
const colorSpace =
photo.ColorSpace !== undefined
? colorSpaceMap[photo.ColorSpace] ||
`${t('exif.unknown')} (${photo.ColorSpace})`
: null
// GPS 信息
let gpsInfo: {
latitude: string | undefined
longitude: string | undefined
altitude: number | null
} | null = null
if (gps.GPSLatitude && gps.GPSLongitude) {
const latitude = convertDMSToDD(gps.GPSLatitude, gps.GPSLatitudeRef || '')
const longitude = convertDMSToDD(
gps.GPSLongitude,
gps.GPSLongitudeRef || '',
)
const altitude = gps.GPSAltitude || null
gpsInfo = {
latitude: latitude?.toFixed(6),
longitude: longitude?.toFixed(6),
altitude: altitude ? Math.round(altitude) : null,
}
}
// 富士相机 Recipe 信息
const fujiRecipe = (exif as any).FujiRecipe || null
return {
focalLength35mm,
focalLength,
iso,
shutterSpeed,
aperture,
maxAperture,
camera,
lens,
software,
dateTime,
exposureMode,
meteringMode,
whiteBalance,
flash,
digitalZoom,
colorSpace,
gps: gpsInfo,
exposureBias,
brightnessValue,
shutterSpeedValue,
apertureValue,
lightSource,
sensingMethod,
customRendered,
focalPlaneXResolution,
focalPlaneYResolution,
focalPlaneResolutionUnit,
megaPixels,
pixelXDimension,
pixelYDimension,
whiteBalanceBias,
wbShiftAB,
wbShiftGM,
whiteBalanceFineTune,
wbGRBLevels,
wbGRBLevelsStandard,
wbGRBLevelsAuto,
fujiRecipe,
}
}
// 将度分秒格式转换为十进制度数
const convertDMSToDD = (dms: number[], ref: string): number | null => {
if (!dms || dms.length !== 3) return null
const [degrees, minutes, seconds] = dms
let dd = degrees + minutes / 60 + seconds / 3600
if (ref === 'S' || ref === 'W') {
dd = dd * -1
}
return dd
}
const Row: FC<{
label: string
value: string | number | null | undefined | number[]
ellipsis?: boolean
}> = ({ label, value, ellipsis }) => {
return (
<div className="flex justify-between gap-4">
<span className="text-text-secondary shrink-0">{label}</span>
{ellipsis ? (
<span className="relative min-w-0 flex-1 shrink">
<span className="absolute inset-0">
<EllipsisHorizontalTextWithTooltip className="text-text min-w-0 text-right">
{Array.isArray(value) ? value.join(' ') : value}
</EllipsisHorizontalTextWithTooltip>
</span>
</span>
) : (
<span className="text-text min-w-0 text-right">
{Array.isArray(value) ? value.join(' ') : value}
</span>
)}
</div>
)
}
const formatDateTime = (date: Date | null | undefined) => {
const i18n = jotaiStore.get(i18nAtom)
const datetimeFormatter = new Intl.DateTimeFormat(i18n.language, {
dateStyle: 'short',
timeStyle: 'medium',
})
if (!date) return ''
return datetimeFormatter.format(date)
}

View File

@@ -0,0 +1,383 @@
import type { FujiRecipe, PickedExif } from '@afilmory/builder'
import type { FC } from 'react'
import { i18nAtom } from '~/i18n'
import { jotaiStore } from '~/lib/jotai'
import { EllipsisHorizontalTextWithTooltip } from '../typography'
// Helper function to clean up EXIF values by removing unnecessary characters
const cleanExifValue = (value: string | null | undefined): string | null => {
if (!value) return null
// Remove parenthetical descriptions like "(medium soft)" from "-1 (medium soft)"
const cleaned = value.replace(/\s*\([^)]*\)$/, '')
return cleaned.trim() || null
}
// Helper function to get translation key for EXIF values
const getTranslationKey = (
category: string,
value: string | number | null,
): string | null => {
if (value === null || value === undefined) return null
const normalizedValue = String(value)
.toLowerCase()
.replaceAll(/\s+/g, '-')
.replaceAll(/[^\w.-]/g, '')
.replaceAll(/-+/g, '-')
.replaceAll(/^-+|-+$/g, '')
return `exif.${category}.${normalizedValue}`
}
// Translation functions for different EXIF categories
const translateExifValue = (
category: string,
value: string | number | null,
): string | null => {
if (!value) return null
const i18n = jotaiStore.get(i18nAtom)
const translationKey = getTranslationKey(category, value)
if (!translationKey) return cleanExifValue(String(value))
// Try to get translation, fallback to cleaned original value
const cleanedValue = cleanExifValue(String(value))
if (!i18n.exists(translationKey)) {
return cleanedValue
}
const translated = i18n.t(translationKey as any)
return translated || cleanedValue
}
const createTranslator =
(category: string) =>
(value: string | number | null): string | null => {
if (value === null || value === undefined) return null
return translateExifValue(category, value)
}
// Specific translation functions for different EXIF fields
const translateExposureMode = createTranslator('exposure.mode')
const translateMeteringMode = createTranslator('metering.mode')
const translateWhiteBalance = createTranslator('white.balance')
const translateFlash = createTranslator('flash')
const translateLightSource = createTranslator('light.source')
const translateSensingMethod = createTranslator('sensing.method')
const translateColorSpace = createTranslator('colorspace')
const translateExposureProgram = createTranslator('exposureprogram')
const translateFujiGrainEffectRoughness = createTranslator(
'fujirecipe-graineffectroughness',
)
const translateFujiGrainEffectSize = createTranslator(
'fujirecipe-graineffectsize',
)
const translateFujiColorChromeEffect = createTranslator(
'fujirecipe-colorchromeeffect',
)
const translateFujiColorChromeFxBlue = createTranslator(
'fujirecipe-colorchromefxblue',
)
const translateFujiDynamicRange = createTranslator('fujirecipe-dynamicrange')
const translateFujiSharpness = createTranslator('fujirecipe-sharpness')
const translateFujiWhiteBalance = createTranslator('fujirecipe-whitebalance')
// 翻译白平衡偏移字段中的 Red 和 Blue
const translateWhiteBalanceFineTune = (value: string | null): string | null => {
if (!value) return null
const i18n = jotaiStore.get(i18nAtom)
const redTranslation = i18n.t('exif.white.balance.red')
const blueTranslation = i18n.t('exif.white.balance.blue')
// 替换 Red 和 Blue 文本,保持数值和符号不变
return value
.replaceAll(/\bRed\b/g, redTranslation)
.replaceAll(/\bBlue\b/g, blueTranslation)
}
// Helper function to process Fuji Recipe values and clean them
const processFujiRecipeValue = (
value: string | null | undefined,
): string | null => {
return cleanExifValue(value)
}
// Process entire Fuji Recipe object
const processFujiRecipe = (recipe: FujiRecipe): any => {
if (!recipe) return null
const processed = { ...recipe } as any
// Clean specific fields that commonly have unnecessary characters
if (processed.HighlightTone) {
processed.HighlightTone = processFujiRecipeValue(recipe.HighlightTone)
}
if (processed.ShadowTone) {
processed.ShadowTone = processFujiRecipeValue(recipe.ShadowTone)
}
if (processed.Saturation) {
processed.Saturation = processFujiRecipeValue(recipe.Saturation)
}
if (processed.NoiseReduction) {
processed.NoiseReduction = processFujiRecipeValue(recipe.NoiseReduction)
}
if (processed.FilmMode) {
processed.FilmMode = processFujiRecipeValue(recipe.FilmMode)
}
if (processed.GrainEffectRoughness) {
processed.GrainEffectRoughness = translateFujiGrainEffectRoughness(
recipe.GrainEffectRoughness,
)
}
if (processed.GrainEffectSize) {
processed.GrainEffectSize = translateFujiGrainEffectSize(
recipe.GrainEffectSize,
)
}
if (processed.ColorChromeEffect) {
processed.ColorChromeEffect = translateFujiColorChromeEffect(
recipe.ColorChromeEffect,
)
}
if (processed.ColorChromeFxBlue) {
processed.ColorChromeFxBlue = translateFujiColorChromeFxBlue(
recipe.ColorChromeFxBlue,
)
}
if (processed.DynamicRange) {
processed.DynamicRange = translateFujiDynamicRange(recipe.DynamicRange)
}
if (processed.Sharpness) {
processed.Sharpness = translateFujiSharpness(recipe.Sharpness)
}
if (processed.WhiteBalance) {
processed.WhiteBalance = translateFujiWhiteBalance(recipe.WhiteBalance)
}
if (processed.WhiteBalanceFineTune) {
processed.WhiteBalanceFineTune = translateWhiteBalanceFineTune(
recipe.WhiteBalanceFineTune,
)
}
return processed
}
export const formatExifData = (exif: PickedExif | null) => {
if (!exif) return null
// 等效焦距 (35mm)
const focalLength35mm = exif.FocalLengthIn35mmFormat
? Number.parseInt(exif.FocalLengthIn35mmFormat)
: null
// 实际焦距
const focalLength = exif.FocalLength
? Number.parseInt(exif.FocalLength)
: null
// ISO
const iso = exif.ISO
// 快门速度
const exposureTime = exif.ExposureTime
const shutterSpeed = `${exposureTime}s`
// 光圈
const aperture = exif.FNumber ? `f/${exif.FNumber}` : null
// 最大光圈
const maxAperture = exif.MaxApertureValue
// 相机信息
const camera = exif.Make && exif.Model ? `${exif.Make} ${exif.Model}` : null
// 镜头信息
const lens = exif.LensModel || null
// 软件信息
const software = exif.Software || null
const offsetTimeOriginal = exif.OffsetTimeOriginal || exif.OffsetTime
// 拍摄时间
const dateTime: string | null = (() => {
const originalDateTimeStr = exif.DateTimeOriginal || (exif as any).DateTime
if (!originalDateTimeStr) return null
const date = new Date(originalDateTimeStr)
if (offsetTimeOriginal) {
// 解析时区偏移,例如 "+08:00" 或 "-05:00"
const offsetMatch = offsetTimeOriginal.match(/([+-])(\d{2}):(\d{2})/)
if (offsetMatch) {
const [, sign, hours, minutes] = offsetMatch
const offsetMinutes =
(Number.parseInt(hours) * 60 + Number.parseInt(minutes)) *
(sign === '+' ? 1 : -1)
// 减去偏移量,将本地时间转换为 UTC 时间
const utcTime = new Date(date.getTime() - offsetMinutes * 60 * 1000)
return formatDateTime(utcTime)
}
return formatDateTime(date)
}
return formatDateTime(date)
})()
// 曝光模式 - with translation
const exposureMode = translateExposureMode(exif.ExposureMode || null)
// 测光模式 - with translation
const meteringMode = translateMeteringMode(exif.MeteringMode || null)
// 白平衡 - with translation
const whiteBalance = translateWhiteBalance(exif.WhiteBalance || null)
// 闪光灯 - with translation
const flash = translateFlash(exif.Flash || null)
// 曝光补偿
const exposureBias = exif.ExposureCompensation
? `${exif.ExposureCompensation} EV`
: null
// 亮度值
const brightnessValue = exif.BrightnessValue
? `${exif.BrightnessValue.toFixed(1)} EV`
: null
// 快门速度值
const shutterSpeedValue = exif.ShutterSpeedValue
// 光圈值
const apertureValue = exif.ApertureValue
? `${exif.ApertureValue.toFixed(1)} EV`
: null
// 光源类型 - with translation
const lightSource = translateLightSource(exif.LightSource || null)
// 白平衡偏移/微调相关字段
const whiteBalanceBias = exif.WhiteBalanceBias || null
const wbShiftAB = exif.WBShiftAB || null
const wbShiftGM = exif.WBShiftGM || null
const whiteBalanceFineTune = translateWhiteBalanceFineTune(
exif.WhiteBalanceFineTune ? String(exif.WhiteBalanceFineTune) : null,
)
// 感光方法 - with translation
const sensingMethod = translateSensingMethod(exif.SensingMethod || null)
// 焦平面分辨率
const focalPlaneXResolution = exif.FocalPlaneXResolution
? Math.round(exif.FocalPlaneXResolution)
: null
const focalPlaneYResolution = exif.FocalPlaneYResolution
? Math.round(exif.FocalPlaneYResolution)
: null
// 像素信息
const pixelXDimension = exif.ImageWidth || null
const pixelYDimension = exif.ImageHeight || null
const totalPixels =
pixelXDimension && pixelYDimension
? pixelXDimension * pixelYDimension
: null
const megaPixels = totalPixels
? `${(totalPixels / 1000000).toFixed(1)}MP`
: null
// 色彩空间 - with translation
const colorSpace = translateColorSpace(exif.ColorSpace || null)
// GPS 信息
const gpsInfo = null
// 富士相机 Recipe 信息 - with cleaning
const exposureProgram = translateExposureProgram(exif.ExposureProgram || null)
return {
focalLength35mm,
focalLength,
iso,
shutterSpeed,
aperture,
maxAperture,
camera,
lens,
software,
dateTime,
exposureMode,
meteringMode,
whiteBalance,
flash,
colorSpace,
gps: gpsInfo,
exposureBias,
brightnessValue,
shutterSpeedValue,
apertureValue,
lightSource,
sensingMethod,
focalPlaneXResolution,
focalPlaneYResolution,
megaPixels,
pixelXDimension,
pixelYDimension,
whiteBalanceBias,
wbShiftAB,
wbShiftGM,
whiteBalanceFineTune,
fujiRecipe: exif.FujiRecipe ? processFujiRecipe(exif.FujiRecipe) : null,
exposureProgram,
}
}
export const Row: FC<{
label: string
value: string | number | null | undefined | number[]
ellipsis?: boolean
}> = ({ label, value, ellipsis }) => {
return (
<div className="flex justify-between gap-4">
<span className="text-text-secondary shrink-0">{label}</span>
{ellipsis ? (
<span className="relative min-w-0 flex-1 shrink">
<span className="absolute inset-0">
<EllipsisHorizontalTextWithTooltip className="text-text min-w-0 text-right">
{Array.isArray(value) ? value.join(' ') : value}
</EllipsisHorizontalTextWithTooltip>
</span>
</span>
) : (
<span className="text-text min-w-0 text-right">
{Array.isArray(value) ? value.join(' ') : value}
</span>
)}
</div>
)
}
const formatDateTime = (date: Date | null | undefined) => {
const i18n = jotaiStore.get(i18nAtom)
const datetimeFormatter = new Intl.DateTimeFormat(i18n.language, {
dateStyle: 'short',
timeStyle: 'medium',
})
if (!date) return ''
return datetimeFormatter.format(date)
}

View File

@@ -1,7 +1,6 @@
import { photoLoader } from '@afilmory/data'
import { atom, useAtom, useAtomValue } from 'jotai'
import { useCallback, useEffect, useMemo } from 'react'
import { useLocation, useNavigate } from 'react-router'
import { useCallback, useMemo } from 'react'
import { gallerySettingAtom } from '~/atoms/app'
@@ -26,14 +25,14 @@ export const usePhotos = () => {
let aDateStr = ''
let bDateStr = ''
if (a.exif && a.exif.Photo && a.exif.Photo.DateTimeOriginal) {
aDateStr = a.exif.Photo.DateTimeOriginal as unknown as string
if (a.exif && a.exif.DateTimeOriginal) {
aDateStr = a.exif.DateTimeOriginal as unknown as string
} else {
aDateStr = a.lastModified
}
if (b.exif && b.exif.Photo && b.exif.Photo.DateTimeOriginal) {
bDateStr = b.exif.Photo.DateTimeOriginal as unknown as string
if (b.exif && b.exif.DateTimeOriginal) {
bDateStr = b.exif.DateTimeOriginal as unknown as string
} else {
bDateStr = b.lastModified
}
@@ -53,8 +52,6 @@ export const usePhotoViewer = () => {
const [currentIndex, setCurrentIndex] = useAtom(currentIndexAtom)
const [triggerElement, setTriggerElement] = useAtom(triggerElementAtom)
const navigate = useNavigate()
const openViewer = useCallback(
(index: number, element?: HTMLElement) => {
setCurrentIndex(index)
@@ -79,20 +76,6 @@ export const usePhotoViewer = () => {
}
}, [currentIndex, photos.length, setCurrentIndex])
const location = useLocation()
useEffect(() => {
if (!isOpen) {
const timer = setTimeout(() => {
navigate('/')
}, 500)
return () => clearTimeout(timer)
}
const targetPathname = `/${photos[currentIndex].id}`
if (location.pathname !== targetPathname) {
navigate(targetPathname)
}
}, [currentIndex, isOpen, location.pathname, navigate, photos])
const goToPrevious = useCallback(() => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1)

View File

@@ -31,8 +31,8 @@ export const useVisiblePhotosDateRange = (_photos: PhotoManifest[]) => {
const getPhotoDate = useCallback((photo: PhotoManifest): Date => {
// 优先使用 EXIF 中的拍摄时间
if (photo.exif?.Photo?.DateTimeOriginal) {
const dateStr = photo.exif.Photo.DateTimeOriginal as unknown as string
if (photo.exif?.DateTimeOriginal) {
const dateStr = photo.exif.DateTimeOriginal as unknown as string
// EXIF 日期格式通常是 "YYYY:MM:DD HH:mm:ss"
const formattedDateStr = dateStr.replace(
/^(\d{4}):(\d{2}):(\d{2})/,

View File

@@ -77,27 +77,26 @@ export const PhotoMasonryItem = ({
}
}
const photo = exif.Photo || {}
const image = exif.Image || {}
// 等效焦距 (35mm)
const focalLength35mm =
photo.FocalLengthIn35mmFilm ||
(photo.FocalLength ? Math.round(photo.FocalLength) : null)
const focalLength35mm = exif.FocalLengthIn35mmFormat
? Number.parseInt(exif.FocalLengthIn35mmFormat)
: exif.FocalLength
? Number.parseInt(exif.FocalLength)
: null
// ISO
const iso = photo.ISOSpeedRatings || image.ISOSpeedRatings
const iso = exif.ISO
// 快门速度
const exposureTime = photo.ExposureTime
const exposureTime = exif.ExposureTime
const shutterSpeed = exposureTime
? exposureTime >= 1
? typeof exposureTime === 'number' && exposureTime >= 1
? `${exposureTime}s`
: `1/${Math.round(1 / exposureTime)}`
: `1/${Math.round(1 / (exposureTime as number))}`
: null
// 光圈
const aperture = photo.FNumber ? `f/${photo.FNumber}` : null
const aperture = exif.FNumber ? `f/${exif.FNumber}` : null
return {
focalLength35mm,

View File

@@ -2,7 +2,13 @@ import { photoLoader } from '@afilmory/data'
import { siteConfig } from '@config'
import { useAtomValue, useSetAtom } from 'jotai'
import { useEffect, useRef } from 'react'
import { Outlet, useParams, useSearchParams } from 'react-router'
import {
Outlet,
useLocation,
useNavigate,
useParams,
useSearchParams,
} from 'react-router'
import { gallerySettingAtom } from '~/atoms/app'
import { ScrollElementContext } from '~/components/ui/scroll-areas/ctx'
@@ -14,7 +20,6 @@ import { MasonryRoot } from '~/modules/gallery/MasonryRoot'
export const Component = () => {
useStateRestoreFromUrl()
useSyncStateToUrl()
useSyncStateToUrl()
const isMobile = useMobile()
@@ -52,6 +57,7 @@ export const Component = () => {
)
}
let isRestored = false
const useStateRestoreFromUrl = () => {
const triggerOnceRef = useRef(false)
@@ -63,6 +69,7 @@ const useStateRestoreFromUrl = () => {
useEffect(() => {
if (triggerOnceRef.current) return
triggerOnceRef.current = true
isRestored = true
if (photoId) {
const photo = photoLoader
@@ -86,8 +93,29 @@ const useStateRestoreFromUrl = () => {
const useSyncStateToUrl = () => {
const { selectedTags } = useAtomValue(gallerySettingAtom)
const [_, setSearchParams] = useSearchParams()
const navigate = useNavigate()
const location = useLocation()
const { isOpen, currentIndex } = usePhotoViewer()
useEffect(() => {
if (!isRestored) return
if (!isOpen) {
const timer = setTimeout(() => {
navigate('/')
}, 500)
return () => clearTimeout(timer)
}
const photos = photoLoader.getPhotos()
const targetPathname = `/${photos[currentIndex].id}`
if (location.pathname !== targetPathname) {
navigate(targetPathname)
}
}, [currentIndex, isOpen, location.pathname, navigate])
useEffect(() => {
if (!isRestored) return
const tags = selectedTags.join(',')
if (tags) {
setSearchParams((search) => {

View File

@@ -1,23 +1 @@
import type { Exif } from 'exif-reader'
import type getRecipe from 'fuji-recipes'
export interface PhotoManifest {
id: string
title: string
description: string
views: number
tags: string[]
originalUrl: string
thumbnailUrl: string
blurhash: string
width: number
height: number
aspectRatio: number
s3Key: string
lastModified: string
size: number
exif: Exif & { FujiRecipe?: ReturnType<typeof getRecipe> }
isLivePhoto?: boolean
livePhotoVideoUrl?: string
livePhotoVideoS3Key?: string
}
export { type PhotoManifest } from '@afilmory/data'

View File

@@ -1,14 +1,14 @@
{
"action.auto": "Auto",
"action.columns.setting": "Columns Setting",
"action.columns.setting": "Column Settings",
"action.sort.mode": "Sort Mode",
"action.tag.filter": "Tag Filter",
"action.view.github": "View GitHub Repository",
"error.feedback": "Still having this issue? Please give feedback in Github, thanks!",
"error.feedback": "Still having this issue? Please provide feedback on Github, thank you!",
"error.reload": "Reload",
"error.submit.issue": "Submit Issue",
"error.temporary.description": "The App has a temporary problem, click the button below to try reloading the app or another solution?",
"error.title": "Sorry, the app has encountered an error",
"error.temporary.description": "The application has encountered a temporary issue. Click the button below to try reloading the application or try other solutions?",
"error.title": "Sorry, the application encountered an error",
"exif.aperture.value": "Aperture Value",
"exif.auto.white.balance.grb": "Auto White Balance GRB",
"exif.basic.info": "Basic Information",
@@ -22,32 +22,50 @@
"exif.clarity": "Clarity",
"exif.color.effect": "Color Effect",
"exif.color.space": "Color Space",
"exif.custom.rendered.normal": "Normal process",
"exif.custom.rendered.special": "Custom process",
"exif.colorspace.adobe.rgb": "Adobe RGB",
"exif.colorspace.srgb": "sRGB",
"exif.colorspace.uncalibrated": "Uncalibrated",
"exif.custom.rendered.normal": "Normal Process",
"exif.custom.rendered.special": "Custom Process",
"exif.custom.rendered.type": "Custom Rendered",
"exif.device.info": "Device Information",
"exif.digital.zoom": "Digital Zoom",
"exif.dimensions": "Dimensions",
"exif.dynamic.range": "Dynamic Range",
"exif.exposure.mode.auto": "Auto exposure",
"exif.exposure.mode.bracket": "Auto bracket",
"exif.exposure.mode.manual": "Manual exposure",
"exif.exposure.mode.auto": "Auto Exposure",
"exif.exposure.mode.bracket": "Auto Bracket",
"exif.exposure.mode.manual": "Manual Exposure",
"exif.exposure.mode.title": "Exposure Mode",
"exif.exposureprogram.action": "Action Program",
"exif.exposureprogram.aperture-priority": "Aperture Priority",
"exif.exposureprogram.aperture-priority-ae": "Aperture Priority AE",
"exif.exposureprogram.creative": "Creative Program",
"exif.exposureprogram.landscape": "Landscape Mode",
"exif.exposureprogram.manual": "Manual",
"exif.exposureprogram.normal": "Program",
"exif.exposureprogram.not-defined": "Not Defined",
"exif.exposureprogram.portrait": "Portrait Mode",
"exif.exposureprogram.program-ae": "Program AE",
"exif.exposureprogram.shutter-priority": "Shutter Priority",
"exif.exposureprogram.title": "Exposure Program",
"exif.file.size": "File Size",
"exif.filename": "Filename",
"exif.film.mode": "Film Mode",
"exif.flash.auto.no.return": "Flash fired, auto mode, no return detected",
"exif.flash.auto.no.title": "Flash did not fire, auto mode",
"exif.flash.auto.return": "Flash fired, auto mode, return detected",
"exif.flash.auto.no-return": "Flash fired, auto mode, return light not detected",
"exif.flash.auto.no.title": "No flash, auto mode",
"exif.flash.auto.return": "Flash fired, auto mode, return light detected",
"exif.flash.auto.yes": "Flash fired, auto mode",
"exif.flash.disabled": "Flash did not fire",
"exif.flash.enabled": "Flash fired",
"exif.flash.forced.mode": "Flash fired, compulsory mode",
"exif.flash.forced.no.return": "Flash fired, compulsory mode, no return detected",
"exif.flash.forced.return": "Flash fired, compulsory mode, return detected",
"exif.flash.no.return": "Flash fired, no return detected",
"exif.flash.off.mode": "Flash did not fire, compulsory mode",
"exif.flash.return.detected": "Flash fired, return detected",
"exif.flash.fired": "Fired",
"exif.flash.forced.mode": "Flash fired, compulsory flash mode",
"exif.flash.forced.no.return": "Flash fired, compulsory flash mode, return light not detected",
"exif.flash.forced.return": "Flash fired, compulsory flash mode, return light detected",
"exif.flash.no-flash": "No Flash",
"exif.flash.no.return": "Flash fired, return light not detected",
"exif.flash.off-did-not-fire": "Off, Did not fire",
"exif.flash.off.mode": "Flash did not fire, compulsory flash mode",
"exif.flash.return.detected": "Flash fired, return light detected",
"exif.flash.title": "Flash",
"exif.flash.unavailable": "No flash function",
"exif.focal.length.actual": "Focal Length",
@@ -55,43 +73,60 @@
"exif.focal.plane.resolution": "Focal Plane Resolution",
"exif.format": "Format",
"exif.fuji.film.simulation": "Fuji Film Simulation",
"exif.fujirecipe-colorchromeeffect.off": "Off",
"exif.fujirecipe-colorchromeeffect.strong": "Strong",
"exif.fujirecipe-colorchromeeffect.weak": "Weak",
"exif.fujirecipe-colorchromefxblue.off": "Off",
"exif.fujirecipe-colorchromefxblue.strong": "Strong",
"exif.fujirecipe-colorchromefxblue.weak": "Weak",
"exif.fujirecipe-dynamicrange.standard": "Standard",
"exif.fujirecipe-graineffectroughness.off": "Off",
"exif.fujirecipe-graineffectsize.off": "Off",
"exif.fujirecipe-sharpness.hard": "Hard",
"exif.fujirecipe-sharpness.normal": "Normal",
"exif.fujirecipe-sharpness.soft": "Soft",
"exif.fujirecipe-whitebalance.auto": "Auto",
"exif.fujirecipe-whitebalance.kelvin": "Auto",
"exif.gps.altitude": "Altitude",
"exif.gps.latitude": "Latitude",
"exif.gps.location.info": "Location Information",
"exif.gps.location.name": "Photo Location",
"exif.gps.location.name": "Location Name",
"exif.gps.longitude": "Longitude",
"exif.gps.view.map": "View in Amap",
"exif.gps.view.map": "View on Amap",
"exif.grain.effect.intensity": "Grain Effect Intensity",
"exif.grain.effect.size": "Grain Effect Size",
"exif.header.title": "Photo Inspector",
"exif.highlight.tone": "Highlight Tone",
"exif.lens": "Lens",
"exif.light.source.auto": "Auto",
"exif.light.source.cloudy": "Cloudy weather",
"exif.light.source.cool.white.fluorescent": "Cool white fluorescent (W 3900 4500K)",
"exif.light.source.cloudy": "Cloudy Weather",
"exif.light.source.cool.white.fluorescent": "Cool White Fluorescent (W 3900 4500K)",
"exif.light.source.d50": "D50",
"exif.light.source.d55": "D55",
"exif.light.source.d65": "D65",
"exif.light.source.d75": "D75",
"exif.light.source.day.white.fluorescent": "Day white fluorescent (N 4600 5400K)",
"exif.light.source.daylight.fluorescent": "Daylight fluorescent (D 5700 7100K)",
"exif.light.source.daylight.main": "Daylight",
"exif.light.source.fine.weather": "Fine weather",
"exif.light.source.day.white.fluorescent": "Day White Fluorescent (N 4600 5400K)",
"exif.light.source.daylight": "Daylight",
"exif.light.source.daylight-fluorescent": "Daylight Fluorescent (D 5700 7100K)",
"exif.light.source.fine.weather": "Fine Weather",
"exif.light.source.flash": "Flash",
"exif.light.source.fluorescent": "Fluorescent",
"exif.light.source.iso.tungsten": "ISO studio tungsten",
"exif.light.source.other": "Other light source",
"exif.light.source.iso.tungsten": "ISO Studio Tungsten",
"exif.light.source.other": "Other Light Source",
"exif.light.source.shade": "Shade",
"exif.light.source.standard.a": "Standard light A",
"exif.light.source.standard.b": "Standard light B",
"exif.light.source.standard.c": "Standard light C",
"exif.light.source.tungsten": "Tungsten",
"exif.light.source.standard.a": "Standard Light A",
"exif.light.source.standard.b": "Standard Light B",
"exif.light.source.standard.c": "Standard Light C",
"exif.light.source.tungsten": "Tungsten (Incandescent Light)",
"exif.light.source.type": "Light Source",
"exif.light.source.white.fluorescent": "White fluorescent (WW 3200 3700K)",
"exif.light.source.unknown": "Unknown",
"exif.light.source.white.fluorescent": "White Fluorescent (WW 3200 3700K)",
"exif.max.aperture": "Max Aperture",
"exif.metering.mode.average": "Average",
"exif.metering.mode.center": "Center-weighted average",
"exif.metering.mode.multi": "Multi-spot",
"exif.metering.mode.center": "Center-weighted Average",
"exif.metering.mode.center-weighted-average": "Center-weighted Average",
"exif.metering.mode.multi": "Multi-segment",
"exif.metering.mode.multi-segment": "Multi-segment",
"exif.metering.mode.partial": "Partial",
"exif.metering.mode.pattern": "Pattern",
"exif.metering.mode.spot": "Spot",
@@ -102,16 +137,17 @@
"exif.red.adjustment": "Red Adjustment",
"exif.resolution.unit.cm": "Centimeters",
"exif.resolution.unit.inches": "Inches",
"exif.resolution.unit.none": "No unit",
"exif.resolution.unit.none": "No Unit",
"exif.saturation": "Saturation",
"exif.sensing.method.color.sequential.linear": "Color sequential linear sensor",
"exif.sensing.method.color.sequential.main": "Color sequential area sensor",
"exif.sensing.method.one.chip": "One-chip color area sensor",
"exif.sensing.method.three.chip": "Three-chip color area sensor",
"exif.sensing.method.trilinear": "Trilinear sensor",
"exif.sensing.method.two.chip": "Two-chip color area sensor",
"exif.sensing.method.color.sequential.linear": "Color Sequential Linear Sensor",
"exif.sensing.method.color.sequential.main": "Color Sequential Area Sensor",
"exif.sensing.method.one-chip-color-area": "One-chip color area",
"exif.sensing.method.one.chip": "One-chip Color Area Sensor",
"exif.sensing.method.three.chip": "Three-chip Color Area Sensor",
"exif.sensing.method.trilinear": "Trilinear Sensor",
"exif.sensing.method.two.chip": "Two-chip Color Area Sensor",
"exif.sensing.method.type": "Sensing Method",
"exif.sensing.method.undefined": "Not defined",
"exif.sensing.method.undefined": "Undefined",
"exif.shadow.tone": "Shadow Tone",
"exif.sharpness": "Sharpness",
"exif.shutter.speed.value": "Shutter Speed Value",
@@ -119,11 +155,15 @@
"exif.tags": "Tags",
"exif.technical.parameters": "Technical Parameters",
"exif.unknown": "Unknown",
"exif.white.balance.auto": "Auto white balance",
"exif.white.balance.auto": "Auto White Balance",
"exif.white.balance.bias": "White Balance Bias",
"exif.white.balance.blue": "Blue",
"exif.white.balance.daylight": "Daylight",
"exif.white.balance.fine.tune": "White Balance Fine Tune",
"exif.white.balance.grb": "White Balance GRB Level",
"exif.white.balance.manual": "Manual white balance",
"exif.white.balance.kelvin": "Kelvin",
"exif.white.balance.manual": "Manual White Balance",
"exif.white.balance.red": "Red",
"exif.white.balance.shift.ab": "White Balance Shift (Amber-Blue)",
"exif.white.balance.shift.gm": "White Balance Shift (Green-Magenta)",
"exif.white.balance.title": "White Balance",
@@ -135,7 +175,7 @@
"loading.heic.converting": "Converting HEIC/HEIF image format...",
"loading.heic.main": "HEIC",
"loading.webgl.building": "Building high-quality textures...",
"loading.webgl.main": "WebGL texture loading",
"loading.webgl.main": "WebGL Texture Loading",
"photo.conversion.ffmpeg": "FFmpeg",
"photo.conversion.webcodecs": "WebCodecs",
"photo.copy.error": "Failed to copy image, please try again later",
@@ -143,14 +183,14 @@
"photo.copy.success": "Image copied to clipboard",
"photo.copying": "Copying image...",
"photo.download": "Download Image",
"photo.error.loading": "Image loading failed",
"photo.error.loading": "Failed to load image",
"photo.live.badge": "Live",
"photo.live.converting.detail": "Converting video format using {{method}}...",
"photo.live.converting.video": "Converting Live Photo video",
"photo.live.tooltip.desktop.main": "Hover to play Live Photo",
"photo.live.tooltip.desktop.zoom": "Hover Live badge to play / Double click to zoom",
"photo.live.tooltip.desktop.zoom": "Hover to play Live Photo / Double-click to zoom",
"photo.live.tooltip.mobile.main": "Long press to play Live Photo",
"photo.live.tooltip.mobile.zoom": "Long press to play Live Photo / Double tap to zoom",
"photo.live.tooltip.mobile.zoom": "Long press to play Live Photo / Double-tap to zoom",
"photo.share.actions": "Actions",
"photo.share.copy.failed": "Copy failed",
"photo.share.copy.link": "Copy Link",
@@ -161,22 +201,22 @@
"photo.share.text": "Check out this beautiful photo: {{title}}",
"photo.share.title": "Share Photo",
"photo.share.weibo": "Weibo",
"photo.webgl.unavailable": "WebGL unavailable, cannot render image",
"photo.zoom.hint": "Double click or pinch to zoom",
"photo.webgl.unavailable": "WebGL is unavailable, unable to render image",
"photo.zoom.hint": "Double-tap or pinch to zoom",
"slider.auto": "Auto",
"video.codec.keyword": "encoder",
"video.codec.keyword": "Encoder",
"video.conversion.cached.result": "Using cached result",
"video.conversion.codec.fallback": "Could not find a supported MP4 codec for this resolution. Falling back to WebM.",
"video.conversion.codec.fallback": "No MP4 codec found that supports this resolution. Falling back to WebM.",
"video.conversion.complete": "Conversion complete",
"video.conversion.converting": "Converting... {{current}}/{{total}} frames",
"video.conversion.duration.error": "Could not determine video duration or duration is not finite.",
"video.conversion.duration.error": "Unable to determine video duration or duration is not finite.",
"video.conversion.encoder.error": "Aborting conversion due to encoder error.",
"video.conversion.failed": "Video conversion failed",
"video.conversion.initializing": "Initializing video converter...",
"video.conversion.loading": "Loading video file...",
"video.conversion.starting": "Starting conversion...",
"video.conversion.webcodecs.high.quality": "Using high-quality WebCodecs converter...",
"video.conversion.webcodecs.not.supported": "WebCodecs not supported in this browser",
"video.format.mov.not.supported": "Browser does not support MOV format, conversion needed",
"video.conversion.webcodecs.not.supported": "WebCodecs is not supported in this browser",
"video.format.mov.not.supported": "Browser does not support MOV format, conversion required",
"video.format.mov.supported": "Browser natively supports MOV format, skipping conversion"
}

View File

@@ -1,20 +1,18 @@
{
"action.auto": "自動",
"action.columns.setting": "列設定",
"action.sort.mode": "並べ替え方法",
"action.columns.setting": "列設定",
"action.sort.mode": "ソートモード",
"action.tag.filter": "タグフィルター",
"action.view.github": "GitHubリポジトリを表示",
"error.feedback": "まだ問題が解決しない場合、GitHubでフィードバックを提供してください。ありがとうございます",
"error.feedback": "まだ問題が解決しませんか?GitHubでフィードバックをお願いします",
"error.reload": "再読み込み",
"error.submit.issue": "問題を報告",
"error.temporary.description": "アプリケーションで一時的な問題が発生しました。下のボタンをクリックしてアプリケーションを再読み込みするか、他の解決策を試しください。",
"error.title": "申し訳ありません、アプリケーションでエラーが発生しました",
"error.temporary.description": "アプリケーションで一時的な問題が発生しました。下のボタンをクリックしてアプリケーションを再読み込みするか、他の解決策を試しください。",
"error.title": "申し訳ありません、アプリケーションでエラーが発生しました",
"exif.aperture.value": "絞り値",
"exif.auto.white.balance.grb": "自動ホワイトバランス GRB",
"exif.basic.info": "基本情報",
"exif.blue.adjustment": "青色調整",
"exif.blue.color.effect": "ブルーカラーエフェクト",
"exif.brightness.value": "輝度値",
"exif.blue.color.effect": "ブルーエフェクト",
"exif.brightness.value": "輝度",
"exif.camera": "カメラ",
"exif.capture.mode": "撮影モード",
"exif.capture.parameters": "撮影パラメータ",
@@ -22,161 +20,138 @@
"exif.clarity": "明瞭度",
"exif.color.effect": "カラーエフェクト",
"exif.color.space": "色空間",
"exif.custom.rendered.normal": "通常処理",
"exif.custom.rendered.special": "カスタム処理",
"exif.custom.rendered.type": "画像処理",
"exif.colorspace.adobe.rgb": "Adobe RGB",
"exif.colorspace.srgb": "sRGB",
"exif.colorspace.uncalibrated": "未調整",
"exif.custom.rendered.type": "カスタムレンダリング",
"exif.device.info": "デバイス情報",
"exif.digital.zoom": "デジタルズーム",
"exif.dimensions": "サイズ",
"exif.dynamic.range": "ダイナミックレンジ",
"exif.exposure.mode.auto": "自動露出",
"exif.exposure.mode.bracket": "オートブラケット露出",
"exif.exposure.mode.manual": "マニュアル露出",
"exif.exposure.mode.manual": "手動",
"exif.exposure.mode.title": "露出モード",
"exif.exposureprogram.action": "アクションプログラム",
"exif.exposureprogram.aperture-priority": "絞り優先",
"exif.exposureprogram.aperture-priority-ae": "絞り優先",
"exif.exposureprogram.creative": "クリエイティブプログラム",
"exif.exposureprogram.landscape": "風景モード",
"exif.exposureprogram.manual": "手動",
"exif.exposureprogram.normal": "標準",
"exif.exposureprogram.not-defined": "未定義",
"exif.exposureprogram.portrait": "ポートレートモード",
"exif.exposureprogram.program-ae": "プログラムAE",
"exif.exposureprogram.shutter-priority": "シャッター優先",
"exif.exposureprogram.title": "露出プログラム",
"exif.file.size": "ファイルサイズ",
"exif.filename": "ファイル名",
"exif.film.mode": "フィルムモード",
"exif.flash.auto.no.return": "フラッシュ使用、自動モード、リターン検出なし",
"exif.flash.auto.no.title": "フラッシュなし、自動モード",
"exif.flash.auto.return": "フラッシュ使用、自動モード、リターン検出あり",
"exif.flash.auto.yes": "フラッシュ使用、自動モード",
"exif.flash.disabled": "フラッシュなし",
"exif.flash.enabled": "フラッシュ使用",
"exif.flash.forced.mode": "強制フラッシュモード",
"exif.flash.forced.no.return": "強制フラッシュ、リターン検出なし",
"exif.flash.forced.return": "強制フラッシュ、リターン検出あり",
"exif.flash.no.return": "フラッシュ使用、リターン検出なし",
"exif.flash.off.mode": "フラッシュなし、強制モード",
"exif.flash.return.detected": "フラッシュ使用、リターン検出あり",
"exif.flash.fired": "オン",
"exif.flash.no-flash": "オフ",
"exif.flash.off-did-not-fire": "オフ",
"exif.flash.title": "フラッシュ",
"exif.flash.unavailable": "フラッシュ機能なし",
"exif.focal.length.actual": "焦点距離",
"exif.focal.length.equivalent": "35mm換算焦点距離",
"exif.focal.length.equivalent": "35mm換算",
"exif.focal.plane.resolution": "焦点面解像度",
"exif.format": "フォーマット",
"exif.fuji.film.simulation": "富士フイルムシミュレーション",
"exif.gps.altitude": "高度",
"exif.gps.latitude": "緯度",
"exif.gps.location.info": "位置情報",
"exif.gps.location.name": "撮影場所",
"exif.gps.longitude": "経度",
"exif.gps.view.map": "Amapマップで表示",
"exif.fuji.film.simulation": "フィルムシミュレーションレシピ",
"exif.fujirecipe-colorchromeeffect.off": "オフ",
"exif.fujirecipe-colorchromeeffect.strong": "",
"exif.fujirecipe-colorchromeeffect.weak": "",
"exif.fujirecipe-colorchromefxblue.off": "オフ",
"exif.fujirecipe-colorchromefxblue.strong": "",
"exif.fujirecipe-colorchromefxblue.weak": "弱",
"exif.fujirecipe-dynamicrange.standard": "標準",
"exif.fujirecipe-graineffectroughness.off": "オフ",
"exif.fujirecipe-graineffectsize.off": "オフ",
"exif.fujirecipe-sharpness.hard": "硬調",
"exif.fujirecipe-sharpness.normal": "標準",
"exif.fujirecipe-sharpness.soft": "軟調",
"exif.fujirecipe-whitebalance.auto": "自動",
"exif.fujirecipe-whitebalance.kelvin": "自動",
"exif.grain.effect.intensity": "グレインエフェクト強度",
"exif.grain.effect.size": "グレインエフェクトサイズ",
"exif.header.title": "写真情報",
"exif.header.title": "フォトインスペクター",
"exif.highlight.tone": "ハイライトトーン",
"exif.lens": "レンズ",
"exif.light.source.auto": "自動",
"exif.light.source.cloudy": "曇天",
"exif.light.source.cool.white.fluorescent": "冷白色蛍光灯 (W 3900 4500K)",
"exif.light.source.d50": "D50",
"exif.light.source.d55": "D55",
"exif.light.source.d65": "D65",
"exif.light.source.d75": "D75",
"exif.light.source.day.white.fluorescent": "昼白色蛍光灯 (N 4600 5400K)",
"exif.light.source.daylight.fluorescent": "昼光蛍光灯 (D 5700 7100K)",
"exif.light.source.daylight.main": "昼光",
"exif.light.source.fine.weather": "晴天",
"exif.light.source.flash": "フラッシュ",
"exif.light.source.fluorescent": "蛍光灯",
"exif.light.source.iso.tungsten": "ISOタングステン",
"exif.light.source.other": "その他の光源",
"exif.light.source.shade": "日陰",
"exif.light.source.standard.a": "標準光源 A",
"exif.light.source.standard.b": "標準光源 B",
"exif.light.source.standard.c": "標準光源 C",
"exif.light.source.tungsten": "タングステン",
"exif.light.source.type": "光源タイプ",
"exif.light.source.white.fluorescent": "白色蛍光灯 (WW 3200 3700K)",
"exif.light.source.type": "光源",
"exif.light.source.unknown": "不明",
"exif.max.aperture": "最大絞り",
"exif.metering.mode.average": "平均測光",
"exif.metering.mode.center": "中央重点測光",
"exif.metering.mode.multi": "マルチ測光",
"exif.metering.mode.partial": "部分測光",
"exif.metering.mode.pattern": "パターン測光",
"exif.metering.mode.center-weighted-average": "中央部重点平均測光",
"exif.metering.mode.multi-segment": "多分割測光",
"exif.metering.mode.spot": "スポット測光",
"exif.metering.mode.type": "測光モード",
"exif.metering.mode.unknown": "不明",
"exif.noise.reduction": "ノイズリダクション",
"exif.pixels": "ピクセル",
"exif.red.adjustment": "赤色調整",
"exif.resolution.unit.cm": "センチメートル",
"exif.resolution.unit.inches": "インチ",
"exif.resolution.unit.none": "単位なし",
"exif.saturation": "彩度",
"exif.sensing.method.color.sequential.linear": "カラーシーケンシャルリニアセンサー",
"exif.sensing.method.color.sequential.main": "カラーシーケンシャルエリアセンサー",
"exif.sensing.method.one.chip": "単一チップカラーエリアセンサー",
"exif.sensing.method.three.chip": "3チップカラーエリアセンサー",
"exif.sensing.method.trilinear": "3ラインセンサー",
"exif.sensing.method.two.chip": "2チップカラーエリアセンサー",
"exif.sensing.method.type": "センシング方式",
"exif.sensing.method.undefined": "未定義",
"exif.sensing.method.one-chip-color-area": "1チップカラーエリアセンサー",
"exif.sensing.method.type": "撮像方式",
"exif.shadow.tone": "シャドウトーン",
"exif.sharpness": "シャープネス",
"exif.shutter.speed.value": "シャッタースピード",
"exif.standard.white.balance.grb": "標準ホワイトバランス GRB",
"exif.shutter.speed.value": "シャッタースピード",
"exif.tags": "タグ",
"exif.technical.parameters": "技術パラメータ",
"exif.unknown": "不明",
"exif.white.balance.auto": "自動ホワイトバランス",
"exif.white.balance.auto": "自動",
"exif.white.balance.bias": "ホワイトバランス補正",
"exif.white.balance.daylight": "昼光",
"exif.white.balance.fine.tune": "ホワイトバランス微調整",
"exif.white.balance.grb": "ホワイトバランス GRB レベル",
"exif.white.balance.manual": "マニュアルホワイトバランス",
"exif.white.balance.kelvin": "ケルビン",
"exif.white.balance.manual": "手動",
"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}} 枚の写真",
"gallery.built.at": "ビルド日時 ",
"gallery.photos_one": "写真{{count}}",
"gallery.photos_other": "写真{{count}}",
"loading.converting": "変換中...",
"loading.default": "読み込み中",
"loading.heic.converting": "HEIC/HEIF画像フォーマット変換中...",
"loading.heic.converting": "HEIC/HEIF画像フォーマット変換中...",
"loading.heic.main": "HEIC",
"loading.webgl.building": "高品質テクスチャを構築中...",
"loading.webgl.main": "WebGLテクスチャ読み込み",
"loading.webgl.main": "WebGLテクスチャ読み込み",
"photo.conversion.ffmpeg": "FFmpeg",
"photo.conversion.webcodecs": "WebCodecs",
"photo.copy.error": "画像のコピーに失敗しました。後でもう一度お試しください",
"photo.copy.error": "画像のコピーに失敗しました。後でもう一度お試しください",
"photo.copy.image": "画像をコピー",
"photo.copy.success": "画像がクリップボードにコピーされました",
"photo.copying": "画像をコピー...",
"photo.copy.success": "画像がクリップボードにコピーされました",
"photo.copying": "画像をコピーしています...",
"photo.download": "画像をダウンロード",
"photo.error.loading": "画像の読み込みに失敗しました",
"photo.live.badge": "Live",
"photo.live.converting.detail": "{{method}}を使用して動画形式を変換...",
"photo.live.converting.video": "Live Photo動画変換中",
"photo.live.tooltip.desktop.main": "ホバーしてLive Photoを再生",
"photo.live.tooltip.desktop.zoom": "ホバーでライブ写真再生 / ダブルクリックズーム",
"photo.live.tooltip.mobile.main": "長押しライブ写真再生",
"photo.live.tooltip.mobile.zoom": "長押しライブ写真再生 / ダブルタップズーム",
"photo.share.actions": "操作",
"photo.live.badge": "ライブ",
"photo.live.converting.detail": "{{method}}を使用してビデオ形式を変換しています...",
"photo.live.converting.video": "ライブフォトビデオを変換しています",
"photo.live.tooltip.desktop.main": "ホバーしてライブフォトを再生",
"photo.live.tooltip.desktop.zoom": "「ライブ」バッジをホバーして再生/ダブルクリックしてズーム",
"photo.live.tooltip.mobile.main": "長押ししてライブフォトを再生",
"photo.live.tooltip.mobile.zoom": "長押ししてライブフォトを再生/ダブルタップしてズーム",
"photo.share.actions": "アクション",
"photo.share.copy.failed": "コピーに失敗しました",
"photo.share.copy.link": "リンクをコピー",
"photo.share.default.title": "写真共有",
"photo.share.default.title": "写真共有",
"photo.share.link.copied": "リンクがクリップボードにコピーされました",
"photo.share.social.media": "ソーシャルメディア",
"photo.share.system": "システム共有",
"photo.share.text": "この素晴らしい写真を見てください:{{title}}",
"photo.share.text": "この素敵な写真を見てください:{{title}}",
"photo.share.title": "写真を共有",
"photo.share.weibo": "Weibo",
"photo.webgl.unavailable": "WebGLが利用できないため、画像をレンダリングできません",
"photo.zoom.hint": "ダブルタップまたはピンチズーム",
"photo.zoom.hint": "ダブルタップまたはピンチしてズーム",
"slider.auto": "自動",
"video.codec.keyword": "エンコーダー",
"video.conversion.cached.result": "キャッシュされた結果を使用",
"video.conversion.codec.fallback": "この解像度でサポートされているMP4コーデックが見つかりません。WebMにフォールバックします。",
"video.conversion.complete": "変換完了",
"video.conversion.converting": "変換中... {{current}}/{{total}} フレーム",
"video.conversion.duration.error": "ビデオの長さを特定できないか、長さが有限ではありません。",
"video.conversion.encoder.error": "エンコーダーエラーのため変換を中止します。",
"video.conversion.converting": "変換中... {{current}}/{{total}}フレーム",
"video.conversion.duration.error": "ビデオの長さを特定できないか、長さが有限ではありません。",
"video.conversion.encoder.error": "エンコーダーエラーのため変換を中止します。",
"video.conversion.failed": "ビデオ変換に失敗しました",
"video.conversion.initializing": "ビデオコンバーターを初期化...",
"video.conversion.loading": "ビデオファイルを読み込み中...",
"video.conversion.starting": "変換を開始...",
"video.conversion.webcodecs.high.quality": "高品質WebCodecsコンバーターを使用...",
"video.conversion.initializing": "ビデオコンバーターを初期化しています...",
"video.conversion.loading": "ビデオファイルを読み込んでいます...",
"video.conversion.starting": "変換を開始しています...",
"video.conversion.webcodecs.high.quality": "高品質WebCodecsコンバーターを使用しています...",
"video.conversion.webcodecs.not.supported": "このブラウザはWebCodecsをサポートしていません",
"video.format.mov.not.supported": "ブラウザがMOV形式をサポートしていないため、変換が必要です",
"video.format.mov.supported": "ブラウザがMOV形式をネイティブサポートしているため、変換をスキップします"
"video.format.mov.supported": "ブラウザがMOV形式をネイティブサポートしているため、変換をスキップします"
}

View File

@@ -1,182 +1,157 @@
{
"action.auto": "자동",
"action.columns.setting": "열 설정",
"action.sort.mode": "정렬 방식",
"action.columns.setting": "열 설정",
"action.sort.mode": "정렬 모드",
"action.tag.filter": "태그 필터",
"action.view.github": "GitHub 리포지토리 보기",
"error.feedback": "여전히 문제가 발생하나요? GitHub에 피드백을 제공해 주세요. 감사합니다!",
"error.reload": "다시 로드",
"error.feedback": "문제가 계속 발생하나요? GitHub에 피드백을 남겨주세요. 감사합니다!",
"error.reload": "새로고침",
"error.submit.issue": "문제 제출",
"error.temporary.description": "애플리케이션에 일시적인 문제가 발생했습니다. 아래 버튼을 클릭하여 애플리케이션을 다시 로드하거나 다른 해결 방법을 시도해 보세요.",
"error.temporary.description": "애플리케이션에 일시적인 문제가 발생했습니다. 아래 버튼을 클릭하여 애플리케이션을 새로고침하거나 다른 해결 방법을 시도해 보세요.",
"error.title": "죄송합니다, 애플리케이션에 오류가 발생했습니다",
"exif.aperture.value": "조리개 값",
"exif.auto.white.balance.grb": "자동 화이트 밸런스 GRB",
"exif.basic.info": "기본 정보",
"exif.blue.adjustment": "파란색 조정",
"exif.blue.color.effect": "파란색 색상 효과",
"exif.brightness.value": "밝기 값",
"exif.blue.color.effect": "블루 효과",
"exif.brightness.value": "밝기",
"exif.camera": "카메라",
"exif.capture.mode": "촬영 모드",
"exif.capture.parameters": "촬영 매개변수",
"exif.capture.time": "촬영 시간",
"exif.clarity": "선명도",
"exif.color.effect": "색상 효과",
"exif.color.space": "색 공간",
"exif.custom.rendered.normal": "정상 처리",
"exif.custom.rendered.special": "사용자 지정 처리",
"exif.custom.rendered.type": "이미지 처리",
"exif.color.space": "색 공간",
"exif.colorspace.adobe.rgb": "Adobe RGB",
"exif.colorspace.srgb": "sRGB",
"exif.colorspace.uncalibrated": "보정되지 않음",
"exif.custom.rendered.type": "사용자 정의 렌더링",
"exif.device.info": "장치 정보",
"exif.digital.zoom": "디지털 줌",
"exif.dimensions": "크기",
"exif.dynamic.range": "다이믹 레인지",
"exif.exposure.mode.auto": "자동 노출",
"exif.exposure.mode.bracket": "자동 브래킷 노출",
"exif.exposure.mode.manual": "수동 노출",
"exif.dynamic.range": "다이믹 레인지",
"exif.exposure.mode.manual": "수동",
"exif.exposure.mode.title": "노출 모드",
"exif.exposureprogram.action": "액션 프로그램",
"exif.exposureprogram.aperture-priority": "조리개 우선",
"exif.exposureprogram.aperture-priority-ae": "조리개 우선",
"exif.exposureprogram.creative": "크리에이티브 프로그램",
"exif.exposureprogram.landscape": "풍경 모드",
"exif.exposureprogram.manual": "수동",
"exif.exposureprogram.normal": "표준",
"exif.exposureprogram.not-defined": "정의되지 않음",
"exif.exposureprogram.portrait": "인물 모드",
"exif.exposureprogram.program-ae": "프로그램 AE",
"exif.exposureprogram.shutter-priority": "셔터 우선",
"exif.exposureprogram.title": "노출 프로그램",
"exif.file.size": "파일 크기",
"exif.filename": "파일 이름",
"exif.film.mode": "필름 모드",
"exif.flash.auto.no.return": "플래시 사용, 자동 모드, 반사 감지 안 됨",
"exif.flash.auto.no.title": "플래시 사용 안 함, 자동 모드",
"exif.flash.auto.return": "플래시 사용, 자동 모드, 반사 감지됨",
"exif.flash.auto.yes": "플래시 사용, 자동 모드",
"exif.flash.disabled": "플래시 사용 안 함",
"exif.flash.enabled": "플래시 사용",
"exif.flash.forced.mode": "강제 플래시 모드",
"exif.flash.forced.no.return": "강제 플래시, 반사 감지 안 됨",
"exif.flash.forced.return": "강제 플래시, 반사 감지됨",
"exif.flash.no.return": "플래시 사용, 반사 감지 안 됨",
"exif.flash.off.mode": "플래시 사용 안 함, 강제 모드",
"exif.flash.return.detected": "플래시 사용, 반사 감지됨",
"exif.flash.fired": "켜짐",
"exif.flash.no-flash": "꺼짐",
"exif.flash.off-did-not-fire": "꺼짐",
"exif.flash.title": "플래시",
"exif.flash.unavailable": "플래시 기능 없음",
"exif.focal.length.actual": "초점 거리",
"exif.focal.length.equivalent": "35mm 환산 초점거리",
"exif.focal.length.equivalent": "35mm 환산",
"exif.focal.plane.resolution": "초점면 해상도",
"exif.format": "형식",
"exif.fuji.film.simulation": "후지 필름 시뮬레이션",
"exif.gps.altitude": "고도",
"exif.gps.latitude": "위도",
"exif.gps.location.info": "위치 정보",
"exif.gps.location.name": "Photo Location",
"exif.gps.longitude": "경도",
"exif.gps.view.map": "Amap 지도에서 보기",
"exif.format": "포맷",
"exif.fuji.film.simulation": "필름 시뮬레이션 레시피",
"exif.fujirecipe-colorchromeeffect.off": "꺼짐",
"exif.fujirecipe-colorchromeeffect.strong": "강하게",
"exif.fujirecipe-colorchromeeffect.weak": "약하게",
"exif.fujirecipe-colorchromefxblue.off": "꺼짐",
"exif.fujirecipe-colorchromefxblue.strong": "강하게",
"exif.fujirecipe-colorchromefxblue.weak": "약하게",
"exif.fujirecipe-dynamicrange.standard": "표준",
"exif.fujirecipe-graineffectroughness.off": "꺼짐",
"exif.fujirecipe-graineffectsize.off": "꺼짐",
"exif.fujirecipe-sharpness.hard": "하드",
"exif.fujirecipe-sharpness.normal": "표준",
"exif.fujirecipe-sharpness.soft": "소프트",
"exif.fujirecipe-whitebalance.auto": "자동",
"exif.fujirecipe-whitebalance.kelvin": "자동",
"exif.grain.effect.intensity": "그레인 효과 강도",
"exif.grain.effect.size": "그레인 효과 크기",
"exif.header.title": "사진 정보",
"exif.header.title": "사진 검사기",
"exif.highlight.tone": "하이라이트 톤",
"exif.lens": "렌즈",
"exif.light.source.auto": "자동",
"exif.light.source.cloudy": "흐린 날씨",
"exif.light.source.cool.white.fluorescent": "냉백색 형광등 (W 3900 4500K)",
"exif.light.source.d50": "D50",
"exif.light.source.d55": "D55",
"exif.light.source.d65": "D65",
"exif.light.source.d75": "D75",
"exif.light.source.day.white.fluorescent": "주백색 형광등 (N 4600 5400K)",
"exif.light.source.daylight.fluorescent": "일광 형광등 (D 5700 7100K)",
"exif.light.source.daylight.main": "일광",
"exif.light.source.fine.weather": "맑은 날씨",
"exif.light.source.flash": "플래시",
"exif.light.source.fluorescent": "형광등",
"exif.light.source.iso.tungsten": "ISO 텅스텐 전구",
"exif.light.source.other": "기타 광원",
"exif.light.source.shade": "그늘",
"exif.light.source.standard.a": "표준 광원 A",
"exif.light.source.standard.b": "표준 광원 B",
"exif.light.source.standard.c": "표준 광원 C",
"exif.light.source.tungsten": "텅스텐 전구",
"exif.light.source.type": "광원 유형",
"exif.light.source.white.fluorescent": "백색 형광등 (WW 3200 3700K)",
"exif.light.source.type": "광원",
"exif.light.source.unknown": "알 수 없음",
"exif.max.aperture": "최대 조리개",
"exif.metering.mode.average": "평균 측광",
"exif.metering.mode.center": "중앙 중점 측광",
"exif.metering.mode.multi": "다중 측광",
"exif.metering.mode.partial": "부분 측광",
"exif.metering.mode.pattern": "패턴 측광",
"exif.metering.mode.center-weighted-average": "중앙 중점 평균 측광",
"exif.metering.mode.multi-segment": "다분할 측광",
"exif.metering.mode.spot": "스팟 측광",
"exif.metering.mode.type": "측광 모드",
"exif.metering.mode.unknown": "알 수 없음",
"exif.noise.reduction": "노이즈 감소",
"exif.pixels": "픽셀",
"exif.red.adjustment": "빨간색 조정",
"exif.resolution.unit.cm": "센티미터",
"exif.resolution.unit.inches": "인치",
"exif.resolution.unit.none": "단위 없음",
"exif.saturation": "채도",
"exif.sensing.method.color.sequential.linear": "컬러 순차 선형 센서",
"exif.sensing.method.color.sequential.main": "컬러 순차 영역 센서",
"exif.sensing.method.one.chip": "단일 칩 컬러 영역 센서",
"exif.sensing.method.three.chip": "삼중 칩 컬러 영역 센서",
"exif.sensing.method.trilinear": "삼선형 센서",
"exif.sensing.method.two.chip": "이중 칩 컬러 영역 센서",
"exif.sensing.method.type": "센싱 방식",
"exif.sensing.method.undefined": "정의되지 않음",
"exif.shadow.tone": "그림자 톤",
"exif.sensing.method.one-chip-color-area": "1칩 컬러 영역 센서",
"exif.sensing.method.type": "감지 방식",
"exif.shadow.tone": "섀도우 톤",
"exif.sharpness": "선명도",
"exif.shutter.speed.value": "셔터 속도",
"exif.standard.white.balance.grb": "표준 화이트 밸런스 GRB",
"exif.shutter.speed.value": "셔터 속도",
"exif.tags": "태그",
"exif.technical.parameters": "기술 매개변수",
"exif.unknown": "알 수 없음",
"exif.white.balance.auto": "자동 화이트 밸런스",
"exif.white.balance.bias": "화이트 밸런스 바이어스",
"exif.white.balance.auto": "자동",
"exif.white.balance.bias": "화이트 밸런스 보정",
"exif.white.balance.daylight": "일광",
"exif.white.balance.fine.tune": "화이트 밸런스 미세 조정",
"exif.white.balance.grb": "화이트 밸런스 GRB 레벨",
"exif.white.balance.manual": "수동 화이트 밸런스",
"exif.white.balance.shift.ab": "화이트 밸런스 이동 (호박색-파랑)",
"exif.white.balance.shift.gm": "화이트 밸런스 이동 (녹색-마젠타)",
"exif.white.balance.kelvin": "켈빈",
"exif.white.balance.manual": "수동",
"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}}장 사진",
"gallery.built.at": "빌드 날짜 ",
"gallery.photos_one": "사진 {{count}}장",
"gallery.photos_other": "사진 {{count}}장",
"loading.converting": "변환 중...",
"loading.default": "로 중",
"loading.default": "로 중",
"loading.heic.converting": "HEIC/HEIF 이미지 형식 변환 중...",
"loading.heic.main": "HEIC",
"loading.webgl.building": "고품질 텍스처 구축 중...",
"loading.webgl.main": "WebGL 텍스처 로딩",
"photo.conversion.ffmpeg": "FFmpeg",
"photo.conversion.webcodecs": "WebCodecs",
"photo.copy.error": "이미지 복사 실패, 나중에 다시 시도해 주세요",
"photo.copy.error": "이미지 복사 실패했습니다. 나중에 다시 시도해 주세요.",
"photo.copy.image": "이미지 복사",
"photo.copy.success": "이미지 클립보드에 복사되었습니다",
"photo.copy.success": "이미지 클립보드에 복사습니다.",
"photo.copying": "이미지 복사 중...",
"photo.download": "이미지 다운로드",
"photo.error.loading": "이미지 로 실패",
"photo.live.badge": "Live",
"photo.live.converting.detail": "{{method}} 사용하여 비디오 형식 변환 중...",
"photo.live.converting.video": "Live Photo 비디오 변환 중",
"photo.live.tooltip.desktop.main": "마우스를 올려 Live Photo 재생",
"photo.live.tooltip.desktop.zoom": "마우스를 올려 라이브 표시 재생 / 두 번 클릭하여 확대",
"photo.error.loading": "이미지 로 실패",
"photo.live.badge": "라이브",
"photo.live.converting.detail": "{{method}}을(를) 사용하여 비디오 형식 변환 중...",
"photo.live.converting.video": "라이브 포토 비디오 변환 중",
"photo.live.tooltip.desktop.main": "마우스를 올려 라이브 포토 재생",
"photo.live.tooltip.desktop.zoom": "‘라이브’ 배지에 마우스를 올려 재생/더블 클릭하여 확대",
"photo.live.tooltip.mobile.main": "길게 눌러 라이브 포토 재생",
"photo.live.tooltip.mobile.zoom": "길게 눌러 라이브 포토 재생 / 두 번 탭하여 확대",
"photo.live.tooltip.mobile.zoom": "길게 눌러 라이브 포토 재생/더블 탭하여 확대",
"photo.share.actions": "작업",
"photo.share.copy.failed": "복사 실패",
"photo.share.copy.link": "링크 복사",
"photo.share.default.title": "사진 공유",
"photo.share.link.copied": "링크 클립보드에 복사되었습니다",
"photo.share.link.copied": "링크 클립보드에 복사습니다",
"photo.share.social.media": "소셜 미디어",
"photo.share.system": "시스템 공유",
"photo.share.text": "이 멋진 사진을 확인세요: {{title}}",
"photo.share.text": "이 멋진 사진을 확인해 보세요: {{title}}",
"photo.share.title": "사진 공유",
"photo.share.weibo": "Weibo",
"photo.webgl.unavailable": "WebGL을 사용할 수 없어 이미지를 렌더링할 수 없습니다",
"photo.zoom.hint": "두 번 탭 또는 손가락으로 확대",
"photo.zoom.hint": "더블 탭 또는 손가락으로 확대/축소",
"slider.auto": "자동",
"video.codec.keyword": "코덱",
"video.codec.keyword": "인코더",
"video.conversion.cached.result": "캐시된 결과 사용",
"video.conversion.codec.fallback": "이 해상도에서 지원되는 MP4 코덱을 찾을 수 없습니다. WebM으로 대체합니다.",
"video.conversion.complete": "변환 완료",
"video.conversion.converting": "변환 중... {{current}}/{{total}} 프레임",
"video.conversion.duration.error": "비디오 길이를 확인할 수 없거나 길이가 유한값이 아닙니다.",
"video.conversion.encoder.error": "인코더 오류로 인해 변환 중단니다.",
"video.conversion.duration.error": "비디오 길이를 확인할 수 없거나 유한값이 아닙니다.",
"video.conversion.encoder.error": "인코더 오류로 인해 변환 중단되었습니다.",
"video.conversion.failed": "비디오 변환 실패",
"video.conversion.initializing": "비디오 변환기 초기화 중...",
"video.conversion.loading": "비디오 파일 로딩 중...",
"video.conversion.starting": "변환 시작 중...",
"video.conversion.webcodecs.high.quality": "고품질 WebCodecs 변환기 사용 중...",
"video.conversion.webcodecs.not.supported": "이 브라우저는 WebCodecs를 지원하지 않습니다",
"video.format.mov.not.supported": "브라우저가 MOV 형식을 지원하지 않으므로 변환이 필요합니다",
"video.format.mov.supported": "브라우저가 MOV 형식을 기본 지원하므로 변환을 건너뜁니다"
"video.format.mov.not.supported": "브라우저가 MOV 형식을 지원하지 않 변환이 필요합니다.",
"video.format.mov.supported": "브라우저가 MOV 형식을 기본적으로 지원하므로 변환을 건너뜁니다."
}

View File

@@ -1,20 +1,18 @@
{
"action.auto": "自动",
"action.columns.setting": "列设置",
"action.sort.mode": "排序式",
"action.columns.setting": "列设置",
"action.sort.mode": "排序式",
"action.tag.filter": "标签筛选",
"action.view.github": "查看 GitHub 仓库",
"error.feedback": "仍然遇到这个问题?请在 Github 中提供反馈,谢谢!",
"error.feedback": "仍然存在此问题?请在 Github 中提供反馈,谢谢!",
"error.reload": "重新加载",
"error.submit.issue": "提交问题",
"error.temporary.description": "应用程序遇到了临时问题,击下面的按钮尝试重新加载应用程序或其他解决方案?",
"error.title": "抱歉,应用程序遇到错误",
"error.temporary.description": "应用程序出现临时问题,击下面的按钮尝试重新加载应用程序或采用其他解决方案?",
"error.title": "抱歉,应用程序遇到错误",
"exif.aperture.value": "光圈值",
"exif.auto.white.balance.grb": "自动白平衡 GRB",
"exif.basic.info": "基本信息",
"exif.blue.adjustment": "蓝色调整",
"exif.blue.color.effect": "蓝色色彩效果",
"exif.brightness.value": "亮度值",
"exif.blue.color.effect": "蓝色效果",
"exif.brightness.value": "亮度",
"exif.camera": "相机",
"exif.capture.mode": "拍摄模式",
"exif.capture.parameters": "拍摄参数",
@@ -22,135 +20,114 @@
"exif.clarity": "清晰度",
"exif.color.effect": "色彩效果",
"exif.color.space": "色彩空间",
"exif.custom.rendered.normal": "正常处理",
"exif.custom.rendered.special": "自定义处理",
"exif.custom.rendered.type": "图像处理",
"exif.colorspace.adobe.rgb": "Adobe RGB",
"exif.colorspace.srgb": "sRGB",
"exif.colorspace.uncalibrated": "未校准",
"exif.custom.rendered.type": "自定义渲染",
"exif.device.info": "设备信息",
"exif.digital.zoom": "数字变焦",
"exif.dimensions": "尺寸",
"exif.dynamic.range": "动态范围",
"exif.exposure.mode.auto": "自动曝光",
"exif.exposure.mode.bracket": "自动包围曝光",
"exif.exposure.mode.manual": "手动曝光",
"exif.exposure.mode.manual": "手动",
"exif.exposure.mode.title": "曝光模式",
"exif.exposureprogram.action": "运动程序",
"exif.exposureprogram.aperture-priority": "光圈优先",
"exif.exposureprogram.aperture-priority-ae": "光圈优先",
"exif.exposureprogram.creative": "创意程序",
"exif.exposureprogram.landscape": "风景模式",
"exif.exposureprogram.manual": "手动",
"exif.exposureprogram.normal": "正常",
"exif.exposureprogram.not-defined": "未定义",
"exif.exposureprogram.portrait": "人像模式",
"exif.exposureprogram.program-ae": "程序自动曝光",
"exif.exposureprogram.shutter-priority": "快门优先",
"exif.exposureprogram.title": "曝光程序",
"exif.file.size": "文件大小",
"exif.filename": "文件名",
"exif.film.mode": "胶片模式",
"exif.flash.auto.no.return": "闪光,自动模式,未检测到回闪",
"exif.flash.auto.no.title": "未闪光,自动模式",
"exif.flash.auto.return": "闪光,自动模式,检测到回闪",
"exif.flash.auto.yes": "闪光,自动模式",
"exif.flash.disabled": "未闪光",
"exif.flash.enabled": "闪光",
"exif.flash.forced.mode": "强制闪光模式",
"exif.flash.forced.no.return": "强制闪光,未检测到回闪",
"exif.flash.forced.return": "强制闪光,检测到回闪",
"exif.flash.no.return": "闪光,未检测到回闪",
"exif.flash.off.mode": "未闪光,强制模式",
"exif.flash.return.detected": "闪光,检测到回闪",
"exif.flash.fired": "开",
"exif.flash.no-flash": "关",
"exif.flash.off-did-not-fire": "关",
"exif.flash.title": "闪光灯",
"exif.flash.unavailable": "未提供闪光功能",
"exif.focal.length.actual": "焦距",
"exif.focal.length.equivalent": "35mm 等效焦距",
"exif.focal.length.equivalent": "35mm 等效",
"exif.focal.plane.resolution": "焦平面分辨率",
"exif.format": "格式",
"exif.fuji.film.simulation": "富士胶片模拟",
"exif.gps.altitude": "海拔",
"exif.gps.latitude": "纬度",
"exif.gps.location.info": "位置信息",
"exif.gps.location.name": "拍摄位置",
"exif.gps.longitude": "经度",
"exif.gps.view.map": "在高德地图中查看",
"exif.fuji.film.simulation": "胶片模拟配方",
"exif.fujirecipe-colorchromeeffect.off": "",
"exif.fujirecipe-colorchromeeffect.strong": "",
"exif.fujirecipe-colorchromeeffect.weak": "",
"exif.fujirecipe-colorchromefxblue.off": "",
"exif.fujirecipe-colorchromefxblue.strong": "",
"exif.fujirecipe-colorchromefxblue.weak": "弱",
"exif.fujirecipe-dynamicrange.standard": "标准",
"exif.fujirecipe-graineffectroughness.off": "关",
"exif.fujirecipe-graineffectsize.off": "关",
"exif.fujirecipe-sharpness.hard": "锐利",
"exif.fujirecipe-sharpness.normal": "标准",
"exif.fujirecipe-sharpness.soft": "柔和",
"exif.fujirecipe-whitebalance.auto": "自动",
"exif.fujirecipe-whitebalance.kelvin": "自动",
"exif.grain.effect.intensity": "颗粒效果强度",
"exif.grain.effect.size": "颗粒效果大小",
"exif.header.title": "照片信息",
"exif.highlight.tone": "高光色调",
"exif.lens": "镜头",
"exif.light.source.auto": "自动",
"exif.light.source.cloudy": "阴天",
"exif.light.source.cool.white.fluorescent": "冷白荧光灯 (W 3900 4500K)",
"exif.light.source.d50": "D50",
"exif.light.source.d55": "D55",
"exif.light.source.d65": "D65",
"exif.light.source.d75": "D75",
"exif.light.source.day.white.fluorescent": "日白荧光灯 (N 4600 5400K)",
"exif.light.source.daylight.fluorescent": "日光荧光灯 (D 5700 7100K)",
"exif.light.source.daylight.main": "日光",
"exif.light.source.fine.weather": "晴天",
"exif.light.source.flash": "闪光灯",
"exif.light.source.fluorescent": "荧光灯",
"exif.light.source.iso.tungsten": "ISO 钨丝灯",
"exif.light.source.other": "其他光源",
"exif.light.source.shade": "阴影",
"exif.light.source.standard.a": "标准光源 A",
"exif.light.source.standard.b": "标准光源 B",
"exif.light.source.standard.c": "标准光源 C",
"exif.light.source.tungsten": "钨丝灯",
"exif.light.source.type": "光源类型",
"exif.light.source.white.fluorescent": "白荧光灯 (WW 3200 3700K)",
"exif.light.source.type": "光源",
"exif.light.source.unknown": "未知",
"exif.max.aperture": "最大光圈",
"exif.metering.mode.average": "平均测光",
"exif.metering.mode.center": "中央重点测光",
"exif.metering.mode.multi": "多点测光",
"exif.metering.mode.partial": "局部测光",
"exif.metering.mode.pattern": "评价测光",
"exif.metering.mode.center-weighted-average": "中心重点平均测光",
"exif.metering.mode.multi-segment": "多重测光",
"exif.metering.mode.spot": "点测光",
"exif.metering.mode.type": "测光模式",
"exif.metering.mode.unknown": "未知",
"exif.noise.reduction": "降噪",
"exif.pixels": "像素",
"exif.red.adjustment": "红色调整",
"exif.resolution.unit.cm": "厘米",
"exif.resolution.unit.inches": "英寸",
"exif.resolution.unit.none": "无单位",
"exif.saturation": "饱和度",
"exif.sensing.method.color.sequential.linear": "彩色顺序线性传感器",
"exif.sensing.method.color.sequential.main": "彩色顺序区域传感器",
"exif.sensing.method.one.chip": "单芯片彩色区域传感器",
"exif.sensing.method.three.chip": "三芯片彩色区域传感器",
"exif.sensing.method.trilinear": "三线传感器",
"exif.sensing.method.two.chip": "双芯片彩色区域传感器",
"exif.sensing.method.one-chip-color-area": "单片彩色区域传感器",
"exif.sensing.method.type": "感光方式",
"exif.sensing.method.undefined": "未定义",
"exif.shadow.tone": "阴影色调",
"exif.sharpness": "锐度",
"exif.shutter.speed.value": "快门速度",
"exif.standard.white.balance.grb": "标准白平衡 GRB",
"exif.shutter.speed.value": "快门速度",
"exif.tags": "标签",
"exif.technical.parameters": "技术参数",
"exif.unknown": "未知",
"exif.white.balance.auto": "自动白平衡",
"exif.white.balance.auto": "自动",
"exif.white.balance.bias": "白平衡偏移",
"exif.white.balance.blue": "蓝色",
"exif.white.balance.daylight": "日光",
"exif.white.balance.fine.tune": "白平衡微调",
"exif.white.balance.grb": "白平衡 GRB 级别",
"exif.white.balance.manual": "手动白平衡",
"exif.white.balance.shift.ab": "白平衡偏移 (琥珀-蓝)",
"exif.white.balance.shift.gm": "白平衡偏移 (绿-洋红)",
"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.title": "白平衡",
"gallery.built.at": "构建于 ",
"gallery.photos_one": "{{count}} 张照片",
"gallery.photos_other": "{{count}} 张照片",
"loading.converting": "转换中...",
"loading.default": "加载中",
"loading.heic.converting": "HEIC/HEIF 图格式转换中...",
"loading.heic.converting": "正在转换 HEIC/HEIF 图格式...",
"loading.heic.main": "HEIC",
"loading.webgl.building": "正在构建高质量纹理...",
"loading.webgl.main": "WebGL 纹理加载",
"photo.conversion.ffmpeg": "FFmpeg",
"photo.conversion.webcodecs": "WebCodecs",
"photo.copy.error": "复制图失败,请稍后重试",
"photo.copy.image": "复制图",
"photo.copy.success": "图已复制到剪贴板",
"photo.copying": "正在复制图...",
"photo.download": "下载图",
"photo.error.loading": "图加载失败",
"photo.copy.error": "复制图失败,请稍后重试",
"photo.copy.image": "复制图",
"photo.copy.success": "图已复制到剪贴板",
"photo.copying": "正在复制图...",
"photo.download": "下载图",
"photo.error.loading": "图加载失败",
"photo.live.badge": "实况",
"photo.live.converting.detail": "正在使用 {{method}} 转换视频格式...",
"photo.live.converting.video": "实况视频转换中",
"photo.live.tooltip.desktop.main": "悬播放实况照片",
"photo.live.tooltip.desktop.zoom": "悬浮实况标识播放 / 双击缩放",
"photo.live.tooltip.mobile.main": "长按播放实况照片",
"photo.live.tooltip.mobile.zoom": "长按播放实况照片 / 双击缩放",
"photo.live.converting.video": "正在转换实况照片视频",
"photo.live.tooltip.desktop.main": "悬停以播放实况照片",
"photo.live.tooltip.desktop.zoom": "悬停“实况”徽章以播放/双击缩放",
"photo.live.tooltip.mobile.main": "长按播放实况照片",
"photo.live.tooltip.mobile.zoom": "长按播放实况照片/双击缩放",
"photo.share.actions": "操作",
"photo.share.copy.failed": "复制失败",
"photo.share.copy.link": "复制链接",
@@ -158,18 +135,18 @@
"photo.share.link.copied": "链接已复制到剪贴板",
"photo.share.social.media": "社交媒体",
"photo.share.system": "系统分享",
"photo.share.text": "看这张精美的照片:{{title}}",
"photo.share.text": "看这张漂亮的照片:{{title}}",
"photo.share.title": "分享照片",
"photo.share.weibo": "微博",
"photo.webgl.unavailable": "WebGL 不可用,无法渲染图",
"photo.zoom.hint": "双击或双指缩放",
"photo.webgl.unavailable": "WebGL 不可用,无法渲染图",
"photo.zoom.hint": "双击或捏合缩放",
"slider.auto": "自动",
"video.codec.keyword": "编码器",
"video.conversion.cached.result": "使用缓存结果",
"video.conversion.codec.fallback": "无法找到此分辨率支持的 MP4 编解码器。回退到 WebM。",
"video.conversion.codec.fallback": "找到此分辨率支持的 MP4 编解码器。回退到 WebM。",
"video.conversion.complete": "转换完成",
"video.conversion.converting": "转换中... {{current}}/{{total}} 帧",
"video.conversion.duration.error": "无法确定视频时长或时长不是有限。",
"video.conversion.duration.error": "无法确定视频时长或时长不是有限。",
"video.conversion.encoder.error": "由于编码器错误,中止转换。",
"video.conversion.failed": "视频转换失败",
"video.conversion.initializing": "正在初始化视频转换器...",

View File

@@ -1,20 +1,18 @@
{
"action.auto": "自動",
"action.columns.setting": "列數設定",
"action.sort.mode": "排序式",
"action.columns.setting": "欄位設定",
"action.sort.mode": "排序式",
"action.tag.filter": "標籤篩選",
"action.view.github": "查看 GitHub 倉庫",
"error.feedback": "仍然遇到問題?請 GitHub 提供意見,多謝!",
"error.feedback": "仍然遇到問題?請 Github 提供反饋,謝謝!",
"error.reload": "重新載入",
"error.submit.issue": "提交問題",
"error.temporary.description": "應用程式遇到暫時性問題,點擊下按鈕嘗試重新載入應用程式或尋找其他解決方案?",
"error.title": "抱歉,應用程式發生錯誤",
"error.temporary.description": "應用程式出現臨時問題,點擊下面的按鈕嘗試重新載入應用程式或採用其他解決方案?",
"error.title": "抱歉,應用程式遇到錯誤",
"exif.aperture.value": "光圈值",
"exif.auto.white.balance.grb": "自動白平衡 GRB",
"exif.basic.info": "基本資訊",
"exif.blue.adjustment": "藍色調整",
"exif.blue.color.effect": "藍色色彩效果",
"exif.brightness.value": "亮度值",
"exif.blue.color.effect": "藍色效果",
"exif.brightness.value": "亮度",
"exif.camera": "相機",
"exif.capture.mode": "拍攝模式",
"exif.capture.parameters": "拍攝參數",
@@ -22,158 +20,135 @@
"exif.clarity": "清晰度",
"exif.color.effect": "色彩效果",
"exif.color.space": "色彩空間",
"exif.custom.rendered.normal": "正常處理",
"exif.custom.rendered.special": "自訂處理",
"exif.custom.rendered.type": "圖像處理",
"exif.device.info": "設備資訊",
"exif.digital.zoom": "數碼變焦",
"exif.colorspace.adobe.rgb": "Adobe RGB",
"exif.colorspace.srgb": "sRGB",
"exif.colorspace.uncalibrated": "未校準",
"exif.custom.rendered.type": "自訂渲染",
"exif.device.info": "裝置資訊",
"exif.dimensions": "尺寸",
"exif.dynamic.range": "動態範圍",
"exif.exposure.mode.auto": "自動曝光",
"exif.exposure.mode.bracket": "自動包圍曝光",
"exif.exposure.mode.manual": "手動曝光",
"exif.exposure.mode.manual": "手動",
"exif.exposure.mode.title": "曝光模式",
"exif.exposureprogram.action": "運動模式",
"exif.exposureprogram.aperture-priority": "光圈優先",
"exif.exposureprogram.aperture-priority-ae": "光圈優先",
"exif.exposureprogram.creative": "創意模式",
"exif.exposureprogram.landscape": "風景模式",
"exif.exposureprogram.manual": "手動",
"exif.exposureprogram.normal": "正常",
"exif.exposureprogram.not-defined": "未定義",
"exif.exposureprogram.portrait": "人像模式",
"exif.exposureprogram.program-ae": "程式自動曝光",
"exif.exposureprogram.shutter-priority": "快門優先",
"exif.exposureprogram.title": "曝光程式",
"exif.file.size": "檔案大小",
"exif.filename": "檔案名",
"exif.film.mode": "菲林模式",
"exif.flash.auto.no.return": "閃光,自動模式,未偵測到回閃",
"exif.flash.auto.no.title": "未閃光,自動模式",
"exif.flash.auto.return": "閃光,自動模式,偵測到回閃",
"exif.flash.auto.yes": "閃光,自動模式",
"exif.flash.disabled": "未閃光",
"exif.flash.enabled": "閃光",
"exif.flash.forced.mode": "強制閃光模式",
"exif.flash.forced.no.return": "強制閃光,未偵測到回閃",
"exif.flash.forced.return": "強制閃光,偵測到回閃",
"exif.flash.no.return": "閃光,未偵測到回閃",
"exif.flash.off.mode": "未閃光,強制模式",
"exif.flash.return.detected": "閃光,偵測到回閃",
"exif.filename": "檔案名",
"exif.film.mode": "膠片模式",
"exif.flash.fired": "開啟",
"exif.flash.no-flash": "關閉",
"exif.flash.off-did-not-fire": "關閉",
"exif.flash.title": "閃光燈",
"exif.flash.unavailable": "未提供閃光功能",
"exif.focal.length.actual": "焦距",
"exif.focal.length.equivalent": "35mm 等效焦距",
"exif.focal.plane.resolution": "焦平面解度",
"exif.focal.length.equivalent": "35mm 等效",
"exif.focal.plane.resolution": "焦平面解度",
"exif.format": "格式",
"exif.fuji.film.simulation": "富士菲林模擬",
"exif.gps.altitude": "海拔",
"exif.gps.latitude": "緯度",
"exif.gps.location.info": "位置資訊",
"exif.gps.location.name": "拍攝位置",
"exif.gps.longitude": "經度",
"exif.gps.view.map": "喺高德地圖中查看",
"exif.fuji.film.simulation": "膠片模擬配方",
"exif.fujirecipe-colorchromeeffect.off": "關閉",
"exif.fujirecipe-colorchromeeffect.strong": "",
"exif.fujirecipe-colorchromeeffect.weak": "",
"exif.fujirecipe-colorchromefxblue.off": "關閉",
"exif.fujirecipe-colorchromefxblue.strong": "",
"exif.fujirecipe-colorchromefxblue.weak": "弱",
"exif.fujirecipe-dynamicrange.standard": "標準",
"exif.fujirecipe-graineffectroughness.off": "關閉",
"exif.fujirecipe-graineffectsize.off": "關閉",
"exif.fujirecipe-sharpness.hard": "銳利",
"exif.fujirecipe-sharpness.normal": "標準",
"exif.fujirecipe-sharpness.soft": "柔和",
"exif.fujirecipe-whitebalance.auto": "自動",
"exif.fujirecipe-whitebalance.kelvin": "自動",
"exif.grain.effect.intensity": "顆粒效果強度",
"exif.grain.effect.size": "顆粒效果大小",
"exif.header.title": "相片資訊",
"exif.header.title": "照片檢查器",
"exif.highlight.tone": "高光色調",
"exif.lens": "鏡頭",
"exif.light.source.auto": "自動",
"exif.light.source.cloudy": "陰天",
"exif.light.source.cool.white.fluorescent": "冷白螢光燈 (W 3900 4500K)",
"exif.light.source.d50": "D50",
"exif.light.source.d55": "D55",
"exif.light.source.d65": "D65",
"exif.light.source.d75": "D75",
"exif.light.source.day.white.fluorescent": "日白螢光燈 (N 4600 5400K)",
"exif.light.source.daylight.fluorescent": "日光螢光燈 (D 5700 7100K)",
"exif.light.source.daylight.main": "日光",
"exif.light.source.fine.weather": "晴天",
"exif.light.source.flash": "閃光燈",
"exif.light.source.fluorescent": "螢光燈",
"exif.light.source.iso.tungsten": "ISO 鎢絲燈",
"exif.light.source.other": "其他光源",
"exif.light.source.shade": "陰影",
"exif.light.source.standard.a": "標準光源 A",
"exif.light.source.standard.b": "標準光源 B",
"exif.light.source.standard.c": "標準光源 C",
"exif.light.source.tungsten": "鎢絲燈",
"exif.light.source.type": "光源類型",
"exif.light.source.white.fluorescent": "白螢光燈 (WW 3200 3700K)",
"exif.light.source.type": "光源",
"exif.light.source.unknown": "未知",
"exif.max.aperture": "最大光圈",
"exif.metering.mode.average": "平均測光",
"exif.metering.mode.center": "中央重點測光",
"exif.metering.mode.multi": "多點測光",
"exif.metering.mode.partial": "局部測光",
"exif.metering.mode.pattern": "評價測光",
"exif.metering.mode.center-weighted-average": "中央重點平均測光",
"exif.metering.mode.multi-segment": "多重測光",
"exif.metering.mode.spot": "點測光",
"exif.metering.mode.type": "測光模式",
"exif.metering.mode.unknown": "未知",
"exif.noise.reduction": "降噪",
"exif.pixels": "像素",
"exif.red.adjustment": "紅色調整",
"exif.resolution.unit.cm": "厘米",
"exif.resolution.unit.inches": "英寸",
"exif.resolution.unit.none": "無單位",
"exif.saturation": "飽和度",
"exif.sensing.method.color.sequential.linear": "彩色順序線性感應器",
"exif.sensing.method.color.sequential.main": "彩色順序區域感應器",
"exif.sensing.method.one.chip": "單晶片彩色區域感應器",
"exif.sensing.method.three.chip": "三晶片彩色區域感應器",
"exif.sensing.method.trilinear": "三線感應器",
"exif.sensing.method.two.chip": "雙晶片彩色區域感應器",
"exif.sensing.method.one-chip-color-area": "單晶片彩色區域感測器",
"exif.sensing.method.type": "感光方式",
"exif.sensing.method.undefined": "未定義",
"exif.shadow.tone": "陰影色調",
"exif.sharpness": "銳度",
"exif.shutter.speed.value": "快門速度",
"exif.standard.white.balance.grb": "標準白平衡 GRB",
"exif.shutter.speed.value": "快門速度",
"exif.tags": "標籤",
"exif.technical.parameters": "技術參數",
"exif.unknown": "未知",
"exif.white.balance.auto": "自動白平衡",
"exif.white.balance.auto": "自動",
"exif.white.balance.bias": "白平衡偏移",
"exif.white.balance.daylight": "日光",
"exif.white.balance.fine.tune": "白平衡微調",
"exif.white.balance.grb": "白平衡 GRB 級別",
"exif.white.balance.manual": "手動白平衡",
"exif.white.balance.shift.ab": "白平衡偏移 (琥珀-藍)",
"exif.white.balance.shift.gm": "白平衡偏移 (綠-洋紅)",
"exif.white.balance.kelvin": "色溫",
"exif.white.balance.manual": "手動",
"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}} 張片",
"gallery.built.at": "建於 ",
"gallery.photos_one": "{{count}} 張片",
"gallery.photos_other": "{{count}} 張片",
"loading.converting": "轉換中...",
"loading.default": "載入中",
"loading.heic.converting": "HEIC/HEIF 圖格式轉換中...",
"loading.heic.converting": "正在轉換 HEIC/HEIF 圖格式...",
"loading.heic.main": "HEIC",
"loading.webgl.building": "正在構建高質素紋理...",
"loading.webgl.building": "正在建置高品質紋理...",
"loading.webgl.main": "WebGL 紋理載入",
"photo.conversion.ffmpeg": "FFmpeg",
"photo.conversion.webcodecs": "WebCodecs",
"photo.copy.error": "複製圖失敗,請稍後重試",
"photo.copy.image": "複製圖",
"photo.copy.success": "圖已複製到剪貼簿",
"photo.copying": "正在複製圖...",
"photo.download": "下載圖",
"photo.error.loading": "圖載入失敗",
"photo.copy.error": "複製圖失敗,請稍後重試",
"photo.copy.image": "複製圖",
"photo.copy.success": "圖已複製到剪貼簿",
"photo.copying": "正在複製圖...",
"photo.download": "下載圖",
"photo.error.loading": "圖載入失敗",
"photo.live.badge": "實況",
"photo.live.converting.detail": "正在使用 {{method}} 轉換影片格式...",
"photo.live.converting.video": "實況影片轉換中",
"photo.live.tooltip.desktop.main": "懸播放實況照片",
"photo.live.tooltip.desktop.zoom": "懸浮實況標識播放 / 雙擊縮放",
"photo.live.tooltip.mobile.main": "長按播放實況片",
"photo.live.tooltip.mobile.zoom": "長按播放實況相片 / 雙擊縮放",
"photo.live.converting.video": "正在轉換實況照片影片",
"photo.live.tooltip.desktop.main": "懸停以播放實況照片",
"photo.live.tooltip.desktop.zoom": "懸停「實況」徽章以播放/雙擊縮放",
"photo.live.tooltip.mobile.main": "長按播放實況片",
"photo.live.tooltip.mobile.zoom": "長按播放實況照片/雙擊縮放",
"photo.share.actions": "操作",
"photo.share.copy.failed": "複製失敗",
"photo.share.copy.link": "複製連結",
"photo.share.default.title": "片分享",
"photo.share.default.title": "片分享",
"photo.share.link.copied": "連結已複製到剪貼簿",
"photo.share.social.media": "社交媒體",
"photo.share.system": "系統分享",
"photo.share.text": "睇吓呢張靚相{{title}}",
"photo.share.title": "分享片",
"photo.share.text": "看看這張漂亮的照片{{title}}",
"photo.share.title": "分享片",
"photo.share.weibo": "微博",
"photo.webgl.unavailable": "WebGL 不可用,無法渲染圖",
"photo.zoom.hint": "雙擊或雙指縮放",
"photo.webgl.unavailable": "WebGL 不可用,無法渲染圖",
"photo.zoom.hint": "雙擊或捏合縮放",
"slider.auto": "自動",
"video.codec.keyword": "編碼器",
"video.conversion.cached.result": "使用快取結果",
"video.conversion.codec.fallback": "無法找到此解析度支援的 MP4 編解碼器。回退到 WebM。",
"video.conversion.codec.fallback": "找到此解析度支援的 MP4 編解碼器。回退到 WebM。",
"video.conversion.complete": "轉換完成",
"video.conversion.converting": "轉換中... {{current}}/{{total}} 幀",
"video.conversion.duration.error": "無法確定視頻時長或時長不是有限。",
"video.conversion.duration.error": "無法確定影片時長或時長不是有限。",
"video.conversion.encoder.error": "由於編碼器錯誤,中止轉換。",
"video.conversion.failed": "視頻轉換失敗",
"video.conversion.initializing": "正在初始化視頻轉換器...",
"video.conversion.loading": "正在載入視頻檔案...",
"video.conversion.failed": "影片轉換失敗",
"video.conversion.initializing": "正在初始化影片轉換器...",
"video.conversion.loading": "正在載入影片檔案...",
"video.conversion.starting": "開始轉換...",
"video.conversion.webcodecs.high.quality": "使用高品質 WebCodecs 轉換器...",
"video.conversion.webcodecs.not.supported": "此瀏覽器不支援 WebCodecs",

View File

@@ -1,20 +1,18 @@
{
"action.auto": "自動",
"action.columns.setting": "欄設定",
"action.sort.mode": "排序式",
"action.columns.setting": "欄設定",
"action.sort.mode": "排序式",
"action.tag.filter": "標籤篩選",
"action.view.github": "檢視 GitHub 存庫",
"error.feedback": "仍然遇到這個問題?請在 GitHub 中提供回饋,謝謝!",
"action.view.github": "檢視 GitHub 存庫",
"error.feedback": "仍然遇到問題?請在 Github 中提供回饋,謝謝!",
"error.reload": "重新載入",
"error.submit.issue": "提交問題",
"error.temporary.description": "應用程式遇到暫時性問題,點擊下按鈕嘗試重新載入應用程式或其他解決方案?",
"error.title": "抱歉,應用程式發生錯誤",
"error.temporary.description": "應用程式出現臨時問題,點擊下面的按鈕嘗試重新載入應用程式或採用其他解決方案?",
"error.title": "抱歉,應用程式遇到錯誤",
"exif.aperture.value": "光圈值",
"exif.auto.white.balance.grb": "自動白平衡 GRB",
"exif.basic.info": "基本資訊",
"exif.blue.adjustment": "藍色調整",
"exif.blue.color.effect": "藍色色彩效果",
"exif.brightness.value": "亮度值",
"exif.blue.color.effect": "藍色效果",
"exif.brightness.value": "亮度",
"exif.camera": "相機",
"exif.capture.mode": "拍攝模式",
"exif.capture.parameters": "拍攝參數",
@@ -22,135 +20,112 @@
"exif.clarity": "清晰度",
"exif.color.effect": "色彩效果",
"exif.color.space": "色彩空間",
"exif.custom.rendered.normal": "正常處理",
"exif.custom.rendered.special": "自訂處理",
"exif.custom.rendered.type": "影像處理",
"exif.device.info": "設備資訊",
"exif.digital.zoom": "數位變焦",
"exif.colorspace.adobe.rgb": "Adobe RGB",
"exif.colorspace.srgb": "sRGB",
"exif.colorspace.uncalibrated": "未校準",
"exif.custom.rendered.type": "自訂渲染",
"exif.device.info": "裝置資訊",
"exif.dimensions": "尺寸",
"exif.dynamic.range": "動態範圍",
"exif.exposure.mode.auto": "自動曝光",
"exif.exposure.mode.bracket": "自動包圍曝光",
"exif.exposure.mode.manual": "手動曝光",
"exif.exposure.mode.manual": "手動",
"exif.exposure.mode.title": "曝光模式",
"exif.exposureprogram.action": "運動模式",
"exif.exposureprogram.aperture-priority": "光圈優先",
"exif.exposureprogram.aperture-priority-ae": "光圈優先",
"exif.exposureprogram.creative": "創意模式",
"exif.exposureprogram.landscape": "風景模式",
"exif.exposureprogram.manual": "手動",
"exif.exposureprogram.normal": "正常",
"exif.exposureprogram.not-defined": "未定義",
"exif.exposureprogram.portrait": "人像模式",
"exif.exposureprogram.program-ae": "程式自動曝光",
"exif.exposureprogram.shutter-priority": "快門優先",
"exif.exposureprogram.title": "曝光程式",
"exif.file.size": "檔案大小",
"exif.filename": "檔案名",
"exif.filename": "檔案名",
"exif.film.mode": "底片模式",
"exif.flash.auto.no.return": "閃光,自動模式,未偵測到回閃",
"exif.flash.auto.no.title": "未閃光,自動模式",
"exif.flash.auto.return": "閃光,自動模式,偵測到回閃",
"exif.flash.auto.yes": "閃光,自動模式",
"exif.flash.disabled": "未閃光",
"exif.flash.enabled": "閃光",
"exif.flash.forced.mode": "強制閃光模式",
"exif.flash.forced.no.return": "強制閃光,未偵測到回閃",
"exif.flash.forced.return": "強制閃光,偵測到回閃",
"exif.flash.no.return": "閃光,未偵測到回閃",
"exif.flash.off.mode": "未閃光,強制模式",
"exif.flash.return.detected": "閃光,偵測到回閃",
"exif.flash.fired": "開啟",
"exif.flash.no-flash": "關閉",
"exif.flash.off-did-not-fire": "關閉",
"exif.flash.title": "閃光燈",
"exif.flash.unavailable": "未提供閃光功能",
"exif.focal.length.actual": "焦距",
"exif.focal.length.equivalent": "35mm 等效焦距",
"exif.focal.length.equivalent": "35mm 等效",
"exif.focal.plane.resolution": "焦平面解析度",
"exif.format": "格式",
"exif.fuji.film.simulation": "富士底片模擬",
"exif.gps.altitude": "海拔",
"exif.gps.latitude": "緯度",
"exif.gps.location.info": "位置資訊",
"exif.gps.location.name": "拍攝位置",
"exif.gps.longitude": "經度",
"exif.gps.view.map": "在高德地圖中檢視",
"exif.fuji.film.simulation": "底片模擬配方",
"exif.fujirecipe-colorchromeeffect.off": "關閉",
"exif.fujirecipe-colorchromeeffect.strong": "",
"exif.fujirecipe-colorchromeeffect.weak": "",
"exif.fujirecipe-colorchromefxblue.off": "關閉",
"exif.fujirecipe-colorchromefxblue.strong": "",
"exif.fujirecipe-colorchromefxblue.weak": "弱",
"exif.fujirecipe-dynamicrange.standard": "標準",
"exif.fujirecipe-graineffectroughness.off": "關閉",
"exif.fujirecipe-graineffectsize.off": "關閉",
"exif.fujirecipe-sharpness.hard": "銳利",
"exif.fujirecipe-sharpness.normal": "標準",
"exif.fujirecipe-sharpness.soft": "柔和",
"exif.fujirecipe-whitebalance.auto": "自動",
"exif.fujirecipe-whitebalance.kelvin": "自動",
"exif.grain.effect.intensity": "顆粒效果強度",
"exif.grain.effect.size": "顆粒效果大小",
"exif.header.title": "照片資訊",
"exif.header.title": "照片檢查器",
"exif.highlight.tone": "高光色調",
"exif.lens": "鏡頭",
"exif.light.source.auto": "自動",
"exif.light.source.cloudy": "陰天",
"exif.light.source.cool.white.fluorescent": "冷白螢光燈 (W 3900 4500K)",
"exif.light.source.d50": "D50",
"exif.light.source.d55": "D55",
"exif.light.source.d65": "D65",
"exif.light.source.d75": "D75",
"exif.light.source.day.white.fluorescent": "日白螢光燈 (N 4600 5400K)",
"exif.light.source.daylight.fluorescent": "日光螢光燈 (D 5700 7100K)",
"exif.light.source.daylight.main": "日光",
"exif.light.source.fine.weather": "晴天",
"exif.light.source.flash": "閃光燈",
"exif.light.source.fluorescent": "螢光燈",
"exif.light.source.iso.tungsten": "ISO 鎢絲燈",
"exif.light.source.other": "其他光源",
"exif.light.source.shade": "陰影",
"exif.light.source.standard.a": "標準光源 A",
"exif.light.source.standard.b": "標準光源 B",
"exif.light.source.standard.c": "標準光源 C",
"exif.light.source.tungsten": "鎢絲燈",
"exif.light.source.type": "光源類型",
"exif.light.source.white.fluorescent": "白螢光燈 (WW 3200 3700K)",
"exif.light.source.type": "光源",
"exif.light.source.unknown": "未知",
"exif.max.aperture": "最大光圈",
"exif.metering.mode.average": "平均測光",
"exif.metering.mode.center": "中央重點測光",
"exif.metering.mode.multi": "多點測光",
"exif.metering.mode.partial": "局部測光",
"exif.metering.mode.pattern": "評價測光",
"exif.metering.mode.center-weighted-average": "中央重點平均測光",
"exif.metering.mode.multi-segment": "多重測光",
"exif.metering.mode.spot": "點測光",
"exif.metering.mode.type": "測光模式",
"exif.metering.mode.unknown": "未知",
"exif.noise.reduction": "降噪",
"exif.pixels": "像素",
"exif.red.adjustment": "紅色調整",
"exif.resolution.unit.cm": "公分",
"exif.resolution.unit.inches": "英寸",
"exif.resolution.unit.none": "無單位",
"exif.saturation": "飽和度",
"exif.sensing.method.color.sequential.linear": "彩色順序線性感測器",
"exif.sensing.method.color.sequential.main": "彩色順序區域感測器",
"exif.sensing.method.one.chip": "單晶片彩色區域感測器",
"exif.sensing.method.three.chip": "三晶片彩色區域感測器",
"exif.sensing.method.trilinear": "三線感測器",
"exif.sensing.method.two.chip": "雙晶片彩色區域感測器",
"exif.sensing.method.one-chip-color-area": "單晶片彩色區域感測器",
"exif.sensing.method.type": "感光方式",
"exif.sensing.method.undefined": "未定義",
"exif.shadow.tone": "陰影色調",
"exif.sharpness": "銳度",
"exif.shutter.speed.value": "快門速度",
"exif.standard.white.balance.grb": "標準白平衡 GRB",
"exif.shutter.speed.value": "快門速度",
"exif.tags": "標籤",
"exif.technical.parameters": "技術參數",
"exif.unknown": "未知",
"exif.white.balance.auto": "自動白平衡",
"exif.white.balance.auto": "自動",
"exif.white.balance.bias": "白平衡偏移",
"exif.white.balance.daylight": "日光",
"exif.white.balance.fine.tune": "白平衡微調",
"exif.white.balance.grb": "白平衡 GRB 等級",
"exif.white.balance.manual": "手動白平衡",
"exif.white.balance.shift.ab": "白平衡偏移 (琥珀-藍)",
"exif.white.balance.shift.gm": "白平衡偏移 (綠-洋紅)",
"exif.white.balance.kelvin": "色溫",
"exif.white.balance.manual": "手動",
"exif.white.balance.shift.ab": "白平衡偏移 (琥珀-藍)",
"exif.white.balance.shift.gm": "白平衡偏移 (綠-洋紅)",
"exif.white.balance.title": "白平衡",
"gallery.built.at": "建於 ",
"gallery.built.at": "建於 ",
"gallery.photos_one": "{{count}} 張照片",
"gallery.photos_other": "{{count}} 張照片",
"loading.converting": "轉換中...",
"loading.default": "載入中",
"loading.heic.converting": "HEIC/HEIF 圖格式轉換中...",
"loading.heic.converting": "正在轉換 HEIC/HEIF 圖格式...",
"loading.heic.main": "HEIC",
"loading.webgl.building": "正在建高品質紋理...",
"loading.webgl.building": "正在建高品質紋理...",
"loading.webgl.main": "WebGL 紋理載入",
"photo.conversion.ffmpeg": "FFmpeg",
"photo.conversion.webcodecs": "WebCodecs",
"photo.copy.error": "複製圖失敗,請稍後重試",
"photo.copy.image": "複製圖",
"photo.copy.success": "圖已複製到剪貼簿",
"photo.copying": "正在複製圖...",
"photo.download": "下載圖",
"photo.error.loading": "圖載入失敗",
"photo.copy.error": "複製圖失敗,請稍後重試",
"photo.copy.image": "複製圖",
"photo.copy.success": "圖已複製到剪貼簿",
"photo.copying": "正在複製圖...",
"photo.download": "下載圖",
"photo.error.loading": "圖載入失敗",
"photo.live.badge": "實況",
"photo.live.converting.detail": "正在使用 {{method}} 轉換影片格式...",
"photo.live.converting.video": "實況影片轉換中",
"photo.live.tooltip.desktop.main": "懸播放實況照片",
"photo.live.tooltip.desktop.zoom": "懸浮實況標識播放 / 雙擊縮放",
"photo.live.tooltip.mobile.main": "長按播放實況照片",
"photo.live.tooltip.mobile.zoom": "長按播放實況照片 / 雙擊縮放",
"photo.live.converting.video": "正在轉換實況照片影片",
"photo.live.tooltip.desktop.main": "懸停以播放實況照片",
"photo.live.tooltip.desktop.zoom": "懸停「實況」徽章以播放/雙擊縮放",
"photo.live.tooltip.mobile.main": "長按播放實況照片",
"photo.live.tooltip.mobile.zoom": "長按播放實況照片/雙擊縮放",
"photo.share.actions": "操作",
"photo.share.copy.failed": "複製失敗",
"photo.share.copy.link": "複製連結",
@@ -158,18 +133,18 @@
"photo.share.link.copied": "連結已複製到剪貼簿",
"photo.share.social.media": "社群媒體",
"photo.share.system": "系統分享",
"photo.share.text": "檢視這張精美的照片:{{title}}",
"photo.share.text": "看看這張漂亮的照片:{{title}}",
"photo.share.title": "分享照片",
"photo.share.weibo": "微博",
"photo.webgl.unavailable": "WebGL 不可用,無法渲染圖",
"photo.zoom.hint": "雙擊或雙指縮放",
"photo.webgl.unavailable": "WebGL 不可用,無法渲染圖",
"photo.zoom.hint": "雙擊或捏合縮放",
"slider.auto": "自動",
"video.codec.keyword": "編碼器",
"video.conversion.cached.result": "使用快取結果",
"video.conversion.codec.fallback": "無法找到此解析度支援的 MP4 編解碼器。回退到 WebM。",
"video.conversion.codec.fallback": "找到此解析度支援的 MP4 編解碼器。回退到 WebM。",
"video.conversion.complete": "轉換完成",
"video.conversion.converting": "轉換中... {{current}}/{{total}} 幀",
"video.conversion.duration.error": "無法確定影片時長或時長不是有限。",
"video.conversion.duration.error": "無法確定影片時長或時長不是有限。",
"video.conversion.encoder.error": "由於編碼器錯誤,中止轉換。",
"video.conversion.failed": "影片轉換失敗",
"video.conversion.initializing": "正在初始化影片轉換器...",

View File

@@ -16,8 +16,7 @@
"@vingle/bmp-js": "^0.2.5",
"blurhash": "2.0.5",
"execa": "9.6.0",
"exif-reader": "2.0.2",
"fuji-recipes": "1.0.2",
"exiftool-vendored": "30.2.0",
"heic-convert": "2.1.0",
"heic-to": "1.1.14",
"sharp": "0.34.2"

View File

@@ -1,79 +1,33 @@
import type { Exif } from 'exif-reader'
import exifReader from 'exif-reader'
import getRecipe from 'fuji-recipes'
import { writeFileSync } from 'node:fs'
import { mkdir, unlink } from 'node:fs/promises'
import path from 'node:path'
import { noop } from 'es-toolkit'
import type { ExifDateTime, Tags } from 'exiftool-vendored'
import { exiftool } from 'exiftool-vendored'
import type { Metadata } from 'sharp'
import sharp from 'sharp'
import type { Logger } from '../logger/index.js'
import type { PickedExif } from '../types/photo.js'
// 清理 EXIF 数据中的空字符和无用信息
function cleanExifData(exifData: any): any {
if (!exifData || typeof exifData !== 'object') {
return exifData
}
if (Array.isArray(exifData)) {
return exifData.map((item) => cleanExifData(item))
}
// 如果是 Date 对象,直接返回
if (exifData instanceof Date) {
return exifData
}
const cleaned: any = {}
// 重要的日期字段,不应该被过度清理
const importantDateFields = new Set([
'DateTimeOriginal',
'DateTime',
'DateTimeDigitized',
'CreateDate',
'ModifyDate',
])
for (const [key, value] of Object.entries(exifData)) {
if (value === null || value === undefined) {
continue
}
if (typeof value === 'string') {
// 对于重要的日期字段,只移除空字符,不进行过度清理
if (importantDateFields.has(key)) {
const cleanedString = value.replaceAll('\0', '')
if (cleanedString.length > 0) {
cleaned[key] = cleanedString
}
} else {
// 对于其他字符串字段,移除空字符并清理空白字符
const cleanedString = value.replaceAll('\0', '').trim()
if (cleanedString.length > 0) {
cleaned[key] = cleanedString
}
}
} else if (value instanceof Date) {
// Date 对象直接保留
cleaned[key] = value
} else if (typeof value === 'object') {
// 递归清理嵌套对象
const cleanedNested = cleanExifData(value)
if (cleanedNested && Object.keys(cleanedNested).length > 0) {
cleaned[key] = cleanedNested
}
} else {
// 其他类型直接保留
cleaned[key] = value
}
}
return cleaned
}
const baseImageBuffer = sharp({
create: {
width: 1,
height: 1,
channels: 3,
background: { r: 255, g: 255, b: 255 },
},
})
.jpeg()
.toBuffer()
// 提取 EXIF 数据
export async function extractExifData(
imageBuffer: Buffer,
originalBuffer?: Buffer,
exifLogger?: Logger['exif'],
): Promise<Exif | null> {
): Promise<PickedExif | null> {
const log = exifLogger
try {
@@ -113,19 +67,40 @@ export async function extractExifData(
}
const exifBuffer = metadata.exif.subarray(startIndex)
// 使用 exif-reader 解析 EXIF 数据
const exifData = exifReader(exifBuffer)
const soi = Buffer.from([0xff, 0xd8])
const app1Marker = Buffer.from([0xff, 0xe1])
const exifLength = Buffer.alloc(2)
exifLength.writeUInt16BE(exifBuffer.length + 2, 0)
if (exifData.Photo?.MakerNote) {
const recipe = getRecipe(exifData.Photo.MakerNote)
;(exifData as any).FujiRecipe = recipe
log?.info('检测到富士胶片配方信息')
}
const finalBuffer = Buffer.concat([
soi,
app1Marker,
exifLength,
exifBuffer as any,
(await baseImageBuffer).subarray(2),
])
await mkdir('/tmp/image_process', { recursive: true })
const tempImagePath = path.resolve(
'/tmp/image_process',
`${crypto.randomUUID()}.jpg`,
)
delete exifData.Photo?.MakerNote
delete exifData.Photo?.UserComment
delete exifData.Photo?.PrintImageMatching
delete exifData.Image?.PrintImageMatching
writeFileSync(tempImagePath, finalBuffer)
const exifData = await exiftool.read(tempImagePath)
const result = handleExifData(exifData, metadata)
// const makerNote = exifReader(exifBuffer).Photo?.MakerNote
// if (makerNote) {
// const recipe = getRecipe(makerNote)
// if (recipe) {
// ;(exifData as any).FujiRecipe = recipe
// log?.info('检测到富士胶片配方信息')
// }
// }
await unlink(tempImagePath).catch(noop)
if (!exifData) {
log?.warn('EXIF 数据解析失败')
@@ -133,12 +108,129 @@ export async function extractExifData(
}
// 清理 EXIF 数据中的空字符和无用数据
const cleanedExifData = cleanExifData(exifData)
delete exifData.warnings
delete exifData.errors
// const cleanedExifData = cleanExifData(exifData)
log?.success('EXIF 数据提取完成')
return cleanedExifData
return result
} catch (error) {
log?.error('提取 EXIF 数据失败:', error)
return null
}
}
const pickKeys: Array<keyof Tags | (string & {})> = [
'zone',
'tz',
'tzSource',
'Orientation',
'Make',
'Model',
'Software',
'Artist',
'Copyright',
'ExposureTime',
'FNumber',
'ExposureProgram',
'ISO',
'OffsetTime',
'OffsetTimeOriginal',
'OffsetTimeDigitized',
'ShutterSpeedValue',
'ApertureValue',
'BrightnessValue',
'ExposureCompensationSet',
'ExposureCompensationMode',
'ExposureCompensationSetting',
'ExposureCompensation',
'MaxApertureValue',
'LightSource',
'Flash',
'FocalLength',
'ColorSpace',
'ExposureMode',
'FocalLengthIn35mmFormat',
'SceneCaptureType',
'LensMake',
'LensModel',
'MeteringMode',
'WhiteBalance',
'WBShiftAB',
'WBShiftGM',
'WhiteBalanceBias',
'WhiteBalanceFineTune',
'FlashMeteringMode',
'SensingMethod',
'FocalPlaneXResolution',
'FocalPlaneYResolution',
'Aperture',
'ScaleFactor35efl',
'ShutterSpeed',
'LightValue',
]
function handleExifData(exifData: Tags, metadata: Metadata): PickedExif {
const date = {
DateTimeOriginal: formatExifDate(exifData.DateTimeOriginal),
DateTimeDigitized: formatExifDate(exifData.DateTimeDigitized),
OffsetTime: exifData.OffsetTime,
OffsetTimeOriginal: exifData.OffsetTimeOriginal,
OffsetTimeDigitized: exifData.OffsetTimeDigitized,
}
let FujiRecipe: any = null
if (exifData.FilmMode) {
FujiRecipe = {
FilmMode: exifData.FilmMode,
GrainEffectRoughness: exifData.GrainEffectRoughness,
GrainEffectSize: exifData.GrainEffectSize,
ColorChromeEffect: exifData.ColorChromeEffect,
ColorChromeFxBlue: exifData.ColorChromeFXBlue,
WhiteBalance: exifData.WhiteBalance,
WhiteBalanceFineTune: exifData.WhiteBalanceFineTune,
DynamicRange: exifData.DynamicRange,
HighlightTone: exifData.HighlightTone,
ShadowTone: exifData.ShadowTone,
Saturation: exifData.Saturation,
Sharpness: exifData.Sharpness,
NoiseReduction: exifData.NoiseReduction,
Clarity: exifData.Clarity,
}
}
const size = {
ImageWidth: exifData.ExifImageWidth || metadata.width,
ImageHeight: exifData.ExifImageHeight || metadata.height,
}
const result: any = structuredClone(exifData)
for (const key in result) {
Reflect.deleteProperty(result, key)
}
for (const key of pickKeys) {
result[key] = exifData[key]
}
return {
...result,
...date,
...size,
...(FujiRecipe ? { FujiRecipe } : {}),
}
}
const formatExifDate = (date: string | ExifDateTime | undefined) => {
if (!date) {
return
}
if (typeof date === 'string') {
return new Date(date).toISOString()
}
return date.toISOString()
}

View File

@@ -52,7 +52,7 @@ export {
needsUpdate,
saveManifest,
} from './manifest/manager.js'
export type { FujiRecipe, PickedExif } from './types/photo.js'
// Worker 池
export {
type TaskFunction,

View File

@@ -1,15 +1,14 @@
import path from 'node:path'
import { env } from '@env'
import type { Exif } from 'exif-reader'
import type { Logger } from '../logger/index.js'
import type { PhotoInfo } from '../types/photo.js'
import type { PhotoInfo, PickedExif } from '../types/photo.js'
// 从文件名提取照片信息
export function extractPhotoInfo(
key: string,
exifData?: Exif | null,
exifData?: PickedExif | null,
imageLogger?: Logger['image'],
): PhotoInfo {
const log = imageLogger
@@ -48,23 +47,14 @@ export function extractPhotoInfo(
}
// 优先使用 EXIF 中的 DateTimeOriginal
if (exifData?.Photo?.DateTimeOriginal) {
if (exifData?.DateTimeOriginal) {
try {
const dateTimeOriginal = exifData.Photo.DateTimeOriginal as any
const dateTimeOriginal = new Date(exifData.DateTimeOriginal)
// 如果是 Date 对象,直接使用
if (dateTimeOriginal instanceof Date) {
dateTaken = dateTimeOriginal.toISOString()
log?.debug('使用 EXIF Date 对象作为拍摄时间')
} else if (typeof dateTimeOriginal === 'string') {
// 如果是字符串,按原来的方式处理
// EXIF 日期格式通常是 "YYYY:MM:DD HH:MM:SS"
const formattedDateStr = dateTimeOriginal.replace(
/^(\d{4}):(\d{2}):(\d{2})/,
'$1-$2-$3',
)
dateTaken = new Date(formattedDateStr).toISOString()
log?.debug(`使用 EXIF 字符串作为拍摄时间:${dateTimeOriginal}`)
} else {
log?.warn(
`未知的 DateTimeOriginal 类型:${typeof dateTimeOriginal}`,
@@ -73,7 +63,7 @@ export function extractPhotoInfo(
}
} catch (error) {
log?.warn(
`解析 EXIF DateTimeOriginal 失败:${exifData.Photo.DateTimeOriginal}`,
`解析 EXIF DateTimeOriginal 失败:${exifData.DateTimeOriginal}`,
error,
)
}

View File

@@ -2,7 +2,6 @@ import path from 'node:path'
import { workdir } from '@afilmory/builder/path.js'
import type { _Object } from '@aws-sdk/client-s3'
import type { Exif } from 'exif-reader'
import sharp from 'sharp'
import { HEIC_FORMATS } from '../constants/index.js'
@@ -20,7 +19,11 @@ import {
import type { Logger } from '../logger/index.js'
import { needsUpdate } from '../manifest/manager.js'
import { generateS3Url, getImageFromS3 } from '../s3/operations.js'
import type { PhotoManifestItem, ProcessPhotoResult } from '../types/photo.js'
import type {
PhotoManifestItem,
PickedExif,
ProcessPhotoResult,
} from '../types/photo.js'
import { extractPhotoInfo } from './info-extractor.js'
export interface PhotoProcessorOptions {
@@ -197,7 +200,7 @@ export async function processPhoto(
}
// 如果是增量更新且已有 EXIF 数据,可以复用
let exifData: Exif | null = null
let exifData: PickedExif | null = null
if (
!options.isForceMode &&
!options.isForceManifest &&

View File

@@ -1,4 +1,4 @@
import type { Exif } from 'exif-reader'
import type { Tags } from 'exiftool-vendored'
export interface PhotoInfo {
title: string
@@ -30,7 +30,7 @@ export interface PhotoManifestItem {
s3Key: string
lastModified: string
size: number
exif: Exif | null
exif: PickedExif | null
isLivePhoto?: boolean
livePhotoVideoUrl?: string
livePhotoVideoS3Key?: string
@@ -41,8 +41,159 @@ export interface ProcessPhotoResult {
type: 'processed' | 'skipped' | 'new' | 'failed'
}
export interface PickedExif {
// 时区和时间相关
zone?: string
tz?: string
tzSource?: string
// 基本相机信息
Orientation?: number
Make?: string
Model?: string
Software?: string
Artist?: string
Copyright?: string
// 曝光相关
ExposureTime?: string | number
FNumber?: number
ExposureProgram?: string
ISO?: number
ShutterSpeedValue?: string | number
ApertureValue?: number
BrightnessValue?: number
ExposureCompensation?: number
MaxApertureValue?: number
// 时间偏移
OffsetTime?: string
OffsetTimeOriginal?: string
OffsetTimeDigitized?: string
// 光源和闪光灯
LightSource?: string
Flash?: string
// 焦距相关
FocalLength?: string
FocalLengthIn35mmFormat?: string
// 镜头相关
LensMake?: string
LensModel?: string
// 颜色和拍摄模式
ColorSpace?: string
ExposureMode?: string
SceneCaptureType?: string
// 计算字段
Aperture?: number
ScaleFactor35efl?: number
ShutterSpeed?: string | number
LightValue?: number
// 日期时间处理后的ISO格式
DateTimeOriginal?: string
DateTimeDigitized?: string
// 图像尺寸
ImageWidth?: number
ImageHeight?: number
MeteringMode: Tags['MeteringMode']
WhiteBalance: Tags['WhiteBalance']
WBShiftAB: Tags['WBShiftAB']
WBShiftGM: Tags['WBShiftGM']
WhiteBalanceBias: Tags['WhiteBalanceBias']
WhiteBalanceFineTune: Tags['WhiteBalanceFineTune']
FlashMeteringMode: Tags['FlashMeteringMode']
SensingMethod: Tags['SensingMethod']
FocalPlaneXResolution: Tags['FocalPlaneXResolution']
FocalPlaneYResolution: Tags['FocalPlaneYResolution']
// 富士胶片配方
FujiRecipe?: FujiRecipe
}
export interface ThumbnailResult {
thumbnailUrl: string | null
thumbnailBuffer: Buffer | null
blurhash: string | null
}
export type FujiRecipe = {
FilmMode:
| 'F0/Standard (Provia)'
| 'F1/Studio Portrait'
| 'F1a/Studio Portrait Enhanced Saturation'
| 'F1b/Studio Portrait Smooth Skin Tone (Astia)'
| 'F1c/Studio Portrait Increased Sharpness'
| 'F2/Fujichrome (Velvia)'
| 'F3/Studio Portrait Ex'
| 'F4/Velvia'
| 'Pro Neg. Std'
| 'Pro Neg. Hi'
| 'Classic Chrome'
| 'Eterna'
| 'Classic Negative'
| 'Bleach Bypass'
| 'Nostalgic Neg'
| 'Reala ACE'
GrainEffectRoughness: 'Off' | 'Weak' | 'Strong'
GrainEffectSize: 'Off' | 'Small' | 'Large'
ColorChromeEffect: 'Off' | 'Weak' | 'Strong'
ColorChromeFxBlue: 'Off' | 'Weak' | 'Strong'
WhiteBalance:
| 'Auto'
| 'Auto (white priority)'
| 'Auto (ambiance priority)'
| 'Daylight'
| 'Cloudy'
| 'Daylight Fluorescent'
| 'Day White Fluorescent'
| 'White Fluorescent'
| 'Warm White Fluorescent'
| 'Living Room Warm White Fluorescent'
| 'Incandescent'
| 'Flash'
| 'Underwater'
| 'Custom'
| 'Custom2'
| 'Custom3'
| 'Custom4'
| 'Custom5'
| 'Kelvin'
/**
* White balance fine tune adjustment (e.g., "Red +0, Blue +0")
*/
WhiteBalanceFineTune: string
DynamicRange: 'Standard' | 'Wide'
/**
* Highlight tone adjustment (e.g., "+2 (hard)", "0 (normal)", "-1 (medium soft)")
*/
HighlightTone: string
/**
* Shadow tone adjustment (e.g., "-2 (soft)", "0 (normal)")
*/
ShadowTone: string
/**
* Saturation adjustment (e.g., "+4 (highest)", "0 (normal)", "-2 (low)")
*/
Saturation: string
/**
* Sharpness setting (e.g., "Normal", "Hard", "Soft")
*/
Sharpness: string
/**
* Noise reduction setting (e.g., "0 (normal)", "-1 (medium weak)")
*/
NoiseReduction: string
/**
* Clarity adjustment (typically 0)
*/
Clarity: number
}

View File

@@ -7,7 +7,6 @@
"./types": "./src/types.ts"
},
"dependencies": {
"exif-reader": "2.0.2",
"fuji-recipes": "1.0.2"
"@afilmory/builder": "workspace:*"
}
}

View File

@@ -34,3 +34,6 @@ class PhotoLoader {
}
}
export const photoLoader = new PhotoLoader()
export type { PhotoManifest } from './types'
export type { PickedExif } from '@afilmory/builder'

View File

@@ -1,5 +1,4 @@
import type { Exif } from 'exif-reader'
import type getRecipe from 'fuji-recipes'
import type { PickedExif } from '@afilmory/builder'
export interface PhotoManifest {
id: string
@@ -16,7 +15,7 @@ export interface PhotoManifest {
s3Key: string
lastModified: string
size: number
exif: Exif & { FujiRecipe?: ReturnType<typeof getRecipe> }
exif: PickedExif
isLivePhoto?: boolean
livePhotoVideoUrl?: string
livePhotoVideoS3Key?: string

63
pnpm-lock.yaml generated
View File

@@ -368,6 +368,9 @@ importers:
exif-reader:
specifier: 2.0.2
version: 2.0.2
exiftool-vendored:
specifier: 30.2.0
version: 30.2.0
fuji-recipes:
specifier: 1.0.2
version: 1.0.2
@@ -383,12 +386,9 @@ importers:
packages/data:
dependencies:
exif-reader:
specifier: 2.0.2
version: 2.0.2
fuji-recipes:
specifier: 1.0.2
version: 1.0.2
'@afilmory/builder':
specifier: workspace:*
version: link:../builder
packages/webgl-viewer:
dependencies:
@@ -1579,6 +1579,9 @@ packages:
'@oxc-project/types@0.72.2':
resolution: {integrity: sha512-il5RF8AP85XC0CMjHF4cnVT9nT/v/ocm6qlZQpSiAR9qBbQMGkFKloBZwm7PcnOdiUX97yHgsKM7uDCCWCu3tg==}
'@photostructure/tz-lookup@11.2.0':
resolution: {integrity: sha512-DwrvodcXHNSdGdeSF7SBL5o8aBlsaeuCuG7633F04nYsL3hn5Hxe3z/5kCqxv61J1q7ggKZ27GPylR3x0cPNXQ==}
'@pivanov/utils@0.0.2':
resolution: {integrity: sha512-q9CN0bFWxWgMY5hVVYyBgez1jGiLBa6I+LkG37ycylPhFvEGOOeaADGtUSu46CaZasPnlY8fCdVJZmrgKb1EPA==}
peerDependencies:
@@ -2663,6 +2666,9 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/luxon@3.6.2':
resolution: {integrity: sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==}
'@types/node@16.18.11':
resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==}
@@ -3216,6 +3222,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
batch-cluster@14.0.0:
resolution: {integrity: sha512-tymryLKMSho+WAaYiVQCMQ9SNHOrNDKO+J8l89RyICkSwK05XknrILAPUz6ABWa8MZOoTbLpIxFCFfyBTZGrIA==}
engines: {node: '>=20'}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -4298,6 +4308,18 @@ packages:
exif-reader@2.0.2:
resolution: {integrity: sha512-CVFqcwdwFe2GnbbW7/Q7sUhVP5Ilaw7fuXQc6ad3AbX20uGfHhXTpkF/hQHPrtOuys9elFVgsUkvwfhfvjDa1A==}
exiftool-vendored.exe@13.30.0:
resolution: {integrity: sha512-E6vxhCDJ+8avB7SFKlUZxfsxYcO1+T5ynCjbnXl2NcLhmvjgLIBlwXPjTLRNUKpqrgxdUirn7rAnY0i5fSNPPA==}
os: [win32]
exiftool-vendored.pl@13.30.0:
resolution: {integrity: sha512-sz98jtSIOb+K5Z/jFTeSOyt0KTn3NCLpx7SqRZtCquUW2UJdIx1GxZItFcavS5YkWUHkH95EDshDH3nTL30mLg==}
os: ['!win32']
hasBin: true
exiftool-vendored@30.2.0:
resolution: {integrity: sha512-WXLQn+E7bZapIpjf1q4pjzw8xOZUl3YWACgYQykHFvwLAihF6koDTMQTn6bv07wsFVN1OekmclLLHD5K531s5g==}
exit-hook@2.2.1:
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
engines: {node: '>=6'}
@@ -4949,6 +4971,10 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
luxon@3.6.1:
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
engines: {node: '>=12'}
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
@@ -8184,6 +8210,8 @@ snapshots:
'@oxc-project/types@0.72.2': {}
'@photostructure/tz-lookup@11.2.0': {}
'@pivanov/utils@0.0.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
react: 19.1.0
@@ -9312,6 +9340,8 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/luxon@3.6.2': {}
'@types/node@16.18.11': {}
'@types/node@20.17.57':
@@ -9940,6 +9970,8 @@ snapshots:
base64-js@1.5.1: {}
batch-cluster@14.0.0: {}
binary-extensions@2.3.0: {}
bindings@1.5.0:
@@ -11081,6 +11113,23 @@ snapshots:
exif-reader@2.0.2: {}
exiftool-vendored.exe@13.30.0:
optional: true
exiftool-vendored.pl@13.30.0:
optional: true
exiftool-vendored@30.2.0:
dependencies:
'@photostructure/tz-lookup': 11.2.0
'@types/luxon': 3.6.2
batch-cluster: 14.0.0
he: 1.2.0
luxon: 3.6.1
optionalDependencies:
exiftool-vendored.exe: 13.30.0
exiftool-vendored.pl: 13.30.0
exit-hook@2.2.1: {}
exsolve@1.0.5: {}
@@ -11692,6 +11741,8 @@ snapshots:
dependencies:
yallist: 4.0.0
luxon@3.6.1: {}
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0