UNPKG

fuji-recipes

Version:

A library to extract film simulation data from EXIF

327 lines (320 loc) 9.94 kB
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 };