Skip to main content

nom_exif/
lib.rs

1//! `nom-exif` is a pure Rust library for **both image EXIF and
2//! video / audio track metadata** through a single unified API.
3//!
4//! # Highlights
5//!
6//! - Pure Rust — no FFmpeg, no libexif, no system deps; cross-compiles
7//!   cleanly.
8//! - Image **and** video / audio in one crate — [`MediaParser`] dispatches
9//!   to the right backend by detected MIME, no per-format wrappers.
10//! - RAW format support — Canon CR3, Fujifilm RAF, Phase One IIQ,
11//!   alongside JPEG / HEIC / AVIF / PNG / TIFF.
12//! - **Motion Photo** support — Pixel and Samsung Motion Photos (JPEG
13//!   with an embedded MP4) are detected automatically; `parse_track`
14//!   extracts the embedded video's track metadata.
15//! - Three input modes — files, arbitrary `Read` / `Read + Seek`
16//!   (network streams, pipes), or in-RAM bytes (WASM, mobile, HTTP
17//!   proxies).
18//! - Sync and async unified under one [`MediaParser`].
19//! - Eager ([`Exif`], get-by-tag) or lazy ([`ExifIter`], parse-on-demand)
20//!   — per-entry errors surface in both modes ([`Exif::errors`] /
21//!   per-iter `Result`), so one bad tag doesn't poison the parse.
22//! - Allocation-frugal — parser buffer is recycled across calls;
23//!   sub-IFDs share the same allocation (no deep copies).
24//! - Fuzz-tested with `cargo-fuzz` against malformed and adversarial input.
25//!
26//! # Quick start
27//!
28//! For a one-shot read, use the helpers:
29//!
30//! ```rust
31//! use nom_exif::{read_exif, ExifTag};
32//!
33//! let exif = read_exif("./testdata/exif.jpg")?;
34//! let make = exif.get(ExifTag::Make).and_then(|v| v.as_str());
35//! assert_eq!(make, Some("vivo"));
36//! # Ok::<(), nom_exif::Error>(())
37//! ```
38//!
39//! For batch processing, build a [`MediaParser`] once and reuse its
40//! buffer:
41//!
42//! ```rust
43//! use nom_exif::{MediaKind, MediaParser, MediaSource};
44//!
45//! let mut parser = MediaParser::new();
46//! for path in ["./testdata/exif.jpg", "./testdata/meta.mov"] {
47//!     let ms = MediaSource::open(path)?;
48//!     match ms.kind() {
49//!         MediaKind::Image => { let _ = parser.parse_exif(ms)?; }
50//!         MediaKind::Track => { let _ = parser.parse_track(ms)?; }
51//!     }
52//! }
53//! # Ok::<(), nom_exif::Error>(())
54//! ```
55//!
56//! Async variants live behind `feature = "tokio"`:
57//! [`read_exif_async`], [`read_track_async`], [`read_metadata_async`],
58//! plus [`MediaParser::parse_exif_async`] / [`MediaParser::parse_track_async`].
59//!
60//! # Motion Photos (embedded media tracks)
61//!
62//! Some images embed a media track that `parse_exif` doesn't surface —
63//! most commonly **Pixel/Google Motion Photo** JPEGs, which carry a short
64//! MP4 video appended after the JPEG image data. The
65//! [`Exif::has_embedded_track`] / [`ExifIter::has_embedded_track`] flags
66//! are set by `parse_exif` when it observes a concrete content signal
67//! (e.g. the `GCamera:MotionPhoto="1"` XMP attribute). When the flag is
68//! `true`, call [`MediaParser::parse_track`] on the same source to
69//! extract the embedded MP4's metadata — `parse_track` automatically
70//! locates and parses the trailer.
71//!
72//! ```no_run
73//! use nom_exif::{MediaParser, MediaSource};
74//! let mut parser = MediaParser::new();
75//! let path = "PXL_20240101_120000000.MP.jpg";
76//! let iter = parser.parse_exif(MediaSource::open(path)?)?;
77//! if iter.has_embedded_track() {
78//!     // Re-open: MediaSource is consumed by parse_exif.
79//!     let track = parser.parse_track(MediaSource::open(path)?)?;
80//!     // ...
81//! }
82//! # Ok::<(), nom_exif::Error>(())
83//! ```
84//!
85//! **Coverage**: Pixel/Google Motion Photos and Samsung Galaxy Motion
86//! Photos that use the Adobe XMP Container directory format (modern
87//! Pixel including Ultra HDR, modern Galaxy JPEGs).
88//!
89//! # Reading from in-memory bytes
90//!
91//! When the payload is already in RAM (WASM, mobile, HTTP proxy, decoded
92//! response body), use [`MediaSource::from_memory`] to skip the `File` /
93//! `Read` round-trip entirely. Memory mode is **zero-copy**: the underlying
94//! allocation is shared with the returned [`Exif`] / [`ExifIter`] /
95//! [`TrackInfo`] via [`bytes::Bytes`] reference counting.
96//!
97//! ```rust
98//! use nom_exif::{MediaSource, MediaParser, ExifTag};
99//!
100//! let raw = std::fs::read("./testdata/exif.jpg")?;
101//! let ms = MediaSource::from_memory(raw)?;
102//! let mut parser = MediaParser::new();
103//! let iter = parser.parse_exif(ms)?;
104//! let exif: nom_exif::Exif = iter.into();
105//! assert_eq!(exif.get(ExifTag::Make).and_then(|v| v.as_str()), Some("vivo"));
106//! # Ok::<(), nom_exif::Error>(())
107//! ```
108//!
109//! # Image metadata beyond EXIF
110//!
111//! Some image formats carry metadata that does not fit the EXIF / IFD
112//! model. PNG's `tEXt` chunks are the headline example: arbitrary
113//! Latin-1 key/value pairs (`Title`, `Author`, `Comment`, …). For
114//! PNG-aware (or future GIF / WebP / JXL extras-aware) callers, use
115//! [`MediaParser::parse_image_metadata`]:
116//!
117//! ```rust
118//! use nom_exif::{MediaParser, MediaSource, ImageFormatMetadata};
119//!
120//! let mut parser = MediaParser::new();
121//! let ms = MediaSource::open("./testdata/exif.png")?;
122//! let img = parser.parse_image_metadata(ms)?;
123//!
124//! if let Some(ImageFormatMetadata::Png(text_chunks)) = img.format {
125//!     let _title = text_chunks.get("Title");
126//! }
127//! # Ok::<(), nom_exif::Error>(())
128//! ```
129//!
130//! Returns [`ImageMetadata<ExifIter>`](ImageMetadata) (lazy form);
131//! convert to the eager `ImageMetadata<Exif>` via `.into()` if
132//! needed. Top-level `read_image_metadata` helpers are deferred to
133//! v4 alongside the [`Metadata`] enum redesign.
134//!
135//! # API surface
136//!
137//! - **One-shot helpers**: [`read_exif`], [`read_exif_iter`], [`read_track`], [`read_metadata`].
138//! - **Reusable parser**: [`MediaParser`] + [`MediaSource`] (or [`AsyncMediaSource`])
139//!   + [`MediaKind`]. Use [`MediaSource::from_memory`] for in-RAM bytes.
140//! - **Image metadata**: [`Exif`] (eager, get-by-tag) or [`ExifIter`]
141//!   (lazy iterator with per-entry errors). Convert: `let exif: Exif = iter.into();`.
142//! - **Track metadata**: [`TrackInfo`] (audio/video container metadata).
143//! - **Discriminated union**: [`Metadata`] returned by [`read_metadata`].
144//! - **Errors**: [`Error`] for parse-level, [`EntryError`] for per-entry
145//!   IFD errors, [`ConvertError`] for type-conversion peer errors.
146//! - **Convenience**: [`prelude`] re-exports the symbols you most often need.
147//!
148//! See `docs/MIGRATION.md` for the v2 → v3 migration guide and
149//! `docs/V3_API_DESIGN.md` for the internal design contract.
150//!
151//! # Cargo features
152//!
153//! - `tokio` — async API via tokio (`AsyncMediaSource`, `read_*_async`,
154//!   `MediaParser::parse_*_async`).
155//! - `serde` — derives `Serialize`/`Deserialize` on the public types.
156
157pub use parser::{MediaKind, MediaParser, MediaSource};
158pub use video::{TrackInfo, TrackInfoTag};
159
160#[cfg(feature = "tokio")]
161pub use parser_async::AsyncMediaSource;
162
163pub use exif::gps::{Altitude, LatRef, LonRef, Speed, SpeedUnit};
164pub use exif::png_text::PngTextChunks;
165pub use exif::{
166    Exif, ExifEntry, ExifIter, ExifIterEntry, ExifTag, GPSInfo, IfdIndex, LatLng, TagOrCode,
167};
168pub use image_metadata::{ExifRepr, ImageFormatMetadata, ImageMetadata};
169pub use values::{EntryValue, ExifDateTime, IRational, Rational, URational};
170
171pub use error::{ConvertError, EntryError, Error, MalformedKind};
172
173/// Convenient one-line import of the most common v3 symbols.
174///
175/// ```rust
176/// use nom_exif::prelude::*;
177/// # fn main() -> Result<()> { Ok(()) }
178/// ```
179///
180/// Includes [`Error`] and [`MalformedKind`] so error-matching code does
181/// not need a second import. Cold-path types (e.g. `Rational`,
182/// `LatLng`, `ConvertError`, `ExifDateTime`) are intentionally **not**
183/// in the prelude — import them explicitly via `nom_exif::Type`.
184pub mod prelude {
185    pub use crate::{read_exif, read_metadata, read_track};
186    pub use crate::{
187        EntryValue, Error, Exif, ExifIter, ExifTag, GPSInfo, IfdIndex, MalformedKind, MediaKind,
188        MediaParser, MediaSource, Metadata, Result, TrackInfo, TrackInfoTag,
189    };
190}
191
192/// Crate-wide convenience alias for `std::result::Result<T, Error>`.
193pub type Result<T> = std::result::Result<T, Error>;
194
195/// One-shot result of [`read_metadata`]: either Exif (image) or TrackInfo
196/// (video/audio). Closed enum — see spec §8.6 for why there's no `Both`
197/// variant.
198#[derive(Debug, Clone)]
199pub enum Metadata {
200    Exif(Exif),
201    Track(TrackInfo),
202}
203
204use std::path::Path;
205
206/// Read EXIF metadata from a file in a single call.
207///
208/// For batch processing, prefer constructing a [`MediaParser`] once and
209/// reusing its parse buffer via [`MediaParser::parse_exif`].
210pub fn read_exif(path: impl AsRef<Path>) -> Result<Exif> {
211    let iter = read_exif_iter(path)?;
212    Ok(iter.into())
213}
214
215/// Read EXIF metadata from a file as a lazy iterator. Like [`read_exif`]
216/// but returns an [`ExifIter`] so per-entry errors can be inspected and
217/// values fetched without materializing the full [`Exif`] map.
218///
219/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_exif`].
220pub fn read_exif_iter(path: impl AsRef<Path>) -> Result<ExifIter> {
221    let file = std::fs::File::open(path)?;
222    let ms = MediaSource::seekable(file)?;
223    let mut parser = MediaParser::new();
224    parser.parse_exif(ms)
225}
226
227/// Read track metadata from a video / audio file in a single call.
228///
229/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_track`].
230pub fn read_track(path: impl AsRef<Path>) -> Result<TrackInfo> {
231    let file = std::fs::File::open(path)?;
232    let ms = MediaSource::seekable(file)?;
233    let mut parser = MediaParser::new();
234    parser.parse_track(ms)
235}
236
237/// Read metadata from a file, dispatching by detected [`MediaKind`]:
238/// images return [`Metadata::Exif`], video / audio containers return
239/// [`Metadata::Track`].
240///
241/// Use this when the caller does not know up-front whether the file is an
242/// image or a track. For batch processing, reuse a [`MediaParser`] and
243/// branch on [`MediaSource::kind`] manually.
244pub fn read_metadata(path: impl AsRef<Path>) -> Result<Metadata> {
245    let file = std::fs::File::open(path)?;
246    let ms = MediaSource::seekable(file)?;
247    let mut parser = MediaParser::new();
248    match ms.kind() {
249        MediaKind::Image => parser.parse_exif(ms).map(|i| Metadata::Exif(i.into())),
250        MediaKind::Track => parser.parse_track(ms).map(Metadata::Track),
251    }
252}
253
254/// **Deprecated since v3.3.0**: use [`read_exif`] with
255/// [`MediaSource::from_memory`] directly.
256#[deprecated(
257    since = "3.3.0",
258    note = "Use `read_exif` with `MediaSource::from_memory`."
259)]
260pub fn read_exif_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Exif> {
261    #[allow(deprecated)]
262    let iter = read_exif_iter_from_bytes(bytes)?;
263    Ok(iter.into())
264}
265
266#[deprecated(
267    since = "3.3.0",
268    note = "Use `read_exif_iter` with `MediaSource::from_memory`."
269)]
270pub fn read_exif_iter_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<ExifIter> {
271    let ms = MediaSource::from_memory(bytes)?;
272    let mut parser = MediaParser::new();
273    parser.parse_exif(ms)
274}
275
276#[deprecated(
277    since = "3.3.0",
278    note = "Use `read_track` with `MediaSource::from_memory`."
279)]
280pub fn read_track_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<TrackInfo> {
281    let ms = MediaSource::from_memory(bytes)?;
282    let mut parser = MediaParser::new();
283    parser.parse_track(ms)
284}
285
286#[deprecated(
287    since = "3.3.0",
288    note = "Use `read_metadata` with `MediaSource::from_memory`."
289)]
290pub fn read_metadata_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Metadata> {
291    let ms = MediaSource::from_memory(bytes)?;
292    let mut parser = MediaParser::new();
293    match ms.kind() {
294        MediaKind::Image => parser.parse_exif(ms).map(|i| Metadata::Exif(i.into())),
295        MediaKind::Track => parser.parse_track(ms).map(Metadata::Track),
296    }
297}
298
299#[cfg(feature = "tokio")]
300mod tokio_top_level {
301    use super::*;
302
303    pub async fn read_exif_async(path: impl AsRef<std::path::Path>) -> Result<Exif> {
304        let iter = read_exif_iter_async(path).await?;
305        Ok(iter.into())
306    }
307
308    pub async fn read_exif_iter_async(path: impl AsRef<std::path::Path>) -> Result<ExifIter> {
309        let file = tokio::fs::File::open(path).await?;
310        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
311        let mut parser = MediaParser::new();
312        parser.parse_exif_async(ms).await
313    }
314
315    pub async fn read_track_async(path: impl AsRef<std::path::Path>) -> Result<TrackInfo> {
316        let file = tokio::fs::File::open(path).await?;
317        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
318        let mut parser = MediaParser::new();
319        parser.parse_track_async(ms).await
320    }
321
322    pub async fn read_metadata_async(path: impl AsRef<std::path::Path>) -> Result<Metadata> {
323        let file = tokio::fs::File::open(path).await?;
324        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
325        let mut parser = MediaParser::new();
326        match ms.kind() {
327            MediaKind::Image => parser
328                .parse_exif_async(ms)
329                .await
330                .map(|i| Metadata::Exif(i.into())),
331            MediaKind::Track => parser.parse_track_async(ms).await.map(Metadata::Track),
332        }
333    }
334}
335
336#[cfg(feature = "tokio")]
337pub use tokio_top_level::{
338    read_exif_async, read_exif_iter_async, read_metadata_async, read_track_async,
339};
340
341mod bbox;
342mod cr3;
343mod ebml;
344mod error;
345mod exif;
346mod file;
347mod heif;
348mod image_metadata;
349mod jpeg;
350mod mov;
351mod parser;
352#[cfg(feature = "tokio")]
353mod parser_async;
354mod png;
355mod raf;
356mod slice;
357mod utils;
358mod values;
359mod video;
360
361#[cfg(test)]
362mod testkit;
363
364#[cfg(test)]
365mod v3_top_level_tests {
366    use super::*;
367
368    #[test]
369    fn read_exif_jpg() {
370        let exif = read_exif("testdata/exif.jpg").unwrap();
371        assert!(exif.get(ExifTag::Make).is_some());
372    }
373
374    #[test]
375    fn read_track_mov() {
376        let info = read_track("testdata/meta.mov").unwrap();
377        assert!(info.get(TrackInfoTag::Make).is_some());
378    }
379
380    #[test]
381    fn read_metadata_dispatches_image() {
382        match read_metadata("testdata/exif.jpg").unwrap() {
383            Metadata::Exif(_) => {}
384            Metadata::Track(_) => panic!("expected Exif variant"),
385        }
386    }
387
388    #[test]
389    fn read_metadata_dispatches_track() {
390        match read_metadata("testdata/meta.mov").unwrap() {
391            Metadata::Track(_) => {}
392            Metadata::Exif(_) => panic!("expected Track variant"),
393        }
394    }
395
396    #[cfg(feature = "tokio")]
397    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
398    async fn read_exif_async_jpg() {
399        let exif = read_exif_async("testdata/exif.jpg").await.unwrap();
400        assert!(exif.get(ExifTag::Make).is_some());
401    }
402
403    #[cfg(feature = "tokio")]
404    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
405    async fn read_track_async_mov() {
406        let info = read_track_async("testdata/meta.mov").await.unwrap();
407        assert!(info.get(TrackInfoTag::Make).is_some());
408    }
409
410    #[test]
411    #[allow(deprecated)]
412    fn read_exif_from_bytes_jpg() {
413        let raw = std::fs::read("testdata/exif.jpg").unwrap();
414        let exif = read_exif_from_bytes(raw).unwrap();
415        assert!(exif.get(ExifTag::Make).is_some());
416    }
417
418    #[test]
419    #[allow(deprecated)]
420    fn read_exif_iter_from_bytes_jpg() {
421        let raw = std::fs::read("testdata/exif.jpg").unwrap();
422        let iter = read_exif_iter_from_bytes(raw).unwrap();
423        assert!(iter.into_iter().count() > 0);
424    }
425
426    #[test]
427    #[allow(deprecated)]
428    fn read_track_from_bytes_mov() {
429        let raw = std::fs::read("testdata/meta.mov").unwrap();
430        let info = read_track_from_bytes(raw).unwrap();
431        assert!(info.get(TrackInfoTag::Make).is_some());
432    }
433
434    #[test]
435    #[allow(deprecated)]
436    fn read_metadata_from_bytes_dispatches_image() {
437        let raw = std::fs::read("testdata/exif.jpg").unwrap();
438        match read_metadata_from_bytes(raw).unwrap() {
439            Metadata::Exif(_) => {}
440            Metadata::Track(_) => panic!("expected Exif variant"),
441        }
442    }
443
444    #[test]
445    #[allow(deprecated)]
446    fn read_metadata_from_bytes_dispatches_track() {
447        let raw = std::fs::read("testdata/meta.mov").unwrap();
448        match read_metadata_from_bytes(raw).unwrap() {
449            Metadata::Track(_) => {}
450            Metadata::Exif(_) => panic!("expected Track variant"),
451        }
452    }
453
454    #[test]
455    #[allow(deprecated)]
456    fn read_exif_from_bytes_static_slice() {
457        let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
458        let exif = read_exif_from_bytes(raw).unwrap();
459        assert!(exif.get(ExifTag::Make).is_some());
460    }
461
462    #[test]
463    fn prelude_imports_compile() {
464        use crate::prelude::*;
465        fn _consume(_: Option<Exif>, _: Option<TrackInfo>, _: Option<MediaParser>) {}
466        // Verify the function symbols are in scope (compilation is the test).
467        let _e = read_exif("testdata/exif.jpg");
468        let _t = read_track("testdata/meta.mov");
469        let _m = read_metadata("testdata/exif.jpg");
470    }
471}