# Nom-Exif [![crates.io](https://img.shields.io/crates/v/nom-exif.svg)](https://crates.io/crates/nom-exif) [![Documentation](https://docs.rs/nom-exif/badge.svg)](https://docs.rs/nom-exif) [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![CI](https://github.com/mindeng/nom-exif/actions/workflows/rust.yml/badge.svg)](https://github.com/mindeng/nom-exif/actions) `nom-exif` is an Exif/metadata parsing library written in pure Rust with [nom](https://github.com/rust-bakery/nom). ## Supported File Types - Image - *.heic, *.heif, etc. - *.jpg, *.jpeg - *.tiff, *.tif - Video/Audio - ISO base media file format (ISOBMFF): *.mp4, *.mov, *.3gp, etc. - Matroska based file format: *.webm, *.mkv, *.mka, etc. ## Key Features - Ergonomic Design - **Unified Workflow** for Various File Types Now, multimedia files of different types and formats (including images, videos, and audio) can be processed using a unified method. This consistent API interface simplifies user experience and reduces cognitive load. The usage is demonstrated in the following examples. `examples/rexiftool` is also a good example. - Two style APIs for Exif *iterator* style ([`ExifIter`]) and *get* style ([`Exif`]). The former is parse-on-demand, and therefore, more detailed error information can be captured; the latter is simpler and easier to use. - Performance - *Zero-copy* when appropriate: Use borrowing and slicing instead of copying whenever possible. - Minimize I/O operations: When metadata is stored at the end/middle of a large file (such as a QuickTime file does), `Seek` rather than `Read` to quickly locate the location of the metadata (if the reader supports `Seek`). - Share I/O and parsing buffer between multiple parse calls: This can improve performance and avoid the overhead and memory fragmentation caused by frequent memory allocation. This feature is very useful when you need to perform batch parsing. - Pay as you go: When working with [`ExifIter`], all entries are lazy-parsed. That is, only when you iterate over [`ExifIter`] will the IFD entries be parsed one by one. - Robustness and stability Through long-term [Fuzz testing](https://github.com/rust-fuzz/afl.rs), and tons of crash issues discovered during testing have been fixed. Thanks to [@sigaloid](https://github.com/sigaloid) for [pointing this out](https://github.com/mindeng/nom-exif/pull/5)! - Supports both *sync* and *async* APIs ## Unified Workflow for Various File Types By using `MediaSource` & `MediaParser`, multimedia files of different types and formats (including images, videos, and audio) can be processed using a unified method. Here's an example: ```rust use nom_exif::*; fn main() -> Result<()> { let mut parser = MediaParser::new(); let files = [ "./testdata/exif.heic", "./testdata/exif.jpg", "./testdata/tif.tif", "./testdata/meta.mov", "./testdata/meta.mp4", "./testdata/webm_480.webm", "./testdata/mkv_640x360.mkv", "./testdata/mka.mka", "./testdata/3gp_640x360.3gp" ]; for f in files { let ms = MediaSource::file_path(f)?; if ms.has_exif() { // Parse the file as an Exif-compatible file let mut iter: ExifIter = parser.parse(ms)?; // ... } else if ms.has_track() { // Parse the file as a track let info: TrackInfo = parser.parse(ms)?; // ... } } Ok(()) } ``` ## Sync API: `MediaSource` + `MediaParser` `MediaSource` is an abstraction of multimedia data sources, which can be created from any object that implements the `Read` trait, and can be parsed by `MediaParser`. Example: ```rust use nom_exif::*; fn main() -> Result<()> { let mut parser = MediaParser::new(); let ms = MediaSource::file_path("./testdata/exif.heic")?; assert!(ms.has_exif()); let mut iter: ExifIter = parser.parse(ms)?; let exif: Exif = iter.into(); assert_eq!(exif.get(ExifTag::Make).unwrap().as_str().unwrap(), "Apple"); let ms = MediaSource::file_path("./testdata/meta.mov")?; assert!(ms.has_track()); let info: TrackInfo = parser.parse(ms)?; assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into())); assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into())); assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into())); assert_eq!(info.get_gps_info().unwrap().latitude_ref, 'N'); assert_eq!( info.get_gps_info().unwrap().latitude, [(27, 1), (7, 1), (68, 100)].into(), ); // `MediaSource` can also be created from a `TcpStream`: // let ms = MediaSource::tcp_stream(stream)?; // Or from any `Read + Seek`: // let ms = MediaSource::seekable(stream)?; // From any `Read`: // let ms = MediaSource::unseekable(stream)?; Ok(()) } ``` See [`MediaSource`] & [`MediaParser`] for more information. ## Async API: `AsyncMediaSource` + `AsyncMediaParser` Likewise, `AsyncMediaParser` is an abstraction for asynchronous multimedia data sources, which can be created from any object that implements the `AsyncRead` trait, and can be parsed by `AsyncMediaParser`. Enable `async` feature flag for `nom-exif` in your `Cargo.toml`: ```toml [dependencies] nom-exif = { version = "1", features = ["async"] } ``` See [`AsyncMediaSource`] & [`AsyncMediaParser`] for more information. ## GPS Info `ExifIter` provides a convenience method for parsing gps information. (`Exif` & `TrackInfo` also provide a `get_gps_info` mthod). ```rust use nom_exif::*; fn main() -> Result<()> { let mut parser = MediaParser::new(); let ms = MediaSource::file_path("./testdata/exif.heic")?; let iter: ExifIter = parser.parse(ms)?; let gps_info = iter.parse_gps_info()?.unwrap(); assert_eq!(gps_info.format_iso6709(), "+43.29013+084.22713+1595.950CRSWGS_84/"); assert_eq!(gps_info.latitude_ref, 'N'); assert_eq!(gps_info.longitude_ref, 'E'); assert_eq!( gps_info.latitude, [(43, 1), (17, 1), (2446, 100)].into(), ); Ok(()) } ``` For more usage details, please refer to the [API documentation](https://docs.rs/nom-exif/latest/nom_exif/). ## CLI Tool `rexiftool` ### Human Readable Output `cargo run --example rexiftool testdata/meta.mov`: ``` text Make => Apple Model => iPhone X Software => 12.1.2 CreateDate => 2024-02-02T08:09:57+00:00 DurationMs => 500 ImageWidth => 720 ImageHeight => 1280 GpsIso6709 => +27.1281+100.2508+000.000/ ``` ### Json Dump `cargo run --example rexiftool testdata/meta.mov -j`: ``` text { "ImageWidth": "720", "Software": "12.1.2", "ImageHeight": "1280", "Make": "Apple", "GpsIso6709": "+27.1281+100.2508+000.000/", "CreateDate": "2024-02-02T08:09:57+00:00", "Model": "iPhone X", "DurationMs": "500" } ``` ### Parsing Files in Directory `rexiftool` also supports batch parsing of all files in a folder (non-recursive). `cargo run --example rexiftool testdata/`: ```text File: "testdata/embedded-in-heic.mov" ------------------------------------------------ Make => Apple Model => iPhone 15 Pro Software => 17.1 CreateDate => 2023-11-02T12:01:02+00:00 DurationMs => 2795 ImageWidth => 1920 ImageHeight => 1440 GpsIso6709 => +22.5797+113.9380+028.396/ File: "testdata/compatible-brands-fail.heic" ------------------------------------------------ Unrecognized file format, consider filing a bug @ https://github.com/mindeng/nom-exif. File: "testdata/webm_480.webm" ------------------------------------------------ CreateDate => 2009-09-09T09:09:09+00:00 DurationMs => 30543 ImageWidth => 480 ImageHeight => 270 File: "testdata/mka.mka" ------------------------------------------------ DurationMs => 3422 ImageWidth => 0 ImageHeight => 0 File: "testdata/exif-one-entry.heic" ------------------------------------------------ Orientation => 1 File: "testdata/no-exif.jpg" ------------------------------------------------ Error: parse failed: Exif not found File: "testdata/exif.jpg" ------------------------------------------------ ImageWidth => 3072 Model => vivo X90 Pro+ ImageHeight => 4096 ModifyDate => 2023-07-09T20:36:33+08:00 YCbCrPositioning => 1 ExifOffset => 201 MakerNote => Undefined[0x30] RecommendedExposureIndex => 454 SensitivityType => 2 ISOSpeedRatings => 454 ExposureProgram => 2 FNumber => 175/100 (1.7500) ExposureTime => 9997/1000000 (0.0100) SensingMethod => 2 SubSecTimeDigitized => 616 OffsetTimeOriginal => +08:00 SubSecTimeOriginal => 616 OffsetTime => +08:00 SubSecTime => 616 FocalLength => 8670/1000 (8.6700) Flash => 16 LightSource => 21 MeteringMode => 1 SceneCaptureType => 0 UserComment => filter: 0; fileterIntensity: 0.0; filterMask: 0; algolist: 0; ... ``` ## Changelog [CHANGELOG.md](CHANGELOG.md)