chore: reorganize cli traits

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-10-19 21:24:54 +01:00
parent fb4610bdaa
commit 3127887b82
5 changed files with 773 additions and 742 deletions

View File

@@ -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<Item = &'a S>,
S: AsRef<str> + '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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u16((*self).into())
}
}
impl FromStr for FujiCustomSetting {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
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<String> for FujiCustomSettingName {
}
}
impl FromStr for FujiCustomSettingName {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl FromStr for FujiImageSize {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
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::<u32>(), h_str.trim().parse::<u32>())
{
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<Self> {
let input = s
.trim()
.parse::<i16>()
.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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
use anyhow::Context;
let input = s
.trim()
.parse::<f32>()
.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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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,
)]

View File

@@ -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<FujiColorSpace>,
}
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<Self> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<Self> {
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<Self> {
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::<u32>(), h_str.trim().parse::<u32>())
{
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<String> = 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
let input = s
.trim()
.parse::<i16>()
.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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<Self> {
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<String> = 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<Self> {
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<String> = 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<Self> {
use anyhow::Context;
let input = s
.trim()
.parse::<f32>()
.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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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}")
}
}

View File

@@ -1,2 +1,3 @@
pub mod file;
pub mod film;
pub mod suggest;

29
src/cli/common/suggest.rs Normal file
View File

@@ -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<Item = &'a S>,
S: AsRef<str> + '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
}
}

View File

@@ -163,7 +163,7 @@ impl fmt::Display for FilmSimulationRepr {
)?;
}
_ => {}
};
}
if self.dynamic_range_priority == FujiDynamicRangePriority::Off {
writeln!(f, "Highlights: {}", self.highlight)?;