Introduce fujifilm simulations dropdown

This commit is contained in:
Sam Becker 2023-10-27 20:48:02 -07:00
parent fdc35beff1
commit 00bffcf4fc
5 changed files with 176 additions and 119 deletions

View File

@ -11,6 +11,8 @@ export default function FieldSetWithStatus({
note,
value,
onChange,
selectOptions,
selectOptionsDefaultLabel,
placeholder,
loading,
required,
@ -23,6 +25,8 @@ export default function FieldSetWithStatus({
note?: string
value: string
onChange?: (value: string) => void
selectOptions?: { value: string, label: string }[]
selectOptionsDefaultLabel?: string
placeholder?: string
loading?: boolean
required?: boolean
@ -52,21 +56,43 @@ export default function FieldSetWithStatus({
<Spinner />
</span>}
</label>
<input
ref={inputRef}
id={id}
name={id}
value={value}
checked={type === 'checkbox' ? value === 'true' : undefined}
placeholder={placeholder}
onChange={e => onChange?.(type === 'checkbox'
? e.target.value === 'true' ? 'false' : 'true'
: e.target.value)}
type={type}
autoComplete="off"
readOnly={readOnly || pending}
className={cc(type === 'text' && 'w-full')}
/>
{selectOptions
? <select
id={id}
name={id}
value={value}
onChange={e => onChange?.(e.target.value)}
className={cc(
'w-full',
// Use special class because `select` can't be readonly
readOnly || pending && 'disabled-select',
)}
>
{selectOptionsDefaultLabel &&
<option value="">{selectOptionsDefaultLabel}</option>}
{selectOptions.map(({ value: optionValue, label: optionLabel }) =>
<option
key={optionValue}
value={optionValue}
>
{optionLabel}
</option>)}
</select>
: <input
ref={inputRef}
id={id}
name={id}
value={value}
checked={type === 'checkbox' ? value === 'true' : undefined}
placeholder={placeholder}
onChange={e => onChange?.(type === 'checkbox'
? e.target.value === 'true' ? 'false' : 'true'
: e.target.value)}
type={type}
autoComplete="off"
readOnly={readOnly || pending}
className={cc(type === 'text' && 'w-full')}
/>}
</div>
);
};

View File

@ -96,6 +96,8 @@ export default function PhotoForm({
label,
note,
required,
options,
optionsDefaultLabel,
readOnly,
hideIfEmpty,
hideBasedOnCamera,
@ -113,6 +115,8 @@ export default function PhotoForm({
note={note}
value={formData[key] ?? ''}
onChange={value => setFormData({ ...formData, [key]: value })}
selectOptions={options}
selectOptionsDefaultLabel={optionsDefaultLabel}
required={required}
readOnly={readOnly}
placeholder={loadingMessage && !formData[key]

View File

@ -8,7 +8,10 @@ import { getOffsetFromExif } from '@/utility/exif';
import { toFixedNumber } from '@/utility/number';
import { convertStringToArray } from '@/utility/string';
import { generateNanoid } from '@/utility/nanoid';
import { FujifilmSimulation } from '@/vendors/fujifilm';
import {
FILM_SIMULATION_FORM_INPUT_OPTIONS,
FujifilmSimulation,
} from '@/vendors/fujifilm';
export type PhotoFormData = Record<keyof PhotoDbInsert, string>;
@ -22,14 +25,20 @@ type FormMeta = {
hideBasedOnCamera?: (make?: string, mode?: string) => boolean
loadingMessage?: string
checkbox?: boolean
options?: { value: string, label: string }[]
optionsDefaultLabel?: string
};
const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = {
title: { label: 'title' },
tags: { label: 'tags', note: 'comma-separated values' },
id: { label: 'id', readOnly: true, hideIfEmpty: true },
// eslint-disable-next-line max-len
blurData: { label: 'blur data', readOnly: true, required: true, loadingMessage: 'Generating blur data ...' },
blurData: {
label: 'blur data',
readOnly: true,
required: true,
loadingMessage: 'Generating blur data ...',
},
url: { label: 'url', readOnly: true },
extension: { label: 'extension', readOnly: true },
aspectRatio: { label: 'aspect ratio', readOnly: true },
@ -37,7 +46,8 @@ const FORM_METADATA: Record<keyof PhotoFormData, FormMeta> = {
model: { label: 'camera model' },
filmSimulation: {
label: 'fujifilm simulation',
readOnly: true,
options: FILM_SIMULATION_FORM_INPUT_OPTIONS,
optionsDefaultLabel: 'Unknown',
hideBasedOnCamera: make => make !== 'FUJIFILM',
},
focalLength: { label: 'focal length' },

View File

@ -18,22 +18,32 @@
tracking-wider
}
button, .button,
input[type=text], input[type=email], input[type=password] {
input[type=text], input[type=email], input[type=password], select {
@apply
px-2 py-1.5
px-2.5 py-2
border rounded-md
bg-white dark:bg-black
border-gray-200 dark:border-gray-700
font-mono text-base leading-none
font-mono text-base leading-tight
min-h-[2.25rem]
}
input[type=text], input[type=email], input[type=password] {
input[type=text], input[type=email], input[type=password], select {
@apply
text-[1rem] /* Prevent iOS auto-zoom behavior */
min-w-[20rem] read-only:cursor-default
}
input[type=text], input[type=email], input[type=password] {
@apply
read-only:bg-gray-100
dark:read-only:bg-gray-900 dark:read-only:text-gray-400
}
/* Required for readonly behavior on <select /> */
.disabled-select {
@apply
bg-gray-100
dark:bg-gray-900 dark:text-gray-400
pointer-events-none
}
input[type=file] {
@apply
block font-mono w-full text-gray-500 dark:text-gray-400

View File

@ -1,5 +1,5 @@
// MakerNote tag IDs and values referenced from:
// exiftool/lib/Image/ExifTool/Fujifilm.pm
// github.com/exiftool/exiftool/lib/Image/ExifTool/Fujifilm.pm
import type { ExifData } from 'ts-exif-parser';
@ -13,32 +13,32 @@ const TAG_ID_SATURATION = 0x1003;
const TAG_ID_FILM_MODE = 0x1401;
type FujifilmSimulationFromSaturation =
'Monochrome' |
'Monochrome + Ye' |
'Monochrome + R' |
'Monochrome + G' |
'Sepia' |
'Acros' |
'Acros + Ye' |
'Acros + R' |
'Acros + G';
'monochrome' |
'monochrome-ye' |
'monochrome-r' |
'monochrome-g' |
'sepia' |
'acros' |
'acros-ye' |
'acros-r' |
'acros-g';
type FujifilmMode =
'Provia' |
'Portrait' |
'Portrait Saturation' |
'Portrait Skin Tone' |
'Portrait Sharpness' |
'Portrait Ex' |
'Velvia' |
'Pro Neg. Std' |
'Pro Neg. Hi' |
'Classic Chrome' |
'Eterna' |
'Classic Neg.' |
'Eterna Bleach Bypass' |
'Nostalgic Neg.' |
'Reala';
'provia' |
'portrait' |
'portrait-saturation' |
'portrait-skin-tone' |
'portrait-sharpness' |
'portrait-ex' |
'velvia' |
'pro-neg-std' |
'pro-neg-hi' |
'classic-chrome' |
'eterna' |
'classic-neg' |
'eterna-bleach-bypass' |
'nostalgic-neg' |
'reala';
export type FujifilmSimulation =
FujifilmSimulationFromSaturation |
@ -47,6 +47,84 @@ export type FujifilmSimulation =
export const isExifForFujifilm = (data: ExifData) =>
data.tags?.Make === MAKE_FUJIFILM;
const getFujifilmSimulationFromSaturation = (
value?: number,
): FujifilmSimulationFromSaturation | undefined => {
switch (value) {
case 0x300: return 'monochrome';
case 0x301: return 'monochrome-r';
case 0x302: return 'monochrome-ye';
case 0x303: return 'monochrome-g';
case 0x310: return 'sepia';
case 0x500: return 'acros';
case 0x501: return 'acros-r';
case 0x502: return 'acros-ye';
case 0x503: return 'acros-g';
}
};
const getFujifilmMode = (
value?: number,
): FujifilmMode | undefined => {
switch (value) {
case 0x000: return 'provia';
case 0x100: return 'portrait';
case 0x110: return 'portrait-saturation';
case 0x120: return 'portrait-skin-tone';
case 0x130: return 'portrait-sharpness';
case 0x300: return 'portrait-ex';
case 0x200:
case 0x400: return 'velvia';
case 0x500: return 'pro-neg-std';
case 0x501: return 'pro-neg-hi';
case 0x600: return 'classic-chrome';
case 0x700: return 'eterna';
case 0x800: return 'classic-neg';
case 0x900: return 'eterna-bleach-bypass';
case 0xa00: return 'nostalgic-neg';
case 0xb00: return 'reala';
}
};
const FILM_SIMULATION_LABELS: Record<FujifilmSimulation, string> = {
'monochrome': 'Monochrome',
'monochrome-ye': 'Monochrome + Yellow Filter',
'monochrome-r': 'Monochrome + Red Filter',
'monochrome-g': 'Monochrome + Green Filter',
'sepia': 'Sepia',
'acros': 'ACROS',
'acros-ye': 'ACROS + Yellow Filter',
'acros-r': 'ACROS + Red Filter',
'acros-g': 'ACROS + Green Filter',
'provia': 'PROVIA / Standard',
'portrait': 'Studio Portrait',
'portrait-saturation': 'Studio Portrait + Enhanced Saturation',
'portrait-skin-tone': 'ASTIA / Soft',
'portrait-sharpness': 'Studio Portrait + Enhanced Sharpness',
'portrait-ex': 'Studio Portrait Ex',
'velvia': 'Velvia / Vivid',
'pro-neg-std': 'PRO Neg. Std',
'pro-neg-hi': 'PRO Neg. Hi',
'classic-chrome': 'Classic Chrome',
'eterna': 'ETERNA / Cinema',
'classic-neg': 'Classic Neg.',
'eterna-bleach-bypass': 'ETERNA Bleach Bypass',
'nostalgic-neg': 'Nostalgic Neg.',
'reala': 'REALA ACE',
};
export const FILM_SIMULATION_FORM_INPUT_OPTIONS = Object
.entries(FILM_SIMULATION_LABELS)
.map(([value, label]) => (
{ value, label } as { value: FujifilmSimulation, label: string }
))
.sort((a, b) => a.label.localeCompare(b.label));
export const getLabelForFilmSimulation = (
simulation: FujifilmSimulation
): string =>
FILM_SIMULATION_LABELS[simulation];
const parseFujifilmMakerNote = (
bytes: Buffer,
valueForTag: (tag: number, value: number) => void
@ -62,77 +140,6 @@ const parseFujifilmMakerNote = (
}
};
const getFujifilmSimulationFromSaturation = (
value?: number,
): FujifilmSimulationFromSaturation | undefined => {
switch (value) {
case 0x300: return 'Monochrome';
case 0x301: return 'Monochrome + R';
case 0x302: return 'Monochrome + Ye';
case 0x303: return 'Monochrome + G';
case 0x310: return 'Sepia';
case 0x500: return 'Acros';
case 0x501: return 'Acros + R';
case 0x502: return 'Acros + Ye';
case 0x503: return 'Acros + G';
}
};
const getFujifilmMode = (
value?: number,
): FujifilmMode | undefined => {
switch (value) {
case 0x000: return 'Provia';
case 0x100: return 'Portrait';
case 0x110: return 'Portrait Saturation';
case 0x120: return 'Portrait Skin Tone';
case 0x130: return 'Portrait Sharpness';
case 0x300: return 'Portrait Ex';
case 0x200:
case 0x400: return 'Velvia';
case 0x500: return 'Pro Neg. Std';
case 0x501: return 'Pro Neg. Hi';
case 0x600: return 'Classic Chrome';
case 0x700: return 'Eterna';
case 0x800: return 'Classic Neg.';
case 0x900: return 'Eterna Bleach Bypass';
case 0xa00: return 'Nostalgic Neg.';
case 0xb00: return 'Reala';
}
};
const LABEL_FOR_FILM_SIMULATION: Record<FujifilmSimulation, string> = {
'Monochrome': 'Monochrome',
'Monochrome + Ye': 'Monochrome + Yellow Filter',
'Monochrome + R': 'Monochrome + Red Filter',
'Monochrome + G': 'Monochrome + Green Filter',
'Sepia': 'Sepia',
'Acros': 'ACROS',
'Acros + Ye': 'ACROS + Yellow Filter',
'Acros + R': 'ACROS + Red Filter',
'Acros + G': 'ACROS + Green Filter',
'Provia': 'PROVIA / Standard',
'Portrait': 'Studio Portrait',
'Portrait Saturation': 'Studio Portrait + Enhanced Saturation',
'Portrait Skin Tone': 'ASTIA / Soft',
'Portrait Sharpness': 'Studio Portrait + Enhanced Sharpness',
'Portrait Ex': 'Studio Portrait Ex',
'Velvia': 'Velvia / Vivid',
'Pro Neg. Std': 'PRO Neg. Std',
'Pro Neg. Hi': 'PRO Neg. Hi',
'Classic Chrome': 'Classic Chrome',
'Eterna': 'ETERNA / Cinema',
'Classic Neg.': 'Classic Neg.',
'Eterna Bleach Bypass': 'ETERNA Bleach Bypass',
'Nostalgic Neg.': 'Nostalgic Neg.',
'Reala': 'REALA ACE',
};
export const getLabelForFilmSimulation = (
simulation: FujifilmSimulation
): string =>
LABEL_FOR_FILM_SIMULATION[simulation];
export const getFujifilmSimulationFromMakerNote = (
bytes: Buffer,
): FujifilmSimulation | undefined => {