Gracefully handle missing EXIF data
This commit is contained in:
parent
a0fd1da8c8
commit
068a0638a0
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
FORM_METADATA_ENTRIES,
|
||||
PhotoFormData,
|
||||
@ -13,6 +13,10 @@ import Link from 'next/link';
|
||||
import { cc } from '@/utility/css';
|
||||
import CanvasBlurCapture from '@/components/CanvasBlurCapture';
|
||||
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
|
||||
import {
|
||||
generateLocalNaivePostgresString,
|
||||
generateLocalPostgresString,
|
||||
} from '@/utility/date';
|
||||
|
||||
const THUMBNAIL_WIDTH = 300;
|
||||
const THUMBNAIL_HEIGHT = 200;
|
||||
@ -29,6 +33,22 @@ export default function PhotoForm({
|
||||
const [formData, setFormData] =
|
||||
useState<Partial<PhotoFormData>>(initialPhotoForm);
|
||||
|
||||
// Generate local date strings when
|
||||
// none can be harvested from EXIF
|
||||
useEffect(() => {
|
||||
if (!formData.takenAt || !formData.takenAtNaive) {
|
||||
setFormData(data => ({
|
||||
...data,
|
||||
...!formData.takenAt && {
|
||||
takenAt: generateLocalPostgresString(),
|
||||
},
|
||||
...!formData.takenAtNaive && {
|
||||
takenAtNaive: generateLocalNaivePostgresString(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [formData.takenAt, formData.takenAtNaive]);
|
||||
|
||||
const url = formData.url ?? '';
|
||||
|
||||
const updateBlurData = useCallback((blurData: string) => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Photo, titleForPhoto } from '.';
|
||||
import { Photo, photoHasCameraData, photoHasExifData, titleForPhoto } from '.';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import ImageLarge from '@/components/ImageLarge';
|
||||
import { cc } from '@/utility/css';
|
||||
@ -72,7 +72,7 @@ export default function PhotoLarge({
|
||||
{tagsToShow.length > 0 &&
|
||||
<PhotoTags tags={tagsToShow} />}
|
||||
</div>
|
||||
{showCamera &&
|
||||
{showCamera && photoHasCameraData(photo) &&
|
||||
<PhotoCamera
|
||||
camera={camera}
|
||||
showIcon={false}
|
||||
@ -80,25 +80,29 @@ export default function PhotoLarge({
|
||||
/>}
|
||||
</>)}
|
||||
{renderMiniGrid(<>
|
||||
<ul className={cc(
|
||||
'text-gray-500',
|
||||
'dark:text-gray-400',
|
||||
)}>
|
||||
<li>
|
||||
{photo.focalLengthFormatted}
|
||||
{' '}
|
||||
<span className={cc(
|
||||
'text-gray-400/80',
|
||||
'dark:text-gray-400/50',
|
||||
)}>
|
||||
{photo.focalLengthIn35MmFormatFormatted}
|
||||
</span>
|
||||
</li>
|
||||
<li>{photo.fNumberFormatted}</li>
|
||||
<li>{photo.isoFormatted}</li>
|
||||
<li>{photo.exposureTimeFormatted}</li>
|
||||
<li>{photo.exposureCompensationFormatted ?? '—'}</li>
|
||||
</ul>
|
||||
{photoHasExifData(photo) &&
|
||||
<ul className={cc(
|
||||
'text-gray-500',
|
||||
'dark:text-gray-400',
|
||||
)}>
|
||||
<li>
|
||||
{photo.focalLengthFormatted}
|
||||
{photo.focalLengthIn35MmFormatFormatted &&
|
||||
<>
|
||||
{' '}
|
||||
<span className={cc(
|
||||
'text-gray-400/80',
|
||||
'dark:text-gray-400/50',
|
||||
)}>
|
||||
{photo.focalLengthIn35MmFormatFormatted}
|
||||
</span>
|
||||
</>}
|
||||
</li>
|
||||
<li>{photo.fNumberFormatted}</li>
|
||||
<li>{photo.isoFormatted}</li>
|
||||
<li>{photo.exposureTimeFormatted}</li>
|
||||
<li>{photo.exposureCompensationFormatted ?? '—'}</li>
|
||||
</ul>}
|
||||
<div className={cc(
|
||||
'flex gap-y-4',
|
||||
'flex-col sm:flex-row md:flex-col',
|
||||
|
||||
@ -94,13 +94,15 @@ export const convertExifToFormData = (
|
||||
latitude: data.tags?.GPSLatitude?.toString(),
|
||||
longitude: data.tags?.GPSLongitude?.toString(),
|
||||
filmSimulation: undefined,
|
||||
takenAt: convertTimestampWithOffsetToPostgresString(
|
||||
data.tags?.DateTimeOriginal,
|
||||
getOffsetFromExif(data),
|
||||
),
|
||||
takenAtNaive: convertTimestampToNaivePostgresString(
|
||||
data.tags?.DateTimeOriginal,
|
||||
),
|
||||
takenAt: data.tags?.DateTimeOriginal
|
||||
? convertTimestampWithOffsetToPostgresString(
|
||||
data.tags?.DateTimeOriginal,
|
||||
getOffsetFromExif(data),
|
||||
)
|
||||
: undefined,
|
||||
takenAtNaive: data.tags?.DateTimeOriginal
|
||||
? convertTimestampToNaivePostgresString(data.tags?.DateTimeOriginal)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const convertFormDataToPhoto = (
|
||||
@ -109,9 +111,14 @@ export const convertFormDataToPhoto = (
|
||||
): PhotoDbInsert => {
|
||||
const photoForm = Object.fromEntries(formData) as PhotoFormData;
|
||||
|
||||
// Remove Server Action ID
|
||||
// Parse FormData:
|
||||
// - remove server action ID
|
||||
// - remove empty strings
|
||||
Object.keys(photoForm).forEach(key => {
|
||||
if (key.startsWith('$ACTION_ID_')) {
|
||||
if (
|
||||
key.startsWith('$ACTION_ID_') ||
|
||||
(photoForm as any)[key] === ''
|
||||
) {
|
||||
delete (photoForm as any)[key];
|
||||
}
|
||||
});
|
||||
|
||||
@ -201,3 +201,15 @@ export const dateRangeForPhotos = (
|
||||
: `${start}–${end}`;
|
||||
return { start, end, description };
|
||||
};
|
||||
|
||||
export const photoHasCameraData = (photo: Photo) =>
|
||||
photo.make ||
|
||||
photo.model;
|
||||
|
||||
export const photoHasExifData = (photo: Photo) =>
|
||||
photo.focalLength ||
|
||||
photo.focalLengthIn35MmFormat ||
|
||||
photo.fNumberFormatted ||
|
||||
photo.isoFormatted ||
|
||||
photo.exposureTimeFormatted ||
|
||||
photo.exposureCompensationFormatted;
|
||||
|
||||
@ -16,24 +16,34 @@ export const formatDateForPostgres = (date: Date) =>
|
||||
'$1-$2-$3 $4',
|
||||
);
|
||||
|
||||
const createNaiveDateWithOffset = (
|
||||
dateTimestamp = 0,
|
||||
offset = '+00:00',
|
||||
) => {
|
||||
const date = new Date(dateTimestamp * 1000);
|
||||
const dateFromTimestamp = (timestamp?: number) =>
|
||||
timestamp !== undefined ? new Date(timestamp * 1000) : new Date();
|
||||
|
||||
const createNaiveDateWithOffset = (timestamp?: number, offset = '+00:00') => {
|
||||
const date = dateFromTimestamp(timestamp);
|
||||
const dateString = `${date.toISOString()}`.replace(/\.[\d]+Z/, offset);
|
||||
return parseISO(dateString);
|
||||
};
|
||||
|
||||
export const convertTimestampWithOffsetToPostgresString = (
|
||||
dateTimestamp?: number,
|
||||
offset?: string,
|
||||
) => formatDateForPostgres(
|
||||
createNaiveDateWithOffset(dateTimestamp, offset)
|
||||
);
|
||||
// Run on the server, when there are date/timestamp/offset inputs
|
||||
|
||||
export const convertTimestampToNaivePostgresString = (timestamp = 0) =>
|
||||
new Date(timestamp * 1000).toISOString().replace(
|
||||
/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(.[\d]+Z)*/,
|
||||
'$1 $2',
|
||||
);
|
||||
export const convertTimestampWithOffsetToPostgresString = (
|
||||
timestamp?: number,
|
||||
offset?: string,
|
||||
) =>
|
||||
formatDateForPostgres(createNaiveDateWithOffset(timestamp, offset));
|
||||
|
||||
export const convertTimestampToNaivePostgresString = (timestamp?: number) =>
|
||||
dateFromTimestamp(timestamp)
|
||||
.toISOString().replace(
|
||||
/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(.[\d]+Z)*/,
|
||||
'$1 $2',
|
||||
);
|
||||
|
||||
// Run in the browser, to get generate local date time strings
|
||||
|
||||
export const generateLocalPostgresString = () =>
|
||||
formatDateForPostgres(new Date());
|
||||
|
||||
export const generateLocalNaivePostgresString = () =>
|
||||
format(new Date(), DATE_STRING_FORMAT_POSTGRES);
|
||||
|
||||
@ -10,16 +10,16 @@ export const getOffsetFromExif = (data: ExifData) =>
|
||||
) as string | undefined;
|
||||
|
||||
export const formatFocalLength = (focalLength?: number) =>
|
||||
focalLength !== undefined ? `${focalLength}mm` : undefined;
|
||||
focalLength ? `${focalLength}mm` : undefined;
|
||||
|
||||
export const formatAperture = (aperture?: number) =>
|
||||
aperture !== undefined ? `ƒ/${aperture}` : undefined;
|
||||
aperture ? `ƒ/${aperture}` : undefined;
|
||||
|
||||
export const formatIso = (iso?: number) =>
|
||||
iso !== undefined ? `ISO ${iso}` : undefined;
|
||||
iso ? `ISO ${iso}` : undefined;
|
||||
|
||||
export const formatExposureTime = (exposureTime?: number) =>
|
||||
exposureTime !== undefined
|
||||
exposureTime
|
||||
? `Shutter 1/${Math.floor(1 / (exposureTime ?? 1))}`
|
||||
: undefined;
|
||||
|
||||
@ -37,7 +37,7 @@ const fractionForDecimal = (decimal: number, fractionCharacter?: boolean) => {
|
||||
|
||||
export const formatExposureCompensation = (exposureCompensation?: number) => {
|
||||
if (
|
||||
exposureCompensation !== undefined &&
|
||||
exposureCompensation &&
|
||||
Math.abs(exposureCompensation) >= 0.33
|
||||
) {
|
||||
const decimal = exposureCompensation % 1;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user