From 3127887b8204ca1bb4b18eb1a9965f196524b046 Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Sun, 19 Oct 2025 21:24:54 +0100 Subject: [PATCH] chore: reorganize cli traits Signed-off-by: Nikolaos Karaolidis --- src/camera/ptp/hex.rs | 742 +------------------------------------- src/cli/common/film.rs | 741 ++++++++++++++++++++++++++++++++++++- src/cli/common/mod.rs | 1 + src/cli/common/suggest.rs | 29 ++ src/cli/simulation/mod.rs | 2 +- 5 files changed, 773 insertions(+), 742 deletions(-) create mode 100644 src/cli/common/suggest.rs diff --git a/src/camera/ptp/hex.rs b/src/camera/ptp/hex.rs index 290cd45..fc1db47 100644 --- a/src/camera/ptp/hex.rs +++ b/src/camera/ptp/hex.rs @@ -1,16 +1,13 @@ use std::{ - fmt, io::{self, Cursor}, ops::{Deref, DerefMut}, - str::FromStr, }; -use anyhow::{Context, bail}; +use anyhow::bail; use num_enum::{IntoPrimitive, TryFromPrimitive}; use ptp_cursor::{PtpDeserialize, PtpSerialize, Read}; use ptp_macro::{PtpDeserialize, PtpSerialize}; -use serde::{Serialize, Serializer}; -use strum::IntoEnumIterator; +use serde::Serialize; use strum_macros::EnumIter; #[repr(u16)] @@ -166,34 +163,6 @@ pub enum DevicePropCode { FujiBatteryInfo2 = 0xD36B, } -const SIMILARITY_THRESHOLD: usize = 8; - -fn suggest_closest<'a, I, S>(input: &str, choices: I) -> Option<&'a str> -where - I: IntoIterator, - S: AsRef + 'a, -{ - let mut best_score = usize::MAX; - let mut best_match: Option<&'a str> = None; - - for choice in choices { - let choice_str = choice.as_ref(); - let dist = strsim::damerau_levenshtein(&input.to_lowercase(), &choice_str.to_lowercase()); - - if dist < best_score { - best_score = dist; - best_match = Some(choice_str); - } - } - - println!("{best_score}"); - if best_score <= SIMILARITY_THRESHOLD { - best_match - } else { - None - } -} - #[repr(u16)] #[derive( Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize, @@ -233,55 +202,15 @@ pub enum FujiCustomSetting { C7 = 0x7, } -impl fmt::Display for FujiCustomSetting { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::C1 => write!(f, "C1"), - Self::C2 => write!(f, "C2"), - Self::C3 => write!(f, "C3"), - Self::C4 => write!(f, "C4"), - Self::C5 => write!(f, "C5"), - Self::C6 => write!(f, "C6"), - Self::C7 => write!(f, "C7"), - } - } -} - -impl Serialize for FujiCustomSetting { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_u16((*self).into()) - } -} - -impl FromStr for FujiCustomSetting { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase(); - - let variant = match input.as_str() { - "c1" | "1" => Self::C1, - "c2" | "2" => Self::C2, - "c3" | "3" => Self::C3, - "c4" | "4" => Self::C4, - "c5" | "5" => Self::C5, - "c6" | "6" => Self::C6, - "c7" | "7" => Self::C7, - _ => bail!("Unknown custom setting '{s}'"), - }; - - Ok(variant) - } -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, PtpSerialize, PtpDeserialize)] pub struct FujiCustomSettingName(String); impl FujiCustomSettingName { pub const MAX_LEN: usize = 25; + + pub const unsafe fn new_unchecked(value: String) -> Self { + Self(value) + } } impl Deref for FujiCustomSettingName { @@ -308,23 +237,6 @@ impl TryFrom for FujiCustomSettingName { } } -impl FromStr for FujiCustomSettingName { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - if s.len() > Self::MAX_LEN { - bail!("Value '{}' exceeds max length of {}", s, Self::MAX_LEN); - } - Ok(Self(s.to_string())) - } -} - -impl std::fmt::Display for FujiCustomSettingName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - #[repr(u16)] #[derive( Debug, @@ -356,81 +268,6 @@ pub enum FujiImageSize { R3264x2592 = 0xc, } -impl fmt::Display for FujiImageSize { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::R7728x5152 => write!(f, "7728x5152"), - Self::R7728x4344 => write!(f, "7728x4344"), - Self::R5152x5152 => write!(f, "5152x5152"), - Self::R6864x5152 => write!(f, "6864x5152"), - Self::R6432x5152 => write!(f, "6432x5152"), - Self::R5472x3648 => write!(f, "5472x3648"), - Self::R5472x3080 => write!(f, "5472x3080"), - Self::R3648x3648 => write!(f, "3648x3648"), - Self::R4864x3648 => write!(f, "4864x3648"), - Self::R4560x3648 => write!(f, "4560x3648"), - Self::R3888x2592 => write!(f, "3888x2592"), - Self::R3888x2184 => write!(f, "3888x2184"), - Self::R2592x2592 => write!(f, "2592x2592"), - Self::R3456x2592 => write!(f, "3456x2592"), - Self::R3264x2592 => write!(f, "3264x2592"), - } - } -} - -impl Serialize for FujiImageSize { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl FromStr for FujiImageSize { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase(); - - match input.as_str() { - "max" | "maximum" | "full" | "largest" => return Ok(Self::R7728x5152), - _ => {} - } - - let input = s.replace(' ', "x").replace("by", "x"); - if let Some((w_str, h_str)) = input.split_once('x') - && let (Ok(w), Ok(h)) = (w_str.trim().parse::(), h_str.trim().parse::()) - { - match (w, h) { - (7728, 5152) => return Ok(Self::R7728x5152), - (7728, 4344) => return Ok(Self::R7728x4344), - (5152, 5152) => return Ok(Self::R5152x5152), - (6864, 5152) => return Ok(Self::R6864x5152), - (6432, 5152) => return Ok(Self::R6432x5152), - (5472, 3648) => return Ok(Self::R5472x3648), - (5472, 3080) => return Ok(Self::R5472x3080), - (3648, 3648) => return Ok(Self::R3648x3648), - (4864, 3648) => return Ok(Self::R4864x3648), - (4560, 3648) => return Ok(Self::R4560x3648), - (3888, 2592) => return Ok(Self::R3888x2592), - (3888, 2184) => return Ok(Self::R3888x2184), - (2592, 2592) => return Ok(Self::R2592x2592), - (3456, 2592) => return Ok(Self::R3456x2592), - (3264, 2592) => return Ok(Self::R3264x2592), - _ => {} - } - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown image size '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown image size '{s}'. Expected a resolution (e.g., '5472x3648') or 'maximum'."); - } -} - #[repr(u16)] #[derive( Debug, @@ -453,42 +290,6 @@ pub enum FujiImageQuality { Raw = 0x1, } -impl fmt::Display for FujiImageQuality { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::FineRaw => write!(f, "Fine + RAW"), - Self::Fine => write!(f, "Fine"), - Self::NormalRaw => write!(f, "Normal + RAW"), - Self::Normal => write!(f, "Normal"), - Self::Raw => write!(f, "RAW"), - } - } -} - -impl FromStr for FujiImageQuality { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase().replace(['+', ' '].as_ref(), ""); - - match input.as_str() { - "fineraw" => return Ok(Self::FineRaw), - "fine" => return Ok(Self::Fine), - "normalraw" => return Ok(Self::NormalRaw), - "normal" => return Ok(Self::Normal), - "raw" => return Ok(Self::Raw), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown image quality '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown image quality '{s}'"); - } -} - #[repr(u16)] #[derive( Debug, @@ -510,40 +311,6 @@ pub enum FujiDynamicRange { HDR400 = 0x190, } -impl fmt::Display for FujiDynamicRange { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Auto => write!(f, "Auto"), - Self::HDR100 => write!(f, "HDR100"), - Self::HDR200 => write!(f, "HDR200"), - Self::HDR400 => write!(f, "HDR400"), - } - } -} - -impl FromStr for FujiDynamicRange { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), ""); - - match input.as_str() { - "auto" | "hdrauto" | "drauto" => return Ok(Self::Auto), - "100" | "hdr100" | "dr100" => return Ok(Self::HDR100), - "200" | "hdr200" | "dr200" => return Ok(Self::HDR200), - "400" | "hdr400" | "dr400" => return Ok(Self::HDR400), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown dynamic range '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown dynamic range '{s}'"); - } -} - #[repr(u16)] #[derive( Debug, @@ -565,40 +332,6 @@ pub enum FujiDynamicRangePriority { Off = 0x0, } -impl fmt::Display for FujiDynamicRangePriority { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Auto => write!(f, "Auto"), - Self::Strong => write!(f, "Strong"), - Self::Weak => write!(f, "Weak"), - Self::Off => write!(f, "Off"), - } - } -} - -impl FromStr for FujiDynamicRangePriority { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), ""); - - match input.as_str() { - "auto" | "drpauto" => return Ok(Self::Auto), - "strong" | "drpstrong" => return Ok(Self::Strong), - "weak" | "drpweak" => return Ok(Self::Weak), - "off" | "drpoff" => return Ok(Self::Off), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown dynamic range priority '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown dynamic range priority '{s}'"); - } -} - #[repr(u16)] #[derive( Debug, @@ -636,100 +369,6 @@ pub enum FujiFilmSimulation { RealaAce = 0x14, } -impl fmt::Display for FujiFilmSimulation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Provia => write!(f, "Provia"), - Self::Velvia => write!(f, "Velvia"), - Self::Astia => write!(f, "Astia"), - Self::PRONegHi => write!(f, "PRO Neg. Hi"), - Self::PRONegStd => write!(f, "PRO Neg. Std"), - Self::Monochrome => write!(f, "Monochrome"), - Self::MonochromeYe => write!(f, "Monochrome + Ye"), - Self::MonochromeR => write!(f, "Monochrome + R"), - Self::MonochromeG => write!(f, "Monochrome + G"), - Self::Sepia => write!(f, "Sepia"), - Self::ClassicChrome => write!(f, "Classic Chrome"), - Self::AcrosSTD => write!(f, "Acros"), - Self::AcrosYe => write!(f, "Acros + Ye"), - Self::AcrosR => write!(f, "Acros + R"), - Self::AcrosG => write!(f, "Acros + G"), - Self::Eterna => write!(f, "Eterna"), - Self::ClassicNegative => write!(f, "Classic Negative"), - Self::NostalgicNegative => write!(f, "Nostalgic Negative"), - Self::EternaBleachBypass => write!(f, "Eterna Bleach Bypass"), - Self::RealaAce => write!(f, "Reala Ace"), - } - } -} - -impl FromStr for FujiFilmSimulation { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s - .trim() - .to_lowercase() - .replace([' ', '.', '+'].as_ref(), ""); - - match input.as_str() { - "provia" => return Ok(Self::Provia), - "velvia" => return Ok(Self::Velvia), - "astia" => return Ok(Self::Astia), - "proneghi" | "proneghigh" => { - return Ok(Self::PRONegHi); - } - "pronegstd" | "pronegstandard" => { - return Ok(Self::PRONegStd); - } - "mono" | "monochrome" => return Ok(Self::Monochrome), - "monoy" | "monoye" | "monoyellow" | "monochromey" | "monochromeye" - | "monochromeyellow" => { - return Ok(Self::MonochromeYe); - } - "monor" | "monored" | "monochromer" | "monochromered" => { - return Ok(Self::MonochromeR); - } - "monog" | "monogreen" | "monochromeg" | "monochromegreen" => { - return Ok(Self::MonochromeG); - } - "sepia" => return Ok(Self::Sepia), - "classicchrome" => return Ok(Self::ClassicChrome), - "acros" => return Ok(Self::AcrosSTD), - "acrosy" | "acrosye" | "acrosyellow" => { - return Ok(Self::AcrosYe); - } - "acrossr" | "acrossred" => { - return Ok(Self::AcrosR); - } - "acrossg" | "acrossgreen" => { - return Ok(Self::AcrosG); - } - "eterna" => return Ok(Self::Eterna), - "classicneg" | "classicnegative" => { - return Ok(Self::ClassicNegative); - } - "nostalgicneg" | "nostalgicnegative" => { - return Ok(Self::NostalgicNegative); - } - "eternabb" | "eternableach" | "eternableachbypass" => { - return Ok(Self::EternaBleachBypass); - } - "realaace" => { - return Ok(Self::RealaAce); - } - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown value '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown value '{input}'"); - } -} - #[repr(u16)] #[derive( Debug, @@ -752,45 +391,6 @@ pub enum FujiGrainEffect { Off = 0x6, } -impl fmt::Display for FujiGrainEffect { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::StrongLarge => write!(f, "Strong Large"), - Self::WeakLarge => write!(f, "Weak Large"), - Self::StrongSmall => write!(f, "Strong Small"), - Self::WeakSmall => write!(f, "Weak Small"), - Self::Off => write!(f, "Off"), - } - } -} - -impl FromStr for FujiGrainEffect { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s - .trim() - .to_lowercase() - .replace(['+', '-', ',', ' '].as_ref(), ""); - - match input.as_str() { - "stronglarge" | "largestrong" => return Ok(Self::StrongLarge), - "weaklarge" | "largeweak" => return Ok(Self::WeakLarge), - "strongsmall" | "smallstrong" => return Ok(Self::StrongSmall), - "weaksmall" | "smallweak" => return Ok(Self::WeakSmall), - "off" => return Ok(Self::Off), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(&input, &choices) { - bail!("Unknown grain effect '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown grain effect '{s}'"); - } -} - #[repr(u16)] #[derive( Debug, @@ -811,38 +411,6 @@ pub enum FujiColorChromeEffect { Off = 0x1, } -impl fmt::Display for FujiColorChromeEffect { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Strong => write!(f, "Strong"), - Self::Weak => write!(f, "Weak"), - Self::Off => write!(f, "Off"), - } - } -} - -impl FromStr for FujiColorChromeEffect { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase(); - - match input.as_str() { - "strong" => return Ok(Self::Strong), - "weak" => return Ok(Self::Weak), - "off" => return Ok(Self::Off), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown color chrome effect '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown color chrome effect '{s}'"); - } -} - #[repr(u16)] #[derive( Debug, @@ -863,38 +431,6 @@ pub enum FujiColorChromeFXBlue { Off = 0x1, } -impl fmt::Display for FujiColorChromeFXBlue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Strong => write!(f, "Strong"), - Self::Weak => write!(f, "Weak"), - Self::Off => write!(f, "Off"), - } - } -} - -impl FromStr for FujiColorChromeFXBlue { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase(); - - match input.as_str() { - "strong" => return Ok(Self::Strong), - "weak" => return Ok(Self::Weak), - "off" => return Ok(Self::Off), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown color chrome fx blue '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown color chrome fx blue '{s}'"); - } -} - #[repr(u16)] #[derive( Debug, @@ -915,38 +451,6 @@ pub enum FujiSmoothSkinEffect { Off = 0x1, } -impl fmt::Display for FujiSmoothSkinEffect { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Strong => write!(f, "Strong"), - Self::Weak => write!(f, "Weak"), - Self::Off => write!(f, "Off"), - } - } -} - -impl FromStr for FujiSmoothSkinEffect { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase(); - - match input.as_str() { - "strong" => return Ok(Self::Strong), - "weak" => return Ok(Self::Weak), - "off" => return Ok(Self::Off), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown smooth skin effect '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown smooth skin effect '{s}'"); - } -} - #[repr(u16)] #[derive( Debug, @@ -962,6 +466,7 @@ impl FromStr for FujiSmoothSkinEffect { EnumIter, )] pub enum FujiWhiteBalance { + AsShot = 0x1, WhitePriority = 0x8020, Auto = 0x2, AmbiencePriority = 0x8021, @@ -978,67 +483,6 @@ pub enum FujiWhiteBalance { Underwater = 0x8, } -impl fmt::Display for FujiWhiteBalance { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::WhitePriority => write!(f, "White Priority"), - Self::Auto => write!(f, "Auto"), - Self::AmbiencePriority => write!(f, "Ambience Priority"), - Self::Custom1 => write!(f, "Custom 1"), - Self::Custom2 => write!(f, "Custom 2"), - Self::Custom3 => write!(f, "Custom 3"), - Self::Temperature => write!(f, "Temperature"), - Self::Daylight => write!(f, "Daylight"), - Self::Shade => write!(f, "Shade"), - Self::Fluorescent1 => write!(f, "Fluorescent 1"), - Self::Fluorescent2 => write!(f, "Fluorescent 2"), - Self::Fluorescent3 => write!(f, "Fluorescent 3"), - Self::Incandescent => write!(f, "Incandescent"), - Self::Underwater => write!(f, "Underwater"), - } - } -} -impl FromStr for FujiWhiteBalance { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), ""); - - match input.as_str() { - "whitepriority" | "white" => return Ok(Self::WhitePriority), - "auto" => return Ok(Self::Auto), - "ambiencepriority" | "ambience" | "ambient" => { - return Ok(Self::AmbiencePriority); - } - "custom1" | "c1" => return Ok(Self::Custom1), - "custom2" | "c2" => return Ok(Self::Custom2), - "custom3" | "c3" => return Ok(Self::Custom3), - "temperature" | "k" | "kelvin" => return Ok(Self::Temperature), - "daylight" | "sunny" => return Ok(Self::Daylight), - "shade" | "cloudy" => return Ok(Self::Shade), - "fluorescent1" => { - return Ok(Self::Fluorescent1); - } - "fluorescent2" => { - return Ok(Self::Fluorescent2); - } - "fluorescent3" => { - return Ok(Self::Fluorescent3); - } - "incandescent" | "tungsten" => return Ok(Self::Incandescent), - "underwater" => return Ok(Self::Underwater), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown white balance '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown white balance '{s}'"); - } -} - #[repr(u16)] #[derive( Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize, @@ -1055,65 +499,6 @@ pub enum FujiHighISONR { Minus4 = 0x8000, } -impl fmt::Display for FujiHighISONR { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Plus4 => write!(f, "+4"), - Self::Plus3 => write!(f, "+3"), - Self::Plus2 => write!(f, "+2"), - Self::Plus1 => write!(f, "+1"), - Self::Zero => write!(f, "0"), - Self::Minus1 => write!(f, "-1"), - Self::Minus2 => write!(f, "-2"), - Self::Minus3 => write!(f, "-3"), - Self::Minus4 => write!(f, "-4"), - } - } -} - -impl Serialize for FujiHighISONR { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - Self::Plus4 => serializer.serialize_i16(4), - Self::Plus3 => serializer.serialize_i16(3), - Self::Plus2 => serializer.serialize_i16(2), - Self::Plus1 => serializer.serialize_i16(1), - Self::Zero => serializer.serialize_i16(0), - Self::Minus1 => serializer.serialize_i16(-1), - Self::Minus2 => serializer.serialize_i16(-2), - Self::Minus3 => serializer.serialize_i16(-3), - Self::Minus4 => serializer.serialize_i16(-4), - } - } -} - -impl FromStr for FujiHighISONR { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s - .trim() - .parse::() - .with_context(|| format!("Invalid numeric value '{s}'"))?; - - match input { - 4 => Ok(Self::Plus4), - 3 => Ok(Self::Plus3), - 2 => Ok(Self::Plus2), - 1 => Ok(Self::Plus1), - 0 => Ok(Self::Zero), - -1 => Ok(Self::Minus1), - -2 => Ok(Self::Minus2), - -3 => Ok(Self::Minus3), - -4 => Ok(Self::Minus4), - _ => bail!("Value {input} is out of range",), - } - } -} - #[repr(u16)] #[derive( Debug, @@ -1133,36 +518,6 @@ pub enum FujiLensModulationOptimizer { On = 0x1, } -impl fmt::Display for FujiLensModulationOptimizer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Off => write!(f, "Off"), - Self::On => write!(f, "On"), - } - } -} - -impl FromStr for FujiLensModulationOptimizer { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase(); - - match input.as_str() { - "off" | "false" => return Ok(Self::Off), - "on" | "true" => return Ok(Self::On), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown lens modulation optimizer '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown lens modulation optimizer '{s}'"); - } -} - #[repr(u16)] #[derive( Debug, @@ -1177,41 +532,12 @@ impl FromStr for FujiLensModulationOptimizer { PtpDeserialize, EnumIter, )] +#[allow(clippy::upper_case_acronyms)] pub enum FujiColorSpace { SRGB = 0x2, AdobeRGB = 0x1, } -impl fmt::Display for FujiColorSpace { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::SRGB => write!(f, "sRGB"), - Self::AdobeRGB => write!(f, "Adobe RGB"), - } - } -} - -impl FromStr for FujiColorSpace { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - let input = s.trim().to_lowercase(); - - match input.as_str() { - "s" | "srgb" => return Ok(Self::SRGB), - "adobe" | "adobergb" => return Ok(Self::AdobeRGB), - _ => {} - } - - let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); - if let Some(best) = suggest_closest(s, &choices) { - bail!("Unknown color space '{s}'. Did you mean '{best}'?"); - } - - bail!("Unknown color space '{s}'"); - } -} - macro_rules! fuji_i16 { ($name:ident, $min:expr, $max:expr, $step:expr, $scale:literal) => { #[derive(Debug, Clone, Copy, PartialEq, Eq, PtpSerialize, PtpDeserialize)] @@ -1263,49 +589,6 @@ macro_rules! fuji_i16 { Ok(Self(value)) } } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let value = (f32::from(self.0) / Self::SCALE); - write!(f, "{}", value) - } - } - - impl std::str::FromStr for $name { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - use anyhow::Context; - - let input = s - .trim() - .parse::() - .with_context(|| format!("Invalid numeric value '{s}'"))?; - - if !(Self::MIN..=Self::MAX).contains(&input) { - anyhow::bail!("Value {} is out of range", input); - } - #[allow(clippy::modulo_one)] - if (input - Self::MIN) % Self::STEP != 0.0 { - anyhow::bail!("Value {} is not aligned to step {}", input, Self::STEP); - } - - #[allow(clippy::cast_possible_truncation)] - let raw = (input * Self::SCALE).round() as i16; - - unsafe { Ok(Self::new_unchecked(raw)) } - } - } - - impl serde::Serialize for $name { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let val = f32::from(self.0) / Self::SCALE; - serializer.serialize_f32(val) - } - } }; } @@ -1327,15 +610,6 @@ pub enum UsbMode { RawConversion = 0x6, } -impl fmt::Display for UsbMode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Self::RawConversion => "USB RAW CONV./BACKUP RESTORE", - }; - write!(f, "{s}") - } -} - #[derive( Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize, )] diff --git a/src/cli/common/film.rs b/src/cli/common/film.rs index 768a613..6c6c241 100644 --- a/src/cli/common/film.rs +++ b/src/cli/common/film.rs @@ -1,11 +1,20 @@ -use clap::Args; +use std::{fmt, ops::Deref, str::FromStr}; -use crate::camera::ptp::hex::{ - FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace, - FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, - FujiHighlightTone, FujiImageQuality, FujiImageSize, FujiLensModulationOptimizer, - FujiMonochromaticColorTemperature, FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, - FujiSmoothSkinEffect, FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, +use anyhow::{Context, bail}; +use clap::Args; +use serde::{Serialize, Serializer}; +use strum::IntoEnumIterator; + +use crate::{ + camera::ptp::hex::{ + FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace, + FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority, + FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality, + FujiImageSize, FujiLensModulationOptimizer, FujiMonochromaticColorTemperature, + FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect, + FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode, + }, + cli::common::suggest::get_closest, }; #[derive(Args, Debug)] @@ -102,3 +111,721 @@ pub struct FilmSimulationOptions { #[clap(long)] pub color_space: Option, } + +impl fmt::Display for FujiCustomSetting { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::C1 => write!(f, "C1"), + Self::C2 => write!(f, "C2"), + Self::C3 => write!(f, "C3"), + Self::C4 => write!(f, "C4"), + Self::C5 => write!(f, "C5"), + Self::C6 => write!(f, "C6"), + Self::C7 => write!(f, "C7"), + } + } +} + +impl FromStr for FujiCustomSetting { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase(); + + let variant = match input.as_str() { + "c1" | "1" => Self::C1, + "c2" | "2" => Self::C2, + "c3" | "3" => Self::C3, + "c4" | "4" => Self::C4, + "c5" | "5" => Self::C5, + "c6" | "6" => Self::C6, + "c7" | "7" => Self::C7, + _ => bail!("Unknown custom setting '{s}'"), + }; + + Ok(variant) + } +} + +impl Serialize for FujiCustomSetting { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u16((*self).into()) + } +} + +impl fmt::Display for FujiCustomSettingName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", &**self) + } +} + +impl FromStr for FujiCustomSettingName { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + if s.len() > Self::MAX_LEN { + bail!("Value '{}' exceeds max length of {}", s, Self::MAX_LEN); + } + Ok(unsafe { Self::new_unchecked(s.to_string()) }) + } +} + +impl fmt::Display for FujiImageSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::R7728x5152 => write!(f, "7728x5152"), + Self::R7728x4344 => write!(f, "7728x4344"), + Self::R5152x5152 => write!(f, "5152x5152"), + Self::R6864x5152 => write!(f, "6864x5152"), + Self::R6432x5152 => write!(f, "6432x5152"), + Self::R5472x3648 => write!(f, "5472x3648"), + Self::R5472x3080 => write!(f, "5472x3080"), + Self::R3648x3648 => write!(f, "3648x3648"), + Self::R4864x3648 => write!(f, "4864x3648"), + Self::R4560x3648 => write!(f, "4560x3648"), + Self::R3888x2592 => write!(f, "3888x2592"), + Self::R3888x2184 => write!(f, "3888x2184"), + Self::R2592x2592 => write!(f, "2592x2592"), + Self::R3456x2592 => write!(f, "3456x2592"), + Self::R3264x2592 => write!(f, "3264x2592"), + } + } +} + +impl FromStr for FujiImageSize { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase(); + + match input.as_str() { + "max" | "maximum" | "full" | "largest" => return Ok(Self::R7728x5152), + _ => {} + } + + let input = s.replace(' ', "x").replace("by", "x"); + if let Some((w_str, h_str)) = input.split_once('x') + && let (Ok(w), Ok(h)) = (w_str.trim().parse::(), h_str.trim().parse::()) + { + match (w, h) { + (7728, 5152) => return Ok(Self::R7728x5152), + (7728, 4344) => return Ok(Self::R7728x4344), + (5152, 5152) => return Ok(Self::R5152x5152), + (6864, 5152) => return Ok(Self::R6864x5152), + (6432, 5152) => return Ok(Self::R6432x5152), + (5472, 3648) => return Ok(Self::R5472x3648), + (5472, 3080) => return Ok(Self::R5472x3080), + (3648, 3648) => return Ok(Self::R3648x3648), + (4864, 3648) => return Ok(Self::R4864x3648), + (4560, 3648) => return Ok(Self::R4560x3648), + (3888, 2592) => return Ok(Self::R3888x2592), + (3888, 2184) => return Ok(Self::R3888x2184), + (2592, 2592) => return Ok(Self::R2592x2592), + (3456, 2592) => return Ok(Self::R3456x2592), + (3264, 2592) => return Ok(Self::R3264x2592), + _ => {} + } + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown image size '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown image size '{s}'. Expected a resolution (e.g., '5472x3648') or 'maximum'."); + } +} + +impl Serialize for FujiImageSize { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl fmt::Display for FujiImageQuality { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::FineRaw => write!(f, "Fine + RAW"), + Self::Fine => write!(f, "Fine"), + Self::NormalRaw => write!(f, "Normal + RAW"), + Self::Normal => write!(f, "Normal"), + Self::Raw => write!(f, "RAW"), + } + } +} + +impl FromStr for FujiImageQuality { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase().replace(['+', ' '].as_ref(), ""); + + match input.as_str() { + "fineraw" => return Ok(Self::FineRaw), + "fine" => return Ok(Self::Fine), + "normalraw" => return Ok(Self::NormalRaw), + "normal" => return Ok(Self::Normal), + "raw" => return Ok(Self::Raw), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown image quality '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown image quality '{s}'"); + } +} + +impl fmt::Display for FujiDynamicRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Auto => write!(f, "Auto"), + Self::HDR100 => write!(f, "HDR100"), + Self::HDR200 => write!(f, "HDR200"), + Self::HDR400 => write!(f, "HDR400"), + } + } +} + +impl FromStr for FujiDynamicRange { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), ""); + + match input.as_str() { + "auto" | "hdrauto" | "drauto" => return Ok(Self::Auto), + "100" | "hdr100" | "dr100" => return Ok(Self::HDR100), + "200" | "hdr200" | "dr200" => return Ok(Self::HDR200), + "400" | "hdr400" | "dr400" => return Ok(Self::HDR400), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown dynamic range '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown dynamic range '{s}'"); + } +} + +impl fmt::Display for FujiDynamicRangePriority { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Auto => write!(f, "Auto"), + Self::Strong => write!(f, "Strong"), + Self::Weak => write!(f, "Weak"), + Self::Off => write!(f, "Off"), + } + } +} + +impl FromStr for FujiDynamicRangePriority { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), ""); + + match input.as_str() { + "auto" | "drpauto" => return Ok(Self::Auto), + "strong" | "drpstrong" => return Ok(Self::Strong), + "weak" | "drpweak" => return Ok(Self::Weak), + "off" | "drpoff" => return Ok(Self::Off), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown dynamic range priority '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown dynamic range priority '{s}'"); + } +} + +impl fmt::Display for FujiFilmSimulation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Provia => write!(f, "Provia"), + Self::Velvia => write!(f, "Velvia"), + Self::Astia => write!(f, "Astia"), + Self::PRONegHi => write!(f, "PRO Neg. Hi"), + Self::PRONegStd => write!(f, "PRO Neg. Std"), + Self::Monochrome => write!(f, "Monochrome"), + Self::MonochromeYe => write!(f, "Monochrome + Ye"), + Self::MonochromeR => write!(f, "Monochrome + R"), + Self::MonochromeG => write!(f, "Monochrome + G"), + Self::Sepia => write!(f, "Sepia"), + Self::ClassicChrome => write!(f, "Classic Chrome"), + Self::AcrosSTD => write!(f, "Acros"), + Self::AcrosYe => write!(f, "Acros + Ye"), + Self::AcrosR => write!(f, "Acros + R"), + Self::AcrosG => write!(f, "Acros + G"), + Self::Eterna => write!(f, "Eterna"), + Self::ClassicNegative => write!(f, "Classic Negative"), + Self::NostalgicNegative => write!(f, "Nostalgic Negative"), + Self::EternaBleachBypass => write!(f, "Eterna Bleach Bypass"), + Self::RealaAce => write!(f, "Reala Ace"), + } + } +} + +impl FromStr for FujiFilmSimulation { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s + .trim() + .to_lowercase() + .replace([' ', '.', '+'].as_ref(), ""); + + match input.as_str() { + "provia" => return Ok(Self::Provia), + "velvia" => return Ok(Self::Velvia), + "astia" => return Ok(Self::Astia), + "proneghi" | "proneghigh" => { + return Ok(Self::PRONegHi); + } + "pronegstd" | "pronegstandard" => { + return Ok(Self::PRONegStd); + } + "mono" | "monochrome" => return Ok(Self::Monochrome), + "monoy" | "monoye" | "monoyellow" | "monochromey" | "monochromeye" + | "monochromeyellow" => { + return Ok(Self::MonochromeYe); + } + "monor" | "monored" | "monochromer" | "monochromered" => { + return Ok(Self::MonochromeR); + } + "monog" | "monogreen" | "monochromeg" | "monochromegreen" => { + return Ok(Self::MonochromeG); + } + "sepia" => return Ok(Self::Sepia), + "classicchrome" => return Ok(Self::ClassicChrome), + "acros" => return Ok(Self::AcrosSTD), + "acrosy" | "acrosye" | "acrosyellow" => { + return Ok(Self::AcrosYe); + } + "acrossr" | "acrossred" => { + return Ok(Self::AcrosR); + } + "acrossg" | "acrossgreen" => { + return Ok(Self::AcrosG); + } + "eterna" => return Ok(Self::Eterna), + "classicneg" | "classicnegative" => { + return Ok(Self::ClassicNegative); + } + "nostalgicneg" | "nostalgicnegative" => { + return Ok(Self::NostalgicNegative); + } + "eternabb" | "eternableach" | "eternableachbypass" => { + return Ok(Self::EternaBleachBypass); + } + "realaace" => { + return Ok(Self::RealaAce); + } + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown value '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown value '{input}'"); + } +} + +impl fmt::Display for FujiGrainEffect { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::StrongLarge => write!(f, "Strong Large"), + Self::WeakLarge => write!(f, "Weak Large"), + Self::StrongSmall => write!(f, "Strong Small"), + Self::WeakSmall => write!(f, "Weak Small"), + Self::Off => write!(f, "Off"), + } + } +} + +impl FromStr for FujiGrainEffect { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s + .trim() + .to_lowercase() + .replace(['+', '-', ',', ' '].as_ref(), ""); + + match input.as_str() { + "stronglarge" | "largestrong" => return Ok(Self::StrongLarge), + "weaklarge" | "largeweak" => return Ok(Self::WeakLarge), + "strongsmall" | "smallstrong" => return Ok(Self::StrongSmall), + "weaksmall" | "smallweak" => return Ok(Self::WeakSmall), + "off" => return Ok(Self::Off), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(&input, &choices) { + bail!("Unknown grain effect '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown grain effect '{s}'"); + } +} + +impl fmt::Display for FujiColorChromeEffect { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Strong => write!(f, "Strong"), + Self::Weak => write!(f, "Weak"), + Self::Off => write!(f, "Off"), + } + } +} + +impl FromStr for FujiColorChromeEffect { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase(); + + match input.as_str() { + "strong" => return Ok(Self::Strong), + "weak" => return Ok(Self::Weak), + "off" => return Ok(Self::Off), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown color chrome effect '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown color chrome effect '{s}'"); + } +} + +impl fmt::Display for FujiColorChromeFXBlue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Strong => write!(f, "Strong"), + Self::Weak => write!(f, "Weak"), + Self::Off => write!(f, "Off"), + } + } +} + +impl FromStr for FujiColorChromeFXBlue { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase(); + + match input.as_str() { + "strong" => return Ok(Self::Strong), + "weak" => return Ok(Self::Weak), + "off" => return Ok(Self::Off), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown color chrome fx blue '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown color chrome fx blue '{s}'"); + } +} + +impl fmt::Display for FujiSmoothSkinEffect { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Strong => write!(f, "Strong"), + Self::Weak => write!(f, "Weak"), + Self::Off => write!(f, "Off"), + } + } +} + +impl FromStr for FujiSmoothSkinEffect { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase(); + + match input.as_str() { + "strong" => return Ok(Self::Strong), + "weak" => return Ok(Self::Weak), + "off" => return Ok(Self::Off), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown smooth skin effect '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown smooth skin effect '{s}'"); + } +} + +impl fmt::Display for FujiWhiteBalance { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AsShot => write!(f, "As Shot"), + Self::WhitePriority => write!(f, "White Priority"), + Self::Auto => write!(f, "Auto"), + Self::AmbiencePriority => write!(f, "Ambience Priority"), + Self::Custom1 => write!(f, "Custom 1"), + Self::Custom2 => write!(f, "Custom 2"), + Self::Custom3 => write!(f, "Custom 3"), + Self::Temperature => write!(f, "Temperature"), + Self::Daylight => write!(f, "Daylight"), + Self::Shade => write!(f, "Shade"), + Self::Fluorescent1 => write!(f, "Fluorescent 1"), + Self::Fluorescent2 => write!(f, "Fluorescent 2"), + Self::Fluorescent3 => write!(f, "Fluorescent 3"), + Self::Incandescent => write!(f, "Incandescent"), + Self::Underwater => write!(f, "Underwater"), + } + } +} + +impl FromStr for FujiWhiteBalance { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), ""); + + match input.as_str() { + "whitepriority" | "white" => return Ok(Self::WhitePriority), + // We can't set a film simulation to be "As Shot", so silently parse it to Auto + "auto" | "shot" | "asshot" | "original" => return Ok(Self::Auto), + "ambiencepriority" | "ambience" | "ambient" => { + return Ok(Self::AmbiencePriority); + } + "custom1" | "c1" => return Ok(Self::Custom1), + "custom2" | "c2" => return Ok(Self::Custom2), + "custom3" | "c3" => return Ok(Self::Custom3), + "temperature" | "k" | "kelvin" => return Ok(Self::Temperature), + "daylight" | "sunny" => return Ok(Self::Daylight), + "shade" | "cloudy" => return Ok(Self::Shade), + "fluorescent1" => { + return Ok(Self::Fluorescent1); + } + "fluorescent2" => { + return Ok(Self::Fluorescent2); + } + "fluorescent3" => { + return Ok(Self::Fluorescent3); + } + "incandescent" | "tungsten" => return Ok(Self::Incandescent), + "underwater" => return Ok(Self::Underwater), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown white balance '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown white balance '{s}'"); + } +} + +impl fmt::Display for FujiHighISONR { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Plus4 => write!(f, "+4"), + Self::Plus3 => write!(f, "+3"), + Self::Plus2 => write!(f, "+2"), + Self::Plus1 => write!(f, "+1"), + Self::Zero => write!(f, "0"), + Self::Minus1 => write!(f, "-1"), + Self::Minus2 => write!(f, "-2"), + Self::Minus3 => write!(f, "-3"), + Self::Minus4 => write!(f, "-4"), + } + } +} + +impl FromStr for FujiHighISONR { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s + .trim() + .parse::() + .with_context(|| format!("Invalid numeric value '{s}'"))?; + + match input { + 4 => Ok(Self::Plus4), + 3 => Ok(Self::Plus3), + 2 => Ok(Self::Plus2), + 1 => Ok(Self::Plus1), + 0 => Ok(Self::Zero), + -1 => Ok(Self::Minus1), + -2 => Ok(Self::Minus2), + -3 => Ok(Self::Minus3), + -4 => Ok(Self::Minus4), + _ => bail!("Value {input} is out of range",), + } + } +} + +impl Serialize for FujiHighISONR { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Plus4 => serializer.serialize_i16(4), + Self::Plus3 => serializer.serialize_i16(3), + Self::Plus2 => serializer.serialize_i16(2), + Self::Plus1 => serializer.serialize_i16(1), + Self::Zero => serializer.serialize_i16(0), + Self::Minus1 => serializer.serialize_i16(-1), + Self::Minus2 => serializer.serialize_i16(-2), + Self::Minus3 => serializer.serialize_i16(-3), + Self::Minus4 => serializer.serialize_i16(-4), + } + } +} + +impl fmt::Display for FujiLensModulationOptimizer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Off => write!(f, "Off"), + Self::On => write!(f, "On"), + } + } +} + +impl FromStr for FujiLensModulationOptimizer { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase(); + + match input.as_str() { + "off" | "false" => return Ok(Self::Off), + "on" | "true" => return Ok(Self::On), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown lens modulation optimizer '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown lens modulation optimizer '{s}'"); + } +} + +impl fmt::Display for FujiColorSpace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::SRGB => write!(f, "sRGB"), + Self::AdobeRGB => write!(f, "Adobe RGB"), + } + } +} + +impl FromStr for FujiColorSpace { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase(); + + match input.as_str() { + "s" | "srgb" => return Ok(Self::SRGB), + "adobe" | "adobergb" => return Ok(Self::AdobeRGB), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown color space '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown color space '{s}'"); + } +} + +macro_rules! fuji_i16_cli { + ($name:ident) => { + impl std::str::FromStr for $name { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + use anyhow::Context; + + let input = s + .trim() + .parse::() + .with_context(|| format!("Invalid numeric value '{s}'"))?; + + if !(Self::MIN..=Self::MAX).contains(&input) { + anyhow::bail!("Value {} is out of range", input); + } + #[allow(clippy::modulo_one)] + if (input - Self::MIN) % Self::STEP != 0.0 { + anyhow::bail!("Value {} is not aligned to step {}", input, Self::STEP); + } + + #[allow(clippy::cast_possible_truncation)] + let raw = (input * Self::SCALE).round() as i16; + + unsafe { Ok(Self::new_unchecked(raw)) } + } + } + + impl serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let val = f32::from(*self.deref()) / Self::SCALE; + serializer.serialize_f32(val) + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = (f32::from(*self.deref()) / Self::SCALE); + write!(f, "{}", value) + } + } + }; +} + +fuji_i16_cli!(FujiMonochromaticColorTemperature); +fuji_i16_cli!(FujiMonochromaticColorTint); +fuji_i16_cli!(FujiWhiteBalanceShift); +fuji_i16_cli!(FujiWhiteBalanceTemperature); +fuji_i16_cli!(FujiHighlightTone); +fuji_i16_cli!(FujiShadowTone); +fuji_i16_cli!(FujiColor); +fuji_i16_cli!(FujiSharpness); +fuji_i16_cli!(FujiClarity); + +impl fmt::Display for UsbMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::RawConversion => "USB RAW CONV./BACKUP RESTORE", + }; + write!(f, "{s}") + } +} diff --git a/src/cli/common/mod.rs b/src/cli/common/mod.rs index 8aaf8f1..2ada0f4 100644 --- a/src/cli/common/mod.rs +++ b/src/cli/common/mod.rs @@ -1,2 +1,3 @@ pub mod file; pub mod film; +pub mod suggest; diff --git a/src/cli/common/suggest.rs b/src/cli/common/suggest.rs new file mode 100644 index 0000000..1fe5d46 --- /dev/null +++ b/src/cli/common/suggest.rs @@ -0,0 +1,29 @@ +use strsim::damerau_levenshtein; + +const SIMILARITY_THRESHOLD: usize = 8; + +pub fn get_closest<'a, I, S>(input: &str, choices: I) -> Option<&'a str> +where + I: IntoIterator, + S: AsRef + 'a, +{ + let mut best_score = usize::MAX; + let mut best_match: Option<&'a str> = None; + + for choice in choices { + let choice_str = choice.as_ref(); + let dist = damerau_levenshtein(&input.to_lowercase(), &choice_str.to_lowercase()); + + if dist < best_score { + best_score = dist; + best_match = Some(choice_str); + } + } + + println!("{best_score}"); + if best_score <= SIMILARITY_THRESHOLD { + best_match + } else { + None + } +} diff --git a/src/cli/simulation/mod.rs b/src/cli/simulation/mod.rs index 003a34a..588efc3 100644 --- a/src/cli/simulation/mod.rs +++ b/src/cli/simulation/mod.rs @@ -163,7 +163,7 @@ impl fmt::Display for FilmSimulationRepr { )?; } _ => {} - }; + } if self.dynamic_range_priority == FujiDynamicRangePriority::Off { writeln!(f, "Highlights: {}", self.highlight)?;