···11+(** Pure OCaml EXIF parsing library
22+33+ Based on EXIF 2.32 specification and TIFF 6.0 specification.
44+55+ EXIF data is stored in JPEG APP1 markers with the prefix "Exif\x00\x00".
66+ The data follows TIFF format with IFDs (Image File Directories). *)
77+88+(** {1 Types} *)
99+1010+type byte_order =
1111+ | Big_endian
1212+ | Little_endian
1313+1414+type format =
1515+ | Byte
1616+ | Ascii
1717+ | Short
1818+ | Long
1919+ | Rational
2020+ | Sbyte
2121+ | Undefined
2222+ | Sshort
2323+ | Slong
2424+ | Srational
2525+ | Float
2626+ | Double
2727+2828+type ifd =
2929+ | IFD0
3030+ | IFD1
3131+ | EXIF
3232+ | GPS
3333+ | Interoperability
3434+3535+type rational = {
3636+ numerator : int32;
3737+ denominator : int32;
3838+}
3939+4040+type srational = {
4141+ snumerator : int32;
4242+ sdenominator : int32;
4343+}
4444+4545+type value =
4646+ | VByte of int array
4747+ | VAscii of string
4848+ | VShort of int array
4949+ | VLong of int32 array
5050+ | VRational of rational array
5151+ | VSbyte of int array
5252+ | VUndefined of bytes
5353+ | VSshort of int array
5454+ | VSlong of int32 array
5555+ | VSrational of srational array
5656+ | VFloat of float array
5757+ | VDouble of float array
5858+5959+type entry = {
6060+ tag : int;
6161+ ifd : ifd;
6262+ format : format;
6363+ components : int;
6464+ value : value;
6565+}
6666+6767+type t = {
6868+ byte_order : byte_order;
6969+ entries : entry list;
7070+ thumbnail : bytes option;
7171+}
7272+7373+(** {1 Tag constants} *)
7474+7575+module Tag = struct
7676+ (* IFD0/IFD1 tags - TIFF 6.0 baseline *)
7777+ let image_width = 0x0100
7878+ let image_length = 0x0101
7979+ let bits_per_sample = 0x0102
8080+ let compression = 0x0103
8181+ let photometric_interpretation = 0x0106
8282+ let image_description = 0x010E
8383+ let make = 0x010F
8484+ let model = 0x0110
8585+ let strip_offsets = 0x0111
8686+ let orientation = 0x0112
8787+ let samples_per_pixel = 0x0115
8888+ let rows_per_strip = 0x0116
8989+ let strip_byte_counts = 0x0117
9090+ let x_resolution = 0x011A
9191+ let y_resolution = 0x011B
9292+ let planar_configuration = 0x011C
9393+ let resolution_unit = 0x0128
9494+ let transfer_function = 0x012D
9595+ let software = 0x0131
9696+ let date_time = 0x0132
9797+ let artist = 0x013B
9898+ let white_point = 0x013E
9999+ let primary_chromaticities = 0x013F
100100+ let jpeg_interchange_format = 0x0201
101101+ let jpeg_interchange_format_length = 0x0202
102102+ let ycbcr_coefficients = 0x0211
103103+ let ycbcr_sub_sampling = 0x0212
104104+ let ycbcr_positioning = 0x0213
105105+ let reference_black_white = 0x0214
106106+ let copyright = 0x8298
107107+ let exif_ifd_pointer = 0x8769
108108+ let gps_info_ifd_pointer = 0x8825
109109+110110+ (* EXIF IFD tags - camera/exposure settings *)
111111+ let exposure_time = 0x829A
112112+ let f_number = 0x829D
113113+ let exposure_program = 0x8822
114114+ let spectral_sensitivity = 0x8824
115115+ let iso_speed_ratings = 0x8827
116116+ let oecf = 0x8828
117117+ let sensitivity_type = 0x8830
118118+ let exif_version = 0x9000
119119+ let date_time_original = 0x9003
120120+ let date_time_digitized = 0x9004
121121+ let offset_time = 0x9010
122122+ let offset_time_original = 0x9011
123123+ let offset_time_digitized = 0x9012
124124+ let components_configuration = 0x9101
125125+ let compressed_bits_per_pixel = 0x9102
126126+ let shutter_speed_value = 0x9201
127127+ let aperture_value = 0x9202
128128+ let brightness_value = 0x9203
129129+ let exposure_bias_value = 0x9204
130130+ let max_aperture_value = 0x9205
131131+ let subject_distance = 0x9206
132132+ let metering_mode = 0x9207
133133+ let light_source = 0x9208
134134+ let flash = 0x9209
135135+ let focal_length = 0x920A
136136+ let subject_area = 0x9214
137137+ let maker_note = 0x927C
138138+ let user_comment = 0x9286
139139+ let sub_sec_time = 0x9290
140140+ let sub_sec_time_original = 0x9291
141141+ let sub_sec_time_digitized = 0x9292
142142+ let flash_pix_version = 0xA000
143143+ let color_space = 0xA001
144144+ let pixel_x_dimension = 0xA002
145145+ let pixel_y_dimension = 0xA003
146146+ let related_sound_file = 0xA004
147147+ let interoperability_ifd_pointer = 0xA005
148148+ let flash_energy = 0xA20B
149149+ let spatial_frequency_response = 0xA20C
150150+ let focal_plane_x_resolution = 0xA20E
151151+ let focal_plane_y_resolution = 0xA20F
152152+ let focal_plane_resolution_unit = 0xA210
153153+ let subject_location = 0xA214
154154+ let exposure_index = 0xA215
155155+ let sensing_method = 0xA217
156156+ let file_source = 0xA300
157157+ let scene_type = 0xA301
158158+ let cfa_pattern = 0xA302
159159+ let custom_rendered = 0xA401
160160+ let exposure_mode = 0xA402
161161+ let white_balance = 0xA403
162162+ let digital_zoom_ratio = 0xA404
163163+ let focal_length_in_35mm_film = 0xA405
164164+ let scene_capture_type = 0xA406
165165+ let gain_control = 0xA407
166166+ let contrast = 0xA408
167167+ let saturation = 0xA409
168168+ let sharpness = 0xA40A
169169+ let device_setting_description = 0xA40B
170170+ let subject_distance_range = 0xA40C
171171+ let image_unique_id = 0xA420
172172+ let camera_owner_name = 0xA430
173173+ let body_serial_number = 0xA431
174174+ let lens_specification = 0xA432
175175+ let lens_make = 0xA433
176176+ let lens_model = 0xA434
177177+ let lens_serial_number = 0xA435
178178+ let gamma = 0xA500
179179+180180+ (* GPS IFD tags *)
181181+ let gps_version_id = 0x0000
182182+ let gps_latitude_ref = 0x0001
183183+ let gps_latitude = 0x0002
184184+ let gps_longitude_ref = 0x0003
185185+ let gps_longitude = 0x0004
186186+ let gps_altitude_ref = 0x0005
187187+ let gps_altitude = 0x0006
188188+ let gps_time_stamp = 0x0007
189189+ let gps_satellites = 0x0008
190190+ let gps_status = 0x0009
191191+ let gps_measure_mode = 0x000A
192192+ let gps_dop = 0x000B
193193+ let gps_speed_ref = 0x000C
194194+ let gps_speed = 0x000D
195195+ let gps_track_ref = 0x000E
196196+ let gps_track = 0x000F
197197+ let gps_img_direction_ref = 0x0010
198198+ let gps_img_direction = 0x0011
199199+ let gps_map_datum = 0x0012
200200+ let gps_dest_latitude_ref = 0x0013
201201+ let gps_dest_latitude = 0x0014
202202+ let gps_dest_longitude_ref = 0x0015
203203+ let gps_dest_longitude = 0x0016
204204+ let gps_dest_bearing_ref = 0x0017
205205+ let gps_dest_bearing = 0x0018
206206+ let gps_dest_distance_ref = 0x0019
207207+ let gps_dest_distance = 0x001A
208208+ let gps_processing_method = 0x001B
209209+ let gps_area_information = 0x001C
210210+ let gps_date_stamp = 0x001D
211211+ let gps_differential = 0x001E
212212+ let gps_h_positioning_error = 0x001F
213213+214214+ let name_of_tag tag ifd =
215215+ match ifd with
216216+ | GPS ->
217217+ (match tag with
218218+ | 0x0000 -> "GPSVersionID"
219219+ | 0x0001 -> "GPSLatitudeRef"
220220+ | 0x0002 -> "GPSLatitude"
221221+ | 0x0003 -> "GPSLongitudeRef"
222222+ | 0x0004 -> "GPSLongitude"
223223+ | 0x0005 -> "GPSAltitudeRef"
224224+ | 0x0006 -> "GPSAltitude"
225225+ | 0x0007 -> "GPSTimeStamp"
226226+ | 0x0008 -> "GPSSatellites"
227227+ | 0x0009 -> "GPSStatus"
228228+ | 0x000A -> "GPSMeasureMode"
229229+ | 0x000B -> "GPSDOP"
230230+ | 0x000C -> "GPSSpeedRef"
231231+ | 0x000D -> "GPSSpeed"
232232+ | 0x000E -> "GPSTrackRef"
233233+ | 0x000F -> "GPSTrack"
234234+ | 0x0010 -> "GPSImgDirectionRef"
235235+ | 0x0011 -> "GPSImgDirection"
236236+ | 0x0012 -> "GPSMapDatum"
237237+ | 0x001D -> "GPSDateStamp"
238238+ | _ -> Printf.sprintf "GPS_0x%04X" tag)
239239+ | _ ->
240240+ (match tag with
241241+ | 0x0100 -> "ImageWidth"
242242+ | 0x0101 -> "ImageLength"
243243+ | 0x0102 -> "BitsPerSample"
244244+ | 0x0103 -> "Compression"
245245+ | 0x0106 -> "PhotometricInterpretation"
246246+ | 0x010E -> "ImageDescription"
247247+ | 0x010F -> "Make"
248248+ | 0x0110 -> "Model"
249249+ | 0x0111 -> "StripOffsets"
250250+ | 0x0112 -> "Orientation"
251251+ | 0x0115 -> "SamplesPerPixel"
252252+ | 0x0116 -> "RowsPerStrip"
253253+ | 0x0117 -> "StripByteCounts"
254254+ | 0x011A -> "XResolution"
255255+ | 0x011B -> "YResolution"
256256+ | 0x011C -> "PlanarConfiguration"
257257+ | 0x0128 -> "ResolutionUnit"
258258+ | 0x012D -> "TransferFunction"
259259+ | 0x0131 -> "Software"
260260+ | 0x0132 -> "DateTime"
261261+ | 0x013B -> "Artist"
262262+ | 0x013E -> "WhitePoint"
263263+ | 0x013F -> "PrimaryChromaticities"
264264+ | 0x0201 -> "JPEGInterchangeFormat"
265265+ | 0x0202 -> "JPEGInterchangeFormatLength"
266266+ | 0x0211 -> "YCbCrCoefficients"
267267+ | 0x0212 -> "YCbCrSubSampling"
268268+ | 0x0213 -> "YCbCrPositioning"
269269+ | 0x0214 -> "ReferenceBlackWhite"
270270+ | 0x8298 -> "Copyright"
271271+ | 0x8769 -> "ExifIFDPointer"
272272+ | 0x8825 -> "GPSInfoIFDPointer"
273273+ | 0x829A -> "ExposureTime"
274274+ | 0x829D -> "FNumber"
275275+ | 0x8822 -> "ExposureProgram"
276276+ | 0x8824 -> "SpectralSensitivity"
277277+ | 0x8827 -> "ISOSpeedRatings"
278278+ | 0x9000 -> "ExifVersion"
279279+ | 0x9003 -> "DateTimeOriginal"
280280+ | 0x9004 -> "DateTimeDigitized"
281281+ | 0x9010 -> "OffsetTime"
282282+ | 0x9011 -> "OffsetTimeOriginal"
283283+ | 0x9012 -> "OffsetTimeDigitized"
284284+ | 0x9101 -> "ComponentsConfiguration"
285285+ | 0x9102 -> "CompressedBitsPerPixel"
286286+ | 0x9201 -> "ShutterSpeedValue"
287287+ | 0x9202 -> "ApertureValue"
288288+ | 0x9203 -> "BrightnessValue"
289289+ | 0x9204 -> "ExposureBiasValue"
290290+ | 0x9205 -> "MaxApertureValue"
291291+ | 0x9206 -> "SubjectDistance"
292292+ | 0x9207 -> "MeteringMode"
293293+ | 0x9208 -> "LightSource"
294294+ | 0x9209 -> "Flash"
295295+ | 0x920A -> "FocalLength"
296296+ | 0x9214 -> "SubjectArea"
297297+ | 0x927C -> "MakerNote"
298298+ | 0x9286 -> "UserComment"
299299+ | 0x9290 -> "SubSecTime"
300300+ | 0x9291 -> "SubSecTimeOriginal"
301301+ | 0x9292 -> "SubSecTimeDigitized"
302302+ | 0xA000 -> "FlashPixVersion"
303303+ | 0xA001 -> "ColorSpace"
304304+ | 0xA002 -> "PixelXDimension"
305305+ | 0xA003 -> "PixelYDimension"
306306+ | 0xA004 -> "RelatedSoundFile"
307307+ | 0xA005 -> "InteroperabilityIFDPointer"
308308+ | 0xA20B -> "FlashEnergy"
309309+ | 0xA20C -> "SpatialFrequencyResponse"
310310+ | 0xA20E -> "FocalPlaneXResolution"
311311+ | 0xA20F -> "FocalPlaneYResolution"
312312+ | 0xA210 -> "FocalPlaneResolutionUnit"
313313+ | 0xA214 -> "SubjectLocation"
314314+ | 0xA215 -> "ExposureIndex"
315315+ | 0xA217 -> "SensingMethod"
316316+ | 0xA300 -> "FileSource"
317317+ | 0xA301 -> "SceneType"
318318+ | 0xA302 -> "CFAPattern"
319319+ | 0xA401 -> "CustomRendered"
320320+ | 0xA402 -> "ExposureMode"
321321+ | 0xA403 -> "WhiteBalance"
322322+ | 0xA404 -> "DigitalZoomRatio"
323323+ | 0xA405 -> "FocalLengthIn35mmFilm"
324324+ | 0xA406 -> "SceneCaptureType"
325325+ | 0xA407 -> "GainControl"
326326+ | 0xA408 -> "Contrast"
327327+ | 0xA409 -> "Saturation"
328328+ | 0xA40A -> "Sharpness"
329329+ | 0xA40C -> "SubjectDistanceRange"
330330+ | 0xA420 -> "ImageUniqueID"
331331+ | 0xA430 -> "CameraOwnerName"
332332+ | 0xA431 -> "BodySerialNumber"
333333+ | 0xA432 -> "LensSpecification"
334334+ | 0xA433 -> "LensMake"
335335+ | 0xA434 -> "LensModel"
336336+ | 0xA435 -> "LensSerialNumber"
337337+ | 0xA500 -> "Gamma"
338338+ | _ -> Printf.sprintf "Tag_0x%04X" tag)
339339+end
340340+341341+(** {1 Parsing} *)
342342+343343+exception Parse_error of string
344344+345345+let format_size = function
346346+ | Byte | Ascii | Sbyte | Undefined -> 1
347347+ | Short | Sshort -> 2
348348+ | Long | Slong | Float -> 4
349349+ | Rational | Srational | Double -> 8
350350+351351+let format_of_int = function
352352+ | 1 -> Byte
353353+ | 2 -> Ascii
354354+ | 3 -> Short
355355+ | 4 -> Long
356356+ | 5 -> Rational
357357+ | 6 -> Sbyte
358358+ | 7 -> Undefined
359359+ | 8 -> Sshort
360360+ | 9 -> Slong
361361+ | 10 -> Srational
362362+ | 11 -> Float
363363+ | 12 -> Double
364364+ | n -> raise (Parse_error (Printf.sprintf "Unknown format: %d" n))
365365+366366+(** Read 16-bit value with given byte order *)
367367+let read_u16 data offset byte_order =
368368+ let b0 = Bytes.get_uint8 data offset in
369369+ let b1 = Bytes.get_uint8 data (offset + 1) in
370370+ match byte_order with
371371+ | Big_endian -> (b0 lsl 8) lor b1
372372+ | Little_endian -> (b1 lsl 8) lor b0
373373+374374+(** Read 32-bit value with given byte order *)
375375+let read_u32 data offset byte_order =
376376+ let b0 = Int32.of_int (Bytes.get_uint8 data offset) in
377377+ let b1 = Int32.of_int (Bytes.get_uint8 data (offset + 1)) in
378378+ let b2 = Int32.of_int (Bytes.get_uint8 data (offset + 2)) in
379379+ let b3 = Int32.of_int (Bytes.get_uint8 data (offset + 3)) in
380380+ match byte_order with
381381+ | Big_endian ->
382382+ Int32.(logor (shift_left b0 24)
383383+ (logor (shift_left b1 16)
384384+ (logor (shift_left b2 8) b3)))
385385+ | Little_endian ->
386386+ Int32.(logor (shift_left b3 24)
387387+ (logor (shift_left b2 16)
388388+ (logor (shift_left b1 8) b0)))
389389+390390+let read_s32 = read_u32
391391+392392+(** Read IEEE 754 single precision float *)
393393+let read_float data offset byte_order =
394394+ Int32.float_of_bits (read_u32 data offset byte_order)
395395+396396+(** Read IEEE 754 double precision float *)
397397+let read_double data offset byte_order =
398398+ let b = Bytes.create 8 in
399399+ (match byte_order with
400400+ | Big_endian ->
401401+ for i = 0 to 7 do
402402+ Bytes.set b i (Bytes.get data (offset + i))
403403+ done
404404+ | Little_endian ->
405405+ for i = 0 to 7 do
406406+ Bytes.set b (7 - i) (Bytes.get data (offset + i))
407407+ done);
408408+ Int64.float_of_bits (Bytes.get_int64_be b 0)
409409+410410+(** Parse entry value from data *)
411411+let parse_value data offset byte_order format components =
412412+ let size = format_size format * components in
413413+ if offset + size > Bytes.length data then
414414+ raise (Parse_error "Value extends beyond data");
415415+ match format with
416416+ | Byte ->
417417+ VByte (Array.init components (fun i ->
418418+ Bytes.get_uint8 data (offset + i)))
419419+ | Ascii ->
420420+ let s = Bytes.sub_string data offset components in
421421+ let len = String.length s in
422422+ let s =
423423+ if len > 0 && s.[len - 1] = '\000' then
424424+ String.sub s 0 (len - 1)
425425+ else
426426+ s
427427+ in
428428+ VAscii s
429429+ | Short ->
430430+ VShort (Array.init components (fun i ->
431431+ read_u16 data (offset + i * 2) byte_order))
432432+ | Long ->
433433+ VLong (Array.init components (fun i ->
434434+ read_u32 data (offset + i * 4) byte_order))
435435+ | Rational ->
436436+ VRational (Array.init components (fun i ->
437437+ let off = offset + i * 8 in
438438+ { numerator = read_u32 data off byte_order;
439439+ denominator = read_u32 data (off + 4) byte_order }))
440440+ | Sbyte ->
441441+ VSbyte (Array.init components (fun i ->
442442+ let v = Bytes.get_uint8 data (offset + i) in
443443+ if v >= 128 then v - 256 else v))
444444+ | Undefined ->
445445+ VUndefined (Bytes.sub data offset components)
446446+ | Sshort ->
447447+ VSshort (Array.init components (fun i ->
448448+ let v = read_u16 data (offset + i * 2) byte_order in
449449+ if v >= 32768 then v - 65536 else v))
450450+ | Slong ->
451451+ VSlong (Array.init components (fun i ->
452452+ read_s32 data (offset + i * 4) byte_order))
453453+ | Srational ->
454454+ VSrational (Array.init components (fun i ->
455455+ let off = offset + i * 8 in
456456+ { snumerator = read_s32 data off byte_order;
457457+ sdenominator = read_s32 data (off + 4) byte_order }))
458458+ | Float ->
459459+ VFloat (Array.init components (fun i ->
460460+ read_float data (offset + i * 4) byte_order))
461461+ | Double ->
462462+ VDouble (Array.init components (fun i ->
463463+ read_double data (offset + i * 8) byte_order))
464464+465465+(** Parse a single IFD entry *)
466466+let parse_entry data offset byte_order ifd =
467467+ if offset + 12 > Bytes.length data then
468468+ raise (Parse_error "Entry extends beyond data");
469469+ let tag = read_u16 data offset byte_order in
470470+ let format_code = read_u16 data (offset + 2) byte_order in
471471+ let format = format_of_int format_code in
472472+ let components = Int32.to_int (read_u32 data (offset + 4) byte_order) in
473473+ let value_size = format_size format * components in
474474+ let value_offset =
475475+ if value_size <= 4 then
476476+ offset + 8
477477+ else
478478+ Int32.to_int (read_u32 data (offset + 8) byte_order)
479479+ in
480480+ let value = parse_value data value_offset byte_order format components in
481481+ { tag; ifd; format; components; value }
482482+483483+(** Parse an IFD and return entries and next IFD offset *)
484484+let parse_ifd data offset byte_order ifd =
485485+ if offset + 2 > Bytes.length data then
486486+ raise (Parse_error "IFD extends beyond data");
487487+ let entry_count = read_u16 data offset byte_order in
488488+ let entries_end = offset + 2 + entry_count * 12 in
489489+ if entries_end + 4 > Bytes.length data then
490490+ raise (Parse_error "IFD entries extend beyond data");
491491+ let entries =
492492+ List.init entry_count (fun i ->
493493+ parse_entry data (offset + 2 + i * 12) byte_order ifd)
494494+ in
495495+ let next_ifd_offset = Int32.to_int (read_u32 data entries_end byte_order) in
496496+ (entries, next_ifd_offset)
497497+498498+(** Find IFD pointer entry *)
499499+let find_ifd_pointer entries tag =
500500+ match List.find_opt (fun e -> e.tag = tag) entries with
501501+ | Some { value = VLong [| offset |]; _ } -> Some (Int32.to_int offset)
502502+ | _ -> None
503503+504504+(** Parse complete EXIF data from bytes *)
505505+let parse data =
506506+ let len = Bytes.length data in
507507+ if len < 8 then
508508+ raise (Parse_error "Data too short for EXIF header");
509509+510510+ (* Check byte order marker *)
511511+ let byte_order =
512512+ match Bytes.get_uint8 data 0, Bytes.get_uint8 data 1 with
513513+ | 0x4D, 0x4D -> Big_endian (* "MM" - Motorola *)
514514+ | 0x49, 0x49 -> Little_endian (* "II" - Intel *)
515515+ | _ -> raise (Parse_error "Invalid byte order marker")
516516+ in
517517+518518+ (* Check TIFF magic number (42) *)
519519+ let magic = read_u16 data 2 byte_order in
520520+ if magic <> 42 then
521521+ raise (Parse_error (Printf.sprintf "Invalid TIFF magic: %d" magic));
522522+523523+ (* Get offset to first IFD *)
524524+ let ifd0_offset = Int32.to_int (read_u32 data 4 byte_order) in
525525+ if ifd0_offset < 8 || ifd0_offset >= len then
526526+ raise (Parse_error "Invalid IFD0 offset");
527527+528528+ (* Parse IFD0 *)
529529+ let ifd0_entries, ifd1_offset = parse_ifd data ifd0_offset byte_order IFD0 in
530530+531531+ (* Parse EXIF sub-IFD if present *)
532532+ let exif_entries =
533533+ find_ifd_pointer ifd0_entries Tag.exif_ifd_pointer
534534+ |> Option.fold ~none:[] ~some:(fun off ->
535535+ if off > 0 && off < len then
536536+ fst (parse_ifd data off byte_order EXIF)
537537+ else [])
538538+ in
539539+540540+ (* Parse GPS sub-IFD if present *)
541541+ let gps_entries =
542542+ find_ifd_pointer ifd0_entries Tag.gps_info_ifd_pointer
543543+ |> Option.fold ~none:[] ~some:(fun off ->
544544+ if off > 0 && off < len then
545545+ fst (parse_ifd data off byte_order GPS)
546546+ else [])
547547+ in
548548+549549+ (* Parse Interoperability IFD if present (pointer is in EXIF IFD) *)
550550+ let interop_entries =
551551+ find_ifd_pointer exif_entries Tag.interoperability_ifd_pointer
552552+ |> Option.fold ~none:[] ~some:(fun off ->
553553+ if off > 0 && off < len then
554554+ fst (parse_ifd data off byte_order Interoperability)
555555+ else [])
556556+ in
557557+558558+ (* Parse IFD1 (thumbnail) if present *)
559559+ let ifd1_entries, thumbnail =
560560+ if ifd1_offset > 0 && ifd1_offset < len then
561561+ let entries, _ = parse_ifd data ifd1_offset byte_order IFD1 in
562562+ (* Look for JPEG thumbnail *)
563563+ let thumb_offset =
564564+ find_ifd_pointer entries Tag.jpeg_interchange_format
565565+ in
566566+ let thumb_length =
567567+ find_ifd_pointer entries Tag.jpeg_interchange_format_length
568568+ in
569569+ let thumbnail =
570570+ match thumb_offset, thumb_length with
571571+ | Some off, Some len when off > 0 && off + len <= Bytes.length data ->
572572+ Some (Bytes.sub data off len)
573573+ | _ -> None
574574+ in
575575+ (entries, thumbnail)
576576+ else
577577+ ([], None)
578578+ in
579579+580580+ let entries =
581581+ ifd0_entries @ ifd1_entries @ exif_entries @ gps_entries @ interop_entries
582582+ in
583583+ { byte_order; entries; thumbnail }
584584+585585+let parse_from_app1 = parse
586586+587587+(** {1 Query functions} *)
588588+589589+let find_entry tag exif =
590590+ List.find_opt (fun e -> e.tag = tag) exif.entries
591591+592592+let find_entry_in_ifd tag ifd exif =
593593+ List.find_opt (fun e -> e.tag = tag && e.ifd = ifd) exif.entries
594594+595595+let entries_in_ifd ifd exif =
596596+ List.filter (fun e -> e.ifd = ifd) exif.entries
597597+598598+(** {1 Value extraction helpers} *)
599599+600600+let get_string entry =
601601+ match entry.value with
602602+ | VAscii s -> Some s
603603+ | _ -> None
604604+605605+let get_short entry =
606606+ match entry.value with
607607+ | VShort [| v |] -> Some v
608608+ | _ -> None
609609+610610+let get_long entry =
611611+ match entry.value with
612612+ | VLong [| v |] -> Some (Int32.to_int v)
613613+ | _ -> None
614614+615615+let get_rational entry =
616616+ match entry.value with
617617+ | VRational [| { numerator; denominator } |] ->
618618+ if denominator = 0l then None
619619+ else Some (Int32.to_float numerator /. Int32.to_float denominator)
620620+ | _ -> None
621621+622622+let get_rationals entry =
623623+ match entry.value with
624624+ | VRational arr ->
625625+ Some (Array.map (fun { numerator; denominator } ->
626626+ if denominator = 0l then 0.0
627627+ else Int32.to_float numerator /. Int32.to_float denominator) arr)
628628+ | _ -> None
629629+630630+(** {1 Common metadata accessors} *)
631631+632632+let make exif =
633633+ Option.bind (find_entry Tag.make exif) get_string
634634+635635+let model exif =
636636+ Option.bind (find_entry Tag.model exif) get_string
637637+638638+let software exif =
639639+ Option.bind (find_entry Tag.software exif) get_string
640640+641641+let image_description exif =
642642+ Option.bind (find_entry Tag.image_description exif) get_string
643643+644644+let artist exif =
645645+ Option.bind (find_entry Tag.artist exif) get_string
646646+647647+let copyright exif =
648648+ Option.bind (find_entry Tag.copyright exif) get_string
649649+650650+let date_time_original exif =
651651+ Option.bind (find_entry Tag.date_time_original exif) get_string
652652+653653+let date_time_digitized exif =
654654+ Option.bind (find_entry Tag.date_time_digitized exif) get_string
655655+656656+let date_time exif =
657657+ Option.bind (find_entry Tag.date_time exif) get_string
658658+659659+let orientation exif =
660660+ Option.bind (find_entry Tag.orientation exif) get_short
661661+662662+let image_width exif =
663663+ Option.bind (find_entry Tag.image_width exif) (fun e ->
664664+ match e.value with
665665+ | VShort [| v |] -> Some v
666666+ | VLong [| v |] -> Some (Int32.to_int v)
667667+ | _ -> None)
668668+669669+let image_height exif =
670670+ Option.bind (find_entry Tag.image_length exif) (fun e ->
671671+ match e.value with
672672+ | VShort [| v |] -> Some v
673673+ | VLong [| v |] -> Some (Int32.to_int v)
674674+ | _ -> None)
675675+676676+let x_resolution exif =
677677+ Option.bind (find_entry Tag.x_resolution exif) get_rational
678678+679679+let y_resolution exif =
680680+ Option.bind (find_entry Tag.y_resolution exif) get_rational
681681+682682+let resolution_unit exif =
683683+ Option.bind (find_entry Tag.resolution_unit exif) get_short
684684+685685+let exposure_time exif =
686686+ Option.bind (find_entry Tag.exposure_time exif) get_rational
687687+688688+let f_number exif =
689689+ Option.bind (find_entry Tag.f_number exif) get_rational
690690+691691+let iso_speed exif =
692692+ Option.bind (find_entry Tag.iso_speed_ratings exif) (fun e ->
693693+ match e.value with
694694+ | VShort [| v |] -> Some v
695695+ | VShort arr when Array.length arr > 0 -> Some arr.(0)
696696+ | _ -> None)
697697+698698+let focal_length exif =
699699+ Option.bind (find_entry Tag.focal_length exif) get_rational
700700+701701+let focal_length_35mm exif =
702702+ Option.bind (find_entry Tag.focal_length_in_35mm_film exif) get_short
703703+704704+let flash exif =
705705+ Option.bind (find_entry Tag.flash exif) get_short
706706+707707+let color_space exif =
708708+ Option.bind (find_entry Tag.color_space exif) get_short
709709+710710+let exif_version exif =
711711+ Option.bind (find_entry Tag.exif_version exif) (fun e ->
712712+ match e.value with
713713+ | VUndefined b -> Some (Bytes.to_string b)
714714+ | _ -> None)
715715+716716+(** {1 GPS accessors} *)
717717+718718+(** Convert DMS (degrees/minutes/seconds) rationals to decimal degrees *)
719719+let dms_to_decimal rationals =
720720+ if Array.length rationals < 3 then
721721+ None
722722+ else
723723+ let degrees =
724724+ Int32.to_float rationals.(0).numerator /.
725725+ Int32.to_float rationals.(0).denominator
726726+ in
727727+ let minutes =
728728+ Int32.to_float rationals.(1).numerator /.
729729+ Int32.to_float rationals.(1).denominator
730730+ in
731731+ let seconds =
732732+ Int32.to_float rationals.(2).numerator /.
733733+ Int32.to_float rationals.(2).denominator
734734+ in
735735+ Some (degrees +. minutes /. 60.0 +. seconds /. 3600.0)
736736+737737+let gps_latitude exif =
738738+ Option.bind (find_entry_in_ifd Tag.gps_latitude GPS exif) (fun e ->
739739+ match e.value with
740740+ | VRational arr ->
741741+ Option.map (fun lat ->
742742+ match find_entry_in_ifd Tag.gps_latitude_ref GPS exif with
743743+ | Some { value = VAscii "S"; _ } -> -.lat
744744+ | _ -> lat)
745745+ (dms_to_decimal arr)
746746+ | _ -> None)
747747+748748+let gps_longitude exif =
749749+ Option.bind (find_entry_in_ifd Tag.gps_longitude GPS exif) (fun e ->
750750+ match e.value with
751751+ | VRational arr ->
752752+ Option.map (fun lon ->
753753+ match find_entry_in_ifd Tag.gps_longitude_ref GPS exif with
754754+ | Some { value = VAscii "W"; _ } -> -.lon
755755+ | _ -> lon)
756756+ (dms_to_decimal arr)
757757+ | _ -> None)
758758+759759+let gps_altitude exif =
760760+ Option.map (fun alt ->
761761+ match find_entry_in_ifd Tag.gps_altitude_ref GPS exif with
762762+ | Some { value = VByte [| 1 |]; _ } -> -.alt
763763+ | _ -> alt)
764764+ (Option.bind (find_entry_in_ifd Tag.gps_altitude GPS exif) get_rational)
765765+766766+(** {1 Pretty printing} *)
767767+768768+let string_of_value = function
769769+ | VByte arr ->
770770+ arr |> Array.to_list |> List.map string_of_int |> String.concat " "
771771+ | VAscii s -> s
772772+ | VShort arr ->
773773+ arr |> Array.to_list |> List.map string_of_int |> String.concat " "
774774+ | VLong arr ->
775775+ arr |> Array.to_list |> List.map Int32.to_string |> String.concat " "
776776+ | VRational arr ->
777777+ arr
778778+ |> Array.to_list
779779+ |> List.map (fun r -> Printf.sprintf "%ld/%ld" r.numerator r.denominator)
780780+ |> String.concat " "
781781+ | VSbyte arr ->
782782+ arr |> Array.to_list |> List.map string_of_int |> String.concat " "
783783+ | VUndefined b ->
784784+ if Bytes.length b <= 16 then
785785+ List.init (Bytes.length b) (fun i ->
786786+ Printf.sprintf "%02X" (Bytes.get_uint8 b i))
787787+ |> String.concat " "
788788+ else
789789+ Printf.sprintf "<%d bytes>" (Bytes.length b)
790790+ | VSshort arr ->
791791+ arr |> Array.to_list |> List.map string_of_int |> String.concat " "
792792+ | VSlong arr ->
793793+ arr |> Array.to_list |> List.map Int32.to_string |> String.concat " "
794794+ | VSrational arr ->
795795+ arr
796796+ |> Array.to_list
797797+ |> List.map (fun r ->
798798+ Printf.sprintf "%ld/%ld" r.snumerator r.sdenominator)
799799+ |> String.concat " "
800800+ | VFloat arr ->
801801+ arr |> Array.to_list |> List.map string_of_float |> String.concat " "
802802+ | VDouble arr ->
803803+ arr |> Array.to_list |> List.map string_of_float |> String.concat " "
804804+805805+let string_of_entry entry =
806806+ Printf.sprintf "%s: %s"
807807+ (Tag.name_of_tag entry.tag entry.ifd)
808808+ (string_of_value entry.value)
809809+810810+let string_of_ifd = function
811811+ | IFD0 -> "IFD0"
812812+ | IFD1 -> "IFD1"
813813+ | EXIF -> "EXIF"
814814+ | GPS -> "GPS"
815815+ | Interoperability -> "Interoperability"
816816+817817+let to_string exif =
818818+ exif.entries
819819+ |> List.map (fun entry ->
820820+ Printf.sprintf "[%s] %s" (string_of_ifd entry.ifd) (string_of_entry entry))
821821+ |> String.concat "\n"
+639
project/ocaml-exif/src/exif.mli
···11+(** {1 OCaml EXIF Library}
22+33+ A pure OCaml implementation of EXIF (Exchangeable Image File Format)
44+ metadata parsing, based on the {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
55+ EXIF 2.32 Specification} and {{:https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf}
66+ TIFF 6.0 Specification}.
77+88+ {2 Overview}
99+1010+ EXIF data is embedded in JPEG, TIFF, and other image formats to store
1111+ metadata about the image, including:
1212+1313+ - Camera make and model
1414+ - Date and time the image was captured
1515+ - Exposure settings (shutter speed, aperture, ISO)
1616+ - GPS coordinates
1717+ - Orientation information
1818+ - Thumbnail image
1919+2020+ {2 EXIF Structure}
2121+2222+ EXIF data follows the TIFF file format structure:
2323+2424+ {v
2525+ +-------------------------------------------+
2626+ | TIFF Header (8 bytes) |
2727+ | Byte order (II=Intel, MM=Motorola) |
2828+ | Magic number (42) |
2929+ | Offset to IFD0 |
3030+ +-------------------------------------------+
3131+ | IFD0 (Primary Image) |
3232+ | - Image dimensions |
3333+ | - Make, Model, Software |
3434+ | - DateTime |
3535+ | - Pointer to EXIF IFD |
3636+ | - Pointer to GPS IFD |
3737+ +-------------------------------------------+
3838+ | EXIF IFD (Camera Settings) |
3939+ | - ExposureTime, FNumber, ISO |
4040+ | - DateTimeOriginal |
4141+ | - FocalLength, Flash |
4242+ +-------------------------------------------+
4343+ | GPS IFD (Location Data) |
4444+ | - Latitude, Longitude, Altitude |
4545+ | - Timestamp, DateStamp |
4646+ +-------------------------------------------+
4747+ | IFD1 (Thumbnail Image) |
4848+ | - Thumbnail dimensions |
4949+ | - JPEG thumbnail data |
5050+ +-------------------------------------------+
5151+ v}
5252+5353+ {2 Quick Start}
5454+5555+ {[
5656+ (* Parse EXIF from raw APP1 data *)
5757+ let exif = Exif.parse_from_app1 app1_data in
5858+5959+ (* Get camera make and model *)
6060+ let make = Exif.make exif in
6161+ let model = Exif.model exif in
6262+6363+ (* Get GPS coordinates *)
6464+ match Exif.gps_latitude exif, Exif.gps_longitude exif with
6565+ | Some lat, Some lon ->
6666+ Printf.printf "Location: %f, %f\n" lat lon
6767+ | _ -> ()
6868+6969+ (* Dump all EXIF tags *)
7070+ print_endline (Exif.to_string exif)
7171+ ]}
7272+7373+ {2 References}
7474+7575+ - {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
7676+ EXIF 2.32 Specification (CIPA DC-008-Translation-2019)}
7777+ - {{:https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf}
7878+ TIFF 6.0 Specification}
7979+ - {{:https://exiftool.org/TagNames/EXIF.html} ExifTool Tag Reference} *)
8080+8181+(** {1 Types} *)
8282+8383+(** {2 Byte Order}
8484+8585+ TIFF/EXIF data can be stored in either byte order, indicated by
8686+ the first two bytes of the TIFF header.
8787+8888+ {{:https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf}
8989+ TIFF 6.0 Section 2: Structure of a TIFF File} *)
9090+9191+type byte_order =
9292+ | Big_endian
9393+ (** "MM" (0x4D4D) - Motorola byte order, most significant byte first *)
9494+ | Little_endian
9595+ (** "II" (0x4949) - Intel byte order, least significant byte first *)
9696+9797+(** {2 Data Formats}
9898+9999+ EXIF inherits TIFF's data type system. Each IFD entry specifies a format
100100+ code indicating how to interpret the value bytes.
101101+102102+ {{:https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf}
103103+ TIFF 6.0 Section 2: TIFF Field Types} *)
104104+105105+type format =
106106+ | Byte (** Type 1: 8-bit unsigned integer (1 byte) *)
107107+ | Ascii (** Type 2: 8-bit byte containing 7-bit ASCII, NUL-terminated *)
108108+ | Short (** Type 3: 16-bit unsigned integer (2 bytes) *)
109109+ | Long (** Type 4: 32-bit unsigned integer (4 bytes) *)
110110+ | Rational (** Type 5: Two LONGs - numerator and denominator (8 bytes) *)
111111+ | Sbyte (** Type 6: 8-bit signed integer (1 byte) *)
112112+ | Undefined (** Type 7: 8-bit byte, application-defined meaning *)
113113+ | Sshort (** Type 8: 16-bit signed integer (2 bytes) *)
114114+ | Slong (** Type 9: 32-bit signed integer (4 bytes) *)
115115+ | Srational (** Type 10: Two SLONGs - signed numerator/denominator (8 bytes) *)
116116+ | Float (** Type 11: IEEE 754 single precision (4 bytes) *)
117117+ | Double (** Type 12: IEEE 754 double precision (8 bytes) *)
118118+119119+(** {2 Image File Directories (IFDs)}
120120+121121+ EXIF data is organized into multiple IFDs, each containing related tags.
122122+123123+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
124124+ EXIF 2.32 Section 4.6: IFD Structure} *)
125125+126126+type ifd =
127127+ | IFD0
128128+ (** Primary image IFD - contains basic image info and pointers to
129129+ sub-IFDs. {{:https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf}
130130+ TIFF 6.0 Section 2} *)
131131+ | IFD1
132132+ (** Thumbnail image IFD - contains thumbnail image info.
133133+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
134134+ EXIF 2.32 Section 4.5.4} *)
135135+ | EXIF
136136+ (** EXIF private IFD - contains camera exposure data.
137137+ Pointed to by tag 0x8769 (ExifIFDPointer) in IFD0.
138138+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
139139+ EXIF 2.32 Section 4.6.3} *)
140140+ | GPS
141141+ (** GPS info IFD - contains geolocation data.
142142+ Pointed to by tag 0x8825 (GPSInfoIFDPointer) in IFD0.
143143+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
144144+ EXIF 2.32 Section 4.6.6} *)
145145+ | Interoperability
146146+ (** Interoperability IFD - contains compatibility info.
147147+ Pointed to by tag 0xA005 in EXIF IFD.
148148+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
149149+ EXIF 2.32 Section 4.6.4} *)
150150+151151+(** {2 Rational Numbers}
152152+153153+ EXIF uses rational numbers (numerator/denominator pairs) for
154154+ precise representation of values like exposure time and GPS coordinates. *)
155155+156156+(** Unsigned rational number (two 32-bit unsigned integers).
157157+ {{:https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf}
158158+ TIFF Type 5} *)
159159+type rational = {
160160+ numerator : int32;
161161+ denominator : int32;
162162+}
163163+164164+(** Signed rational number (two 32-bit signed integers).
165165+ {{:https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf}
166166+ TIFF Type 10} *)
167167+type srational = {
168168+ snumerator : int32;
169169+ sdenominator : int32;
170170+}
171171+172172+(** {2 Tag Values}
173173+174174+ Typed representation of EXIF tag values. The variant corresponds
175175+ to the format type of the IFD entry. *)
176176+177177+type value =
178178+ | VByte of int array
179179+ | VAscii of string
180180+ | VShort of int array
181181+ | VLong of int32 array
182182+ | VRational of rational array
183183+ | VSbyte of int array
184184+ | VUndefined of bytes
185185+ | VSshort of int array
186186+ | VSlong of int32 array
187187+ | VSrational of srational array
188188+ | VFloat of float array
189189+ | VDouble of float array
190190+191191+(** {2 EXIF Entry}
192192+193193+ A single tag entry from an IFD.
194194+195195+ {{:https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf}
196196+ TIFF 6.0 Section 2: IFD Entry} *)
197197+198198+type entry = {
199199+ tag : int;
200200+ (** Tag number identifying this field (e.g., 0x010F for Make) *)
201201+ ifd : ifd;
202202+ (** Which IFD this entry belongs to *)
203203+ format : format;
204204+ (** Data type of the value *)
205205+ components : int;
206206+ (** Number of values (array length) *)
207207+ value : value;
208208+ (** The actual value(s) *)
209209+}
210210+211211+(** {2 Complete EXIF Data}
212212+213213+ Parsed EXIF metadata container. *)
214214+215215+type t = {
216216+ byte_order : byte_order;
217217+ (** Byte order used in the original data *)
218218+ entries : entry list;
219219+ (** All parsed IFD entries *)
220220+ thumbnail : bytes option;
221221+ (** Embedded JPEG thumbnail image, if present *)
222222+}
223223+224224+(** {1 Tag Constants}
225225+226226+ Standard EXIF tag numbers organized by IFD.
227227+228228+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
229229+ EXIF 2.32 Section 4.6.4-4.6.8: Tag Tables} *)
230230+231231+module Tag : sig
232232+ (** {2 TIFF/IFD0 Tags}
233233+234234+ Basic image information tags from TIFF specification.
235235+ {{:https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf}
236236+ TIFF 6.0 Section 8: Baseline TIFF Tags}
237237+238238+ - [image_width] (0x0100): Image width in pixels
239239+ - [image_length] (0x0101): Image height in pixels
240240+ - [bits_per_sample] (0x0102): Bits per component
241241+ - [compression] (0x0103): Compression scheme
242242+ - [photometric_interpretation] (0x0106): Color space
243243+ - [image_description] (0x010E): Image title
244244+ - [make] (0x010F): Camera manufacturer
245245+ - [model] (0x0110): Camera model
246246+ - [orientation] (0x0112): Image orientation (1-8)
247247+ - [x_resolution] (0x011A): Horizontal resolution
248248+ - [y_resolution] (0x011B): Vertical resolution
249249+ - [resolution_unit] (0x0128): Resolution unit
250250+ - [software] (0x0131): Software used
251251+ - [date_time] (0x0132): File modification date
252252+ - [artist] (0x013B): Image creator
253253+ - [copyright] (0x8298): Copyright notice *)
254254+255255+ val image_width : int
256256+ val image_length : int
257257+ val bits_per_sample : int
258258+ val compression : int
259259+ val photometric_interpretation : int
260260+ val image_description : int
261261+ val make : int
262262+ val model : int
263263+ val strip_offsets : int
264264+ val orientation : int
265265+ val samples_per_pixel : int
266266+ val rows_per_strip : int
267267+ val strip_byte_counts : int
268268+ val x_resolution : int
269269+ val y_resolution : int
270270+ val planar_configuration : int
271271+ val resolution_unit : int
272272+ val transfer_function : int
273273+ val software : int
274274+ val date_time : int
275275+ val artist : int
276276+ val white_point : int
277277+ val primary_chromaticities : int
278278+ val jpeg_interchange_format : int
279279+ val jpeg_interchange_format_length : int
280280+ val ycbcr_coefficients : int
281281+ val ycbcr_sub_sampling : int
282282+ val ycbcr_positioning : int
283283+ val reference_black_white : int
284284+ val copyright : int
285285+ val exif_ifd_pointer : int
286286+ val gps_info_ifd_pointer : int
287287+288288+ (** {2 EXIF IFD Tags}
289289+290290+ Camera and exposure settings.
291291+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
292292+ EXIF 2.32 Section 4.6.5: EXIF IFD Tags}
293293+294294+ - [exposure_time] (0x829A): Exposure time in seconds
295295+ - [f_number] (0x829D): F-number (aperture)
296296+ - [iso_speed_ratings] (0x8827): ISO speed
297297+ - [exif_version] (0x9000): EXIF version
298298+ - [date_time_original] (0x9003): When photo was taken
299299+ - [date_time_digitized] (0x9004): When digitized
300300+ - [shutter_speed_value] (0x9201): Shutter speed (APEX)
301301+ - [aperture_value] (0x9202): Aperture (APEX)
302302+ - [focal_length] (0x920A): Focal length in mm
303303+ - [flash] (0x9209): Flash status
304304+ - [color_space] (0xA001): Color space (1=sRGB)
305305+ - [focal_length_in_35mm_film] (0xA405): 35mm equivalent *)
306306+307307+ val exposure_time : int
308308+ val f_number : int
309309+ val exposure_program : int
310310+ val spectral_sensitivity : int
311311+ val iso_speed_ratings : int
312312+ val oecf : int
313313+ val sensitivity_type : int
314314+ val exif_version : int
315315+ val date_time_original : int
316316+ val date_time_digitized : int
317317+ val offset_time : int
318318+ val offset_time_original : int
319319+ val offset_time_digitized : int
320320+ val components_configuration : int
321321+ val compressed_bits_per_pixel : int
322322+ val shutter_speed_value : int
323323+ val aperture_value : int
324324+ val brightness_value : int
325325+ val exposure_bias_value : int
326326+ val max_aperture_value : int
327327+ val subject_distance : int
328328+ val metering_mode : int
329329+ val light_source : int
330330+ val flash : int
331331+ val focal_length : int
332332+ val subject_area : int
333333+ val maker_note : int
334334+ val user_comment : int
335335+ val sub_sec_time : int
336336+ val sub_sec_time_original : int
337337+ val sub_sec_time_digitized : int
338338+ val flash_pix_version : int
339339+ val color_space : int
340340+ val pixel_x_dimension : int
341341+ val pixel_y_dimension : int
342342+ val related_sound_file : int
343343+ val interoperability_ifd_pointer : int
344344+ val flash_energy : int
345345+ val spatial_frequency_response : int
346346+ val focal_plane_x_resolution : int
347347+ val focal_plane_y_resolution : int
348348+ val focal_plane_resolution_unit : int
349349+ val subject_location : int
350350+ val exposure_index : int
351351+ val sensing_method : int
352352+ val file_source : int
353353+ val scene_type : int
354354+ val cfa_pattern : int
355355+ val custom_rendered : int
356356+ val exposure_mode : int
357357+ val white_balance : int
358358+ val digital_zoom_ratio : int
359359+ val focal_length_in_35mm_film : int
360360+ val scene_capture_type : int
361361+ val gain_control : int
362362+ val contrast : int
363363+ val saturation : int
364364+ val sharpness : int
365365+ val device_setting_description : int
366366+ val subject_distance_range : int
367367+ val image_unique_id : int
368368+ val camera_owner_name : int
369369+ val body_serial_number : int
370370+ val lens_specification : int
371371+ val lens_make : int
372372+ val lens_model : int
373373+ val lens_serial_number : int
374374+ val gamma : int
375375+376376+ (** {2 GPS IFD Tags}
377377+378378+ Geolocation information.
379379+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
380380+ EXIF 2.32 Section 4.6.6: GPS IFD Tags}
381381+382382+ - [gps_latitude_ref] (0x0001): "N" or "S"
383383+ - [gps_latitude] (0x0002): Latitude in DMS
384384+ - [gps_longitude_ref] (0x0003): "E" or "W"
385385+ - [gps_longitude] (0x0004): Longitude in DMS
386386+ - [gps_altitude_ref] (0x0005): 0=above, 1=below sea level
387387+ - [gps_altitude] (0x0006): Altitude in meters
388388+ - [gps_time_stamp] (0x0007): GPS time (UTC)
389389+ - [gps_date_stamp] (0x001D): GPS date *)
390390+391391+ val gps_version_id : int
392392+ val gps_latitude_ref : int
393393+ val gps_latitude : int
394394+ val gps_longitude_ref : int
395395+ val gps_longitude : int
396396+ val gps_altitude_ref : int
397397+ val gps_altitude : int
398398+ val gps_time_stamp : int
399399+ val gps_satellites : int
400400+ val gps_status : int
401401+ val gps_measure_mode : int
402402+ val gps_dop : int
403403+ val gps_speed_ref : int
404404+ val gps_speed : int
405405+ val gps_track_ref : int
406406+ val gps_track : int
407407+ val gps_img_direction_ref : int
408408+ val gps_img_direction : int
409409+ val gps_map_datum : int
410410+ val gps_dest_latitude_ref : int
411411+ val gps_dest_latitude : int
412412+ val gps_dest_longitude_ref : int
413413+ val gps_dest_longitude : int
414414+ val gps_dest_bearing_ref : int
415415+ val gps_dest_bearing : int
416416+ val gps_dest_distance_ref : int
417417+ val gps_dest_distance : int
418418+ val gps_processing_method : int
419419+ val gps_area_information : int
420420+ val gps_date_stamp : int
421421+ val gps_differential : int
422422+ val gps_h_positioning_error : int
423423+424424+ (** {2 Tag Name Lookup} *)
425425+426426+ val name_of_tag : int -> ifd -> string
427427+ (** Get human-readable name for a tag.
428428+429429+ @param tag Tag number
430430+ @param ifd IFD containing the tag
431431+ @return Tag name (e.g., "Make", "ExposureTime", "GPSLatitude") *)
432432+end
433433+434434+(** {1 Parsing} *)
435435+436436+exception Parse_error of string
437437+(** Parse error with description. *)
438438+439439+val parse : bytes -> t
440440+(** Parse complete EXIF data from raw TIFF-format bytes.
441441+442442+ The input should start with the TIFF header ("II" or "MM" byte order
443443+ marker, magic number 42, and IFD0 offset).
444444+445445+ @raise Parse_error if the data is malformed *)
446446+447447+val parse_from_app1 : bytes -> t
448448+(** Parse EXIF data from a JPEG APP1 segment.
449449+450450+ The APP1 marker in JPEG files contains EXIF data prefixed with
451451+ "Exif\x00\x00". This function expects the data {i after} that
452452+ prefix has been stripped.
453453+454454+ @raise Parse_error if the data is malformed *)
455455+456456+(** {1 Query Functions}
457457+458458+ Functions to find and extract tag entries. *)
459459+460460+val find_entry : int -> t -> entry option
461461+(** Find an entry by tag number, searching all IFDs. *)
462462+463463+val find_entry_in_ifd : int -> ifd -> t -> entry option
464464+(** Find an entry by tag number in a specific IFD. *)
465465+466466+val entries_in_ifd : ifd -> t -> entry list
467467+(** Get all entries belonging to a specific IFD. *)
468468+469469+(** {1 Value Extraction Helpers}
470470+471471+ Convenience functions to extract typed values from entries. *)
472472+473473+val get_string : entry -> string option
474474+(** Get ASCII string value. *)
475475+476476+val get_short : entry -> int option
477477+(** Get single SHORT (16-bit unsigned) value. *)
478478+479479+val get_long : entry -> int option
480480+(** Get single LONG (32-bit unsigned) value. *)
481481+482482+val get_rational : entry -> float option
483483+(** Get single RATIONAL value as a float. *)
484484+485485+val get_rationals : entry -> float array option
486486+(** Get array of RATIONAL values as floats. *)
487487+488488+(** {1 Common Metadata Accessors}
489489+490490+ High-level functions to retrieve common EXIF fields.
491491+ These search the appropriate IFDs and extract values. *)
492492+493493+(** {2 Camera Information} *)
494494+495495+val make : t -> string option
496496+(** Camera manufacturer (tag 0x010F in IFD0). *)
497497+498498+val model : t -> string option
499499+(** Camera model name (tag 0x0110 in IFD0). *)
500500+501501+val software : t -> string option
502502+(** Software used to create/modify the image (tag 0x0131 in IFD0). *)
503503+504504+val image_description : t -> string option
505505+(** Image title or description (tag 0x010E in IFD0). *)
506506+507507+val artist : t -> string option
508508+(** Creator/photographer name (tag 0x013B in IFD0). *)
509509+510510+val copyright : t -> string option
511511+(** Copyright notice (tag 0x8298 in IFD0). *)
512512+513513+(** {2 Date and Time} *)
514514+515515+val date_time_original : t -> string option
516516+(** Date/time when the original image was created (tag 0x9003 in EXIF IFD).
517517+ Format: "YYYY:MM:DD HH:MM:SS" *)
518518+519519+val date_time_digitized : t -> string option
520520+(** Date/time when the image was digitized (tag 0x9004 in EXIF IFD). *)
521521+522522+val date_time : t -> string option
523523+(** Date/time when the file was last modified (tag 0x0132 in IFD0). *)
524524+525525+(** {2 Image Properties} *)
526526+527527+val orientation : t -> int option
528528+(** Image orientation (tag 0x0112 in IFD0).
529529+530530+ Values 1-8 indicate rotation and mirroring:
531531+ - 1: Normal (no rotation)
532532+ - 2: Flipped horizontally
533533+ - 3: Rotated 180 degrees
534534+ - 4: Flipped vertically
535535+ - 5: Rotated 90 CCW, then flipped horizontally
536536+ - 6: Rotated 90 CW
537537+ - 7: Rotated 90 CW, then flipped horizontally
538538+ - 8: Rotated 90 CCW *)
539539+540540+val image_width : t -> int option
541541+(** Image width from EXIF (may differ from actual image size). *)
542542+543543+val image_height : t -> int option
544544+(** Image height from EXIF (may differ from actual image size). *)
545545+546546+val x_resolution : t -> float option
547547+(** Horizontal resolution (tag 0x011A in IFD0). *)
548548+549549+val y_resolution : t -> float option
550550+(** Vertical resolution (tag 0x011B in IFD0). *)
551551+552552+val resolution_unit : t -> int option
553553+(** Resolution unit (tag 0x0128 in IFD0).
554554+ - 1: No unit (aspect ratio only)
555555+ - 2: Dots per inch
556556+ - 3: Dots per centimeter *)
557557+558558+(** {2 Exposure Settings} *)
559559+560560+val exposure_time : t -> float option
561561+(** Exposure time in seconds (tag 0x829A in EXIF IFD).
562562+ Example: 0.001 = 1/1000 second *)
563563+564564+val f_number : t -> float option
565565+(** F-number (aperture) (tag 0x829D in EXIF IFD).
566566+ Example: 2.8 = f/2.8 *)
567567+568568+val iso_speed : t -> int option
569569+(** ISO speed rating (tag 0x8827 in EXIF IFD). *)
570570+571571+val focal_length : t -> float option
572572+(** Focal length in millimeters (tag 0x920A in EXIF IFD). *)
573573+574574+val focal_length_35mm : t -> int option
575575+(** 35mm equivalent focal length (tag 0xA405 in EXIF IFD). *)
576576+577577+val flash : t -> int option
578578+(** Flash status (tag 0x9209 in EXIF IFD).
579579+580580+ Bit field:
581581+ - Bit 0: Flash fired
582582+ - Bits 1-2: Flash return status
583583+ - Bits 3-4: Flash mode
584584+ - Bit 5: Flash function present
585585+ - Bit 6: Red-eye reduction *)
586586+587587+(** {2 Color Information} *)
588588+589589+val color_space : t -> int option
590590+(** Color space (tag 0xA001 in EXIF IFD).
591591+ - 1: sRGB
592592+ - 65535: Uncalibrated *)
593593+594594+val exif_version : t -> string option
595595+(** EXIF version string (tag 0x9000 in EXIF IFD).
596596+ Example: "0232" for EXIF 2.32 *)
597597+598598+(** {1 GPS Accessors}
599599+600600+ Functions to extract GPS/geolocation data from the GPS IFD.
601601+602602+ GPS coordinates are stored as three RATIONAL values representing
603603+ degrees, minutes, and seconds (DMS format). These functions convert
604604+ to decimal degrees for easier use. *)
605605+606606+val gps_latitude : t -> float option
607607+(** GPS latitude in decimal degrees.
608608+ Positive values indicate North, negative values indicate South. *)
609609+610610+val gps_longitude : t -> float option
611611+(** GPS longitude in decimal degrees.
612612+ Positive values indicate East, negative values indicate West. *)
613613+614614+val gps_altitude : t -> float option
615615+(** GPS altitude in meters.
616616+ Positive values indicate above sea level, negative below. *)
617617+618618+(** {1 Pretty Printing}
619619+620620+ Functions for converting EXIF data to human-readable strings. *)
621621+622622+val string_of_value : value -> string
623623+(** Convert a value to a display string.
624624+625625+ - Arrays are space-separated
626626+ - Rationals are shown as "num/denom"
627627+ - Long byte arrays show "<N bytes>" *)
628628+629629+val string_of_entry : entry -> string
630630+(** Convert an entry to a human-readable string.
631631+ Format: "TagName: value" *)
632632+633633+val string_of_ifd : ifd -> string
634634+(** Convert IFD type to string (e.g., "IFD0", "EXIF", "GPS"). *)
635635+636636+val to_string : t -> string
637637+(** Dump all EXIF data to a multi-line string.
638638+ Each entry is shown on its own line in the format:
639639+ "[IFD] TagName: value" *)
+87
project/ocaml-exif/src/exif_jpeg.ml
···11+(** JPEG EXIF extraction
22+33+ Extract EXIF data from JPEG files by scanning marker structure.
44+ No full JPEG decoding required - just marker parsing. *)
55+66+(** Error type for JPEG extraction *)
77+type error =
88+ | Not_a_jpeg
99+ | Truncated
1010+ | No_exif
1111+1212+let string_of_error = function
1313+ | Not_a_jpeg -> "Not a JPEG file"
1414+ | Truncated -> "Truncated JPEG file"
1515+ | No_exif -> "No EXIF data found"
1616+1717+(** Extract raw EXIF/TIFF data from JPEG bytes.
1818+ Returns the TIFF data (after "Exif\x00\x00" prefix). *)
1919+let extract_exif_bytes data =
2020+ let len = Bytes.length data in
2121+ if len < 2 then
2222+ Error Truncated
2323+ else if Bytes.get_uint8 data 0 <> 0xFF || Bytes.get_uint8 data 1 <> 0xD8 then
2424+ Error Not_a_jpeg
2525+ else
2626+ let rec scan_markers pos =
2727+ if pos + 2 > len then Error Truncated
2828+ else if Bytes.get_uint8 data pos <> 0xFF then Error Truncated
2929+ else
3030+ let marker = Bytes.get_uint8 data (pos + 1) in
3131+ match marker with
3232+ | 0xD9 -> Error No_exif (* EOI *)
3333+ | 0xD8 -> scan_markers (pos + 2) (* SOI *)
3434+ | 0x00 -> scan_markers (pos + 1) (* Stuffed byte *)
3535+ | m when m >= 0xD0 && m <= 0xD7 ->
3636+ scan_markers (pos + 2) (* RST markers *)
3737+ | 0xE1 -> (* APP1 *)
3838+ if pos + 4 > len then Error Truncated
3939+ else
4040+ let seg_len = Bytes.get_uint16_be data (pos + 2) in
4141+ if pos + 2 + seg_len > len then Error Truncated
4242+ else if seg_len >= 8
4343+ && Bytes.get_uint8 data (pos + 4) = 0x45 (* E *)
4444+ && Bytes.get_uint8 data (pos + 5) = 0x78 (* x *)
4545+ && Bytes.get_uint8 data (pos + 6) = 0x69 (* i *)
4646+ && Bytes.get_uint8 data (pos + 7) = 0x66 (* f *)
4747+ && Bytes.get_uint8 data (pos + 8) = 0x00
4848+ && Bytes.get_uint8 data (pos + 9) = 0x00 then
4949+ let tiff_start = pos + 10 in
5050+ let tiff_len = seg_len - 8 in
5151+ Ok (Bytes.sub data tiff_start tiff_len)
5252+ else
5353+ scan_markers (pos + 2 + seg_len)
5454+ | _ ->
5555+ if pos + 4 > len then Error Truncated
5656+ else
5757+ let seg_len = Bytes.get_uint16_be data (pos + 2) in
5858+ scan_markers (pos + 2 + seg_len)
5959+ in
6060+ scan_markers 2
6161+6262+(** Extract and parse EXIF from JPEG bytes. *)
6363+let extract_exif data =
6464+ match extract_exif_bytes data with
6565+ | Error e -> Error e
6666+ | Ok tiff_data ->
6767+ try Ok (Exif.parse tiff_data)
6868+ with Exif.Parse_error _ ->
6969+ Error (Truncated) (* Treat parse errors as truncation *)
7070+7171+(** Read JPEG file and extract EXIF. *)
7272+let from_file filename =
7373+ let ic = open_in_bin filename in
7474+ let len = in_channel_length ic in
7575+ let data = Bytes.create len in
7676+ really_input ic data 0 len;
7777+ close_in ic;
7878+ extract_exif data
7979+8080+(** Read JPEG file and extract raw EXIF bytes. *)
8181+let bytes_from_file filename =
8282+ let ic = open_in_bin filename in
8383+ let len = in_channel_length ic in
8484+ let data = Bytes.create len in
8585+ really_input ic data 0 len;
8686+ close_in ic;
8787+ extract_exif_bytes data
+41
project/ocaml-exif/src/exif_jpeg.mli
···11+(** {1 JPEG EXIF Extraction}
22+33+ Extract EXIF metadata from JPEG files without full image decoding.
44+55+ JPEG files store EXIF data in APP1 markers with an "Exif\x00\x00" prefix.
66+ This module scans the marker structure to locate and extract EXIF data.
77+88+ {2 Example}
99+1010+ {[
1111+ match Exif_jpeg.from_file "photo.jpg" with
1212+ | Ok exif ->
1313+ Printf.printf "Camera: %s\n"
1414+ (Option.value ~default:"unknown" (Exif.model exif))
1515+ | Error e ->
1616+ Printf.eprintf "Error: %s\n" (Exif_jpeg.string_of_error e)
1717+ ]} *)
1818+1919+(** Error types for JPEG extraction. *)
2020+type error =
2121+ | Not_a_jpeg (** File doesn't start with JPEG SOI marker *)
2222+ | Truncated (** File is truncated or malformed *)
2323+ | No_exif (** No EXIF APP1 marker found *)
2424+2525+val string_of_error : error -> string
2626+(** Convert error to human-readable string. *)
2727+2828+val extract_exif_bytes : bytes -> (bytes, error) result
2929+(** Extract raw EXIF/TIFF data from JPEG bytes.
3030+3131+ Returns the TIFF-format data after the "Exif\x00\x00" prefix.
3232+ This can be passed to {!Exif.parse} for parsing. *)
3333+3434+val extract_exif : bytes -> (Exif.t, error) result
3535+(** Extract and parse EXIF from JPEG bytes in one step. *)
3636+3737+val from_file : string -> (Exif.t, error) result
3838+(** Read JPEG file and extract EXIF metadata. *)
3939+4040+val bytes_from_file : string -> (bytes, error) result
4141+(** Read JPEG file and extract raw EXIF bytes. *)
+412
project/ocaml-exif/src/exif_pp.ml
···11+(** Pretty printing for EXIF data
22+33+ Comprehensive formatters for all EXIF types with human-readable
44+ output for common tag values. *)
55+66+open Exif
77+88+(** {1 Format Printers} *)
99+1010+let pp_byte_order fmt = function
1111+ | Big_endian -> Format.fprintf fmt "Big-endian (Motorola)"
1212+ | Little_endian -> Format.fprintf fmt "Little-endian (Intel)"
1313+1414+let pp_format fmt = function
1515+ | Byte -> Format.fprintf fmt "BYTE"
1616+ | Ascii -> Format.fprintf fmt "ASCII"
1717+ | Short -> Format.fprintf fmt "SHORT"
1818+ | Long -> Format.fprintf fmt "LONG"
1919+ | Rational -> Format.fprintf fmt "RATIONAL"
2020+ | Sbyte -> Format.fprintf fmt "SBYTE"
2121+ | Undefined -> Format.fprintf fmt "UNDEFINED"
2222+ | Sshort -> Format.fprintf fmt "SSHORT"
2323+ | Slong -> Format.fprintf fmt "SLONG"
2424+ | Srational -> Format.fprintf fmt "SRATIONAL"
2525+ | Float -> Format.fprintf fmt "FLOAT"
2626+ | Double -> Format.fprintf fmt "DOUBLE"
2727+2828+let pp_ifd fmt = function
2929+ | IFD0 -> Format.fprintf fmt "IFD0"
3030+ | IFD1 -> Format.fprintf fmt "IFD1 (Thumbnail)"
3131+ | EXIF -> Format.fprintf fmt "EXIF"
3232+ | GPS -> Format.fprintf fmt "GPS"
3333+ | Interoperability -> Format.fprintf fmt "Interoperability"
3434+3535+(** {1 Value Printers} *)
3636+3737+let pp_rational fmt { numerator; denominator } =
3838+ if denominator = 0l then
3939+ Format.fprintf fmt "%ld/0" numerator
4040+ else if denominator = 1l then
4141+ Format.fprintf fmt "%ld" numerator
4242+ else
4343+ let f = Int32.to_float numerator /. Int32.to_float denominator in
4444+ if Float.is_integer f then
4545+ Format.fprintf fmt "%.0f" f
4646+ else
4747+ Format.fprintf fmt "%ld/%ld (%.4g)" numerator denominator f
4848+4949+let pp_srational fmt { snumerator; sdenominator } =
5050+ if sdenominator = 0l then
5151+ Format.fprintf fmt "%ld/0" snumerator
5252+ else if sdenominator = 1l then
5353+ Format.fprintf fmt "%ld" snumerator
5454+ else
5555+ let f = Int32.to_float snumerator /. Int32.to_float sdenominator in
5656+ Format.fprintf fmt "%ld/%ld (%.4g)" snumerator sdenominator f
5757+5858+let pp_bytes_hex fmt b =
5959+ let len = Bytes.length b in
6060+ if len <= 32 then
6161+ for i = 0 to len - 1 do
6262+ if i > 0 then Format.fprintf fmt " ";
6363+ Format.fprintf fmt "%02X" (Bytes.get_uint8 b i)
6464+ done
6565+ else begin
6666+ for i = 0 to 15 do
6767+ if i > 0 then Format.fprintf fmt " ";
6868+ Format.fprintf fmt "%02X" (Bytes.get_uint8 b i)
6969+ done;
7070+ Format.fprintf fmt " ... <%d more bytes>" (len - 16)
7171+ end
7272+7373+let pp_value fmt = function
7474+ | VByte arr ->
7575+ if Array.length arr = 1 then
7676+ Format.fprintf fmt "%d" arr.(0)
7777+ else
7878+ Format.fprintf fmt "[%s]"
7979+ (String.concat ", " (Array.to_list (Array.map string_of_int arr)))
8080+ | VAscii s ->
8181+ Format.fprintf fmt "\"%s\"" (String.escaped s)
8282+ | VShort arr ->
8383+ if Array.length arr = 1 then
8484+ Format.fprintf fmt "%d" arr.(0)
8585+ else
8686+ Format.fprintf fmt "[%s]"
8787+ (String.concat ", " (Array.to_list (Array.map string_of_int arr)))
8888+ | VLong arr ->
8989+ if Array.length arr = 1 then
9090+ Format.fprintf fmt "%ld" arr.(0)
9191+ else
9292+ Format.fprintf fmt "[%s]"
9393+ (String.concat ", " (Array.to_list (Array.map Int32.to_string arr)))
9494+ | VRational arr ->
9595+ if Array.length arr = 1 then
9696+ pp_rational fmt arr.(0)
9797+ else begin
9898+ Format.fprintf fmt "[";
9999+ Array.iteri (fun i r ->
100100+ if i > 0 then Format.fprintf fmt ", ";
101101+ pp_rational fmt r) arr;
102102+ Format.fprintf fmt "]"
103103+ end
104104+ | VSbyte arr ->
105105+ Format.fprintf fmt "[%s]"
106106+ (String.concat ", " (Array.to_list (Array.map string_of_int arr)))
107107+ | VUndefined b ->
108108+ pp_bytes_hex fmt b
109109+ | VSshort arr ->
110110+ Format.fprintf fmt "[%s]"
111111+ (String.concat ", " (Array.to_list (Array.map string_of_int arr)))
112112+ | VSlong arr ->
113113+ Format.fprintf fmt "[%s]"
114114+ (String.concat ", " (Array.to_list (Array.map Int32.to_string arr)))
115115+ | VSrational arr ->
116116+ Format.fprintf fmt "[";
117117+ Array.iteri (fun i r ->
118118+ if i > 0 then Format.fprintf fmt ", ";
119119+ pp_srational fmt r) arr;
120120+ Format.fprintf fmt "]"
121121+ | VFloat arr ->
122122+ Format.fprintf fmt "[%s]"
123123+ (String.concat ", " (Array.to_list (Array.map
124124+ (Printf.sprintf "%.6g") arr)))
125125+ | VDouble arr ->
126126+ Format.fprintf fmt "[%s]"
127127+ (String.concat ", " (Array.to_list (Array.map
128128+ (Printf.sprintf "%.10g") arr)))
129129+130130+(** {1 Semantic Value Interpreters} *)
131131+132132+let pp_orientation fmt = function
133133+ | 1 -> Format.fprintf fmt "Normal"
134134+ | 2 -> Format.fprintf fmt "Flipped horizontally"
135135+ | 3 -> Format.fprintf fmt "Rotated 180°"
136136+ | 4 -> Format.fprintf fmt "Flipped vertically"
137137+ | 5 -> Format.fprintf fmt "Rotated 90° CCW, flipped horizontally"
138138+ | 6 -> Format.fprintf fmt "Rotated 90° CW"
139139+ | 7 -> Format.fprintf fmt "Rotated 90° CW, flipped horizontally"
140140+ | 8 -> Format.fprintf fmt "Rotated 90° CCW"
141141+ | n -> Format.fprintf fmt "Unknown (%d)" n
142142+143143+let pp_resolution_unit fmt = function
144144+ | 1 -> Format.fprintf fmt "None"
145145+ | 2 -> Format.fprintf fmt "inches"
146146+ | 3 -> Format.fprintf fmt "centimeters"
147147+ | n -> Format.fprintf fmt "Unknown (%d)" n
148148+149149+let pp_exposure_program fmt = function
150150+ | 0 -> Format.fprintf fmt "Not defined"
151151+ | 1 -> Format.fprintf fmt "Manual"
152152+ | 2 -> Format.fprintf fmt "Normal program"
153153+ | 3 -> Format.fprintf fmt "Aperture priority"
154154+ | 4 -> Format.fprintf fmt "Shutter priority"
155155+ | 5 -> Format.fprintf fmt "Creative program"
156156+ | 6 -> Format.fprintf fmt "Action program"
157157+ | 7 -> Format.fprintf fmt "Portrait mode"
158158+ | 8 -> Format.fprintf fmt "Landscape mode"
159159+ | n -> Format.fprintf fmt "Unknown (%d)" n
160160+161161+let pp_metering_mode fmt = function
162162+ | 0 -> Format.fprintf fmt "Unknown"
163163+ | 1 -> Format.fprintf fmt "Average"
164164+ | 2 -> Format.fprintf fmt "Center-weighted average"
165165+ | 3 -> Format.fprintf fmt "Spot"
166166+ | 4 -> Format.fprintf fmt "Multi-spot"
167167+ | 5 -> Format.fprintf fmt "Pattern"
168168+ | 6 -> Format.fprintf fmt "Partial"
169169+ | 255 -> Format.fprintf fmt "Other"
170170+ | n -> Format.fprintf fmt "Unknown (%d)" n
171171+172172+let pp_light_source fmt = function
173173+ | 0 -> Format.fprintf fmt "Unknown"
174174+ | 1 -> Format.fprintf fmt "Daylight"
175175+ | 2 -> Format.fprintf fmt "Fluorescent"
176176+ | 3 -> Format.fprintf fmt "Tungsten (incandescent)"
177177+ | 4 -> Format.fprintf fmt "Flash"
178178+ | 9 -> Format.fprintf fmt "Fine weather"
179179+ | 10 -> Format.fprintf fmt "Cloudy weather"
180180+ | 11 -> Format.fprintf fmt "Shade"
181181+ | 12 -> Format.fprintf fmt "Daylight fluorescent (D 5700-7100K)"
182182+ | 13 -> Format.fprintf fmt "Day white fluorescent (N 4600-5500K)"
183183+ | 14 -> Format.fprintf fmt "Cool white fluorescent (W 3800-4500K)"
184184+ | 15 -> Format.fprintf fmt "White fluorescent (WW 3200-3700K)"
185185+ | 16 -> Format.fprintf fmt "Warm white fluorescent (L 2600-3250K)"
186186+ | 17 -> Format.fprintf fmt "Standard light A"
187187+ | 18 -> Format.fprintf fmt "Standard light B"
188188+ | 19 -> Format.fprintf fmt "Standard light C"
189189+ | 20 -> Format.fprintf fmt "D55"
190190+ | 21 -> Format.fprintf fmt "D65"
191191+ | 22 -> Format.fprintf fmt "D75"
192192+ | 23 -> Format.fprintf fmt "D50"
193193+ | 24 -> Format.fprintf fmt "ISO studio tungsten"
194194+ | 255 -> Format.fprintf fmt "Other"
195195+ | n -> Format.fprintf fmt "Unknown (%d)" n
196196+197197+let pp_flash fmt v =
198198+ let fired = v land 1 = 1 in
199199+ let return_status = (v lsr 1) land 3 in
200200+ let mode = (v lsr 3) land 3 in
201201+ let function_present = (v lsr 5) land 1 = 0 in
202202+ let red_eye = (v lsr 6) land 1 = 1 in
203203+ Format.fprintf fmt "%s" (if fired then "Fired" else "Did not fire");
204204+ (match return_status with
205205+ | 2 -> Format.fprintf fmt ", no strobe return"
206206+ | 3 -> Format.fprintf fmt ", strobe return detected"
207207+ | _ -> ());
208208+ (match mode with
209209+ | 1 -> Format.fprintf fmt ", compulsory on"
210210+ | 2 -> Format.fprintf fmt ", compulsory off"
211211+ | 3 -> Format.fprintf fmt ", auto"
212212+ | _ -> ());
213213+ if not function_present then Format.fprintf fmt ", no flash function";
214214+ if red_eye then Format.fprintf fmt ", red-eye reduction"
215215+216216+let pp_color_space fmt = function
217217+ | 1 -> Format.fprintf fmt "sRGB"
218218+ | 65535 -> Format.fprintf fmt "Uncalibrated"
219219+ | n -> Format.fprintf fmt "Unknown (%d)" n
220220+221221+let pp_exposure_mode fmt = function
222222+ | 0 -> Format.fprintf fmt "Auto"
223223+ | 1 -> Format.fprintf fmt "Manual"
224224+ | 2 -> Format.fprintf fmt "Auto bracket"
225225+ | n -> Format.fprintf fmt "Unknown (%d)" n
226226+227227+let pp_white_balance fmt = function
228228+ | 0 -> Format.fprintf fmt "Auto"
229229+ | 1 -> Format.fprintf fmt "Manual"
230230+ | n -> Format.fprintf fmt "Unknown (%d)" n
231231+232232+let pp_scene_capture_type fmt = function
233233+ | 0 -> Format.fprintf fmt "Standard"
234234+ | 1 -> Format.fprintf fmt "Landscape"
235235+ | 2 -> Format.fprintf fmt "Portrait"
236236+ | 3 -> Format.fprintf fmt "Night scene"
237237+ | n -> Format.fprintf fmt "Unknown (%d)" n
238238+239239+let pp_contrast fmt = function
240240+ | 0 -> Format.fprintf fmt "Normal"
241241+ | 1 -> Format.fprintf fmt "Soft"
242242+ | 2 -> Format.fprintf fmt "Hard"
243243+ | n -> Format.fprintf fmt "Unknown (%d)" n
244244+245245+let pp_saturation fmt = function
246246+ | 0 -> Format.fprintf fmt "Normal"
247247+ | 1 -> Format.fprintf fmt "Low"
248248+ | 2 -> Format.fprintf fmt "High"
249249+ | n -> Format.fprintf fmt "Unknown (%d)" n
250250+251251+let pp_sharpness fmt = function
252252+ | 0 -> Format.fprintf fmt "Normal"
253253+ | 1 -> Format.fprintf fmt "Soft"
254254+ | 2 -> Format.fprintf fmt "Hard"
255255+ | n -> Format.fprintf fmt "Unknown (%d)" n
256256+257257+let pp_sensing_method fmt = function
258258+ | 1 -> Format.fprintf fmt "Not defined"
259259+ | 2 -> Format.fprintf fmt "One-chip color area sensor"
260260+ | 3 -> Format.fprintf fmt "Two-chip color area sensor"
261261+ | 4 -> Format.fprintf fmt "Three-chip color area sensor"
262262+ | 5 -> Format.fprintf fmt "Color sequential area sensor"
263263+ | 7 -> Format.fprintf fmt "Trilinear sensor"
264264+ | 8 -> Format.fprintf fmt "Color sequential linear sensor"
265265+ | n -> Format.fprintf fmt "Unknown (%d)" n
266266+267267+let pp_compression fmt = function
268268+ | 1 -> Format.fprintf fmt "Uncompressed"
269269+ | 6 -> Format.fprintf fmt "JPEG"
270270+ | n -> Format.fprintf fmt "Unknown (%d)" n
271271+272272+let pp_gps_altitude_ref fmt = function
273273+ | 0 -> Format.fprintf fmt "Above sea level"
274274+ | 1 -> Format.fprintf fmt "Below sea level"
275275+ | n -> Format.fprintf fmt "Unknown (%d)" n
276276+277277+(** Format exposure time as fraction *)
278278+let pp_exposure_time fmt r =
279279+ let num = Int32.to_float r.numerator in
280280+ let den = Int32.to_float r.denominator in
281281+ if den = 0. then
282282+ Format.fprintf fmt "Invalid"
283283+ else
284284+ let t = num /. den in
285285+ if t >= 1. then
286286+ Format.fprintf fmt "%.1f sec" t
287287+ else if t >= 0.1 then
288288+ Format.fprintf fmt "1/%.0f sec" (1. /. t)
289289+ else
290290+ Format.fprintf fmt "1/%.0f sec" (den /. num)
291291+292292+(** Format f-number *)
293293+let pp_fnumber fmt r =
294294+ let f = Int32.to_float r.numerator /. Int32.to_float r.denominator in
295295+ Format.fprintf fmt "f/%.1f" f
296296+297297+(** Format focal length *)
298298+let pp_focal_length fmt r =
299299+ let f = Int32.to_float r.numerator /. Int32.to_float r.denominator in
300300+ Format.fprintf fmt "%.1f mm" f
301301+302302+(** Format GPS coordinates in DMS *)
303303+let pp_gps_coord fmt arr ref_str =
304304+ if Array.length arr >= 3 then
305305+ let d = Int32.to_float arr.(0).numerator /. Int32.to_float arr.(0).denominator in
306306+ let m = Int32.to_float arr.(1).numerator /. Int32.to_float arr.(1).denominator in
307307+ let s = Int32.to_float arr.(2).numerator /. Int32.to_float arr.(2).denominator in
308308+ Format.fprintf fmt "%.0f° %.0f' %.2f\" %s (%.6f°)"
309309+ d m s ref_str (d +. m /. 60. +. s /. 3600.)
310310+ else
311311+ Format.fprintf fmt "Invalid"
312312+313313+(** {1 Entry Pretty Printer} *)
314314+315315+let get_short_value = function
316316+ | VShort [| v |] -> Some v
317317+ | _ -> None
318318+319319+let get_rational_value = function
320320+ | VRational [| r |] -> Some r
321321+ | _ -> None
322322+323323+let get_byte_value = function
324324+ | VByte [| v |] -> Some v
325325+ | _ -> None
326326+327327+(** Print entry with semantic interpretation where applicable *)
328328+let pp_entry_interpreted fmt entry =
329329+ let tag_name = Tag.name_of_tag entry.tag entry.ifd in
330330+ Format.fprintf fmt "@[<h>%-30s " tag_name;
331331+332332+ (* Try to interpret known tags semantically *)
333333+ let interpreted = match entry.tag, entry.ifd with
334334+ | 0x0112, (IFD0 | IFD1) -> (* Orientation *)
335335+ Option.map (fun v -> pp_orientation fmt v) (get_short_value entry.value)
336336+ | 0x0128, (IFD0 | IFD1) -> (* ResolutionUnit *)
337337+ Option.map (fun v -> pp_resolution_unit fmt v) (get_short_value entry.value)
338338+ | 0x0103, (IFD0 | IFD1) -> (* Compression *)
339339+ Option.map (fun v -> pp_compression fmt v) (get_short_value entry.value)
340340+ | 0x8822, EXIF -> (* ExposureProgram *)
341341+ Option.map (fun v -> pp_exposure_program fmt v) (get_short_value entry.value)
342342+ | 0x9207, EXIF -> (* MeteringMode *)
343343+ Option.map (fun v -> pp_metering_mode fmt v) (get_short_value entry.value)
344344+ | 0x9208, EXIF -> (* LightSource *)
345345+ Option.map (fun v -> pp_light_source fmt v) (get_short_value entry.value)
346346+ | 0x9209, EXIF -> (* Flash *)
347347+ Option.map (fun v -> pp_flash fmt v) (get_short_value entry.value)
348348+ | 0xA001, EXIF -> (* ColorSpace *)
349349+ Option.map (fun v -> pp_color_space fmt v) (get_short_value entry.value)
350350+ | 0xA402, EXIF -> (* ExposureMode *)
351351+ Option.map (fun v -> pp_exposure_mode fmt v) (get_short_value entry.value)
352352+ | 0xA403, EXIF -> (* WhiteBalance *)
353353+ Option.map (fun v -> pp_white_balance fmt v) (get_short_value entry.value)
354354+ | 0xA406, EXIF -> (* SceneCaptureType *)
355355+ Option.map (fun v -> pp_scene_capture_type fmt v) (get_short_value entry.value)
356356+ | 0xA408, EXIF -> (* Contrast *)
357357+ Option.map (fun v -> pp_contrast fmt v) (get_short_value entry.value)
358358+ | 0xA409, EXIF -> (* Saturation *)
359359+ Option.map (fun v -> pp_saturation fmt v) (get_short_value entry.value)
360360+ | 0xA40A, EXIF -> (* Sharpness *)
361361+ Option.map (fun v -> pp_sharpness fmt v) (get_short_value entry.value)
362362+ | 0xA217, EXIF -> (* SensingMethod *)
363363+ Option.map (fun v -> pp_sensing_method fmt v) (get_short_value entry.value)
364364+ | 0x829A, EXIF -> (* ExposureTime *)
365365+ Option.map (fun r -> pp_exposure_time fmt r) (get_rational_value entry.value)
366366+ | 0x829D, EXIF -> (* FNumber *)
367367+ Option.map (fun r -> pp_fnumber fmt r) (get_rational_value entry.value)
368368+ | 0x920A, EXIF -> (* FocalLength *)
369369+ Option.map (fun r -> pp_focal_length fmt r) (get_rational_value entry.value)
370370+ | 0x0005, GPS -> (* GPSAltitudeRef *)
371371+ Option.map (fun v -> pp_gps_altitude_ref fmt v) (get_byte_value entry.value)
372372+ | _ -> None
373373+ in
374374+375375+ if Option.is_none interpreted then
376376+ pp_value fmt entry.value;
377377+378378+ Format.fprintf fmt "@]"
379379+380380+(** {1 Full EXIF Pretty Printer} *)
381381+382382+let pp_exif fmt exif =
383383+ Format.fprintf fmt "@[<v>";
384384+ Format.fprintf fmt "Byte order: %a@," pp_byte_order exif.byte_order;
385385+ Format.fprintf fmt "Entries: %d@,@," (List.length exif.entries);
386386+387387+ (* Group entries by IFD *)
388388+ let ifds = [IFD0; EXIF; GPS; IFD1; Interoperability] in
389389+ List.iter (fun ifd ->
390390+ let entries = Exif.entries_in_ifd ifd exif in
391391+ if entries <> [] then begin
392392+ Format.fprintf fmt "@[<v 2>[%a]@," pp_ifd ifd;
393393+ List.iter (fun e ->
394394+ Format.fprintf fmt "%a@," pp_entry_interpreted e) entries;
395395+ Format.fprintf fmt "@]@,"
396396+ end) ifds;
397397+398398+ (* Thumbnail info *)
399399+ (match exif.thumbnail with
400400+ | Some thumb ->
401401+ Format.fprintf fmt "@,Thumbnail: %d bytes (JPEG)" (Bytes.length thumb)
402402+ | None -> ());
403403+404404+ Format.fprintf fmt "@]"
405405+406406+(** Print to string *)
407407+let to_string exif =
408408+ Format.asprintf "%a" pp_exif exif
409409+410410+(** Print entry to string *)
411411+let entry_to_string entry =
412412+ Format.asprintf "%a" pp_entry_interpreted entry
+151
project/ocaml-exif/src/exif_pp.mli
···11+(** {1 EXIF Pretty Printing}
22+33+ Comprehensive formatters for all EXIF types with human-readable
44+ output for common tag values.
55+66+ {2 Overview}
77+88+ This module provides Format-style pretty printers for EXIF data types.
99+ Many EXIF tags have enumerated values with specific meanings defined in
1010+ the {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
1111+ EXIF 2.32 Specification}. These printers decode enumerated values to
1212+ human-readable descriptions.
1313+1414+ {2 Example}
1515+1616+ {[
1717+ let () =
1818+ let exif = Exif.parse data in
1919+ Format.printf "%a@." Exif_pp.pp_exif exif
2020+ ]} *)
2121+2222+open Exif
2323+2424+(** {1 Basic Type Printers} *)
2525+2626+val pp_byte_order : Format.formatter -> byte_order -> unit
2727+(** Print byte order as "Big-endian (Motorola)" or "Little-endian (Intel)". *)
2828+2929+val pp_format : Format.formatter -> format -> unit
3030+(** Print data format name (e.g., "BYTE", "RATIONAL", "ASCII"). *)
3131+3232+val pp_ifd : Format.formatter -> ifd -> unit
3333+(** Print IFD name (e.g., "IFD0", "EXIF", "GPS"). *)
3434+3535+(** {1 Value Printers} *)
3636+3737+val pp_rational : Format.formatter -> rational -> unit
3838+(** Print rational value. Shows as "num/denom (decimal)" or just
3939+ the simplified value if denominator is 1. *)
4040+4141+val pp_srational : Format.formatter -> srational -> unit
4242+(** Print signed rational value. *)
4343+4444+val pp_bytes_hex : Format.formatter -> bytes -> unit
4545+(** Print bytes as hex. Truncates after 32 bytes. *)
4646+4747+val pp_value : Format.formatter -> value -> unit
4848+(** Print any EXIF value. Arrays are formatted with brackets. *)
4949+5050+(** {1 Semantic Interpreters}
5151+5252+ These printers decode enumerated tag values to human-readable
5353+ descriptions per the EXIF specification. *)
5454+5555+val pp_orientation : Format.formatter -> int -> unit
5656+(** Print orientation value (1-8) as description.
5757+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
5858+ EXIF 2.32 Section 4.6.4.A Orientation} *)
5959+6060+val pp_resolution_unit : Format.formatter -> int -> unit
6161+(** Print resolution unit (1=none, 2=inches, 3=cm). *)
6262+6363+val pp_exposure_program : Format.formatter -> int -> unit
6464+(** Print exposure program (0-8: Manual, Aperture Priority, etc).
6565+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
6666+ EXIF 2.32 Section 4.6.5.B ExposureProgram} *)
6767+6868+val pp_metering_mode : Format.formatter -> int -> unit
6969+(** Print metering mode (1-6: Average, Spot, Pattern, etc).
7070+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
7171+ EXIF 2.32 Section 4.6.5.E MeteringMode} *)
7272+7373+val pp_light_source : Format.formatter -> int -> unit
7474+(** Print light source/white balance preset (0-24).
7575+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
7676+ EXIF 2.32 Section 4.6.5.F LightSource} *)
7777+7878+val pp_flash : Format.formatter -> int -> unit
7979+(** Print flash status bit field as description.
8080+ Decodes fired status, return detection, mode, and red-eye reduction.
8181+ {{:https://www.cipa.jp/std/documents/e/DC-X008-Translation-2019-E.pdf}
8282+ EXIF 2.32 Section 4.6.5.G Flash} *)
8383+8484+val pp_color_space : Format.formatter -> int -> unit
8585+(** Print color space (1=sRGB, 65535=Uncalibrated). *)
8686+8787+val pp_exposure_mode : Format.formatter -> int -> unit
8888+(** Print exposure mode (0=Auto, 1=Manual, 2=Auto bracket). *)
8989+9090+val pp_white_balance : Format.formatter -> int -> unit
9191+(** Print white balance mode (0=Auto, 1=Manual). *)
9292+9393+val pp_scene_capture_type : Format.formatter -> int -> unit
9494+(** Print scene capture type (0=Standard, 1=Landscape, 2=Portrait, 3=Night). *)
9595+9696+val pp_contrast : Format.formatter -> int -> unit
9797+(** Print contrast setting (0=Normal, 1=Soft, 2=Hard). *)
9898+9999+val pp_saturation : Format.formatter -> int -> unit
100100+(** Print saturation setting (0=Normal, 1=Low, 2=High). *)
101101+102102+val pp_sharpness : Format.formatter -> int -> unit
103103+(** Print sharpness setting (0=Normal, 1=Soft, 2=Hard). *)
104104+105105+val pp_sensing_method : Format.formatter -> int -> unit
106106+(** Print sensor type (1-8: One-chip, Three-chip, Trilinear, etc). *)
107107+108108+val pp_compression : Format.formatter -> int -> unit
109109+(** Print compression type (1=Uncompressed, 6=JPEG). *)
110110+111111+val pp_gps_altitude_ref : Format.formatter -> int -> unit
112112+(** Print GPS altitude reference (0=above, 1=below sea level). *)
113113+114114+(** {1 Formatted Value Printers}
115115+116116+ Printers for specific EXIF values with appropriate units. *)
117117+118118+val pp_exposure_time : Format.formatter -> rational -> unit
119119+(** Print exposure time as fraction (e.g., "1/250 sec"). *)
120120+121121+val pp_fnumber : Format.formatter -> rational -> unit
122122+(** Print f-number (e.g., "f/2.8"). *)
123123+124124+val pp_focal_length : Format.formatter -> rational -> unit
125125+(** Print focal length (e.g., "50.0 mm"). *)
126126+127127+val pp_gps_coord : Format.formatter -> rational array -> string -> unit
128128+(** Print GPS coordinate in DMS format with decimal conversion.
129129+ @param arr Array of 3 rationals: degrees, minutes, seconds
130130+ @param ref_str Direction reference ("N", "S", "E", or "W") *)
131131+132132+(** {1 Entry Printers} *)
133133+134134+val pp_entry_interpreted : Format.formatter -> entry -> unit
135135+(** Print entry with semantic interpretation.
136136+ Known enumerated values are decoded to descriptions.
137137+ Unknown values are printed as raw values. *)
138138+139139+(** {1 Full EXIF Printer} *)
140140+141141+val pp_exif : Format.formatter -> t -> unit
142142+(** Print complete EXIF data, organized by IFD.
143143+ Includes byte order, entry count, and thumbnail info. *)
144144+145145+(** {1 String Conversions} *)
146146+147147+val to_string : t -> string
148148+(** Convert EXIF data to formatted string. *)
149149+150150+val entry_to_string : entry -> string
151151+(** Convert single entry to string with interpretation. *)