Files
ds-galo/frontend/src/lib/exif.ts
T
iu 1b4906648d feat(preferences): configurable photo info attributes stored in user profile
- backend: users.preferences jsonb (migration 000004), Me returns preferences, PATCH /auth/me/preferences with validation (visible list + labels)
- frontend: gear in info section opens PhotoInfoSettingsDialog; base attribute set shown by default, remaining under ellipsis; per-attribute visibility and custom labels persisted to the profile; reset clears settings from the profile
- frontend: dialog has select-all/deselect-all icon buttons (no 'show all'); EXIF constants extracted to lib/exif.ts
- fix: render modal overlays via portal to document.body so they center in the viewport despite transformed panel/rail ancestors
- openspec: sync lightroom-workspace-ui artifacts (spec/design/proposal/tasks 9.8)
2026-10-10 21:04:08 +03:00

225 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const EXIF_LABELS: Record<string, string> = {
Make: 'Производитель',
Model: 'Модель',
LensMake: 'Производитель объектива',
LensModel: 'Объектив',
ExposureTime: 'Выдержка',
ShutterSpeedValue: 'Выдержка (APEX)',
FNumber: 'Диафрагма',
ApertureValue: 'Диафрагма (APEX)',
MaxApertureValue: 'Макс. диафрагма',
FocalLength: 'Фокусное расстояние',
FocalLengthIn35mmFilm: 'Фокусное (35 мм)',
ISOSpeedRatings: 'ISO',
PhotographicSensitivity: 'ISO',
ExposureProgram: 'Программа съёмки',
ExposureMode: 'Режим экспозиции',
ExposureBiasValue: 'Экспокоррекция',
BrightnessValue: 'Яркость (APEX)',
MeteringMode: 'Режим замера',
LightSource: 'Источник света',
Flash: 'Вспышка',
WhiteBalance: 'Баланс белого',
Orientation: 'Ориентация',
Software: 'ПО',
ColorSpace: 'Цветовое пространство',
SceneCaptureType: 'Тип сцены',
Contrast: 'Контраст',
Saturation: 'Насыщенность',
Sharpness: 'Резкость',
CustomRendered: 'Обработка',
GainControl: 'Усиление',
SensingMethod: 'Метод съёмки',
SubjectDistanceRange: 'Дистанция',
DigitalZoomRatio: 'Цифровой зум',
CompressedBitsPerPixel: 'Сжатие (бит/пиксель)',
ImageDescription: 'Описание'
};
export const EXIF_SKIP = new Set([
'GPSLatitude',
'GPSLongitude',
'GPSLatitudeRef',
'GPSLongitudeRef',
'GPSAltitude',
'GPSAltitudeRef',
'GPSVersionID',
'GPSDateStamp',
'GPSTimeStamp',
'GPSProcessingMethod',
'GPSAreaInformation',
'GPSImgDirection',
'GPSImgDirectionRef',
'GPSDestBearing',
'GPSDestBearingRef',
'GPSSpeed',
'GPSSpeedRef',
'GPSTrack',
'GPSTrackRef',
'GPSMapDatum',
'GPSDifferential',
'DateTime',
'DateTimeDigitized',
'DateTimeOriginal',
'SubSecTime',
'SubSecTimeDigitized',
'SubSecTimeOriginal',
'ExifVersion',
'FlashpixVersion',
'InteroperabilityVersion',
'InteroperabilityIndex',
'ExifTag',
'GPSTag',
'InteroperabilityTag',
'ExifIFDPointer',
'GPSInfoIFDPointer',
'ComponentsConfiguration',
'MakerNote',
'UserComment',
'FileSource',
'SceneType',
'PhotometricInterpretation',
'YCbCrPositioning',
'ResolutionUnit',
'BitsPerSample',
'XResolution',
'YResolution',
'ImageWidth',
'ImageLength',
'PixelXDimension',
'PixelYDimension'
]);
export const EXIF_ENUMS: Record<string, Record<number, string>> = {
Orientation: {
1: 'Горизонтальная',
2: 'Зеркальная (гориз.)',
3: 'Повёрнута на 180°',
4: 'Зеркальная (верт.)',
5: 'Зеркальная + 90° CW',
6: 'Повёрнута на 90° CW',
7: 'Зеркальная + 90° CCW',
8: 'Повёрнута на 90° CCW'
},
ExposureProgram: {
0: 'Не задано',
1: 'Ручная',
2: 'Программная',
3: 'Приоритет диафрагмы',
4: 'Приоритет выдержки',
5: 'Творческая',
6: 'Спортивная',
7: 'Портрет',
8: 'Пейзаж'
},
ExposureMode: { 0: 'Авто', 1: 'Ручной', 2: 'Авто-брекетинг' },
MeteringMode: {
0: 'Неизвестно',
1: 'Средневзвешенный',
2: 'Центрально-взвешенный',
3: 'Точечный',
4: 'Многозонный',
5: 'Матричный',
6: 'Частичный',
255: 'Другое'
},
LightSource: {
0: 'Неизвестно',
1: 'Дневной свет',
2: 'Люминесцентный',
3: 'Лампа накаливания',
4: 'Вспышка',
9: 'Хорошая погода',
10: 'Облачно',
11: 'Тень'
},
Flash: {
0: 'Не сработала',
1: 'Сработала',
9: 'Сработала (принуд.)',
16: 'Отключена',
24: 'Не сработала (авто)',
25: 'Сработала (авто)'
},
WhiteBalance: { 0: 'Авто', 1: 'Ручной' },
SceneCaptureType: { 0: 'Стандартный', 1: 'Пейзаж', 2: 'Портрет', 3: 'Ночной' },
Contrast: { 0: 'Обычный', 1: 'Мягкий', 2: 'Жёсткий' },
Saturation: { 0: 'Обычная', 1: 'Низкая', 2: 'Высокая' },
Sharpness: { 0: 'Обычная', 1: 'Мягкая', 2: 'Жёсткая' },
CustomRendered: { 0: 'Обычная', 1: 'Особая' },
GainControl: {
0: 'Нет',
1: 'Малое усиление',
2: 'Большое усиление',
3: 'Низкое усиление',
4: 'Высокое усиление'
},
SensingMethod: {
1: 'Не задано',
2: 'Однокристальный',
3: 'Двухчиповый',
4: 'Трёхчиповый',
5: 'Цветной последовательный',
7: 'Трёхлинейный',
8: 'Цветной последовательный'
},
SubjectDistanceRange: { 0: 'Неизвестно', 1: 'Макро', 2: 'Близко', 3: 'Далеко' },
ColorSpace: { 1: 'sRGB', 65535: 'Некалиброванное' }
};
export function formatExifValue(key: string, value: unknown): string {
if (value == null) return '';
const n = Number(value);
if (key === 'ExposureTime' && !Number.isNaN(n) && n > 0) {
return n >= 1 ? `${n} с` : `1/${Math.round(1 / n)}`;
}
if (
(key === 'FNumber' || key === 'ApertureValue' || key === 'MaxApertureValue') &&
!Number.isNaN(n)
) {
return `f/${n}`;
}
if ((key === 'FocalLength' || key === 'FocalLengthIn35mmFilm') && !Number.isNaN(n)) {
return `${n} мм`;
}
if (key === 'ExposureBiasValue' && !Number.isNaN(n)) {
return `${n > 0 ? '+' : ''}${n} EV`;
}
const enums = EXIF_ENUMS[key];
if (enums && !Number.isNaN(n) && enums[n] != null) return enums[n];
return String(value);
}
export interface ExifRow {
key: string;
label: string;
value: string;
}
export const EXIF_BASE: string[] = [
'Make',
'Model',
'LensModel',
'ExposureTime',
'FNumber',
'ISOSpeedRatings',
'FocalLength',
'FocalLengthIn35mmFilm'
];
export function buildExifRows(
exif: Record<string, unknown>,
labels: Record<string, string> = {}
): ExifRow[] {
return Object.entries(exif)
.filter(([k, v]) => !EXIF_SKIP.has(k) && v != null && String(v) !== '')
.map(([k, v]) => ({
key: k,
label: labels[k] ?? EXIF_LABELS[k] ?? k,
value: formatExifValue(k, v)
}))
.sort((a, b) => a.label.localeCompare(b.label, 'ru'));
}
export const EXIF_CATALOG: string[] = Object.keys(EXIF_LABELS);