Vercel/src/vendors/fujifilm/index.ts
2023-11-06 10:07:07 -06:00

286 lines
6.6 KiB
TypeScript

// MakerNote tag IDs and values referenced from:
// github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/FujiFilm.pm
import type { ExifData } from 'ts-exif-parser';
export const MAKE_FUJIFILM = 'FUJIFILM';
const BYTE_INDEX_TAG_COUNT = 12;
const BYTE_INDEX_FIRST_TAG = 14;
const BYTES_PER_TAG = 12;
const BYTE_OFFSET_TAG_TYPE = 2;
const BYTE_OFFSET_TAG_VALUE = 8;
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';
type FujifilmMode =
'provia' |
'portrait' |
'portrait-saturation' |
'astia' |
'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 |
FujifilmMode;
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 'astia'; // can be encoded as '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';
}
};
interface FujifilmSimulationLabel {
small: string
medium: string
large: string
}
const FILM_SIMULATION_LABELS: Record<
FujifilmSimulation,
FujifilmSimulationLabel
> = {
'monochrome': {
small: 'Monochrome',
medium: 'Monochrome',
large: 'Monochrome',
},
'monochrome-ye': {
small: 'Monochrome+Ye',
medium: 'Monochrome+Ye',
large: 'Monochrome + Yellow Filter',
},
'monochrome-r': {
small: 'Monochrome+R',
medium: 'Monochrome+R',
large: 'Monochrome + Red Filter',
},
'monochrome-g': {
small: 'Monochrome+G',
medium: 'Monochrome+G',
large: 'Monochrome + Green Filter',
},
'sepia': {
small: 'Sepia',
medium: 'Sepia',
large: 'Sepia',
},
'acros': {
small: 'ACROS',
medium: 'ACROS',
large: 'ACROS',
},
'acros-ye': {
small: 'ACROS+Ye',
medium: 'ACROS+Ye',
large: 'ACROS + Yellow Filter',
},
'acros-r': {
small: 'ACROS+R',
medium: 'ACROS+R',
large: 'ACROS + Red Filter',
},
'acros-g': {
small: 'ACROS+G',
medium: 'ACROS+G',
large: 'ACROS + Green Filter',
},
'provia': {
small: 'PROVIA',
medium: 'PROVIA/Std',
large: 'PROVIA / Standard',
},
'portrait': {
small: 'Portrait',
medium: 'Portrait',
large: 'Studio Portrait',
},
'portrait-saturation': {
small: 'Portrait+Sat.',
medium: 'Portrait+Sat.',
large: 'Studio Portrait + Enhanced Saturation',
},
'astia': {
small: 'ASTIA',
medium: 'ASTIA/Soft',
large: 'ASTIA / Soft',
},
'portrait-sharpness': {
small: 'Portrait+Sharp.',
medium: 'Portrait+Sharp.',
large: 'Studio Portrait + Enhanced Sharpness',
},
'portrait-ex': {
small: 'Portrait+Ex',
medium: 'Portrait+Ex',
large: 'Studio Portrait + Ex',
},
'velvia': {
small: 'Velvia',
medium: 'Velvia/Vivid',
large: 'Velvia / Vivid',
},
'pro-neg-std': {
small: 'PRO Neg. Std',
medium: 'PRO Neg. Std',
large: 'PRO Neg. Std',
},
'pro-neg-hi': {
small: 'PRO Neg. Hi',
medium: 'PRO Neg. Hi',
large: 'PRO Neg. Hi',
},
'classic-chrome': {
small: 'Classic Chrome',
medium: 'Classic Chrome',
large: 'Classic Chrome',
},
'eterna': {
small: 'ETERNA',
medium: 'ETERNA/Cinema',
large: 'ETERNA / Cinema',
},
'classic-neg': {
small: 'Classic Neg.',
medium: 'Classic Neg.',
large: 'Classic Neg.',
},
'eterna-bleach-bypass': {
small: 'ETERNA Bypass',
medium: 'ETERNA Bypass',
large: 'ETERNA Bleach Bypass',
},
'nostalgic-neg': {
small: 'Nostalgic Neg.',
medium: 'Nostalgic Neg.',
large: 'Nostalgic Neg.',
},
'reala': {
small: 'REALA',
medium: 'REALA ACE',
large: 'REALA ACE',
},
};
export const FILM_SIMULATION_FORM_INPUT_OPTIONS = Object
.entries(FILM_SIMULATION_LABELS)
.map(([value, label]) => (
{ value, label: label.large } as
{ value: FujifilmSimulation, label: string }
))
.sort((a, b) => a.label.localeCompare(b.label));
export const labelForFilmSimulation = (simulation: FujifilmSimulation) =>
FILM_SIMULATION_LABELS[simulation];
const parseFujifilmMakerNote = (
bytes: Buffer,
valueForTagUInt: (tagId: number, value: number) => void
) => {
const tagCount = bytes.readUint16LE(BYTE_INDEX_TAG_COUNT);
for (let i = 0; i < tagCount; i++) {
const index = BYTE_INDEX_FIRST_TAG + i * BYTES_PER_TAG;
if (index + BYTES_PER_TAG < bytes.length) {
const tagId = bytes.readUInt16LE(index);
const tagType = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_TYPE);
switch (tagType) {
// UInt16
case 3:
valueForTagUInt(
tagId,
bytes.readUInt16LE(index + BYTE_OFFSET_TAG_VALUE),
);
break;
// UInt32
case 4:
valueForTagUInt(
tagId,
bytes.readUInt32LE(index + BYTE_OFFSET_TAG_VALUE),
);
break;
}
}
}
};
export const getFujifilmSimulationFromMakerNote = (
bytes: Buffer,
): FujifilmSimulation | undefined => {
let filmModeFromSaturation: FujifilmSimulationFromSaturation | undefined;
let filmMode: FujifilmMode | undefined;
parseFujifilmMakerNote(
bytes,
(tag, value) => {
switch (tag) {
case TAG_ID_SATURATION:
filmModeFromSaturation = getFujifilmSimulationFromSaturation(value);
break;
case TAG_ID_FILM_MODE:
filmMode = getFujifilmMode(value);
break;
}
},
);
return filmModeFromSaturation ?? filmMode;
};