-
-
- {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;
+ }
+ }
}
};