Compare commits
17 Commits
0ef41cf4f8
...
main
Author | SHA1 | Date | |
---|---|---|---|
9e7c99e804
|
|||
7e8599fa61
|
|||
fb4610bdaa
|
|||
91d5d5b16b
|
|||
76ab55acd1
|
|||
e3e41999a6
|
|||
8120690caa
|
|||
6b0753b072
|
|||
0f5997042c
|
|||
a1668bb277
|
|||
7c43e0f7ab
|
|||
4825b699a6
|
|||
1f26a91dcd
|
|||
943f22c074
|
|||
2e9fb61762
|
|||
913ca06512
|
|||
e3720b8afb
|
10
.gitignore
vendored
10
.gitignore
vendored
@@ -16,3 +16,13 @@ target/
|
|||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
# ---> Nix
|
||||||
|
# Ignore build outputs from performing a nix-build or `nix build` command
|
||||||
|
result
|
||||||
|
result-*
|
||||||
|
|
||||||
|
# Ignore automatically generated direnv output
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
.cargo/
|
||||||
|
153
Cargo.lock
generated
153
Cargo.lock
generated
@@ -210,6 +210,17 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "erased-serde"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"typeid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -237,13 +248,21 @@ name = "fujicli"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"byteorder",
|
||||||
"clap",
|
"clap",
|
||||||
"libptp",
|
"erased-serde",
|
||||||
"log",
|
"log",
|
||||||
"log4rs",
|
"log4rs",
|
||||||
|
"num_enum",
|
||||||
|
"paste",
|
||||||
|
"ptp_cursor",
|
||||||
|
"ptp_macro",
|
||||||
"rusb",
|
"rusb",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"strsim",
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -338,17 +357,6 @@ version = "0.2.177"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libptp"
|
|
||||||
version = "0.6.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2e6b84822d9579c3adb36bcea61c396dc2596a95ca03a0ffd69636fc85ccc4e2"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
"log",
|
|
||||||
"rusb",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libusb1-sys"
|
name = "libusb1-sys"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -441,6 +449,28 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a"
|
||||||
|
dependencies = [
|
||||||
|
"num_enum_derive",
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum_derive"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -485,6 +515,12 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -500,6 +536,15 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
|
||||||
|
dependencies = [
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.101"
|
version = "1.0.101"
|
||||||
@@ -509,6 +554,24 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ptp_cursor"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ptp_macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"proc-macro2",
|
||||||
|
"ptp_cursor",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.41"
|
version = "1.0.41"
|
||||||
@@ -687,6 +750,27 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.106"
|
version = "2.0.106"
|
||||||
@@ -738,6 +822,42 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.23.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typeid"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typemap-ors"
|
name = "typemap-ors"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -1106,6 +1226,15 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
|
10
Cargo.toml
10
Cargo.toml
@@ -15,10 +15,18 @@ codegen-units = 1
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
|
byteorder = "1.5.0"
|
||||||
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
|
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
|
||||||
libptp = "0.6.5"
|
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
log4rs = "1.4.0"
|
log4rs = "1.4.0"
|
||||||
|
num_enum = "0.7.4"
|
||||||
rusb = "0.9.4"
|
rusb = "0.9.4"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
|
strsim = "0.11.1"
|
||||||
|
ptp_macro = { path = "crates/ptp/macro" }
|
||||||
|
ptp_cursor = { path = "crates/ptp/cursor" }
|
||||||
|
strum = { version = "0.27.2", features = ["strum_macros"] }
|
||||||
|
strum_macros = "0.27.2"
|
||||||
|
paste = "1.0.15"
|
||||||
|
erased-serde = "0.4.8"
|
||||||
|
20
README.md
20
README.md
@@ -1,3 +1,21 @@
|
|||||||
# fujicli
|
# fujicli
|
||||||
|
|
||||||
A CLI that performs on-device image rendering similar to Fujifilm X RAW Studio
|
A CLI to manage Fujifilm devices, simulations, backups, and rendering.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This tool has only been tested with the **Fujifilm X-T5**, as it is the sole camera I own. While the underlying PTP commands may be compatible with other models, **compatibility is not guaranteed**.
|
||||||
|
|
||||||
|
**Use this software at your own risk.** I am not responsible for any damage, loss of data, or other adverse outcomes - physical or psychological - to your camera or equipment resulting from the use of this program.
|
||||||
|
|
||||||
|
This project is currently under heavy development. Contributions are welcome. If you own a different Fujifilm camera, testing and reporting compatibility is highly appreciated.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
This project builds upon the following fantastic reverse-engineering efforts:
|
||||||
|
|
||||||
|
* [fujihack](https://github.com/fujihack/fujihack)
|
||||||
|
* [fudge](https://github.com/petabyt/fudge)
|
||||||
|
* [libpict](https://github.com/petabyt/libpict)
|
||||||
|
* [fp](https://github.com/petabyt/fp)
|
||||||
|
* [libgphoto2](https://github.com/gphoto/libgphoto2)
|
||||||
|
16
crates/ptp/cursor/Cargo.lock
generated
Normal file
16
crates/ptp/cursor/Cargo.lock
generated
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ptp_cursor"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
7
crates/ptp/cursor/Cargo.toml
Normal file
7
crates/ptp/cursor/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "ptp_cursor"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
byteorder = { version = "1.5.0" }
|
286
crates/ptp/cursor/src/lib.rs
Normal file
286
crates/ptp/cursor/src/lib.rs
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
#![allow(clippy::redundant_closure_for_method_calls)]
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use std::io::{self, Cursor};
|
||||||
|
|
||||||
|
pub trait Read: ReadBytesExt {
|
||||||
|
fn read_ptp_u8(&mut self) -> io::Result<u8> {
|
||||||
|
self.read_u8()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i8(&mut self) -> io::Result<i8> {
|
||||||
|
self.read_i8()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u16(&mut self) -> io::Result<u16> {
|
||||||
|
self.read_u16::<LittleEndian>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i16(&mut self) -> io::Result<i16> {
|
||||||
|
self.read_i16::<LittleEndian>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u32(&mut self) -> io::Result<u32> {
|
||||||
|
self.read_u32::<LittleEndian>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i32(&mut self) -> io::Result<i32> {
|
||||||
|
self.read_i32::<LittleEndian>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u64(&mut self) -> io::Result<u64> {
|
||||||
|
self.read_u64::<LittleEndian>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i64(&mut self) -> io::Result<i64> {
|
||||||
|
self.read_i64::<LittleEndian>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_vec<T, F>(&mut self, func: F) -> io::Result<Vec<T>>
|
||||||
|
where
|
||||||
|
F: Fn(&mut Self) -> io::Result<T>,
|
||||||
|
{
|
||||||
|
let len = self.read_u32::<LittleEndian>()? as usize;
|
||||||
|
(0..len).map(|_| func(self)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u8_vec(&mut self) -> io::Result<Vec<u8>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_u8())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i8_vec(&mut self) -> io::Result<Vec<i8>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_i8())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u16_vec(&mut self) -> io::Result<Vec<u16>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_u16())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i16_vec(&mut self) -> io::Result<Vec<i16>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_i16())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u32_vec(&mut self) -> io::Result<Vec<u32>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_u32())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i32_vec(&mut self) -> io::Result<Vec<i32>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_i32())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u64_vec(&mut self) -> io::Result<Vec<u64>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_u64())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i64_vec(&mut self) -> io::Result<Vec<i64>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_i64())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_str(&mut self) -> io::Result<String> {
|
||||||
|
let len = self.read_u8()?;
|
||||||
|
if len == 0 {
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: Vec<u16> = (0..(len - 1))
|
||||||
|
.map(|_| self.read_u16::<LittleEndian>())
|
||||||
|
.collect::<io::Result<_>>()?;
|
||||||
|
self.read_u16::<LittleEndian>()?;
|
||||||
|
String::from_utf16(&data)
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_end(&mut self) -> io::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]>> Read for Cursor<T> {
|
||||||
|
fn expect_end(&mut self) -> io::Result<()> {
|
||||||
|
let len = self.get_ref().as_ref().len();
|
||||||
|
if len as u64 != self.position() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::UnexpectedEof,
|
||||||
|
format!(
|
||||||
|
"Buffer contained {} bytes, expected {} bytes",
|
||||||
|
len,
|
||||||
|
self.position()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Write: WriteBytesExt {
|
||||||
|
fn write_ptp_u8(&mut self, v: &u8) -> io::Result<()> {
|
||||||
|
self.write_u8(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_i8(&mut self, v: &i8) -> io::Result<()> {
|
||||||
|
self.write_i8(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_u16(&mut self, v: &u16) -> io::Result<()> {
|
||||||
|
self.write_u16::<LittleEndian>(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_i16(&mut self, v: &i16) -> io::Result<()> {
|
||||||
|
self.write_i16::<LittleEndian>(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_u32(&mut self, v: &u32) -> io::Result<()> {
|
||||||
|
self.write_u32::<LittleEndian>(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_i32(&mut self, v: &i32) -> io::Result<()> {
|
||||||
|
self.write_i32::<LittleEndian>(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_u64(&mut self, v: &u64) -> io::Result<()> {
|
||||||
|
self.write_u64::<LittleEndian>(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_i64(&mut self, v: &i64) -> io::Result<()> {
|
||||||
|
self.write_i64::<LittleEndian>(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_vec<T, F>(&mut self, vec: &[T], func: F) -> io::Result<()>
|
||||||
|
where
|
||||||
|
F: Fn(&mut Self, &T) -> io::Result<()>,
|
||||||
|
{
|
||||||
|
self.write_u32::<LittleEndian>(vec.len() as u32)?;
|
||||||
|
for v in vec {
|
||||||
|
func(self, v)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_u8_vec(&mut self, vec: &[u8]) -> io::Result<()> {
|
||||||
|
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_u8(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_i8_vec(&mut self, vec: &[i8]) -> io::Result<()> {
|
||||||
|
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_i8(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_u16_vec(&mut self, vec: &[u16]) -> io::Result<()> {
|
||||||
|
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_u16(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_i16_vec(&mut self, vec: &[i16]) -> io::Result<()> {
|
||||||
|
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_i16(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_u32_vec(&mut self, vec: &[u32]) -> io::Result<()> {
|
||||||
|
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_u32(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_i32_vec(&mut self, vec: &[i32]) -> io::Result<()> {
|
||||||
|
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_i32(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_u64_vec(&mut self, vec: &[u64]) -> io::Result<()> {
|
||||||
|
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_u64(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_i64_vec(&mut self, vec: &[i64]) -> io::Result<()> {
|
||||||
|
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_i64(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ptp_str(&mut self, s: &str) -> io::Result<()> {
|
||||||
|
if s.is_empty() {
|
||||||
|
return self.write_u8(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let utf16: Vec<u16> = s.encode_utf16().collect();
|
||||||
|
self.write_u8((utf16.len() + 1) as u8)?;
|
||||||
|
for c in utf16 {
|
||||||
|
self.write_u16::<LittleEndian>(c)?;
|
||||||
|
}
|
||||||
|
self.write_u16::<LittleEndian>(0)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for Vec<u8> {}
|
||||||
|
|
||||||
|
pub trait PtpSerialize: Sized {
|
||||||
|
fn try_into_ptp(&self) -> io::Result<Vec<u8>>;
|
||||||
|
|
||||||
|
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> io::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PtpDeserialize: Sized {
|
||||||
|
fn try_from_ptp(buf: &[u8]) -> io::Result<Self>;
|
||||||
|
|
||||||
|
fn try_read_ptp<R: Read>(cur: &mut R) -> io::Result<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! ptp_ser {
|
||||||
|
($ty:ty, $read_fn:ident, $write_fn:ident) => {
|
||||||
|
impl PtpSerialize for $ty {
|
||||||
|
fn try_into_ptp(&self) -> io::Result<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
self.try_write_ptp(&mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> io::Result<()> {
|
||||||
|
buf.$write_fn(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! ptp_de {
|
||||||
|
($ty:ty, $read_fn:ident, $write_fn:ident) => {
|
||||||
|
impl PtpDeserialize for $ty {
|
||||||
|
fn try_from_ptp(buf: &[u8]) -> io::Result<Self> {
|
||||||
|
let mut cur = Cursor::new(buf);
|
||||||
|
let val = Self::try_read_ptp(&mut cur)?;
|
||||||
|
cur.expect_end()?;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read_ptp<R: Read>(cur: &mut R) -> io::Result<Self> {
|
||||||
|
cur.$read_fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ptp_ser!(u8, read_ptp_u8, write_ptp_u8);
|
||||||
|
ptp_de!(u8, read_ptp_u8, write_ptp_u8);
|
||||||
|
ptp_ser!(i8, read_ptp_i8, write_ptp_i8);
|
||||||
|
ptp_de!(i8, read_ptp_i8, write_ptp_i8);
|
||||||
|
ptp_ser!(u16, read_ptp_u16, write_ptp_u16);
|
||||||
|
ptp_de!(u16, read_ptp_u16, write_ptp_u16);
|
||||||
|
ptp_ser!(i16, read_ptp_i16, write_ptp_i16);
|
||||||
|
ptp_de!(i16, read_ptp_i16, write_ptp_i16);
|
||||||
|
ptp_ser!(u32, read_ptp_u32, write_ptp_u32);
|
||||||
|
ptp_de!(u32, read_ptp_u32, write_ptp_u32);
|
||||||
|
ptp_ser!(i32, read_ptp_i32, write_ptp_i32);
|
||||||
|
ptp_de!(i32, read_ptp_i32, write_ptp_i32);
|
||||||
|
ptp_ser!(u64, read_ptp_u64, write_ptp_u64);
|
||||||
|
ptp_de!(u64, read_ptp_u64, write_ptp_u64);
|
||||||
|
ptp_ser!(i64, read_ptp_i64, write_ptp_i64);
|
||||||
|
ptp_de!(i64, read_ptp_i64, write_ptp_i64);
|
||||||
|
ptp_ser!(&str, read_ptp_str, write_ptp_str);
|
||||||
|
ptp_ser!(String, read_ptp_str, write_ptp_str);
|
||||||
|
ptp_de!(String, read_ptp_str, write_ptp_str);
|
||||||
|
ptp_ser!(Vec<u8>, read_ptp_u8_vec, write_ptp_u8_vec);
|
||||||
|
ptp_de!(Vec<u8>, read_ptp_u8_vec, write_ptp_u8_vec);
|
||||||
|
ptp_ser!(Vec<i8>, read_ptp_i8_vec, write_ptp_i8_vec);
|
||||||
|
ptp_de!(Vec<i8>, read_ptp_i8_vec, write_ptp_i8_vec);
|
||||||
|
ptp_ser!(Vec<u16>, read_ptp_u16_vec, write_ptp_u16_vec);
|
||||||
|
ptp_de!(Vec<u16>, read_ptp_u16_vec, write_ptp_u16_vec);
|
||||||
|
ptp_ser!(Vec<i16>, read_ptp_i16_vec, write_ptp_i16_vec);
|
||||||
|
ptp_de!(Vec<i16>, read_ptp_i16_vec, write_ptp_i16_vec);
|
||||||
|
ptp_ser!(Vec<u32>, read_ptp_u32_vec, write_ptp_u32_vec);
|
||||||
|
ptp_de!(Vec<u32>, read_ptp_u32_vec, write_ptp_u32_vec);
|
||||||
|
ptp_ser!(Vec<i32>, read_ptp_i32_vec, write_ptp_i32_vec);
|
||||||
|
ptp_de!(Vec<i32>, read_ptp_i32_vec, write_ptp_i32_vec);
|
||||||
|
ptp_ser!(Vec<u64>, read_ptp_u64_vec, write_ptp_u64_vec);
|
||||||
|
ptp_de!(Vec<u64>, read_ptp_u64_vec, write_ptp_u64_vec);
|
||||||
|
ptp_ser!(Vec<i64>, read_ptp_i64_vec, write_ptp_i64_vec);
|
||||||
|
ptp_de!(Vec<i64>, read_ptp_i64_vec, write_ptp_i64_vec);
|
62
crates/ptp/macro/Cargo.lock
generated
Normal file
62
crates/ptp/macro/Cargo.lock
generated
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ptp_cursor"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ptp_macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"proc-macro2",
|
||||||
|
"ptp_cursor",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
14
crates/ptp/macro/Cargo.toml
Normal file
14
crates/ptp/macro/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "ptp_macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
byteorder = "1.5.0"
|
||||||
|
proc-macro2 = "1.0.101"
|
||||||
|
quote = "1.0.41"
|
||||||
|
syn = { version = "2.0.106", features = ["full"] }
|
||||||
|
ptp_cursor = { path = "../cursor" }
|
253
crates/ptp/macro/src/lib.rs
Normal file
253
crates/ptp/macro/src/lib.rs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{Data, DeriveInput, Expr, Fields, parse_macro_input, punctuated::Punctuated};
|
||||||
|
|
||||||
|
#[proc_macro_derive(PtpSerialize)]
|
||||||
|
pub fn derive_ptp_serialize(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
let name = &input.ident;
|
||||||
|
|
||||||
|
let expanded = match &input.data {
|
||||||
|
Data::Struct(s) => match &s.fields {
|
||||||
|
Fields::Named(named) => {
|
||||||
|
let fields = &named.named;
|
||||||
|
|
||||||
|
let write_fields = fields.iter().map(|f| {
|
||||||
|
let name = &f.ident;
|
||||||
|
quote! {
|
||||||
|
self.#name.try_write_ptp(buf)?;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl ptp_cursor::PtpSerialize for #name {
|
||||||
|
fn try_into_ptp(&self) -> std::io::Result<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
self.try_write_ptp(&mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> std::io::Result<()> {
|
||||||
|
#(#write_fields)*
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Fields::Unnamed(unnamed) => {
|
||||||
|
let fields = &unnamed.unnamed;
|
||||||
|
|
||||||
|
let write_fields = (0..fields.len()).map(|i| {
|
||||||
|
let idx = syn::Index::from(i);
|
||||||
|
quote! { self.#idx.try_write_ptp(buf)?; }
|
||||||
|
});
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl ptp_cursor::PtpSerialize for #name {
|
||||||
|
fn try_into_ptp(&self) -> std::io::Result<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
self.try_write_ptp(&mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> std::io::Result<()> {
|
||||||
|
#(#write_fields)*
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Fields::Unit => {
|
||||||
|
quote! {
|
||||||
|
impl ptp_cursor::PtpSerialize for #name {
|
||||||
|
fn try_into_ptp(&self) -> std::io::Result<Vec<u8>> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_write_ptp(&self, _buf: &mut Vec<u8>) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Data::Enum(_) => {
|
||||||
|
let repr_ty = input
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.find_map(|attr| {
|
||||||
|
if attr.path().is_ident("repr") {
|
||||||
|
attr.parse_args_with(
|
||||||
|
Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.and_then(|args| args.into_iter().next())
|
||||||
|
.and_then(|expr| match expr {
|
||||||
|
Expr::Path(path) => path.path.get_ident().cloned(),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("Enums must have a #[repr(T)] attribute for PtpSerialize");
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl ptp_cursor::PtpSerialize for #name
|
||||||
|
where
|
||||||
|
#name: Clone + Copy + TryFrom<#repr_ty> + Into<#repr_ty>
|
||||||
|
{
|
||||||
|
fn try_into_ptp(&self) -> std::io::Result<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
self.try_write_ptp(&mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> std::io::Result<()> {
|
||||||
|
let discriminant: #repr_ty = (*self).into();
|
||||||
|
discriminant.try_write_ptp(buf)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
unimplemented!("PtpSerialize cannot be automatically derived for unions")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expanded.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(PtpDeserialize)]
|
||||||
|
pub fn derive_ptp_deserialize(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
let name = &input.ident;
|
||||||
|
|
||||||
|
let expanded = match &input.data {
|
||||||
|
Data::Struct(s) => match &s.fields {
|
||||||
|
Fields::Named(named) => {
|
||||||
|
let fields = &named.named;
|
||||||
|
|
||||||
|
let read_fields = fields.iter().map(|f| {
|
||||||
|
let name = &f.ident;
|
||||||
|
let ty = &f.ty;
|
||||||
|
quote! {
|
||||||
|
#name: <#ty>::try_read_ptp(cur)?
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl ptp_cursor::PtpDeserialize for #name {
|
||||||
|
fn try_from_ptp(buf: &[u8]) -> std::io::Result<Self> {
|
||||||
|
use ptp_cursor::Read;
|
||||||
|
|
||||||
|
let mut cur = std::io::Cursor::new(buf);
|
||||||
|
let val = Self::try_read_ptp(&mut cur)?;
|
||||||
|
cur.expect_end()?;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read_ptp<R: ptp_cursor::Read>(cur: &mut R) -> std::io::Result<Self> {
|
||||||
|
Ok(Self { #(#read_fields),* })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Fields::Unnamed(unnamed) => {
|
||||||
|
let fields = &unnamed.unnamed;
|
||||||
|
|
||||||
|
let read_fields = fields.iter().map(|f| {
|
||||||
|
let ty = &f.ty;
|
||||||
|
quote! { <#ty>::try_read_ptp(cur)? }
|
||||||
|
});
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl ptp_cursor::PtpDeserialize for #name {
|
||||||
|
fn try_from_ptp(buf: &[u8]) -> std::io::Result<Self> {
|
||||||
|
use ptp_cursor::Read;
|
||||||
|
|
||||||
|
let mut cur = std::io::Cursor::new(buf);
|
||||||
|
let val = Self::try_read_ptp(&mut cur)?;
|
||||||
|
cur.expect_end()?;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read_ptp<R: ptp_cursor::Read>(cur: &mut R) -> std::io::Result<Self> {
|
||||||
|
Ok(Self(#(#read_fields),*))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Fields::Unit => {
|
||||||
|
quote! {
|
||||||
|
impl ptp_cursor::PtpDeserialize for #name {
|
||||||
|
fn try_from_ptp(buf: &[u8]) -> std::io::Result<Self> {
|
||||||
|
use ptp_cursor::Read;
|
||||||
|
|
||||||
|
let mut cur = std::io::Cursor::new(buf);
|
||||||
|
cur.expect_end()?;
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read_ptp<R: ptp_cursor::Read>(_cur: &mut R) -> std::io::Result<Self> {
|
||||||
|
Ok(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Data::Enum(_) => {
|
||||||
|
let repr_ty = input
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.find_map(|attr| {
|
||||||
|
if attr.path().is_ident("repr") {
|
||||||
|
attr.parse_args_with(
|
||||||
|
Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.and_then(|args| args.into_iter().next())
|
||||||
|
.and_then(|expr| match expr {
|
||||||
|
Expr::Path(path) => path.path.get_ident().cloned(),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("Enums must have a #[repr(T)] attribute for PtpDeserialize");
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl ptp_cursor::PtpDeserialize for #name
|
||||||
|
where
|
||||||
|
#name: Clone + Copy + TryFrom<#repr_ty> + Into<#repr_ty>
|
||||||
|
{
|
||||||
|
fn try_from_ptp(buf: &[u8]) -> std::io::Result<Self> {
|
||||||
|
use ptp_cursor::Read;
|
||||||
|
|
||||||
|
let mut cur = std::io::Cursor::new(buf);
|
||||||
|
let val = Self::try_read_ptp(&mut cur)?;
|
||||||
|
cur.expect_end()?;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read_ptp<R: ptp_cursor::Read>(cur: &mut R) -> std::io::Result<Self> {
|
||||||
|
let discriminant = <#repr_ty>::try_read_ptp(cur)?;
|
||||||
|
discriminant.try_into().map_err(|_| {
|
||||||
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
format!("Invalid discriminant for {}: {:?}", stringify!(#name), discriminant)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
unimplemented!("PtpDeserialize cannot be automatically derived for unions")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expanded.into()
|
||||||
|
}
|
72
flake.lock
generated
Normal file
72
flake.lock
generated
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"lib": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"treefmt-nix": [
|
||||||
|
"treefmt-nix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1758632667,
|
||||||
|
"narHash": "sha256-C0aBPv8vqTI1QNVhygZxL0f49UERx2UejVdtyz67jhs=",
|
||||||
|
"ref": "refs/heads/main",
|
||||||
|
"rev": "5e0737c20f3c265dbff604170a6433cc1e1a4b41",
|
||||||
|
"revCount": 8,
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.karaolidis.com/karaolidis/nix-lib.git"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.karaolidis.com/karaolidis/nix-lib.git"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1760284886,
|
||||||
|
"narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"lib": "lib",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1760120816,
|
||||||
|
"narHash": "sha256-gq9rdocpmRZCwLS5vsHozwB6b5nrOBDNc2kkEaTXHfg=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "761ae7aff00907b607125b2f57338b74177697ed",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
93
flake.nix
Executable file
93
flake.nix
Executable file
@@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
treefmt-nix = {
|
||||||
|
url = "github:numtide/treefmt-nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
lib = {
|
||||||
|
# FIXME: https://github.com/NixOS/nix/issues/12281
|
||||||
|
url = "git+https://git.karaolidis.com/karaolidis/nix-lib.git";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.follows = "nixpkgs";
|
||||||
|
treefmt-nix.follows = "treefmt-nix";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs:
|
||||||
|
{
|
||||||
|
overlays.default =
|
||||||
|
final: prev:
|
||||||
|
let
|
||||||
|
pkgs = final;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
fujicli = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "fujicli";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// (
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
|
||||||
|
pkgs = import inputs.nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config.allowUnfree = true;
|
||||||
|
overlays = [
|
||||||
|
inputs.lib.overlays.default
|
||||||
|
inputs.self.overlays.default
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
treefmt = inputs.treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.${system}.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
cargo
|
||||||
|
rustc
|
||||||
|
rustfmt
|
||||||
|
clippy
|
||||||
|
cargo-udeps
|
||||||
|
cargo-outdated
|
||||||
|
cargo-expand
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
TOP="$(git rev-parse --show-toplevel)"
|
||||||
|
export CARGO_HOME="$TOP/.cargo"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.${system} = with pkgs; {
|
||||||
|
default = fujicli;
|
||||||
|
inherit fujicli;
|
||||||
|
};
|
||||||
|
|
||||||
|
formatter.${system} = treefmt.config.build.wrapper;
|
||||||
|
|
||||||
|
checks.${system} =
|
||||||
|
let
|
||||||
|
packages = pkgs.lib.mapAttrs' (
|
||||||
|
name: pkgs.lib.nameValuePair "package-${name}"
|
||||||
|
) inputs.self.packages.${system};
|
||||||
|
|
||||||
|
devShells = pkgs.lib.mapAttrs' (
|
||||||
|
name: pkgs.lib.nameValuePair "devShell-${name}"
|
||||||
|
) inputs.self.devShells.${system};
|
||||||
|
|
||||||
|
formatter.formatting = treefmt.config.build.check inputs.self;
|
||||||
|
in
|
||||||
|
packages // devShells // formatter;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
225
src/camera/devices/mod.rs
Normal file
225
src/camera/devices/mod.rs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
pub mod x_trans_v;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
io::{self, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use log::debug;
|
||||||
|
use ptp_cursor::{PtpDeserialize, PtpSerialize};
|
||||||
|
use serde::Serialize;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
camera::ptp::hex::CommandCode,
|
||||||
|
cli::{common::film::FilmSimulationOptions, simulation::SetFilmSimulationOptions},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
CameraResult, SupportedCamera,
|
||||||
|
ptp::{
|
||||||
|
Ptp,
|
||||||
|
hex::{DevicePropCode, FujiCustomSetting, ObjectFormat, UsbMode},
|
||||||
|
structs::ObjectInfo,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait DeviceImpl<P: rusb::UsbContext> {
|
||||||
|
fn camera_definition(&self) -> &'static SupportedCamera<P>;
|
||||||
|
|
||||||
|
fn chunk_size(&self) -> usize {
|
||||||
|
// Default conservative estimate.
|
||||||
|
1024 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
fn custom_settings_slots(&self) -> Vec<FujiCustomSetting> {
|
||||||
|
FujiCustomSetting::iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info_get(&self, ptp: &mut Ptp) -> anyhow::Result<Box<dyn CameraResult>> {
|
||||||
|
let info = ptp.get_info()?;
|
||||||
|
|
||||||
|
let bytes = ptp.get_prop_value(DevicePropCode::FujiUsbMode)?;
|
||||||
|
let mode = UsbMode::try_from_ptp(&bytes)?;
|
||||||
|
|
||||||
|
let bytes = ptp.get_prop_value(DevicePropCode::FujiBatteryInfo2)?;
|
||||||
|
debug!("Raw battery data: {bytes:?}");
|
||||||
|
|
||||||
|
let battery_string = String::try_from_ptp(&bytes)?;
|
||||||
|
debug!("Decoded raw string: {battery_string}");
|
||||||
|
|
||||||
|
let battery: u32 = battery_string
|
||||||
|
.split(',')
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to parse battery percentage"))?
|
||||||
|
.parse()?;
|
||||||
|
|
||||||
|
let repr = CameraInfo {
|
||||||
|
manufacturer: info.manufacturer.clone(),
|
||||||
|
model: info.model.clone(),
|
||||||
|
device_version: info.device_version.clone(),
|
||||||
|
serial_number: info.serial_number,
|
||||||
|
mode,
|
||||||
|
battery,
|
||||||
|
};
|
||||||
|
|
||||||
|
let repr = Box::new(repr);
|
||||||
|
|
||||||
|
Ok(repr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backup_export(&self, ptp: &mut Ptp) -> anyhow::Result<Vec<u8>> {
|
||||||
|
const HANDLE: u32 = 0x0;
|
||||||
|
|
||||||
|
debug!("Sending GetObjectInfo command for backup");
|
||||||
|
let response = ptp.send(CommandCode::GetObjectInfo, &[HANDLE], None)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
|
debug!("Sending GetObject command for backup");
|
||||||
|
let response = ptp.send(CommandCode::GetObject, &[HANDLE], None)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backup_import(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> {
|
||||||
|
debug!("Sending SendObjectInfo command for backup");
|
||||||
|
let object_info = FujiBackupObjectInfo::new(buffer.len())?;
|
||||||
|
let response = ptp.send(
|
||||||
|
CommandCode::SendObjectInfo,
|
||||||
|
&[0x0, 0x0],
|
||||||
|
Some(&object_info.try_into_ptp()?),
|
||||||
|
)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
|
debug!("Sending SendObject command for backup");
|
||||||
|
let response = ptp.send(CommandCode::SendObject, &[0x0], Some(buffer))?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Naively assuming that all cameras support getting basic info.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CameraInfo {
|
||||||
|
pub manufacturer: String,
|
||||||
|
pub model: String,
|
||||||
|
pub device_version: String,
|
||||||
|
pub serial_number: String,
|
||||||
|
pub mode: UsbMode,
|
||||||
|
pub battery: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CameraInfo {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, "Manufacturer: {}", self.manufacturer)?;
|
||||||
|
writeln!(f, "Model: {}", self.model)?;
|
||||||
|
writeln!(f, "Version: {}", self.device_version)?;
|
||||||
|
writeln!(f, "Serial Number: {}", self.serial_number)?;
|
||||||
|
writeln!(f, "Mode: {}", self.mode)?;
|
||||||
|
write!(f, "Battery: {}%", self.battery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Naively assuming that all cameras support backup/restore
|
||||||
|
// using the same structs.
|
||||||
|
pub struct FujiBackupObjectInfo {
|
||||||
|
compressed_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FujiBackupObjectInfo {
|
||||||
|
pub fn new(buffer_len: usize) -> anyhow::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
compressed_size: u32::try_from(buffer_len)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PtpSerialize for FujiBackupObjectInfo {
|
||||||
|
fn try_into_ptp(&self) -> io::Result<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
self.try_write_ptp(&mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> io::Result<()> {
|
||||||
|
let object_info = ObjectInfo {
|
||||||
|
object_format: ObjectFormat::FujiBackup,
|
||||||
|
compressed_size: self.compressed_size,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
object_info.try_write_ptp(buf)?;
|
||||||
|
|
||||||
|
// TODO: What is this?
|
||||||
|
buf.write_all(&[0x0u8; 1020])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SensorImpl<P: rusb::UsbContext> {
|
||||||
|
fn simulation_list(
|
||||||
|
&self,
|
||||||
|
ptp: &mut Ptp,
|
||||||
|
device: &dyn DeviceImpl<P>,
|
||||||
|
) -> anyhow::Result<Vec<Box<dyn CameraResult>>>;
|
||||||
|
|
||||||
|
fn simulation_get(
|
||||||
|
&self,
|
||||||
|
ptp: &mut Ptp,
|
||||||
|
device: &dyn DeviceImpl<P>,
|
||||||
|
slot: FujiCustomSetting,
|
||||||
|
) -> anyhow::Result<Box<dyn CameraResult>>;
|
||||||
|
|
||||||
|
fn simulation_set(
|
||||||
|
&self,
|
||||||
|
ptp: &mut Ptp,
|
||||||
|
device: &dyn DeviceImpl<P>,
|
||||||
|
slot: FujiCustomSetting,
|
||||||
|
set_options: &SetFilmSimulationOptions,
|
||||||
|
options: &FilmSimulationOptions,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! prop_getter {
|
||||||
|
($name:ident: $type:ty => $code:expr) => {
|
||||||
|
fn $name(&self, ptp: &mut crate::camera::ptp::Ptp) -> anyhow::Result<$type> {
|
||||||
|
use ptp_cursor::PtpDeserialize;
|
||||||
|
|
||||||
|
let bytes = ptp.get_prop_value($code)?;
|
||||||
|
let result = <$type>::try_from_ptp(&bytes)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! prop_setter {
|
||||||
|
($name:ident: $type:ty => $code:expr) => {
|
||||||
|
fn $name(&self, ptp: &mut crate::camera::ptp::Ptp, value: &$type) -> anyhow::Result<()> {
|
||||||
|
use ptp_cursor::PtpSerialize;
|
||||||
|
|
||||||
|
let bytes = value.try_into_ptp()?;
|
||||||
|
ptp.set_prop_value($code, &bytes)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! set_prop_if_some {
|
||||||
|
($self:ident, $ptp:ident, $options:ident,
|
||||||
|
$( $field:ident => $setter:ident ),* $(,)? ) => {
|
||||||
|
$(
|
||||||
|
if let Some(val) = &$options.$field {
|
||||||
|
$self.$setter($ptp, val)?;
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use prop_getter;
|
||||||
|
pub(crate) use prop_setter;
|
||||||
|
pub(crate) use set_prop_if_some;
|
535
src/camera/devices/x_trans_v/mod.rs
Normal file
535
src/camera/devices/x_trans_v/mod.rs
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
pub mod x_t5;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
io::{self, Cursor, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use log::error;
|
||||||
|
use ptp_cursor::{PtpDeserialize, PtpSerialize};
|
||||||
|
use ptp_macro::{PtpDeserialize, PtpSerialize};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
camera::{
|
||||||
|
CameraResult,
|
||||||
|
devices::set_prop_if_some,
|
||||||
|
ptp::{
|
||||||
|
Ptp,
|
||||||
|
hex::{
|
||||||
|
DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect,
|
||||||
|
FujiColorChromeFXBlue, FujiColorSpace, FujiCustomSetting, FujiCustomSettingName,
|
||||||
|
FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect,
|
||||||
|
FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize,
|
||||||
|
FujiLensModulationOptimizer, FujiMonochromaticColorTemperature,
|
||||||
|
FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect,
|
||||||
|
FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cli::{common::film::FilmSimulationOptions, simulation::SetFilmSimulationOptions},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{DeviceImpl, SensorImpl, prop_getter, prop_setter};
|
||||||
|
|
||||||
|
pub struct XTransV;
|
||||||
|
|
||||||
|
impl XTransV {
|
||||||
|
prop_getter!(get_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName);
|
||||||
|
prop_getter!(get_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize);
|
||||||
|
prop_getter!(get_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality);
|
||||||
|
prop_getter!(get_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange);
|
||||||
|
prop_getter!(get_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority);
|
||||||
|
prop_getter!(get_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation);
|
||||||
|
prop_getter!(get_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature);
|
||||||
|
prop_getter!(get_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint);
|
||||||
|
prop_getter!(get_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect);
|
||||||
|
prop_getter!(get_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance);
|
||||||
|
prop_getter!(get_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR);
|
||||||
|
prop_getter!(get_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone);
|
||||||
|
prop_getter!(get_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone);
|
||||||
|
prop_getter!(get_color: FujiColor => DevicePropCode::FujiCustomSettingColor);
|
||||||
|
prop_getter!(get_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness);
|
||||||
|
prop_getter!(get_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity);
|
||||||
|
prop_getter!(get_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed);
|
||||||
|
prop_getter!(get_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue);
|
||||||
|
prop_getter!(get_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature);
|
||||||
|
prop_getter!(get_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect);
|
||||||
|
prop_getter!(get_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue);
|
||||||
|
prop_getter!(get_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect);
|
||||||
|
prop_getter!(get_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer);
|
||||||
|
prop_getter!(get_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace);
|
||||||
|
|
||||||
|
prop_setter!(set_active_custom_setting: FujiCustomSetting => DevicePropCode::FujiCustomSetting);
|
||||||
|
prop_setter!(set_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName);
|
||||||
|
prop_setter!(set_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize);
|
||||||
|
prop_setter!(set_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality);
|
||||||
|
prop_setter!(set_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange);
|
||||||
|
prop_setter!(set_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority);
|
||||||
|
prop_setter!(set_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation);
|
||||||
|
prop_setter!(set_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature);
|
||||||
|
prop_setter!(set_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint);
|
||||||
|
prop_setter!(set_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect);
|
||||||
|
prop_setter!(set_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance);
|
||||||
|
prop_setter!(set_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR);
|
||||||
|
prop_setter!(set_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone);
|
||||||
|
prop_setter!(set_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone);
|
||||||
|
prop_setter!(set_color: FujiColor => DevicePropCode::FujiCustomSettingColor);
|
||||||
|
prop_setter!(set_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness);
|
||||||
|
prop_setter!(set_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity);
|
||||||
|
prop_setter!(set_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed);
|
||||||
|
prop_setter!(set_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue);
|
||||||
|
prop_setter!(set_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature);
|
||||||
|
prop_setter!(set_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect);
|
||||||
|
prop_setter!(set_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue);
|
||||||
|
prop_setter!(set_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect);
|
||||||
|
prop_setter!(set_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer);
|
||||||
|
prop_setter!(set_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace);
|
||||||
|
|
||||||
|
fn validate_monochromatic(
|
||||||
|
final_options: &FilmSimulationOptions,
|
||||||
|
prev_simulation: FujiFilmSimulation,
|
||||||
|
) -> bool {
|
||||||
|
let mut fail = true;
|
||||||
|
|
||||||
|
if !matches!(
|
||||||
|
prev_simulation,
|
||||||
|
FujiFilmSimulation::Monochrome
|
||||||
|
| FujiFilmSimulation::MonochromeYe
|
||||||
|
| FujiFilmSimulation::MonochromeR
|
||||||
|
| FujiFilmSimulation::MonochromeG
|
||||||
|
| FujiFilmSimulation::AcrosSTD
|
||||||
|
| FujiFilmSimulation::AcrosYe
|
||||||
|
| FujiFilmSimulation::AcrosR
|
||||||
|
| FujiFilmSimulation::AcrosG
|
||||||
|
) && (final_options.monochromatic_color_temperature.is_some()
|
||||||
|
|| final_options.monochromatic_color_tint.is_some())
|
||||||
|
{
|
||||||
|
if final_options.monochromatic_color_temperature.is_some() {
|
||||||
|
error!(
|
||||||
|
"A B&W film simulation is not selected, refusing to set monochromatic color temperature"
|
||||||
|
);
|
||||||
|
fail = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if final_options.monochromatic_color_tint.is_some() {
|
||||||
|
error!(
|
||||||
|
"A B&W film simulation is not selected, refusing to set monochromatic color tint"
|
||||||
|
);
|
||||||
|
fail = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fail
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_white_balance_temperature(
|
||||||
|
final_options: &FilmSimulationOptions,
|
||||||
|
prev_white_balance: FujiWhiteBalance,
|
||||||
|
) -> bool {
|
||||||
|
if prev_white_balance != FujiWhiteBalance::Temperature
|
||||||
|
&& final_options.white_balance_temperature.is_some()
|
||||||
|
{
|
||||||
|
error!("White Balance mode is not set to 'Temperature', refusing to set temperature");
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_exposure(
|
||||||
|
final_options: &FilmSimulationOptions,
|
||||||
|
previous_dynamic_range_priority: FujiDynamicRangePriority,
|
||||||
|
) -> bool {
|
||||||
|
let mut fail = true;
|
||||||
|
|
||||||
|
if previous_dynamic_range_priority != FujiDynamicRangePriority::Off
|
||||||
|
&& (final_options.dynamic_range.is_some()
|
||||||
|
|| final_options.highlight.is_some()
|
||||||
|
|| final_options.shadow.is_some())
|
||||||
|
{
|
||||||
|
if final_options.dynamic_range.is_some() {
|
||||||
|
error!("Dynamic Range Priority is enabled, refusing to set dynamic range");
|
||||||
|
fail = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if final_options.highlight.is_some() {
|
||||||
|
error!("Dynamic Range Priority is enabled, refusing to set highlight tone");
|
||||||
|
fail = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if final_options.shadow.is_some() {
|
||||||
|
error!("Dynamic Range Priority is enabled, refusing to set shadow tone");
|
||||||
|
fail = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fail
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_simulation_set(
|
||||||
|
&self,
|
||||||
|
ptp: &mut Ptp,
|
||||||
|
options: &FilmSimulationOptions,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let prev_simulation = if let Some(simulation) = options.simulation {
|
||||||
|
simulation
|
||||||
|
} else {
|
||||||
|
self.get_film_simulation(ptp)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev_white_balance = if let Some(white_balance) = options.white_balance {
|
||||||
|
white_balance
|
||||||
|
} else {
|
||||||
|
self.get_white_balance(ptp)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev_dynamic_range_priority =
|
||||||
|
if let Some(dynamic_range_priority) = options.dynamic_range_priority {
|
||||||
|
dynamic_range_priority
|
||||||
|
} else {
|
||||||
|
self.get_dynamic_range_priority(ptp)?
|
||||||
|
};
|
||||||
|
|
||||||
|
if !Self::validate_monochromatic(options, prev_simulation)
|
||||||
|
|| !Self::validate_white_balance_temperature(options, prev_white_balance)
|
||||||
|
|| !Self::validate_exposure(options, prev_dynamic_range_priority)
|
||||||
|
{
|
||||||
|
bail!("Incompatible options detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: rusb::UsbContext> SensorImpl<P> for XTransV {
|
||||||
|
fn simulation_list(
|
||||||
|
&self,
|
||||||
|
ptp: &mut Ptp,
|
||||||
|
device: &dyn DeviceImpl<P>,
|
||||||
|
) -> anyhow::Result<Vec<Box<dyn CameraResult>>> {
|
||||||
|
let mut slots = Vec::new();
|
||||||
|
|
||||||
|
for slot in device.custom_settings_slots() {
|
||||||
|
self.set_active_custom_setting(ptp, &slot)?;
|
||||||
|
let name = self.get_custom_setting_name(ptp)?;
|
||||||
|
let repr = SimulationListItem { slot, name };
|
||||||
|
let repr: Box<dyn CameraResult> = Box::new(repr);
|
||||||
|
slots.push(repr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(slots)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simulation_get(
|
||||||
|
&self,
|
||||||
|
ptp: &mut Ptp,
|
||||||
|
device: &dyn DeviceImpl<P>,
|
||||||
|
slot: FujiCustomSetting,
|
||||||
|
) -> anyhow::Result<Box<dyn CameraResult>> {
|
||||||
|
if !device.custom_settings_slots().contains(&slot) {
|
||||||
|
bail!("Unsupported custom setting slot '{slot}'")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_active_custom_setting(ptp, &slot)?;
|
||||||
|
|
||||||
|
let repr = Simulation {
|
||||||
|
name: self.get_custom_setting_name(ptp)?,
|
||||||
|
size: self.get_image_size(ptp)?,
|
||||||
|
quality: self.get_image_quality(ptp)?,
|
||||||
|
simulation: self.get_film_simulation(ptp)?,
|
||||||
|
monochromatic_color_temperature: self.get_monochromatic_color_temperature(ptp)?,
|
||||||
|
monochromatic_color_tint: self.get_monochromatic_color_tint(ptp)?,
|
||||||
|
highlight: self.get_highlight_tone(ptp)?,
|
||||||
|
shadow: self.get_shadow_tone(ptp)?,
|
||||||
|
color: self.get_color(ptp)?,
|
||||||
|
sharpness: self.get_sharpness(ptp)?,
|
||||||
|
clarity: self.get_clarity(ptp)?,
|
||||||
|
noise_reduction: self.get_high_iso_nr(ptp)?,
|
||||||
|
grain: self.get_grain_effect(ptp)?,
|
||||||
|
color_chrome_effect: self.get_color_chrome_effect(ptp)?,
|
||||||
|
color_chrome_fx_blue: self.get_color_chrome_fx_blue(ptp)?,
|
||||||
|
smooth_skin_effect: self.get_smooth_skin_effect(ptp)?,
|
||||||
|
white_balance: self.get_white_balance(ptp)?,
|
||||||
|
white_balance_shift_red: self.get_white_balance_shift_red(ptp)?,
|
||||||
|
white_balance_shift_blue: self.get_white_balance_shift_blue(ptp)?,
|
||||||
|
white_balance_temperature: self.get_white_balance_temperature(ptp)?,
|
||||||
|
dynamic_range: self.get_dynamic_range(ptp)?,
|
||||||
|
dynamic_range_priority: self.get_dynamic_range_priority(ptp)?,
|
||||||
|
lens_modulation_optimizer: self.get_lens_modulation_optimizer(ptp)?,
|
||||||
|
color_space: self.get_color_space(ptp)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let repr = Box::new(repr);
|
||||||
|
|
||||||
|
Ok(repr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simulation_set(
|
||||||
|
&self,
|
||||||
|
ptp: &mut Ptp,
|
||||||
|
device: &dyn DeviceImpl<P>,
|
||||||
|
slot: FujiCustomSetting,
|
||||||
|
set_options: &SetFilmSimulationOptions,
|
||||||
|
options: &FilmSimulationOptions,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if !device.custom_settings_slots().contains(&slot) {
|
||||||
|
bail!("Unsupported custom setting slot '{slot}'")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_active_custom_setting(ptp, &slot)?;
|
||||||
|
|
||||||
|
self.validate_simulation_set(ptp, options)?;
|
||||||
|
|
||||||
|
set_prop_if_some!(self, ptp, set_options,
|
||||||
|
name => set_custom_setting_name,
|
||||||
|
);
|
||||||
|
|
||||||
|
set_prop_if_some!(self, ptp, options,
|
||||||
|
size => set_image_size,
|
||||||
|
quality => set_image_quality,
|
||||||
|
simulation => set_film_simulation,
|
||||||
|
monochromatic_color_temperature => set_monochromatic_color_temperature,
|
||||||
|
monochromatic_color_tint => set_monochromatic_color_tint,
|
||||||
|
color => set_color,
|
||||||
|
sharpness => set_sharpness,
|
||||||
|
clarity => set_clarity,
|
||||||
|
noise_reduction => set_high_iso_nr,
|
||||||
|
grain => set_grain_effect,
|
||||||
|
color_chrome_effect => set_color_chrome_effect,
|
||||||
|
color_chrome_fx_blue => set_color_chrome_fx_blue,
|
||||||
|
smooth_skin_effect => set_smooth_skin_effect,
|
||||||
|
white_balance => set_white_balance,
|
||||||
|
white_balance_temperature => set_white_balance_temperature,
|
||||||
|
white_balance_shift_red => set_white_balance_shift_red,
|
||||||
|
white_balance_shift_blue => set_white_balance_shift_blue,
|
||||||
|
dynamic_range_priority => set_dynamic_range_priority,
|
||||||
|
dynamic_range => set_dynamic_range,
|
||||||
|
highlight => set_highlight_tone,
|
||||||
|
shadow => set_shadow_tone,
|
||||||
|
lens_modulation_optimizer => set_lens_modulation_optimizer,
|
||||||
|
color_space => set_color_space,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Naively assuming that all cameras using the same sensor
|
||||||
|
// also have the same simulation feature set.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SimulationListItem {
|
||||||
|
pub slot: FujiCustomSetting,
|
||||||
|
pub name: FujiCustomSettingName,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SimulationListItem {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}: {}", self.slot, self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Simulation {
|
||||||
|
pub name: FujiCustomSettingName,
|
||||||
|
pub size: FujiImageSize,
|
||||||
|
pub quality: FujiImageQuality,
|
||||||
|
#[allow(clippy::struct_field_names)]
|
||||||
|
pub simulation: FujiFilmSimulation,
|
||||||
|
pub monochromatic_color_temperature: FujiMonochromaticColorTemperature,
|
||||||
|
pub monochromatic_color_tint: FujiMonochromaticColorTint,
|
||||||
|
pub highlight: FujiHighlightTone,
|
||||||
|
pub shadow: FujiShadowTone,
|
||||||
|
pub color: FujiColor,
|
||||||
|
pub sharpness: FujiSharpness,
|
||||||
|
pub clarity: FujiClarity,
|
||||||
|
pub noise_reduction: FujiHighISONR,
|
||||||
|
pub grain: FujiGrainEffect,
|
||||||
|
pub color_chrome_effect: FujiColorChromeEffect,
|
||||||
|
pub color_chrome_fx_blue: FujiColorChromeFXBlue,
|
||||||
|
pub smooth_skin_effect: FujiSmoothSkinEffect,
|
||||||
|
pub white_balance: FujiWhiteBalance,
|
||||||
|
pub white_balance_shift_red: FujiWhiteBalanceShift,
|
||||||
|
pub white_balance_shift_blue: FujiWhiteBalanceShift,
|
||||||
|
pub white_balance_temperature: FujiWhiteBalanceTemperature,
|
||||||
|
pub dynamic_range: FujiDynamicRange,
|
||||||
|
pub dynamic_range_priority: FujiDynamicRangePriority,
|
||||||
|
pub lens_modulation_optimizer: FujiLensModulationOptimizer,
|
||||||
|
pub color_space: FujiColorSpace,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Simulation {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, "Name: {}", self.name)?;
|
||||||
|
writeln!(f, "Size: {}", self.size)?;
|
||||||
|
writeln!(f, "Quality: {}", self.quality)?;
|
||||||
|
|
||||||
|
writeln!(f, "Simulation: {}", self.simulation)?;
|
||||||
|
|
||||||
|
match self.simulation {
|
||||||
|
FujiFilmSimulation::Monochrome
|
||||||
|
| FujiFilmSimulation::MonochromeYe
|
||||||
|
| FujiFilmSimulation::MonochromeR
|
||||||
|
| FujiFilmSimulation::MonochromeG
|
||||||
|
| FujiFilmSimulation::AcrosSTD
|
||||||
|
| FujiFilmSimulation::AcrosYe
|
||||||
|
| FujiFilmSimulation::AcrosR
|
||||||
|
| FujiFilmSimulation::AcrosG => {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Monochromatic Color Temperature: {}",
|
||||||
|
self.monochromatic_color_temperature
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Monochromatic Color Tint: {}",
|
||||||
|
self.monochromatic_color_tint
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.dynamic_range_priority == FujiDynamicRangePriority::Off {
|
||||||
|
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)?;
|
||||||
|
writeln!(f, "Noise Reduction: {}", self.noise_reduction)?;
|
||||||
|
writeln!(f, "Grain: {}", self.grain)?;
|
||||||
|
writeln!(f, "Color Chrome Effect: {}", self.color_chrome_effect)?;
|
||||||
|
writeln!(f, "Color Chrome FX Blue: {}", self.color_chrome_fx_blue)?;
|
||||||
|
writeln!(f, "Smooth Skin Effect: {}", self.smooth_skin_effect)?;
|
||||||
|
|
||||||
|
writeln!(f, "White Balance: {}", self.white_balance)?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"White Balance Shift (R/B): {} / {}",
|
||||||
|
self.white_balance_shift_red, self.white_balance_shift_blue
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if self.white_balance == FujiWhiteBalance::Temperature {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"White Balance Temperature: {}K",
|
||||||
|
self.white_balance_temperature
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.dynamic_range_priority == FujiDynamicRangePriority::Off {
|
||||||
|
writeln!(f, "Dynamic Range: {}", self.dynamic_range)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f, "Dynamic Range Priority: {}", self.dynamic_range_priority)?;
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Lens Modulation Optimizer: {}",
|
||||||
|
self.lens_modulation_optimizer
|
||||||
|
)?;
|
||||||
|
writeln!(f, "Color Space: {}", self.color_space)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PtpSerialize, PtpDeserialize)]
|
||||||
|
pub struct FujiConversionProfileContents {
|
||||||
|
// TODO: What is this, and why is it always 0x2?
|
||||||
|
pub unknown_0: i32,
|
||||||
|
pub file_type: u32,
|
||||||
|
pub size: u32,
|
||||||
|
pub quality: u32,
|
||||||
|
pub exposure_offset: i32,
|
||||||
|
pub dynamic_range: u32,
|
||||||
|
pub dynamic_range_priority: u32,
|
||||||
|
pub simulation: u32,
|
||||||
|
pub grain: u32,
|
||||||
|
pub color_chrome_effect: u32,
|
||||||
|
pub white_balance_as_shot: u32,
|
||||||
|
pub white_balance: u32,
|
||||||
|
pub white_balance_shift_red: i32,
|
||||||
|
pub white_balance_shift_blue: i32,
|
||||||
|
pub white_balance_temperature: i32,
|
||||||
|
pub highlight: i32,
|
||||||
|
pub shadow: i32,
|
||||||
|
pub color: i32,
|
||||||
|
pub sharpness: i32,
|
||||||
|
pub noise_reduction: u32,
|
||||||
|
pub lens_modulation_optimizer: u32,
|
||||||
|
pub color_space: u32,
|
||||||
|
pub monochromatic_color_temperature: i32,
|
||||||
|
pub smooth_skin_effect: u32,
|
||||||
|
pub color_chrome_fx_blue: u32,
|
||||||
|
pub monochromatic_color_tint: i32,
|
||||||
|
pub clarity: i32,
|
||||||
|
pub teleconverter: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FujiConversionProfile {
|
||||||
|
pub contents: FujiConversionProfileContents,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FujiConversionProfile {
|
||||||
|
const EXPECTED_N_PROPS: i16 = 29;
|
||||||
|
const EXPECTED_PROFILE_CODE: &str = "FF17950";
|
||||||
|
const PADDING: usize = 0x1EE;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PtpDeserialize for FujiConversionProfile {
|
||||||
|
fn try_from_ptp(buf: &[u8]) -> io::Result<Self> {
|
||||||
|
let mut cur = Cursor::new(buf);
|
||||||
|
let value = Self::try_read_ptp(&mut cur)?;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read_ptp<R: ptp_cursor::Read>(cur: &mut R) -> io::Result<Self> {
|
||||||
|
let n_props = <i16>::try_read_ptp(cur)?;
|
||||||
|
if n_props != Self::EXPECTED_N_PROPS {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("Expected {} props, got {n_props}", Self::EXPECTED_N_PROPS),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let profile_code = String::try_read_ptp(cur)?;
|
||||||
|
if profile_code != Self::EXPECTED_PROFILE_CODE {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!(
|
||||||
|
"Expected profile code '{}', got '{profile_code}'",
|
||||||
|
Self::EXPECTED_PROFILE_CODE
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut padding = [0u8; Self::PADDING];
|
||||||
|
cur.read_exact(&mut padding)?;
|
||||||
|
|
||||||
|
let contents = FujiConversionProfileContents::try_read_ptp(cur)?;
|
||||||
|
Ok(Self { contents })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PtpSerialize for FujiConversionProfile {
|
||||||
|
fn try_into_ptp(&self) -> io::Result<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
self.try_write_ptp(&mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> io::Result<()> {
|
||||||
|
Self::EXPECTED_N_PROPS.try_write_ptp(buf)?;
|
||||||
|
Self::EXPECTED_PROFILE_CODE.try_write_ptp(buf)?;
|
||||||
|
|
||||||
|
let padding = [0u8; Self::PADDING];
|
||||||
|
buf.write_all(&padding)?;
|
||||||
|
|
||||||
|
self.contents.try_write_ptp(buf)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
26
src/camera/devices/x_trans_v/x_t5/mod.rs
Normal file
26
src/camera/devices/x_trans_v/x_t5/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use rusb::GlobalContext;
|
||||||
|
|
||||||
|
use crate::camera::{DeviceImpl, devices::SupportedCamera};
|
||||||
|
|
||||||
|
use super::XTransV;
|
||||||
|
|
||||||
|
pub const FUJIFILM_XT5: SupportedCamera<GlobalContext> = SupportedCamera {
|
||||||
|
name: "FUJIFILM XT-5",
|
||||||
|
vendor: 0x04cb,
|
||||||
|
product: 0x02fc,
|
||||||
|
device_factory: || Box::new(FujifilmXT5 {}),
|
||||||
|
sensor_factory: || Box::new(XTransV {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct FujifilmXT5 {}
|
||||||
|
|
||||||
|
impl DeviceImpl<GlobalContext> for FujifilmXT5 {
|
||||||
|
fn camera_definition(&self) -> &'static SupportedCamera<GlobalContext> {
|
||||||
|
&FUJIFILM_XT5
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chunk_size(&self) -> usize {
|
||||||
|
// 15.75 * 1024^2
|
||||||
|
16128 * 1024
|
||||||
|
}
|
||||||
|
}
|
13
src/camera/error.rs
Normal file
13
src/camera/error.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UnsupportedFeatureError;
|
||||||
|
|
||||||
|
impl fmt::Display for UnsupportedFeatureError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "feature is not supported for this device")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for UnsupportedFeatureError {}
|
213
src/camera/mod.rs
Normal file
213
src/camera/mod.rs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
pub mod devices;
|
||||||
|
pub mod error;
|
||||||
|
pub mod ptp;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::common::film::FilmSimulationOptions, cli::simulation::SetFilmSimulationOptions,
|
||||||
|
usb::find_endpoint,
|
||||||
|
};
|
||||||
|
use anyhow::bail;
|
||||||
|
use devices::{DeviceImpl, SensorImpl, x_trans_v};
|
||||||
|
use erased_serde::serialize_trait_object;
|
||||||
|
use log::{debug, error};
|
||||||
|
use ptp::{Ptp, hex::FujiCustomSetting};
|
||||||
|
use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
const SESSION: u32 = 1;
|
||||||
|
|
||||||
|
pub struct Camera {
|
||||||
|
pub device: Box<dyn DeviceImpl<GlobalContext>>,
|
||||||
|
pub sensor: Box<dyn SensorImpl<GlobalContext>>,
|
||||||
|
pub ptp: Ptp,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! camera_to_device {
|
||||||
|
($name:ident -> $ret:ty) => {
|
||||||
|
pub fn $name(&mut self) -> $ret {
|
||||||
|
self.device.$name(&mut self.ptp)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($name:ident($($arg:ident: $arg_ty:ty),*) -> $ret:ty) => {
|
||||||
|
pub fn $name(&mut self, $($arg: $arg_ty),*) -> $ret {
|
||||||
|
self.device.$name(&mut self.ptp, $($arg),*)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! camera_to_sensor {
|
||||||
|
($name:ident -> $ret:ty) => {
|
||||||
|
pub fn $name(&mut self) -> $ret {
|
||||||
|
self.sensor.$name(&mut self.ptp, &self.device)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($name:ident($($arg:ident: $arg_ty:ty),*) -> $ret:ty) => {
|
||||||
|
pub fn $name(&mut self, $($arg: $arg_ty),*) -> $ret {
|
||||||
|
self.sensor.$name(&mut self.ptp, &*self.device, $($arg),*)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CameraResult: fmt::Display + erased_serde::Serialize {}
|
||||||
|
impl<T: fmt::Display + serde::Serialize> CameraResult for T {}
|
||||||
|
serialize_trait_object!(CameraResult);
|
||||||
|
|
||||||
|
impl Camera {
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
self.device.camera_definition().name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vendor_id(&self) -> u16 {
|
||||||
|
self.device.camera_definition().vendor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn product_id(&self) -> u16 {
|
||||||
|
self.device.camera_definition().product
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connected_usb_id(&self) -> String {
|
||||||
|
format!("{}.{}", self.ptp.bus, self.ptp.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
camera_to_device!(info_get() -> anyhow::Result<Box<dyn CameraResult>>);
|
||||||
|
|
||||||
|
camera_to_device!(backup_export -> anyhow::Result<Vec<u8>>);
|
||||||
|
camera_to_device!(backup_import(buffer: &[u8]) -> anyhow::Result<()>);
|
||||||
|
|
||||||
|
camera_to_sensor!(simulation_list() -> anyhow::Result<Vec<Box<dyn CameraResult>>>);
|
||||||
|
camera_to_sensor!(simulation_get(slot: FujiCustomSetting) -> anyhow::Result<Box<dyn CameraResult>>);
|
||||||
|
camera_to_sensor!(simulation_set(slot: FujiCustomSetting, set_options: &SetFilmSimulationOptions, options: &FilmSimulationOptions) -> anyhow::Result<()>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Camera {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
debug!("Closing session");
|
||||||
|
if let Err(e) = self.ptp.close_session(SESSION) {
|
||||||
|
error!("Error closing session: {e}");
|
||||||
|
}
|
||||||
|
debug!("Session closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&rusb::Device<GlobalContext>> for Camera {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(device: &rusb::Device<GlobalContext>) -> anyhow::Result<Self> {
|
||||||
|
let descriptor = device.device_descriptor()?;
|
||||||
|
|
||||||
|
let vendor = descriptor.vendor_id();
|
||||||
|
let product = descriptor.product_id();
|
||||||
|
|
||||||
|
for supported_camera in SUPPORTED {
|
||||||
|
if vendor != supported_camera.vendor || product != supported_camera.product {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_impl = (supported_camera.device_factory)();
|
||||||
|
let sensor_impl = (supported_camera.sensor_factory)();
|
||||||
|
|
||||||
|
let bus = device.bus_number();
|
||||||
|
let address = device.address();
|
||||||
|
|
||||||
|
let config_descriptor = device.active_config_descriptor()?;
|
||||||
|
|
||||||
|
let interface_descriptor = config_descriptor
|
||||||
|
.interfaces()
|
||||||
|
.flat_map(|i| i.descriptors())
|
||||||
|
.find(|x| x.class_code() == LIBUSB_CLASS_IMAGE)
|
||||||
|
.ok_or(rusb::Error::NotFound)?;
|
||||||
|
|
||||||
|
let interface = interface_descriptor.interface_number();
|
||||||
|
debug!("Found interface {interface}");
|
||||||
|
|
||||||
|
let handle = device.open()?;
|
||||||
|
handle.claim_interface(interface)?;
|
||||||
|
|
||||||
|
let bulk_in = find_endpoint(
|
||||||
|
&interface_descriptor,
|
||||||
|
rusb::Direction::In,
|
||||||
|
rusb::TransferType::Bulk,
|
||||||
|
)?;
|
||||||
|
let bulk_out = find_endpoint(
|
||||||
|
&interface_descriptor,
|
||||||
|
rusb::Direction::Out,
|
||||||
|
rusb::TransferType::Bulk,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let transaction_id = 0;
|
||||||
|
|
||||||
|
let chunk_size = device_impl.chunk_size();
|
||||||
|
|
||||||
|
let mut ptp = Ptp {
|
||||||
|
bus,
|
||||||
|
address,
|
||||||
|
interface,
|
||||||
|
bulk_in,
|
||||||
|
bulk_out,
|
||||||
|
handle,
|
||||||
|
transaction_id,
|
||||||
|
chunk_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("Opening session");
|
||||||
|
let () = ptp.open_session(SESSION)?;
|
||||||
|
debug!("Session opened");
|
||||||
|
|
||||||
|
return Ok(Self {
|
||||||
|
ptp,
|
||||||
|
device: device_impl,
|
||||||
|
sensor: sensor_impl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("Device not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceFactory<P> = fn() -> Box<dyn DeviceImpl<P>>;
|
||||||
|
type SensorFactory<P> = fn() -> Box<dyn SensorImpl<P>>;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CameraInfoListItem {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub usb_id: String,
|
||||||
|
pub vendor_id: String,
|
||||||
|
pub product_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Camera> for CameraInfoListItem {
|
||||||
|
fn from(camera: &Camera) -> Self {
|
||||||
|
Self {
|
||||||
|
name: camera.name(),
|
||||||
|
usb_id: camera.connected_usb_id(),
|
||||||
|
vendor_id: format!("0x{:04x}", camera.vendor_id()),
|
||||||
|
product_id: format!("0x{:04x}", camera.product_id()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CameraInfoListItem {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} ({}:{}) (USB ID: {})",
|
||||||
|
self.name, self.vendor_id, self.product_id, self.usb_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SupportedCamera<P: rusb::UsbContext> {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub vendor: u16,
|
||||||
|
pub product: u16,
|
||||||
|
pub device_factory: DeviceFactory<P>,
|
||||||
|
pub sensor_factory: SensorFactory<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const SUPPORTED: &[SupportedCamera<GlobalContext>] = &[x_trans_v::x_t5::FUJIFILM_XT5];
|
53
src/camera/ptp/error.rs
Normal file
53
src/camera/ptp/error.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
use crate::camera::ptp::hex::ResponseCode;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Response(u16),
|
||||||
|
Malformed(String),
|
||||||
|
Usb(rusb::Error),
|
||||||
|
Io(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Self::Response(r) => {
|
||||||
|
let name = ResponseCode::try_from(r)
|
||||||
|
.map_or_else(|_| "Unknown".to_string(), |c| format!("{c:?}"));
|
||||||
|
write!(f, "{name} (0x{r:04x})")
|
||||||
|
}
|
||||||
|
Self::Usb(ref e) => write!(f, "USB error: {e}"),
|
||||||
|
Self::Io(ref e) => write!(f, "IO error: {e}"),
|
||||||
|
Self::Malformed(ref e) => write!(f, "{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::error::Error for Error {
|
||||||
|
fn cause(&self) -> Option<&dyn (::std::error::Error)> {
|
||||||
|
match *self {
|
||||||
|
Self::Usb(ref e) => Some(e),
|
||||||
|
Self::Io(ref e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rusb::Error> for Error {
|
||||||
|
fn from(e: rusb::Error) -> Self {
|
||||||
|
Self::Usb(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(e: io::Error) -> Self {
|
||||||
|
match e.kind() {
|
||||||
|
io::ErrorKind::UnexpectedEof => {
|
||||||
|
Self::Malformed("Unexpected end of message".to_string())
|
||||||
|
}
|
||||||
|
_ => Self::Io(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
719
src/camera/ptp/hex.rs
Normal file
719
src/camera/ptp/hex.rs
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
use std::{
|
||||||
|
io::{self, Cursor},
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
|
use ptp_cursor::{PtpDeserialize, PtpSerialize, Read};
|
||||||
|
use ptp_macro::{PtpDeserialize, PtpSerialize};
|
||||||
|
use serde::Serialize;
|
||||||
|
use strum_macros::EnumIter;
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
|
||||||
|
)]
|
||||||
|
pub enum CommandCode {
|
||||||
|
GetDeviceInfo = 0x1001,
|
||||||
|
OpenSession = 0x1002,
|
||||||
|
CloseSession = 0x1003,
|
||||||
|
GetObjectHandles = 0x1007,
|
||||||
|
GetObjectInfo = 0x1008,
|
||||||
|
GetObject = 0x1009,
|
||||||
|
DeleteObject = 0x100B,
|
||||||
|
SendObjectInfo = 0x100C,
|
||||||
|
SendObject = 0x100D,
|
||||||
|
GetDevicePropValue = 0x1015,
|
||||||
|
SetDevicePropValue = 0x1016,
|
||||||
|
FujiSendObjectInfo = 0x900c,
|
||||||
|
FujiSendObject = 0x900d,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
|
||||||
|
)]
|
||||||
|
pub enum ResponseCode {
|
||||||
|
Undefined = 0x2000,
|
||||||
|
Ok = 0x2001,
|
||||||
|
GeneralError = 0x2002,
|
||||||
|
SessionNotOpen = 0x2003,
|
||||||
|
InvalidTransactionId = 0x2004,
|
||||||
|
OperationNotSupported = 0x2005,
|
||||||
|
ParameterNotSupported = 0x2006,
|
||||||
|
IncompleteTransfer = 0x2007,
|
||||||
|
InvalidStorageId = 0x2008,
|
||||||
|
InvalidObjectHandle = 0x2009,
|
||||||
|
DevicePropNotSupported = 0x200A,
|
||||||
|
InvalidObjectFormatCode = 0x200B,
|
||||||
|
StoreFull = 0x200C,
|
||||||
|
ObjectWriteProtected = 0x200D,
|
||||||
|
StoreReadOnly = 0x200E,
|
||||||
|
AccessDenied = 0x200F,
|
||||||
|
NoThumbnailPresent = 0x2010,
|
||||||
|
SelfTestFailed = 0x2011,
|
||||||
|
PartialDeletion = 0x2012,
|
||||||
|
StoreNotAvailable = 0x2013,
|
||||||
|
SpecificationByFormatUnsupported = 0x2014,
|
||||||
|
NoValidObjectInfo = 0x2015,
|
||||||
|
InvalidCodeFormat = 0x2016,
|
||||||
|
UnknownVendorCode = 0x2017,
|
||||||
|
CaptureAlreadyTerminated = 0x2018,
|
||||||
|
DeviceBusy = 0x2019,
|
||||||
|
InvalidParentObject = 0x201A,
|
||||||
|
InvalidDevicePropFormat = 0x201B,
|
||||||
|
InvalidDevicePropValue = 0x201C,
|
||||||
|
InvalidParameter = 0x201D,
|
||||||
|
SessionAlreadyOpen = 0x201E,
|
||||||
|
TransactionCancelled = 0x201F,
|
||||||
|
SpecificationOfDestinationUnsupported = 0x2020,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ContainerCode {
|
||||||
|
Command(CommandCode),
|
||||||
|
Response(ResponseCode),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ContainerCode> for u16 {
|
||||||
|
fn from(code: ContainerCode) -> Self {
|
||||||
|
match code {
|
||||||
|
ContainerCode::Command(cmd) => cmd.into(),
|
||||||
|
ContainerCode::Response(resp) => resp.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u16> for ContainerCode {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||||
|
if let Ok(cmd) = CommandCode::try_from(value) {
|
||||||
|
return Ok(Self::Command(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(resp) = ResponseCode::try_from(value) {
|
||||||
|
return Ok(Self::Response(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("Unknown container code '{value:x?}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PtpSerialize for ContainerCode {
|
||||||
|
fn try_into_ptp(&self) -> io::Result<Vec<u8>> {
|
||||||
|
let value: u16 = (*self).into();
|
||||||
|
value.try_into_ptp()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> io::Result<()> {
|
||||||
|
let value: u16 = (*self).into();
|
||||||
|
value.try_write_ptp(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PtpDeserialize for ContainerCode {
|
||||||
|
fn try_from_ptp(buf: &[u8]) -> io::Result<Self> {
|
||||||
|
let mut cur = Cursor::new(buf);
|
||||||
|
let value = Self::try_read_ptp(&mut cur)?;
|
||||||
|
cur.expect_end()?;
|
||||||
|
io::Result::Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read_ptp<R: ptp_cursor::Read>(cur: &mut R) -> io::Result<Self> {
|
||||||
|
let value = <u16>::try_read_ptp(cur)?;
|
||||||
|
Self::try_from(value)
|
||||||
|
.map_err(|e: anyhow::Error| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
|
||||||
|
)]
|
||||||
|
pub enum DevicePropCode {
|
||||||
|
FujiUsbMode = 0xd16e,
|
||||||
|
FujiRawConversionRun = 0xD183,
|
||||||
|
FujiRawConversionProfile = 0xD185,
|
||||||
|
FujiCustomSetting = 0xD18C,
|
||||||
|
FujiCustomSettingName = 0xD18D,
|
||||||
|
FujiCustomSettingImageSize = 0xD18E,
|
||||||
|
FujiCustomSettingImageQuality = 0xD18F,
|
||||||
|
FujiCustomSettingDynamicRange = 0xD190,
|
||||||
|
FujiCustomSettingDynamicRangePriority = 0xD191,
|
||||||
|
FujiCustomSettingFilmSimulation = 0xD192,
|
||||||
|
FujiCustomSettingMonochromaticColorTemperature = 0xD193,
|
||||||
|
FujiCustomSettingMonochromaticColorTint = 0xD194,
|
||||||
|
FujiCustomSettingGrainEffect = 0xD195,
|
||||||
|
FujiCustomSettingColorChromeEffect = 0xD196,
|
||||||
|
FujiCustomSettingColorChromeFXBlue = 0xD197,
|
||||||
|
FujiCustomSettingSmoothSkinEffect = 0xD198,
|
||||||
|
FujiCustomSettingWhiteBalance = 0xD199,
|
||||||
|
FujiCustomSettingWhiteBalanceShiftRed = 0xD19A,
|
||||||
|
FujiCustomSettingWhiteBalanceShiftBlue = 0xD19B,
|
||||||
|
FujiCustomSettingWhiteBalanceTemperature = 0xD19C,
|
||||||
|
FujiCustomSettingHighlightTone = 0xD19D,
|
||||||
|
FujiCustomSettingShadowTone = 0xD19E,
|
||||||
|
FujiCustomSettingColor = 0xD19F,
|
||||||
|
FujiCustomSettingSharpness = 0xD1A0,
|
||||||
|
FujiCustomSettingHighISONR = 0xD1A1,
|
||||||
|
FujiCustomSettingClarity = 0xD1A2,
|
||||||
|
FujiCustomSettingLensModulationOptimizer = 0xD1A3,
|
||||||
|
FujiCustomSettingColorSpace = 0xD1A4,
|
||||||
|
// TODO: 0xD1A5 All 7s
|
||||||
|
FujiBatteryInfo2 = 0xD36B,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
|
||||||
|
)]
|
||||||
|
pub enum ObjectFormat {
|
||||||
|
None = 0x0,
|
||||||
|
FujiBackup = 0x5000,
|
||||||
|
FujiRAF = 0xf802,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ObjectFormat {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiCustomSetting {
|
||||||
|
C1 = 0x1,
|
||||||
|
C2 = 0x2,
|
||||||
|
C3 = 0x3,
|
||||||
|
C4 = 0x4,
|
||||||
|
C5 = 0x5,
|
||||||
|
C6 = 0x6,
|
||||||
|
C7 = 0x7,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
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<String> for FujiCustomSettingName {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> anyhow::Result<Self> {
|
||||||
|
if value.len() > Self::MAX_LEN {
|
||||||
|
bail!("Value '{}' exceeds max length of {}", value, Self::MAX_LEN);
|
||||||
|
}
|
||||||
|
Ok(Self(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiImageSize {
|
||||||
|
R7728x5152 = 0x7,
|
||||||
|
R7728x4344 = 0x8,
|
||||||
|
R5152x5152 = 0x9,
|
||||||
|
R6864x5152 = 0xe,
|
||||||
|
R6432x5152 = 0x10,
|
||||||
|
R5472x3648 = 0x4,
|
||||||
|
R5472x3080 = 0x5,
|
||||||
|
R3648x3648 = 0x6,
|
||||||
|
R4864x3648 = 0x12,
|
||||||
|
R4560x3648 = 0x14,
|
||||||
|
R3888x2592 = 0x1,
|
||||||
|
R3888x2184 = 0x2,
|
||||||
|
R2592x2592 = 0x3,
|
||||||
|
R3456x2592 = 0xa,
|
||||||
|
R3264x2592 = 0xc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiImageQuality {
|
||||||
|
FineRaw = 0x4,
|
||||||
|
Fine = 0x2,
|
||||||
|
NormalRaw = 0x5,
|
||||||
|
Normal = 0x3,
|
||||||
|
Raw = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiDynamicRange {
|
||||||
|
Auto = 0xffff,
|
||||||
|
HDR100 = 0x64,
|
||||||
|
HDR200 = 0xc8,
|
||||||
|
HDR400 = 0x190,
|
||||||
|
// TODO: Limit this when setting film sim
|
||||||
|
HDR800 = 0x320,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiDynamicRangePriority {
|
||||||
|
Auto = 0x8000,
|
||||||
|
// TODO: Limit this, used in conjuction with HDR800
|
||||||
|
Plus = 0x3,
|
||||||
|
Strong = 0x2,
|
||||||
|
Weak = 0x1,
|
||||||
|
Off = 0x0,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiFilmSimulation {
|
||||||
|
Provia = 0x1,
|
||||||
|
Velvia = 0x2,
|
||||||
|
Astia = 0x3,
|
||||||
|
PRONegHi = 0x4,
|
||||||
|
PRONegStd = 0x5,
|
||||||
|
Monochrome = 0x6,
|
||||||
|
MonochromeYe = 0x7,
|
||||||
|
MonochromeR = 0x8,
|
||||||
|
MonochromeG = 0x9,
|
||||||
|
Sepia = 0xa,
|
||||||
|
ClassicChrome = 0xb,
|
||||||
|
AcrosSTD = 0xc,
|
||||||
|
AcrosYe = 0xd,
|
||||||
|
AcrosR = 0xe,
|
||||||
|
AcrosG = 0xf,
|
||||||
|
Eterna = 0x10,
|
||||||
|
ClassicNegative = 0x11,
|
||||||
|
NostalgicNegative = 0x13,
|
||||||
|
EternaBleachBypass = 0x12,
|
||||||
|
RealaAce = 0x14,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiFileType {
|
||||||
|
Jpeg = 0x7,
|
||||||
|
Heif = 0x12,
|
||||||
|
Tiff8 = 0x9,
|
||||||
|
Tiff10 = 0xb,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiGrainEffect {
|
||||||
|
StrongLarge = 0x5,
|
||||||
|
WeakLarge = 0x4,
|
||||||
|
StrongSmall = 0x3,
|
||||||
|
WeakSmall = 0x2,
|
||||||
|
// TODO: God knows what the fuck is happening here.
|
||||||
|
// If I do Set 0x1 and immediately Get, camera returns 0x6 or 0x7.
|
||||||
|
#[num_enum(alternatives = [0x6, 0x7])]
|
||||||
|
Off = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiColorChromeEffect {
|
||||||
|
Strong = 0x3,
|
||||||
|
Weak = 0x2,
|
||||||
|
Off = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiColorChromeFXBlue {
|
||||||
|
Strong = 0x3,
|
||||||
|
Weak = 0x2,
|
||||||
|
Off = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiSmoothSkinEffect {
|
||||||
|
Strong = 0x3,
|
||||||
|
Weak = 0x2,
|
||||||
|
Off = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiWhiteBalanceAsShot {
|
||||||
|
False = 0x2,
|
||||||
|
True = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiWhiteBalance {
|
||||||
|
WhitePriority = 0x8020,
|
||||||
|
Auto = 0x2,
|
||||||
|
AmbiencePriority = 0x8021,
|
||||||
|
Custom1 = 0x8008,
|
||||||
|
Custom2 = 0x8009,
|
||||||
|
Custom3 = 0x800A,
|
||||||
|
Temperature = 0x8007,
|
||||||
|
Daylight = 0x4,
|
||||||
|
Shade = 0x8006,
|
||||||
|
Fluorescent1 = 0x8001,
|
||||||
|
Fluorescent2 = 0x8002,
|
||||||
|
Fluorescent3 = 0x8003,
|
||||||
|
Incandescent = 0x6,
|
||||||
|
Underwater = 0x8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
|
||||||
|
)]
|
||||||
|
pub enum FujiHighISONR {
|
||||||
|
Plus4 = 0x5000,
|
||||||
|
Plus3 = 0x6000,
|
||||||
|
Plus2 = 0x0,
|
||||||
|
Plus1 = 0x1000,
|
||||||
|
Zero = 0x2000,
|
||||||
|
Minus1 = 0x3000,
|
||||||
|
Minus2 = 0x4000,
|
||||||
|
Minus3 = 0x7000,
|
||||||
|
Minus4 = 0x8000,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiLensModulationOptimizer {
|
||||||
|
Off = 0x2,
|
||||||
|
On = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
pub enum FujiColorSpace {
|
||||||
|
SRGB = 0x2,
|
||||||
|
AdobeRGB = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
IntoPrimitive,
|
||||||
|
TryFromPrimitive,
|
||||||
|
PtpSerialize,
|
||||||
|
PtpDeserialize,
|
||||||
|
EnumIter,
|
||||||
|
)]
|
||||||
|
pub enum FujiTeleconverter {
|
||||||
|
Off = 0x2,
|
||||||
|
On = 0x1,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! fuji_i16 {
|
||||||
|
($name:ident, $min:expr, $max:expr, $step:expr, $scale:literal) => {
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PtpSerialize, PtpDeserialize)]
|
||||||
|
pub struct $name(i16);
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
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 {
|
||||||
|
type Target = i16;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::DerefMut for $name {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::TryFrom<i16> for $name {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: i16) -> anyhow::Result<Self> {
|
||||||
|
if !(Self::RAW_MIN..=Self::RAW_MAX).contains(&value) {
|
||||||
|
anyhow::bail!("Value {} is out of range", value);
|
||||||
|
}
|
||||||
|
#[allow(clippy::modulo_one)]
|
||||||
|
if (value - Self::RAW_MIN) % Self::RAW_STEP != 0 {
|
||||||
|
anyhow::bail!("Value {} is not aligned to step {}", value, Self::RAW_STEP);
|
||||||
|
}
|
||||||
|
Ok(Self(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<$name> for i16 {
|
||||||
|
fn from(value: $name) -> i16 {
|
||||||
|
*value.deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fuji_i16!(FujiMonochromaticColorTemperature, -18.0, 18.0, 1.0, 10i16);
|
||||||
|
fuji_i16!(FujiMonochromaticColorTint, -18.0, 18.0, 1.0, 10i16);
|
||||||
|
fuji_i16!(FujiWhiteBalanceShift, -9.0, 9.0, 1.0, 1i16);
|
||||||
|
fuji_i16!(FujiWhiteBalanceTemperature, 2500.0, 10000.0, 10.0, 1i16);
|
||||||
|
fuji_i16!(FujiHighlightTone, -2.0, 4.0, 0.5, 10i16);
|
||||||
|
fuji_i16!(FujiShadowTone, -2.0, 4.0, 0.5, 10i16);
|
||||||
|
fuji_i16!(FujiColor, -4.0, 4.0, 1.0, 10i16);
|
||||||
|
fuji_i16!(FujiSharpness, -4.0, 4.0, 1.0, 10i16);
|
||||||
|
fuji_i16!(FujiClarity, -5.0, 5.0, 1.0, 10i16);
|
||||||
|
|
||||||
|
#[repr(i16)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, EnumIter)]
|
||||||
|
pub enum FujiExposureOffset {
|
||||||
|
Plus3 = 3000,
|
||||||
|
Plus2_7 = 2667,
|
||||||
|
Plus2_3 = 2333,
|
||||||
|
Plus2 = 2000,
|
||||||
|
Plus1_7 = 1667,
|
||||||
|
Plus1_3 = 1333,
|
||||||
|
Plus1 = 1000,
|
||||||
|
Plus0_7 = 667,
|
||||||
|
Plus0_3 = 333,
|
||||||
|
Zero = 0,
|
||||||
|
Minus0_3 = -333,
|
||||||
|
Minus0_7 = -667,
|
||||||
|
Minus1 = -1000,
|
||||||
|
Minus1_3 = -1333,
|
||||||
|
Minus1_7 = -1667,
|
||||||
|
Minus2 = -2000,
|
||||||
|
Minus2_3 = -2333,
|
||||||
|
Minus2_7 = -2667,
|
||||||
|
Minus3 = -3000,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, Serialize, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpDeserialize,
|
||||||
|
)]
|
||||||
|
pub enum UsbMode {
|
||||||
|
RawConversion = 0x6,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
|
||||||
|
)]
|
||||||
|
#[repr(u16)]
|
||||||
|
pub enum ContainerType {
|
||||||
|
Command = 1,
|
||||||
|
Data = 2,
|
||||||
|
Response = 3,
|
||||||
|
Event = 4,
|
||||||
|
}
|
228
src/camera/ptp/mod.rs
Normal file
228
src/camera/ptp/mod.rs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
pub mod error;
|
||||||
|
pub mod hex;
|
||||||
|
pub mod structs;
|
||||||
|
|
||||||
|
use std::{cmp::min, io::Cursor, time::Duration};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use hex::{CommandCode, ContainerCode, ContainerType, DevicePropCode, ResponseCode};
|
||||||
|
use log::{debug, error, trace, warn};
|
||||||
|
use ptp_cursor::{PtpDeserialize, PtpSerialize};
|
||||||
|
use rusb::GlobalContext;
|
||||||
|
use structs::{ContainerInfo, DeviceInfo};
|
||||||
|
|
||||||
|
pub struct Ptp {
|
||||||
|
pub bus: u8,
|
||||||
|
pub address: u8,
|
||||||
|
pub interface: u8,
|
||||||
|
pub bulk_in: u8,
|
||||||
|
pub bulk_out: u8,
|
||||||
|
pub handle: rusb::DeviceHandle<GlobalContext>,
|
||||||
|
pub transaction_id: u32,
|
||||||
|
pub chunk_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ptp {
|
||||||
|
pub fn send(
|
||||||
|
&mut self,
|
||||||
|
code: CommandCode,
|
||||||
|
params: &[u32],
|
||||||
|
data: Option<&[u8]>,
|
||||||
|
) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let transaction_id = self.transaction_id;
|
||||||
|
self.send_header(code, params, transaction_id)?;
|
||||||
|
if let Some(data) = data {
|
||||||
|
self.write(ContainerType::Data, code, data, transaction_id)?;
|
||||||
|
}
|
||||||
|
let response = self.receive_response();
|
||||||
|
self.transaction_id += 1;
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_session(&mut self, session_id: u32) -> anyhow::Result<()> {
|
||||||
|
debug!("Sending OpenSession command");
|
||||||
|
self.send(CommandCode::OpenSession, &[session_id], None)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_session(&mut self, _: u32) -> anyhow::Result<()> {
|
||||||
|
debug!("Sending CloseSession command");
|
||||||
|
self.send(CommandCode::CloseSession, &[], None)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_info(&mut self) -> anyhow::Result<DeviceInfo> {
|
||||||
|
debug!("Sending GetDeviceInfo command");
|
||||||
|
let response = self.send(CommandCode::GetDeviceInfo, &[], None)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
let info = DeviceInfo::try_from_ptp(&response)?;
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_prop_value(&mut self, prop: DevicePropCode) -> anyhow::Result<Vec<u8>> {
|
||||||
|
debug!("Sending GetDevicePropValue command for property {prop:?}");
|
||||||
|
let response = self.send(CommandCode::GetDevicePropValue, &[prop.into()], None)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_prop_value(
|
||||||
|
&mut self,
|
||||||
|
prop: DevicePropCode,
|
||||||
|
value: &[u8],
|
||||||
|
) -> anyhow::Result<Vec<u8>> {
|
||||||
|
debug!("Sending GetDevicePropValue command for property {prop:?}");
|
||||||
|
let response = self.send(CommandCode::SetDevicePropValue, &[prop.into()], Some(value))?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_header(
|
||||||
|
&self,
|
||||||
|
code: CommandCode,
|
||||||
|
params: &[u32],
|
||||||
|
transaction_id: u32,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut payload = Vec::with_capacity(params.len() * 4);
|
||||||
|
for p in params {
|
||||||
|
p.try_write_ptp(&mut payload)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Sending PTP command: {:?}, transaction: {:?}, parameters ({} bytes): {:x?}",
|
||||||
|
code,
|
||||||
|
transaction_id,
|
||||||
|
payload.len(),
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
self.write(ContainerType::Command, code, &payload, transaction_id)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_response(&self) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let mut response = Vec::new();
|
||||||
|
loop {
|
||||||
|
let (container, payload) = self.read()?;
|
||||||
|
match container.kind {
|
||||||
|
ContainerType::Data => {
|
||||||
|
trace!("Response received: data ({} bytes)", payload.len());
|
||||||
|
response = payload;
|
||||||
|
}
|
||||||
|
ContainerType::Response => {
|
||||||
|
trace!("Response received: code {:?}", container.code);
|
||||||
|
|
||||||
|
if self.transaction_id != container.transaction_id {
|
||||||
|
warn!(
|
||||||
|
"Mismatched transaction_id {}, expecting {}",
|
||||||
|
container.transaction_id, self.transaction_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
match container.code {
|
||||||
|
ContainerCode::Command(_) | ContainerCode::Response(ResponseCode::Ok) => {}
|
||||||
|
ContainerCode::Response(code) => {
|
||||||
|
bail!(error::Error::Response(code.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Command completed successfully, response payload of {} bytes",
|
||||||
|
response.len(),
|
||||||
|
);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("Unexpected container type: {:?}", container.kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(
|
||||||
|
&self,
|
||||||
|
kind: ContainerType,
|
||||||
|
code: CommandCode,
|
||||||
|
payload: &[u8],
|
||||||
|
transaction_id: u32,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let container_info = ContainerInfo::new(kind, code, transaction_id, payload.len())?;
|
||||||
|
let mut buffer: Vec<u8> = container_info.try_into_ptp()?;
|
||||||
|
|
||||||
|
let first_chunk_len = min(payload.len(), self.chunk_size - ContainerInfo::SIZE);
|
||||||
|
buffer.extend_from_slice(&payload[..first_chunk_len]);
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Writing PTP {kind:?} container, code: {code:?}, transaction: {transaction_id:?}, first payload chunk ({first_chunk_len} bytes)",
|
||||||
|
);
|
||||||
|
self.handle
|
||||||
|
.write_bulk(self.bulk_out, &buffer, Duration::ZERO)?;
|
||||||
|
|
||||||
|
for chunk in payload[first_chunk_len..].chunks(self.chunk_size) {
|
||||||
|
trace!("Writing additional payload chunk ({} bytes)", chunk.len(),);
|
||||||
|
self.handle
|
||||||
|
.write_bulk(self.bulk_out, chunk, Duration::ZERO)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Write completed for code {:?}, total payload of {} bytes",
|
||||||
|
code,
|
||||||
|
payload.len()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(&self) -> anyhow::Result<(ContainerInfo, Vec<u8>)> {
|
||||||
|
let mut stack_buf = [0u8; 8 * 1024];
|
||||||
|
|
||||||
|
let n = self
|
||||||
|
.handle
|
||||||
|
.read_bulk(self.bulk_in, &mut stack_buf, Duration::ZERO)?;
|
||||||
|
let buf = &stack_buf[..n];
|
||||||
|
trace!("Read chunk ({n} bytes)");
|
||||||
|
|
||||||
|
let mut cur = Cursor::new(buf);
|
||||||
|
let container_info = ContainerInfo::try_read_ptp(&mut cur)?;
|
||||||
|
|
||||||
|
let payload_len = container_info.payload_len();
|
||||||
|
if payload_len == 0 {
|
||||||
|
trace!("No payload in container");
|
||||||
|
return Ok((container_info, Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut payload = Vec::with_capacity(payload_len);
|
||||||
|
if buf.len() > ContainerInfo::SIZE {
|
||||||
|
payload.extend_from_slice(&buf[ContainerInfo::SIZE..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
while payload.len() < payload_len {
|
||||||
|
let remaining = payload_len - payload.len();
|
||||||
|
let mut chunk = vec![0u8; min(remaining, self.chunk_size)];
|
||||||
|
let n = self
|
||||||
|
.handle
|
||||||
|
.read_bulk(self.bulk_in, &mut chunk, Duration::ZERO)?;
|
||||||
|
trace!("Read additional chunk ({n} bytes)");
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
payload.extend_from_slice(&chunk[..n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Finished reading container, total payload of {} bytes",
|
||||||
|
payload.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((container_info, payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Ptp {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
debug!("Releasing interface");
|
||||||
|
if let Err(e) = self.handle.release_interface(self.interface) {
|
||||||
|
error!("Error releasing interface: {e}");
|
||||||
|
}
|
||||||
|
debug!("Interface released");
|
||||||
|
}
|
||||||
|
}
|
78
src/camera/ptp/structs.rs
Normal file
78
src/camera/ptp/structs.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use ptp_macro::{PtpDeserialize, PtpSerialize};
|
||||||
|
|
||||||
|
use super::hex::{CommandCode, ContainerCode, ContainerType, ObjectFormat};
|
||||||
|
|
||||||
|
#[derive(Debug, PtpSerialize, PtpDeserialize)]
|
||||||
|
pub struct DeviceInfo {
|
||||||
|
pub version: u16,
|
||||||
|
pub vendor_ex_id: u32,
|
||||||
|
pub vendor_ex_version: u16,
|
||||||
|
pub vendor_extension_desc: String,
|
||||||
|
pub functional_mode: u16,
|
||||||
|
pub operations_supported: Vec<u16>,
|
||||||
|
pub events_supported: Vec<u16>,
|
||||||
|
pub device_properties_supported: Vec<u16>,
|
||||||
|
pub capture_formats: Vec<u16>,
|
||||||
|
pub image_formats: Vec<u16>,
|
||||||
|
pub manufacturer: String,
|
||||||
|
pub model: String,
|
||||||
|
pub device_version: String,
|
||||||
|
pub serial_number: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PtpSerialize, PtpDeserialize)]
|
||||||
|
pub struct ContainerInfo {
|
||||||
|
pub total_len: u32,
|
||||||
|
pub kind: ContainerType,
|
||||||
|
pub code: ContainerCode,
|
||||||
|
pub transaction_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerInfo {
|
||||||
|
pub const SIZE: usize =
|
||||||
|
size_of::<u32>() + size_of::<u16>() + size_of::<u16>() + size_of::<u32>();
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
kind: ContainerType,
|
||||||
|
code: CommandCode,
|
||||||
|
transaction_id: u32,
|
||||||
|
payload_len: usize,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let total_len = u32::try_from(Self::SIZE + payload_len)?;
|
||||||
|
let code = ContainerCode::Command(code);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
total_len,
|
||||||
|
kind,
|
||||||
|
code,
|
||||||
|
transaction_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn payload_len(&self) -> usize {
|
||||||
|
self.total_len as usize - Self::SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PtpSerialize, PtpDeserialize)]
|
||||||
|
pub struct ObjectInfo {
|
||||||
|
pub storage_id: u32,
|
||||||
|
pub object_format: ObjectFormat,
|
||||||
|
pub protection_status: u16,
|
||||||
|
pub compressed_size: u32,
|
||||||
|
pub thumb_format: u16,
|
||||||
|
pub thumb_compressed_size: u32,
|
||||||
|
pub thumb_width: u32,
|
||||||
|
pub thumb_height: u32,
|
||||||
|
pub image_width: u32,
|
||||||
|
pub image_height: u32,
|
||||||
|
pub image_bit_depth: u32,
|
||||||
|
pub parent_object: u32,
|
||||||
|
pub association_type: u16,
|
||||||
|
pub association_desc: u32,
|
||||||
|
pub sequence_number: u32,
|
||||||
|
pub filename: String,
|
||||||
|
pub date_created: String,
|
||||||
|
pub date_modified: String,
|
||||||
|
pub keywords: String,
|
||||||
|
}
|
@@ -20,30 +20,28 @@ pub enum BackupCmd {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> {
|
fn handle_export(device_id: Option<&str>, output: &Output) -> anyhow::Result<()> {
|
||||||
let camera = usb::get_camera(device_id)?;
|
let mut camera = usb::get_camera(device_id)?;
|
||||||
let mut ptp = camera.ptp_session()?;
|
|
||||||
|
|
||||||
let mut writer = output.get_writer()?;
|
let mut writer = output.get_writer()?;
|
||||||
let backup = camera.export_backup(&mut ptp)?;
|
let backup = camera.backup_export()?;
|
||||||
writer.write_all(&backup)?;
|
writer.write_all(&backup)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> {
|
fn handle_import(device_id: Option<&str>, input: &Input) -> anyhow::Result<()> {
|
||||||
let camera = usb::get_camera(device_id)?;
|
let mut camera = usb::get_camera(device_id)?;
|
||||||
let mut ptp = camera.ptp_session()?;
|
|
||||||
|
|
||||||
let mut reader = input.get_reader()?;
|
let mut reader = input.get_reader()?;
|
||||||
let mut buffer = Vec::new();
|
let mut backup = Vec::new();
|
||||||
reader.read_to_end(&mut buffer)?;
|
reader.read_to_end(&mut backup)?;
|
||||||
camera.import_backup(&mut ptp, &buffer)?;
|
camera.backup_import(&backup)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> {
|
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> anyhow::Result<()> {
|
||||||
match cmd {
|
match cmd {
|
||||||
BackupCmd::Export { output_file } => handle_export(device_id, &output_file),
|
BackupCmd::Export { output_file } => handle_export(device_id, &output_file),
|
||||||
BackupCmd::Import { input_file } => handle_import(device_id, &input_file),
|
BackupCmd::Import { input_file } => handle_import(device_id, &input_file),
|
||||||
|
@@ -8,6 +8,7 @@ pub enum Input {
|
|||||||
|
|
||||||
impl FromStr for Input {
|
impl FromStr for Input {
|
||||||
type Err = anyhow::Error;
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if s == "-" {
|
if s == "-" {
|
||||||
Ok(Self::Stdin)
|
Ok(Self::Stdin)
|
||||||
@@ -18,7 +19,7 @@ impl FromStr for Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
pub fn get_reader(&self) -> Result<Box<dyn io::Read>, anyhow::Error> {
|
pub fn get_reader(&self) -> anyhow::Result<Box<dyn io::Read>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Stdin => Ok(Box::new(io::stdin())),
|
Self::Stdin => Ok(Box::new(io::stdin())),
|
||||||
Self::Path(path) => Ok(Box::new(File::open(path)?)),
|
Self::Path(path) => Ok(Box::new(File::open(path)?)),
|
||||||
@@ -34,6 +35,7 @@ pub enum Output {
|
|||||||
|
|
||||||
impl FromStr for Output {
|
impl FromStr for Output {
|
||||||
type Err = anyhow::Error;
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if s == "-" {
|
if s == "-" {
|
||||||
Ok(Self::Stdout)
|
Ok(Self::Stdout)
|
||||||
@@ -44,7 +46,7 @@ impl FromStr for Output {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Output {
|
impl Output {
|
||||||
pub fn get_writer(&self) -> Result<Box<dyn io::Write>, anyhow::Error> {
|
pub fn get_writer(&self) -> anyhow::Result<Box<dyn io::Write>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Stdout => Ok(Box::new(io::stdout())),
|
Self::Stdout => Ok(Box::new(io::stdout())),
|
||||||
Self::Path(path) => Ok(Box::new(File::create(path)?)),
|
Self::Path(path) => Ok(Box::new(File::create(path)?)),
|
||||||
|
@@ -1,27 +1,936 @@
|
|||||||
use anyhow::bail;
|
use std::{fmt, ops::Deref, str::FromStr};
|
||||||
use clap::Args;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
use anyhow::{Context, bail};
|
||||||
pub enum SimulationSelector {
|
use clap::Args;
|
||||||
Slot(u8),
|
use serde::{Serialize, Serializer};
|
||||||
Name(String),
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
camera::ptp::hex::{
|
||||||
|
FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace,
|
||||||
|
FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority,
|
||||||
|
FujiExposureOffset, FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone,
|
||||||
|
FujiImageQuality, FujiImageSize, FujiLensModulationOptimizer,
|
||||||
|
FujiMonochromaticColorTemperature, FujiMonochromaticColorTint, FujiShadowTone,
|
||||||
|
FujiSharpness, FujiSmoothSkinEffect, FujiWhiteBalance, FujiWhiteBalanceShift,
|
||||||
|
FujiWhiteBalanceTemperature, UsbMode,
|
||||||
|
},
|
||||||
|
cli::common::suggest::get_closest,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct FilmSimulationOptions {
|
||||||
|
/// Fujifilm Film Simulation
|
||||||
|
#[clap(long)]
|
||||||
|
pub simulation: Option<FujiFilmSimulation>,
|
||||||
|
|
||||||
|
/// Monochromatic Color Temperature (only applicable to B&W film simulations)
|
||||||
|
#[clap(long)]
|
||||||
|
pub monochromatic_color_temperature: Option<FujiMonochromaticColorTemperature>,
|
||||||
|
|
||||||
|
/// Monochromatic Color Tint (only applicable to B&W film simulations)
|
||||||
|
#[clap(long)]
|
||||||
|
pub monochromatic_color_tint: Option<FujiMonochromaticColorTint>,
|
||||||
|
|
||||||
|
/// The output image resolution
|
||||||
|
#[clap(long)]
|
||||||
|
pub size: Option<FujiImageSize>,
|
||||||
|
|
||||||
|
/// The output image quality (JPEG compression level)
|
||||||
|
#[clap(long)]
|
||||||
|
pub quality: Option<FujiImageQuality>,
|
||||||
|
|
||||||
|
/// Highlight Tone
|
||||||
|
#[clap(long, allow_hyphen_values(true))]
|
||||||
|
pub highlight: Option<FujiHighlightTone>,
|
||||||
|
|
||||||
|
/// Shadow Tone
|
||||||
|
#[clap(long, allow_hyphen_values(true))]
|
||||||
|
pub shadow: Option<FujiShadowTone>,
|
||||||
|
|
||||||
|
/// Color
|
||||||
|
#[clap(long, allow_hyphen_values(true))]
|
||||||
|
pub color: Option<FujiColor>,
|
||||||
|
|
||||||
|
/// Sharpness
|
||||||
|
#[clap(long, allow_hyphen_values(true))]
|
||||||
|
pub sharpness: Option<FujiSharpness>,
|
||||||
|
|
||||||
|
/// Clarity
|
||||||
|
#[clap(long, allow_hyphen_values(true))]
|
||||||
|
pub clarity: Option<FujiClarity>,
|
||||||
|
|
||||||
|
/// White Balance
|
||||||
|
#[clap(long)]
|
||||||
|
pub white_balance: Option<FujiWhiteBalance>,
|
||||||
|
|
||||||
|
/// White Balance Shift Red
|
||||||
|
#[clap(long, allow_hyphen_values(true))]
|
||||||
|
pub white_balance_shift_red: Option<FujiWhiteBalanceShift>,
|
||||||
|
|
||||||
|
/// White Balance Shift Blue
|
||||||
|
#[clap(long, allow_hyphen_values(true))]
|
||||||
|
pub white_balance_shift_blue: Option<FujiWhiteBalanceShift>,
|
||||||
|
|
||||||
|
/// White Balance Temperature (Only used if WB is set to 'Temperature')
|
||||||
|
#[clap(long)]
|
||||||
|
pub white_balance_temperature: Option<FujiWhiteBalanceTemperature>,
|
||||||
|
|
||||||
|
/// Dynamic Range
|
||||||
|
#[clap(long)]
|
||||||
|
pub dynamic_range: Option<FujiDynamicRange>,
|
||||||
|
|
||||||
|
/// Dynamic Range Priority
|
||||||
|
#[clap(long)]
|
||||||
|
pub dynamic_range_priority: Option<FujiDynamicRangePriority>,
|
||||||
|
|
||||||
|
/// High ISO Noise Reduction
|
||||||
|
#[clap(long, allow_hyphen_values(true))]
|
||||||
|
pub noise_reduction: Option<FujiHighISONR>,
|
||||||
|
|
||||||
|
/// Grain Effect
|
||||||
|
#[clap(long)]
|
||||||
|
pub grain: Option<FujiGrainEffect>,
|
||||||
|
|
||||||
|
/// Color Chrome Effect
|
||||||
|
#[clap(long)]
|
||||||
|
pub color_chrome_effect: Option<FujiColorChromeEffect>,
|
||||||
|
|
||||||
|
/// Color Chrome FX Blue
|
||||||
|
#[clap(long)]
|
||||||
|
pub color_chrome_fx_blue: Option<FujiColorChromeFXBlue>,
|
||||||
|
|
||||||
|
/// Smooth Skin Effect
|
||||||
|
#[clap(long)]
|
||||||
|
pub smooth_skin_effect: Option<FujiSmoothSkinEffect>,
|
||||||
|
|
||||||
|
/// Lens Modulation Optimizer
|
||||||
|
#[clap(long)]
|
||||||
|
pub lens_modulation_optimizer: Option<FujiLensModulationOptimizer>,
|
||||||
|
|
||||||
|
/// Color Space
|
||||||
|
#[clap(long)]
|
||||||
|
pub color_space: Option<FujiColorSpace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for SimulationSelector {
|
impl fmt::Display for FujiCustomSetting {
|
||||||
type Err = anyhow::Error;
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
Self::C1 => write!(f, "C1"),
|
||||||
if let Ok(slot) = s.parse::<u8>() {
|
Self::C2 => write!(f, "C2"),
|
||||||
return Ok(Self::Slot(slot));
|
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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.is_empty() {
|
|
||||||
bail!("Simulation name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self::Name(s.to_string()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
impl FromStr for FujiCustomSetting {
|
||||||
pub struct FilmSimulationOptions {}
|
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"),
|
||||||
|
Self::HDR800 => write!(f, "HDR800"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
"800" | "hdr800" | "dr800" => return Ok(Self::HDR800),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
Self::Plus => writeln!(f, "Plus"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
"plus" => return Ok(Self::Plus),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::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) = 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}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for FujiExposureOffset {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> anyhow::Result<Self> {
|
||||||
|
let input = s
|
||||||
|
.trim()
|
||||||
|
.parse::<f32>()
|
||||||
|
.with_context(|| format!("Invalid numeric value '{s}'"))?;
|
||||||
|
|
||||||
|
let round = (input * 10.0).round() / 10.0;
|
||||||
|
|
||||||
|
match round {
|
||||||
|
3.0 => return Ok(Self::Plus3),
|
||||||
|
2.7 => return Ok(Self::Plus2_7),
|
||||||
|
2.3 => return Ok(Self::Plus2_3),
|
||||||
|
2.0 => return Ok(Self::Plus2),
|
||||||
|
1.7 => return Ok(Self::Plus1_7),
|
||||||
|
1.3 => return Ok(Self::Plus1_3),
|
||||||
|
1.0 => return Ok(Self::Plus1),
|
||||||
|
0.7 => return Ok(Self::Plus0_7),
|
||||||
|
0.3 => return Ok(Self::Plus0_3),
|
||||||
|
0.0 => return Ok(Self::Zero),
|
||||||
|
-0.3 => return Ok(Self::Minus0_3),
|
||||||
|
-0.7 => return Ok(Self::Minus0_7),
|
||||||
|
-1.0 => return Ok(Self::Minus1),
|
||||||
|
-1.3 => return Ok(Self::Minus1_3),
|
||||||
|
-1.7 => return Ok(Self::Minus1_7),
|
||||||
|
-2.0 => return Ok(Self::Minus2),
|
||||||
|
-2.3 => return Ok(Self::Minus2_3),
|
||||||
|
-2.7 => return Ok(Self::Minus2_7),
|
||||||
|
-3.0 => return Ok(Self::Minus3),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
|
||||||
|
if let Some(best) = get_closest(s, &choices) {
|
||||||
|
bail!("Unknown exposure offset '{s}'. Did you mean '{best}'?");
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("Unknown exposure offset '{s}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FujiExposureOffset {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let val = match self {
|
||||||
|
Self::Minus3 => -3.0,
|
||||||
|
Self::Minus2_7 => -2.7,
|
||||||
|
Self::Minus2_3 => -2.3,
|
||||||
|
Self::Minus2 => -2.0,
|
||||||
|
Self::Minus1_7 => -1.7,
|
||||||
|
Self::Minus1_3 => -1.3,
|
||||||
|
Self::Minus1 => -1.0,
|
||||||
|
Self::Minus0_7 => -0.7,
|
||||||
|
Self::Minus0_3 => -0.3,
|
||||||
|
Self::Zero => 0.0,
|
||||||
|
Self::Plus0_3 => 0.3,
|
||||||
|
Self::Plus0_7 => 0.7,
|
||||||
|
Self::Plus1 => 1.0,
|
||||||
|
Self::Plus1_3 => 1.3,
|
||||||
|
Self::Plus1_7 => 1.7,
|
||||||
|
Self::Plus2 => 2.0,
|
||||||
|
Self::Plus2_3 => 2.3,
|
||||||
|
Self::Plus2_7 => 2.7,
|
||||||
|
Self::Plus3 => 3.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{val}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for FujiExposureOffset {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let val = match self {
|
||||||
|
Self::Minus3 => -3.0,
|
||||||
|
Self::Minus2_7 => -2.7,
|
||||||
|
Self::Minus2_3 => -2.3,
|
||||||
|
Self::Minus2 => -2.0,
|
||||||
|
Self::Minus1_7 => -1.7,
|
||||||
|
Self::Minus1_3 => -1.3,
|
||||||
|
Self::Minus1 => -1.0,
|
||||||
|
Self::Minus0_7 => -0.7,
|
||||||
|
Self::Minus0_3 => -0.3,
|
||||||
|
Self::Zero => 0.0,
|
||||||
|
Self::Plus0_3 => 0.3,
|
||||||
|
Self::Plus0_7 => 0.7,
|
||||||
|
Self::Plus1 => 1.0,
|
||||||
|
Self::Plus1_3 => 1.3,
|
||||||
|
Self::Plus1_7 => 1.7,
|
||||||
|
Self::Plus2 => 2.0,
|
||||||
|
Self::Plus2_3 => 2.3,
|
||||||
|
Self::Plus2_7 => 2.7,
|
||||||
|
Self::Plus3 => 3.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
serializer.serialize_f32(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,2 +1,3 @@
|
|||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod film;
|
pub mod film;
|
||||||
|
pub mod suggest;
|
||||||
|
29
src/cli/common/suggest.rs
Normal file
29
src/cli/common/suggest.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@@ -1,12 +1,6 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{camera::CameraInfoListItem, usb};
|
||||||
hardware::{CameraImpl, UsbMode},
|
|
||||||
usb,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Clone, Copy)]
|
#[derive(Subcommand, Debug, Clone, Copy)]
|
||||||
pub enum DeviceCmd {
|
pub enum DeviceCmd {
|
||||||
@@ -19,37 +13,8 @@ pub enum DeviceCmd {
|
|||||||
Info,
|
Info,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
fn handle_list(json: bool) -> anyhow::Result<()> {
|
||||||
pub struct CameraItemRepr {
|
let cameras: Vec<CameraInfoListItem> = usb::get_connected_cameras()?
|
||||||
pub name: String,
|
|
||||||
pub id: String,
|
|
||||||
pub vendor_id: String,
|
|
||||||
pub product_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Box<dyn CameraImpl>> for CameraItemRepr {
|
|
||||||
fn from(camera: &Box<dyn CameraImpl>) -> Self {
|
|
||||||
Self {
|
|
||||||
id: camera.usb_id(),
|
|
||||||
name: camera.id().name.to_string(),
|
|
||||||
vendor_id: format!("0x{:04x}", camera.id().vendor),
|
|
||||||
product_id: format!("0x{:04x}", camera.id().product),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CameraItemRepr {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{} ({}:{}) (ID: {})",
|
|
||||||
self.name, self.vendor_id, self.product_id, self.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_list(json: bool) -> Result<(), anyhow::Error> {
|
|
||||||
let cameras: Vec<CameraItemRepr> = usb::get_connected_camers()?
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(std::convert::Into::into)
|
.map(std::convert::Into::into)
|
||||||
.collect();
|
.collect();
|
||||||
@@ -60,11 +25,10 @@ fn handle_list(json: bool) -> Result<(), anyhow::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cameras.is_empty() {
|
if cameras.is_empty() {
|
||||||
println!("No supported cameras connected.");
|
println!("No supported cameras connected");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Connected cameras:");
|
|
||||||
for d in cameras {
|
for d in cameras {
|
||||||
println!("- {d}");
|
println!("- {d}");
|
||||||
}
|
}
|
||||||
@@ -72,56 +36,10 @@ fn handle_list(json: bool) -> Result<(), anyhow::Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
|
||||||
pub struct CameraRepr {
|
let mut camera = usb::get_camera(device_id)?;
|
||||||
#[serde(flatten)]
|
|
||||||
pub device: CameraItemRepr,
|
|
||||||
|
|
||||||
pub manufacturer: String,
|
let repr = camera.info_get()?;
|
||||||
pub model: String,
|
|
||||||
pub device_version: String,
|
|
||||||
pub serial_number: String,
|
|
||||||
pub mode: UsbMode,
|
|
||||||
pub battery: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CameraRepr {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
writeln!(f, "Name: {}", self.device.name)?;
|
|
||||||
writeln!(f, "ID: {}", self.device.id)?;
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Vendor ID: {}, Product ID: {}",
|
|
||||||
self.device.vendor_id, self.device.product_id
|
|
||||||
)?;
|
|
||||||
writeln!(f, "Manufacturer: {}", self.manufacturer)?;
|
|
||||||
writeln!(f, "Model: {}", self.model)?;
|
|
||||||
writeln!(f, "Version: {}", self.device_version)?;
|
|
||||||
writeln!(f, "Serial Number: {}", self.serial_number)?;
|
|
||||||
writeln!(f, "Mode: {}", self.mode)?;
|
|
||||||
write!(f, "Battery: {}%", self.battery)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> {
|
|
||||||
let camera = usb::get_camera(device_id)?;
|
|
||||||
let mut ptp = camera.ptp();
|
|
||||||
|
|
||||||
let info = camera.get_info(&mut ptp)?;
|
|
||||||
|
|
||||||
let mut ptp = camera.open_session(ptp)?;
|
|
||||||
let mode = camera.get_usb_mode(&mut ptp)?;
|
|
||||||
let battery = camera.get_battery_info(&mut ptp)?;
|
|
||||||
|
|
||||||
let repr = CameraRepr {
|
|
||||||
device: (&camera).into(),
|
|
||||||
manufacturer: info.Manufacturer.clone(),
|
|
||||||
model: info.Model.clone(),
|
|
||||||
device_version: info.DeviceVersion.clone(),
|
|
||||||
serial_number: info.SerialNumber,
|
|
||||||
mode,
|
|
||||||
battery,
|
|
||||||
};
|
|
||||||
|
|
||||||
if json {
|
if json {
|
||||||
println!("{}", serde_json::to_string_pretty(&repr)?);
|
println!("{}", serde_json::to_string_pretty(&repr)?);
|
||||||
@@ -132,7 +50,7 @@ fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> {
|
pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
|
||||||
match cmd {
|
match cmd {
|
||||||
DeviceCmd::List => handle_list(json),
|
DeviceCmd::List => handle_list(json),
|
||||||
DeviceCmd::Info => handle_info(json, device_id),
|
DeviceCmd::Info => handle_info(json, device_id),
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
mod common;
|
|
||||||
|
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
|
pub mod common;
|
||||||
pub mod device;
|
pub mod device;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod simulation;
|
pub mod simulation;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{ArgAction, Parser, Subcommand};
|
||||||
|
|
||||||
use backup::BackupCmd;
|
use backup::BackupCmd;
|
||||||
use device::DeviceCmd;
|
use device::DeviceCmd;
|
||||||
@@ -23,13 +22,9 @@ pub struct Cli {
|
|||||||
#[arg(long, short = 'j', global = true)]
|
#[arg(long, short = 'j', global = true)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
|
|
||||||
/// Only log warnings and errors
|
/// Log extra debugging information (multiple instances increase verbosity)
|
||||||
#[arg(long, short = 'q', global = true, conflicts_with = "verbose")]
|
#[arg(long, short = 'v', action = ArgAction::Count, global = true)]
|
||||||
pub quiet: bool,
|
pub verbose: u8,
|
||||||
|
|
||||||
/// Log extra debugging information
|
|
||||||
#[arg(long, short = 'v', global = true, conflicts_with = "quiet")]
|
|
||||||
pub verbose: bool,
|
|
||||||
|
|
||||||
/// Manually specify target device
|
/// Manually specify target device
|
||||||
#[arg(long, short = 'd', global = true)]
|
#[arg(long, short = 'd', global = true)]
|
||||||
|
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use super::common::{
|
use super::common::{
|
||||||
file::{Input, Output},
|
file::{Input, Output},
|
||||||
film::{FilmSimulationOptions, SimulationSelector},
|
film::FilmSimulationOptions,
|
||||||
};
|
};
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ use clap::Args;
|
|||||||
pub struct RenderCmd {
|
pub struct RenderCmd {
|
||||||
/// Simulation number or name
|
/// Simulation number or name
|
||||||
#[arg(long, conflicts_with = "simulation_file")]
|
#[arg(long, conflicts_with = "simulation_file")]
|
||||||
simulation: Option<SimulationSelector>,
|
simulation: Option<u8>,
|
||||||
|
|
||||||
/// Path to exported simulation
|
/// Path to exported simulation
|
||||||
#[arg(long, conflicts_with = "simulation")]
|
#[arg(long, conflicts_with = "simulation")]
|
||||||
|
@@ -1,8 +1,13 @@
|
|||||||
|
use crate::{
|
||||||
|
camera::ptp::hex::{FujiCustomSetting, FujiCustomSettingName},
|
||||||
|
usb,
|
||||||
|
};
|
||||||
|
|
||||||
use super::common::{
|
use super::common::{
|
||||||
file::{Input, Output},
|
file::{Input, Output},
|
||||||
film::{FilmSimulationOptions, SimulationSelector},
|
film::FilmSimulationOptions,
|
||||||
};
|
};
|
||||||
use clap::Subcommand;
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum SimulationCmd {
|
pub enum SimulationCmd {
|
||||||
@@ -13,15 +18,18 @@ pub enum SimulationCmd {
|
|||||||
/// Get simulation
|
/// Get simulation
|
||||||
#[command(alias = "g")]
|
#[command(alias = "g")]
|
||||||
Get {
|
Get {
|
||||||
/// Simulation number or name
|
/// Simulation slot number
|
||||||
simulation: SimulationSelector,
|
slot: FujiCustomSetting,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Set simulation parameters
|
/// Set simulation parameters
|
||||||
#[command(alias = "s")]
|
#[command(alias = "s")]
|
||||||
Set {
|
Set {
|
||||||
/// Simulation number or name
|
/// Simulation slot number
|
||||||
simulation: SimulationSelector,
|
slot: FujiCustomSetting,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
set_film_simulation_options: SetFilmSimulationOptions,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
film_simulation_options: FilmSimulationOptions,
|
film_simulation_options: FilmSimulationOptions,
|
||||||
@@ -30,8 +38,8 @@ pub enum SimulationCmd {
|
|||||||
/// Export simulation
|
/// Export simulation
|
||||||
#[command(alias = "e")]
|
#[command(alias = "e")]
|
||||||
Export {
|
Export {
|
||||||
/// Simulation number or name
|
/// Simulation slot number
|
||||||
simulation: SimulationSelector,
|
slot: FujiCustomSetting,
|
||||||
|
|
||||||
/// Output file (use '-' to write to stdout)
|
/// Output file (use '-' to write to stdout)
|
||||||
output_file: Output,
|
output_file: Output,
|
||||||
@@ -40,10 +48,93 @@ pub enum SimulationCmd {
|
|||||||
/// Import simulation
|
/// Import simulation
|
||||||
#[command(alias = "i")]
|
#[command(alias = "i")]
|
||||||
Import {
|
Import {
|
||||||
/// Simulation number
|
/// Simulation slot number
|
||||||
slot: u8,
|
slot: FujiCustomSetting,
|
||||||
|
|
||||||
/// Input file (use '-' to read from stdin)
|
/// Input file (use '-' to read from stdin)
|
||||||
input_file: Input,
|
input_file: Input,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct SetFilmSimulationOptions {
|
||||||
|
/// The name of the slot
|
||||||
|
#[clap(long)]
|
||||||
|
pub name: Option<FujiCustomSettingName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_list(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
|
||||||
|
let mut camera = usb::get_camera(device_id)?;
|
||||||
|
let slots = camera.simulation_list()?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&slots)?);
|
||||||
|
} else {
|
||||||
|
for repr in slots {
|
||||||
|
println!("- {repr}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_get(json: bool, device_id: Option<&str>, slot: FujiCustomSetting) -> anyhow::Result<()> {
|
||||||
|
let mut camera = usb::get_camera(device_id)?;
|
||||||
|
let repr = camera.simulation_get(slot)?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&repr)?);
|
||||||
|
} else {
|
||||||
|
println!("{repr}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cognitive_complexity)]
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn handle_set(
|
||||||
|
device_id: Option<&str>,
|
||||||
|
slot: FujiCustomSetting,
|
||||||
|
set_options: &SetFilmSimulationOptions,
|
||||||
|
options: &FilmSimulationOptions,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut camera = usb::get_camera(device_id)?;
|
||||||
|
camera.simulation_set(slot, set_options, options)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_export(
|
||||||
|
_device_id: Option<&str>,
|
||||||
|
_slot: FujiCustomSetting,
|
||||||
|
_output: &Output,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_import(
|
||||||
|
_device_id: Option<&str>,
|
||||||
|
_slot: FujiCustomSetting,
|
||||||
|
_input: &Input,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(cmd: SimulationCmd, json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
|
||||||
|
match cmd {
|
||||||
|
SimulationCmd::List => handle_list(json, device_id),
|
||||||
|
SimulationCmd::Get { slot } => handle_get(json, device_id, slot),
|
||||||
|
SimulationCmd::Set {
|
||||||
|
slot,
|
||||||
|
set_film_simulation_options,
|
||||||
|
film_simulation_options,
|
||||||
|
} => handle_set(
|
||||||
|
device_id,
|
||||||
|
slot,
|
||||||
|
&set_film_simulation_options,
|
||||||
|
&film_simulation_options,
|
||||||
|
),
|
||||||
|
SimulationCmd::Export { slot, output_file } => handle_export(device_id, slot, &output_file),
|
||||||
|
SimulationCmd::Import { slot, input_file } => handle_import(device_id, slot, &input_file),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,385 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fmt,
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::bail;
|
|
||||||
use libptp::{DeviceInfo, StandardCommandCode};
|
|
||||||
use log::{debug, error};
|
|
||||||
use rusb::{DeviceDescriptor, GlobalContext};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct CameraId {
|
|
||||||
pub name: &'static str,
|
|
||||||
pub vendor: u16,
|
|
||||||
pub product: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
type CameraFactory = fn(rusb::Device<GlobalContext>) -> Result<Box<dyn CameraImpl>, anyhow::Error>;
|
|
||||||
|
|
||||||
pub struct SupportedCamera {
|
|
||||||
pub id: CameraId,
|
|
||||||
pub factory: CameraFactory,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera {
|
|
||||||
id: FUJIFILM_XT5,
|
|
||||||
factory: |d| FujifilmXT5::new_boxed(&d),
|
|
||||||
}];
|
|
||||||
|
|
||||||
impl SupportedCamera {
|
|
||||||
pub fn matches_descriptor(&self, descriptor: &DeviceDescriptor) -> bool {
|
|
||||||
descriptor.vendor_id() == self.id.vendor && descriptor.product_id() == self.id.product
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const TIMEOUT: Duration = Duration::from_millis(500);
|
|
||||||
|
|
||||||
#[repr(u32)]
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum DevicePropCode {
|
|
||||||
FujiUsbMode = 0xd16e,
|
|
||||||
FujiBatteryInfo2 = 0xD36B,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
|
||||||
pub enum UsbMode {
|
|
||||||
RawConversion, // mode == 6
|
|
||||||
Unsupported,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u32> for UsbMode {
|
|
||||||
fn from(val: u32) -> Self {
|
|
||||||
match val {
|
|
||||||
6 => Self::RawConversion,
|
|
||||||
_ => Self::Unsupported,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
Self::Unsupported => "Unsupported USB Mode",
|
|
||||||
};
|
|
||||||
write!(f, "{s}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Ptp {
|
|
||||||
ptp: libptp::Camera<GlobalContext>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Ptp {
|
|
||||||
type Target = libptp::Camera<GlobalContext>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.ptp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Ptp {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.ptp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<libptp::Camera<GlobalContext>> for Ptp {
|
|
||||||
fn from(ptp: libptp::Camera<GlobalContext>) -> Self {
|
|
||||||
Self { ptp }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionCloseFn =
|
|
||||||
Box<dyn FnOnce(u32, &mut libptp::Camera<GlobalContext>) -> Result<(), anyhow::Error>>;
|
|
||||||
|
|
||||||
pub struct PtpSession {
|
|
||||||
ptp: libptp::Camera<GlobalContext>,
|
|
||||||
session_id: u32,
|
|
||||||
close_fn: Option<SessionCloseFn>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for PtpSession {
|
|
||||||
type Target = libptp::Camera<GlobalContext>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.ptp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for PtpSession {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.ptp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for PtpSession {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Some(close_fn) = self.close_fn.take() {
|
|
||||||
// take ownership
|
|
||||||
if let Err(e) = close_fn(self.session_id, &mut self.ptp) {
|
|
||||||
error!("Error closing session {}: {}", self.session_id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait CameraImpl {
|
|
||||||
fn id(&self) -> &'static CameraId;
|
|
||||||
|
|
||||||
fn device(&self) -> &rusb::Device<rusb::GlobalContext>;
|
|
||||||
|
|
||||||
fn usb_id(&self) -> String {
|
|
||||||
let bus = self.device().bus_number();
|
|
||||||
let address = self.device().address();
|
|
||||||
format!("{bus}.{address}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ptp(&self) -> Ptp;
|
|
||||||
|
|
||||||
fn ptp_session(&self) -> Result<PtpSession, anyhow::Error>;
|
|
||||||
|
|
||||||
fn get_info(&self, ptp: &mut Ptp) -> Result<DeviceInfo, anyhow::Error> {
|
|
||||||
debug!("Sending GetDeviceInfo command");
|
|
||||||
let response = ptp.command(StandardCommandCode::GetDeviceInfo, &[], None, Some(TIMEOUT))?;
|
|
||||||
debug!("Received response with {} bytes", response.len());
|
|
||||||
|
|
||||||
let info = DeviceInfo::decode(&response)?;
|
|
||||||
Ok(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_session_id(&self) -> u32;
|
|
||||||
|
|
||||||
fn open_session(&self, ptp: Ptp) -> Result<PtpSession, anyhow::Error> {
|
|
||||||
let session_id = self.next_session_id();
|
|
||||||
let mut ptp = ptp.ptp;
|
|
||||||
|
|
||||||
debug!("Opening session with id {session_id}");
|
|
||||||
ptp.command(
|
|
||||||
StandardCommandCode::OpenSession,
|
|
||||||
&[session_id],
|
|
||||||
None,
|
|
||||||
Some(TIMEOUT),
|
|
||||||
)?;
|
|
||||||
debug!("Session {session_id} open");
|
|
||||||
|
|
||||||
let close_fn: Option<SessionCloseFn> = Some(Box::new(move |_, ptp| {
|
|
||||||
debug!("Closing session with id {session_id}");
|
|
||||||
ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?;
|
|
||||||
debug!("Session {session_id} closed");
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
Ok(PtpSession {
|
|
||||||
ptp,
|
|
||||||
session_id,
|
|
||||||
close_fn,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_prop_value_raw(
|
|
||||||
&self,
|
|
||||||
ptp: &mut PtpSession,
|
|
||||||
prop: DevicePropCode,
|
|
||||||
) -> Result<Vec<u8>, anyhow::Error> {
|
|
||||||
debug!("Getting property {prop:?}");
|
|
||||||
|
|
||||||
let response = ptp.command(
|
|
||||||
StandardCommandCode::GetDevicePropValue,
|
|
||||||
&[prop as u32],
|
|
||||||
None,
|
|
||||||
Some(TIMEOUT),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
debug!("Received response with {} bytes", response.len());
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_prop_value_scalar(
|
|
||||||
&self,
|
|
||||||
ptp: &mut PtpSession,
|
|
||||||
prop: DevicePropCode,
|
|
||||||
) -> Result<u32, anyhow::Error> {
|
|
||||||
let data = self.get_prop_value_raw(ptp, prop)?;
|
|
||||||
|
|
||||||
match data.len() {
|
|
||||||
1 => Ok(u32::from(data[0])),
|
|
||||||
2 => Ok(u32::from(u16::from_le_bytes([data[0], data[1]]))),
|
|
||||||
4 => Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])),
|
|
||||||
n => bail!("Cannot parse property {prop:?} as scalar: {n} bytes"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_usb_mode(&self, ptp: &mut PtpSession) -> Result<UsbMode, anyhow::Error> {
|
|
||||||
let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?;
|
|
||||||
Ok(result.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_battery_info(&self, ptp: &mut PtpSession) -> Result<u32, anyhow::Error> {
|
|
||||||
let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?;
|
|
||||||
debug!("Raw battery data: {data:?}");
|
|
||||||
|
|
||||||
if data.len() < 3 {
|
|
||||||
bail!("Battery info payload too short");
|
|
||||||
}
|
|
||||||
|
|
||||||
let utf16: Vec<u16> = data[1..]
|
|
||||||
.chunks(2)
|
|
||||||
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
|
||||||
.take_while(|&c| c != 0)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
debug!("Decoded UTF-16 units: {utf16:?}");
|
|
||||||
|
|
||||||
let utf8_string = String::from_utf16(&utf16)?;
|
|
||||||
debug!("Decoded UTF-16 string: {utf8_string}");
|
|
||||||
|
|
||||||
let percentage: u32 = utf8_string
|
|
||||||
.split(',')
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Failed to parse battery percentage"))?
|
|
||||||
.parse()?;
|
|
||||||
|
|
||||||
Ok(percentage)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn export_backup(&self, ptp: &mut PtpSession) -> Result<Vec<u8>, anyhow::Error> {
|
|
||||||
const HANDLE: u32 = 0x0;
|
|
||||||
|
|
||||||
debug!("Getting object info for backup");
|
|
||||||
|
|
||||||
let info = ptp.command(
|
|
||||||
StandardCommandCode::GetObjectInfo,
|
|
||||||
&[HANDLE],
|
|
||||||
None,
|
|
||||||
Some(TIMEOUT),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
debug!("Got object info, {} bytes", info.len());
|
|
||||||
|
|
||||||
debug!("Downloading backup object");
|
|
||||||
|
|
||||||
let object = ptp.command(
|
|
||||||
StandardCommandCode::GetObject,
|
|
||||||
&[HANDLE],
|
|
||||||
None,
|
|
||||||
Some(TIMEOUT),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
debug!("Downloaded backup object ({} bytes)", object.len());
|
|
||||||
|
|
||||||
Ok(object)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn import_backup(&self, ptp: &mut PtpSession, buffer: &[u8]) -> Result<(), anyhow::Error> {
|
|
||||||
todo!("This is currently broken");
|
|
||||||
|
|
||||||
debug!("Preparing ObjectInfo header for backup");
|
|
||||||
|
|
||||||
let mut obj_info = vec![0u8; 1088];
|
|
||||||
let mut offset = 0;
|
|
||||||
|
|
||||||
let padding0: u32 = 0x0;
|
|
||||||
let object_format: u16 = 0x5000;
|
|
||||||
let padding1: u16 = 0x0;
|
|
||||||
|
|
||||||
obj_info[offset..offset + size_of::<u32>()].copy_from_slice(&padding0.to_le_bytes());
|
|
||||||
offset += size_of::<u32>();
|
|
||||||
obj_info[offset..offset + size_of::<u16>()].copy_from_slice(&object_format.to_le_bytes());
|
|
||||||
offset += size_of::<u16>();
|
|
||||||
obj_info[offset..offset + size_of::<u16>()].copy_from_slice(&padding1.to_le_bytes());
|
|
||||||
offset += size_of::<u16>();
|
|
||||||
obj_info[offset..offset + size_of::<u32>()]
|
|
||||||
.copy_from_slice(&u32::try_from(buffer.len())?.to_le_bytes());
|
|
||||||
|
|
||||||
let param0: u32 = 0x0;
|
|
||||||
let param1: u32 = 0x0;
|
|
||||||
|
|
||||||
debug!("Sending ObjectInfo for backup");
|
|
||||||
|
|
||||||
ptp.command(
|
|
||||||
libptp::StandardCommandCode::SendObjectInfo,
|
|
||||||
&[param0, param1],
|
|
||||||
Some(&obj_info),
|
|
||||||
Some(TIMEOUT),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
debug!("Sending backup payload ({} bytes)", buffer.len());
|
|
||||||
|
|
||||||
ptp.command(
|
|
||||||
libptp::StandardCommandCode::SendObject,
|
|
||||||
&[],
|
|
||||||
Some(buffer),
|
|
||||||
Some(TIMEOUT),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! default_camera_impl {
|
|
||||||
(
|
|
||||||
$const_name:ident,
|
|
||||||
$struct_name:ident,
|
|
||||||
$vendor:expr,
|
|
||||||
$product:expr,
|
|
||||||
$display_name:expr
|
|
||||||
) => {
|
|
||||||
pub const $const_name: CameraId = CameraId {
|
|
||||||
name: $display_name,
|
|
||||||
vendor: $vendor,
|
|
||||||
product: $product,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct $struct_name {
|
|
||||||
device: rusb::Device<rusb::GlobalContext>,
|
|
||||||
session_counter: std::sync::atomic::AtomicU32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl $struct_name {
|
|
||||||
pub fn new_boxed(
|
|
||||||
rusb_device: &rusb::Device<rusb::GlobalContext>,
|
|
||||||
) -> Result<Box<dyn CameraImpl>, anyhow::Error> {
|
|
||||||
let session_counter = std::sync::atomic::AtomicU32::new(1);
|
|
||||||
|
|
||||||
let handle = rusb_device.open()?;
|
|
||||||
let device = handle.device();
|
|
||||||
|
|
||||||
Ok(Box::new(Self {
|
|
||||||
device,
|
|
||||||
session_counter,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CameraImpl for $struct_name {
|
|
||||||
fn id(&self) -> &'static CameraId {
|
|
||||||
&$const_name
|
|
||||||
}
|
|
||||||
|
|
||||||
fn device(&self) -> &rusb::Device<rusb::GlobalContext> {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ptp(&self) -> Ptp {
|
|
||||||
libptp::Camera::new(&self.device).unwrap().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ptp_session(&self) -> Result<PtpSession, anyhow::Error> {
|
|
||||||
let ptp = self.ptp();
|
|
||||||
self.open_session(ptp)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_session_id(&self) -> u32 {
|
|
||||||
self.session_counter
|
|
||||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5");
|
|
21
src/log.rs
21
src/log.rs
@@ -6,16 +6,21 @@ use log4rs::{
|
|||||||
encode::pattern::PatternEncoder,
|
encode::pattern::PatternEncoder,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> {
|
pub fn init(verbose: u8) -> anyhow::Result<()> {
|
||||||
let level = if quiet {
|
let level = match verbose {
|
||||||
LevelFilter::Warn
|
0 => LevelFilter::Warn,
|
||||||
} else if verbose {
|
1 => LevelFilter::Info,
|
||||||
LevelFilter::Debug
|
2 => LevelFilter::Debug,
|
||||||
} else {
|
_ => LevelFilter::Trace,
|
||||||
LevelFilter::Info
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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()
|
let console = ConsoleAppender::builder()
|
||||||
.encoder(encoder)
|
.encoder(encoder)
|
||||||
|
11
src/main.rs
11
src/main.rs
@@ -4,22 +4,25 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::Commands;
|
use cli::Commands;
|
||||||
|
|
||||||
|
mod camera;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod hardware;
|
|
||||||
mod log;
|
mod log;
|
||||||
mod usb;
|
mod usb;
|
||||||
|
|
||||||
fn main() -> Result<(), anyhow::Error> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let cli = cli::Cli::parse();
|
let cli = cli::Cli::parse();
|
||||||
|
|
||||||
log::init(cli.quiet, cli.verbose)?;
|
log::init(cli.verbose)?;
|
||||||
|
|
||||||
let device_id = cli.device.as_deref();
|
let device_id = cli.device.as_deref();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?,
|
Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?,
|
||||||
Commands::Backup(backup_cmd) => cli::backup::handle(backup_cmd, device_id)?,
|
Commands::Backup(backup_cmd) => cli::backup::handle(backup_cmd, device_id)?,
|
||||||
_ => todo!(),
|
Commands::Simulation(simulation_cmd) => {
|
||||||
|
cli::simulation::handle(simulation_cmd, cli.json, device_id)?;
|
||||||
|
}
|
||||||
|
Commands::Render(_) => todo!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@@ -1,28 +1,32 @@
|
|||||||
use anyhow::{anyhow, bail};
|
use anyhow::{anyhow, bail};
|
||||||
|
|
||||||
use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS};
|
use crate::camera::Camera;
|
||||||
|
|
||||||
pub fn get_connected_camers() -> Result<Vec<Box<dyn crate::hardware::CameraImpl>>, anyhow::Error> {
|
pub fn find_endpoint(
|
||||||
|
interface_descriptor: &rusb::InterfaceDescriptor<'_>,
|
||||||
|
direction: rusb::Direction,
|
||||||
|
transfer_type: rusb::TransferType,
|
||||||
|
) -> Result<u8, rusb::Error> {
|
||||||
|
interface_descriptor
|
||||||
|
.endpoint_descriptors()
|
||||||
|
.find(|ep| ep.direction() == direction && ep.transfer_type() == transfer_type)
|
||||||
|
.map(|x| x.address())
|
||||||
|
.ok_or(rusb::Error::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_connected_cameras() -> anyhow::Result<Vec<Camera>> {
|
||||||
let mut connected_cameras = Vec::new();
|
let mut connected_cameras = Vec::new();
|
||||||
|
|
||||||
for device in rusb::devices()?.iter() {
|
for device in rusb::devices()?.iter() {
|
||||||
let Ok(descriptor) = device.device_descriptor() else {
|
if let Ok(camera) = Camera::try_from(&device) {
|
||||||
continue;
|
connected_cameras.push(camera);
|
||||||
};
|
|
||||||
|
|
||||||
for camera in SUPPORTED_CAMERAS {
|
|
||||||
if camera.matches_descriptor(&descriptor) {
|
|
||||||
let camera = (camera.factory)(device)?;
|
|
||||||
connected_cameras.push(camera);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(connected_cameras)
|
Ok(connected_cameras)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_connected_camera_by_id(id: &str) -> Result<Box<dyn CameraImpl>, anyhow::Error> {
|
pub fn get_connected_camera_by_id(id: &str) -> anyhow::Result<Camera> {
|
||||||
let parts: Vec<&str> = id.split('.').collect();
|
let parts: Vec<&str> = id.split('.').collect();
|
||||||
if parts.len() != 2 {
|
if parts.len() != 2 {
|
||||||
bail!("Invalid device id format: {id}");
|
bail!("Invalid device id format: {id}");
|
||||||
@@ -33,28 +37,19 @@ pub fn get_connected_camera_by_id(id: &str) -> Result<Box<dyn CameraImpl>, anyho
|
|||||||
|
|
||||||
for device in rusb::devices()?.iter() {
|
for device in rusb::devices()?.iter() {
|
||||||
if device.bus_number() == bus && device.address() == address {
|
if device.bus_number() == bus && device.address() == address {
|
||||||
let descriptor = device.device_descriptor()?;
|
return Camera::try_from(&device);
|
||||||
|
|
||||||
for camera in SUPPORTED_CAMERAS {
|
|
||||||
if camera.matches_descriptor(&descriptor) {
|
|
||||||
let camera = (camera.factory)(device)?;
|
|
||||||
return Ok(camera);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("Device found at {id} but is not supported");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!("No device found with id: {id}");
|
bail!("No device found with id: {id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_camera(device_id: Option<&str>) -> Result<Box<dyn CameraImpl>, anyhow::Error> {
|
pub fn get_camera(device_id: Option<&str>) -> anyhow::Result<Camera> {
|
||||||
match device_id {
|
match device_id {
|
||||||
Some(id) => get_connected_camera_by_id(id),
|
Some(id) => get_connected_camera_by_id(id),
|
||||||
None => get_connected_camers()?
|
None => get_connected_cameras()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| anyhow!("No supported devices connected.")),
|
.ok_or_else(|| anyhow!("No supported devices connected")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
treefmt.nix
Executable file
15
treefmt.nix
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
projectRootFile = "flake.nix";
|
||||||
|
|
||||||
|
programs = {
|
||||||
|
nixfmt = {
|
||||||
|
enable = true;
|
||||||
|
strict = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
rustfmt.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
settings.global.excludes = [ ".envrc" ];
|
||||||
|
}
|
Reference in New Issue
Block a user