Display basic fujifilm recipes

This commit is contained in:
Sam Becker 2025-02-19 20:34:31 -06:00
parent a63c05a502
commit 62a681a424
6 changed files with 121 additions and 17 deletions

View File

@ -40,6 +40,7 @@ import { LuExpand } from 'react-icons/lu';
import LoaderButton from '@/components/primitives/LoaderButton';
import Tooltip from '@/components/Tooltip';
import ZoomControls, { ZoomControlsRef } from '@/components/image/ZoomControls';
import PhotoRecipe from './PhotoRecipe';
export default function PhotoLarge({
photo,
@ -275,10 +276,15 @@ export default function PhotoLarge({
<li>{photo.exposureCompensationFormatted ?? '0ev'}</li>
</ul>
{showSimulation && photo.filmSimulation &&
<PhotoFilmSimulation
simulation={photo.filmSimulation}
prefetch={prefetchRelatedLinks}
/>}
<Tooltip content={photo.fujifilmRecipe
? <PhotoRecipe recipe={photo.fujifilmRecipe} />
: undefined
}>
<PhotoFilmSimulation
simulation={photo.filmSimulation}
prefetch={prefetchRelatedLinks}
/>
</Tooltip>}
</>}
<div className={clsx(
'flex gap-x-3 gap-y-baseline',

93
src/photo/PhotoRecipe.tsx Normal file
View File

@ -0,0 +1,93 @@
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
import clsx from 'clsx/lite';
const addSign = (value: number) => value < 0 ? value : `+${value}`;
export default function PhotoRecipe({ recipe: {
dynamicRange,
highlight,
shadow,
color,
highISONoiseReduction,
noiseReductionLegacy,
sharpness,
clarity,
grainEffect,
colorChromeEffect,
colorChromeFXBlue,
whiteBalance,
bwAdjustment,
bwMagentaGreen,
} }: { recipe: FujifilmRecipe }) {
return <div className="text-left space-y-4">
<div className="font-bold">
Fujifilm Recipe
</div>
<div className={clsx(
'grid grid-cols-2 gap-2',
'*:odd:text-dim *:even:uppercase',
)}>
{dynamicRange !== undefined && <>
<div>DR</div>
<div>{dynamicRange}</div>
</>}
{highlight !== undefined && <>
<div>Highlight</div>
<div>{addSign(highlight)}</div>
</>}
{shadow !== undefined && <>
<div>Shadow</div>
<div>{addSign(shadow)}</div>
</>}
{color !== undefined && <>
<div>Color</div>
<div>{addSign(color)}</div>
</>}
{highISONoiseReduction !== undefined && <>
<div>High ISO NR</div>
<div>{addSign(highISONoiseReduction)}</div>
</>}
{noiseReductionLegacy !== undefined && <>
<div>NR</div>
<div>{noiseReductionLegacy}</div>
</>}
{sharpness !== undefined && <>
<div>Sharpness</div>
<div>{addSign(sharpness)}</div>
</>}
{clarity !== undefined && <>
<div>Clarity</div>
<div>{addSign(clarity)}</div>
</>}
{grainEffect !== undefined && <>
<div>Grain</div>
<div>{grainEffect.roughness} / {grainEffect.size}</div>
</>}
{colorChromeEffect !== undefined && <>
<div>Chrome</div>
<div>{colorChromeEffect}</div>
</>}
{colorChromeFXBlue !== undefined && <>
<div>Chrome FX Blue</div>
<div>{colorChromeFXBlue}</div>
</>}
{whiteBalance !== undefined && <>
<div>White Balance</div>
<div>
<div>{whiteBalance.type}</div>
<div>
{addSign(whiteBalance.red)} / {addSign(whiteBalance.blue)}
</div>
</div>
</>}
{bwAdjustment !== undefined && <>
<div>BW</div>
<div>{addSign(bwAdjustment)}</div>
</>}
{bwMagentaGreen !== undefined && <>
<div>BW MG</div>
<div>{addSign(bwMagentaGreen)}</div>
</>}
</div>
</div>;
}

View File

@ -187,6 +187,8 @@ export const convertPhotoToFormData = (
return value?.toISOString ? value.toISOString() : value;
case 'hidden':
return value ? 'true' : 'false';
case 'fujifilmRecipe':
return JSON.stringify(value);
default:
return value !== undefined && value !== null
? value.toString()
@ -206,7 +208,7 @@ export const convertPhotoToFormData = (
export const convertExifToFormData = (
data: ExifData,
filmSimulation?: FilmSimulation,
fujifilmRecipe?: Partial<FujifilmRecipe>,
fujifilmRecipe?: FujifilmRecipe,
): Omit<
Record<keyof PhotoExif, string | undefined>,
'takenAt' | 'takenAtNaive'

View File

@ -91,16 +91,15 @@ export interface PhotoDbInsert extends PhotoExif {
// Raw db response
export interface PhotoDb extends
Omit<PhotoDbInsert, 'takenAt' | 'tags' | 'fujifilmRecipe'> {
Omit<PhotoDbInsert, 'takenAt' | 'tags'> {
updatedAt: Date
createdAt: Date
takenAt: Date
tags: string[]
fujifilmRecipe?: Partial<FujifilmRecipe>
}
// Parsed db response
export interface Photo extends PhotoDb {
export interface Photo extends Omit<PhotoDb, 'fujifilmRecipe'> {
focalLengthFormatted?: string
focalLengthIn35MmFormatFormatted?: string
fNumberFormatted?: string
@ -108,6 +107,7 @@ export interface Photo extends PhotoDb {
exposureTimeFormatted?: string
exposureCompensationFormatted?: string
takenAtNaiveFormatted: string
fujifilmRecipe?: FujifilmRecipe
}
export interface PhotoSetCategory {
@ -143,6 +143,9 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
formatExposureTime(photoDb.exposureTime),
exposureCompensationFormatted:
formatExposureCompensation(photoDb.exposureCompensation),
fujifilmRecipe: photoDb.fujifilmRecipe
? JSON.parse(photoDb.fujifilmRecipe)
: undefined,
takenAtNaiveFormatted:
formatDateFromPostgresString(photoDb.takenAtNaive),
};

View File

@ -51,7 +51,7 @@ export const extractImageDataFromBlobPath = async (
let exifData: ExifData | undefined;
let filmSimulation: FilmSimulation | undefined;
let recipe: Partial<FujifilmRecipe> | undefined;
let recipe: FujifilmRecipe | undefined;
let blurData: string | undefined;
let imageResizedBase64: string | undefined;
let shouldStripGpsData = false;

View File

@ -5,7 +5,7 @@ const TAG_ID_HIGHLIGHT = 0x1041;
const TAG_ID_SHADOW = 0x1040;
const TAG_ID_SATURATION = 0x1003;
const TAG_ID_NOISE_REDUCTION = 0x100e;
const TAG_ID_NOISE_REDUCTION_LEGACY = 0x100b;
const TAG_ID_NOISE_REDUCTION_BASIC = 0x100b;
const TAG_ID_SHARPNESS = 0x1001;
const TAG_ID_CLARITY = 0x100f;
const TAG_ID_GRAIN_EFFECT_ROUGHNESS = 0x1047;
@ -19,7 +19,7 @@ const TAG_ID_BW_MAGENTA_GREEN = 0x104b;
type WeakStrong = 'off' | 'weak' | 'strong';
export interface FujifilmRecipe {
export type FujifilmRecipe = Partial<{
dynamicRange: number
highlight: number
shadow: number
@ -41,7 +41,7 @@ export interface FujifilmRecipe {
}
bwAdjustment: number
bwMagentaGreen: number
}
}>;
const DEFAULT_GRAIN_EFFECT = {
roughness: 'off',
@ -75,7 +75,7 @@ export const processNoiseReductionLegacy = (value: number) => {
switch (value) {
case 0x40: return 'low';
case 0x80: return 'normal';
default: return 'n/a';
default: return 'n/a';
}
};
@ -119,7 +119,7 @@ export const processWeakStrong = (value: number): WeakStrong => {
export const processGrainEffectSize = (
value: number,
): FujifilmRecipe['grainEffect']['size'] => {
): Required<FujifilmRecipe>['grainEffect']['size'] => {
switch (value) {
case 16: return 'small';
case 32: return 'large';
@ -155,8 +155,8 @@ export const processWhiteBalanceComponent = (value: number) => value / 20;
export const getFujifilmRecipeFromMakerNote = (
bytes: Buffer,
): Partial<FujifilmRecipe> => {
const recipe: Partial<FujifilmRecipe> = {};
): FujifilmRecipe => {
const recipe: FujifilmRecipe = {};
parseFujifilmMakerNote(
bytes,
@ -177,7 +177,7 @@ export const getFujifilmRecipeFromMakerNote = (
case TAG_ID_NOISE_REDUCTION:
recipe.highISONoiseReduction = processNoiseReduction(numbers[0]);
break;
case TAG_ID_NOISE_REDUCTION_LEGACY:
case TAG_ID_NOISE_REDUCTION_BASIC:
recipe.noiseReductionLegacy =
processNoiseReductionLegacy(numbers[0]);
break;