286 lines
6.6 KiB
TypeScript
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;
|
|
};
|