From 3c52c84a61a90b7f5c92e7951caae4b83d89d456 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 2 Aug 2026 03:32:09 -0500 Subject: [PATCH] fix(makernotes): delete the fabricated Google MakerNote parser src/parsers/tiff/makernotes/google.rs registered a numeric TIFF-IFD MakerNote table for Make="Google" and emitted Astrophotography, ColorPop, FaceRetouching, HDRPlusMode, MergedFrameCount, NightSight, NightSightExposureTime, SceneDetection, SuperResZoom under the Google: group. None of these names appear in any ExifTool 13.59 source file (verified with grep -r "Name => ''" against the full lib/ tree). ExifTool's only Google MakerNote table is Google::HDRPlusMakerNote, which is not a numeric TIFF IFD at all: it's string-ID keyed (e.g. '1-1', '9-36-1') and decodes a base64+encrypted+gzipped protobuf blob. Its real tags (ImageName, ImageData, TimeLogText, SummaryText, FrameCount, CreateDate) are not implemented by the deleted parser either -- this was a structurally wrong, 100% invented table, not a wrong-id bug. Follows the Qualcomm precedent (cdc643f5, #345): delete the parser and its integration test, drop the `pub mod google` declaration and dispatcher arm, and leave an explanatory comment in their place. Confirmed no other code references the deleted symbols; cargo build, cargo test --workspace --lib, cargo fmt, and cargo clippy are all clean. google.rs exposed no cbindgen FFI symbols, so api/oxidex.h is unaffected. --- src/parsers/tiff/makernote_dispatcher.rs | 6 +- src/parsers/tiff/makernotes/google.rs | 534 ------------------ src/parsers/tiff/makernotes/mod.rs | 6 +- src/parsers/tiff/makernotes/registries/mod.rs | 10 +- tests/integration/google_makernotes_tests.rs | 329 ----------- 5 files changed, 16 insertions(+), 869 deletions(-) delete mode 100644 src/parsers/tiff/makernotes/google.rs delete mode 100644 tests/integration/google_makernotes_tests.rs diff --git a/src/parsers/tiff/makernote_dispatcher.rs b/src/parsers/tiff/makernote_dispatcher.rs index 6a5ef720c..c6bc30115 100644 --- a/src/parsers/tiff/makernote_dispatcher.rs +++ b/src/parsers/tiff/makernote_dispatcher.rs @@ -234,7 +234,11 @@ pub fn dispatch_makernote_with_context_and_values( // Smartphones "apple" => Some(Box::new(apple::AppleParser)), - "google" => Some(Box::new(google::GoogleParser)), + // "google" is absent on purpose: there is no fabricated `google` + // parser to dispatch to. ExifTool's real Google MakerNote table + // (Google::HDRPlusMakerNote) is string-id-keyed and reads an + // encrypted/gzipped protobuf blob, not a numeric TIFF IFD, so it + // can't be reached through this Make-keyed dispatch at all. "microsoft" | "microsoft corporation" => Some(Box::new(microsoft::MicrosoftParser)), // "qualcomm" is absent on purpose: there is no fabricated `qualcomm` // parser to dispatch to. ExifTool has no TIFF-IFD MakerNote table for diff --git a/src/parsers/tiff/makernotes/google.rs b/src/parsers/tiff/makernotes/google.rs deleted file mode 100644 index 79ddb2e4b..000000000 --- a/src/parsers/tiff/makernotes/google.rs +++ /dev/null @@ -1,534 +0,0 @@ -//! Google (Pixel) MakerNote parser -//! -//! Parses Google Pixel-specific EXIF MakerNote tags containing computational -//! photography settings, AI processing data, and Pixel-exclusive features. -//! -//! ## Supported Features -//! - HDR+ processing mode -//! - Night Sight activation and exposure time -//! - Super Res Zoom level -//! - Motion Photos status -//! - Face retouching level -//! - AI-based scene detection -//! - Computational photography settings -//! - Astrophotography mode -//! -//! ## Architecture -//! Google's MakerNotes use a custom binary format with tags specific to their -//! computational photography pipeline. These tags capture the extensive AI and -//! multi-frame processing that Pixel phones perform. - -#![allow(dead_code)] -#![allow(unused_imports)] - -use crate::const_decoder; -use crate::io::EndianReader; -use crate::parsers::tiff::ifd_parser::{ByteOrder, IfdEntry}; -use std::collections::HashMap; - -use super::shared::MakerNoteParser; -use super::shared::array_extractors::extract_i16_array; - -// Google Pixel MakerNote Tag IDs -// Note: Google's tag structure is proprietary and reverse-engineered -const GOOGLE_HDR_PLUS_MODE: u16 = 0x0001; // HDR+ processing mode -const GOOGLE_NIGHT_SIGHT: u16 = 0x0003; // Night Sight mode status -const GOOGLE_NIGHT_SIGHT_EXPOSURE: u16 = 0x0004; // Night Sight exposure time (ms) -const GOOGLE_SUPER_RES_ZOOM: u16 = 0x0005; // Super Res Zoom level -const GOOGLE_MOTION_PHOTO_ID: u16 = 0x0007; // Motion Photo video identifier -const GOOGLE_FACE_RETOUCHING: u16 = 0x0009; // Face retouching level (0-100) -const GOOGLE_SCENE_DETECTION: u16 = 0x000B; // AI scene detection result -const GOOGLE_PORTRAIT_BLUR: u16 = 0x000D; // Portrait mode blur amount -const GOOGLE_COLOR_POP: u16 = 0x000F; // Color Pop effect status -const GOOGLE_ASTROPHOTOGRAPHY: u16 = 0x0011; // Astrophotography mode -const GOOGLE_CINEMATIC_MODE: u16 = 0x0013; // Cinematic blur mode -const GOOGLE_MAGIC_ERASER: u16 = 0x0015; // Magic Eraser applied -const GOOGLE_UNBLUR: u16 = 0x0017; // Face Unblur applied -const GOOGLE_FRAME_COUNT: u16 = 0x0019; // Number of frames merged -const GOOGLE_EXPOSURE_STACK: u16 = 0x001B; // Multi-exposure stack info - -// Google signature for validation -const GOOGLE_SIGNATURE: &[u8] = b"Google"; - -// Decodes Google -const_decoder! { - DECODE_HDR_PLUS_MODE, i16, [ - (0, "Off"), - (1, "HDR+ On"), - (2, "HDR+ Enhanced"), - (3, "HDR+ Auto"), - (4, "HDR+ Bracketing"), - ] -} - -// Decodes Google -const_decoder! { - DECODE_NIGHT_SIGHT, i16, [ - (0, "Off"), - (1, "Auto"), - (2, "On"), - (3, "Astrophotography"), - ] -} - -// Decodes Google -const_decoder! { - DECODE_SCENE_TYPE, i16, [ - (0, "None"), - (1, "Sunset"), - (2, "Blue Sky"), - (3, "Snow"), - (4, "Greenery"), - (5, "Beach"), - (6, "Night"), - (7, "Food"), - (8, "Pet"), - (9, "Flower"), - (10, "Landmark"), - (11, "Document"), - (12, "Text"), - ] -} - -/// Decodes Super Res Zoom level -/// -/// # Arguments -/// * `value` - Zoom level multiplier (10 = 1.0x, 20 = 2.0x, etc.) -/// -/// # Returns -/// Human-readable zoom level -fn decode_super_res_zoom(value: i16) -> String { - if value <= 0 { - return "Off".to_string(); - } - let zoom_level = value as f32 / 10.0; - format!("{:.1}x", zoom_level) -} - -/// Extracts a 16-bit signed value from IFD entry -/// -/// # Arguments -/// * `entry` - IFD entry containing the value -/// * `data` - Full MakerNote data buffer -/// * `byte_order` - Byte order for parsing -/// -/// # Returns -/// Extracted value or None if invalid -fn extract_i16_value(entry: &IfdEntry, _data: &[u8], byte_order: ByteOrder) -> Option { - if entry.value_count != 1 { - return None; - } - - // For SHORT type (count=1), value is inline in value_offset field - let value = match byte_order { - ByteOrder::LittleEndian => (entry.value_offset & 0xFFFF) as i16, - ByteOrder::BigEndian => ((entry.value_offset >> 16) & 0xFFFF) as i16, - }; - - Some(value) -} - -/// Extracts a 32-bit unsigned value from IFD entry -/// -/// # Arguments -/// * `entry` - IFD entry containing the value -/// * `data` - Full MakerNote data buffer -/// * `byte_order` - Byte order for parsing -/// -/// # Returns -/// Extracted value or None if invalid -fn extract_u32_value(entry: &IfdEntry, _data: &[u8], _byte_order: ByteOrder) -> Option { - if entry.value_count != 1 { - return None; - } - - Some(entry.value_offset) -} - -/// Extracts an ASCII string from IFD entry -/// -/// # Arguments -/// * `entry` - IFD entry containing the string -/// * `data` - Full MakerNote data buffer -/// * `byte_order` - Byte order for parsing -/// -/// # Returns -/// Extracted string or None if invalid -fn extract_string(entry: &IfdEntry, data: &[u8], byte_order: ByteOrder) -> Option { - if entry.value_count == 0 { - return None; - } - - let value_bytes = if entry.value_count <= 4 { - // Inline string (stored in value_offset field) - let mut bytes = Vec::new(); - for i in 0..entry.value_count as usize { - let byte = match byte_order { - ByteOrder::LittleEndian => ((entry.value_offset >> (i * 8)) & 0xFF) as u8, - ByteOrder::BigEndian => ((entry.value_offset >> (24 - i * 8)) & 0xFF) as u8, - }; - if byte == 0 { - break; - } - bytes.push(byte); - } - bytes - } else { - // External string (offset points to data) - let offset = entry.value_offset as usize; - if offset >= data.len() { - return None; - } - let end = std::cmp::min(offset + entry.value_count as usize, data.len()); - data[offset..end].to_vec() - }; - - if value_bytes.is_empty() { - return None; - } - - let string = String::from_utf8_lossy(&value_bytes) - .trim_end_matches('\0') - .to_string(); - - if string.is_empty() { - None - } else { - Some(string) - } -} - -/// Google Pixel MakerNote parser implementation -pub struct GoogleParser; - -impl Default for GoogleParser { - fn default() -> Self { - Self::new() - } -} - -impl GoogleParser { - /// Creates a new Google parser instance - pub fn new() -> Self { - GoogleParser - } - - /// Parse a single IFD entry and extract tag value - /// - /// # Arguments - /// * `entry` - IFD entry to parse - /// * `data` - Full MakerNote data buffer - /// * `byte_order` - Byte order for multi-byte values - /// * `tags` - HashMap to insert extracted tags into - fn parse_entry( - &self, - entry: &IfdEntry, - data: &[u8], - byte_order: ByteOrder, - tags: &mut HashMap, - ) { - let tag_id = entry.tag_id; - - match tag_id { - GOOGLE_HDR_PLUS_MODE => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - tags.insert( - "Google:HDRPlusMode".to_string(), - DECODE_HDR_PLUS_MODE.decode(value), - ); - } - } - GOOGLE_NIGHT_SIGHT => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - tags.insert( - "Google:NightSight".to_string(), - DECODE_NIGHT_SIGHT.decode(value), - ); - } - } - GOOGLE_NIGHT_SIGHT_EXPOSURE => { - if let Some(value) = extract_u32_value(entry, data, byte_order) { - tags.insert( - "Google:NightSightExposureTime".to_string(), - format!("{} ms", value), - ); - } - } - GOOGLE_SUPER_RES_ZOOM => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - tags.insert( - "Google:SuperResZoom".to_string(), - decode_super_res_zoom(value), - ); - } - } - GOOGLE_MOTION_PHOTO_ID => { - if let Some(id) = extract_string(entry, data, byte_order) { - tags.insert("Google:MotionPhotoID".to_string(), id); - tags.insert("Google:MotionPhoto".to_string(), "Yes".to_string()); - } - } - GOOGLE_FACE_RETOUCHING => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - tags.insert("Google:FaceRetouching".to_string(), value.to_string()); - } - } - GOOGLE_SCENE_DETECTION => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - tags.insert( - "Google:SceneDetection".to_string(), - DECODE_SCENE_TYPE.decode(value), - ); - } - } - GOOGLE_PORTRAIT_BLUR => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - tags.insert("Google:PortraitBlur".to_string(), value.to_string()); - } - } - GOOGLE_COLOR_POP => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - let status = if value > 0 { "On" } else { "Off" }; - tags.insert("Google:ColorPop".to_string(), status.to_string()); - } - } - GOOGLE_ASTROPHOTOGRAPHY => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - let status = if value > 0 { "On" } else { "Off" }; - tags.insert("Google:Astrophotography".to_string(), status.to_string()); - } - } - GOOGLE_CINEMATIC_MODE => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - let status = if value > 0 { "On" } else { "Off" }; - tags.insert("Google:CinematicMode".to_string(), status.to_string()); - } - } - GOOGLE_MAGIC_ERASER => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - let status = if value > 0 { "Applied" } else { "Not Applied" }; - tags.insert("Google:MagicEraser".to_string(), status.to_string()); - } - } - GOOGLE_UNBLUR => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - let status = if value > 0 { "Applied" } else { "Not Applied" }; - tags.insert("Google:FaceUnblur".to_string(), status.to_string()); - } - } - GOOGLE_FRAME_COUNT => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - tags.insert("Google:MergedFrameCount".to_string(), value.to_string()); - } - } - GOOGLE_EXPOSURE_STACK => { - if let Some(value) = extract_i16_value(entry, data, byte_order) { - tags.insert("Google:ExposureStack".to_string(), value.to_string()); - } - } - _ => { - // Unknown tag - skip or log for debugging - } - } - } -} - -impl MakerNoteParser for GoogleParser { - fn manufacturer_name(&self) -> &'static str { - "Google" - } - - fn tag_prefix(&self) -> &'static str { - "Google:" - } - - fn parse( - &self, - data: &[u8], - byte_order: ByteOrder, - tags: &mut HashMap, - ) -> Result<(), String> { - if data.len() < 8 { - return Err("Google MakerNote data too short".to_string()); - } - - // Google MakerNotes may start with "Google" signature - let ifd_offset = if data.len() >= 6 && &data[0..6] == GOOGLE_SIGNATURE { - // Skip signature and padding (usually 8 bytes total) - 8 - } else { - // Assume IFD starts immediately - 0 - }; - - if ifd_offset + 2 > data.len() { - return Err("Invalid IFD offset".to_string()); - } - - // Read number of IFD entries using EndianReader - let reader = EndianReader::new(data, byte_order.to_io_byte_order()); - let entry_count = reader.u16_at(ifd_offset).unwrap_or(0); - - if entry_count == 0 || entry_count > 500 { - return Err(format!( - "Invalid entry count: {} (expected 1-500)", - entry_count - )); - } - - // Parse each IFD entry - let entry_size = 12; // Standard IFD entry size - let mut offset = ifd_offset + 2; - - for _ in 0..entry_count { - if offset + entry_size > data.len() { - break; - } - - // Parse IFD entry using EndianReader - let entry_data = &data[offset..offset + entry_size]; - let entry_reader = EndianReader::new(entry_data, byte_order.to_io_byte_order()); - - let tag = entry_reader.u16_at(0).unwrap_or(0); - let field_type = entry_reader.u16_at(2).unwrap_or(0); - let count = entry_reader.u32_at(4).unwrap_or(0); - let value_offset = entry_reader.u32_at(8).unwrap_or(0); - - let entry = IfdEntry { - tag_id: tag, - field_type, - value_count: count, - value_offset, - }; - - self.parse_entry(&entry, data, byte_order, tags); - - offset += entry_size; - } - - Ok(()) - } - - fn validate_header(&self, data: &[u8]) -> bool { - // Accept data with or without Google signature - if data.len() >= 6 && &data[0..6] == GOOGLE_SIGNATURE { - return true; - } - - // Also accept if it looks like valid IFD data - if data.len() >= 2 { - let reader = EndianReader::little_endian(data); - let entry_count = reader.u16_at(0).unwrap_or(0); - if entry_count > 0 && entry_count < 500 { - return true; - } - } - - false - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_decode_hdr_plus_mode() { - assert_eq!(DECODE_HDR_PLUS_MODE.decode(0), "Off"); - assert_eq!(DECODE_HDR_PLUS_MODE.decode(1), "HDR+ On"); - assert_eq!(DECODE_HDR_PLUS_MODE.decode(2), "HDR+ Enhanced"); - } - - #[test] - fn test_decode_night_sight() { - assert_eq!(DECODE_NIGHT_SIGHT.decode(0), "Off"); - assert_eq!(DECODE_NIGHT_SIGHT.decode(2), "On"); - assert_eq!(DECODE_NIGHT_SIGHT.decode(3), "Astrophotography"); - } - - #[test] - fn test_decode_scene_type() { - assert_eq!(DECODE_SCENE_TYPE.decode(0), "None"); - assert_eq!(DECODE_SCENE_TYPE.decode(7), "Food"); - assert_eq!(DECODE_SCENE_TYPE.decode(11), "Document"); - } - - #[test] - fn test_decode_super_res_zoom() { - assert_eq!(decode_super_res_zoom(0), "Off"); - assert_eq!(decode_super_res_zoom(10), "1.0x"); - assert_eq!(decode_super_res_zoom(20), "2.0x"); - assert_eq!(decode_super_res_zoom(75), "7.5x"); - } - - #[test] - fn test_google_parser_trait() { - let parser = GoogleParser::new(); - assert_eq!(parser.manufacturer_name(), "Google"); - assert_eq!(parser.tag_prefix(), "Google:"); - } - - #[test] - fn test_validate_header_with_signature() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - data.extend_from_slice(b"Google"); - data.extend_from_slice(&[0x00, 0x00]); // Padding - data.extend_from_slice(&[0x05, 0x00]); // 5 entries - - assert!(parser.validate_header(&data)); - } - - #[test] - fn test_validate_header_without_signature() { - let parser = GoogleParser::new(); - let data = vec![0x05, 0x00]; // Just entry count - - assert!(parser.validate_header(&data)); - } - - #[test] - fn test_parse_hdr_plus_tag() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - // Create minimal IFD with one entry - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - - // HDR+ tag entry (tag=0x0001, type=3 (SHORT), count=1, value=2 (Enhanced)) - data.extend_from_slice(&[0x01, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // Value: 2 (inline) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!( - tags.get("Google:HDRPlusMode"), - Some(&"HDR+ Enhanced".to_string()) - ); - } - - #[test] - fn test_parse_night_sight_tag() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - // Create minimal IFD with one entry - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - - // Night Sight tag entry (tag=0x0003, type=3 (SHORT), count=1, value=2 (On)) - data.extend_from_slice(&[0x03, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // Value: 2 (inline) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:NightSight"), Some(&"On".to_string())); - } -} diff --git a/src/parsers/tiff/makernotes/mod.rs b/src/parsers/tiff/makernotes/mod.rs index 991d314ae..5061963db 100644 --- a/src/parsers/tiff/makernotes/mod.rs +++ b/src/parsers/tiff/makernotes/mod.rs @@ -25,10 +25,14 @@ pub mod sony_lens_database; // Smartphone manufacturers (Phase 3) pub mod apple; -pub mod google; pub mod microsoft; pub mod samsung; +// (no `google` parser: fabricated tag table with no ExifTool source -- see +// `registries::mod` for the finding. ExifTool's real Google MakerNote table +// (Google::HDRPlusMakerNote) is string-id-keyed and reads an +// encrypted/gzipped protobuf blob, not a numeric TIFF IFD.) + // (no `qualcomm` parser: fabricated tag table with no ExifTool source -- // see `registries::mod` for the finding. ExifTool's real Qualcomm.pm tables // are read from JPEG APP7/APP4 segments, not a TIFF MakerNote IFD.) diff --git a/src/parsers/tiff/makernotes/registries/mod.rs b/src/parsers/tiff/makernotes/registries/mod.rs index 8cdf945f6..954bb73a7 100644 --- a/src/parsers/tiff/makernotes/registries/mod.rs +++ b/src/parsers/tiff/makernotes/registries/mod.rs @@ -6,10 +6,12 @@ // (no `google` registry: its 15 tag ids were invented -- none of the names it // declared is a tag ExifTool reports for any Google file, and Google's own // MakerNote is not the numeric IFD that registry assumed. It was never -// declared here, so it never compiled; see `makernotes::google` for the real -// parser. Likewise no `nikon` registry: `makernotes::nikon` and its submodules -// carry the real per-table id mapping, and the registry copy was never -// declared either.) +// declared here, so it never compiled. The `makernotes::google` parser built +// on the same invented ids has since been deleted too -- ExifTool's real +// Google MakerNote table (Google::HDRPlusMakerNote) is string-id-keyed and +// reads an encrypted/gzipped protobuf blob, not a numeric TIFF IFD. Likewise +// no `nikon` registry: `makernotes::nikon` and its submodules carry the real +// per-table id mapping, and the registry copy was never declared either.) pub mod apple; pub mod canon; pub mod captureone; // Capture One migration complete (Batch 4, Task 4.2) diff --git a/tests/integration/google_makernotes_tests.rs b/tests/integration/google_makernotes_tests.rs deleted file mode 100644 index d17a0595b..000000000 --- a/tests/integration/google_makernotes_tests.rs +++ /dev/null @@ -1,329 +0,0 @@ -//! Integration tests for Google (Pixel) MakerNotes parser -//! -//! Tests the Google Pixel MakerNotes parsing functionality including: -//! - MakerNoteParser trait implementation -//! - Header validation -//! - HDR+ mode detection -//! - Night Sight status -//! - Super Res Zoom -//! - Motion Photos -//! - Computational photography settings - -use oxidex::parsers::tiff::ifd_parser::ByteOrder; -use oxidex::parsers::tiff::makernotes::google::GoogleParser; -use oxidex::parsers::tiff::makernotes::shared::MakerNoteParser; -use std::collections::HashMap; - -#[test] -fn test_google_parser_trait() { - let parser = GoogleParser::new(); - assert_eq!(parser.manufacturer_name(), "Google"); - assert_eq!(parser.tag_prefix(), "Google:"); -} - -#[test] -fn test_google_validate_header_with_signature() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - data.extend_from_slice(b"Google"); - data.extend_from_slice(&[0x00, 0x00]); // Padding - data.extend_from_slice(&[0x05, 0x00]); // 5 entries - - assert!(parser.validate_header(&data)); -} - -#[test] -fn test_google_hdr_plus_off() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x01, 0x00]); // Tag: HDR+ Mode - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Value: 0 (Off) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:HDRPlusMode"), Some(&"Off".to_string())); -} - -#[test] -fn test_google_hdr_plus_enhanced() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x01, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // Value: 2 (Enhanced) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!( - tags.get("Google:HDRPlusMode"), - Some(&"HDR+ Enhanced".to_string()) - ); -} - -#[test] -fn test_google_night_sight_off() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x03, 0x00]); // Tag: Night Sight - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Value: 0 (Off) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:NightSight"), Some(&"Off".to_string())); -} - -#[test] -fn test_google_night_sight_on() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x03, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // Value: 2 (On) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:NightSight"), Some(&"On".to_string())); -} - -#[test] -fn test_google_night_sight_astrophotography() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x03, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x03, 0x00, 0x00, 0x00]); // Value: 3 (Astrophotography) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!( - tags.get("Google:NightSight"), - Some(&"Astrophotography".to_string()) - ); -} - -#[test] -fn test_google_super_res_zoom_off() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x05, 0x00]); // Tag: Super Res Zoom - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Value: 0 (Off) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:SuperResZoom"), Some(&"Off".to_string())); -} - -#[test] -fn test_google_super_res_zoom_2x() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x05, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x14, 0x00, 0x00, 0x00]); // Value: 20 (2.0x) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:SuperResZoom"), Some(&"2.0x".to_string())); -} - -#[test] -fn test_google_super_res_zoom_7_5x() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x05, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x4B, 0x00, 0x00, 0x00]); // Value: 75 (7.5x) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:SuperResZoom"), Some(&"7.5x".to_string())); -} - -#[test] -fn test_google_scene_detection_food() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x0B, 0x00]); // Tag: Scene Detection - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x07, 0x00, 0x00, 0x00]); // Value: 7 (Food) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:SceneDetection"), Some(&"Food".to_string())); -} - -#[test] -fn test_google_face_retouching() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x09, 0x00]); // Tag: Face Retouching - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x32, 0x00, 0x00, 0x00]); // Value: 50 - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:FaceRetouching"), Some(&"50".to_string())); -} - -#[test] -fn test_google_color_pop_on() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x0F, 0x00]); // Tag: Color Pop - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Value: 1 (On) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.get("Google:ColorPop"), Some(&"On".to_string())); -} - -#[test] -fn test_google_astrophotography_on() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x11, 0x00]); // Tag: Astrophotography - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Value: 1 (On) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!( - tags.get("Google:Astrophotography"), - Some(&"On".to_string()) - ); -} - -#[test] -fn test_google_frame_merge_count() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - data.extend_from_slice(&[0x01, 0x00]); // 1 entry - data.extend_from_slice(&[0x19, 0x00]); // Tag: Frame Count - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x0F, 0x00, 0x00, 0x00]); // Value: 15 frames - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!( - tags.get("Google:MergedFrameCount"), - Some(&"15".to_string()) - ); -} - -#[test] -fn test_google_multiple_tags() { - let parser = GoogleParser::new(); - let mut data = Vec::new(); - - // Create IFD with multiple entries - data.extend_from_slice(&[0x03, 0x00]); // 3 entries - - // HDR+ Mode - data.extend_from_slice(&[0x01, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // Value: 2 (Enhanced) - - // Night Sight - data.extend_from_slice(&[0x03, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // Value: 2 (On) - - // Super Res Zoom - data.extend_from_slice(&[0x05, 0x00]); // Tag - data.extend_from_slice(&[0x03, 0x00]); // Type: SHORT - data.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1 - data.extend_from_slice(&[0x20, 0x00, 0x00, 0x00]); // Value: 32 (3.2x) - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_ok()); - assert_eq!(tags.len(), 3); - assert_eq!( - tags.get("Google:HDRPlusMode"), - Some(&"HDR+ Enhanced".to_string()) - ); - assert_eq!(tags.get("Google:NightSight"), Some(&"On".to_string())); - assert_eq!(tags.get("Google:SuperResZoom"), Some(&"3.2x".to_string())); -} - -#[test] -fn test_google_invalid_data() { - let parser = GoogleParser::new(); - let data = vec![0x01]; // Too short - - let mut tags = HashMap::new(); - let result = parser.parse(&data, ByteOrder::LittleEndian, &mut tags); - - assert!(result.is_err()); -}