Skip to content

OxiDex Library API Reference ​

Version: 0.1.0 Last Updated: 2025-10-29

Table of Contents ​

  1. Introduction
  2. Core Concepts
  3. High-Level API
  4. Low-Level API
  5. Error Handling
  6. Code Examples
  7. Advanced Topics

Introduction ​

OxiDex is a Rust library for reading and writing metadata in various image and media file formats. The library provides both a high-level ergonomic API for common operations and a low-level API for fine-grained control over metadata manipulation.

This document covers the Rust library API only. For CLI usage, see the CLI Documentation. For C FFI bindings, see the FFI Documentation.

Key Features:

  • Zero-cost abstractions: Efficient parsing with minimal overhead
  • Type-safe: Strongly-typed metadata values with compile-time checks
  • Synchronous design: Simple, predictable execution model
  • Error handling: Comprehensive error types with context
  • Format support: JPEG, TIFF, XMP, PNG, and more

Core Concepts ​

Tag Naming Convention ​

All metadata tags in OxiDex follow a standardized naming convention:

<FormatFamily>:<TagName>

Examples:

  • EXIF:Make - Camera manufacturer (EXIF format)
  • EXIF:Model - Camera model
  • EXIF:DateTime - Image capture date/time
  • XMP-dc:Creator - Document creator (XMP Dublin Core namespace)
  • XMP-dc:Title - Document title
  • GPS:Latitude - GPS latitude coordinate
  • GPS:Longitude - GPS longitude coordinate
  • IPTC:Keywords - Image keywords
  • PNG:Description - PNG text chunk description

Supported Format Families:

Format FamilyDescriptionExample Tags
EXIFExchangeable Image File FormatEXIF:Make, EXIF:Model, EXIF:ISO
XMPExtensible Metadata PlatformXMP-dc:Creator, XMP-dc:Rights
IPTCInternational Press Telecommunications CouncilIPTC:Keywords, IPTC:Caption
GPSGPS location dataGPS:Latitude, GPS:Longitude
ICC_ProfileColor management profilesICC_Profile:ProfileDescription
PhotoshopAdobe Photoshop metadataPhotoshop:Credit, Photoshop:Source
MakerNotesCamera-specific maker notesMakerNotes:SerialNumber
PNGPortable Network GraphicsPNG:Title, PNG:Author
JFIFJPEG File Interchange FormatJFIF:XResolution, JFIF:YResolution
QuickTimeQuickTime/MOV video metadataQuickTime:Duration, QuickTime:CreateDate

Case Sensitivity: Tag names are case-sensitive. Always use the exact capitalization shown in the tag database.

Synchronous API Design ​

OxiDex uses a synchronous, blocking API design:

  • All operations complete before returning
  • No async/await or futures
  • File I/O is the bottleneck, not computation
  • Parallel processing is achieved via rayon at the application level

Rationale: File I/O dominates performance in metadata extraction. The overhead of async runtimes provides no benefit and adds complexity. For batch processing, use rayon's parallel iterators (see Example 5).

Type Safety ​

Metadata values are represented by the TagValue enum, which provides type safety at runtime:

rust
pub enum TagValue {
    String(String),
    Integer(i64),
    Float(f64),
    Rational { numerator: i32, denominator: i32 },
    Binary(Vec<u8>),
    DateTime(chrono::DateTime<Utc>),
    Struct(Box<HashMap<String, TagValue>>),
}

The API provides typed accessor methods that return Option<T>, preventing type confusion errors:

rust
let iso = metadata.get_integer("EXIF:ISO")?;  // Option<i64>
let make = metadata.get_string("EXIF:Make")?;  // Option<&str>

High-Level API ​

Metadata Struct ​

Note: The Metadata struct is the primary entry point for the high-level API. This API is planned for implementation and represents the future public interface.

rust,ignore
use oxidex::Metadata;

pub struct Metadata {
    // Internal fields (not public)
}

The Metadata struct wraps the lower-level MetadataMap and provides convenient methods for reading and writing metadata.

Reading Metadata ​

Metadata::from_path(path: impl AsRef<Path>) -> Result<Metadata> ​

Opens a file and extracts all metadata tags.

rust,ignore
use oxidex::{Metadata, Result};

fn main() -> Result<()> {
    let metadata = Metadata::from_path("photo.jpg")?;

    // Access metadata through typed getters
    if let Some(make) = metadata.get_string("EXIF:Make") {
        println!("Camera: {}", make);
    }

    Ok(())
}

Parameters:

  • path: File path (accepts anything implementing AsRef<Path>)

Returns:

  • Result<Metadata>: Metadata object on success, or ExifToolError on failure

Errors:

  • IoError: File not found, permission denied, etc.
  • UnsupportedFormat: File format not recognized
  • ParseError: File is corrupted or malformed

Metadata::from_bytes(data: &[u8], format_hint: Option<FileFormat>) -> Result<Metadata> ​

Parses metadata from a byte buffer.

rust,ignore
use oxidex::{Metadata, FileFormat};

let file_data = std::fs::read("image.jpg")?;
let metadata = Metadata::from_bytes(&file_data, Some(FileFormat::JPEG))?;

Parameters:

  • data: Raw file bytes
  • format_hint: Optional format hint to skip format detection

Use Cases:

  • Processing files from memory (e.g., HTTP uploads)
  • Working with embedded resources
  • Testing with synthetic data

Writing Metadata ​

Builder Pattern for Modifications ​

OxiDex uses a builder pattern for metadata write operations, enabling fluent, chainable API calls:

rust,ignore
use oxidex::Metadata;

Metadata::from_path("input.jpg")?
    .set_tag("EXIF:Artist", "John Doe")?
    .set_tag("EXIF:Copyright", "2025 John Doe")?
    .set_tag("EXIF:DateTime", "2025:10:29 14:30:00")?
    .write_to("output.jpg")?;

Key Methods:

set_tag(tag_name: &str, value: impl Into<TagValue>) -> Result<Self> ​

Sets a single tag value.

rust,ignore
metadata.set_tag("EXIF:Make", "Canon")?;
metadata.set_tag("EXIF:ISO", 400)?;  // Accepts integers
metadata.set_tag("EXIF:FNumber", 2.8)?;  // Accepts floats
remove_tag(tag_name: &str) -> Result<Self> ​

Removes a tag from the metadata.

rust,ignore
metadata.remove_tag("EXIF:Thumbnail")?;
write_to(path: impl AsRef<Path>) -> Result<()> ​

Writes the modified metadata to a new file.

rust,ignore
metadata.write_to("output.jpg")?;
write_in_place() -> Result<()> ​

Writes the modified metadata back to the original file.

rust,ignore
Metadata::from_path("photo.jpg")?
    .set_tag("EXIF:Artist", "Jane Smith")?
    .write_in_place()?;

Warning: In-place writes modify the original file. Always work on copies for critical files.

Builder Pattern Operations ​

Copy Metadata Between Files ​

rust,ignore
use oxidex::Metadata;

// Copy all tags from source to destination
Metadata::from_path("source.jpg")?
    .copy_tags_to("destination.jpg")?
    .execute()?;

// Copy specific tags only
Metadata::from_path("source.jpg")?
    .copy_tags_to("destination.jpg")?
    .with_tags(&["EXIF:DateTime", "EXIF:Make", "EXIF:Model"])?
    .execute()?;

// Copy all except specific tags
Metadata::from_path("source.jpg")?
    .copy_tags_to("destination.jpg")?
    .exclude_tags(&["EXIF:Thumbnail", "MakerNotes:*"])?
    .execute()?;

Advanced Options ​

rust,ignore
// Preserve file modification times
Metadata::from_path("input.jpg")?
    .set_tag("EXIF:Artist", "Photographer")?
    .preserve_file_times(true)?
    .write_to("output.jpg")?;

// Overwrite existing file
Metadata::from_path("source.jpg")?
    .copy_tags_to("existing.jpg")?
    .overwrite(true)?
    .execute()?;

Low-Level API ​

The low-level API provides direct access to the core data structures. Use this when you need fine-grained control or when building higher-level abstractions.

MetadataMap ​

MetadataMap is the in-memory representation of file metadata. It stores key-value pairs where keys are tag names and values are TagValue enums.

Location: src/core/metadata_map.rs:19

rust
use oxidex::core::metadata_map::MetadataMap;
use oxidex::core::tag_value::TagValue;

let mut metadata = MetadataMap::new();
metadata.insert("EXIF:Make", TagValue::new_string("Canon"));

Construction ​

new() -> Self ​

Creates an empty MetadataMap.

rust
let metadata = MetadataMap::new();
assert_eq!(metadata.len(), 0);
with_capacity(capacity: usize) -> Self ​

Creates a MetadataMap pre-allocated for the specified number of tags.

rust
let metadata = MetadataMap::with_capacity(50);

Use Case: Improves performance when you know the approximate tag count in advance.

Insertion and Modification ​

insert<K: Into<String>>(&mut self, key: K, value: TagValue) -> Option<TagValue> ​

Inserts or replaces a tag. Returns the previous value if the tag existed.

rust
let mut metadata = MetadataMap::new();
metadata.insert("EXIF:Make", TagValue::new_string("Nikon"));

// Replace existing value
let old_value = metadata.insert("EXIF:Make", TagValue::new_string("Canon"));
assert_eq!(old_value.unwrap().as_string(), Some("Nikon"));
get_mut(&mut self, key: &str) -> Option<&mut TagValue> ​

Returns a mutable reference to a tag value.

rust
if let Some(tag) = metadata.get_mut("EXIF:ISO") {
    *tag = TagValue::new_integer(800);
}
remove(&mut self, key: &str) -> Option<TagValue> ​

Removes a tag and returns its value.

rust
let removed = metadata.remove("EXIF:Thumbnail");
clear(&mut self) ​

Removes all tags.

rust
metadata.clear();
assert!(metadata.is_empty());

Retrieval ​

get(&self, key: &str) -> Option<&TagValue> ​

Returns a reference to a tag value.

rust
if let Some(tag_value) = metadata.get("EXIF:Make") {
    println!("Value: {:?}", tag_value);
}
get_string(&self, key: &str) -> Option<&str> ​

Typed accessor for string values. Returns None if the tag doesn't exist or isn't a String variant.

rust
match metadata.get_string("EXIF:Make") {
    Some(make) => println!("Camera make: {}", make),
    None => println!("Make tag not found or wrong type"),
}

See also: src/core/metadata_map.rs:128

get_integer(&self, key: &str) -> Option<i64> ​

Typed accessor for integer values.

rust
if let Some(iso) = metadata.get_integer("EXIF:ISO") {
    println!("ISO: {}", iso);
}

Alias: This method is also available as get_i64() (planned).

See also: src/core/metadata_map.rs:135

get_float(&self, key: &str) -> Option<f64> ​

Typed accessor for floating-point values.

rust
if let Some(aperture) = metadata.get_float("EXIF:FNumber") {
    println!("f/{:.1}", aperture);
}

Alias: This method is also available as get_f64() (planned).

See also: src/core/metadata_map.rs:142

get_datetime(&self, key: &str) -> Option<DateTime<Utc>> (Planned) ​

Typed accessor for datetime values. This method will extract DateTime variants from tags.

rust,ignore
use chrono::{DateTime, Utc};

if let Some(dt) = metadata.get_datetime("EXIF:DateTime") {
    println!("Photo taken: {}", dt.format("%Y-%m-%d %H:%M:%S"));
}

Status: Planned for implementation

Querying ​

contains_key(&self, key: &str) -> bool ​

Checks if a tag exists.

rust
if metadata.contains_key("EXIF:Make") {
    println!("Make tag is present");
}
len(&self) -> usize ​

Returns the number of tags.

rust
println!("Found {} tags", metadata.len());
is_empty(&self) -> bool ​

Returns true if no tags are present.

rust
if metadata.is_empty() {
    println!("No metadata found");
}

Iteration ​

iter(&self) -> impl Iterator<Item = (&String, &TagValue)> ​

Returns an iterator over all tag name-value pairs.

rust
for (name, value) in metadata.iter() {
    println!("{}: {:?}", name, value);
}

Alias: This method is also available as iter_tags() (planned).

keys(&self) -> impl Iterator<Item = &String> ​

Returns an iterator over tag names.

rust
for tag_name in metadata.keys() {
    println!("Tag: {}", tag_name);
}
values(&self) -> impl Iterator<Item = &TagValue> ​

Returns an iterator over tag values.

rust
for value in metadata.values() {
    if value.is_string() {
        println!("String value: {}", value.as_string().unwrap());
    }
}

Serialization ​

MetadataMap implements serde::Serialize and serde::Deserialize, enabling JSON serialization:

rust
use serde_json;

let json = serde_json::to_string_pretty(&metadata)?;
println!("{}", json);

// Deserialize from JSON
let metadata: MetadataMap = serde_json::from_str(&json)?;

JSON Format:

json
{
  "EXIF:Make": {
    "type": "String",
    "value": "Canon"
  },
  "EXIF:ISO": {
    "type": "Integer",
    "value": 400
  },
  "EXIF:FNumber": {
    "type": "Float",
    "value": 2.8
  }
}

TagValue ​

TagValue is an enum representing different metadata value types.

Location: src/core/tag_value.rs:17

rust
use oxidex::core::tag_value::TagValue;
use chrono::{DateTime, Utc};
use std::collections::HashMap;

pub enum TagValue {
    String(String),
    Integer(i64),
    Float(f64),
    Rational { numerator: i32, denominator: i32 },
    Binary(Vec<u8>),
    DateTime(DateTime<Utc>),
    Struct(Box<HashMap<String, TagValue>>),
}

Variants ​

VariantDescriptionCommon Use Cases
String(String)UTF-8 textCamera make/model, artist name, copyright
Integer(i64)64-bit signed integerISO speed, image width/height, orientation
Float(f64)64-bit floating pointGPS coordinates, aperture, shutter speed
Rational { numerator, denominator }Fraction (n/d)EXIF rational values, exposure time (e.g., 1/100)
Binary(Vec<u8>)Arbitrary byte dataThumbnails, ICC profiles, maker notes
DateTime(DateTime<Utc>)UTC timestampCreation date, modification date
Struct(Box<HashMap<String, TagValue>>)Nested structureComplex XMP structures

Constructors ​

rust
// String
let value = TagValue::new_string("Canon EOS 5D");
let value = TagValue::new_string(String::from("Nikon"));

// Integer
let value = TagValue::new_integer(400);

// Float
let value = TagValue::new_float(2.8);

// Rational
let value = TagValue::new_rational(1, 100);  // 1/100 second

// Binary
let value = TagValue::new_binary(vec![0xFF, 0xD8, 0xFF, 0xE0]);

// DateTime
use chrono::Utc;
let value = TagValue::new_datetime(Utc::now());

// Struct
let mut structure = HashMap::new();
structure.insert("author".to_string(), TagValue::new_string("John Doe"));
structure.insert("version".to_string(), TagValue::new_integer(1));
let value = TagValue::new_struct(structure);

See also: src/core/tag_value.rs:47-83

Type Checking ​

rust
let value = TagValue::new_string("Canon");

assert!(value.is_string());
assert!(!value.is_integer());
assert!(!value.is_float());
assert!(!value.is_rational());
assert!(!value.is_binary());
assert!(!value.is_datetime());
assert!(!value.is_struct());

See also: src/core/tag_value.rs:85-118

Type Extraction ​

rust
let value = TagValue::new_string("Canon");

// Safe extraction - returns Option
if let Some(s) = value.as_string() {
    println!("String value: {}", s);
}

// Pattern matching
match value {
    TagValue::String(s) => println!("String: {}", s),
    TagValue::Integer(i) => println!("Integer: {}", i),
    TagValue::Float(f) => println!("Float: {}", f),
    _ => println!("Other type"),
}

Available Extractors:

  • as_string(&self) -> Option<&str>
  • as_integer(&self) -> Option<i64>
  • as_float(&self) -> Option<f64>

See also: src/core/tag_value.rs:120-143

Note: Extractors for Rational, Binary, DateTime, and Struct variants are planned for future implementation.


Error Handling ​

ExifToolError ​

All fallible operations return Result<T, ExifToolError>. The ExifToolError enum provides detailed error information with context.

Location: src/error/mod.rs:14

rust
pub enum ExifToolError {
    IoError(io::Error),
    ParseError { message: String, offset: Option<usize> },
    TagNotFound { tag_name: String },
    InvalidTagValue { tag_name: String, reason: String },
    UnsupportedFormat { message: String },
}

Error Variants ​

IoError(io::Error) ​

Wraps standard I/O errors (file not found, permission denied, etc.).

rust
use oxidex::{Metadata, ExifToolError};

match Metadata::from_path("missing.jpg") {
    Err(ExifToolError::IoError(e)) => {
        eprintln!("File error: {}", e);
        // Check specific I/O error kind
        if e.kind() == std::io::ErrorKind::NotFound {
            eprintln!("File does not exist");
        }
    }
    Ok(metadata) => { /* ... */ }
    _ => {}
}
ParseError { message, offset } ​

Indicates a malformed or corrupted file. Optionally includes the byte offset where parsing failed.

rust,ignore
match Metadata::from_path("corrupted.jpg") {
    Err(ExifToolError::ParseError { message, offset }) => {
        eprintln!("Parse error: {}", message);
        if let Some(off) = offset {
            eprintln!("Failed at byte offset: {}", off);
        }
    }
    _ => {}
}

Common Causes:

  • Truncated files
  • Invalid JPEG/TIFF markers
  • Malformed XMP XML
  • Corrupt IFD structures
TagNotFound { tag_name } ​

The requested tag does not exist in the metadata.

rust,ignore
match metadata.get_string("EXIF:Artist") {
    Some(artist) => println!("Artist: {}", artist),
    None => {
        // Tag doesn't exist or wrong type
        // This returns None, not an error
    }
}

// If using a method that returns Result:
match metadata.require_tag("EXIF:Artist") {
    Err(ExifToolError::TagNotFound { tag_name }) => {
        eprintln!("Required tag '{}' not found", tag_name);
    }
    Ok(value) => { /* ... */ }
    _ => {}
}

Note: Most get_* methods return Option<T> rather than Result<T>, so missing tags don't produce errors. Only operations that explicitly require a tag will return TagNotFound.

InvalidTagValue { tag_name, reason } ​

Tag value doesn't match the expected type or is invalid.

rust,ignore
match metadata.set_tag("EXIF:ISO", "not_a_number") {
    Err(ExifToolError::InvalidTagValue { tag_name, reason }) => {
        eprintln!("Invalid value for {}: {}", tag_name, reason);
    }
    _ => {}
}

Common Causes:

  • Type mismatch (e.g., string provided for integer tag)
  • Value out of valid range
  • Invalid date/time format
UnsupportedFormat { message } ​

File format is not recognized or not yet supported.

rust,ignore
match Metadata::from_path("document.bmp") {
    Err(ExifToolError::UnsupportedFormat { message }) => {
        eprintln!("Format not supported: {}", message);
    }
    _ => {}
}

Error Constructors ​

The ExifToolError enum provides convenient constructor methods:

rust
use oxidex::error::ExifToolError;

// Create errors
let err1 = ExifToolError::parse_error("Invalid marker");
let err2 = ExifToolError::parse_error_at("Unexpected byte", 1024);
let err3 = ExifToolError::tag_not_found("EXIF:Make");
let err4 = ExifToolError::invalid_tag_value("EXIF:ISO", "must be positive");
let err5 = ExifToolError::unsupported_format("BMP not supported");

See also: src/error/mod.rs:49-86

Result Type ​

OxiDex defines a type alias for convenience:

rust
pub type Result<T> = std::result::Result<T, ExifToolError>;

Usage:

rust
use oxidex::error::Result;

fn extract_camera_info(path: &str) -> Result<String> {
    let metadata = Metadata::from_path(path)?;

    let make = metadata.get_string("EXIF:Make")
        .ok_or_else(|| ExifToolError::tag_not_found("EXIF:Make"))?;

    let model = metadata.get_string("EXIF:Model")
        .ok_or_else(|| ExifToolError::tag_not_found("EXIF:Model"))?;

    Ok(format!("{} {}", make, model))
}

See also: src/error/mod.rs:129

Error Handling Patterns ​

Pattern 1: Early Return with ? ​

The most idiomatic approach for functions returning Result:

rust,ignore
use oxidex::{Metadata, Result};

fn process_image(path: &str) -> Result<()> {
    let metadata = Metadata::from_path(path)?;

    let artist = metadata.get_string("EXIF:Artist")
        .unwrap_or("Unknown");

    println!("Artist: {}", artist);

    Ok(())
}

Pattern 2: Match for Detailed Handling ​

When you need to handle different error types differently:

rust,ignore
use oxidex::{Metadata, ExifToolError};

fn process_with_fallback(path: &str) {
    match Metadata::from_path(path) {
        Ok(metadata) => {
            println!("Loaded {} tags", metadata.len());
        }
        Err(ExifToolError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
            eprintln!("File not found, using defaults");
            // Use default metadata
        }
        Err(ExifToolError::UnsupportedFormat { .. }) => {
            eprintln!("Format not supported, skipping");
        }
        Err(e) => {
            eprintln!("Fatal error: {}", e);
            std::process::exit(1);
        }
    }
}

Pattern 3: Context with map_err ​

Add context to errors as they propagate:

rust,ignore
use oxidex::{Metadata, ExifToolError, Result};

fn batch_process(paths: &[&str]) -> Result<()> {
    for path in paths {
        Metadata::from_path(path)
            .map_err(|e| {
                eprintln!("Failed to process '{}': {}", path, e);
                e
            })?;
    }
    Ok(())
}

Pattern 4: Option Conversion ​

Convert Option to Result when needed:

rust,ignore
use oxidex::{Metadata, ExifToolError, Result};

fn get_required_tag(metadata: &Metadata, tag: &str) -> Result<String> {
    metadata.get_string(tag)
        .map(String::from)
        .ok_or_else(|| ExifToolError::tag_not_found(tag))
}

Pattern 5: Validation ​

Use InvalidTagValue for custom validation:

rust,ignore
fn set_iso(metadata: &mut Metadata, iso: i64) -> Result<()> {
    if iso < 0 || iso > 409600 {
        return Err(ExifToolError::invalid_tag_value(
            "EXIF:ISO",
            format!("ISO value {} is out of valid range (0-409600)", iso)
        ));
    }
    metadata.set_tag("EXIF:ISO", iso)
}

Code Examples ​

Example 1: Extract All Tags ​

Extract and display all metadata tags from an image file.

rust,ignore
use oxidex::{Metadata, Result};

fn main() -> Result<()> {
    // Open file and extract metadata
    let metadata = Metadata::from_path("photo.jpg")?;

    // Iterate through all tags
    println!("Found {} metadata tags:", metadata.len());
    for (tag_name, tag_value) in metadata.iter_tags() {
        println!("  {}: {:?}", tag_name, tag_value);
    }

    // Serialize to JSON
    let json = serde_json::to_string_pretty(&metadata)?;
    std::fs::write("metadata.json", json)?;

    Ok(())
}

Output:

Found 47 metadata tags:
  EXIF:Make: String("Canon")
  EXIF:Model: String("Canon EOS 5D Mark IV")
  EXIF:ISO: Integer(400)
  EXIF:FNumber: Float(2.8)
  EXIF:DateTime: DateTime(2025-10-29T14:30:00Z)
  GPS:Latitude: Float(37.7749)
  GPS:Longitude: Float(-122.4194)
  ...

Example 2: Get Specific Tag Values ​

Extract specific metadata fields with type safety.

rust,ignore
use oxidex::{Metadata, Result};

fn main() -> Result<()> {
    let metadata = Metadata::from_path("photo.jpg")?;

    // String values
    let camera_make = metadata.get_string("EXIF:Make")
        .unwrap_or("Unknown");
    let camera_model = metadata.get_string("EXIF:Model")
        .unwrap_or("Unknown");

    // Integer values
    let iso = metadata.get_integer("EXIF:ISO")
        .unwrap_or(0);

    // Float values
    let aperture = metadata.get_float("EXIF:FNumber")
        .unwrap_or(0.0);
    let shutter_speed = metadata.get_float("EXIF:ExposureTime")
        .unwrap_or(0.0);

    // Datetime values
    let date_taken = metadata.get_datetime("EXIF:DateTime");

    // Print camera settings
    println!("Camera: {} {}", camera_make, camera_model);
    println!("Settings: ISO {}, f/{:.1}, {:.4}s", iso, aperture, shutter_speed);

    if let Some(dt) = date_taken {
        println!("Taken: {}", dt.format("%Y-%m-%d %H:%M:%S"));
    }

    // GPS coordinates
    if let (Some(lat), Some(lon)) = (
        metadata.get_float("GPS:Latitude"),
        metadata.get_float("GPS:Longitude")
    ) {
        println!("Location: {:.4}, {:.4}", lat, lon);
    }

    Ok(())
}

Output:

Camera: Canon Canon EOS 5D Mark IV
Settings: ISO 400, f/2.8, 0.0125s
Taken: 2025-10-29 14:30:00
Location: 37.7749, -122.4194

Example 3: Modify Tag Values ​

Modify existing metadata and write to a new file.

rust,ignore
use oxidex::{Metadata, Result};

fn main() -> Result<()> {
    // Load metadata from source file
    let mut metadata = Metadata::from_path("original.jpg")?;

    // Modify multiple tags using builder pattern
    metadata
        .set_tag("EXIF:Artist", "Jane Smith")?
        .set_tag("EXIF:Copyright", "2025 Jane Smith. All rights reserved.")?
        .set_tag("EXIF:Rating", 5)?
        .set_tag("EXIF:DateTime", "2025:10:29 14:30:00")?
        .set_tag("IPTC:Keywords", "landscape, nature, mountains")?
        .remove_tag("EXIF:Thumbnail")?  // Remove thumbnail
        .write_to("modified.jpg")?;

    println!("Metadata updated successfully");

    Ok(())
}

In-Place Modification:

rust,ignore
use oxidex::{Metadata, Result};

fn add_copyright(path: &str, owner: &str) -> Result<()> {
    Metadata::from_path(path)?
        .set_tag("EXIF:Copyright", format!("© 2025 {}", owner))?
        .set_tag("EXIF:Artist", owner)?
        .write_in_place()?;

    Ok(())
}

Example 4: Copy Metadata Between Files ​

Copy metadata from one file to another.

rust,ignore
use oxidex::{Metadata, Result};

fn main() -> Result<()> {
    // Copy all metadata from source to destination
    Metadata::from_path("original.jpg")?
        .copy_tags_to("edited.jpg")?
        .execute()?;

    println!("All metadata copied");

    // Copy only specific tags
    Metadata::from_path("original.jpg")?
        .copy_tags_to("edited.jpg")?
        .with_tags(&[
            "EXIF:DateTime",
            "EXIF:Make",
            "EXIF:Model",
            "EXIF:ISO",
            "GPS:*",  // All GPS tags
        ])?
        .preserve_file_times(true)?
        .execute()?;

    println!("Selected metadata copied");

    // Copy all except thumbnails and maker notes
    Metadata::from_path("original.jpg")?
        .copy_tags_to("edited.jpg")?
        .exclude_tags(&[
            "EXIF:Thumbnail*",
            "MakerNotes:*",
        ])?
        .execute()?;

    Ok(())
}

Example 5: Batch Processing with Error Handling ​

Process multiple files in parallel with comprehensive error handling.

rust,ignore
use oxidex::{Metadata, ExifToolError, Result};
use rayon::prelude::*;
use std::path::PathBuf;

fn main() -> Result<()> {
    let image_files = vec![
        "photo1.jpg",
        "photo2.jpg",
        "photo3.jpg",
        "photo4.jpg",
    ];

    // Process files in parallel using rayon
    let results: Vec<_> = image_files
        .par_iter()
        .map(|path| process_single_file(path))
        .collect();

    // Analyze results
    let mut success_count = 0;
    let mut error_count = 0;

    for (path, result) in image_files.iter().zip(results.iter()) {
        match result {
            Ok(info) => {
                println!("✓ {}: {}", path, info);
                success_count += 1;
            }
            Err(e) => {
                eprintln!("✗ {}: {}", path, e);
                error_count += 1;
            }
        }
    }

    println!("\nProcessed {} files: {} succeeded, {} failed",
             image_files.len(), success_count, error_count);

    Ok(())
}

fn process_single_file(path: &str) -> Result<String> {
    let metadata = Metadata::from_path(path)?;

    // Extract camera info
    let make = metadata.get_string("EXIF:Make")
        .unwrap_or("Unknown");
    let model = metadata.get_string("EXIF:Model")
        .unwrap_or("Unknown");

    Ok(format!("{} {}", make, model))
}

With Detailed Error Handling:

rust,ignore
fn process_single_file_robust(path: &str) -> Result<String> {
    let metadata = match Metadata::from_path(path) {
        Ok(m) => m,
        Err(ExifToolError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
            return Err(ExifToolError::parse_error(format!("File not found: {}", path)));
        }
        Err(ExifToolError::UnsupportedFormat { message }) => {
            return Err(ExifToolError::unsupported_format(
                format!("{}: {}", path, message)
            ));
        }
        Err(ExifToolError::ParseError { message, offset }) => {
            let detail = if let Some(off) = offset {
                format!("at offset {}: {}", off, message)
            } else {
                message
            };
            return Err(ExifToolError::parse_error(format!("{}: {}", path, detail)));
        }
        Err(e) => return Err(e),
    };

    // Extract info...
    Ok(format!("Processed {}", path))
}

Example 6: Working with Different Value Types ​

Demonstrate handling all TagValue variants.

rust,ignore
use oxidex::core::metadata_map::MetadataMap;
use oxidex::core::tag_value::TagValue;
use chrono::{Utc, TimeZone};
use std::collections::HashMap;

fn main() {
    let mut metadata = MetadataMap::new();

    // String
    metadata.insert("EXIF:Make", TagValue::new_string("Canon"));

    // Integer
    metadata.insert("EXIF:ISO", TagValue::new_integer(400));
    metadata.insert("EXIF:PixelWidth", TagValue::new_integer(6000));

    // Float
    metadata.insert("EXIF:FNumber", TagValue::new_float(2.8));
    metadata.insert("GPS:Latitude", TagValue::new_float(37.7749));

    // Rational (exposure time: 1/125 second)
    metadata.insert("EXIF:ExposureTime", TagValue::new_rational(1, 125));

    // Binary (thumbnail data)
    let thumbnail = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
    metadata.insert("EXIF:ThumbnailImage", TagValue::new_binary(thumbnail));

    // DateTime
    let dt = Utc.with_ymd_and_hms(2025, 10, 29, 14, 30, 0).unwrap();
    metadata.insert("EXIF:DateTime", TagValue::new_datetime(dt));

    // Struct (XMP structure)
    let mut author_struct = HashMap::new();
    author_struct.insert("name".to_string(), TagValue::new_string("John Doe"));
    author_struct.insert("email".to_string(), TagValue::new_string("john@example.com"));
    metadata.insert("XMP-dc:Creator", TagValue::new_struct(author_struct));

    // Iterate and print types
    for (name, value) in metadata.iter() {
        let type_name = match value {
            TagValue::String(_) => "String",
            TagValue::Integer(_) => "Integer",
            TagValue::Float(_) => "Float",
            TagValue::Rational { .. } => "Rational",
            TagValue::Binary(_) => "Binary",
            TagValue::DateTime(_) => "DateTime",
            TagValue::Struct(_) => "Struct",
        };
        println!("{}: {} = {:?}", name, type_name, value);
    }
}

Example 7: JSON Serialization ​

Serialize and deserialize metadata to/from JSON.

rust,ignore
use oxidex::{Metadata, Result};
use serde_json;

fn main() -> Result<()> {
    // Load metadata from image
    let metadata = Metadata::from_path("photo.jpg")?;

    // Serialize to JSON (pretty-printed)
    let json = serde_json::to_string_pretty(&metadata)?;

    // Save to file
    std::fs::write("metadata.json", &json)?;
    println!("Metadata exported to metadata.json");

    // Load from JSON
    let json_data = std::fs::read_to_string("metadata.json")?;
    let loaded_metadata: MetadataMap = serde_json::from_str(&json_data)?;

    println!("Loaded {} tags from JSON", loaded_metadata.len());

    // Compact JSON (one line)
    let compact = serde_json::to_string(&metadata)?;
    println!("Compact JSON: {} bytes", compact.len());

    Ok(())
}

JSON Output Format:

json
{
  "EXIF:Make": {
    "type": "String",
    "value": "Canon"
  },
  "EXIF:Model": {
    "type": "String",
    "value": "Canon EOS 5D Mark IV"
  },
  "EXIF:ISO": {
    "type": "Integer",
    "value": 400
  },
  "EXIF:FNumber": {
    "type": "Float",
    "value": 2.8
  },
  "EXIF:ExposureTime": {
    "type": "Rational",
    "value": {
      "numerator": 1,
      "denominator": 125
    }
  },
  "EXIF:DateTime": {
    "type": "DateTime",
    "value": "2025-10-29T14:30:00Z"
  }
}

Advanced Topics ​

Memory-Mapped File Access ​

For large files, OxiDex uses memory-mapped I/O to avoid loading entire files into RAM:

rust,ignore
use memmap2::Mmap;
use std::fs::File;

fn process_large_file(path: &str) -> Result<()> {
    let file = File::open(path)?;
    let mmap = unsafe { Mmap::map(&file)? };

    // Parse directly from memory-mapped region
    let metadata = Metadata::from_bytes(&mmap, None)?;

    Ok(())
}

Benefits:

  • Efficient access to large files (multi-GB video files)
  • Only relevant portions are paged into memory
  • OS-level caching automatically applied

Parallel Processing ​

Use rayon for CPU-bound parallel processing:

rust,ignore
use rayon::prelude::*;
use oxidex::{Metadata, Result};

fn batch_extract(paths: &[String]) -> Vec<Result<String>> {
    paths.par_iter()
        .map(|path| {
            let metadata = Metadata::from_path(path)?;
            let make = metadata.get_string("EXIF:Make")
                .unwrap_or("Unknown");
            Ok(make.to_string())
        })
        .collect()
}

Performance Considerations:

  • I/O Bound: Parallel processing provides minimal benefit for HDD-backed storage
  • SSD/NVMe: Can see 2-4x speedup on fast storage
  • Network Storage: May saturate network bandwidth with parallelism
  • CPU Bound: Parsing XMP and complex formats benefit from parallelism

Thread Pool Configuration:

rust,ignore
use rayon::ThreadPoolBuilder;

fn main() {
    // Configure rayon thread pool
    ThreadPoolBuilder::new()
        .num_threads(4)
        .build_global()
        .unwrap();

    // Now parallel iterators will use 4 threads
}

Additional Resources ​


Document Version: 1.0 Last Updated: 2025-10-29 Minimum Rust Version: 1.75+

Released under the GPL-3.0 License.