From 0e97d292a1bcd9568e7d7145262ae61fe2591aae Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 30 Dec 2025 15:29:27 -0500 Subject: [PATCH] Refine Nikon implementation --- app/film/[film]/[photoId]/page.tsx | 7 +- app/film/[film]/image/route.tsx | 2 +- app/film/[film]/page.tsx | 3 +- package.json | 2 +- src/app/path.ts | 2 +- src/film/FilmOverview.tsx | 5 +- src/film/index.tsx | 6 +- src/photo/form/index.ts | 4 +- src/photo/form/server.ts | 6 +- src/photo/server.ts | 1 + src/platforms/nikon/server.ts | 19 ++-- src/platforms/nikon/simulation.ts | 146 ++++++++++++----------------- 12 files changed, 83 insertions(+), 120 deletions(-) diff --git a/app/film/[film]/[photoId]/page.tsx b/app/film/[film]/[photoId]/page.tsx index 5dc9cebf..9098c358 100644 --- a/app/film/[film]/[photoId]/page.tsx +++ b/app/film/[film]/[photoId]/page.tsx @@ -67,21 +67,20 @@ export default async function PhotoFilmPage({ params, }: PhotoFilmProps) { const { photoId, film } = await params; - const decodedFilm = decodeURIComponent(film); const { photo, photos, photosGrid, indexNumber } = - await getPhotosNearIdCachedCached(photoId, decodedFilm); + await getPhotosNearIdCachedCached(photoId, film); if (!photo) { redirect(PATH_ROOT); } - const { count, dateRange } = await getPhotosMetaCached({ film: decodedFilm }); + const { count, dateRange } = await getPhotosMetaCached({ film }); return ( `${PREFIX_RECIPE}/${recipe}`; export const pathForFilm = (film: string) => - `${PREFIX_FILM}/${film}`; + `${PREFIX_FILM}/${parameterize(film)}`; export const pathForFocalLength = (focal: number) => `${PREFIX_FOCAL_LENGTH}/${focal}mm`; diff --git a/src/film/FilmOverview.tsx b/src/film/FilmOverview.tsx index 454f8756..8a480e09 100644 --- a/src/film/FilmOverview.tsx +++ b/src/film/FilmOverview.tsx @@ -15,15 +15,14 @@ export default function FilmOverview({ dateRange?: PhotoDateRangePostgres, animateOnFirstLoadOnly?: boolean, }) { - const decodedFilm = decodeURIComponent(film); return ( Sharp, diff --git a/src/platforms/nikon/server.ts b/src/platforms/nikon/server.ts index 10219b30..380a1113 100644 --- a/src/platforms/nikon/server.ts +++ b/src/platforms/nikon/server.ts @@ -1,10 +1,6 @@ import type { ExifData } from 'ts-exif-parser'; import { isMakeNikon } from '.'; -// Nikon MakerNote Header -const NIKON_MAKERNOTE_HEADER = 'Nikon\x00\x02\x00\x00\x00'; -const HEADER_SIZE = 18; - export const isExifForNikon = (data: ExifData) => isMakeNikon(data.tags?.Make); export const parseNikonMakerNote = ( @@ -15,18 +11,18 @@ export const parseNikonMakerNote = ( if (bytes.length < 10 || bytes.toString('ascii', 0, 5) !== 'Nikon') { return; } - - // Assume Type 3 for Z series - // Skip 10 bytes header - const baseOffset = 10; const tiffStart = 10; if (bytes.length < tiffStart + 8) return; const isLE = bytes.toString('hex', tiffStart, tiffStart + 2) === '4949'; - const readUInt16 = (offset: number) => isLE ? bytes.readUInt16LE(offset) : bytes.readUInt16BE(offset); - const readUInt32 = (offset: number) => isLE ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset); + const readUInt16 = (offset: number) => isLE + ? bytes.readUInt16LE(offset) + : bytes.readUInt16BE(offset); + const readUInt32 = (offset: number) => isLE + ? bytes.readUInt32LE(offset) + : bytes.readUInt32BE(offset); const ifdOffset = readUInt32(tiffStart + 4); let currentOffset = tiffStart + ifdOffset; @@ -55,7 +51,8 @@ export const parseNikonMakerNote = ( } if (offset + count <= bytes.length) { - value = bytes.toString('ascii', offset, offset + count - 1); // -1 to remove null terminator + // -1 to remove null terminator + value = bytes.toString('ascii', offset, offset + count - 1); } } else if (type === 7) { let offset = valueOffsetOrData; diff --git a/src/platforms/nikon/simulation.ts b/src/platforms/nikon/simulation.ts index 93cfa140..66eb6e2e 100644 --- a/src/platforms/nikon/simulation.ts +++ b/src/platforms/nikon/simulation.ts @@ -1,99 +1,78 @@ +import { deparameterize } from '@/utility/string'; import { parseNikonMakerNote } from './server'; const TAG_ID_PICTURE_CONTROL_DATA = 0x0023; -export type NikonPictureControl = string; - export interface NikonPictureControlLabel { small: string medium: string large: string } -const NIKON_PICTURE_CONTROL_LABELS: Record = { - 'auto': { small: 'Auto', medium: 'Auto', large: 'Auto' }, - 'standard': { small: 'Standard', medium: 'Standard', large: 'Standard' }, - 'neutral': { small: 'Neutral', medium: 'Neutral', large: 'Neutral' }, - 'vivid': { small: 'Vivid', medium: 'Vivid', large: 'Vivid' }, - 'monochrome': { small: 'Monochrome', medium: 'Monochrome', large: 'Monochrome' }, - 'portrait': { small: 'Portrait', medium: 'Portrait', large: 'Portrait' }, - 'landscape': { small: 'Landscape', medium: 'Landscape', large: 'Landscape' }, - 'flat': { small: 'Flat', medium: 'Flat', large: 'Flat' }, - 'rich-tone-portrait': { small: 'Rich Tone Portrait', medium: 'Rich Tone Portrait', large: 'Rich Tone Portrait' }, - 'deep-tone-monochrome': { small: 'Deep Tone Mono', medium: 'Deep Tone Mono', large: 'Deep Tone Monochrome' }, - 'flat-monochrome': { small: 'Flat Mono', medium: 'Flat Mono', large: 'Flat Monochrome' }, - 'dream': { small: 'Dream', medium: 'Dream', large: 'Dream' }, - 'morning': { small: 'Morning', medium: 'Morning', large: 'Morning' }, - 'pop': { small: 'Pop', medium: 'Pop', large: 'Pop' }, - 'sunday': { small: 'Sunday', medium: 'Sunday', large: 'Sunday' }, - 'somber': { small: 'Somber', medium: 'Somber', large: 'Somber' }, - 'dramatic': { small: 'Dramatic', medium: 'Dramatic', large: 'Dramatic' }, - 'silence': { small: 'Silence', medium: 'Silence', large: 'Silence' }, - 'bleached': { small: 'Bleached', medium: 'Bleached', large: 'Bleached' }, - 'melancholic': { small: 'Melancholic', medium: 'Melancholic', large: 'Melancholic' }, - 'pure': { small: 'Pure', medium: 'Pure', large: 'Pure' }, - 'denim': { small: 'Denim', medium: 'Denim', large: 'Denim' }, - 'toy': { small: 'Toy', medium: 'Toy', large: 'Toy' }, - 'sepia': { small: 'Sepia', medium: 'Sepia', large: 'Sepia' }, - 'blue': { small: 'Blue', medium: 'Blue', large: 'Blue' }, - 'red': { small: 'Red', medium: 'Red', large: 'Red' }, - 'pink': { small: 'Pink', medium: 'Pink', large: 'Pink' }, - 'charcoal': { small: 'Charcoal', medium: 'Charcoal', large: 'Charcoal' }, - 'graphite': { small: 'Graphite', medium: 'Graphite', large: 'Graphite' }, - 'binary': { small: 'Binary', medium: 'Binary', large: 'Binary' }, - 'carbon': { small: 'Carbon', medium: 'Carbon', large: 'Carbon' }, -}; +const NIKON_PICTURE_CONTROLS = [ + 'auto', + 'standard', + 'neutral', + 'vivid', + 'monochrome', + 'portrait', + 'landscape', + 'flat', + 'rich-tone-portrait', + 'deep-tone-monochrome', + 'flat-monochrome', + 'dream', + 'morning', + 'pop', + 'sunday', + 'somber', + 'dramatic', + 'silence', + 'bleached', + 'melancholic', + 'pure', + 'denim', + 'toy', + 'sepia', + 'blue', + 'red', + 'pink', + 'charcoal', + 'graphite', + 'binary', + 'carbon', +] as const; -const NIKON_STRING_TO_MODE: Record = { - 'AUTO': 'auto', - 'STANDARD': 'standard', - 'NEUTRAL': 'neutral', - 'VIVID': 'vivid', - 'MONOCHROME': 'monochrome', - 'PORTRAIT': 'portrait', - 'LANDSCAPE': 'landscape', - 'FLAT': 'flat', - 'RICH TONE PORTRAIT': 'rich-tone-portrait', - 'DEEP TONE MONOCHROME': 'deep-tone-monochrome', - 'FLAT MONOCHROME': 'flat-monochrome', - 'DREAM': 'dream', - 'MORNING': 'morning', - 'POP': 'pop', - 'SUNDAY': 'sunday', - 'SOMBER': 'somber', - 'DRAMATIC': 'dramatic', - 'SILENCE': 'silence', - 'BLEACHED': 'bleached', - 'MELANCHOLIC': 'melancholic', - 'PURE': 'pure', - 'DENIM': 'denim', - 'TOY': 'toy', - 'SEPIA': 'sepia', - 'BLUE': 'blue', - 'RED': 'red', - 'PINK': 'pink', - 'CHARCOAL': 'charcoal', - 'GRAPHITE': 'graphite', - 'BINARY': 'binary', - 'CARBON': 'carbon', -}; +export type NikonPictureControl = typeof NIKON_PICTURE_CONTROLS[number]; -export const isStringNikonPictureControl = (film?: string): film is NikonPictureControl => +const NIKON_PICTURE_CONTROL_LABELS = + NIKON_PICTURE_CONTROLS.reduce((labels, control) => { + labels[control] = { + small: deparameterize(control), + medium: deparameterize(control), + large: deparameterize(control), + }; + return labels; + }, {} as Record); + +export const isStringNikonPictureControl = ( + film?: string, +): boolean => film !== undefined && - Object.keys(NIKON_PICTURE_CONTROL_LABELS).includes(film); + NIKON_PICTURE_CONTROLS.includes(film as any); -export const labelForNikonPictureControl = (film: NikonPictureControl): NikonPictureControlLabel => +export const labelForNikonPictureControl = ( + film: NikonPictureControl | string, +): NikonPictureControlLabel => NIKON_PICTURE_CONTROL_LABELS[film] ?? { small: film, medium: film, large: film, }; -export const getNikonPictureControlFromMakerNote = ( - bytes: Buffer, -): NikonPictureControl | undefined => { - let pictureControl: string | undefined; +export const getNikonPictureControlFromMakerNote = (bytes: Buffer)=> { + let pictureControl: NikonPictureControl | undefined; parseNikonMakerNote( bytes, @@ -103,22 +82,13 @@ export const getNikonPictureControlFromMakerNote = ( if (value.length >= 28) { const name = value.toString('ascii', 8, 28); // Remove null bytes and trim - pictureControl = name.replace(/\0/g, '').trim(); + pictureControl = name + .replace(/\0/g, '') + .trim() as NikonPictureControl; } } }, ); - if (pictureControl) { - if (NIKON_STRING_TO_MODE[pictureControl]) { - return NIKON_STRING_TO_MODE[pictureControl]; - } - const upper = pictureControl.toUpperCase(); - if (NIKON_STRING_TO_MODE[upper]) { - return NIKON_STRING_TO_MODE[upper]; - } - return pictureControl; - } - - return undefined; + return pictureControl; };