This is the canonical, user-facing migration guide for the v3.0.0 breaking
release. Every row in this document is exercised by tests/migration_guide.rs,
which compiles against the public API as a downstream crate would.
If a row here is wrong, the test will fail. If you change the public surface,
update the corresponding row here, the entry in CHANGELOG.md, and the
matching test — the three artifacts are meant to stay in lock-step.
For internal architecture decisions and design rationale, see
docs/V3_API_DESIGN.md.
| v2 | v3 |
|---|---|
MediaSource::file_path(p) |
MediaSource::open(p) (or read_exif(p)) |
MediaSource::tcp_stream(s) |
MediaSource::unseekable(s) |
ms.has_exif() |
ms.kind() == MediaKind::Image |
ms.has_track() |
ms.kind() == MediaKind::Track (note: Video was renamed Track; pure-audio containers like .mka fall under this kind too) |
parser.parse::<_, _, ExifIter>(ms) |
parser.parse_exif(ms) |
parser.parse::<_, _, TrackInfo>(ms) |
parser.parse_track(ms) |
MediaSource<R, S> (two type parameters) |
MediaSource<R> (the S parameter was deleted) |
Implicit seek-fallback-to-read (the v2 Skip trait's bool return) |
Removed — seek failure now returns Error::Io |
New convenience helpers (no v2 equivalent):
let exif = nom_exif::read_exif("photo.jpg")?; // one-shot eager
let iter = nom_exif::read_exif_iter("photo.jpg")?; // one-shot lazy
let info = nom_exif::read_track("video.mp4")?;
let meta = nom_exif::read_metadata("file.heic")?; // returns Metadata::{Exif,Track}| v2 | v3 |
|---|---|
Error::ParseFailed(Box<dyn Error>) |
Structured variants: Malformed { kind, message }, UnexpectedEof, UnsupportedFormat |
Error::IOError(e) |
Error::Io(e) (renamed for brevity) |
From<&str> for Error, From<String> for Error |
Removed — use a structured variant |
EntryError (crate-private enum with String payloads) |
Public enum with three structured variants: Truncated, InvalidShape, InvalidValue(&'static str) |
| No entry-level → file-level error propagation | From<EntryError> for Error (maps to Malformed { kind: IfdEntry, .. }) |
Conversion errors scattered across crate::Error and standalone types |
Unified into ConvertError (a peer type — ConvertError and Error do not convert into each other) |
| v2 | v3 |
|---|---|
value.as_time_components() -> Option<(NaiveDateTime, Option<FixedOffset>)> |
value.as_datetime() -> Option<ExifDateTime>, where ExifDateTime is Aware/Naive with aware() / into_naive() / or_offset(fallback) accessors |
value.as_u8array() |
value.as_u8_slice() |
value.to_u8array() |
Removed — use as_u8_slice().map(<[u8]>::to_vec) |
Missing as_i64 / as_f64 / as_u16_slice / etc. |
Filled in. as_f32 is intentionally not provided (as_f64 covers it via widening); as_i8 / as_i16 are present even though those widths are rare in modern EXIF |
| v2 | v3 |
|---|---|
ExifTag::try_from(0x010f) |
ExifTag::from_code(0x010f) |
<&str as From<ExifTag>>::from(t) |
t.name() or t.to_string() |
No &str → ExifTag |
ExifTag::from_str("Make") (impl FromStr) |
| v2 | v3 |
|---|---|
exif.get_gps_info()? -> Option<GPSInfo> (Result-wrapped) |
exif.gps_info() -> Option<&GPSInfo> |
exif.get_by_ifd_tag_code(0, 0x0110) |
exif.get_by_code(IfdIndex::MAIN, 0x0110) |
exif.get_by_ifd_tag_code(ifd, ExifTag::Make.code()) |
exif.get_in(IfdIndex::new(ifd), ExifTag::Make) (IfdIndex field is private — use new() or the MAIN/THUMBNAIL constants) |
Cannot iterate over Exif |
exif.iter() (filter by IFD: exif.iter().filter(|e| e.ifd == IfdIndex::MAIN)) |
Cannot retrieve per-entry errors from Exif |
exif.errors() -> &[(IfdIndex, TagOrCode, EntryError)] |
ParsedExifEntry (the lazy iter's yield type) |
Renamed ExifIterEntry (paired with ExifIter) |
entry.tag() + entry.tag_code() |
entry.tag() -> TagOrCode |
entry.take_value() |
entry.into_result().ok() (or clone first) |
entry.take_result() (panic risk) |
entry.into_result() (consumes self) |
iter.clone_and_rewind() |
iter.clone_rewound() (or let mut x = iter.clone(); x.rewind();) |
iter.parse_gps_info() |
iter.parse_gps() |
| (none) | New: Exif::has_embedded_track() / ExifIter::has_embedded_track() — content-detected flag set when parse_exif sees a Motion Photo XMP signal (GCamera:MotionPhoto="1" plus Container:Directory / MotionPhotoOffset / MicroVideoOffset). For such files, parse_track on the same source extracts the embedded MP4. Renamed in 3.1.0 from the original has_embedded_media(); the old name is a #[deprecated] alias. The 3.0.0 implementation was a coarse MIME-level guess (RAF/HEIC always true even when no track was actually present); 3.1 replaces that with content detection. v3.1 covers Pixel/Google Motion Photos and Samsung Galaxy Motion Photos that use the Adobe XMP Container directory format (JPEG variants). |
// v2
let g = exif.get_gps_info()?.unwrap();
if g.latitude_ref == 'N' { /* ... */ }
let alt_above = g.altitude_ref == 0;
// v3
let g = exif.gps_info().unwrap();
if matches!(g.latitude_ref, LatRef::North) { /* ... */ }
let alt_above = matches!(g.altitude, Altitude::AboveSeaLevel(_));The char / u8 GPS fields are now strongly-typed enums: LatRef,
LonRef, Altitude, Speed, SpeedUnit.
// v2
let r = URational(1, 2);
let f = r.0 as f64 / r.1 as f64;
// v3
let r = URational::new(1, 2);
let f = r.to_f64().unwrap(); // handles denominator == 0
// IRational → URational (v2 silently truncated negatives; v3 fails explicitly)
let u: URational = ir.try_into()?; // ConvertError::NegativeRational// LatLng from decimal degrees
// v2
let p = LatLng::from(43.5_f64); // internal unwrap could panic
// v3
let p = LatLng::try_from_decimal_degrees(43.5)?; // ConvertError::InvalidDecimalDegreesURational / IRational tuple-struct field access (.0 / .1) is gone;
use .numerator() / .denominator().
// v2
let mut parser = AsyncMediaParser::new();
let ms = AsyncMediaSource::file_path("a.jpg").await?;
let iter: ExifIter = parser.parse(ms).await?;
// v3
let mut parser = MediaParser::new();
let ms = AsyncMediaSource::open("a.jpg").await?;
let iter = parser.parse_exif_async(ms).await?;
// Or, one-shot:
let exif = nom_exif::read_exif_async("a.jpg").await?;AsyncMediaParser is gone — there is one MediaParser with feature-gated
async methods (parse_exif_async / parse_track_async). The async surface
is enabled by feature = "tokio".
| v2 | v3 |
|---|---|
nom-exif = { version = "2", features = ["async"] } |
nom-exif = { version = "3", features = ["tokio"] } |
nom-exif = { version = "2", features = ["json_dump"] } |
nom-exif = { version = "3", features = ["serde"] } |
Feature names only — semantics and functionality are unchanged.
| v2 | v3 |
|---|---|
TrackInfoTag::ImageWidth / ImageHeight |
TrackInfoTag::Width / Height (the Image prefix is wrong in a video/audio container; aligns with Matroska's PixelWidth/PixelHeight and ISOBMFF's width/height. ExifTag::ImageWidth/ImageHeight are unchanged — Image is correct in EXIF context) |
info.get_gps_info() -> Option<GPSInfo> (Result-wrapped) |
info.gps_info() -> Option<&GPSInfo> (parallels Exif::gps_info) |
<&str as From<TrackInfoTag>>::from(t) |
t.name() or t.to_string() |
TryFrom<&str> for TrackInfoTag (with UnknownTrackInfoTag error) |
TrackInfoTag::from_str("Make") (impl FromStr, Err = ConvertError) |
From<BTreeMap<TrackInfoTag, EntryValue>> for TrackInfo |
Removed — internal construction detail, not part of the public API |
IntoIterator for TrackInfo (owned iteration) |
Removed — use info.iter() instead |
TrackInfo::has_embedded_media() |
Deprecated, no replacement. 3.0.0 reserved this for "track source carries another embedded track" detection that was never wired up (always returned false). v3.1 leaves it as a deprecated no-op until a real use case emerges; the symmetric has_embedded_track was added to Exif/ExifIter only. |
v3.3 unifies the in-memory-bytes parsing path with file/stream
parsing. The v3.0 *_from_bytes family is deprecated (still
compiles in v3.x; removed in v4). Migration is mechanical:
| Old (v3.0–v3.2, deprecated) | New (v3.3+) |
|---|---|
MediaSource::<()>::from_bytes(bytes) |
MediaSource::from_memory(bytes) |
parser.parse_exif_from_bytes(ms) |
parser.parse_exif(ms) (after from_memory) |
parser.parse_track_from_bytes(ms) |
parser.parse_track(ms) (after from_memory) |
read_exif_from_bytes(bytes) |
read_exif(...) after wrapping bytes via MediaSource::from_memory; or just keep the old call (deprecated, still works) |
read_exif_iter_from_bytes(bytes) |
read_exif_iter(...) analog |
read_track_from_bytes(bytes) |
read_track(...) analog |
read_metadata_from_bytes(bytes) |
read_metadata(...) analog |
Behavior and zero-copy semantics are preserved verbatim — from_memory
returns MediaSource<std::io::Empty> instead of MediaSource<()>,
satisfying the existing <R: Read> bound on parse_exif /
parse_track so the unified methods can dispatch on the
memory: Option<bytes::Bytes> field at runtime.
Example:
// v3.0 (deprecated since v3.3)
use nom_exif::{MediaParser, MediaSource};
let raw = std::fs::read("./testdata/exif.jpg")?;
#[allow(deprecated)]
let ms = MediaSource::<()>::from_bytes(raw)?;
let mut parser = MediaParser::new();
#[allow(deprecated)]
let iter = parser.parse_exif_from_bytes(ms)?;
# let _ = iter;
# Ok::<(), nom_exif::Error>(())// v3.3+ (preferred)
use nom_exif::{MediaParser, MediaSource};
let raw = std::fs::read("./testdata/exif.jpg")?;
let ms = MediaSource::from_memory(raw)?;
let mut parser = MediaParser::new();
let iter = parser.parse_exif(ms)?;
# let _ = iter;
# Ok::<(), nom_exif::Error>(())