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