1// SPDX-License-Identifier: AGPL-3.0-or-later2// Fujifilm RAF container: extract the embedded in-camera JPEG and read what the camera applied.3const MAGIC='FUJIFILMCCD-RAW ';4export function parseRaf(buffer){5 const bytes=new Uint8Array(buffer),view=new DataView(buffer);6 if(bytes.length<112||String.fromCharCode(...bytes.subarray(0,16))!==MAGIC)throw Error('Not a Fujifilm RAF file.');7 const model=String.fromCharCode(...bytes.subarray(28,60)).replace(/\0+$/,'').trim();8 const offset=view.getUint32(84),length=view.getUint32(88);9 if(offset+length>bytes.length||length<4||bytes[offset]!==0xFF||bytes[offset+1]!==0xD8)throw Error('No embedded JPEG found in this RAF.');10 const jpeg=bytes.slice(offset,offset+length);11 return {model,jpeg,...readExif(jpeg)};12}13// Fuji makernote tags (as documented by ExifTool).14const FILM={0:'Provia',0x100:'Studio Portrait',0x110:'Studio Portrait Enhanced',0x120:'Astia',0x130:'Studio Portrait Sharp',0x200:'Velvia',0x300:'Studio Portrait Ex',0x400:'Velvia',0x500:'Pro Neg. Std',0x501:'Pro Neg. Hi',0x600:'Classic Chrome',0x700:'Eterna',0x800:'Classic Neg.',0x900:'Eterna Bleach Bypass',0xa00:'Nostalgic Neg.',0xb00:'Reala Ace'};15const MONO={0x300:'Monochrome',0x301:'Monochrome+R',0x302:'Monochrome+Ye',0x303:'Monochrome+G',0x310:'Sepia',0x500:'Acros',0x501:'Acros+R',0x502:'Acros+Ye',0x503:'Acros+G'};16function readExif(jpeg){17 const out={simulation:'',camera:'',width:0,height:0,date:'',cropMode:0};18 try{19 const v=new DataView(jpeg.buffer,jpeg.byteOffset,jpeg.byteLength);20 let p=2;21 while(p+4<=jpeg.length&&jpeg[p]===0xFF){22 const marker=jpeg[p+1],len=v.getUint16(p+2);23 if(marker===0xE1&&String.fromCharCode(...jpeg.subarray(p+4,p+10))==='Exif\0\0'){parseTiff(jpeg.subarray(p+10,p+2+len),out);}24 if(marker>=0xC0&&marker<=0xC3){out.height=v.getUint16(p+5);out.width=v.getUint16(p+7);}25 if(marker===0xDA)break;26 p+=2+len;27 }28 }catch{}29 return out;30}31function parseTiff(t,out){32 const v=new DataView(t.buffer,t.byteOffset,t.byteLength),le=t[0]===0x49;33 const u16=o=>v.getUint16(o,le),u32=o=>v.getUint32(o,le);34 const str=(o,n)=>String.fromCharCode(...t.subarray(o,o+n)).replace(/\0+$/,'').trim();35 const ifd=(o,cb)=>{const n=u16(o);for(let i=0;i<n;i++){const e=o+2+i*12,tag=u16(e),type=u16(e+2),count=u32(e+4);const size=[0,1,1,2,4,8,1,1,2,4,8,4,8][type]*count;const val=size<=4?e+8:u32(e+8);cb(tag,type,count,val);}};36 let exifOffset=0;37 ifd(u32(4),(tag,type,count,val)=>{if(tag===0x110)out.camera=str(val,count);if(tag===0x8769)exifOffset=u32(val);});38 if(!exifOffset)return;39 let maker=0,makerLen=0;40 ifd(exifOffset,(tag,type,count,val)=>{if(tag===0x927c){maker=val;makerLen=count;}if(tag===0x9003)out.date=str(val,count);});41 if(!maker||str(maker,8)!=='FUJIFILM')return;42 // Fuji makernote: 8 byte signature, 4 byte IFD offset relative to the makernote start, little endian.43 const m=new DataView(t.buffer,t.byteOffset+maker,makerLen);44 const mo=m.getUint32(8,true),n=m.getUint16(mo,true);45 let film=0,mono=-1;46 for(let i=0;i<n;i++){const e=mo+2+i*12,tag=m.getUint16(e,true),type=m.getUint16(e+2,true);const val=type===3?m.getUint16(e+8,true):m.getUint32(e+8,true);if(tag===0x1401)film=val;if(tag===0x1003)mono=val;if(tag===0x104d)out.cropMode=val;}47 out.simulation=MONO[mono]||FILM[film]||'';48}