Support EXIF data in PNG files (#314)
* Use exifr data to fill in gaps from PNG files * Handle exif/exifr location data inconsistencies
This commit is contained in:
parent
a5af7c617c
commit
d13c8b0fae
@ -1,4 +1,5 @@
|
||||
import {
|
||||
getCompatibleExifValue,
|
||||
convertApertureValueToFNumber,
|
||||
getAspectRatioFromExif,
|
||||
getOffsetFromExif,
|
||||
@ -11,21 +12,21 @@ import { GEO_PRIVACY_ENABLED } from '@/app/config';
|
||||
import { PhotoExif } from '..';
|
||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
|
||||
import type { ExifData } from 'ts-exif-parser';
|
||||
import type { ExifData, ExifTags } from 'ts-exif-parser';
|
||||
|
||||
export const convertExifToFormData = (
|
||||
data: ExifData,
|
||||
dataExifr?: any,
|
||||
exif: ExifData,
|
||||
exifr?: any,
|
||||
film?: FujifilmSimulation,
|
||||
recipeData?: FujifilmRecipe,
|
||||
): Partial<Record<keyof PhotoExif, string | undefined>> => {
|
||||
let title: string | undefined = dataExifr?.title?.value;
|
||||
let title: string | undefined = exifr?.title?.value;
|
||||
let caption: string | undefined;
|
||||
const description: string | undefined =
|
||||
data.tags?.ImageDescription ||
|
||||
dataExifr?.ImageDescription ||
|
||||
dataExifr?.description?.value;
|
||||
const tags: string[] | undefined = dataExifr?.subject;
|
||||
exif.tags?.ImageDescription ||
|
||||
exifr?.ImageDescription ||
|
||||
exifr?.description?.value;
|
||||
const tags: string[] | undefined = exifr?.subject;
|
||||
|
||||
if (title && title !== description) {
|
||||
caption = description;
|
||||
@ -33,34 +34,46 @@ export const convertExifToFormData = (
|
||||
title = description;
|
||||
}
|
||||
|
||||
// Convenience function with exif + exifr in scope
|
||||
const getExifValue = (
|
||||
key: keyof ExifTags,
|
||||
exifrSpecificKey?: string,
|
||||
) => getCompatibleExifValue(key, exif, exifr, exifrSpecificKey);
|
||||
|
||||
const dateTimeOriginal = getExifValue('DateTimeOriginal');
|
||||
|
||||
return {
|
||||
aspectRatio: getAspectRatioFromExif(data).toString(),
|
||||
make: data.tags?.Make,
|
||||
model: data.tags?.Model,
|
||||
focalLength: data.tags?.FocalLength?.toString(),
|
||||
focalLengthIn35MmFormat: data.tags?.FocalLengthIn35mmFormat?.toString(),
|
||||
lensMake: data.tags?.LensMake,
|
||||
lensModel: data.tags?.LensModel,
|
||||
aspectRatio: getAspectRatioFromExif(exif).toString(),
|
||||
make: getExifValue('Make'),
|
||||
model: getExifValue('Model'),
|
||||
focalLength: getExifValue('FocalLength')?.toString(),
|
||||
focalLengthIn35MmFormat:getExifValue('FocalLengthIn35mmFormat')?.toString(),
|
||||
lensMake: getExifValue('LensMake'),
|
||||
lensModel: getExifValue('LensModel'),
|
||||
fNumber: (
|
||||
data.tags?.FNumber?.toString() ||
|
||||
convertApertureValueToFNumber(data.tags?.ApertureValue)
|
||||
getExifValue('FNumber')?.toString() ||
|
||||
convertApertureValueToFNumber(getExifValue('ApertureValue'))
|
||||
),
|
||||
iso: data.tags?.ISO?.toString() || data.tags?.ISOSpeed?.toString(),
|
||||
exposureTime: data.tags?.ExposureTime?.toString(),
|
||||
exposureCompensation: data.tags?.ExposureCompensation?.toString(),
|
||||
latitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLatitude?.toString() : undefined,
|
||||
longitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
|
||||
iso:
|
||||
getExifValue('ISO')?.toString() ||
|
||||
getExifValue('ISOSpeed')?.toString(),
|
||||
exposureTime: getExifValue('ExposureTime')?.toString(),
|
||||
exposureCompensation: getExifValue('ExposureCompensation')?.toString(),
|
||||
latitude: !GEO_PRIVACY_ENABLED
|
||||
? getExifValue('GPSLatitude', 'latitude')?.toString()
|
||||
: undefined,
|
||||
longitude: !GEO_PRIVACY_ENABLED
|
||||
? getExifValue('GPSLongitude', 'longitude')?.toString()
|
||||
: undefined,
|
||||
film,
|
||||
recipeData: JSON.stringify(recipeData),
|
||||
...data.tags?.DateTimeOriginal && {
|
||||
...dateTimeOriginal && {
|
||||
takenAt: convertTimestampWithOffsetToPostgresString(
|
||||
data.tags.DateTimeOriginal,
|
||||
getOffsetFromExif(data),
|
||||
dateTimeOriginal,
|
||||
getOffsetFromExif(exif, exifr),
|
||||
),
|
||||
takenAtNaive:
|
||||
convertTimestampToNaivePostgresString(data.tags.DateTimeOriginal),
|
||||
convertTimestampToNaivePostgresString(dateTimeOriginal),
|
||||
},
|
||||
...title && { title },
|
||||
...caption && { caption },
|
||||
|
||||
@ -28,6 +28,7 @@ import { PhotoDbInsert } from '.';
|
||||
import { convertExifToFormData } from './form/server';
|
||||
import { getColorFieldsForPhotoForm } from './color/server';
|
||||
import exifr from 'exifr';
|
||||
import { getCompatibleExifValue } from '@/utility/exif';
|
||||
|
||||
const IMAGE_WIDTH_BLUR = 200;
|
||||
const IMAGE_WIDTH_DEFAULT = 200;
|
||||
@ -60,8 +61,8 @@ export const extractImageDataFromBlobPath = async (
|
||||
fileId: blobId,
|
||||
} = getFileNamePartsFromStorageUrl(url);
|
||||
|
||||
let exifData: ExifData | undefined;
|
||||
let exifrData: any | undefined;
|
||||
let dataExif: ExifData | undefined;
|
||||
let dataExifr: any | undefined;
|
||||
let film: FujifilmSimulation | undefined;
|
||||
let recipe: FujifilmRecipe | undefined;
|
||||
let blurData: string | undefined;
|
||||
@ -83,11 +84,11 @@ export const extractImageDataFromBlobPath = async (
|
||||
|
||||
// Data for form
|
||||
parser.enableBinaryFields(false);
|
||||
exifData = parser.parse();
|
||||
exifrData = await exifr.parse(fileBytes, { xmp: true });
|
||||
dataExif = parser.parse();
|
||||
dataExifr = await exifr.parse(fileBytes, { xmp: true });
|
||||
|
||||
// Capture film simulation for Fujifilm cameras
|
||||
if (isExifForFujifilm(exifData)) {
|
||||
if (isExifForFujifilm(dataExif)) {
|
||||
// Parse exif data again with binary fields
|
||||
// in order to access MakerNote tag
|
||||
parser.enableBinaryFields(true);
|
||||
@ -108,8 +109,8 @@ export const extractImageDataFromBlobPath = async (
|
||||
}
|
||||
|
||||
shouldStripGpsData = GEO_PRIVACY_ENABLED && (
|
||||
Boolean(exifData.tags?.GPSLatitude) ||
|
||||
Boolean(exifData.tags?.GPSLongitude)
|
||||
Boolean(getCompatibleExifValue('GPSLatitude', dataExif, dataExifr)) ||
|
||||
Boolean(getCompatibleExifValue('GPSLongitude', dataExif, dataExifr))
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@ -124,7 +125,7 @@ export const extractImageDataFromBlobPath = async (
|
||||
|
||||
return {
|
||||
blobId,
|
||||
...exifData && {
|
||||
...dataExif && {
|
||||
formDataFromExif: {
|
||||
...includeInitialPhotoFields && {
|
||||
hidden: 'false',
|
||||
@ -133,7 +134,7 @@ export const extractImageDataFromBlobPath = async (
|
||||
url,
|
||||
},
|
||||
...generateBlurData && { blurData },
|
||||
...convertExifToFormData(exifData, exifrData, film, recipe),
|
||||
...convertExifToFormData(dataExif, dataExifr, film, recipe),
|
||||
...colorFields,
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { parseISO, parse, format } from 'date-fns';
|
||||
import { parseISO, parse, format, isDate } from 'date-fns';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
import { Timezone } from './timezone';
|
||||
import { setDefaultDateFnLocale } from '@/i18n';
|
||||
@ -101,7 +101,10 @@ const dateFromTimestamp = (timestamp?: AmbiguousTimestamp): Date => {
|
||||
? /.+Z/i.test(timestamp)
|
||||
? new Date(timestamp)
|
||||
: new Date(`${timestamp}Z`)
|
||||
: undefined;
|
||||
// Check for date last to avoid destabilizing status quo
|
||||
: isDate(timestamp)
|
||||
? timestamp
|
||||
: undefined;
|
||||
return date && !isNaN(date.getTime()) ? date : new Date();
|
||||
};
|
||||
|
||||
|
||||
@ -1,13 +1,25 @@
|
||||
import { OrientationTypes, type ExifData } from 'ts-exif-parser';
|
||||
import { OrientationTypes, type ExifData, ExifTags } from 'ts-exif-parser';
|
||||
|
||||
const OFFSET_REGEX = /[+-]\d\d:\d\d/;
|
||||
|
||||
export const getOffsetFromExif = (data: ExifData) =>
|
||||
Object.values(data.tags as any)
|
||||
.find((value: any) =>
|
||||
typeof value === 'string' &&
|
||||
OFFSET_REGEX.test(value),
|
||||
) as string | undefined;
|
||||
export const getCompatibleExifValue = (
|
||||
key: keyof ExifTags,
|
||||
exif: ExifData,
|
||||
exifr: any,
|
||||
exifrSpecificKey?: string,
|
||||
) => exif.tags?.[key] || exifr?.[exifrSpecificKey || key];
|
||||
|
||||
const isValueOffset = (value: any) =>
|
||||
typeof value === 'string' &&
|
||||
OFFSET_REGEX.test(value);
|
||||
|
||||
export const getOffsetFromExif = (
|
||||
exif: ExifData,
|
||||
exifr: any,
|
||||
) => (
|
||||
Object.values(exif.tags as any).find(isValueOffset) ||
|
||||
Object.values(exifr).find(isValueOffset)
|
||||
) as string | undefined;
|
||||
|
||||
export const getAspectRatioFromExif = (data: ExifData): number => {
|
||||
// Using '||' operator to handle `Orientation` unexpectedly being '0'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user