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 / TIFF.
12//! - Three input modes — files, arbitrary `Read` / `Read + Seek`
13//! (network streams, pipes), or in-RAM bytes (WASM, mobile, HTTP
14//! proxies).
15//! - Sync and async unified under one [`MediaParser`].
16//! - Eager ([`Exif`], get-by-tag) or lazy ([`ExifIter`], parse-on-demand)
17//! — per-entry errors surface in both modes ([`Exif::errors`] /
18//! per-iter `Result`), so one bad tag doesn't poison the parse.
19//! - Allocation-frugal — parser buffer is recycled across calls;
20//! sub-IFDs share the same allocation (no deep copies).
21//! - Fuzz-tested with `cargo-fuzz` against malformed and adversarial input.
22//!
23//! # Quick start
24//!
25//! For a one-shot read, use the helpers:
26//!
27//! ```rust
28//! use nom_exif::{read_exif, ExifTag};
29//!
30//! let exif = read_exif("./testdata/exif.jpg")?;
31//! let make = exif.get(ExifTag::Make).and_then(|v| v.as_str());
32//! assert_eq!(make, Some("vivo"));
33//! # Ok::<(), nom_exif::Error>(())
34//! ```
35//!
36//! For batch processing, build a [`MediaParser`] once and reuse its
37//! buffer:
38//!
39//! ```rust
40//! use nom_exif::{MediaKind, MediaParser, MediaSource};
41//!
42//! let mut parser = MediaParser::new();
43//! for path in ["./testdata/exif.jpg", "./testdata/meta.mov"] {
44//! let ms = MediaSource::open(path)?;
45//! match ms.kind() {
46//! MediaKind::Image => { let _ = parser.parse_exif(ms)?; }
47//! MediaKind::Track => { let _ = parser.parse_track(ms)?; }
48//! }
49//! }
50//! # Ok::<(), nom_exif::Error>(())
51//! ```
52//!
53//! Async variants live behind `feature = "tokio"`:
54//! [`read_exif_async`], [`read_track_async`], [`read_metadata_async`],
55//! plus [`MediaParser::parse_exif_async`] / [`MediaParser::parse_track_async`].
56//!
57//! # Reading from in-memory bytes
58//!
59//! When the payload is already in RAM (WASM, mobile, HTTP proxy, decoded
60//! response body), use the `*_from_bytes` helpers to skip the `File` /
61//! `Read` round-trip entirely. Memory mode is **zero-copy**: the underlying
62//! allocation is shared with the returned [`Exif`] / [`ExifIter`] /
63//! [`TrackInfo`] via [`bytes::Bytes`] reference counting.
64//!
65//! ```rust
66//! use nom_exif::{read_exif_from_bytes, ExifTag};
67//!
68//! let raw = std::fs::read("./testdata/exif.jpg")?;
69//! let exif = read_exif_from_bytes(raw)?;
70//! assert_eq!(exif.get(ExifTag::Make).and_then(|v| v.as_str()), Some("vivo"));
71//! # Ok::<(), nom_exif::Error>(())
72//! ```
73//!
74//! For batch processing of many in-memory payloads, build a [`MediaParser`]
75//! once and call [`MediaParser::parse_exif_from_bytes`] /
76//! [`MediaParser::parse_track_from_bytes`] per payload.
77//!
78//! # API surface
79//!
80//! - **One-shot helpers**: [`read_exif`], [`read_exif_iter`], [`read_track`], [`read_metadata`]
81//! for files; [`read_exif_from_bytes`], [`read_exif_iter_from_bytes`],
82//! [`read_track_from_bytes`], [`read_metadata_from_bytes`] for in-memory bytes.
83//! - **Reusable parser**: [`MediaParser`] + [`MediaSource`] (or [`AsyncMediaSource`])
84//! + [`MediaKind`].
85//! - **Image metadata**: [`Exif`] (eager, get-by-tag) or [`ExifIter`]
86//! (lazy iterator with per-entry errors). Convert: `let exif: Exif = iter.into();`.
87//! - **Track metadata**: [`TrackInfo`] (audio/video container metadata).
88//! - **Discriminated union**: [`Metadata`] returned by [`read_metadata`].
89//! - **Errors**: [`Error`] for parse-level, [`EntryError`] for per-entry
90//! IFD errors, [`ConvertError`] for type-conversion peer errors.
91//! - **Convenience**: [`prelude`] re-exports the symbols you most often need.
92//!
93//! See `docs/MIGRATION.md` for the v2 → v3 migration guide and
94//! `docs/V3_API_DESIGN.md` for the internal design contract.
95//!
96//! # Cargo features
97//!
98//! - `tokio` — async API via tokio (`AsyncMediaSource`, `read_*_async`,
99//! `MediaParser::parse_*_async`).
100//! - `serde` — derives `Serialize`/`Deserialize` on the public types.
101//!
102//! # Embedded media
103//!
104//! Some formats (HEIC Live Photos, RAF JPEG previews, …) embed media
105//! streams that `parse_exif` does not surface. The
106//! [`Exif::has_embedded_media`] / [`ExifIter::has_embedded_media`] /
107//! [`TrackInfo::has_embedded_media`] flags let callers detect this; the
108//! actual extraction API is a v3.x deliverable.
109
110pub use parser::{MediaKind, MediaParser, MediaSource};
111pub use video::{TrackInfo, TrackInfoTag};
112
113#[cfg(feature = "tokio")]
114pub use parser_async::AsyncMediaSource;
115
116pub use exif::gps::{Altitude, LatRef, LonRef, Speed, SpeedUnit};
117pub use exif::{
118 Exif, ExifEntry, ExifIter, ExifIterEntry, ExifTag, GPSInfo, IfdIndex, LatLng, TagOrCode,
119};
120pub use values::{EntryValue, ExifDateTime, IRational, Rational, URational};
121
122pub use error::{ConvertError, EntryError, Error, MalformedKind};
123
124/// Convenient one-line import of the most common v3 symbols.
125///
126/// ```rust
127/// use nom_exif::prelude::*;
128/// # fn main() -> Result<()> { Ok(()) }
129/// ```
130///
131/// Includes [`Error`] and [`MalformedKind`] so error-matching code does
132/// not need a second import. Cold-path types (e.g. `Rational`,
133/// `LatLng`, `ConvertError`, `ExifDateTime`) are intentionally **not**
134/// in the prelude — import them explicitly via `nom_exif::Type`.
135pub mod prelude {
136 pub use crate::{read_exif, read_metadata, read_track};
137 pub use crate::{
138 EntryValue, Error, Exif, ExifIter, ExifTag, GPSInfo, IfdIndex, MalformedKind, MediaKind,
139 MediaParser, MediaSource, Metadata, Result, TrackInfo, TrackInfoTag,
140 };
141}
142
143/// Crate-wide convenience alias for `std::result::Result<T, Error>`.
144pub type Result<T> = std::result::Result<T, Error>;
145
146/// One-shot result of [`read_metadata`]: either Exif (image) or TrackInfo
147/// (video/audio). Closed enum — see spec §8.6 for why there's no `Both`
148/// variant.
149#[derive(Debug, Clone)]
150pub enum Metadata {
151 Exif(Exif),
152 Track(TrackInfo),
153}
154
155use std::path::Path;
156
157/// Read EXIF metadata from a file in a single call.
158///
159/// For batch processing, prefer constructing a [`MediaParser`] once and
160/// reusing its parse buffer via [`MediaParser::parse_exif`].
161pub fn read_exif(path: impl AsRef<Path>) -> Result<Exif> {
162 let iter = read_exif_iter(path)?;
163 Ok(iter.into())
164}
165
166/// Read EXIF metadata from a file as a lazy iterator. Like [`read_exif`]
167/// but returns an [`ExifIter`] so per-entry errors can be inspected and
168/// values fetched without materializing the full [`Exif`] map.
169///
170/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_exif`].
171pub fn read_exif_iter(path: impl AsRef<Path>) -> Result<ExifIter> {
172 let file = std::fs::File::open(path)?;
173 let ms = MediaSource::seekable(file)?;
174 let mut parser = MediaParser::new();
175 parser.parse_exif(ms)
176}
177
178/// Read track metadata from a video / audio file in a single call.
179///
180/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_track`].
181pub fn read_track(path: impl AsRef<Path>) -> Result<TrackInfo> {
182 let file = std::fs::File::open(path)?;
183 let ms = MediaSource::seekable(file)?;
184 let mut parser = MediaParser::new();
185 parser.parse_track(ms)
186}
187
188/// Read metadata from a file, dispatching by detected [`MediaKind`]:
189/// images return [`Metadata::Exif`], video / audio containers return
190/// [`Metadata::Track`].
191///
192/// Use this when the caller does not know up-front whether the file is an
193/// image or a track. For batch processing, reuse a [`MediaParser`] and
194/// branch on [`MediaSource::kind`] manually.
195pub fn read_metadata(path: impl AsRef<Path>) -> Result<Metadata> {
196 let file = std::fs::File::open(path)?;
197 let ms = MediaSource::seekable(file)?;
198 let mut parser = MediaParser::new();
199 match ms.kind() {
200 MediaKind::Image => parser.parse_exif(ms).map(|i| Metadata::Exif(i.into())),
201 MediaKind::Track => parser.parse_track(ms).map(Metadata::Track),
202 }
203}
204
205/// Read EXIF metadata from an in-memory byte payload in a single call.
206/// Zero-copy: the underlying allocation is shared with the returned
207/// [`Exif`] via [`bytes::Bytes`] reference counting.
208///
209/// Accepts anything convertible into [`bytes::Bytes`] — `Vec<u8>`,
210/// `&'static [u8]`, an existing `Bytes`, or HTTP-body types that implement
211/// `Into<Bytes>` directly.
212///
213/// For batch processing or multiple parses against the same buffer, prefer
214/// constructing a [`MediaParser`] once and reusing it via
215/// [`MediaParser::parse_exif_from_bytes`].
216pub fn read_exif_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Exif> {
217 let iter = read_exif_iter_from_bytes(bytes)?;
218 Ok(iter.into())
219}
220
221/// Read EXIF metadata from an in-memory byte payload as a lazy iterator.
222/// Like [`read_exif_from_bytes`] but returns an [`ExifIter`].
223pub fn read_exif_iter_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<ExifIter> {
224 let ms = MediaSource::from_bytes(bytes)?;
225 let mut parser = MediaParser::new();
226 parser.parse_exif_from_bytes(ms)
227}
228
229/// Read track metadata from an in-memory video/audio payload.
230pub fn read_track_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<TrackInfo> {
231 let ms = MediaSource::from_bytes(bytes)?;
232 let mut parser = MediaParser::new();
233 parser.parse_track_from_bytes(ms)
234}
235
236/// Read metadata from an in-memory payload, dispatching by detected
237/// [`MediaKind`]: images return [`Metadata::Exif`], video/audio containers
238/// return [`Metadata::Track`].
239pub fn read_metadata_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Metadata> {
240 let ms = MediaSource::from_bytes(bytes)?;
241 let mut parser = MediaParser::new();
242 match ms.kind() {
243 MediaKind::Image => parser
244 .parse_exif_from_bytes(ms)
245 .map(|i| Metadata::Exif(i.into())),
246 MediaKind::Track => parser.parse_track_from_bytes(ms).map(Metadata::Track),
247 }
248}
249
250#[cfg(feature = "tokio")]
251mod tokio_top_level {
252 use super::*;
253
254 pub async fn read_exif_async(path: impl AsRef<std::path::Path>) -> Result<Exif> {
255 let iter = read_exif_iter_async(path).await?;
256 Ok(iter.into())
257 }
258
259 pub async fn read_exif_iter_async(path: impl AsRef<std::path::Path>) -> Result<ExifIter> {
260 let file = tokio::fs::File::open(path).await?;
261 let ms = parser_async::AsyncMediaSource::seekable(file).await?;
262 let mut parser = MediaParser::new();
263 parser.parse_exif_async(ms).await
264 }
265
266 pub async fn read_track_async(path: impl AsRef<std::path::Path>) -> Result<TrackInfo> {
267 let file = tokio::fs::File::open(path).await?;
268 let ms = parser_async::AsyncMediaSource::seekable(file).await?;
269 let mut parser = MediaParser::new();
270 parser.parse_track_async(ms).await
271 }
272
273 pub async fn read_metadata_async(path: impl AsRef<std::path::Path>) -> Result<Metadata> {
274 let file = tokio::fs::File::open(path).await?;
275 let ms = parser_async::AsyncMediaSource::seekable(file).await?;
276 let mut parser = MediaParser::new();
277 match ms.kind() {
278 MediaKind::Image => parser
279 .parse_exif_async(ms)
280 .await
281 .map(|i| Metadata::Exif(i.into())),
282 MediaKind::Track => parser.parse_track_async(ms).await.map(Metadata::Track),
283 }
284 }
285}
286
287#[cfg(feature = "tokio")]
288pub use tokio_top_level::{
289 read_exif_async, read_exif_iter_async, read_metadata_async, read_track_async,
290};
291
292mod bbox;
293mod cr3;
294mod ebml;
295mod error;
296mod exif;
297mod file;
298mod heif;
299mod jpeg;
300mod mov;
301mod parser;
302#[cfg(feature = "tokio")]
303mod parser_async;
304mod raf;
305mod slice;
306mod utils;
307mod values;
308mod video;
309
310#[cfg(test)]
311mod testkit;
312
313#[cfg(test)]
314mod v3_top_level_tests {
315 use super::*;
316
317 #[test]
318 fn read_exif_jpg() {
319 let exif = read_exif("testdata/exif.jpg").unwrap();
320 assert!(exif.get(ExifTag::Make).is_some());
321 }
322
323 #[test]
324 fn read_track_mov() {
325 let info = read_track("testdata/meta.mov").unwrap();
326 assert!(info.get(TrackInfoTag::Make).is_some());
327 }
328
329 #[test]
330 fn read_metadata_dispatches_image() {
331 match read_metadata("testdata/exif.jpg").unwrap() {
332 Metadata::Exif(_) => {}
333 Metadata::Track(_) => panic!("expected Exif variant"),
334 }
335 }
336
337 #[test]
338 fn read_metadata_dispatches_track() {
339 match read_metadata("testdata/meta.mov").unwrap() {
340 Metadata::Track(_) => {}
341 Metadata::Exif(_) => panic!("expected Track variant"),
342 }
343 }
344
345 #[cfg(feature = "tokio")]
346 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
347 async fn read_exif_async_jpg() {
348 let exif = read_exif_async("testdata/exif.jpg").await.unwrap();
349 assert!(exif.get(ExifTag::Make).is_some());
350 }
351
352 #[cfg(feature = "tokio")]
353 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
354 async fn read_track_async_mov() {
355 let info = read_track_async("testdata/meta.mov").await.unwrap();
356 assert!(info.get(TrackInfoTag::Make).is_some());
357 }
358
359 #[test]
360 fn read_exif_from_bytes_jpg() {
361 let raw = std::fs::read("testdata/exif.jpg").unwrap();
362 let exif = read_exif_from_bytes(raw).unwrap();
363 assert!(exif.get(ExifTag::Make).is_some());
364 }
365
366 #[test]
367 fn read_exif_iter_from_bytes_jpg() {
368 let raw = std::fs::read("testdata/exif.jpg").unwrap();
369 let iter = read_exif_iter_from_bytes(raw).unwrap();
370 assert!(iter.into_iter().count() > 0);
371 }
372
373 #[test]
374 fn read_track_from_bytes_mov() {
375 let raw = std::fs::read("testdata/meta.mov").unwrap();
376 let info = read_track_from_bytes(raw).unwrap();
377 assert!(info.get(TrackInfoTag::Make).is_some());
378 }
379
380 #[test]
381 fn read_metadata_from_bytes_dispatches_image() {
382 let raw = std::fs::read("testdata/exif.jpg").unwrap();
383 match read_metadata_from_bytes(raw).unwrap() {
384 Metadata::Exif(_) => {}
385 Metadata::Track(_) => panic!("expected Exif variant"),
386 }
387 }
388
389 #[test]
390 fn read_metadata_from_bytes_dispatches_track() {
391 let raw = std::fs::read("testdata/meta.mov").unwrap();
392 match read_metadata_from_bytes(raw).unwrap() {
393 Metadata::Track(_) => {}
394 Metadata::Exif(_) => panic!("expected Track variant"),
395 }
396 }
397
398 #[test]
399 fn read_exif_from_bytes_static_slice() {
400 let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
401 let exif = read_exif_from_bytes(raw).unwrap();
402 assert!(exif.get(ExifTag::Make).is_some());
403 }
404
405 #[test]
406 fn prelude_imports_compile() {
407 use crate::prelude::*;
408 fn _consume(_: Option<Exif>, _: Option<TrackInfo>, _: Option<MediaParser>) {}
409 // Verify the function symbols are in scope (compilation is the test).
410 let _e = read_exif("testdata/exif.jpg");
411 let _t = read_track("testdata/meta.mov");
412 let _m = read_metadata("testdata/exif.jpg");
413 }
414}