{"id":18427056,"url":"https://github.com/mindeng/nom-exif","last_synced_at":"2026-05-12T10:01:59.459Z","repository":{"id":222590591,"uuid":"757825836","full_name":"mindeng/nom-exif","owner":"mindeng","description":"Exif/metadata parsing library written in pure Rust, both image (jpeg/heif/heic/jpg/tiff/raf etc.) and video/audio (mov/mp4/3gp/webm/mkv/mka, etc.) files are supported.","archived":false,"fork":false,"pushed_at":"2025-03-13T10:58:49.000Z","size":28025,"stargazers_count":64,"open_issues_count":5,"forks_count":10,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-28T11:06:04.928Z","etag":null,"topics":["exif","heif","jpeg","matroska","metadata","mkv","mov","mp4","nom","parser","quicktime","raw","rust","tiff","webm"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/nom-exif","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mindeng.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-02-15T04:05:16.000Z","updated_at":"2025-03-13T10:57:05.000Z","dependencies_parsed_at":"2024-09-06T07:14:10.590Z","dependency_job_id":"eba260b1-8b2c-48db-bdfe-cf0392f85548","html_url":"https://github.com/mindeng/nom-exif","commit_stats":null,"previous_names":["mindeng/nom-exif"],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindeng%2Fnom-exif","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindeng%2Fnom-exif/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindeng%2Fnom-exif/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindeng%2Fnom-exif/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mindeng","download_url":"https://codeload.github.com/mindeng/nom-exif/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247174387,"owners_count":20896075,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["exif","heif","jpeg","matroska","metadata","mkv","mov","mp4","nom","parser","quicktime","raw","rust","tiff","webm"],"created_at":"2024-11-06T05:09:32.959Z","updated_at":"2026-05-09T16:12:25.837Z","avatar_url":"https://github.com/mindeng.png","language":"Rust","funding_links":[],"categories":["Rust"],"sub_categories":[],"readme":"# Nom-Exif\n\n[![crates.io](https://img.shields.io/crates/v/nom-exif.svg)](https://crates.io/crates/nom-exif)\n[![Documentation](https://docs.rs/nom-exif/badge.svg)](https://docs.rs/nom-exif)\n[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![CI](https://github.com/mindeng/nom-exif/actions/workflows/rust.yml/badge.svg)](https://github.com/mindeng/nom-exif/actions)\n\n`nom-exif` is a pure Rust library for **both image EXIF and video / audio\ntrack metadata** through a single unified API. Built on\n[nom](https://github.com/rust-bakery/nom).\n\n## Highlights\n\n- Pure Rust — no FFmpeg, no libexif, no system deps; cross-compiles\n  cleanly.\n- Image **and** video / audio in one crate — `MediaParser` dispatches to\n  the right backend by detected MIME, no per-format wrappers.\n- **Motion Photo** support — Pixel and Samsung Motion Photos (JPEG with\n  an embedded MP4) are detected automatically; `parse_track` extracts\n  the embedded video's track metadata.\n- RAW format support — Canon CR3, Fujifilm RAF, Phase One IIQ,\n  alongside JPEG / HEIC / TIFF.\n- Three input modes — files, arbitrary `Read` / `Read + Seek` (network\n  streams, pipes), or in-RAM bytes (WASM, mobile, HTTP proxies).\n- Sync and async unified under one `MediaParser`.\n- Eager (`Exif`, get-by-tag) or lazy (`ExifIter`, parse-on-demand) —\n  per-entry errors surface in both modes (`Exif::errors()` /\n  per-iter `Result`), so one bad tag doesn't poison the parse.\n- Allocation-frugal — parser buffer is recycled across calls; sub-IFDs\n  share the same allocation (no deep copies).\n- Fuzz-tested with `cargo-fuzz` against malformed and adversarial input.\n\n## Supported File Types\n\n- Image\n  - .heic, .heif, etc.\n  - .jpg, .jpeg\n  - .tiff, .tif, .iiq (Phase One IIQ images), etc.\n  - .RAF (Fujifilm RAW)\n  - .CR3 (Canon RAW)\n- Video/Audio\n  - ISO base media file format (ISOBMFF): .mp4, .mov, .3gp, etc.\n  - Matroska based file format: .webm, .mkv, .mka, etc.\n\n## Quick Start\n\n```rust\nuse nom_exif::{read_exif, read_track, read_metadata, ExifTag, TrackInfoTag, Metadata};\n\n// One image:\nlet exif = read_exif(\"./testdata/exif.jpg\")?;\nlet make = exif.get(ExifTag::Make).and_then(|v| v.as_str());\n\n// One video:\nlet info = read_track(\"./testdata/meta.mov\")?;\nlet model = info.get(TrackInfoTag::Model).and_then(|v| v.as_str());\n\n// Auto-detect:\nmatch read_metadata(\"./testdata/exif.jpg\")? {\n    Metadata::Exif(_)  =\u003e { /* image */ }\n    Metadata::Track(_) =\u003e { /* video/audio */ }\n}\n# Ok::\u003c(), nom_exif::Error\u003e(())\n```\n\n## Reusable Parser\n\nFor batch processing, build a `MediaParser` once and reuse its buffer\nacross calls:\n\n```rust\nuse nom_exif::{MediaKind, MediaParser, MediaSource, ExifTag, TrackInfoTag};\n\nlet mut parser = MediaParser::new();\n\nlet files = [\n    \"./testdata/exif.heic\",\n    \"./testdata/exif.jpg\",\n    \"./testdata/meta.mov\",\n];\n\nfor f in files {\n    let ms = MediaSource::open(f)?;\n    match ms.kind() {\n        MediaKind::Image =\u003e {\n            let iter = parser.parse_exif(ms)?;\n            let exif: nom_exif::Exif = iter.into();\n            let _ = exif.get(ExifTag::Make);\n        }\n        MediaKind::Track =\u003e {\n            let info = parser.parse_track(ms)?;\n            let _ = info.get(TrackInfoTag::Make);\n        }\n    }\n}\n# Ok::\u003c(), nom_exif::Error\u003e(())\n```\n\n`MediaSource` accepts any `Read` (or `Read + Seek`):\n\n- `MediaSource::open(path)` — convenience for files.\n- `MediaSource::seekable(reader)` — any `Read + Seek` source.\n- `MediaSource::unseekable(reader)` — `Read`-only source (e.g. a network\n  stream); slower for formats that store metadata at the end of the file\n  (such as `.mov`).\n\n## In-Memory Bytes\n\nWhen the payload is already in RAM (decoded HTTP body, WASM-loaded\nasset, mobile-cached blob), use the `*_from_bytes` helpers to skip the\n`File` / `Read` round-trip. Memory mode is **zero-copy**: the underlying\nallocation is shared with the returned `Exif` / `ExifIter` / `TrackInfo`\nvia `bytes::Bytes` reference counting.\n\n```rust\nuse nom_exif::{read_exif_from_bytes, ExifTag};\n\nlet raw: Vec\u003cu8\u003e = std::fs::read(\"./testdata/exif.jpg\")?;\nlet exif = read_exif_from_bytes(raw)?;\nlet make = exif.get(ExifTag::Make).and_then(|v| v.as_str());\n# let _ = make; Ok::\u003c(), nom_exif::Error\u003e(())\n```\n\nFor batch processing many in-memory payloads, reuse a `MediaParser`:\n\n```rust\nuse nom_exif::{MediaParser, MediaSource};\n\nlet mut parser = MediaParser::new();\nlet raw = std::fs::read(\"./testdata/exif.jpg\")?;\nlet ms = MediaSource::from_bytes(raw)?;\nlet iter = parser.parse_exif_from_bytes(ms)?;\n# let _ = iter; Ok::\u003c(), nom_exif::Error\u003e(())\n```\n\n`MediaSource::from_bytes` accepts anything convertible into\n`bytes::Bytes`: `Vec\u003cu8\u003e`, `\u0026'static [u8]`, `Bytes`, and HTTP-body types\nthat implement `Into\u003cBytes\u003e` directly.\n\n## Embedded Media Tracks (Motion Photos)\n\nPixel and Google phones store **Motion Photos** as a single JPEG with a\nshort MP4 video appended after the image data. `parse_exif` reads the\nphoto's EXIF as usual and sets a flag when it sees the\n`GCamera:MotionPhoto=\"1\"` XMP signal; `parse_track` on the same source\nthen extracts the embedded MP4's metadata.\n\n```rust\nuse nom_exif::{MediaParser, MediaSource, TrackInfoTag};\n\nlet path = \"PXL_20240101_120000000.MP.jpg\";\nlet mut parser = MediaParser::new();\n\n// 1. Parse the still image as usual.\nlet iter = parser.parse_exif(MediaSource::open(path)?)?;\nprintln!(\"has_embedded_track = {}\", iter.has_embedded_track());\n\n// 2. If true, re-open the source (parse_exif consumed it) and call\n//    parse_track to extract the embedded MP4's metadata.\nif iter.has_embedded_track() {\n    let track = parser.parse_track(MediaSource::open(path)?)?;\n    println!(\"video {:?}x{:?}\",\n        track.get(TrackInfoTag::Width),\n        track.get(TrackInfoTag::Height));\n}\n# Ok::\u003c(), nom_exif::Error\u003e(())\n```\n\n`has_embedded_track` is **content-detected**, not a MIME-level guess — a\nplain JPEG without the Motion Photo XMP returns `false` and `parse_track`\nreturns `Error::TrackNotFound`.\n\n**Coverage**: Pixel/Google Motion Photos and Samsung Galaxy Motion\nPhotos that use the Adobe XMP Container directory format (modern Pixel\nincluding Ultra HDR, modern Galaxy JPEGs).\n\n## Two API styles for Exif\n\nThe library exposes both **eager** and **lazy** views of EXIF metadata.\n\n```rust\nuse nom_exif::{read_exif, read_exif_iter, ExifTag};\n\n// Eager — easiest. Get-by-tag, parsed up front.\nlet exif = read_exif(\"./testdata/exif.jpg\")?;\nlet make = exif.get(ExifTag::Make).and_then(|v| v.as_str());\n\n// Lazy — finer-grained. Parse-on-demand, per-entry errors visible.\nlet iter = read_exif_iter(\"./testdata/exif.jpg\")?;\nfor entry in iter {\n    let _tag = entry.tag();          // TagOrCode (Tag(...) or Unknown(code))\n    let _ifd = entry.ifd();          // IfdIndex\n    let _ = entry.into_result();      // Result\u003cEntryValue, EntryError\u003e\n}\n# Ok::\u003c(), nom_exif::Error\u003e(())\n```\n\n## Async API\n\nEnable the `tokio` feature in your `Cargo.toml`:\n\n```toml\n[dependencies]\nnom-exif = { version = \"3\", features = [\"tokio\"] }\n```\n\nThen use the `_async` helpers, or call `parse_exif_async` /\n`parse_track_async` on a `MediaParser` directly:\n\n```rust,no_run\n# #[cfg(feature = \"tokio\")]\n# async fn demo() -\u003e nom_exif::Result\u003c()\u003e {\nuse nom_exif::{read_exif_async, MediaParser, AsyncMediaSource};\n\n// One-shot:\nlet exif = read_exif_async(\"./testdata/exif.jpg\").await?;\n\n// Reusable:\nlet mut parser = MediaParser::new();\nlet ms = AsyncMediaSource::open(\"./testdata/exif.jpg\").await?;\nlet iter = parser.parse_exif_async(ms).await?;\n# let _ = (exif, iter); Ok(())\n# }\n```\n\n## GPS Info\n\n`Exif` and `TrackInfo` both expose `gps_info()`. `ExifIter` adds\n`parse_gps()` for early termination once GPS tags have been read.\n\n```rust\nuse nom_exif::{read_exif, LatRef, LonRef, Altitude};\n\nlet exif = read_exif(\"./testdata/exif.heic\")?;\nif let Some(g) = exif.gps_info() {\n    let _ = matches!(g.latitude_ref, LatRef::North | LatRef::South);\n    let _ = matches!(g.longitude_ref, LonRef::East | LonRef::West);\n    let _ = matches!(g.altitude, Altitude::AboveSeaLevel(_) | Altitude::BelowSeaLevel(_));\n    let _iso = g.to_iso6709();\n}\n# Ok::\u003c(), nom_exif::Error\u003e(())\n```\n\n## Migration from v2\n\nv3.0.0 reshapes the public API end-to-end. The full migration guide lives\nin [`docs/MIGRATION.md`](docs/MIGRATION.md) — every row there is exercised\nby `tests/migration_guide.rs`. A few high-traffic items:\n\n- `MediaSource::file_path(p)` → `MediaSource::open(p)` or `read_exif(p)`.\n- `parser.parse::\u003c_,_,ExifIter\u003e(ms)` → `parser.parse_exif(ms)`.\n- `parser.parse::\u003c_,_,TrackInfo\u003e(ms)` → `parser.parse_track(ms)`.\n- `entry.take_result()` (panicky) → `entry.into_result()` (consumes self).\n- `iter.parse_gps_info()` → `iter.parse_gps()`.\n- `info.get_gps_info()` → `info.gps_info()` (returns `Option\u003c\u0026GPSInfo\u003e`).\n- `g.latitude_ref == 'N'` → `matches!(g.latitude_ref, LatRef::North)`.\n- Cargo features: `async` → `tokio`, `json_dump` → `serde`.\n\n## CLI Tool `rexiftool`\n\n### Human Readable Output\n\n`cargo run --example rexiftool testdata/meta.mov`:\n\n```text\nMake                            =\u003e Apple\nModel                           =\u003e iPhone X\nSoftware                        =\u003e 12.1.2\nCreateDate                      =\u003e 2024-02-02T08:09:57+00:00\nDurationMs                      =\u003e 500\nWidth                           =\u003e 720\nHeight                          =\u003e 1280\nGpsIso6709                      =\u003e +27.1281+100.2508+000.000/\n```\n\nPass `--debug` to enable tracing logs:\n\n`cargo run --example rexiftool -- --debug ./testdata/meta.mov`\n\nWhen the source carries an embedded media track (e.g. a Pixel Motion\nPhoto MP4 trailer), its metadata is appended after the EXIF entries\nunder an `-- Embedded Track --` separator. Pass `--no-track` to skip\nthis and show only EXIF.\n\n### JSON Dump\n\n`cargo run --features serde --example rexiftool testdata/meta.mov -j`:\n\n```text\n{\n  \"Width\": \"720\",\n  \"Software\": \"12.1.2\",\n  \"Height\": \"1280\",\n  \"Make\": \"Apple\",\n  \"GpsIso6709\": \"+27.1281+100.2508+000.000/\",\n  \"CreateDate\": \"2024-02-02T08:09:57+00:00\",\n  \"Model\": \"iPhone X\",\n  \"DurationMs\": \"500\"\n}\n```\n\nFor images with embedded tracks (Pixel Motion Photo etc.), the track's\nmetadata appears under a nested `_embedded_track` key. Pass `--no-track`\nto omit it.\n\n### Parsing Files in a Directory\n\n`rexiftool` also supports batch parsing of all files in a folder\n(non-recursive).\n\n`cargo run --example rexiftool testdata/`:\n\n```text\nFile: \"testdata/embedded-in-heic.mov\"\n------------------------------------------------\nMake                            =\u003e Apple\nModel                           =\u003e iPhone 15 Pro\nSoftware                        =\u003e 17.1\nCreateDate                      =\u003e 2023-11-02T12:01:02+00:00\nDurationMs                      =\u003e 2795\nWidth                           =\u003e 1920\nHeight                          =\u003e 1440\nGpsIso6709                      =\u003e +22.5797+113.9380+028.396/\n\nFile: \"testdata/exif.jpg\"\n------------------------------------------------\nImageWidth                      =\u003e 3072\nModel                           =\u003e vivo X90 Pro+\nImageHeight                     =\u003e 4096\nModifyDate                      =\u003e 2023-07-09T20:36:33+08:00\n...\n```\n\n## Contributing\n\nEnable the repository's pre-commit hook once per clone so commits that\nwould fail `cargo fmt --check` in CI are rejected locally:\n\n```sh\ngit config core.hooksPath .githooks\n```\n\nThe hook lives in `.githooks/pre-commit` and runs `cargo fmt --check`\n(sub-second). Bypass with `git commit --no-verify` for emergencies.\n\n## Fuzz Testing\n\nThe project uses [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz)\n(libFuzzer) for fuzz testing. Requires nightly Rust.\n\n**Run the fuzzer:**\n\n```sh\n# Use testdata/ as seed corpus, write new corpus to fuzz/corpus/media_parser/\ncargo +nightly fuzz run media_parser fuzz/corpus/media_parser/ testdata/\n```\n\n**Reproduce a crash:**\n\n```sh\ncargo +nightly fuzz run media_parser fuzz/artifacts/media_parser/\u003ccrash-file\u003e\n```\n\n**Minimize a crash input:**\n\n```sh\ncargo +nightly fuzz tmin media_parser fuzz/artifacts/media_parser/\u003ccrash-file\u003e\n```\n\n## Changelog\n\n[CHANGELOG.md](CHANGELOG.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmindeng%2Fnom-exif","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmindeng%2Fnom-exif","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmindeng%2Fnom-exif/lists"}