From 0f5997042c9120cf843ef6020c97e8988ea0ca0f Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Sat, 18 Oct 2025 17:05:18 +0100 Subject: [PATCH] feat: custom option setter Signed-off-by: Nikolaos Karaolidis --- crates/ptp/cursor/src/lib.rs | 58 ++++-- src/camera/mod.rs | 336 ++++++++++++++++++++++++++++++++--- src/camera/ptp/hex.rs | 148 ++++++++++----- src/cli/common/film.rs | 52 +++--- src/cli/simulation/mod.rs | 158 +++++++++++++--- src/log.rs | 8 +- 6 files changed, 621 insertions(+), 139 deletions(-) diff --git a/crates/ptp/cursor/src/lib.rs b/crates/ptp/cursor/src/lib.rs index f122f7f..d91f68e 100644 --- a/crates/ptp/cursor/src/lib.rs +++ b/crates/ptp/cursor/src/lib.rs @@ -216,7 +216,7 @@ pub trait PtpDeserialize: Sized { fn try_read_ptp(cur: &mut R) -> io::Result; } -macro_rules! impl_ptp { +macro_rules! impl_ptp_ser { ($ty:ty, $read_fn:ident, $write_fn:ident) => { impl PtpSerialize for $ty { fn try_into_ptp(&self) -> io::Result> { @@ -229,7 +229,11 @@ macro_rules! impl_ptp { buf.$write_fn(self) } } + }; +} +macro_rules! impl_ptp_de { + ($ty:ty, $read_fn:ident, $write_fn:ident) => { impl PtpDeserialize for $ty { fn try_from_ptp(buf: &[u8]) -> io::Result { let mut cur = Cursor::new(buf); @@ -245,20 +249,38 @@ macro_rules! impl_ptp { }; } -impl_ptp!(u8, read_ptp_u8, write_ptp_u8); -impl_ptp!(i8, read_ptp_i8, write_ptp_i8); -impl_ptp!(u16, read_ptp_u16, write_ptp_u16); -impl_ptp!(i16, read_ptp_i16, write_ptp_i16); -impl_ptp!(u32, read_ptp_u32, write_ptp_u32); -impl_ptp!(i32, read_ptp_i32, write_ptp_i32); -impl_ptp!(u64, read_ptp_u64, write_ptp_u64); -impl_ptp!(i64, read_ptp_i64, write_ptp_i64); -impl_ptp!(String, read_ptp_str, write_ptp_str); -impl_ptp!(Vec, read_ptp_u8_vec, write_ptp_u8_vec); -impl_ptp!(Vec, read_ptp_i8_vec, write_ptp_i8_vec); -impl_ptp!(Vec, read_ptp_u16_vec, write_ptp_u16_vec); -impl_ptp!(Vec, read_ptp_i16_vec, write_ptp_i16_vec); -impl_ptp!(Vec, read_ptp_u32_vec, write_ptp_u32_vec); -impl_ptp!(Vec, read_ptp_i32_vec, write_ptp_i32_vec); -impl_ptp!(Vec, read_ptp_u64_vec, write_ptp_u64_vec); -impl_ptp!(Vec, read_ptp_i64_vec, write_ptp_i64_vec); +impl_ptp_ser!(u8, read_ptp_u8, write_ptp_u8); +impl_ptp_de!(u8, read_ptp_u8, write_ptp_u8); +impl_ptp_ser!(i8, read_ptp_i8, write_ptp_i8); +impl_ptp_de!(i8, read_ptp_i8, write_ptp_i8); +impl_ptp_ser!(u16, read_ptp_u16, write_ptp_u16); +impl_ptp_de!(u16, read_ptp_u16, write_ptp_u16); +impl_ptp_ser!(i16, read_ptp_i16, write_ptp_i16); +impl_ptp_de!(i16, read_ptp_i16, write_ptp_i16); +impl_ptp_ser!(u32, read_ptp_u32, write_ptp_u32); +impl_ptp_de!(u32, read_ptp_u32, write_ptp_u32); +impl_ptp_ser!(i32, read_ptp_i32, write_ptp_i32); +impl_ptp_de!(i32, read_ptp_i32, write_ptp_i32); +impl_ptp_ser!(u64, read_ptp_u64, write_ptp_u64); +impl_ptp_de!(u64, read_ptp_u64, write_ptp_u64); +impl_ptp_ser!(i64, read_ptp_i64, write_ptp_i64); +impl_ptp_de!(i64, read_ptp_i64, write_ptp_i64); +impl_ptp_ser!(&str, read_ptp_str, write_ptp_str); +impl_ptp_ser!(String, read_ptp_str, write_ptp_str); +impl_ptp_de!(String, read_ptp_str, write_ptp_str); +impl_ptp_ser!(Vec, read_ptp_u8_vec, write_ptp_u8_vec); +impl_ptp_de!(Vec, read_ptp_u8_vec, write_ptp_u8_vec); +impl_ptp_ser!(Vec, read_ptp_i8_vec, write_ptp_i8_vec); +impl_ptp_de!(Vec, read_ptp_i8_vec, write_ptp_i8_vec); +impl_ptp_ser!(Vec, read_ptp_u16_vec, write_ptp_u16_vec); +impl_ptp_de!(Vec, read_ptp_u16_vec, write_ptp_u16_vec); +impl_ptp_ser!(Vec, read_ptp_i16_vec, write_ptp_i16_vec); +impl_ptp_de!(Vec, read_ptp_i16_vec, write_ptp_i16_vec); +impl_ptp_ser!(Vec, read_ptp_u32_vec, write_ptp_u32_vec); +impl_ptp_de!(Vec, read_ptp_u32_vec, write_ptp_u32_vec); +impl_ptp_ser!(Vec, read_ptp_i32_vec, write_ptp_i32_vec); +impl_ptp_de!(Vec, read_ptp_i32_vec, write_ptp_i32_vec); +impl_ptp_ser!(Vec, read_ptp_u64_vec, write_ptp_u64_vec); +impl_ptp_de!(Vec, read_ptp_u64_vec, write_ptp_u64_vec); +impl_ptp_ser!(Vec, read_ptp_i64_vec, write_ptp_i64_vec); +impl_ptp_de!(Vec, read_ptp_i64_vec, write_ptp_i64_vec); diff --git a/src/camera/mod.rs b/src/camera/mod.rs index 47b888e..428bdd3 100644 --- a/src/camera/mod.rs +++ b/src/camera/mod.rs @@ -11,10 +11,10 @@ use ptp::{ Ptp, hex::{ CommandCode, DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect, - FujiColorChromeFXBlue, FujiCustomSetting, FujiDynamicRange, FujiFilmSimulation, - FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize, - FujiShadowTone, FujiSharpness, FujiStillDynamicRangePriority, FujiWhiteBalance, - FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode, + FujiColorChromeFXBlue, FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, + FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, + FujiHighlightTone, FujiImageQuality, FujiImageSize, FujiShadowTone, FujiSharpness, + FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode, }, structs::DeviceInfo, }; @@ -158,81 +158,172 @@ impl Camera { self.r#impl.set_custom_setting_slot(&mut self.ptp, slot) } - pub fn get_custom_setting_name(&mut self) -> anyhow::Result { + pub fn get_custom_setting_name(&mut self) -> anyhow::Result { self.r#impl.get_custom_setting_name(&mut self.ptp) } + pub fn set_custom_setting_name(&mut self, value: &str) -> anyhow::Result<()> { + self.r#impl.set_custom_setting_name(&mut self.ptp, value) + } + pub fn get_image_size(&mut self) -> anyhow::Result { self.r#impl.get_image_size(&mut self.ptp) } + pub fn set_image_size(&mut self, value: FujiImageSize) -> anyhow::Result<()> { + self.r#impl.set_image_size(&mut self.ptp, value) + } + pub fn get_image_quality(&mut self) -> anyhow::Result { self.r#impl.get_image_quality(&mut self.ptp) } + pub fn set_image_quality(&mut self, value: FujiImageQuality) -> anyhow::Result<()> { + self.r#impl.set_image_quality(&mut self.ptp, value) + } + pub fn get_dynamic_range(&mut self) -> anyhow::Result { self.r#impl.get_dynamic_range(&mut self.ptp) } - pub fn get_dynamic_range_priority(&mut self) -> anyhow::Result { + pub fn set_dynamic_range(&mut self, value: FujiDynamicRange) -> anyhow::Result<()> { + self.r#impl.set_dynamic_range(&mut self.ptp, value) + } + + pub fn get_dynamic_range_priority(&mut self) -> anyhow::Result { self.r#impl.get_dynamic_range_priority(&mut self.ptp) } + pub fn set_dynamic_range_priority( + &mut self, + value: FujiDynamicRangePriority, + ) -> anyhow::Result<()> { + self.r#impl.set_dynamic_range_priority(&mut self.ptp, value) + } + pub fn get_film_simulation(&mut self) -> anyhow::Result { self.r#impl.get_film_simulation(&mut self.ptp) } + pub fn set_film_simulation(&mut self, value: FujiFilmSimulation) -> anyhow::Result<()> { + self.r#impl.set_film_simulation(&mut self.ptp, value) + } + pub fn get_grain_effect(&mut self) -> anyhow::Result { self.r#impl.get_grain_effect(&mut self.ptp) } + pub fn set_grain_effect(&mut self, value: FujiGrainEffect) -> anyhow::Result<()> { + self.r#impl.set_grain_effect(&mut self.ptp, value) + } + pub fn get_white_balance(&mut self) -> anyhow::Result { self.r#impl.get_white_balance(&mut self.ptp) } + pub fn set_white_balance(&mut self, value: FujiWhiteBalance) -> anyhow::Result<()> { + self.r#impl.set_white_balance(&mut self.ptp, value) + } + pub fn get_high_iso_nr(&mut self) -> anyhow::Result { self.r#impl.get_high_iso_nr(&mut self.ptp) } + pub fn set_high_iso_nr(&mut self, value: FujiHighISONR) -> anyhow::Result<()> { + self.r#impl.set_high_iso_nr(&mut self.ptp, value) + } + pub fn get_highlight_tone(&mut self) -> anyhow::Result { self.r#impl.get_highlight_tone(&mut self.ptp) } + pub fn set_highlight_tone(&mut self, value: FujiHighlightTone) -> anyhow::Result<()> { + self.r#impl.set_highlight_tone(&mut self.ptp, value) + } + pub fn get_shadow_tone(&mut self) -> anyhow::Result { self.r#impl.get_shadow_tone(&mut self.ptp) } + pub fn set_shadow_tone(&mut self, value: FujiShadowTone) -> anyhow::Result<()> { + self.r#impl.set_shadow_tone(&mut self.ptp, value) + } + pub fn get_color(&mut self) -> anyhow::Result { self.r#impl.get_color(&mut self.ptp) } + pub fn set_color(&mut self, value: FujiColor) -> anyhow::Result<()> { + self.r#impl.set_color(&mut self.ptp, value) + } + pub fn get_sharpness(&mut self) -> anyhow::Result { self.r#impl.get_sharpness(&mut self.ptp) } + pub fn set_sharpness(&mut self, value: FujiSharpness) -> anyhow::Result<()> { + self.r#impl.set_sharpness(&mut self.ptp, value) + } + pub fn get_clarity(&mut self) -> anyhow::Result { self.r#impl.get_clarity(&mut self.ptp) } - pub fn get_wb_shift_red(&mut self) -> anyhow::Result { - self.r#impl.get_wb_shift_red(&mut self.ptp) + pub fn set_clarity(&mut self, value: FujiClarity) -> anyhow::Result<()> { + self.r#impl.set_clarity(&mut self.ptp, value) } - pub fn get_wb_shift_blue(&mut self) -> anyhow::Result { - self.r#impl.get_wb_shift_blue(&mut self.ptp) + pub fn get_white_balance_shift_red(&mut self) -> anyhow::Result { + self.r#impl.get_white_balance_shift_red(&mut self.ptp) } - pub fn get_wb_temperature(&mut self) -> anyhow::Result { - self.r#impl.get_wb_temperature(&mut self.ptp) + pub fn set_white_balance_shift_red( + &mut self, + value: FujiWhiteBalanceShift, + ) -> anyhow::Result<()> { + self.r#impl + .set_white_balance_shift_red(&mut self.ptp, value) + } + + pub fn get_white_balance_shift_blue(&mut self) -> anyhow::Result { + self.r#impl.get_white_balance_shift_blue(&mut self.ptp) + } + + pub fn set_white_balance_shift_blue( + &mut self, + value: FujiWhiteBalanceShift, + ) -> anyhow::Result<()> { + self.r#impl + .set_white_balance_shift_blue(&mut self.ptp, value) + } + + pub fn get_white_balance_temperature(&mut self) -> anyhow::Result { + self.r#impl.get_white_balance_temperature(&mut self.ptp) + } + + pub fn set_white_balance_temperature( + &mut self, + value: FujiWhiteBalanceTemperature, + ) -> anyhow::Result<()> { + self.r#impl + .set_white_balance_temperature(&mut self.ptp, value) } pub fn get_color_chrome_effect(&mut self) -> anyhow::Result { self.r#impl.get_color_chrome_effect(&mut self.ptp) } + pub fn set_color_chrome_effect(&mut self, value: FujiColorChromeEffect) -> anyhow::Result<()> { + self.r#impl.set_color_chrome_effect(&mut self.ptp, value) + } + pub fn get_color_chrome_fx_blue(&mut self) -> anyhow::Result { self.r#impl.get_color_chrome_fx_blue(&mut self.ptp) } + + pub fn set_color_chrome_fx_blue(&mut self, value: FujiColorChromeFXBlue) -> anyhow::Result<()> { + self.r#impl.set_color_chrome_fx_blue(&mut self.ptp, value) + } } impl Drop for Camera { @@ -370,10 +461,16 @@ pub trait CameraImpl { Ok(()) } - fn get_custom_setting_name(&self, ptp: &mut Ptp) -> anyhow::Result { + fn get_custom_setting_name(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingName)?; - let name = String::try_from_ptp(&bytes)?; - Ok(name) + let result = FujiCustomSettingName::try_from_ptp(&bytes)?; + Ok(result) + } + + fn set_custom_setting_name(&self, ptp: &mut Ptp, value: &str) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value(ptp, DevicePropCode::FujiStillCustomSettingName, &bytes)?; + Ok(()) } fn get_image_size(&self, ptp: &mut Ptp) -> anyhow::Result { @@ -382,30 +479,70 @@ pub trait CameraImpl { Ok(result) } + fn set_image_size(&self, ptp: &mut Ptp, value: FujiImageSize) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value(ptp, DevicePropCode::FujiStillCustomSettingImageSize, &bytes)?; + Ok(()) + } + fn get_image_quality(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingImageQuality)?; let result = FujiImageQuality::try_from_ptp(&bytes)?; Ok(result) } + fn set_image_quality(&self, ptp: &mut Ptp, value: FujiImageQuality) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingImageQuality, + &bytes, + )?; + Ok(()) + } + fn get_dynamic_range(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingDynamicRange)?; let result = FujiDynamicRange::try_from_ptp(&bytes)?; Ok(result) } + fn set_dynamic_range(&self, ptp: &mut Ptp, value: FujiDynamicRange) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingDynamicRange, + &bytes, + )?; + Ok(()) + } + fn get_dynamic_range_priority( &self, ptp: &mut Ptp, - ) -> anyhow::Result { + ) -> anyhow::Result { let bytes = self.get_prop_value( ptp, DevicePropCode::FujiStillCustomSettingDynamicRangePriority, )?; - let result = FujiStillDynamicRangePriority::try_from_ptp(&bytes)?; + let result = FujiDynamicRangePriority::try_from_ptp(&bytes)?; Ok(result) } + fn set_dynamic_range_priority( + &self, + ptp: &mut Ptp, + value: FujiDynamicRangePriority, + ) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingDynamicRangePriority, + &bytes, + )?; + Ok(()) + } + fn get_film_simulation(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingFilmSimulation)?; @@ -413,24 +550,60 @@ pub trait CameraImpl { Ok(result) } + fn set_film_simulation(&self, ptp: &mut Ptp, value: FujiFilmSimulation) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingFilmSimulation, + &bytes, + )?; + Ok(()) + } + fn get_grain_effect(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingGrainEffect)?; let result = FujiGrainEffect::try_from_ptp(&bytes)?; Ok(result) } + fn set_grain_effect(&self, ptp: &mut Ptp, value: FujiGrainEffect) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingGrainEffect, + &bytes, + )?; + Ok(()) + } + fn get_white_balance(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingWhiteBalance)?; let result = FujiWhiteBalance::try_from_ptp(&bytes)?; Ok(result) } + fn set_white_balance(&self, ptp: &mut Ptp, value: FujiWhiteBalance) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingWhiteBalance, + &bytes, + )?; + Ok(()) + } + fn get_high_iso_nr(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingHighISONR)?; let result = FujiHighISONR::try_from_ptp(&bytes)?; Ok(result) } + fn set_high_iso_nr(&self, ptp: &mut Ptp, value: FujiHighISONR) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value(ptp, DevicePropCode::FujiStillCustomSettingHighISONR, &bytes)?; + Ok(()) + } + fn get_highlight_tone(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingHighlightTone)?; @@ -438,45 +611,118 @@ pub trait CameraImpl { Ok(result) } + fn set_highlight_tone(&self, ptp: &mut Ptp, value: FujiHighlightTone) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingHighlightTone, + &bytes, + )?; + Ok(()) + } + fn get_shadow_tone(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingShadowTone)?; let result = FujiShadowTone::try_from_ptp(&bytes)?; Ok(result) } + fn set_shadow_tone(&self, ptp: &mut Ptp, value: FujiShadowTone) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingShadowTone, + &bytes, + )?; + Ok(()) + } + fn get_color(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColor)?; let result = FujiColor::try_from_ptp(&bytes)?; Ok(result) } + fn set_color(&self, ptp: &mut Ptp, value: FujiColor) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColor, &bytes)?; + Ok(()) + } + fn get_sharpness(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingSharpness)?; let result = FujiSharpness::try_from_ptp(&bytes)?; Ok(result) } + fn set_sharpness(&self, ptp: &mut Ptp, value: FujiSharpness) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value(ptp, DevicePropCode::FujiStillCustomSettingSharpness, &bytes)?; + Ok(()) + } + fn get_clarity(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingClarity)?; let result = FujiClarity::try_from_ptp(&bytes)?; Ok(result) } - fn get_wb_shift_red(&self, ptp: &mut Ptp) -> anyhow::Result { - let bytes = - self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingWhiteBalanceRed)?; + fn set_clarity(&self, ptp: &mut Ptp, value: FujiClarity) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value(ptp, DevicePropCode::FujiStillCustomSettingClarity, &bytes)?; + Ok(()) + } + + fn get_white_balance_shift_red(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingWhiteBalanceShiftRed, + )?; let result = FujiWhiteBalanceShift::try_from_ptp(&bytes)?; Ok(result) } - fn get_wb_shift_blue(&self, ptp: &mut Ptp) -> anyhow::Result { - let bytes = - self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingWhiteBalanceBlue)?; + fn set_white_balance_shift_red( + &self, + ptp: &mut Ptp, + value: FujiWhiteBalanceShift, + ) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingWhiteBalanceShiftRed, + &bytes, + )?; + Ok(()) + } + + fn get_white_balance_shift_blue(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingWhiteBalanceShiftBlue, + )?; let result = FujiWhiteBalanceShift::try_from_ptp(&bytes)?; Ok(result) } - fn get_wb_temperature(&self, ptp: &mut Ptp) -> anyhow::Result { + fn set_white_balance_shift_blue( + &self, + ptp: &mut Ptp, + value: FujiWhiteBalanceShift, + ) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingWhiteBalanceShiftBlue, + &bytes, + )?; + Ok(()) + } + + fn get_white_balance_temperature( + &self, + ptp: &mut Ptp, + ) -> anyhow::Result { let bytes = self.get_prop_value( ptp, DevicePropCode::FujiStillCustomSettingWhiteBalanceTemperature, @@ -485,6 +731,20 @@ pub trait CameraImpl { Ok(result) } + fn set_white_balance_temperature( + &self, + ptp: &mut Ptp, + value: FujiWhiteBalanceTemperature, + ) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingWhiteBalanceTemperature, + &bytes, + )?; + Ok(()) + } + fn get_color_chrome_effect(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColorChromeEffect)?; @@ -492,10 +752,38 @@ pub trait CameraImpl { Ok(result) } + fn set_color_chrome_effect( + &self, + ptp: &mut Ptp, + value: FujiColorChromeEffect, + ) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingColorChromeEffect, + &bytes, + )?; + Ok(()) + } + fn get_color_chrome_fx_blue(&self, ptp: &mut Ptp) -> anyhow::Result { let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColorChromeFXBlue)?; let result = FujiColorChromeFXBlue::try_from_ptp(&bytes)?; Ok(result) } + + fn set_color_chrome_fx_blue( + &self, + ptp: &mut Ptp, + value: FujiColorChromeFXBlue, + ) -> anyhow::Result<()> { + let bytes = value.try_into_ptp()?; + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingColorChromeFXBlue, + &bytes, + )?; + Ok(()) + } } diff --git a/src/camera/ptp/hex.rs b/src/camera/ptp/hex.rs index de96a1e..c5982bb 100644 --- a/src/camera/ptp/hex.rs +++ b/src/camera/ptp/hex.rs @@ -1,6 +1,7 @@ use std::{ fmt, io::{self, Cursor}, + ops::{Deref, DerefMut}, str::FromStr, }; @@ -146,8 +147,8 @@ pub enum DevicePropCode { FujiStillCustomSettingColorChromeFXBlue = 0xD197, // TODO: 0xD198 All 1s FujiStillCustomSettingWhiteBalance = 0xD199, - FujiStillCustomSettingWhiteBalanceRed = 0xD19A, - FujiStillCustomSettingWhiteBalanceBlue = 0xD19B, + FujiStillCustomSettingWhiteBalanceShiftRed = 0xD19A, + FujiStillCustomSettingWhiteBalanceShiftBlue = 0xD19B, FujiStillCustomSettingWhiteBalanceTemperature = 0xD19C, FujiStillCustomSettingHighlightTone = 0xD19D, FujiStillCustomSettingShadowTone = 0xD19E, @@ -181,7 +182,7 @@ where } } - println!("{}", best_score); + println!("{best_score}"); if best_score <= SIMILARITY_THRESHOLD { best_match } else { @@ -231,7 +232,7 @@ impl Serialize for FujiCustomSetting { where S: Serializer, { - serializer.serialize_u16(*self as u16) + serializer.serialize_u16((*self).into()) } } @@ -256,6 +257,52 @@ impl FromStr for FujiCustomSetting { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, PtpSerialize, PtpDeserialize)] +pub struct FujiCustomSettingName(String); + +impl FujiCustomSettingName { + pub const MAX_LEN: usize = 25; +} + +impl Deref for FujiCustomSettingName { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FujiCustomSettingName { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl TryFrom for FujiCustomSettingName { + type Error = anyhow::Error; + fn try_from(value: String) -> Result { + if value.len() > Self::MAX_LEN { + bail!("Value '{}' exceeds max length of {}", value, Self::MAX_LEN); + } + Ok(Self(value)) + } +} + +impl FromStr for FujiCustomSettingName { + type Err = anyhow::Error; + fn from_str(s: &str) -> 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, @@ -329,8 +376,8 @@ impl FromStr for FujiImageSize { _ => {} } - let resolution_input = s.replace(' ', "x").replace("by", "x"); - if let Some((w_str, h_str)) = resolution_input.split_once('x') + 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) { @@ -489,14 +536,14 @@ impl FromStr for FujiDynamicRange { PtpDeserialize, EnumIter, )] -pub enum FujiStillDynamicRangePriority { +pub enum FujiDynamicRangePriority { Auto = 0x8000, Strong = 0x2, Weak = 0x1, Off = 0x0, } -impl fmt::Display for FujiStillDynamicRangePriority { +impl fmt::Display for FujiDynamicRangePriority { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Auto => write!(f, "Auto"), @@ -507,7 +554,7 @@ impl fmt::Display for FujiStillDynamicRangePriority { } } -impl FromStr for FujiStillDynamicRangePriority { +impl FromStr for FujiDynamicRangePriority { type Err = anyhow::Error; fn from_str(s: &str) -> anyhow::Result { @@ -705,10 +752,10 @@ impl FromStr for FujiGrainEffect { .replace(['+', '-', ',', ' '].as_ref(), ""); match input.as_str() { - "stronglarge" => return Ok(Self::StrongLarge), - "weaklarge" => return Ok(Self::WeakLarge), - "strongsmall" => return Ok(Self::StrongSmall), - "weaksmall" => return Ok(Self::WeakSmall), + "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), _ => {} } @@ -999,10 +1046,22 @@ macro_rules! define_fuji_i16 { pub struct $name(i16); impl $name { - pub const MIN: i16 = $min; - pub const MAX: i16 = $max; - pub const STEP: i16 = $step; - pub const SCALE: f32 = $scale; + pub const MIN: f32 = $min; + pub const MAX: f32 = $max; + pub const STEP: f32 = $step; + + pub const SCALE: f32 = $scale as f32; + + #[allow(clippy::cast_possible_truncation)] + pub const RAW_MIN: i16 = ($min * $scale as f32) as i16; + #[allow(clippy::cast_possible_truncation)] + pub const RAW_MAX: i16 = ($max * $scale as f32) as i16; + #[allow(clippy::cast_possible_truncation)] + pub const RAW_STEP: i16 = ($step * $scale as f32) as i16; + + pub const unsafe fn new_unchecked(value: i16) -> Self { + Self(value) + } } impl std::ops::Deref for $name { @@ -1022,12 +1081,12 @@ macro_rules! define_fuji_i16 { type Error = anyhow::Error; fn try_from(value: i16) -> anyhow::Result { - if !(Self::MIN..=Self::MAX).contains(&value) { + if !(Self::RAW_MIN..=Self::RAW_MAX).contains(&value) { anyhow::bail!("Value {} is out of range", value); } #[allow(clippy::modulo_one)] - if (value - Self::MIN) % Self::STEP != 0 { - anyhow::bail!("Value {} is not aligned to step {}", value, Self::STEP); + if (value - Self::RAW_MIN) % Self::RAW_STEP != 0 { + anyhow::bail!("Value {} is not aligned to step {}", value, Self::RAW_STEP); } Ok(Self(value)) } @@ -1035,18 +1094,8 @@ macro_rules! define_fuji_i16 { impl std::fmt::Display for $name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if (Self::SCALE - 1.0).abs() < f32::EPSILON { - write!(f, "{}", self.0) - } else { - let val = f32::from(self.0) * Self::SCALE; - if val.fract().abs() < f32::EPSILON { - #[allow(clippy::cast_possible_truncation)] - let val = val as i32; - write!(f, "{}", val as i32) - } else { - write!(f, "{:.1}", val) - } - } + let value = (f32::from(self.0) / Self::SCALE); + write!(f, "{}", value) } } @@ -1061,9 +1110,18 @@ macro_rules! define_fuji_i16 { .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; - Self::try_from(raw) + let raw = (input * Self::SCALE).round() as i16; + + unsafe { Ok(Self::new_unchecked(raw)) } } } @@ -1072,24 +1130,20 @@ macro_rules! define_fuji_i16 { where S: serde::Serializer, { - let val = f32::from(self.0) * Self::SCALE; - if (val.fract().abs() < f32::EPSILON) { - serializer.serialize_i32(val as i32) - } else { - serializer.serialize_f32(val) - } + let val = f32::from(self.0) / Self::SCALE; + serializer.serialize_f32(val) } } }; } -define_fuji_i16!(FujiWhiteBalanceShift, -9, 9, 1, 1.0); -define_fuji_i16!(FujiWhiteBalanceTemperature, 2500, 10000, 10, 1.0); -define_fuji_i16!(FujiHighlightTone, -40, 20, 5, 0.1); -define_fuji_i16!(FujiShadowTone, -20, 40, 5, 0.1); -define_fuji_i16!(FujiColor, -40, 40, 10, 0.1); -define_fuji_i16!(FujiSharpness, -40, 40, 10, 0.1); -define_fuji_i16!(FujiClarity, -50, 50, 10, 0.1); +define_fuji_i16!(FujiWhiteBalanceShift, -9.0, 9.0, 1.0, 1i16); +define_fuji_i16!(FujiWhiteBalanceTemperature, 2500.0, 10000.0, 10.0, 1i16); +define_fuji_i16!(FujiHighlightTone, -2.0, 4.0, 0.5, 10i16); +define_fuji_i16!(FujiShadowTone, -2.0, 4.0, 0.5, 10i16); +define_fuji_i16!(FujiColor, -4.0, 4.0, 1.0, 10i16); +define_fuji_i16!(FujiSharpness, -4.0, 4.0, 1.0, 10i16); +define_fuji_i16!(FujiClarity, -5.0, 5.0, 1.0, 10i16); #[repr(u16)] #[derive( diff --git a/src/cli/common/film.rs b/src/cli/common/film.rs index 27b92bc..0c9c312 100644 --- a/src/cli/common/film.rs +++ b/src/cli/common/film.rs @@ -1,87 +1,87 @@ use clap::Args; use crate::camera::ptp::hex::{ - FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiDynamicRange, - FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality, - FujiImageSize, FujiShadowTone, FujiSharpness, FujiStillDynamicRangePriority, FujiWhiteBalance, - FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, + FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiCustomSettingName, + FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, + FujiHighlightTone, FujiImageQuality, FujiImageSize, FujiShadowTone, FujiSharpness, + FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, }; #[derive(Args, Debug)] pub struct FilmSimulationOptions { /// The name of the slot #[clap(long)] - pub name: Option, + pub name: Option, /// The Fujifilm film simulation to use #[clap(long)] pub simulation: Option, /// The output image resolution - #[clap(long, alias = "size")] - pub resolution: Option, + #[clap(long)] + pub size: Option, /// The output image quality (JPEG compression level) - #[clap(long, value_parser)] + #[clap(long)] pub quality: Option, /// Highlight Tone - #[clap(long, value_parser)] - pub highlights: Option, + #[clap(long, allow_hyphen_values(true))] + pub highlight: Option, /// Shadow Tone - #[clap(long, value_parser)] - pub shadows: Option, + #[clap(long, allow_hyphen_values(true))] + pub shadow: Option, /// Color - #[clap(long, value_parser)] + #[clap(long, allow_hyphen_values(true))] pub color: Option, /// Sharpness - #[clap(long, value_parser)] + #[clap(long, allow_hyphen_values(true))] pub sharpness: Option, /// Clarity - #[clap(long, value_parser)] + #[clap(long, allow_hyphen_values(true))] pub clarity: Option, /// White Balance - #[clap(long, value_parser)] + #[clap(long)] pub white_balance: Option, /// White Balance Shift Red - #[clap(long, value_parser)] + #[clap(long, allow_hyphen_values(true))] pub white_balance_shift_red: Option, /// White Balance Shift Blue - #[clap(long, value_parser)] + #[clap(long, allow_hyphen_values(true))] pub white_balance_shift_blue: Option, /// White Balance Temperature (Only used if WB is set to 'Temperature') - #[clap(long, value_parser)] + #[clap(long)] pub white_balance_temperature: Option, /// Dynamic Range - #[clap(long, value_parser)] + #[clap(long)] pub dynamic_range: Option, /// Dynamic Range Priority - #[clap(long, value_parser)] - pub dynamic_ranga_priority: Option, + #[clap(long)] + pub dynamic_range_priority: Option, /// High ISO Noise Reduction - #[clap(long, value_parser)] + #[clap(long, allow_hyphen_values(true))] pub noise_reduction: Option, /// Grain Effect - #[clap(long, value_parser)] + #[clap(long)] pub grain: Option, /// Color Chrome Effect - #[clap(long, value_parser)] + #[clap(long)] pub color_chrome_effect: Option, /// Color Chrome FX Blue - #[clap(long, value_parser)] + #[clap(long)] pub color_chrome_fx_blue: Option, } diff --git a/src/cli/simulation/mod.rs b/src/cli/simulation/mod.rs index 298b642..92fa5dd 100644 --- a/src/cli/simulation/mod.rs +++ b/src/cli/simulation/mod.rs @@ -3,9 +3,9 @@ use std::fmt; use crate::{ camera::ptp::hex::{ FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiCustomSetting, - FujiDynamicRange, FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, - FujiImageQuality, FujiImageSize, FujiShadowTone, FujiSharpness, - FujiStillDynamicRangePriority, FujiWhiteBalance, FujiWhiteBalanceShift, + FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, + FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize, + FujiShadowTone, FujiSharpness, FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, }, usb, @@ -16,6 +16,7 @@ use super::common::{ film::FilmSimulationOptions, }; use clap::Subcommand; +use log::warn; use serde::Serialize; use strum::IntoEnumIterator; @@ -67,7 +68,7 @@ pub enum SimulationCmd { #[serde(rename_all = "camelCase")] pub struct CustomSettingRepr { pub slot: FujiCustomSetting, - pub name: String, + pub name: FujiCustomSettingName, } fn handle_list(json: bool, device_id: Option<&str>) -> anyhow::Result<()> { @@ -97,12 +98,12 @@ fn handle_list(json: bool, device_id: Option<&str>) -> anyhow::Result<()> { #[serde(rename_all = "camelCase")] pub struct FilmSimulationRepr { pub slot: FujiCustomSetting, - pub name: String, + pub name: FujiCustomSettingName, pub simulation: FujiFilmSimulation, - pub resolution: FujiImageSize, + pub size: FujiImageSize, pub quality: FujiImageQuality, - pub highlights: FujiHighlightTone, - pub shadows: FujiShadowTone, + pub highlight: FujiHighlightTone, + pub shadow: FujiShadowTone, pub color: FujiColor, pub sharpness: FujiSharpness, pub clarity: FujiClarity, @@ -111,7 +112,7 @@ pub struct FilmSimulationRepr { pub white_balance_shift_blue: FujiWhiteBalanceShift, pub white_balance_temperature: FujiWhiteBalanceTemperature, pub dynamic_range: FujiDynamicRange, - pub dynamic_range_priority: FujiStillDynamicRangePriority, + pub dynamic_range_priority: FujiDynamicRangePriority, pub noise_reduction: FujiHighISONR, pub grain: FujiGrainEffect, pub color_chrome_effect: FujiColorChromeEffect, @@ -123,10 +124,10 @@ impl fmt::Display for FilmSimulationRepr { writeln!(f, "Slot: {}", self.slot)?; writeln!(f, "Name: {}", self.name)?; writeln!(f, "Simulation: {}", self.simulation)?; - writeln!(f, "Resolution: {}", self.resolution)?; + writeln!(f, "Size: {}", self.size)?; writeln!(f, "Quality: {}", self.quality)?; - writeln!(f, "Highlights: {}", self.highlights)?; - writeln!(f, "Shadows: {}", self.shadows)?; + writeln!(f, "Highlights: {}", self.highlight)?; + writeln!(f, "Shadows: {}", self.shadow)?; writeln!(f, "Color: {}", self.color)?; writeln!(f, "Sharpness: {}", self.sharpness)?; writeln!(f, "Clarity: {}", self.clarity)?; @@ -136,7 +137,11 @@ impl fmt::Display for FilmSimulationRepr { "White Balance Shift (R/B): {} / {}", self.white_balance_shift_red, self.white_balance_shift_blue )?; - writeln!(f, "White Balance Temperature: {}K", self.white_balance_temperature)?; + writeln!( + f, + "White Balance Temperature: {}K", + self.white_balance_temperature + )?; writeln!(f, "Dynamic Range: {}", self.dynamic_range)?; writeln!(f, "Dynamic Range Priority: {}", self.dynamic_range_priority)?; writeln!(f, "Noise Reduction: {}", self.noise_reduction)?; @@ -154,17 +159,17 @@ fn handle_get(json: bool, device_id: Option<&str>, slot: FujiCustomSetting) -> a slot, name: camera.get_custom_setting_name()?, simulation: camera.get_film_simulation()?, - resolution: camera.get_image_size()?, + size: camera.get_image_size()?, quality: camera.get_image_quality()?, - highlights: camera.get_highlight_tone()?, - shadows: camera.get_shadow_tone()?, + highlight: camera.get_highlight_tone()?, + shadow: camera.get_shadow_tone()?, color: camera.get_color()?, sharpness: camera.get_sharpness()?, clarity: camera.get_clarity()?, white_balance: camera.get_white_balance()?, - white_balance_shift_red: camera.get_wb_shift_red()?, - white_balance_shift_blue: camera.get_wb_shift_blue()?, - white_balance_temperature: camera.get_wb_temperature()?, + white_balance_shift_red: camera.get_white_balance_shift_red()?, + white_balance_shift_blue: camera.get_white_balance_shift_blue()?, + white_balance_temperature: camera.get_white_balance_temperature()?, dynamic_range: camera.get_dynamic_range()?, dynamic_range_priority: camera.get_dynamic_range_priority()?, noise_reduction: camera.get_high_iso_nr()?, @@ -183,11 +188,118 @@ fn handle_get(json: bool, device_id: Option<&str>, slot: FujiCustomSetting) -> a } fn handle_set( - _device_id: Option<&str>, - _slot: FujiCustomSetting, - _opts: &FilmSimulationOptions, + device_id: Option<&str>, + slot: FujiCustomSetting, + options: &FilmSimulationOptions, ) -> anyhow::Result<()> { - todo!(); + let mut camera = usb::get_camera(device_id)?; + camera.set_active_custom_setting(slot)?; + + // General + if let Some(name) = &options.name { + camera.set_custom_setting_name(name)?; + } + + if let Some(size) = &options.size { + camera.set_image_size(*size)?; + } + + if let Some(quality) = &options.quality { + camera.set_image_quality(*quality)?; + } + + // Style + if let Some(simulation) = &options.simulation { + camera.set_film_simulation(*simulation)?; + } + + if let Some(color) = &options.color { + camera.set_color(*color)?; + } + + if let Some(sharpness) = &options.sharpness { + camera.set_sharpness(*sharpness)?; + } + + if let Some(clarity) = &options.clarity { + camera.set_clarity(*clarity)?; + } + + if let Some(noise_reduction) = &options.noise_reduction { + camera.set_high_iso_nr(*noise_reduction)?; + } + + if let Some(grain) = &options.grain { + camera.set_grain_effect(*grain)?; + } + + if let Some(color_chrome_effect) = &options.color_chrome_effect { + camera.set_color_chrome_effect(*color_chrome_effect)?; + } + + if let Some(color_chrome_fx_blue) = &options.color_chrome_fx_blue { + camera.set_color_chrome_fx_blue(*color_chrome_fx_blue)?; + } + + // White Balance + let white_balance = match &options.white_balance { + Some(white_balance) => { + camera.set_white_balance(*white_balance)?; + white_balance + } + None => &camera.get_white_balance()?, + }; + + if let Some(temperature) = &options.white_balance_temperature { + if *white_balance != FujiWhiteBalance::Temperature { + warn!("White Balance mode is not set to 'Temperature', refusing to set temperature") + } else { + camera.set_white_balance_temperature(*temperature)?; + } + } + + if let Some(shift_red) = &options.white_balance_shift_red { + camera.set_white_balance_shift_red(*shift_red)?; + } + + if let Some(shift_blue) = &options.white_balance_shift_blue { + camera.set_white_balance_shift_blue(*shift_blue)?; + } + + // Exposure + let dynamic_range_priority = match &options.dynamic_range_priority { + Some(dynamic_range_priority) => { + camera.set_dynamic_range_priority(*dynamic_range_priority)?; + dynamic_range_priority + } + None => &camera.get_dynamic_range_priority()?, + }; + + if let Some(dynamic_range) = &options.dynamic_range { + if *dynamic_range_priority != FujiDynamicRangePriority::Off { + warn!("Dynamic Range Priority is enabled, refusing to set dynamic range") + } else { + camera.set_dynamic_range(*dynamic_range)?; + } + } + + if let Some(highlights) = &options.highlight { + if *dynamic_range_priority != FujiDynamicRangePriority::Off { + warn!("Dynamic Range Priority is enabled, refusing to set highlight tone") + } else { + camera.set_highlight_tone(*highlights)?; + } + } + + if let Some(shadows) = &options.shadow { + if *dynamic_range_priority != FujiDynamicRangePriority::Off { + warn!("Dynamic Range Priority is enabled, refusing to set shadow tone") + } else { + camera.set_shadow_tone(*shadows)?; + } + } + + Ok(()) } fn handle_export( diff --git a/src/log.rs b/src/log.rs index 400d5a9..d39a5cc 100644 --- a/src/log.rs +++ b/src/log.rs @@ -14,7 +14,13 @@ pub fn init(verbose: u8) -> anyhow::Result<()> { _ => LevelFilter::Trace, }; - let encoder = Box::new(PatternEncoder::new("{d} {h({l})} {M}::{L} - {m}{n}")); + let pattern = if verbose > 0 { + "{d} {h({l})} {M}::{L} - {m}{n}" + } else { + "{h({l})} - {m}{n}" + }; + + let encoder = Box::new(PatternEncoder::new(pattern)); let console = ConsoleAppender::builder() .encoder(encoder)