blob: 823f87eaeb28cf6260bd2cdc9299ba9a961fc34e [file]
// Copyright 2026 Google LLC
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! EXIF metadata parser FFI bindings.
//!
//! Provides C++ bindings for parsing EXIF metadata. All parsing happens in
//! Rust for memory safety, then the result is passed to the C++ side for
//! conversion to SkExif::Metadata.
mod exif_parse;
#[cxx::bridge(namespace = "rust_exif")]
mod ffi {
/// Parsed EXIF metadata fields.
///
/// CXX does not support Rust's `Option<T>`, so each field is represented
/// as a `has_*` boolean paired with a value field. The value is only valid
/// when `has_*` is true.
struct ExifMetadata {
/// Image orientation (SkEncodedOrigin), values 1-8 per EXIF tag 0x0112.
has_origin: bool,
origin: u32,
/// Apple HDR headroom (computed from MakerNote tags 33 and 48).
/// See: https://developer.apple.com/documentation/appkit/images_and_pdf/applying_apple_hdr_effect_to_your_photos
has_hdr_headroom: bool,
hdr_headroom: f32,
/// Resolution unit (EXIF tag 0x0128): 2 = inch, 3 = centimeter.
has_resolution_unit: bool,
resolution_unit: u16,
/// X resolution (EXIF tag 0x011a).
has_x_resolution: bool,
x_resolution: f32,
/// Y resolution (EXIF tag 0x011b).
has_y_resolution: bool,
y_resolution: f32,
/// Image width in pixels (EXIF tag 0xa002, can be SHORT or LONG).
has_pixel_x_dimension: bool,
pixel_x_dimension: u32,
/// Image height in pixels (EXIF tag 0xa003, can be SHORT or LONG).
has_pixel_y_dimension: bool,
pixel_y_dimension: u32,
}
extern "Rust" {
/// Parses raw EXIF data from `data`. Returns `true` on success and
/// populates `out`; individual missing fields are shown by `has_*` flags.
fn parse_exif(data: &[u8], out: &mut ExifMetadata) -> bool;
}
}
/// Parses raw EXIF bytes into `out`. Returns `false` if the data cannot be
/// parsed at all; returns `true` and sets appropriate `has_*` flags otherwise.
pub fn parse_exif(data: &[u8], out: &mut ffi::ExifMetadata) -> bool {
*out = ffi::ExifMetadata {
has_origin: false,
origin: 0,
has_hdr_headroom: false,
hdr_headroom: 0.0,
has_resolution_unit: false,
resolution_unit: 0,
has_x_resolution: false,
x_resolution: 0.0,
has_y_resolution: false,
y_resolution: 0.0,
has_pixel_x_dimension: false,
pixel_x_dimension: 0,
has_pixel_y_dimension: false,
pixel_y_dimension: 0,
};
/// Maximum EXIF data size (4 MiB). Prevents unbounded heap allocation
/// for malformed or adversarially crafted inputs.
const MAX_EXIF_SIZE: usize = 4 * 1024 * 1024;
if data.is_empty() || data.len() > MAX_EXIF_SIZE {
return false;
}
exif_parse::parse(data, out)
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_metadata() -> ffi::ExifMetadata {
ffi::ExifMetadata {
has_origin: false,
origin: 0,
has_hdr_headroom: false,
hdr_headroom: 0.0,
has_resolution_unit: false,
resolution_unit: 0,
has_x_resolution: false,
x_resolution: 0.0,
has_y_resolution: false,
y_resolution: 0.0,
has_pixel_x_dimension: false,
pixel_x_dimension: 0,
has_pixel_y_dimension: false,
pixel_y_dimension: 0,
}
}
#[test]
fn test_empty_data_returns_false() {
let mut out = empty_metadata();
assert!(!parse_exif(&[], &mut out));
}
#[test]
fn test_invalid_data_returns_false() {
let mut out = empty_metadata();
assert!(!parse_exif(b"not valid exif data at all", &mut out));
assert!(!parse_exif(b"MM\x00\x2a", &mut out));
}
}