My working unpac space for OCaml projects in development
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Extract EXIF module to separate ocaml-exif library

- Remove local exif.ml and exif.mli (now in ocaml-exif package)
- Add ocaml-exif dependency to dune-project and src/dune
- All 21 tests still pass

The EXIF parsing functionality is now available as a reusable
opam library with comprehensive documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+4 -1256
+2 -1
dune-project
··· 13 13 (synopsis "Pure OCaml JPEG encoder and decoder") 14 14 (description "A pure OCaml implementation of JPEG encoding and decoding based on ITU-T T.81 / ISO/IEC 10918-1 specification. No C dependencies.") 15 15 (depends 16 - (ocaml (>= 4.14)))) 16 + (ocaml (>= 4.14)) 17 + ocaml-exif))
+1
ocaml-jpeg.opam
··· 11 11 depends: [ 12 12 "dune" {>= "3.20"} 13 13 "ocaml" {>= "4.14"} 14 + "ocaml-exif" 14 15 "odoc" {with-doc} 15 16 ] 16 17 build: [
+1
src/dune
··· 1 1 (library 2 2 (name jpeg) 3 3 (public_name ocaml-jpeg) 4 + (libraries ocaml-exif) 4 5 (wrapped false))
-884
src/exif.ml
··· 1 - (** Pure OCaml EXIF parsing library 2 - 3 - Based on EXIF 2.32 specification and libexif reference implementation. 4 - 5 - EXIF data is stored in JPEG APP1 markers with the prefix "Exif\x00\x00". 6 - The data follows TIFF format with IFDs (Image File Directories). *) 7 - 8 - (** {1 Types} *) 9 - 10 - (** Byte order for multi-byte values *) 11 - type byte_order = 12 - | Big_endian (** "MM" - Motorola byte order *) 13 - | Little_endian (** "II" - Intel byte order *) 14 - 15 - (** EXIF data formats (TIFF types) *) 16 - type format = 17 - | Byte (** 8-bit unsigned integer *) 18 - | Ascii (** 8-bit byte containing 7-bit ASCII *) 19 - | Short (** 16-bit unsigned integer *) 20 - | Long (** 32-bit unsigned integer *) 21 - | Rational (** Two LONGs: numerator and denominator *) 22 - | Sbyte (** 8-bit signed integer *) 23 - | Undefined (** 8-bit byte *) 24 - | Sshort (** 16-bit signed integer *) 25 - | Slong (** 32-bit signed integer *) 26 - | Srational (** Two SLONGs: numerator and denominator *) 27 - | Float (** Single precision IEEE float *) 28 - | Double (** Double precision IEEE float *) 29 - 30 - (** Image File Directory types *) 31 - type ifd = 32 - | IFD0 (** Primary image IFD *) 33 - | IFD1 (** Thumbnail image IFD *) 34 - | EXIF (** EXIF private tags IFD *) 35 - | GPS (** GPS info IFD *) 36 - | Interoperability (** Interoperability IFD *) 37 - 38 - (** Rational number (numerator/denominator) *) 39 - type rational = { 40 - numerator : int32; 41 - denominator : int32; 42 - } 43 - 44 - (** Signed rational number *) 45 - type srational = { 46 - snumerator : int32; 47 - sdenominator : int32; 48 - } 49 - 50 - (** EXIF tag values *) 51 - type value = 52 - | VByte of int array 53 - | VAscii of string 54 - | VShort of int array 55 - | VLong of int32 array 56 - | VRational of rational array 57 - | VSbyte of int array 58 - | VUndefined of bytes 59 - | VSshort of int array 60 - | VSlong of int32 array 61 - | VSrational of srational array 62 - | VFloat of float array 63 - | VDouble of float array 64 - 65 - (** A single EXIF entry *) 66 - type entry = { 67 - tag : int; 68 - ifd : ifd; 69 - format : format; 70 - components : int; 71 - value : value; 72 - } 73 - 74 - (** Complete EXIF data *) 75 - type t = { 76 - byte_order : byte_order; 77 - entries : entry list; 78 - thumbnail : bytes option; 79 - } 80 - 81 - (** {1 Tag constants} *) 82 - 83 - (** Common EXIF tags *) 84 - module Tag = struct 85 - (* IFD0/IFD1 tags *) 86 - let image_width = 0x0100 87 - let image_length = 0x0101 88 - let bits_per_sample = 0x0102 89 - let compression = 0x0103 90 - let photometric_interpretation = 0x0106 91 - let image_description = 0x010e 92 - let make = 0x010f 93 - let model = 0x0110 94 - let strip_offsets = 0x0111 95 - let orientation = 0x0112 96 - let samples_per_pixel = 0x0115 97 - let rows_per_strip = 0x0116 98 - let strip_byte_counts = 0x0117 99 - let x_resolution = 0x011a 100 - let y_resolution = 0x011b 101 - let planar_configuration = 0x011c 102 - let resolution_unit = 0x0128 103 - let transfer_function = 0x012d 104 - let software = 0x0131 105 - let date_time = 0x0132 106 - let artist = 0x013b 107 - let white_point = 0x013e 108 - let primary_chromaticities = 0x013f 109 - let jpeg_interchange_format = 0x0201 110 - let jpeg_interchange_format_length = 0x0202 111 - let ycbcr_coefficients = 0x0211 112 - let ycbcr_sub_sampling = 0x0212 113 - let ycbcr_positioning = 0x0213 114 - let reference_black_white = 0x0214 115 - let copyright = 0x8298 116 - let exif_ifd_pointer = 0x8769 117 - let gps_info_ifd_pointer = 0x8825 118 - 119 - (* EXIF IFD tags *) 120 - let exposure_time = 0x829a 121 - let f_number = 0x829d 122 - let exposure_program = 0x8822 123 - let spectral_sensitivity = 0x8824 124 - let iso_speed_ratings = 0x8827 125 - let oecf = 0x8828 126 - let sensitivity_type = 0x8830 127 - let exif_version = 0x9000 128 - let date_time_original = 0x9003 129 - let date_time_digitized = 0x9004 130 - let offset_time = 0x9010 131 - let offset_time_original = 0x9011 132 - let offset_time_digitized = 0x9012 133 - let components_configuration = 0x9101 134 - let compressed_bits_per_pixel = 0x9102 135 - let shutter_speed_value = 0x9201 136 - let aperture_value = 0x9202 137 - let brightness_value = 0x9203 138 - let exposure_bias_value = 0x9204 139 - let max_aperture_value = 0x9205 140 - let subject_distance = 0x9206 141 - let metering_mode = 0x9207 142 - let light_source = 0x9208 143 - let flash = 0x9209 144 - let focal_length = 0x920a 145 - let subject_area = 0x9214 146 - let maker_note = 0x927c 147 - let user_comment = 0x9286 148 - let sub_sec_time = 0x9290 149 - let sub_sec_time_original = 0x9291 150 - let sub_sec_time_digitized = 0x9292 151 - let flash_pix_version = 0xa000 152 - let color_space = 0xa001 153 - let pixel_x_dimension = 0xa002 154 - let pixel_y_dimension = 0xa003 155 - let related_sound_file = 0xa004 156 - let interoperability_ifd_pointer = 0xa005 157 - let flash_energy = 0xa20b 158 - let spatial_frequency_response = 0xa20c 159 - let focal_plane_x_resolution = 0xa20e 160 - let focal_plane_y_resolution = 0xa20f 161 - let focal_plane_resolution_unit = 0xa210 162 - let subject_location = 0xa214 163 - let exposure_index = 0xa215 164 - let sensing_method = 0xa217 165 - let file_source = 0xa300 166 - let scene_type = 0xa301 167 - let cfa_pattern = 0xa302 168 - let custom_rendered = 0xa401 169 - let exposure_mode = 0xa402 170 - let white_balance = 0xa403 171 - let digital_zoom_ratio = 0xa404 172 - let focal_length_in_35mm_film = 0xa405 173 - let scene_capture_type = 0xa406 174 - let gain_control = 0xa407 175 - let contrast = 0xa408 176 - let saturation = 0xa409 177 - let sharpness = 0xa40a 178 - let device_setting_description = 0xa40b 179 - let subject_distance_range = 0xa40c 180 - let image_unique_id = 0xa420 181 - let camera_owner_name = 0xa430 182 - let body_serial_number = 0xa431 183 - let lens_specification = 0xa432 184 - let lens_make = 0xa433 185 - let lens_model = 0xa434 186 - let lens_serial_number = 0xa435 187 - let gamma = 0xa500 188 - 189 - (* GPS tags *) 190 - let gps_version_id = 0x0000 191 - let gps_latitude_ref = 0x0001 192 - let gps_latitude = 0x0002 193 - let gps_longitude_ref = 0x0003 194 - let gps_longitude = 0x0004 195 - let gps_altitude_ref = 0x0005 196 - let gps_altitude = 0x0006 197 - let gps_time_stamp = 0x0007 198 - let gps_satellites = 0x0008 199 - let gps_status = 0x0009 200 - let gps_measure_mode = 0x000a 201 - let gps_dop = 0x000b 202 - let gps_speed_ref = 0x000c 203 - let gps_speed = 0x000d 204 - let gps_track_ref = 0x000e 205 - let gps_track = 0x000f 206 - let gps_img_direction_ref = 0x0010 207 - let gps_img_direction = 0x0011 208 - let gps_map_datum = 0x0012 209 - let gps_dest_latitude_ref = 0x0013 210 - let gps_dest_latitude = 0x0014 211 - let gps_dest_longitude_ref = 0x0015 212 - let gps_dest_longitude = 0x0016 213 - let gps_dest_bearing_ref = 0x0017 214 - let gps_dest_bearing = 0x0018 215 - let gps_dest_distance_ref = 0x0019 216 - let gps_dest_distance = 0x001a 217 - let gps_processing_method = 0x001b 218 - let gps_area_information = 0x001c 219 - let gps_date_stamp = 0x001d 220 - let gps_differential = 0x001e 221 - let gps_h_positioning_error = 0x001f 222 - 223 - (** Get human-readable name for a tag *) 224 - let name_of_tag tag ifd = 225 - match ifd with 226 - | GPS -> ( 227 - match tag with 228 - | 0x0000 -> "GPSVersionID" 229 - | 0x0001 -> "GPSLatitudeRef" 230 - | 0x0002 -> "GPSLatitude" 231 - | 0x0003 -> "GPSLongitudeRef" 232 - | 0x0004 -> "GPSLongitude" 233 - | 0x0005 -> "GPSAltitudeRef" 234 - | 0x0006 -> "GPSAltitude" 235 - | 0x0007 -> "GPSTimeStamp" 236 - | 0x0008 -> "GPSSatellites" 237 - | 0x0009 -> "GPSStatus" 238 - | 0x000a -> "GPSMeasureMode" 239 - | 0x000b -> "GPSDOP" 240 - | 0x000c -> "GPSSpeedRef" 241 - | 0x000d -> "GPSSpeed" 242 - | 0x000e -> "GPSTrackRef" 243 - | 0x000f -> "GPSTrack" 244 - | 0x0010 -> "GPSImgDirectionRef" 245 - | 0x0011 -> "GPSImgDirection" 246 - | 0x0012 -> "GPSMapDatum" 247 - | 0x001d -> "GPSDateStamp" 248 - | _ -> Printf.sprintf "GPS_0x%04x" tag) 249 - | _ -> ( 250 - match tag with 251 - | 0x0100 -> "ImageWidth" 252 - | 0x0101 -> "ImageLength" 253 - | 0x0102 -> "BitsPerSample" 254 - | 0x0103 -> "Compression" 255 - | 0x0106 -> "PhotometricInterpretation" 256 - | 0x010e -> "ImageDescription" 257 - | 0x010f -> "Make" 258 - | 0x0110 -> "Model" 259 - | 0x0111 -> "StripOffsets" 260 - | 0x0112 -> "Orientation" 261 - | 0x0115 -> "SamplesPerPixel" 262 - | 0x0116 -> "RowsPerStrip" 263 - | 0x0117 -> "StripByteCounts" 264 - | 0x011a -> "XResolution" 265 - | 0x011b -> "YResolution" 266 - | 0x011c -> "PlanarConfiguration" 267 - | 0x0128 -> "ResolutionUnit" 268 - | 0x012d -> "TransferFunction" 269 - | 0x0131 -> "Software" 270 - | 0x0132 -> "DateTime" 271 - | 0x013b -> "Artist" 272 - | 0x013e -> "WhitePoint" 273 - | 0x013f -> "PrimaryChromaticities" 274 - | 0x0201 -> "JPEGInterchangeFormat" 275 - | 0x0202 -> "JPEGInterchangeFormatLength" 276 - | 0x0211 -> "YCbCrCoefficients" 277 - | 0x0212 -> "YCbCrSubSampling" 278 - | 0x0213 -> "YCbCrPositioning" 279 - | 0x0214 -> "ReferenceBlackWhite" 280 - | 0x8298 -> "Copyright" 281 - | 0x8769 -> "ExifIFDPointer" 282 - | 0x8825 -> "GPSInfoIFDPointer" 283 - | 0x829a -> "ExposureTime" 284 - | 0x829d -> "FNumber" 285 - | 0x8822 -> "ExposureProgram" 286 - | 0x8824 -> "SpectralSensitivity" 287 - | 0x8827 -> "ISOSpeedRatings" 288 - | 0x9000 -> "ExifVersion" 289 - | 0x9003 -> "DateTimeOriginal" 290 - | 0x9004 -> "DateTimeDigitized" 291 - | 0x9010 -> "OffsetTime" 292 - | 0x9011 -> "OffsetTimeOriginal" 293 - | 0x9012 -> "OffsetTimeDigitized" 294 - | 0x9101 -> "ComponentsConfiguration" 295 - | 0x9102 -> "CompressedBitsPerPixel" 296 - | 0x9201 -> "ShutterSpeedValue" 297 - | 0x9202 -> "ApertureValue" 298 - | 0x9203 -> "BrightnessValue" 299 - | 0x9204 -> "ExposureBiasValue" 300 - | 0x9205 -> "MaxApertureValue" 301 - | 0x9206 -> "SubjectDistance" 302 - | 0x9207 -> "MeteringMode" 303 - | 0x9208 -> "LightSource" 304 - | 0x9209 -> "Flash" 305 - | 0x920a -> "FocalLength" 306 - | 0x9214 -> "SubjectArea" 307 - | 0x927c -> "MakerNote" 308 - | 0x9286 -> "UserComment" 309 - | 0x9290 -> "SubSecTime" 310 - | 0x9291 -> "SubSecTimeOriginal" 311 - | 0x9292 -> "SubSecTimeDigitized" 312 - | 0xa000 -> "FlashPixVersion" 313 - | 0xa001 -> "ColorSpace" 314 - | 0xa002 -> "PixelXDimension" 315 - | 0xa003 -> "PixelYDimension" 316 - | 0xa004 -> "RelatedSoundFile" 317 - | 0xa005 -> "InteroperabilityIFDPointer" 318 - | 0xa20b -> "FlashEnergy" 319 - | 0xa20c -> "SpatialFrequencyResponse" 320 - | 0xa20e -> "FocalPlaneXResolution" 321 - | 0xa20f -> "FocalPlaneYResolution" 322 - | 0xa210 -> "FocalPlaneResolutionUnit" 323 - | 0xa214 -> "SubjectLocation" 324 - | 0xa215 -> "ExposureIndex" 325 - | 0xa217 -> "SensingMethod" 326 - | 0xa300 -> "FileSource" 327 - | 0xa301 -> "SceneType" 328 - | 0xa302 -> "CFAPattern" 329 - | 0xa401 -> "CustomRendered" 330 - | 0xa402 -> "ExposureMode" 331 - | 0xa403 -> "WhiteBalance" 332 - | 0xa404 -> "DigitalZoomRatio" 333 - | 0xa405 -> "FocalLengthIn35mmFilm" 334 - | 0xa406 -> "SceneCaptureType" 335 - | 0xa407 -> "GainControl" 336 - | 0xa408 -> "Contrast" 337 - | 0xa409 -> "Saturation" 338 - | 0xa40a -> "Sharpness" 339 - | 0xa40c -> "SubjectDistanceRange" 340 - | 0xa420 -> "ImageUniqueID" 341 - | 0xa430 -> "CameraOwnerName" 342 - | 0xa431 -> "BodySerialNumber" 343 - | 0xa432 -> "LensSpecification" 344 - | 0xa433 -> "LensMake" 345 - | 0xa434 -> "LensModel" 346 - | 0xa435 -> "LensSerialNumber" 347 - | 0xa500 -> "Gamma" 348 - | _ -> Printf.sprintf "Tag_0x%04x" tag) 349 - end 350 - 351 - (** {1 Parsing} *) 352 - 353 - (** Parse error *) 354 - exception Parse_error of string 355 - 356 - (** Size in bytes for each format type *) 357 - let format_size = function 358 - | Byte | Ascii | Sbyte | Undefined -> 1 359 - | Short | Sshort -> 2 360 - | Long | Slong | Float -> 4 361 - | Rational | Srational | Double -> 8 362 - 363 - (** Convert format code to format type *) 364 - let format_of_int = function 365 - | 1 -> Byte 366 - | 2 -> Ascii 367 - | 3 -> Short 368 - | 4 -> Long 369 - | 5 -> Rational 370 - | 6 -> Sbyte 371 - | 7 -> Undefined 372 - | 8 -> Sshort 373 - | 9 -> Slong 374 - | 10 -> Srational 375 - | 11 -> Float 376 - | 12 -> Double 377 - | n -> raise (Parse_error (Printf.sprintf "Unknown format: %d" n)) 378 - 379 - (** Read 16-bit value with given byte order *) 380 - let read_u16 data offset byte_order = 381 - let b0 = Bytes.get_uint8 data offset in 382 - let b1 = Bytes.get_uint8 data (offset + 1) in 383 - match byte_order with 384 - | Big_endian -> (b0 lsl 8) lor b1 385 - | Little_endian -> (b1 lsl 8) lor b0 386 - 387 - (** Read 32-bit value with given byte order *) 388 - let read_u32 data offset byte_order = 389 - let b0 = Int32.of_int (Bytes.get_uint8 data offset) in 390 - let b1 = Int32.of_int (Bytes.get_uint8 data (offset + 1)) in 391 - let b2 = Int32.of_int (Bytes.get_uint8 data (offset + 2)) in 392 - let b3 = Int32.of_int (Bytes.get_uint8 data (offset + 3)) in 393 - match byte_order with 394 - | Big_endian -> 395 - Int32.(logor (shift_left b0 24) 396 - (logor (shift_left b1 16) 397 - (logor (shift_left b2 8) b3))) 398 - | Little_endian -> 399 - Int32.(logor (shift_left b3 24) 400 - (logor (shift_left b2 16) 401 - (logor (shift_left b1 8) b0))) 402 - 403 - (** Read signed 32-bit value *) 404 - let read_s32 data offset byte_order = 405 - read_u32 data offset byte_order 406 - 407 - (** Read a float (IEEE 754 single) *) 408 - let read_float data offset byte_order = 409 - Int32.float_of_bits (read_u32 data offset byte_order) 410 - 411 - (** Read a double (IEEE 754 double) *) 412 - let read_double data offset byte_order = 413 - let b = Bytes.create 8 in 414 - (match byte_order with 415 - | Big_endian -> 416 - for i = 0 to 7 do 417 - Bytes.set b i (Bytes.get data (offset + i)) 418 - done 419 - | Little_endian -> 420 - for i = 0 to 7 do 421 - Bytes.set b (7 - i) (Bytes.get data (offset + i)) 422 - done); 423 - Int64.float_of_bits (Bytes.get_int64_be b 0) 424 - 425 - (** Parse entry value from data *) 426 - let parse_value data offset byte_order format components = 427 - let size = format_size format * components in 428 - if offset + size > Bytes.length data then 429 - raise (Parse_error "Value extends beyond data"); 430 - match format with 431 - | Byte -> 432 - VByte (Array.init components (fun i -> 433 - Bytes.get_uint8 data (offset + i))) 434 - | Ascii -> 435 - (* Strip trailing NUL *) 436 - let s = Bytes.sub_string data offset components in 437 - let len = String.length s in 438 - let s = if len > 0 && s.[len - 1] = '\000' then 439 - String.sub s 0 (len - 1) 440 - else s in 441 - VAscii s 442 - | Short -> 443 - VShort (Array.init components (fun i -> 444 - read_u16 data (offset + i * 2) byte_order)) 445 - | Long -> 446 - VLong (Array.init components (fun i -> 447 - read_u32 data (offset + i * 4) byte_order)) 448 - | Rational -> 449 - VRational (Array.init components (fun i -> 450 - let off = offset + i * 8 in 451 - { numerator = read_u32 data off byte_order; 452 - denominator = read_u32 data (off + 4) byte_order })) 453 - | Sbyte -> 454 - VSbyte (Array.init components (fun i -> 455 - let v = Bytes.get_uint8 data (offset + i) in 456 - if v >= 128 then v - 256 else v)) 457 - | Undefined -> 458 - VUndefined (Bytes.sub data offset components) 459 - | Sshort -> 460 - VSshort (Array.init components (fun i -> 461 - let v = read_u16 data (offset + i * 2) byte_order in 462 - if v >= 32768 then v - 65536 else v)) 463 - | Slong -> 464 - VSlong (Array.init components (fun i -> 465 - read_s32 data (offset + i * 4) byte_order)) 466 - | Srational -> 467 - VSrational (Array.init components (fun i -> 468 - let off = offset + i * 8 in 469 - { snumerator = read_s32 data off byte_order; 470 - sdenominator = read_s32 data (off + 4) byte_order })) 471 - | Float -> 472 - VFloat (Array.init components (fun i -> 473 - read_float data (offset + i * 4) byte_order)) 474 - | Double -> 475 - VDouble (Array.init components (fun i -> 476 - read_double data (offset + i * 8) byte_order)) 477 - 478 - (** Parse a single IFD entry *) 479 - let parse_entry data offset byte_order ifd = 480 - if offset + 12 > Bytes.length data then 481 - raise (Parse_error "Entry extends beyond data"); 482 - 483 - let tag = read_u16 data offset byte_order in 484 - let format_code = read_u16 data (offset + 2) byte_order in 485 - let format = format_of_int format_code in 486 - let components = Int32.to_int (read_u32 data (offset + 4) byte_order) in 487 - let value_size = format_size format * components in 488 - 489 - (* Value is inline if <= 4 bytes, otherwise it's an offset *) 490 - let value_offset = 491 - if value_size <= 4 then 492 - offset + 8 493 - else 494 - Int32.to_int (read_u32 data (offset + 8) byte_order) 495 - in 496 - 497 - let value = parse_value data value_offset byte_order format components in 498 - { tag; ifd; format; components; value } 499 - 500 - (** Parse an IFD and return entries and next IFD offset *) 501 - let parse_ifd data offset byte_order ifd = 502 - if offset + 2 > Bytes.length data then 503 - raise (Parse_error "IFD extends beyond data"); 504 - 505 - let entry_count = read_u16 data offset byte_order in 506 - let entries_end = offset + 2 + entry_count * 12 in 507 - 508 - if entries_end + 4 > Bytes.length data then 509 - raise (Parse_error "IFD entries extend beyond data"); 510 - 511 - let entries = List.init entry_count (fun i -> 512 - parse_entry data (offset + 2 + i * 12) byte_order ifd) 513 - in 514 - 515 - let next_ifd_offset = Int32.to_int (read_u32 data entries_end byte_order) in 516 - (entries, next_ifd_offset) 517 - 518 - (** Parse complete EXIF data from bytes *) 519 - let parse data = 520 - let len = Bytes.length data in 521 - if len < 8 then 522 - raise (Parse_error "Data too short for EXIF header"); 523 - 524 - (* Check byte order marker *) 525 - let byte_order = 526 - match Bytes.get_uint8 data 0, Bytes.get_uint8 data 1 with 527 - | 0x4D, 0x4D -> Big_endian (* "MM" *) 528 - | 0x49, 0x49 -> Little_endian (* "II" *) 529 - | _ -> raise (Parse_error "Invalid byte order marker") 530 - in 531 - 532 - (* Check TIFF magic number *) 533 - let magic = read_u16 data 2 byte_order in 534 - if magic <> 42 then 535 - raise (Parse_error (Printf.sprintf "Invalid TIFF magic: %d" magic)); 536 - 537 - (* Get offset to first IFD *) 538 - let ifd0_offset = Int32.to_int (read_u32 data 4 byte_order) in 539 - if ifd0_offset < 8 || ifd0_offset >= len then 540 - raise (Parse_error "Invalid IFD0 offset"); 541 - 542 - (* Parse IFD0 *) 543 - let ifd0_entries, ifd1_offset = parse_ifd data ifd0_offset byte_order IFD0 in 544 - 545 - (* Look for EXIF IFD pointer *) 546 - let exif_entries = 547 - match List.find_opt (fun e -> e.tag = Tag.exif_ifd_pointer) ifd0_entries with 548 - | Some { value = VLong [| offset |]; _ } -> 549 - let off = Int32.to_int offset in 550 - if off > 0 && off < len then 551 - let entries, _ = parse_ifd data off byte_order EXIF in 552 - entries 553 - else [] 554 - | _ -> [] 555 - in 556 - 557 - (* Look for GPS IFD pointer *) 558 - let gps_entries = 559 - match List.find_opt (fun e -> e.tag = Tag.gps_info_ifd_pointer) ifd0_entries with 560 - | Some { value = VLong [| offset |]; _ } -> 561 - let off = Int32.to_int offset in 562 - if off > 0 && off < len then 563 - let entries, _ = parse_ifd data off byte_order GPS in 564 - entries 565 - else [] 566 - | _ -> [] 567 - in 568 - 569 - (* Look for Interoperability IFD pointer in EXIF IFD *) 570 - let interop_entries = 571 - match List.find_opt (fun e -> e.tag = Tag.interoperability_ifd_pointer) exif_entries with 572 - | Some { value = VLong [| offset |]; _ } -> 573 - let off = Int32.to_int offset in 574 - if off > 0 && off < len then 575 - let entries, _ = parse_ifd data off byte_order Interoperability in 576 - entries 577 - else [] 578 - | _ -> [] 579 - in 580 - 581 - (* Parse IFD1 (thumbnail) if present *) 582 - let ifd1_entries, thumbnail = 583 - if ifd1_offset > 0 && ifd1_offset < len then 584 - let entries, _ = parse_ifd data ifd1_offset byte_order IFD1 in 585 - (* Look for JPEG thumbnail *) 586 - let thumb_offset = 587 - match List.find_opt (fun e -> e.tag = Tag.jpeg_interchange_format) entries with 588 - | Some { value = VLong [| off |]; _ } -> Some (Int32.to_int off) 589 - | _ -> None 590 - in 591 - let thumb_length = 592 - match List.find_opt (fun e -> e.tag = Tag.jpeg_interchange_format_length) entries with 593 - | Some { value = VLong [| len |]; _ } -> Some (Int32.to_int len) 594 - | _ -> None 595 - in 596 - let thumbnail = 597 - match thumb_offset, thumb_length with 598 - | Some off, Some len when off > 0 && off + len <= Bytes.length data -> 599 - Some (Bytes.sub data off len) 600 - | _ -> None 601 - in 602 - (entries, thumbnail) 603 - else 604 - ([], None) 605 - in 606 - 607 - let entries = 608 - ifd0_entries @ ifd1_entries @ exif_entries @ gps_entries @ interop_entries 609 - in 610 - 611 - { byte_order; entries; thumbnail } 612 - 613 - (** Parse EXIF data from a JPEG APP1 segment (with "Exif\x00\x00" prefix stripped) *) 614 - let parse_from_app1 data = 615 - parse data 616 - 617 - (** {1 Query functions} *) 618 - 619 - (** Find entry by tag in any IFD *) 620 - let find_entry tag exif = 621 - List.find_opt (fun e -> e.tag = tag) exif.entries 622 - 623 - (** Find entry by tag in specific IFD *) 624 - let find_entry_in_ifd tag ifd exif = 625 - List.find_opt (fun e -> e.tag = tag && e.ifd = ifd) exif.entries 626 - 627 - (** Get all entries for a specific IFD *) 628 - let entries_in_ifd ifd exif = 629 - List.filter (fun e -> e.ifd = ifd) exif.entries 630 - 631 - (** {1 Value extraction helpers} *) 632 - 633 - (** Get ASCII string value *) 634 - let get_string entry = 635 - match entry.value with 636 - | VAscii s -> Some s 637 - | _ -> None 638 - 639 - (** Get single SHORT value *) 640 - let get_short entry = 641 - match entry.value with 642 - | VShort [| v |] -> Some v 643 - | _ -> None 644 - 645 - (** Get single LONG value *) 646 - let get_long entry = 647 - match entry.value with 648 - | VLong [| v |] -> Some (Int32.to_int v) 649 - | _ -> None 650 - 651 - (** Get single RATIONAL value as float *) 652 - let get_rational entry = 653 - match entry.value with 654 - | VRational [| { numerator; denominator } |] -> 655 - if denominator = 0l then None 656 - else Some (Int32.to_float numerator /. Int32.to_float denominator) 657 - | _ -> None 658 - 659 - (** Get RATIONAL array as floats *) 660 - let get_rationals entry = 661 - match entry.value with 662 - | VRational arr -> 663 - Some (Array.map (fun { numerator; denominator } -> 664 - if denominator = 0l then 0.0 665 - else Int32.to_float numerator /. Int32.to_float denominator) arr) 666 - | _ -> None 667 - 668 - (** {1 Common metadata accessors} *) 669 - 670 - (** Get camera make *) 671 - let make exif = 672 - Option.bind (find_entry Tag.make exif) get_string 673 - 674 - (** Get camera model *) 675 - let model exif = 676 - Option.bind (find_entry Tag.model exif) get_string 677 - 678 - (** Get software *) 679 - let software exif = 680 - Option.bind (find_entry Tag.software exif) get_string 681 - 682 - (** Get image description *) 683 - let image_description exif = 684 - Option.bind (find_entry Tag.image_description exif) get_string 685 - 686 - (** Get artist *) 687 - let artist exif = 688 - Option.bind (find_entry Tag.artist exif) get_string 689 - 690 - (** Get copyright *) 691 - let copyright exif = 692 - Option.bind (find_entry Tag.copyright exif) get_string 693 - 694 - (** Get date/time original *) 695 - let date_time_original exif = 696 - Option.bind (find_entry Tag.date_time_original exif) get_string 697 - 698 - (** Get date/time digitized *) 699 - let date_time_digitized exif = 700 - Option.bind (find_entry Tag.date_time_digitized exif) get_string 701 - 702 - (** Get date/time *) 703 - let date_time exif = 704 - Option.bind (find_entry Tag.date_time exif) get_string 705 - 706 - (** Get orientation (1-8) *) 707 - let orientation exif = 708 - Option.bind (find_entry Tag.orientation exif) get_short 709 - 710 - (** Get image width *) 711 - let image_width exif = 712 - match find_entry Tag.image_width exif with 713 - | Some e -> ( 714 - match e.value with 715 - | VShort [| v |] -> Some v 716 - | VLong [| v |] -> Some (Int32.to_int v) 717 - | _ -> None) 718 - | None -> None 719 - 720 - (** Get image height *) 721 - let image_height exif = 722 - match find_entry Tag.image_length exif with 723 - | Some e -> ( 724 - match e.value with 725 - | VShort [| v |] -> Some v 726 - | VLong [| v |] -> Some (Int32.to_int v) 727 - | _ -> None) 728 - | None -> None 729 - 730 - (** Get X resolution *) 731 - let x_resolution exif = 732 - Option.bind (find_entry Tag.x_resolution exif) get_rational 733 - 734 - (** Get Y resolution *) 735 - let y_resolution exif = 736 - Option.bind (find_entry Tag.y_resolution exif) get_rational 737 - 738 - (** Get resolution unit (1=none, 2=inch, 3=cm) *) 739 - let resolution_unit exif = 740 - Option.bind (find_entry Tag.resolution_unit exif) get_short 741 - 742 - (** Get exposure time in seconds *) 743 - let exposure_time exif = 744 - Option.bind (find_entry Tag.exposure_time exif) get_rational 745 - 746 - (** Get F-number *) 747 - let f_number exif = 748 - Option.bind (find_entry Tag.f_number exif) get_rational 749 - 750 - (** Get ISO speed *) 751 - let iso_speed exif = 752 - match find_entry Tag.iso_speed_ratings exif with 753 - | Some { value = VShort [| v |]; _ } -> Some v 754 - | Some { value = VShort arr; _ } when Array.length arr > 0 -> Some arr.(0) 755 - | _ -> None 756 - 757 - (** Get focal length in mm *) 758 - let focal_length exif = 759 - Option.bind (find_entry Tag.focal_length exif) get_rational 760 - 761 - (** Get focal length in 35mm equivalent *) 762 - let focal_length_35mm exif = 763 - Option.bind (find_entry Tag.focal_length_in_35mm_film exif) get_short 764 - 765 - (** Get flash status *) 766 - let flash exif = 767 - Option.bind (find_entry Tag.flash exif) get_short 768 - 769 - (** Get color space (1=sRGB, 65535=uncalibrated) *) 770 - let color_space exif = 771 - Option.bind (find_entry Tag.color_space exif) get_short 772 - 773 - (** Get EXIF version string *) 774 - let exif_version exif = 775 - match find_entry Tag.exif_version exif with 776 - | Some { value = VUndefined b; _ } -> Some (Bytes.to_string b) 777 - | _ -> None 778 - 779 - (** {1 GPS accessors} *) 780 - 781 - (** Convert DMS (degrees/minutes/seconds) rationals to decimal degrees *) 782 - let dms_to_decimal rationals = 783 - if Array.length rationals < 3 then None 784 - else 785 - let degrees = Int32.to_float rationals.(0).numerator /. 786 - Int32.to_float rationals.(0).denominator in 787 - let minutes = Int32.to_float rationals.(1).numerator /. 788 - Int32.to_float rationals.(1).denominator in 789 - let seconds = Int32.to_float rationals.(2).numerator /. 790 - Int32.to_float rationals.(2).denominator in 791 - Some (degrees +. minutes /. 60.0 +. seconds /. 3600.0) 792 - 793 - (** Get GPS latitude in decimal degrees (negative for South) *) 794 - let gps_latitude exif = 795 - match find_entry_in_ifd Tag.gps_latitude GPS exif with 796 - | Some { value = VRational arr; _ } -> ( 797 - match dms_to_decimal arr with 798 - | Some lat -> ( 799 - match find_entry_in_ifd Tag.gps_latitude_ref GPS exif with 800 - | Some { value = VAscii "S"; _ } -> Some (-.lat) 801 - | _ -> Some lat) 802 - | None -> None) 803 - | _ -> None 804 - 805 - (** Get GPS longitude in decimal degrees (negative for West) *) 806 - let gps_longitude exif = 807 - match find_entry_in_ifd Tag.gps_longitude GPS exif with 808 - | Some { value = VRational arr; _ } -> ( 809 - match dms_to_decimal arr with 810 - | Some lon -> ( 811 - match find_entry_in_ifd Tag.gps_longitude_ref GPS exif with 812 - | Some { value = VAscii "W"; _ } -> Some (-.lon) 813 - | _ -> Some lon) 814 - | None -> None) 815 - | _ -> None 816 - 817 - (** Get GPS altitude in meters (negative for below sea level) *) 818 - let gps_altitude exif = 819 - match find_entry_in_ifd Tag.gps_altitude GPS exif with 820 - | Some e -> ( 821 - match get_rational e with 822 - | Some alt -> ( 823 - match find_entry_in_ifd Tag.gps_altitude_ref GPS exif with 824 - | Some { value = VByte [| 1 |]; _ } -> Some (-.alt) 825 - | _ -> Some alt) 826 - | None -> None) 827 - | None -> None 828 - 829 - (** {1 Pretty printing} *) 830 - 831 - (** Convert value to string for display *) 832 - let string_of_value = function 833 - | VByte arr -> 834 - String.concat " " (Array.to_list (Array.map string_of_int arr)) 835 - | VAscii s -> s 836 - | VShort arr -> 837 - String.concat " " (Array.to_list (Array.map string_of_int arr)) 838 - | VLong arr -> 839 - String.concat " " (Array.to_list (Array.map Int32.to_string arr)) 840 - | VRational arr -> 841 - String.concat " " (Array.to_list (Array.map (fun r -> 842 - Printf.sprintf "%ld/%ld" r.numerator r.denominator) arr)) 843 - | VSbyte arr -> 844 - String.concat " " (Array.to_list (Array.map string_of_int arr)) 845 - | VUndefined b -> 846 - if Bytes.length b <= 16 then 847 - String.concat " " (List.init (Bytes.length b) (fun i -> 848 - Printf.sprintf "%02X" (Bytes.get_uint8 b i))) 849 - else 850 - Printf.sprintf "<%d bytes>" (Bytes.length b) 851 - | VSshort arr -> 852 - String.concat " " (Array.to_list (Array.map string_of_int arr)) 853 - | VSlong arr -> 854 - String.concat " " (Array.to_list (Array.map Int32.to_string arr)) 855 - | VSrational arr -> 856 - String.concat " " (Array.to_list (Array.map (fun r -> 857 - Printf.sprintf "%ld/%ld" r.snumerator r.sdenominator) arr)) 858 - | VFloat arr -> 859 - String.concat " " (Array.to_list (Array.map string_of_float arr)) 860 - | VDouble arr -> 861 - String.concat " " (Array.to_list (Array.map string_of_float arr)) 862 - 863 - (** Convert entry to human-readable string *) 864 - let string_of_entry entry = 865 - Printf.sprintf "%s: %s" 866 - (Tag.name_of_tag entry.tag entry.ifd) 867 - (string_of_value entry.value) 868 - 869 - (** Convert IFD to string *) 870 - let string_of_ifd = function 871 - | IFD0 -> "IFD0" 872 - | IFD1 -> "IFD1" 873 - | EXIF -> "EXIF" 874 - | GPS -> "GPS" 875 - | Interoperability -> "Interoperability" 876 - 877 - (** Dump all EXIF data to string *) 878 - let to_string exif = 879 - let lines = List.map (fun entry -> 880 - Printf.sprintf "[%s] %s" 881 - (string_of_ifd entry.ifd) 882 - (string_of_entry entry)) exif.entries 883 - in 884 - String.concat "\n" lines
-371
src/exif.mli
··· 1 - (** Pure OCaml EXIF parsing library 2 - 3 - Based on EXIF 2.32 specification and libexif reference implementation. 4 - 5 - {1 Example Usage} 6 - 7 - {[ 8 - (* Parse EXIF from raw APP1 data *) 9 - let exif = Exif.parse_from_app1 app1_data in 10 - 11 - (* Get camera make and model *) 12 - let make = Exif.make exif in 13 - let model = Exif.model exif in 14 - 15 - (* Get GPS coordinates *) 16 - let lat = Exif.gps_latitude exif in 17 - let lon = Exif.gps_longitude exif in 18 - ]} *) 19 - 20 - (** {1 Types} *) 21 - 22 - (** Byte order for multi-byte values *) 23 - type byte_order = 24 - | Big_endian (** "MM" - Motorola byte order *) 25 - | Little_endian (** "II" - Intel byte order *) 26 - 27 - (** EXIF data formats (TIFF types) *) 28 - type format = 29 - | Byte (** 8-bit unsigned integer *) 30 - | Ascii (** 8-bit byte containing 7-bit ASCII *) 31 - | Short (** 16-bit unsigned integer *) 32 - | Long (** 32-bit unsigned integer *) 33 - | Rational (** Two LONGs: numerator and denominator *) 34 - | Sbyte (** 8-bit signed integer *) 35 - | Undefined (** 8-bit byte *) 36 - | Sshort (** 16-bit signed integer *) 37 - | Slong (** 32-bit signed integer *) 38 - | Srational (** Two SLONGs: numerator and denominator *) 39 - | Float (** Single precision IEEE float *) 40 - | Double (** Double precision IEEE float *) 41 - 42 - (** Image File Directory types *) 43 - type ifd = 44 - | IFD0 (** Primary image IFD *) 45 - | IFD1 (** Thumbnail image IFD *) 46 - | EXIF (** EXIF private tags IFD *) 47 - | GPS (** GPS info IFD *) 48 - | Interoperability (** Interoperability IFD *) 49 - 50 - (** Rational number (numerator/denominator) *) 51 - type rational = { 52 - numerator : int32; 53 - denominator : int32; 54 - } 55 - 56 - (** Signed rational number *) 57 - type srational = { 58 - snumerator : int32; 59 - sdenominator : int32; 60 - } 61 - 62 - (** EXIF tag values *) 63 - type value = 64 - | VByte of int array 65 - | VAscii of string 66 - | VShort of int array 67 - | VLong of int32 array 68 - | VRational of rational array 69 - | VSbyte of int array 70 - | VUndefined of bytes 71 - | VSshort of int array 72 - | VSlong of int32 array 73 - | VSrational of srational array 74 - | VFloat of float array 75 - | VDouble of float array 76 - 77 - (** A single EXIF entry *) 78 - type entry = { 79 - tag : int; 80 - ifd : ifd; 81 - format : format; 82 - components : int; 83 - value : value; 84 - } 85 - 86 - (** Complete EXIF data *) 87 - type t = { 88 - byte_order : byte_order; 89 - entries : entry list; 90 - thumbnail : bytes option; 91 - } 92 - 93 - (** {1 Tag Constants} *) 94 - 95 - module Tag : sig 96 - (** IFD0/IFD1 tags *) 97 - val image_width : int 98 - val image_length : int 99 - val bits_per_sample : int 100 - val compression : int 101 - val photometric_interpretation : int 102 - val image_description : int 103 - val make : int 104 - val model : int 105 - val strip_offsets : int 106 - val orientation : int 107 - val samples_per_pixel : int 108 - val rows_per_strip : int 109 - val strip_byte_counts : int 110 - val x_resolution : int 111 - val y_resolution : int 112 - val planar_configuration : int 113 - val resolution_unit : int 114 - val transfer_function : int 115 - val software : int 116 - val date_time : int 117 - val artist : int 118 - val white_point : int 119 - val primary_chromaticities : int 120 - val jpeg_interchange_format : int 121 - val jpeg_interchange_format_length : int 122 - val ycbcr_coefficients : int 123 - val ycbcr_sub_sampling : int 124 - val ycbcr_positioning : int 125 - val reference_black_white : int 126 - val copyright : int 127 - val exif_ifd_pointer : int 128 - val gps_info_ifd_pointer : int 129 - 130 - (** EXIF IFD tags *) 131 - val exposure_time : int 132 - val f_number : int 133 - val exposure_program : int 134 - val spectral_sensitivity : int 135 - val iso_speed_ratings : int 136 - val oecf : int 137 - val sensitivity_type : int 138 - val exif_version : int 139 - val date_time_original : int 140 - val date_time_digitized : int 141 - val offset_time : int 142 - val offset_time_original : int 143 - val offset_time_digitized : int 144 - val components_configuration : int 145 - val compressed_bits_per_pixel : int 146 - val shutter_speed_value : int 147 - val aperture_value : int 148 - val brightness_value : int 149 - val exposure_bias_value : int 150 - val max_aperture_value : int 151 - val subject_distance : int 152 - val metering_mode : int 153 - val light_source : int 154 - val flash : int 155 - val focal_length : int 156 - val subject_area : int 157 - val maker_note : int 158 - val user_comment : int 159 - val sub_sec_time : int 160 - val sub_sec_time_original : int 161 - val sub_sec_time_digitized : int 162 - val flash_pix_version : int 163 - val color_space : int 164 - val pixel_x_dimension : int 165 - val pixel_y_dimension : int 166 - val related_sound_file : int 167 - val interoperability_ifd_pointer : int 168 - val flash_energy : int 169 - val spatial_frequency_response : int 170 - val focal_plane_x_resolution : int 171 - val focal_plane_y_resolution : int 172 - val focal_plane_resolution_unit : int 173 - val subject_location : int 174 - val exposure_index : int 175 - val sensing_method : int 176 - val file_source : int 177 - val scene_type : int 178 - val cfa_pattern : int 179 - val custom_rendered : int 180 - val exposure_mode : int 181 - val white_balance : int 182 - val digital_zoom_ratio : int 183 - val focal_length_in_35mm_film : int 184 - val scene_capture_type : int 185 - val gain_control : int 186 - val contrast : int 187 - val saturation : int 188 - val sharpness : int 189 - val device_setting_description : int 190 - val subject_distance_range : int 191 - val image_unique_id : int 192 - val camera_owner_name : int 193 - val body_serial_number : int 194 - val lens_specification : int 195 - val lens_make : int 196 - val lens_model : int 197 - val lens_serial_number : int 198 - val gamma : int 199 - 200 - (** GPS tags *) 201 - val gps_version_id : int 202 - val gps_latitude_ref : int 203 - val gps_latitude : int 204 - val gps_longitude_ref : int 205 - val gps_longitude : int 206 - val gps_altitude_ref : int 207 - val gps_altitude : int 208 - val gps_time_stamp : int 209 - val gps_satellites : int 210 - val gps_status : int 211 - val gps_measure_mode : int 212 - val gps_dop : int 213 - val gps_speed_ref : int 214 - val gps_speed : int 215 - val gps_track_ref : int 216 - val gps_track : int 217 - val gps_img_direction_ref : int 218 - val gps_img_direction : int 219 - val gps_map_datum : int 220 - val gps_dest_latitude_ref : int 221 - val gps_dest_latitude : int 222 - val gps_dest_longitude_ref : int 223 - val gps_dest_longitude : int 224 - val gps_dest_bearing_ref : int 225 - val gps_dest_bearing : int 226 - val gps_dest_distance_ref : int 227 - val gps_dest_distance : int 228 - val gps_processing_method : int 229 - val gps_area_information : int 230 - val gps_date_stamp : int 231 - val gps_differential : int 232 - val gps_h_positioning_error : int 233 - 234 - (** Get human-readable name for a tag *) 235 - val name_of_tag : int -> ifd -> string 236 - end 237 - 238 - (** {1 Parsing} *) 239 - 240 - (** Parse error *) 241 - exception Parse_error of string 242 - 243 - (** Parse complete EXIF data from raw TIFF-format bytes *) 244 - val parse : bytes -> t 245 - 246 - (** Parse EXIF data from a JPEG APP1 segment (with "Exif\x00\x00" prefix stripped) *) 247 - val parse_from_app1 : bytes -> t 248 - 249 - (** {1 Query Functions} *) 250 - 251 - (** Find entry by tag in any IFD *) 252 - val find_entry : int -> t -> entry option 253 - 254 - (** Find entry by tag in specific IFD *) 255 - val find_entry_in_ifd : int -> ifd -> t -> entry option 256 - 257 - (** Get all entries for a specific IFD *) 258 - val entries_in_ifd : ifd -> t -> entry list 259 - 260 - (** {1 Value Extraction Helpers} *) 261 - 262 - (** Get ASCII string value *) 263 - val get_string : entry -> string option 264 - 265 - (** Get single SHORT value *) 266 - val get_short : entry -> int option 267 - 268 - (** Get single LONG value *) 269 - val get_long : entry -> int option 270 - 271 - (** Get single RATIONAL value as float *) 272 - val get_rational : entry -> float option 273 - 274 - (** Get RATIONAL array as floats *) 275 - val get_rationals : entry -> float array option 276 - 277 - (** {1 Common Metadata Accessors} *) 278 - 279 - (** Get camera make *) 280 - val make : t -> string option 281 - 282 - (** Get camera model *) 283 - val model : t -> string option 284 - 285 - (** Get software *) 286 - val software : t -> string option 287 - 288 - (** Get image description *) 289 - val image_description : t -> string option 290 - 291 - (** Get artist *) 292 - val artist : t -> string option 293 - 294 - (** Get copyright *) 295 - val copyright : t -> string option 296 - 297 - (** Get date/time original (when photo was taken) *) 298 - val date_time_original : t -> string option 299 - 300 - (** Get date/time digitized *) 301 - val date_time_digitized : t -> string option 302 - 303 - (** Get date/time (file modification) *) 304 - val date_time : t -> string option 305 - 306 - (** Get orientation (1-8) *) 307 - val orientation : t -> int option 308 - 309 - (** Get image width *) 310 - val image_width : t -> int option 311 - 312 - (** Get image height *) 313 - val image_height : t -> int option 314 - 315 - (** Get X resolution *) 316 - val x_resolution : t -> float option 317 - 318 - (** Get Y resolution *) 319 - val y_resolution : t -> float option 320 - 321 - (** Get resolution unit (1=none, 2=inch, 3=cm) *) 322 - val resolution_unit : t -> int option 323 - 324 - (** Get exposure time in seconds *) 325 - val exposure_time : t -> float option 326 - 327 - (** Get F-number *) 328 - val f_number : t -> float option 329 - 330 - (** Get ISO speed *) 331 - val iso_speed : t -> int option 332 - 333 - (** Get focal length in mm *) 334 - val focal_length : t -> float option 335 - 336 - (** Get focal length in 35mm equivalent *) 337 - val focal_length_35mm : t -> int option 338 - 339 - (** Get flash status *) 340 - val flash : t -> int option 341 - 342 - (** Get color space (1=sRGB, 65535=uncalibrated) *) 343 - val color_space : t -> int option 344 - 345 - (** Get EXIF version string *) 346 - val exif_version : t -> string option 347 - 348 - (** {1 GPS Accessors} *) 349 - 350 - (** Get GPS latitude in decimal degrees (negative for South) *) 351 - val gps_latitude : t -> float option 352 - 353 - (** Get GPS longitude in decimal degrees (negative for West) *) 354 - val gps_longitude : t -> float option 355 - 356 - (** Get GPS altitude in meters (negative for below sea level) *) 357 - val gps_altitude : t -> float option 358 - 359 - (** {1 Pretty Printing} *) 360 - 361 - (** Convert value to string for display *) 362 - val string_of_value : value -> string 363 - 364 - (** Convert entry to human-readable string *) 365 - val string_of_entry : entry -> string 366 - 367 - (** Convert IFD to string *) 368 - val string_of_ifd : ifd -> string 369 - 370 - (** Dump all EXIF data to string *) 371 - val to_string : t -> string