From d6adce8e27db379a79a13730a867cbbe89eba629 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 30 Oct 2023 16:38:13 -0500 Subject: [PATCH] Refactor photo edit page data handling --- .../admin/photos/[photoId]/edit/page.tsx | 25 +++----- .../admin/uploads/[uploadPath]/page.tsx | 56 +++-------------- src/components/AdminChildPage.tsx | 50 +++++++++------ src/photo/PhotoEditPageClient.tsx | 31 ++++++++++ src/photo/PhotoForm.tsx | 15 +++-- src/photo/form.ts | 7 ++- src/photo/server.ts | 61 +++++++++++++++++++ src/vendors/fujifilm/index.ts | 39 ++++++++---- 8 files changed, 176 insertions(+), 108 deletions(-) create mode 100644 src/photo/PhotoEditPageClient.tsx create mode 100644 src/photo/server.ts diff --git a/src/app/(auth-state)/admin/photos/[photoId]/edit/page.tsx b/src/app/(auth-state)/admin/photos/[photoId]/edit/page.tsx index a5fc1881..cd065d81 100644 --- a/src/app/(auth-state)/admin/photos/[photoId]/edit/page.tsx +++ b/src/app/(auth-state)/admin/photos/[photoId]/edit/page.tsx @@ -1,31 +1,20 @@ -import PhotoForm from '@/photo/PhotoForm'; -import { convertPhotoToFormData } from '@/photo/form'; -import AdminChildPage from '@/components/AdminChildPage'; import { redirect } from 'next/navigation'; import { getPhotoCached } from '@/cache'; -import { PATH_ADMIN, PATH_ADMIN_PHOTOS } from '@/site/paths'; +import { PATH_ADMIN } from '@/site/paths'; +import PhotoEditPageClient from '@/photo/PhotoEditPageClient'; export const runtime = 'edge'; -interface Props { +export default async function PhotoEditPage({ + params: { photoId }, +}: { params: { photoId: string } -} - -export default async function PhotoPageEdit({ params: { photoId } }: Props) { +}) { const photo = await getPhotoCached(photoId); if (!photo) { redirect(PATH_ADMIN); } return ( - - - + ); }; diff --git a/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx index a908701f..8852e404 100644 --- a/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx @@ -1,66 +1,26 @@ import PhotoForm from '@/photo/PhotoForm'; -import { ExifData, ExifParserFactory } from 'ts-exif-parser'; -import { convertExifToFormData } from '@/photo/form'; import AdminChildPage from '@/components/AdminChildPage'; -import { getExtensionFromBlobUrl, getIdFromBlobUrl } from '@/services/blob'; import { PATH_ADMIN_UPLOADS } from '@/site/paths'; -import { - FujifilmSimulation, - getFujifilmSimulationFromMakerNote, - isExifForFujifilm, -} from '@/vendors/fujifilm'; +import { extractFormDataFromUploadPath } from '@/photo/server'; interface Params { params: { uploadPath: string } } export default async function UploadPage({ params: { uploadPath } }: Params) { - const url = decodeURIComponent(uploadPath); - - const extension = getExtensionFromBlobUrl(url); - - const fileBytes = uploadPath - ? await fetch(url) - .then(res => res.arrayBuffer()) - : undefined; - - let exifDataForm: ExifData | undefined; - let filmSimulation: FujifilmSimulation | undefined; - - if (fileBytes) { - const parser = ExifParserFactory.create(Buffer.from(fileBytes)); - - // Data for form - parser.enableBinaryFields(false); - exifDataForm = parser.parse(); - - // Capture film simulation for Fujifilm cameras - if (isExifForFujifilm(exifDataForm)) { - // Parse exif data again with binary fields - // in order to access MakerNote tag - parser.enableBinaryFields(true); - const exifDataBinary = parser.parse(); - const makerNote = exifDataBinary.tags?.MakerNote; - if (Buffer.isBuffer(makerNote)) { - filmSimulation = getFujifilmSimulationFromMakerNote(makerNote); - } - } - } + const { + blobId, + photoForm, + } = await extractFormDataFromUploadPath(uploadPath); return ( - {exifDataForm - ? + {photoForm + ? : null} ); diff --git a/src/components/AdminChildPage.tsx b/src/components/AdminChildPage.tsx index b2fc46f9..57ee3a85 100644 --- a/src/components/AdminChildPage.tsx +++ b/src/components/AdminChildPage.tsx @@ -8,39 +8,49 @@ function AdminChildPage({ backPath, backLabel, breadcrumb, + accessory, children, }: { backPath?: string backLabel?: string breadcrumb?: ReactNode + accessory?: ReactNode children: ReactNode, }) { return ( - {backPath && + {(backPath || breadcrumb || accessory) &&
- - - {backLabel || 'Back'} - - {breadcrumb && - <> - / - - {breadcrumb} - - } +
+ {backPath && + + + {backLabel || 'Back'} + } + {breadcrumb && + <> + / + + {breadcrumb} + + } +
+ {accessory && +
{accessory}
}
}
{children} diff --git a/src/photo/PhotoEditPageClient.tsx b/src/photo/PhotoEditPageClient.tsx new file mode 100644 index 00000000..d6be09b2 --- /dev/null +++ b/src/photo/PhotoEditPageClient.tsx @@ -0,0 +1,31 @@ +'use client'; + +import AdminChildPage from '@/components/AdminChildPage'; +import { Photo } from '.'; +import { PATH_ADMIN_PHOTOS } from '@/site/paths'; +import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; +import { BiRefresh } from 'react-icons/bi'; +import { convertPhotoToFormData } from './form'; +import PhotoForm from './PhotoForm'; + +export default function PhotoEditPageClient({ + photo, +}: { + photo: Photo +}) { + return ( + }> + Refresh EXIF + } + > + + + ); +}; diff --git a/src/photo/PhotoForm.tsx b/src/photo/PhotoForm.tsx index 9f680de0..c2c7055f 100644 --- a/src/photo/PhotoForm.tsx +++ b/src/photo/PhotoForm.tsx @@ -12,7 +12,7 @@ import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import Link from 'next/link'; import { cc } from '@/utility/css'; import CanvasBlurCapture from '@/components/CanvasBlurCapture'; -import { PATH_ADMIN_PHOTOS } from '@/site/paths'; +import { PATH_ADMIN_PHOTOS, PATH_ADMIN_UPLOADS } from '@/site/paths'; import { generateLocalNaivePostgresString, generateLocalPostgresString, @@ -126,13 +126,12 @@ export default function PhotoForm({ type={checkbox ? 'checkbox' : undefined} />)}
- {type === 'edit' && - - Cancel - } + + Cancel + diff --git a/src/photo/form.ts b/src/photo/form.ts index 37e98342..6c1ea947 100644 --- a/src/photo/form.ts +++ b/src/photo/form.ts @@ -1,4 +1,4 @@ -import { ExifData } from 'ts-exif-parser'; +import type { ExifData } from 'ts-exif-parser'; import { Photo, PhotoDbInsert, PhotoExif } from '.'; import { convertTimestampToNaivePostgresString, @@ -11,6 +11,7 @@ import { generateNanoid } from '@/utility/nanoid'; import { FILM_SIMULATION_FORM_INPUT_OPTIONS, FujifilmSimulation, + MAKE_FUJIFILM, } from '@/vendors/fujifilm'; export type PhotoFormData = Record; @@ -48,7 +49,7 @@ const FORM_METADATA: Record = { label: 'fujifilm simulation', options: FILM_SIMULATION_FORM_INPUT_OPTIONS, optionsDefaultLabel: 'Unknown', - hideBasedOnCamera: make => make !== 'FUJIFILM', + hideBasedOnCamera: make => make !== MAKE_FUJIFILM, }, focalLength: { label: 'focal length' }, focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' }, @@ -59,9 +60,9 @@ const FORM_METADATA: Record = { locationName: { label: 'location name', hideTemporarily: true }, latitude: { label: 'latitude' }, longitude: { label: 'longitude' }, - priorityOrder: { label: 'priority order' }, takenAt: { label: 'taken at' }, takenAtNaive: { label: 'taken at (naive)' }, + priorityOrder: { label: 'priority order' }, hidden: { label: 'hidden', checkbox: true }, }; diff --git a/src/photo/server.ts b/src/photo/server.ts new file mode 100644 index 00000000..cf8969e5 --- /dev/null +++ b/src/photo/server.ts @@ -0,0 +1,61 @@ +import { getExtensionFromBlobUrl, getIdFromBlobUrl } from '@/services/blob'; +import { convertExifToFormData } from '@/photo/form'; +import { + FujifilmSimulation, + getFujifilmSimulationFromMakerNote, + isExifForFujifilm, +} from '@/vendors/fujifilm'; +import { ExifData, ExifParserFactory } from 'ts-exif-parser'; +import { PhotoFormData } from './form'; + +export const extractFormDataFromUploadPath = async ( + uploadPath: string +): Promise<{ + blobId?: string + photoForm?: Partial +}> => { + const url = decodeURIComponent(uploadPath); + + const blobId = getIdFromBlobUrl(url); + + const extension = getExtensionFromBlobUrl(url); + + const fileBytes = uploadPath + ? await fetch(url) + .then(res => res.arrayBuffer()) + : undefined; + + let exifDataForm: ExifData | undefined; + let filmSimulation: FujifilmSimulation | undefined; + + if (fileBytes) { + const parser = ExifParserFactory.create(Buffer.from(fileBytes)); + + // Data for form + parser.enableBinaryFields(false); + exifDataForm = parser.parse(); + + // Capture film simulation for Fujifilm cameras + if (isExifForFujifilm(exifDataForm)) { + // Parse exif data again with binary fields + // in order to access MakerNote tag + parser.enableBinaryFields(true); + const exifDataBinary = parser.parse(); + const makerNote = exifDataBinary.tags?.MakerNote; + if (Buffer.isBuffer(makerNote)) { + filmSimulation = getFujifilmSimulationFromMakerNote(makerNote); + } + } + } + + return { + blobId, + ...exifDataForm && { + photoForm: { + ...convertExifToFormData(exifDataForm, filmSimulation), + extension, + url: decodeURIComponent(uploadPath), + }, + }, + }; +}; diff --git a/src/vendors/fujifilm/index.ts b/src/vendors/fujifilm/index.ts index edef8813..bcc80cc3 100644 --- a/src/vendors/fujifilm/index.ts +++ b/src/vendors/fujifilm/index.ts @@ -3,11 +3,13 @@ import type { ExifData } from 'ts-exif-parser'; -const MAKE_FUJIFILM = 'FUJIFILM'; +export const MAKE_FUJIFILM = 'FUJIFILM'; +const BYTE_INDEX_TAG_COUNT = 12; const BYTE_INDEX_FIRST_TAG = 14; const BYTES_PER_TAG = 12; -const BYTE_OFFSET_FOR_INT_VALUE = 8; +const BYTE_OFFSET_TAG_TYPE = 2; +const BYTE_OFFSET_TAG_VALUE = 8; const TAG_ID_SATURATION = 0x1003; const TAG_ID_FILM_MODE = 0x1401; @@ -233,16 +235,31 @@ export const getLabelForFilmSimulation = ( const parseFujifilmMakerNote = ( bytes: Buffer, - valueForTag: (tag: number, value: number) => void + valueForTagUInt: (tagId: number, value: number) => void ) => { - for ( - let i = BYTE_INDEX_FIRST_TAG; - i + BYTES_PER_TAG < bytes.length; - i += BYTES_PER_TAG - ) { - const tag = bytes.readUInt16LE(i); - const value = bytes.readUInt16LE(i + BYTE_OFFSET_FOR_INT_VALUE); - valueForTag(tag, value); + const tagCount = bytes.readUint16LE(BYTE_INDEX_TAG_COUNT); + for (let i = 0; i < tagCount; i++) { + const index = BYTE_INDEX_FIRST_TAG + i * BYTES_PER_TAG; + if (index + BYTES_PER_TAG < bytes.length) { + const tagId = bytes.readUInt16LE(index); + const tagType = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_TYPE); + switch (tagType) { + // UInt16 + case 3: + valueForTagUInt( + tagId, + bytes.readUInt16LE(index + BYTE_OFFSET_TAG_VALUE), + ); + break; + // UInt32 + case 4: + valueForTagUInt( + tagId, + bytes.readUInt32LE(index + BYTE_OFFSET_TAG_VALUE), + ); + break; + } + } } };