123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533openExifutilmoduleNumbers=structtyperational=int64*int64typesrational=int32*int32letfloat_of_rational(x,y)=Int64.to_floatx/.Int64.to_floatyletfloat_of_srational(x,y)=Int32.to_floatx/.Int32.to_floatyletstring_of_rationali1i2=Printf.sprintf"%Ld/%Ld"i1i2letstring_of_srationali1i2=Printf.sprintf"%ld/%ld"i1i2endopenNumbersmoduleEndian=structtypet=Big(* Motorola *)|Little(* Intel *)letto_string=function|Big->"Big"|Little->"Little"letsys=ifSys.big_endianthenBigelseLittleendmoduleIFD=structtypet=|IFD_0(** Info of the main image *)|IFD_1(** Info of the thumbnail *)|EXIF(** camera info *)|GPS(** location *)|Interop(** exif format interoperability info *)endmoduleDate=struct(* for GPSDateStamp *)typet={year:int;month:int;(** 1-12, I guess *)day:int;}letto_stringt=Printf.sprintf"%04d:%02d:%02d"t.yeart.montht.dayletof_strings=tryifString.lengths<>10thenraiseExit;letcheck_colonn=ifs.[n]<>':'thenraiseExitinletget_intfromlen=(* "0x12" is parsable, but who cares? *)int_of_string(String.subsfromlen)incheck_colon4;check_colon7;letyear=get_int04inletmonth=get_int52inletday=get_int82in`Ok{year;month;day;}with_->`ErrorsendmoduleDateTime=structtypet={year:int;month:int;(** 1-12, I guess *)day:int;hour:int;min:int;sec:int}letto_stringt=Printf.sprintf"%04d:%02d:%02d %02d:%02d:%02d"t.yeart.montht.dayt.hourt.mint.secletof_strings=tryifString.lengths<>19thenraiseExit;letcheck_colonn=ifs.[n]<>':'thenraiseExitinletget_intfromlen=(* "0x12" is parsable, but who cares? *)int_of_string(String.subsfromlen)incheck_colon4;check_colon7;check_colon13;check_colon16;letyear=get_int04inletmonth=get_int52inletday=get_int82inlethour=get_int112inletmin=get_int142inletsec=get_int172in`Ok{year;month;day;hour;min;sec}with_->`Errors(* I had an Android phone which created DateTime tag with
a little endian encoded unsigned int32 of unix time!
This function tries to fix the issue.
*)letof_string_packed_unix_times=tryletfloat_codec=float(Char.codec)inletconvertersec=letopenUnixinlettm=Unix.gmtimesecin{year=tm.tm_year;month=tm.tm_mon+1;day=tm.tm_mday;hour=tm.tm_hour;min=tm.tm_min;sec=tm.tm_sec}inifs.[4]='\000'thenletsec=float_codes.[0]+.float_codes.[1]*.256.0+.float_codes.[2]*.65536.0+.float_codes.[3]*.16777216.0in(* \000\000\000\000\000 is treated as an error
rather than 1970-01-01T00:00:00
*)ifsec=0.0then`Errorselse`Ok(convertersec)else`Errorswith|_->`ErrorsendmoduleTag=structtypet=intexternalto_string:t->IFD.t->string="caml_exif_tag_get_name_in_ifd"endmoduleEntry=structtypetmodulePack=structtypeformat=|ILLEGAL|BYTE(*= 1, *)|ASCII(*= 2, *)|SHORT(*= 3, *)|LONG(*= 4, *)|RATIONAL(*= 5, *)|SBYTE(*= 6, *)|UNDEFINED(*= 7, *)|SSHORT(*= 8, *)|SLONG(*= 9, *)|SRATIONAL(*= 10, *)|FLOAT(*= 11, *)|DOUBLE(*= 12 *)letstring_of_format=function|ILLEGAL->assertfalse|BYTE->"BYTE"|ASCII->"ASCII"|SHORT->"SHORT"|LONG->"LONG"|RATIONAL->"RATIONAL"|SBYTE->"SBYTE"|UNDEFINED->"UNDEFINED"|SSHORT->"SSHORT"|SLONG->"SLONG"|SRATIONAL->"SRATIONAL"|FLOAT->"FLOAT"|DOUBLE->"DOUBLE"typeunpacked=|Bytesofintarray|Asciisofstring|Shortsofintarray|Longsofint64array|Rationalsof(int64*int64)array|SBytesofintarray|Undefinedofstring|SShortsofintarray|SLongsofint32array|SRationalsof(int32*int32)array|Floatsoffloatarray|Doublesoffloatarrayexternaldecode_bytes:string->int->intarray="Val_ExifBytes"externaldecode_shorts:string->int->intarray="Val_ExifShorts"externaldecode_longs:string->int->int64array="Val_ExifLongs"externaldecode_rationals:string->int->rationalarray="Val_ExifRationals"externaldecode_sbytes:string->int->intarray="Val_ExifSBytes"externaldecode_sshorts:string->int->intarray="Val_ExifSShorts"externaldecode_slongs:string->int->int32array="Val_ExifSLongs"externaldecode_srationals:string->int->srationalarray="Val_ExifSRationals"externaldecode_floats:string->int->floatarray="Val_ExifFloats"externaldecode_doubles:string->int->floatarray="Val_ExifDoubles"letunpackformatcomponentscontent=matchformatwith|ILLEGAL->assertfalse|BYTE(*= 1, *)->Bytes(decode_bytescontentcomponents)|ASCII(*= 2, *)->(* remove the last \000 *)letcontent=letlen=String.lengthcontentinifcontent.[len-1]='\000'thenString.subcontent0(len-1)elsecontentinAsciiscontent|SHORT(*= 3, *)->Shorts(decode_shortscontentcomponents)|LONG(*= 4, *)->Longs(decode_longscontentcomponents)|RATIONAL(*= 5, *)->Rationals(decode_rationalscontentcomponents)|SBYTE(*= 6, *)->Bytes(decode_sbytescontentcomponents)|UNDEFINED(*= 7, *)->Undefinedcontent|SSHORT(*= 8, *)->SShorts(decode_sshortscontentcomponents)|SLONG(*= 9, *)->SLongs(decode_slongscontentcomponents)|SRATIONAL(*= 10, *)->SRationals(decode_srationalscontentcomponents)|FLOAT(*= 11, *)->Floats(decode_floatscontentcomponents)|DOUBLE(*= 12 *)->Doubles(decode_doublescontentcomponents)openFormatletformatppfv=beginmatchvwith|Asciis_->()|Undefined_->fprintfppf"Undefined "|Bytes_->fprintfppf"Bytes "|SBytes_->fprintfppf"SBytes "|Shorts_->fprintfppf"Shorts "|Longs_->fprintfppf"Longs "|Rationals_->fprintfppf"Rationals "|SShorts_->fprintfppf"SShorts "|SLongs_->fprintfppf"SLongs "|SRationals_->fprintfppf"SRationals "|Floats_->fprintfppf"Floats "|Doubles_->()end;matchvwith|Asciiss|Undefineds->fprintfppf"%S"s|Bytesis|SBytesis|Shortsis->Format.array(funppf->fprintfppf"%d")ppfis|Longsis->Format.array(funppf->fprintfppf"%Ld")ppfis|Rationalsrs->Format.array(funppf(i1,i2)->fprintfppf"%Ld/%Ld"i1i2)ppfrs|SShortsis->Format.array(funppf->fprintfppf"%d")ppfis|SLongsis->Format.array(funppf->fprintfppf"%ld")ppfis|SRationalsrs->Format.array(funppf(i1,i2)->fprintfppf"%ld/%ld"i1i2)ppfrs|Floatsfs|Doublesfs->Format.array(funppf->fprintfppf"%.20g")ppffsendexternalunref:t->unit="caml_exif_entry_unref"moduleDecoded=structtypet={tag:int;format:Pack.format;components:int;(* hope it will not overflow *)data:string;}endexternaldecode:t->Decoded.t="caml_exif_decode_entry"typeunpacked_entry=Tag.t*Pack.unpackedletunpack:Decoded.t->unpacked_entry=fund->d.Decoded.tag,Pack.unpackd.Decoded.formatd.Decoded.componentsd.Decoded.dataletformat_unpacked_entryifdppf(tag,p)=Format.fprintfppf"%s(%x): %a"(Tag.to_stringtagifd)tagPack.formatpletformatifdppft=format_unpacked_entryifdppf(unpack(decodet))endmoduleContent=structtypetexternalunref:t->unit="caml_exif_content_unref"externalentries:t->Entry.tlist="caml_exif_content_entries"letentriest=letes=entriestinletfinalisev=Gc.finalise(funv->Entry.unrefv)vinList.iterfinalisees;esletformatifdppft=letents=entriestinFormat.fprintfppf"@[[ @[%a@] ]@]"(Format.list";@ "(Entry.formatifd))entsendmoduleData=structtypetexternalfrom_string:string->t="caml_val_exif_data"externalunref:t->unit="caml_exif_data_unref"externalget_byte_order:t->Endian.t="caml_exif_get_byte_order"externalset_byte_order:t->Endian.t->unit="caml_exif_set_byte_order"externalfix:t->unit="caml_exif_data_fix"externaldump:t->unit="caml_exif_data_dump"letfrom_stringdata=lett=from_stringdatainset_byte_ordertEndian.sys;(* Destructively fix the endianess *)Gc.finalise(funv->unrefv)t;ttypecontents={ifd_0:Content.toption;ifd_1:Content.toption;exif:Content.toption;gps:Content.toption;interop:Content.toption}externalcontents:t->contents="caml_exif_data_contents"letcontentst=letcs=contentstinletfinalise=function|None->()|Somev->Gc.finalise(funv->Content.unrefv)vinfinalisecs.ifd_0;finalisecs.ifd_1;finalisecs.exif;finalisecs.gps;finalisecs.interop;csletget_ifd_0t=(contentst).ifd_0letget_ifd_1t=(contentst).ifd_1letget_exift=(contentst).exifletget_gpst=(contentst).gpsletget_interopt=(contentst).interopletunpack_genft=matchftwith|None->None|Somecontent->Some(List.map(funx->Entry.unpack(Entry.decodex))(Content.entriescontent))letunpack_ifd_0=unpack_genget_ifd_0letunpack_ifd_1=unpack_genget_ifd_1letunpack_exif=unpack_genget_exifletunpack_gps=unpack_genget_gpsletunpack_interop=unpack_genget_interopopenFormatletformatppft=letconts=contentstinfprintfppf"{ @[ifd_0=%a;@ ifd_1=%a;@ exif=%a;@ gps=%a;@ inter=%a@] }"(Format.opt(Content.formatIFD.IFD_0))conts.ifd_0(Format.opt(Content.formatIFD.IFD_1))conts.ifd_1(Format.opt(Content.formatIFD.EXIF))conts.exif(Format.opt(Content.formatIFD.GPS))conts.gps(Format.opt(Content.formatIFD.Interop))conts.interopendmoduleAnalyze=struct(* Exif data analyzer
Due to its updated-on-demand and lots-of-tags nature,
This module is implemented in a separate file from exif.ml
and its interface file is auto created.
*)openNumbersopenEntry.Packtypedatetime=[`EncodedInUnixTimeofDateTime.t|`Errorofstring|`OkofDateTime.t](** I have some photos from my old Android with non Ascii datetime.
They have encoded 32 bit int in Unix time instead! :-(
*)letparse_datetimes=matchDateTime.of_stringswith|(`Ok_asr)->r|`Errors->matchDateTime.of_string_packed_unix_timeswith|`Okv->`EncodedInUnixTimev|(`Error_ase)->eletanalyze_ifd(tag,pack)=matchtag,packwith|0x10f,Asciiss->`Makes|0x110,Asciiss->`Models|0x112,Shorts[|1|]->`Orientation`TopLeft|0x112,Shorts[|2|]->`Orientation`TopRight|0x112,Shorts[|3|]->`Orientation`BottomRight|0x112,Shorts[|4|]->`Orientation`BottomLeft|0x112,Shorts[|5|]->`Orientation`LeftTop|0x112,Shorts[|6|]->`Orientation`RightTop|0x112,Shorts[|7|]->`Orientation`RightBottom|0x112,Shorts[|8|]->`Orientation`LeftBottom|0x11a,Rationals[|r|]->`XResolutionr|0x11b,Rationals[|r|]->`YResolutionr|0x128,Shorts[|2|]->`ResolutionUnit`Inches|0x128,Shorts[|3|]->`ResolutionUnit`Centimeters|0x131,s->`Softwares|0x132,Asciiss->`DateTime(parse_datetimes)|_->`Unknown(tag,pack)letanalyze_exif(tag,pack)=matchtag,packwith|0x9000,Undefineds->`ExifVersions|0x927c,Undefineds->`MakerNotes|0x9286,Undefineds->`UserComments(* The first 8 bytes indicate char code:
ASCII 41.H, 53.H, 43.H, 49.H, 49.H, 00.H, 00.H, 00.H
JIS 4A.H, 49.H, 53.H, 00.H, 00.H, 00.H, 00.H, 00.H JIS X0208-1990
Unicode 55.H, 4E.H, 49.H, 43.H, 4F.H, 44.H, 45.H, 00.H Unicode Standard
Undefined 00.H, 00.H, 00.H, 00.H, 00.H, 00.H, 00.H, 00.H
*)|0x9003,Asciiss->`DateTimeOriginal(parse_datetimes)|0x9004,Asciiss->`DateTimeDigitized(parse_datetimes)|0x9290,Asciiss->`SubsecTimes|0x9291,Asciiss->`SubsecTimeOriginals|0x9292,Asciiss->`SubsecTimeDigitizeds|_->`Unknown(tag,pack)(* CR jfuruse: unused
module GPS = struct
type latitude = [ `North | `South ] * rational
type longitude = [ `East | `West ] * rational
type altitude = [ `AboveSeaLevel | `BelowSeaLevel ] * rational
type time_stamp_utc = {
hour : rational;
min : rational;
sec : rational;
}
type direction = [ `True | `Magnetic ] * rational
type map_datum = string
type t = {
version : (int * int * int * int) option;
latitude : latitude option;
longitude : longitude option;
altitude : altitude option;
time_stamp_utc : time_stamp_utc option;
direction : direction option;
map_datum : map_datum option
}
end
*)letanalyze_gps(tag,v)=matchtag,vwith|00,Bytes[|x;y;z;w|]->`GPSVersion(x,y,z,w)|01,Asciis"N"->`NorthLatitude|01,Asciis"S"->`SouthLatitude|02,Rationals[|r|]->`Latituder|03,Asciis"E"->`EastLongitude|03,Asciis"W"->`WestLongitude|04,Rationals[|r|]->`Longituder|05,Bytes[|0|]->`AboveSeaLevel|05,Bytes[|1|]->`BelowSeaLevel|06,Rationals[|r|]->`Altituder|07,Rationals[|h;m;s|]->`TimeStampUTC(float_of_rationalh,float_of_rationalm,float_of_rationals)|07,SRationals[|h;m;s|]->(* It is illegal in the spec but I see some photos with SRationals *)`TimeStampUTCinSRationals(float_of_srationalh,float_of_srationalm,float_of_srationals)|16,Asciis"T"->`ImgDirectionTrue|16,Asciis"M"->`ImgDirectionMagnetic|17,Rationals[|r|]->`ImgDirectionr|18,Asciiss->`GPSMapDatums|29,Asciiss->`GPSDate(Date.of_strings)|_->`Unknown(tag,v)letexif_datetimet=matchData.unpack_exiftwith|Someentries->List.find_map_opt(function|`DateTimeOriginalt->Somet|_->None)(List.mapanalyze_exifentries)|None->Noneletifd_0_datetimet=matchData.unpack_ifd_0twith|Someentries->List.find_map_opt(function|`DateTimet->Somet|_->None)(List.mapanalyze_ifdentries)|None->Noneletdatetimet=matchexif_datetimetwith|(Some_asres)->res|None->ifd_0_datetimetend