fuji-recipes
Version:
A library to extract film simulation data from EXIF
327 lines (320 loc) • 9.94 kB
JavaScript
import { Buffer } from 'buffer';
// https://exiftool.org/TagNames/FujiFilm.html
const TAG_ID_FILM_MODE = 0x1401;
const FILM_MODE_VALUES = {
0x0: 'F0/Standard (Provia)',
0x100: 'F1/Studio Portrait',
0x110: 'F1a/Studio Portrait Enhanced Saturation',
0x120: 'F1b/Studio Portrait Smooth Skin Tone (Astia)',
0x130: 'F1c/Studio Portrait Increased Sharpness',
0x200: 'F2/Fujichrome (Velvia)',
0x300: 'F3/Studio Portrait Ex',
0x400: 'F4/Velvia',
0x500: 'Pro Neg. Std',
0x501: 'Pro Neg. Hi',
0x600: 'Classic Chrome',
0x700: 'Eterna',
0x800: 'Classic Negative',
0x900: 'Bleach Bypass',
0xA00: 'Nostalgic Neg',
0xB00: 'Reala ACE'
};
const TAG_ID_GRAIN_EFFECT_ROUGHNESS = 0x1047;
const GRAIN_EFFECT_ROUGHNESS_VALUES = {
0x0: 'Off',
0x20: 'Weak',
0x40: 'Strong'
};
const TAG_ID_GRAIN_EFFECT_SIZE = 0x104C;
const GRAIN_EFFECT_SIZE_VALUES = {
0x0: 'Off',
0x10: 'Small',
0x20: 'Large'
};
const TAG_ID_COLOR_CHROME_EFFECT = 0x1048;
const COLOR_CHROME_EFFECT_VALUES = {
0x0: 'Off',
0x20: 'Weak',
0x40: 'Strong'
};
const TAG_ID_COLOR_CHROME_FX_BLUE = 0x104E;
const COLOR_CHROME_FX_BLUE_VALUES = {
0x0: 'Off',
0x20: 'Weak',
0x40: 'Strong'
};
const TAG_ID_WHITE_BALANCE = 0x1002;
const WHITE_BALANCE_VALUES = {
0x0: 'Auto',
0x1: 'Auto (white priority)',
0x2: 'Auto (ambiance priority)',
0x100: 'Daylight',
0x200: 'Cloudy',
0x300: 'Daylight Fluorescent',
0x301: 'Day White Fluorescent',
0x302: 'White Fluorescent',
0x303: 'Warm White Fluorescent',
0x304: 'Living Room Warm White Fluorescent',
0x400: 'Incandescent',
0x500: 'Flash',
0x600: 'Underwater',
0xF00: 'Custom',
0xF01: 'Custom2',
0xF02: 'Custom3',
0xF03: 'Custom4',
0xF04: 'Custom5',
0xFF0: 'Kelvin'
};
const TAG_ID_COLOR_TEMPERATURE = 0x1005;
const TAG_ID_WHITE_BALANCE_FINE_TUNE = 0x100A;
const TAG_ID_DYNAMIC_RANGE_SETTING = 0x1402;
const DYNAMIC_RANGE_SETTING_VALUES = {
0x0: 'Auto',
0x1: 'Manual',
0x100: 'Standard (100%)',
0x200: 'Wide1 (230%)',
0x201: 'Wide2 (400%)',
0x8000: 'Film Simulation'
};
const TAG_ID_DEVELOPMENT_DYNAMIC_RANGE = 0x1403;
const TAG_ID_HIGHLIGHT_TONE = 0x1041;
const TONE_VALUES = {
'-64': '+4',
'-48': '+3',
'-32': '+2',
'-16': '+1',
'0': '0',
'16': '-1',
'32': '-2'
};
const TAG_ID_SHADOW_TONE = 0x1040;
const TAG_ID_SATURATION = 0x1003;
const SATURATION_VALUES = {
0x0: '0',
0x80: '+1',
0xC0: '+3',
0xE0: '+4',
0x100: '+2',
0x180: '-1',
0x400: '-2',
0x4C0: '-3',
0x4E0: '-4',
0x200: 'Low',
0x300: 'None (B&W)',
0x301: 'B&W Red Filter',
0x302: 'B&W Yellow Filter',
0x303: 'B&W Green Filter',
0x310: 'B&W Sepia',
0x500: 'Acros',
0x501: 'Acros Red Filter',
0x502: 'Acros Yellow Filter',
0x503: 'Acros Green Filter',
0x8000: 'Film Simulation'
};
const TAG_ID_SHARPNESS = 0x1001;
const SHARPNESS_VALUES = {
0x0: '-4',
0x1: '-3',
0x2: '-2',
0x3: '0',
0x4: '+2',
0x5: '+3',
0x6: '+4',
0x82: '-1',
0x84: '+1',
0x8000: 'Film Simulation',
0xFFFF: 'n/a'
};
const TAG_ID_NOISE_REDUCTION = 0x100E;
const NOISE_REDUCTION_VALUES = {
0x0: '0',
0x100: '+2',
0x180: '+1',
0x1C0: '+3',
0x1E0: '+4',
0x200: '-2',
0x280: '-1',
0x2C0: '-3',
0x2E0: '-4'
};
const TAG_ID_CLARITY = 0x100F;
const CLARITY_VALUES = {
'-5000': '-5',
'-4000': '-4',
'-3000': '-3',
'-2000': '-2',
'-1000': '-1',
'0': '0',
'1000': '+1',
'2000': '+2',
'3000': '+3'
};
// eslint-disable-next-line unicorn/prefer-node-protocol
const tags = {
[TAG_ID_FILM_MODE]: {
name: 'FilmMode',
parser: (input)=>FILM_MODE_VALUES[input]
},
[TAG_ID_GRAIN_EFFECT_ROUGHNESS]: {
name: 'GrainEffectRoughness',
parser: (input)=>GRAIN_EFFECT_ROUGHNESS_VALUES[input]
},
[TAG_ID_GRAIN_EFFECT_SIZE]: {
name: 'GrainEffectSize',
parser: (input)=>GRAIN_EFFECT_SIZE_VALUES[input]
},
[TAG_ID_COLOR_CHROME_EFFECT]: {
name: 'ColorChromeEffect',
parser: (input)=>COLOR_CHROME_EFFECT_VALUES[input]
},
[TAG_ID_COLOR_CHROME_FX_BLUE]: {
name: 'ColorChromeFxBlue',
parser: (input)=>COLOR_CHROME_FX_BLUE_VALUES[input]
},
[TAG_ID_WHITE_BALANCE]: {
name: 'WhiteBalance',
parser: (input)=>WHITE_BALANCE_VALUES[input]
},
[TAG_ID_COLOR_TEMPERATURE]: {
name: 'ColorTemperature',
parser: (input)=>input
},
[TAG_ID_WHITE_BALANCE_FINE_TUNE]: {
name: 'WhiteBalanceFineTune',
parser: (input, buffer)=>{
return [
buffer.readInt32LE(input),
buffer.readInt32LE(input + 4)
];
}
},
[TAG_ID_DYNAMIC_RANGE_SETTING]: {
name: 'DynamicRangeSetting',
parser: (input)=>DYNAMIC_RANGE_SETTING_VALUES[input]
},
[TAG_ID_DEVELOPMENT_DYNAMIC_RANGE]: {
name: 'DevelopmentDynamicRange',
parser: (input)=>input
},
[TAG_ID_HIGHLIGHT_TONE]: {
name: 'HighlightTone',
parser: (input)=>TONE_VALUES[input]
},
[TAG_ID_SHADOW_TONE]: {
name: 'ShadowTone',
parser: (input)=>TONE_VALUES[input]
},
[TAG_ID_SATURATION]: {
name: 'Saturation',
parser: (input)=>SATURATION_VALUES[input]
},
[TAG_ID_SHARPNESS]: {
name: 'Sharpness',
parser: (input)=>SHARPNESS_VALUES[input]
},
[TAG_ID_NOISE_REDUCTION]: {
name: 'NoiseReduction',
parser: (input)=>NOISE_REDUCTION_VALUES[input]
},
[TAG_ID_CLARITY]: {
name: 'Clarity',
parser: (input)=>CLARITY_VALUES[input]
}
};
function format(parsed) {
if (!parsed.FilmMode) {
return null;
}
const result = {
[tags[TAG_ID_FILM_MODE].name]: parsed.FilmMode,
[tags[TAG_ID_GRAIN_EFFECT_ROUGHNESS].name]: parsed.GrainEffectRoughness,
[tags[TAG_ID_GRAIN_EFFECT_SIZE].name]: parsed.GrainEffectSize,
[tags[TAG_ID_COLOR_CHROME_EFFECT].name]: parsed.ColorChromeEffect,
[tags[TAG_ID_COLOR_CHROME_FX_BLUE].name]: parsed.ColorChromeFxBlue,
[tags[TAG_ID_WHITE_BALANCE].name]: parsed.WhiteBalance,
Red: '0',
Blue: '0',
DynamicRange: parsed.DynamicRangeSetting,
[tags[TAG_ID_HIGHLIGHT_TONE].name]: parsed.HighlightTone,
[tags[TAG_ID_SHADOW_TONE].name]: parsed.ShadowTone,
[tags[TAG_ID_SATURATION].name]: parsed.Saturation,
[tags[TAG_ID_SHARPNESS].name]: parsed.Sharpness,
[tags[TAG_ID_NOISE_REDUCTION].name]: parsed.NoiseReduction,
[tags[TAG_ID_CLARITY].name]: parsed.Clarity
};
const whiteBalanceFineTuneName = tags[TAG_ID_WHITE_BALANCE_FINE_TUNE].name;
const whiteBalanceFineTune = parsed[whiteBalanceFineTuneName];
if (whiteBalanceFineTune) {
result.Red = formatWhiteBalanceFineTune(whiteBalanceFineTune[0]);
result.Blue = formatWhiteBalanceFineTune(whiteBalanceFineTune[1]);
}
const whiteBalanceName = tags[TAG_ID_WHITE_BALANCE].name;
const whiteBalance = parsed[whiteBalanceName];
const colorTemperatureName = tags[TAG_ID_COLOR_TEMPERATURE].name;
const colorTemperature = parsed[colorTemperatureName];
if (whiteBalance === WHITE_BALANCE_VALUES['4080']) {
result.WhiteBalance = `${colorTemperature}K`;
}
const dynamicRangeSettingName = tags[TAG_ID_DYNAMIC_RANGE_SETTING].name;
const dynamicRangeSetting = parsed[dynamicRangeSettingName];
const developmentRangeName = tags[TAG_ID_DEVELOPMENT_DYNAMIC_RANGE].name;
const developmentRange = parsed[developmentRangeName];
if (dynamicRangeSetting === DYNAMIC_RANGE_SETTING_VALUES[1]) {
result.DynamicRange = `DR${developmentRange}`;
}
return result;
}
function formatWhiteBalanceFineTune(value) {
return value > 0 ? `+${value / 10}` : `${value / 10}`;
}
// eslint-disable-next-line unicorn/prefer-node-protocol
function parse(makerNote) {
const result = {};
parseFujifilmMakerNote(makerNote, (tagId, value)=>{
const tag = tags[tagId];
if (tag) {
const parsedValue = tag.parser(value, makerNote);
result[tag.name] = parsedValue;
}
});
return result;
}
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;
function parseFujifilmMakerNote(bytes, valueForTagUInt) {
const tagCount = bytes.readUInt16LE(BYTE_INDEX_TAG_COUNT);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
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 = view.getUint16(index, true);
const tagType = view.getUint16(index + BYTE_OFFSET_TAG_TYPE, true);
switch(tagType){
// UInt16
case 3:
valueForTagUInt(tagId, view.getUint16(index + BYTE_OFFSET_TAG_VALUE, true));
break;
// UInt32
case 4:
valueForTagUInt(tagId, view.getUint32(index + BYTE_OFFSET_TAG_VALUE, true));
break;
// Int32
case 9:
valueForTagUInt(tagId, view.getInt32(index + BYTE_OFFSET_TAG_VALUE, true));
break;
}
}
}
}
// eslint-disable-next-line unicorn/prefer-node-protocol
/**
* Get the recipe from a Fujifilm maker note.
* @param makerNote - The maker note as a buffer.
*/ function getRecipe(makerNote) {
const buffer = Buffer.isBuffer(makerNote) ? makerNote : Buffer.from(makerNote);
return format(parse(buffer));
}
export { getRecipe as default, getRecipe };