Introduce fujifilm simulations dropdown
This commit is contained in:
parent
fdc35beff1
commit
00bffcf4fc
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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' },
|
||||
|
||||
@ -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
|
||||
|
||||
199
src/vendors/fujifilm/index.ts
vendored
199
src/vendors/fujifilm/index.ts
vendored
@ -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 => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user