From 2bde4d9331388b469a67c254bc9056dba08b0353 Mon Sep 17 00:00:00 2001 From: swackhamer Date: Sun, 2 Aug 2026 12:30:57 -0500 Subject: [PATCH] feat(pentax): decode CameraSettings (0x0205) via a generated BITMASK-aware table Teaches codegen_subdirs.py ExifTool's hash-with-BITMASK PrintConv shape: an exact-value match against the direct keys first, then DecodeBits (ExifTool.pm:6385) bit by bit -- set bits print their label or [n], joined ', ', with '(none)' when no bits are set (ExifTool.pm:3614-3618 dispatch). A PrintConv carrying both OTHER and BITMASK still raises Unsupported. Pentax 0x0205 CameraSettings (count<25 layout, Pentax.pm:2784-2797, with its table-level BigEndian override) replaces the hand-written partial decoder in pentax.rs; the regenerated subdir_tables.rs is byte-identical to a fresh codegen_subdirs.py run against the pinned ExifTool 13.59 dump. New hand-written ports (PentaxEv, ISOFloor/Tv/Av/BaseExposureCompensation value convs, three PrintConv sprintf ports) each cite their Pentax.pm line and are registered in the generator's expression registries. Co-Authored-By: Claude Fable 5 --- src/parsers/tiff/makernotes/pentax.rs | 301 +------ .../tiff/makernotes/pentax/print_conv.rs | 52 ++ .../tiff/makernotes/pentax/subdir_tables.rs | 780 +++++++++++++++++- .../tiff/makernotes/pentax/value_conv.rs | 74 ++ .../tiff/makernotes/shared/binary_subdir.rs | 85 ++ tools/exiftool-tables/codegen_subdirs.py | 82 +- 6 files changed, 1070 insertions(+), 304 deletions(-) diff --git a/src/parsers/tiff/makernotes/pentax.rs b/src/parsers/tiff/makernotes/pentax.rs index cc3951cdd..083d7434c 100644 --- a/src/parsers/tiff/makernotes/pentax.rs +++ b/src/parsers/tiff/makernotes/pentax.rs @@ -49,10 +49,10 @@ use super::shared::binary_subdir::{self, BinaryTable, Cond, ModelPat}; use super::shared::generic_decoders::ON_OFF; use super::shared::tag_priority::insert_low_priority; use subdir_tables::{ - PENTAX_AFINFO, PENTAX_AWBINFO, PENTAX_BATTERYINFO, PENTAX_EVSTEPINFO, PENTAX_FACEINFO, - PENTAX_FACEPOS, PENTAX_FACESIZE, PENTAX_FILTERINFO, PENTAX_FLASHINFO, PENTAX_KELVINWB, - PENTAX_LENSCORR, PENTAX_LENSINFOQ, PENTAX_LEVELINFO, PENTAX_SHOTINFO, PENTAX_SRINFO2, - PENTAX_TEMPINFO, PENTAX_TIMEINFO, PENTAX_WBLEVELS, + PENTAX_AFINFO, PENTAX_AWBINFO, PENTAX_BATTERYINFO, PENTAX_CAMERASETTINGS, PENTAX_EVSTEPINFO, + PENTAX_FACEINFO, PENTAX_FACEPOS, PENTAX_FACESIZE, PENTAX_FILTERINFO, PENTAX_FLASHINFO, + PENTAX_KELVINWB, PENTAX_LENSCORR, PENTAX_LENSINFOQ, PENTAX_LEVELINFO, PENTAX_SHOTINFO, + PENTAX_SRINFO2, PENTAX_TEMPINFO, PENTAX_TIMEINFO, PENTAX_WBLEVELS, }; // Import declarative decoder macros @@ -1569,107 +1569,6 @@ impl PentaxParser { ); } - // 0x0205 "CameraSettings" binary subdirectory. Only the - // count<25 (non-K-01) layout is currently decoded. - PENTAX_CAMERA_SETTINGS => { - let raw = inline_or_offset_bytes(&entry, data, value_base, byte_order); - if raw.len() >= 11 && raw.len() < 25 { - tags.insert( - "Pentax:PictureMode2".to_string(), - decode_picture_mode2(raw[0]), - ); - tags.insert( - "Pentax:ProgramLine".to_string(), - decode_program_line(raw[1] & 0x03), - ); - tags.insert( - "Pentax:EVSteps".to_string(), - if raw[1] & 0x20 != 0 { - "1/3 EV Steps" - } else { - "1/2 EV Steps" - } - .to_string(), - ); - tags.insert( - "Pentax:E-DialInProgram".to_string(), - if raw[1] & 0x40 != 0 { - "P Shift" - } else { - "Tv or Av" - } - .to_string(), - ); - tags.insert( - "Pentax:ApertureRingUse".to_string(), - if raw[1] & 0x80 != 0 { - "Permitted" - } else { - "Prohibited" - } - .to_string(), - ); - tags.insert( - "Pentax:FlashOptions".to_string(), - decode_flash_options((raw[2] & 0xf0) >> 4), - ); - tags.insert( - "Pentax:MeteringMode2".to_string(), - decode_metering_mode2_bitmask(raw[2] & 0x0f), - ); - tags.insert( - "Pentax:AFPointMode".to_string(), - decode_af_point_mode_bitmask((raw[3] & 0xf0) >> 4), - ); - tags.insert( - "Pentax:FocusMode2".to_string(), - decode_focus_mode2(raw[3] & 0x0f), - ); - if raw.len() >= 6 { - let sel = match byte_order { - ByteOrder::BigEndian => u16::from_be_bytes([raw[4], raw[5]]), - ByteOrder::LittleEndian => u16::from_le_bytes([raw[4], raw[5]]), - }; - tags.insert( - "Pentax:AFPointSelected2".to_string(), - decode_af_point_selected2_bitmask(sel), - ); - } - if raw.len() >= 7 { - let ev = pentax_ev(raw[6] as i32 - 32); - let iso_floor = - (100.0 * (ev * std::f64::consts::LN_2).exp() + 0.5) as i64; - tags.insert("Pentax:ISOFloor".to_string(), iso_floor.to_string()); - } - if raw.len() >= 8 { - tags.insert( - "Pentax:DriveMode2".to_string(), - decode_drive_mode2_bitmask(raw[7]), - ); - } - if raw.len() >= 9 { - tags.insert( - "Pentax:ExposureBracketStepSize".to_string(), - decode_exposure_bracket_step_size(raw[8]), - ); - } - if raw.len() >= 10 { - tags.insert( - "Pentax:BracketShotNumber".to_string(), - decode_bracket_shot_number(raw[9]), - ); - } - tags.insert( - "Pentax:WhiteBalanceSet".to_string(), - decode_white_balance_set((raw[10] & 0xf0) >> 4), - ); - tags.insert( - "Pentax:MultipleExposureSet".to_string(), - if raw[10] & 0x0f != 0 { "On" } else { "Off" }.to_string(), - ); - } - } - // 0x0206 "AEInfo" binary subdirectory (auto-exposure info for // most Pentax DSLR models). Field offsets from 8 onward are // shifted by 1 byte for models with a 24/25-byte record @@ -1759,7 +1658,7 @@ impl PentaxParser { ); } if raw.len() > idx(14) { - let ev = pentax_ev(raw[idx(14)] as i8 as i32); + let ev = value_conv::pentax_ev(raw[idx(14)] as i8 as i32); let formatted = if ev == 0.0 { "0".to_string() } else { @@ -2168,6 +2067,16 @@ fn pentax_binary_subdir( PENTAX_WB_LEVELS if count == 100 => &PENTAX_WBLEVELS, PENTAX_LENS_INFO_Q => &PENTAX_LENSINFOQ, // Pentax.pm:3095 PENTAX_SHOT_INFO => &PENTAX_SHOTINFO, // Pentax.pm:3011 + // 0x0205 is `[{ Condition => '$count < 25' => CameraSettings }, { + // CameraSettingsUnknown }]` (Pentax.pm:2784-2797); `CameraSettingsUnknown` + // is not transcribed (ExifTool reports no named tags from it either, even + // on the K-01 it exists for), so a record at or above 25 bytes produces + // nothing here, matching ExifTool. `CameraSettings` also carries a + // `ByteOrder => 'BigEndian'` override (Pentax.pm:2791), independent of the + // MakerNote's own order -- handled below with `BATTERY_INFO`/`AF_INFO`. + PENTAX_CAMERA_SETTINGS if count < 25 => { + return Some((&PENTAX_CAMERASETTINGS, ByteOrder::BigEndian)); + } // 0x03ff is `TempInfo` on the listed bodies and `UnknownInfo` -- a table // with no tags in it -- on every other (Pentax.pm:3126-3134). PENTAX_TEMP_INFO if TEMP_INFO_MODELS.holds(model) => &PENTAX_TEMPINFO, @@ -2341,23 +2250,6 @@ fn format_pentax_float(v: f64) -> String { } } -/// ExifTool's `PentaxEv()`: converts a raw hex-based EV code (modulo 8) into -/// an EV value, correcting for the fact that 1/3-stop increments don't divide -/// evenly by 8. -fn pentax_ev(val: i32) -> f64 { - let mut v = val as f64; - if val & 1 != 0 { - let sign: f64 = if val < 0 { -1.0 } else { 1.0 }; - let frac = ((val as f64) * sign) as i64 & 0x07; - if frac == 3 { - v += sign * (8.0 / 3.0 - frac as f64); - } else if frac == 5 { - v += sign * (16.0 / 3.0 - frac as f64); - } - } - v / 8.0 -} - /// EV-based aperture formula shared by AEAperture/AEMaxAperture/AEMaxAperture2/ /// AEMinAperture: `2**((raw-68)/16)`. fn ae_aperture_from_raw(raw: i32) -> f64 { @@ -2514,56 +2406,14 @@ fn decode_drive_mode_byte3(b: u8) -> String { } // ---------------------------------------------------------------------------- -// CameraSettings (0x0205) sub-fields +// AEInfo (0x0206) sub-fields +// +// (CameraSettings, 0x0205, is now the generated `subdir_tables::PENTAX_CAMERASETTINGS` +// table; `decode_metering_mode2_bitmask` below is the one CameraSettings helper +// AEInfo's `AEMeteringMode2` also calls, so it stayed hand-written rather than +// moving into the generator's registries.) // ---------------------------------------------------------------------------- -fn decode_picture_mode2(b: u8) -> String { - match b { - 0 => "Scene Mode".to_string(), - 1 => "Auto PICT".to_string(), - 2 => "Program AE".to_string(), - 3 => "Green Mode".to_string(), - 4 => "Shutter Speed Priority".to_string(), - 5 => "Aperture Priority".to_string(), - 6 => "Program Tv Shift".to_string(), - 7 => "Program Av Shift".to_string(), - 8 => "Manual".to_string(), - 9 => "Bulb".to_string(), - 10 => "Aperture Priority, Off-Auto-Aperture".to_string(), - 11 => "Manual, Off-Auto-Aperture".to_string(), - 12 => "Bulb, Off-Auto-Aperture".to_string(), - 13 => "Shutter & Aperture Priority AE".to_string(), - 15 => "Sensitivity Priority AE".to_string(), - 16 => "Flash X-Sync Speed AE".to_string(), - other => other.to_string(), - } -} - -fn decode_program_line(b: u8) -> String { - match b { - 0 => "Normal".to_string(), - 1 => "Hi Speed".to_string(), - 2 => "Depth".to_string(), - 3 => "MTF".to_string(), - other => other.to_string(), - } -} - -fn decode_flash_options(b: u8) -> String { - match b { - 0 => "Normal".to_string(), - 1 => "Red-eye reduction".to_string(), - 2 => "Auto".to_string(), - 3 => "Auto, Red-eye reduction".to_string(), - 5 => "Wireless (Master)".to_string(), - 6 => "Wireless (Control)".to_string(), - 8 => "Slow-sync".to_string(), - 9 => "Slow-sync, Red-eye reduction".to_string(), - 10 => "Trailing-curtain Sync".to_string(), - other => other.to_string(), - } -} - fn decode_metering_mode2_bitmask(b: u8) -> String { format_bitmask( b as u32, @@ -2572,115 +2422,6 @@ fn decode_metering_mode2_bitmask(b: u8) -> String { ) } -fn decode_af_point_mode_bitmask(b: u8) -> String { - format_bitmask( - b as u32, - Some("Auto"), - &[(0, "Select"), (1, "Fixed Center")], - ) -} - -fn decode_focus_mode2(b: u8) -> String { - match b { - 0 => "Manual".to_string(), - 1 => "AF-S".to_string(), - 2 => "AF-C".to_string(), - 3 => "AF-A".to_string(), - other => other.to_string(), - } -} - -fn decode_af_point_selected2_bitmask(v: u16) -> String { - format_bitmask( - v as u32, - Some("Auto"), - &[ - (0, "Upper-left"), - (1, "Top"), - (2, "Upper-right"), - (3, "Left"), - (4, "Mid-left"), - (5, "Center"), - (6, "Mid-right"), - (7, "Right"), - (8, "Lower-left"), - (9, "Bottom"), - (10, "Lower-right"), - ], - ) -} - -fn decode_drive_mode2_bitmask(b: u8) -> String { - format_bitmask( - b as u32, - Some("Single-frame"), - &[ - (0, "Continuous"), - (1, "Continuous (Lo)"), - (2, "Self-timer (12 s)"), - (3, "Self-timer (2 s)"), - (4, "Remote Control (3 s delay)"), - (5, "Remote Control"), - (6, "Exposure Bracket"), - (7, "Multiple Exposure"), - ], - ) -} - -fn decode_exposure_bracket_step_size(b: u8) -> String { - match b { - 3 => "0.3".to_string(), - 4 => "0.5".to_string(), - 5 => "0.7".to_string(), - 8 => "1.0".to_string(), - 11 => "1.3".to_string(), - 12 => "1.5".to_string(), - 13 => "1.7".to_string(), - 16 => "2.0".to_string(), - other => other.to_string(), - } -} - -fn decode_bracket_shot_number(b: u8) -> String { - match b { - 0 => "n/a".to_string(), - 0x02 => "1 of 2".to_string(), - 0x12 => "2 of 2".to_string(), - 0x03 => "1 of 3".to_string(), - 0x13 => "2 of 3".to_string(), - 0x23 => "3 of 3".to_string(), - 0x05 => "1 of 5".to_string(), - 0x15 => "2 of 5".to_string(), - 0x25 => "3 of 5".to_string(), - 0x35 => "4 of 5".to_string(), - 0x45 => "5 of 5".to_string(), - other => format!("0x{:02x}", other), - } -} - -fn decode_white_balance_set(b: u8) -> String { - match b { - 0 => "Auto".to_string(), - 1 => "Daylight".to_string(), - 2 => "Shade".to_string(), - 3 => "Cloudy".to_string(), - 4 => "Daylight Fluorescent".to_string(), - 5 => "Day White Fluorescent".to_string(), - 6 => "White Fluorescent".to_string(), - 7 => "Tungsten".to_string(), - 8 => "Flash".to_string(), - 9 => "Manual".to_string(), - 12 => "Set Color Temperature 1".to_string(), - 13 => "Set Color Temperature 2".to_string(), - 14 => "Set Color Temperature 3".to_string(), - other => other.to_string(), - } -} - -// ---------------------------------------------------------------------------- -// AEInfo (0x0206) sub-fields -// ---------------------------------------------------------------------------- - fn decode_ae_program_mode(b: u8) -> String { match b { 0 => "M, P or TAv".to_string(), diff --git a/src/parsers/tiff/makernotes/pentax/print_conv.rs b/src/parsers/tiff/makernotes/pentax/print_conv.rs index 337332d44..9d2c8fd58 100644 --- a/src/parsers/tiff/makernotes/pentax/print_conv.rs +++ b/src/parsers/tiff/makernotes/pentax/print_conv.rs @@ -11,6 +11,37 @@ use crate::core::formatters::numeric_precision::perl_number; +/// `PrintConv => 'Image::ExifTool::Exif::PrintExposureTime($val)'` +/// (Pentax.pm:3734 `TvExposureTimeSetting`). +/// +/// A thin re-export: `Image::ExifTool::Exif::PrintExposureTime` is one of +/// ExifTool's shared cross-module subs, already ported once as +/// [`crate::core::formatters::exif_print_conv::print_exposure_time`]. Porting +/// it a second time here would be exactly the copy-drift that shared module's +/// own doc comment warns about. +pub(super) fn print_exposure_time(value: f64) -> String { + crate::core::formatters::exif_print_conv::print_exposure_time(value) +} + +/// `PrintConv => 'sprintf("%.1f",$val)'` (Pentax.pm:3743 `AvApertureSetting`). +pub(super) fn one_dp(value: f64) -> String { + format!("{value:.1}") +} + +/// `PrintConv => '$val ? sprintf("%+.1f", $val) : 0'` (Pentax.pm:3765 +/// `BaseExposureCompensation`). +/// +/// Perl's `?:` picks the literal `0` (not `$val`) when `$val` is falsy, and +/// `0.0` is the only falsy float, so an exact-zero exposure compensation +/// prints bare `"0"` while every other value gets an explicit sign. +pub(super) fn signed_1dp_or_zero(value: f64) -> String { + if value == 0.0 { + "0".to_string() + } else { + format!("{value:+.1}") + } +} + /// `PrintConv => 'sprintf("%.2f V", $val)'` (Pentax.pm:4868, :4923, :4932, /// :4948, :4958, :4986 -- every `BodyBattery*`/`GripBatteryVoltage`). pub(super) fn volts_2dp(value: f64) -> String { @@ -116,4 +147,25 @@ mod tests { assert_eq!(five_minus(0.0), "5"); assert_eq!(five_minus(4.0), "1"); } + + #[test] + fn print_exposure_time_reexports_the_shared_port() { + assert_eq!(print_exposure_time(1.0 / 250.0), "1/250"); + assert_eq!(print_exposure_time(2.0), "2"); + } + + #[test] + fn one_dp_keeps_a_single_decimal() { + assert_eq!(one_dp(2.5198), "2.5"); + assert_eq!(one_dp(4.0), "4.0"); + } + + /// `$val ? sprintf("%+.1f", $val) : 0`: exact zero prints bare `"0"`, not + /// `"0.0"` or `"+0.0"`. + #[test] + fn signed_1dp_or_zero_prints_bare_zero_and_signed_otherwise() { + assert_eq!(signed_1dp_or_zero(0.0), "0"); + assert_eq!(signed_1dp_or_zero(1.5), "+1.5"); + assert_eq!(signed_1dp_or_zero(-0.3), "-0.3"); + } } diff --git a/src/parsers/tiff/makernotes/pentax/subdir_tables.rs b/src/parsers/tiff/makernotes/pentax/subdir_tables.rs index 88f64f73c..f5482e088 100644 --- a/src/parsers/tiff/makernotes/pentax/subdir_tables.rs +++ b/src/parsers/tiff/makernotes/pentax/subdir_tables.rs @@ -230,20 +230,26 @@ const PENTAX_CONV24: &[(i64, &str)] = &[ (2, "Grip Battery"), (4, "External Power Supply"), ]; -const PENTAX_CONV25: &[(i64, &str)] = &[ +const PENTAX_CONV25: &[(i64, &str)] = &[]; +const PENTAX_CONV26: &[(i64, &str)] = &[ + (0, "Body Battery"), + (1, "Grip Battery"), + (3, "External Power Supply"), +]; +const PENTAX_CONV27: &[(i64, &str)] = &[ (1, "Empty or Missing"), (2, "Almost Empty"), (3, "Running Low"), (4, "Full"), ]; -const PENTAX_CONV26: &[(i64, &str)] = &[ +const PENTAX_CONV28: &[(i64, &str)] = &[ (1, "Empty or Missing"), (2, "Almost Empty"), (3, "Running Low"), (4, "Close to Full"), (5, "Full"), ]; -const PENTAX_CONV27: &[(i64, &str)] = &[ +const PENTAX_CONV29: &[(i64, &str)] = &[ (0, "Empty or Missing"), (1, "Almost Empty"), (2, "Running Low"), @@ -251,7 +257,7 @@ const PENTAX_CONV27: &[(i64, &str)] = &[ (4, "Close to Full"), (5, "Full"), ]; -const PENTAX_CONV28: &[(i64, &str)] = &[ +const PENTAX_CONV30: &[(i64, &str)] = &[ (16, "Horizontal (normal)"), (32, "Rotate 180"), (48, "Rotate 90 CW"), @@ -259,6 +265,130 @@ const PENTAX_CONV28: &[(i64, &str)] = &[ (80, "Upwards"), (96, "Downwards"), ]; +const PENTAX_CONV31: &[(i64, &str)] = &[ + (0, "Scene Mode"), + (1, "Auto PICT"), + (2, "Program AE"), + (3, "Green Mode"), + (4, "Shutter Speed Priority"), + (5, "Aperture Priority"), + (6, "Program Tv Shift"), + (7, "Program Av Shift"), + (8, "Manual"), + (9, "Bulb"), + (10, "Aperture Priority, Off-Auto-Aperture"), + (11, "Manual, Off-Auto-Aperture"), + (12, "Bulb, Off-Auto-Aperture"), + (13, "Shutter & Aperture Priority AE"), + (15, "Sensitivity Priority AE"), + (16, "Flash X-Sync Speed AE"), +]; +const PENTAX_CONV32: &[(i64, &str)] = &[(0, "Normal"), (1, "Hi Speed"), (2, "Depth"), (3, "MTF")]; +const PENTAX_CONV33: &[(i64, &str)] = &[(0, "Tv or Av"), (1, "P Shift")]; +const PENTAX_CONV34: &[(i64, &str)] = &[(0, "Prohibited"), (1, "Permitted")]; +const PENTAX_CONV35: &[(i64, &str)] = &[ + (0, "Normal"), + (1, "Red-eye reduction"), + (2, "Auto"), + (3, "Auto, Red-eye reduction"), + (5, "Wireless (Master)"), + (6, "Wireless (Control)"), + (8, "Slow-sync"), + (9, "Slow-sync, Red-eye reduction"), + (10, "Trailing-curtain Sync"), +]; +const PENTAX_CONV36: &[(i64, &str)] = &[(0, "Multi-segment")]; +const PENTAX_CONV37: &[(i64, &str)] = &[(0, "Center-weighted average"), (1, "Spot")]; +const PENTAX_CONV38: &[(i64, &str)] = &[(0, "Auto")]; +const PENTAX_CONV39: &[(i64, &str)] = &[(0, "Select"), (1, "Fixed Center")]; +const PENTAX_CONV40: &[(i64, &str)] = &[(0, "Manual"), (1, "AF-S"), (2, "AF-C"), (3, "AF-A")]; +const PENTAX_CONV41: &[(i64, &str)] = &[ + (0, "Upper-left"), + (1, "Top"), + (2, "Upper-right"), + (3, "Left"), + (4, "Mid-left"), + (5, "Center"), + (6, "Mid-right"), + (7, "Right"), + (8, "Lower-left"), + (9, "Bottom"), + (10, "Lower-right"), +]; +const PENTAX_CONV42: &[(i64, &str)] = &[(0, "Single-frame")]; +const PENTAX_CONV43: &[(i64, &str)] = &[ + (0, "Continuous"), + (1, "Continuous (Lo)"), + (2, "Self-timer (12 s)"), + (3, "Self-timer (2 s)"), + (4, "Remote Control (3 s delay)"), + (5, "Remote Control"), + (6, "Exposure Bracket"), + (7, "Multiple Exposure"), +]; +const PENTAX_CONV44: &[(i64, &str)] = &[ + (3, "0.3"), + (4, "0.5"), + (5, "0.7"), + (8, "1.0"), + (11, "1.3"), + (12, "1.5"), + (13, "1.7"), + (16, "2.0"), +]; +const PENTAX_CONV45: &[(i64, &str)] = &[ + (0, "n/a"), + (2, "1 of 2"), + (3, "1 of 3"), + (5, "1 of 5"), + (18, "2 of 2"), + (19, "2 of 3"), + (21, "2 of 5"), + (35, "3 of 3"), + (37, "3 of 5"), + (53, "4 of 5"), + (69, "5 of 5"), +]; +const PENTAX_CONV46: &[(i64, &str)] = &[ + (0, "Auto"), + (1, "Daylight"), + (2, "Shade"), + (3, "Cloudy"), + (4, "Daylight Fluorescent"), + (5, "Day White Fluorescent"), + (6, "White Fluorescent"), + (7, "Tungsten"), + (8, "Flash"), + (9, "Manual"), + (12, "Set Color Temperature 1"), + (13, "Set Color Temperature 2"), + (14, "Set Color Temperature 3"), +]; +const PENTAX_CONV47: &[(i64, &str)] = &[ + (1, "JPEG (Best)"), + (4, "RAW (PEF, Best)"), + (5, "RAW+JPEG (PEF, Best)"), + (8, "RAW (DNG, Best)"), + (9, "RAW+JPEG (DNG, Best)"), + (33, "JPEG (Better)"), + (36, "RAW (PEF, Better)"), + (37, "RAW+JPEG (PEF, Better)"), + (40, "RAW (DNG, Better)"), + (41, "RAW+JPEG (DNG, Better)"), + (65, "JPEG (Good)"), + (68, "RAW (PEF, Good)"), + (69, "RAW+JPEG (PEF, Good)"), + (72, "RAW (DNG, Good)"), + (73, "RAW+JPEG (DNG, Good)"), +]; +const PENTAX_CONV48: &[(i64, &str)] = &[(0, "10 MP"), (1, "6 MP"), (2, "2 MP")]; +const PENTAX_CONV49: &[(i64, &str)] = &[ + (0, "Horizontal (normal)"), + (1, "Rotate 180"), + (2, "Rotate 90 CW"), + (3, "Rotate 270 CW"), +]; +const PENTAX_CONV50: &[(i64, &str)] = &[(0, "Manual"), (1, "Auto")]; /// `Image::ExifTool::Pentax::SRInfo2` -- 1 fields, FORMAT `int8u`. /// @@ -2426,7 +2556,7 @@ pub(crate) static PENTAX_AFINFO: BinaryTable = BinaryTable { ], }; -/// `Image::ExifTool::Pentax::BatteryInfo` -- 21 fields, FORMAT `int8u`. +/// `Image::ExifTool::Pentax::BatteryInfo` -- 22 fields, FORMAT `int8u`. /// /// Transcribed from ExifTool's in-memory tag table by /// `tools/exiftool-tables/codegen_subdirs.py`. Do not edit by hand. @@ -2469,6 +2599,26 @@ pub(crate) static PENTAX_BATTERYINFO: BinaryTable = BinaryTable { print_conv: PrintConv::Map(PENTAX_CONV24), low_priority: false, }, + Field { + key: "0.2", + index: 0, + cond: Cond::Model { + any_of: &[ModelPat { + text: "K-3 Mark III", + word_end: false, + }], + none_of: &[], + }, + name: "PowerAvailable", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(240), + value_conv: ValueConv::None, + print_conv: PrintConv::Bitmask(PENTAX_CONV25, PENTAX_CONV26), + low_priority: false, + }, Field { key: "1.1", index: 1, @@ -2524,7 +2674,7 @@ pub(crate) static PENTAX_BATTERYINFO: BinaryTable = BinaryTable { gate: None, mask: Some(240), value_conv: ValueConv::None, - print_conv: PrintConv::Map(PENTAX_CONV25), + print_conv: PrintConv::Map(PENTAX_CONV27), low_priority: false, }, Field { @@ -2558,7 +2708,7 @@ pub(crate) static PENTAX_BATTERYINFO: BinaryTable = BinaryTable { gate: None, mask: Some(240), value_conv: ValueConv::None, - print_conv: PrintConv::Map(PENTAX_CONV26), + print_conv: PrintConv::Map(PENTAX_CONV28), low_priority: false, }, Field { @@ -2592,7 +2742,7 @@ pub(crate) static PENTAX_BATTERYINFO: BinaryTable = BinaryTable { gate: None, mask: Some(15), value_conv: ValueConv::None, - print_conv: PrintConv::Map(PENTAX_CONV25), + print_conv: PrintConv::Map(PENTAX_CONV27), low_priority: false, }, Field { @@ -2773,7 +2923,7 @@ pub(crate) static PENTAX_BATTERYINFO: BinaryTable = BinaryTable { gate: None, mask: None, value_conv: ValueConv::None, - print_conv: PrintConv::Map(PENTAX_CONV27), + print_conv: PrintConv::Map(PENTAX_CONV29), low_priority: false, }, Field { @@ -3126,7 +3276,7 @@ pub(crate) static PENTAX_BATTERYINFO: BinaryTable = BinaryTable { gate: None, mask: None, value_conv: ValueConv::None, - print_conv: PrintConv::Map(PENTAX_CONV27), + print_conv: PrintConv::Map(PENTAX_CONV29), low_priority: false, }, Field { @@ -3343,7 +3493,7 @@ pub(crate) static PENTAX_SHOTINFO: BinaryTable = BinaryTable { gate: None, mask: None, value_conv: ValueConv::None, - print_conv: PrintConv::Map(PENTAX_CONV28), + print_conv: PrintConv::Map(PENTAX_CONV30), low_priority: false, }], }; @@ -3387,3 +3537,611 @@ pub(crate) static PENTAX_FILTERINFO: BinaryTable = BinaryTable { }, ], }; + +/// `Image::ExifTool::Pentax::CameraSettings` -- 31 fields, FORMAT `int8u`. +/// +/// Transcribed from ExifTool's in-memory tag table by +/// `tools/exiftool-tables/codegen_subdirs.py`. Do not edit by hand. +pub(crate) static PENTAX_CAMERASETTINGS: BinaryTable = BinaryTable { + name: "CameraSettings", + default_format: Fmt::U8, + first_entry: 0, + fields: &[ + Field { + key: "0", + index: 0, + cond: Cond::Always, + name: "PictureMode2", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV31), + low_priority: true, + }, + Field { + key: "1.1", + index: 1, + cond: Cond::Always, + name: "ProgramLine", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(3), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV32), + low_priority: true, + }, + Field { + key: "1.2", + index: 1, + cond: Cond::Always, + name: "EVSteps", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(32), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV13), + low_priority: true, + }, + Field { + key: "1.3", + index: 1, + cond: Cond::Always, + name: "E-DialInProgram", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(64), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV33), + low_priority: true, + }, + Field { + key: "1.4", + index: 1, + cond: Cond::Always, + name: "ApertureRingUse", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(128), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV34), + low_priority: true, + }, + Field { + key: "2", + index: 2, + cond: Cond::Always, + name: "FlashOptions", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(240), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV35), + low_priority: true, + }, + Field { + key: "2.1", + index: 2, + cond: Cond::Always, + name: "MeteringMode2", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(15), + value_conv: ValueConv::None, + print_conv: PrintConv::Bitmask(PENTAX_CONV36, PENTAX_CONV37), + low_priority: true, + }, + Field { + key: "3", + index: 3, + cond: Cond::Always, + name: "AFPointMode", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(240), + value_conv: ValueConv::None, + print_conv: PrintConv::Bitmask(PENTAX_CONV38, PENTAX_CONV39), + low_priority: true, + }, + Field { + key: "3.1", + index: 3, + cond: Cond::Always, + name: "FocusMode2", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(15), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV40), + low_priority: true, + }, + Field { + key: "4", + index: 4, + cond: Cond::Always, + name: "AFPointSelected2", + format: Some(Fmt::U16), + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::None, + print_conv: PrintConv::Bitmask(PENTAX_CONV38, PENTAX_CONV41), + low_priority: true, + }, + Field { + key: "6", + index: 6, + cond: Cond::Always, + name: "ISOFloor", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::Each(super::value_conv::iso_from_pentax_ev), + print_conv: PrintConv::None, + low_priority: true, + }, + Field { + key: "7", + index: 7, + cond: Cond::Always, + name: "DriveMode2", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::None, + print_conv: PrintConv::Bitmask(PENTAX_CONV42, PENTAX_CONV43), + low_priority: true, + }, + Field { + key: "8", + index: 8, + cond: Cond::Always, + name: "ExposureBracketStepSize", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV44), + low_priority: true, + }, + Field { + key: "9", + index: 9, + cond: Cond::Always, + name: "BracketShotNumber", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV45), + low_priority: true, + }, + Field { + key: "10", + index: 10, + cond: Cond::Always, + name: "WhiteBalanceSet", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(240), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV46), + low_priority: true, + }, + Field { + key: "10.1", + index: 10, + cond: Cond::Always, + name: "MultipleExposureSet", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(15), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV2), + low_priority: true, + }, + Field { + key: "13", + index: 13, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "RawAndJpgRecording", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV47), + low_priority: true, + }, + Field { + key: "14.1", + index: 14, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "JpgRecordedPixels", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(3), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV48), + low_priority: true, + }, + Field { + key: "14.2", + index: 14, + cond: Cond::Model { + any_of: &[ModelPat { + text: "K-5", + word_end: true, + }], + none_of: &[], + }, + name: "LinkAEToAFPoint", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(1), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV2), + low_priority: true, + }, + Field { + key: "14.3", + index: 14, + cond: Cond::Model { + any_of: &[ModelPat { + text: "K-5", + word_end: true, + }], + none_of: &[], + }, + name: "SensitivitySteps", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(2), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV14), + low_priority: true, + }, + Field { + key: "14.4", + index: 14, + cond: Cond::Model { + any_of: &[ModelPat { + text: "K-5", + word_end: true, + }], + none_of: &[], + }, + name: "ISOAuto", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(4), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV2), + low_priority: true, + }, + Field { + key: "16", + index: 16, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "FlashOptions2", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(240), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV35), + low_priority: true, + }, + Field { + key: "16.1", + index: 16, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "MeteringMode3", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(15), + value_conv: ValueConv::None, + print_conv: PrintConv::Bitmask(PENTAX_CONV36, PENTAX_CONV37), + low_priority: true, + }, + Field { + key: "17.1", + index: 17, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "SRActive", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(128), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV5), + low_priority: true, + }, + Field { + key: "17.2", + index: 17, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "Rotation", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(96), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV49), + low_priority: true, + }, + Field { + key: "17.3", + index: 17, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "ISOSetting", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(4), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV50), + low_priority: true, + }, + Field { + key: "17.4", + index: 17, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "SensitivitySteps", + format: None, + count: 1, + set_member: None, + gate: None, + mask: Some(2), + value_conv: ValueConv::None, + print_conv: PrintConv::Map(PENTAX_CONV14), + low_priority: true, + }, + Field { + key: "18", + index: 18, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "TvExposureTimeSetting", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::Each(super::value_conv::tv_from_pentax_ev), + print_conv: PrintConv::Expr(super::print_conv::print_exposure_time), + low_priority: true, + }, + Field { + key: "19", + index: 19, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "AvApertureSetting", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::Each(super::value_conv::av_from_pentax_ev), + print_conv: PrintConv::Expr(super::print_conv::one_dp), + low_priority: true, + }, + Field { + key: "20", + index: 20, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "SvISOSetting", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::Each(super::value_conv::iso_from_pentax_ev), + print_conv: PrintConv::None, + low_priority: true, + }, + Field { + key: "21", + index: 21, + cond: Cond::Model { + any_of: &[ + ModelPat { + text: "K10D", + word_end: true, + }, + ModelPat { + text: "GX10", + word_end: true, + }, + ], + none_of: &[], + }, + name: "BaseExposureCompensation", + format: None, + count: 1, + set_member: None, + gate: None, + mask: None, + value_conv: ValueConv::Each(super::value_conv::base_exposure_comp_from_pentax_ev), + print_conv: PrintConv::Expr(super::print_conv::signed_1dp_or_zero), + low_priority: true, + }, + ], +}; diff --git a/src/parsers/tiff/makernotes/pentax/value_conv.rs b/src/parsers/tiff/makernotes/pentax/value_conv.rs index fb62d8bea..5a6ea58cb 100644 --- a/src/parsers/tiff/makernotes/pentax/value_conv.rs +++ b/src/parsers/tiff/makernotes/pentax/value_conv.rs @@ -7,6 +7,55 @@ //! upstream the generator stops rather than leaving one of these behind a real //! tag name. +/// ExifTool's `PentaxEv()` (Pentax.pm:6822): converts a raw hex-based EV code +/// (modulo 8) into an EV value, correcting for the fact that 1/3-stop +/// increments don't divide evenly by 8. +/// +/// Shared by the four `ValueConv`s below and by `%Pentax::AEInfo`'s +/// `FlashExposureCompSet`, which is still hand-written in `pentax.rs`. +pub(super) fn pentax_ev(val: i32) -> f64 { + let mut v = val as f64; + if val & 1 != 0 { + let sign: f64 = if val < 0 { -1.0 } else { 1.0 }; + let frac = ((val as f64) * sign) as i64 & 0x07; + if frac == 3 { + v += sign * (8.0 / 3.0 - frac as f64); + } else if frac == 5 { + v += sign * (16.0 / 3.0 - frac as f64); + } + } + v / 8.0 +} + +/// `ValueConv => 'int(100*exp(Image::ExifTool::Pentax::PentaxEv($val-32)*log(2))+0.5)'` +/// (Pentax.pm:3499 `ISOFloor`, :3756 `SvISOSetting`). +/// +/// Perl's `int()` truncates toward zero; the result is always positive here, +/// so `.trunc()` matches it and leaves the shared `render()` float path's +/// `fract() == 0.0` check true, printing a plain integer rather than a +/// long decimal. +pub(super) fn iso_from_pentax_ev(value: f64) -> f64 { + (100.0 * (pentax_ev(value as i32 - 32) * std::f64::consts::LN_2).exp() + 0.5).trunc() +} + +/// `ValueConv => 'exp(-Image::ExifTool::Pentax::PentaxEv($val-68)*log(2))'` +/// (Pentax.pm:3732 `TvExposureTimeSetting`), an exposure time in seconds. +pub(super) fn tv_from_pentax_ev(value: f64) -> f64 { + (-pentax_ev(value as i32 - 68) * std::f64::consts::LN_2).exp() +} + +/// `ValueConv => 'exp(Image::ExifTool::Pentax::PentaxEv($val-68)*log(2)/2)'` +/// (Pentax.pm:3741 `AvApertureSetting`), an f-number. +pub(super) fn av_from_pentax_ev(value: f64) -> f64 { + (pentax_ev(value as i32 - 68) * std::f64::consts::LN_2 / 2.0).exp() +} + +/// `ValueConv => 'Image::ExifTool::Pentax::PentaxEv(64-$val)'` (Pentax.pm:3763 +/// `BaseExposureCompensation`). +pub(super) fn base_exposure_comp_from_pentax_ev(value: f64) -> f64 { + pentax_ev(64 - value as i32) +} + /// `ValueConv => '-$val'` (Pentax.pm:5744 `CompositionAdjustX`, :5750 /// `CompositionAdjustY`). /// @@ -116,4 +165,29 @@ mod tests { fn kelvin_wb_refuses_a_short_record() { assert!(kelvin_wb(&[1.0, 2.0, 3.0]).is_empty()); } + + /// A raw byte of 32 is `PentaxEv(0)`, exactly 0 EV, so `exp(0)` collapses + /// the whole formula to `100 + 0.5` truncated -- the one input where the + /// odd/even correction inside `PentaxEv` cannot matter. + #[test] + fn iso_from_pentax_ev_matches_the_zero_ev_point() { + assert_eq!(iso_from_pentax_ev(32.0), 100.0); + // 40 - 32 = 8, an even code: no 1/3-stop correction, EV = 1 exactly. + assert_eq!(iso_from_pentax_ev(40.0), 200.0); + } + + /// A raw byte of 68 is `PentaxEv(0)` for both formulas, so each collapses + /// to `exp(0)` -- 1 second and f/1.0 respectively. + #[test] + fn tv_and_av_from_pentax_ev_match_the_zero_ev_point() { + assert_eq!(tv_from_pentax_ev(68.0), 1.0); + assert_eq!(av_from_pentax_ev(68.0), 1.0); + } + + #[test] + fn base_exposure_comp_from_pentax_ev_matches_the_zero_ev_point() { + assert_eq!(base_exposure_comp_from_pentax_ev(64.0), 0.0); + // 64 - 72 = -8, an even code: EV = -1 exactly. + assert_eq!(base_exposure_comp_from_pentax_ev(72.0), -1.0); + } } diff --git a/src/parsers/tiff/makernotes/shared/binary_subdir.rs b/src/parsers/tiff/makernotes/shared/binary_subdir.rs index aff211413..f5ff85b56 100644 --- a/src/parsers/tiff/makernotes/shared/binary_subdir.rs +++ b/src/parsers/tiff/makernotes/shared/binary_subdir.rs @@ -177,6 +177,42 @@ pub(crate) enum PrintConv { /// `%Pentax::BatteryInfo` divides a raw 686 by 100 and then prints /// `sprintf("%.2f V", $val)` (Pentax.pm:4866-4868). Expr(fn(f64) -> String), + /// A hash `PrintConv` carrying a `BITMASK` sub-hash, e.g. + /// `{ 0 => 'Auto', BITMASK => { 0 => 'Select', 1 => 'Fixed Center' } }` + /// (`%Pentax::CameraSettings`'s `AFPointMode`, Pentax.pm:3454-3465). + /// + /// ExifTool's dispatch (`ExifTool.pm:3614-3618`) tries an exact match + /// against `direct` first; only when that misses does it decode the value + /// bit by bit against `bits` (`DecodeBits`, `ExifTool.pm:6385`). This is + /// not a variant of `Map`/`MapOr`: those never fall through to a bitwise + /// reading, and a hash with `BITMASK` never falls through to + /// `Unknown (n)`. + Bitmask( + &'static [(i64, &'static str)], + &'static [(i64, &'static str)], + ), +} + +/// ExifTool's `DecodeBits` (`ExifTool.pm:6385`), for a `PrintConv` hash's +/// `BITMASK` sub-hash: each set bit of `val` becomes its label, joined by +/// `", "`; a set bit `BITMASK` does not name prints as `[n]`; no bits set +/// prints `(none)`. +fn decode_bits(val: i64, bits: &[(i64, &str)]) -> String { + let mut out = Vec::new(); + for bit in 0..32i64 { + if val & (1 << bit) == 0 { + continue; + } + match bits.iter().find(|(b, _)| *b == bit) { + Some((_, label)) => out.push((*label).to_string()), + None => out.push(format!("[{bit}]")), + } + } + if out.is_empty() { + "(none)".to_string() + } else { + out.join(", ") + } } /// The `ValueConv` ExifTool applies before the `PrintConv`. @@ -343,6 +379,10 @@ fn render(elem: &Elem, conv: PrintConv) -> String { .iter() .find(|(key, _)| key == v) .map_or_else(|| other(*v), |(_, label)| (*label).to_string()), + PrintConv::Bitmask(direct, bits) => direct + .iter() + .find(|(key, _)| key == v) + .map_or_else(|| decode_bits(*v, bits), |(_, label)| (*label).to_string()), }, } } @@ -936,4 +976,49 @@ mod tests { ); assert_eq!(tags.get("X:Volts").map(String::as_str), Some("6.86 V")); } + + /// `%Pentax::CameraSettings`'s `AFPointMode` (Pentax.pm:3454-3465): value 0 + /// is the direct-hit override `"Auto"`, not "(none)" from `DecodeBits`. + #[test] + fn bitmask_direct_hit_wins_over_decoding() { + const DIRECT: &[(i64, &str)] = &[(0, "Auto")]; + const BITS: &[(i64, &str)] = &[(0, "Select"), (1, "Fixed Center")]; + assert_eq!(decode_bits_via_render(0, DIRECT, BITS), "Auto"); + } + + /// Two bits set decode to both labels, joined by `", "`. + #[test] + fn bitmask_joins_multiple_set_bits() { + const DIRECT: &[(i64, &str)] = &[(0, "Single-frame")]; + const BITS: &[(i64, &str)] = &[(0, "Continuous"), (1, "Continuous (Lo)")]; + assert_eq!( + decode_bits_via_render(3, DIRECT, BITS), + "Continuous, Continuous (Lo)" + ); + } + + /// A set bit `BITMASK` doesn't name (Pentax.pm:3462: "have seen bit 2 set + /// in pre-production images") still gets reported, as `[n]`. + #[test] + fn bitmask_unlisted_bit_renders_bracketed() { + const DIRECT: &[(i64, &str)] = &[(0, "Auto")]; + const BITS: &[(i64, &str)] = &[(0, "Select"), (1, "Fixed Center")]; + assert_eq!(decode_bits_via_render(4, DIRECT, BITS), "[2]"); + } + + /// A value with no bits set at all -- not reachable from a real `%Pentax` + /// table today (0 is always the direct hit there), but `DecodeBits` itself + /// returns `(none)` here rather than an empty string. + #[test] + fn bitmask_zero_bits_is_none() { + assert_eq!(decode_bits(0, &[(1, "x")]), "(none)"); + } + + fn decode_bits_via_render( + val: i64, + direct: &'static [(i64, &str)], + bits: &'static [(i64, &str)], + ) -> String { + render(&Elem::Num(val), PrintConv::Bitmask(direct, bits)) + } } diff --git a/tools/exiftool-tables/codegen_subdirs.py b/tools/exiftool-tables/codegen_subdirs.py index 0c170f23e..135b8671d 100644 --- a/tools/exiftool-tables/codegen_subdirs.py +++ b/tools/exiftool-tables/codegen_subdirs.py @@ -18,8 +18,10 @@ * `Format`: a scalar int/float format, `string[N]`, `undef[N]`, or an array `fmt[N]` of a scalar format. - * `PrintConv`: absent, a pure enum map (every key/value a plain scalar), or a - Perl expression registered verbatim in `EXPR_PRINT_CONVS`. + * `PrintConv`: absent, a pure enum map (every key/value a plain scalar), a + Perl expression registered verbatim in `EXPR_PRINT_CONVS`, or a hash + carrying a `BITMASK` sub-hash (an exact-value override checked first, a + bit-by-bit decode of the rest). * `ValueConv`: absent, or an expression/sub body registered verbatim. * `RawConv`: absent, or one of ExifTool's two count-gate idioms -- `$$self{X} = $val` (records a data member) and @@ -30,12 +32,12 @@ alternation. An arrayref of such `Condition`-guarded alternatives becomes several `Field`s sharing one ExifTool key, in ExifTool's order. -Anything else -- a `Hook`, a nested `SubDirectory`, a `PrintConv` with -`BITMASK`/`OTHER`/code, a `Condition` on anything but the model, a regex with a -construct the expander has not been taught -- raises `Unsupported`, naming the -table, the tag and the offending construct. Run with `--allow-skip` to -downgrade those to a machine-logged skip line and continue; the log is the -deliverable, not a footnote. +Anything else -- a `Hook`, a nested `SubDirectory`, a `PrintConv` with `OTHER` +and `BITMASK` both present, or code, a `Condition` on anything but the model, a +regex with a construct the expander has not been taught -- raises +`Unsupported`, naming the table, the tag and the offending construct. Run +with `--allow-skip` to downgrade those to a machine-logged skip line and +continue; the log is the deliverable, not a footnote. usage: dump_tables.pl Panasonic > tables.json @@ -370,6 +372,14 @@ def normalize_deparse(text): "$val * 2": "times_2", # Pentax.pm:6118 -- ShotNumber, counted from zero. "$val+1": "plus_1", + # Pentax.pm:3499, :3756 -- ISOFloor, SvISOSetting. + "int(100*exp(Image::ExifTool::Pentax::PentaxEv($val-32)*log(2))+0.5)": "iso_from_pentax_ev", + # Pentax.pm:3732 -- TvExposureTimeSetting, an exposure time in seconds. + "exp(-Image::ExifTool::Pentax::PentaxEv($val-68)*log(2))": "tv_from_pentax_ev", + # Pentax.pm:3741 -- AvApertureSetting, an f-number. + "exp(Image::ExifTool::Pentax::PentaxEv($val-68)*log(2)/2)": "av_from_pentax_ev", + # Pentax.pm:3763 -- BaseExposureCompensation. + "Image::ExifTool::Pentax::PentaxEv(64-$val)": "base_exposure_comp_from_pentax_ev", } LIST_VALUE_CONVS = { # Pentax.pm:837-840 -- `%kelvinWB`, shared by all 17 KelvinWB_* tags. @@ -398,6 +408,12 @@ def normalize_deparse(text): 'sprintf("%.1f C", $val)': "celsius_1dp", # Pentax.pm:5188 -- AFCSensitivity, counted the other way. "5 - $val": "five_minus", + # Pentax.pm:3734 -- TvExposureTimeSetting, after its ValueConv. + "Image::ExifTool::Exif::PrintExposureTime($val)": "print_exposure_time", + # Pentax.pm:3743 -- AvApertureSetting, after its ValueConv. + 'sprintf("%.1f",$val)': "one_dp", + # Pentax.pm:3765 -- BaseExposureCompensation, after its ValueConv. + '$val ? sprintf("%+.1f", $val) : 0': "signed_1dp_or_zero", } @@ -440,6 +456,39 @@ def field_value_conv(tag, table, key, vc_prefix): } +def field_print_conv_bitmask(table, key, pool, pc, bitmask): + """`PrintConv => { N => '...', BITMASK => {...} }` as `PrintConv::Bitmask`. + + ExifTool's dispatch for a hash carrying `BITMASK` (`ExifTool.pm:3614-3618`) + tries an exact match against the plain (non-`BITMASK`) keys first, and + only decodes the value bit by bit (`DecodeBits`, `ExifTool.pm:6385`) when + that misses -- so this is two separate lookup tables, not one. + """ + if not isinstance(bitmask, dict): + raise Unsupported(table, key, f"BITMASK is not a plain hash: {bitmask!r}") + + def entries(items, label): + out = [] + for k, v in items: + try: + ik = int(str(k), 0) + except ValueError: + raise Unsupported(table, key, f"{label} key {k!r} is not an integer") from None + if not isinstance(v, str): + raise Unsupported(table, key, f"{label} value for {k!r} is not a string") + out.append((ik, v)) + out.sort() + return out + + direct = entries(pc.get("map", {}).items(), "PrintConv") + bits = entries(bitmask.items(), "BITMASK") + if not bits: + raise Unsupported(table, key, "BITMASK is an empty map") + direct_name = pool.intern(", ".join(f'({k}, "{rust_str(v)}")' for k, v in direct)) + bits_name = pool.intern(", ".join(f'({k}, "{rust_str(v)}")' for k, v in bits)) + return f"PrintConv::Bitmask({direct_name}, {bits_name})" + + def field_print_conv(tag, table, key, pool, conv_prefix): """A `PrintConv` as a Rust expression, or `PrintConv::None`.""" pc = tag.get("PrintConv") @@ -457,11 +506,15 @@ def field_print_conv(tag, table, key, pool, conv_prefix): return f"PrintConv::Expr({conv_prefix}::{fn})" if kind == "enum_partial": directives = pc.get("directives") or {} - unknown = set(directives) - BENIGN_PC_DIRECTIVES - {"OTHER"} + unknown = set(directives) - BENIGN_PC_DIRECTIVES - {"OTHER", "BITMASK"} if unknown: raise Unsupported( table, key, f"PrintConv carries directive(s) {sorted(unknown)!r}" ) + if "BITMASK" in directives: + if "OTHER" in directives: + raise Unsupported(table, key, "PrintConv carries both BITMASK and OTHER") + return field_print_conv_bitmask(table, key, pool, pc, directives["BITMASK"]) if "OTHER" in directives: spec = directives["OTHER"] source = spec.get("__deparse") if isinstance(spec, dict) else None @@ -604,11 +657,14 @@ def gen_table(module, tname, tbl, pool, skips, allow_skip, conv_prefix, vc_prefi low_priority = field_priority(tag, table_priority, tname, key) pc = field_print_conv(tag, tname, key, pool, conv_prefix) vc = field_value_conv(tag, tname, key, vc_prefix) - if vc != "ValueConv::None" and pc.startswith("PrintConv::Map"): + if vc != "ValueConv::None" and ( + pc.startswith("PrintConv::Map") or pc.startswith("PrintConv::Bitmask") + ): # ExifTool runs ValueConv then PrintConv, so a hash PrintConv - # after one would be a lookup of a computed number in a table - # of raw ones. An expression PrintConv is exactly that - # composition and is allowed. + # (or a BITMASK decode, which is also a lookup) after one + # would be looking a computed number up in a table of raw + # ones. An expression PrintConv is exactly that composition + # and is allowed. raise Unsupported(tname, key, "ValueConv combined with a hash PrintConv") if count > 1 and pc != "PrintConv::None": # ExifTool hands the *joined* array string to a hash