Refine Nikon implementation

This commit is contained in:
Sam Becker 2025-12-30 15:29:27 -05:00
parent f6367e29e7
commit 0e97d292a1
12 changed files with 83 additions and 120 deletions

View File

@ -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 (
<PhotoDetailPage {...{
photo,
photos,
photosGrid,
film: decodedFilm,
film,
indexNumber,
count,
dateRange,

View File

@ -30,7 +30,7 @@ export async function GET(
] = await Promise.all([
getPhotosCached({
limit: MAX_PHOTOS_TO_SHOW_PER_CATEGORY,
film: film,
film,
}),
getIBMPlexMono(),
getImageResponseCacheControlHeaders(),

View File

@ -66,12 +66,11 @@ export default async function FilmPage({
params,
}: FilmProps) {
const { film } = await params;
const decodedFilm = decodeURIComponent(film);
const [
photos,
{ count, dateRange },
] = await getPhotosFilmDataCachedCached(decodedFilm);
] = await getPhotosFilmDataCachedCached(film);
if (photos.length === 0) { redirect(PATH_ROOT); }

View File

@ -6,7 +6,7 @@
"start": "next start",
"lint": "eslint .",
"test": "jest --watch --transformIgnorePatterns 'node_modules/(?!my-library-dir)/'",
"analyze": "ANALYZE=true next build"
"analyze": "ANALYZE=true next build --webpack"
},
"packageManager": "pnpm@10.26.2",
"dependencies": {

View File

@ -218,7 +218,7 @@ export const pathForRecipe = (recipe: string) =>
`${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`;

View File

@ -15,15 +15,14 @@ export default function FilmOverview({
dateRange?: PhotoDateRangePostgres,
animateOnFirstLoadOnly?: boolean,
}) {
const decodedFilm = decodeURIComponent(film);
return (
<PhotoGridContainer {...{
cacheKey: `film-${film}`,
photos,
count,
film: decodedFilm,
film,
header: <FilmHeader {...{
film: decodedFilm,
film,
photos,
count,
dateRange,

View File

@ -16,11 +16,7 @@ import {
isStringNikonPictureControl,
labelForNikonPictureControl,
} from '@/platforms/nikon/simulation';
import {
deparameterize,
formatCount,
formatCountDescriptive,
} from '@/utility/string';
import { deparameterize } from '@/utility/string';
import { AnnotatedTag } from '@/photo/form';
import PhotoFilmIcon from './PhotoFilmIcon';
import { AppTextState } from '@/i18n/state';

View File

@ -151,8 +151,8 @@ const FORM_METADATA = (
film: {
section: 'exif',
label: 'film',
note: 'Intended for Fujifilm / Nikon cameras and analog scans',
noteShort: 'Fujifilm / Nikon cameras / analog scans',
note: 'Intended for Fujifilm / Nikon / analog scans',
noteShort: 'Fujifilm / Nikon / analog scans',
tagOptions: filmOptions,
tagOptionsLimit: 1,
shouldNotOverwriteWithNullDataOnSync: true,

View File

@ -13,8 +13,8 @@ import { PhotoExif } from '..';
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
import type { ExifData, ExifTags } from 'ts-exif-parser';
import { NikonPictureControl } from '@/platforms/nikon/simulation';
import { parameterize } from '@/utility/string';
export const convertExifToFormData = (
exif: ExifData,
@ -67,7 +67,9 @@ export const convertExifToFormData = (
longitude: !GEO_PRIVACY_ENABLED
? getExifValue('GPSLongitude', 'longitude')?.toString()
: undefined,
film,
// Make sure film is parameterized since
// Nikon picture controls may not be parameterized
film: film ? parameterize(film) : undefined,
recipeData: JSON.stringify(recipeData),
...dateTimeOriginal && {
takenAt: convertTimestampWithOffsetToPostgresString(

View File

@ -153,6 +153,7 @@ export const extractImageDataFromBlobPath = async (
error,
};
};
const generateBase64 = async (
image: ArrayBuffer,
middleware?: (sharp: Sharp) => Sharp,

View File

@ -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 = (
@ -16,17 +12,17 @@ export const parseNikonMakerNote = (
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;

View File

@ -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<string, NikonPictureControlLabel> = {
'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<string, string> = {
'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<string, NikonPictureControlLabel>);
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;
};