Skip to main content

kallo_cli/commands/
common.rs

1//! Argument resolution, the input reader, and the report every command shares.
2
3use kallo_api::StockChoice;
4use kallo_core::{CameraProfile, RenderParams};
5
6use crate::cli::StockArgs;
7use crate::error::CliError;
8
9pub use kallo_api::{Selection, check_model, read_input};
10
11impl From<&StockArgs> for StockChoice {
12    fn from(stock: &StockArgs) -> Self {
13        Self {
14            film: stock.film.clone(),
15            paper: stock.paper.clone(),
16            filters: stock.filters.clone(),
17            heat_glass: stock.heat_glass.clone(),
18            dev_time: stock.dev_time,
19            temp: stock.temp,
20            spectral: stock.spectral,
21            lut_size: stock.lut_size,
22            dictionary: stock.dictionary.clone(),
23            lut: stock.lut.clone(),
24            allow_model_mismatch: stock.allow_model_mismatch,
25            camera: stock.camera.clone(),
26            spectral_print: stock.spectral_print,
27            spectral_scan: stock.spectral_scan,
28        }
29    }
30}
31
32/// Validate the numeric flags and look up both stocks.
33pub fn resolve(stock: &StockArgs) -> Result<Selection, CliError> {
34    kallo_api::resolve(&StockChoice::from(stock))
35}
36
37/// The same, from a choice a command built by hand.
38pub fn resolve_stock_choice(choice: &StockChoice) -> Result<Selection, CliError> {
39    kallo_api::resolve(choice)
40}
41
42impl From<crate::cli::Exposure> for kallo_api::Exposure {
43    fn from(value: crate::cli::Exposure) -> Self {
44        match value {
45            crate::cli::Exposure::Stops(ev) => Self::Stops(ev),
46            crate::cli::Exposure::Auto => Self::Auto,
47        }
48    }
49}
50
51/// Resolve `--camera` into a profile.
52pub(crate) fn camera_profile(value: Option<&str>) -> Result<CameraProfile, CliError> {
53    kallo_api::camera_profile(value)
54}
55
56/// Print what the development did, before any pixels are touched.
57/// What the render did beyond the model: the effects that were on, and who ran
58/// them. Grouped because a report function with eight arguments is a report
59/// function that grows one every phase.
60pub struct Rendering<'a> {
61    /// Grain, if it was on.
62    pub grain: Option<&'a kallo_core::GrainField>,
63    /// The emulsion's blur, if it was on.
64    pub emulsion: Option<&'a kallo_core::EmulsionBlur>,
65    /// Halation, if it was on.
66    pub halation: Option<&'a kallo_core::Halation>,
67    /// DIR couplers, if they were on.
68    pub couplers: Option<&'a kallo_core::Couplers>,
69    /// The cyan-sensitive layer's inhibitor, if it was on — ADR 0018.
70    pub interimage: Option<&'a kallo_core::InterImage>,
71    /// The scan's texture and optics, if it was on.
72    pub scanner: Option<&'a kallo_core::Scanner>,
73    /// What the meter read, if the exposure was metered rather than named.
74    pub metering: Option<kallo_core::Metering>,
75    /// Which backend did the work, already formatted.
76    pub backend: String,
77}
78
79impl Default for Rendering<'_> {
80    fn default() -> Self {
81        Self {
82            grain: None,
83            emulsion: None,
84            halation: None,
85            couplers: None,
86            interimage: None,
87            scanner: None,
88            metering: None,
89            backend: "cpu".to_owned(),
90        }
91    }
92}
93
94/// Print what the render did, before the pixels are written.
95pub fn print_report(
96    params: &RenderParams,
97    prepared: &kallo_core::PreparedRender,
98    width: u32,
99    height: u32,
100    rendering: &Rendering<'_>,
101) {
102    let development = params.development;
103
104    println!("kallo {}", env!("CARGO_PKG_VERSION"));
105    println!("  backend   {}", rendering.backend);
106    println!("  film      {}  ({})", params.film.id, params.film.label);
107    println!("  paper     {}  ({})", params.paper.id, params.paper.label);
108    println!(
109        "  process   {}",
110        match params.film.process {
111            kallo_core::FilmProcess::Negative => "negative — developed, printed, scanned",
112            kallo_core::FilmProcess::Reversal => "reversal — the film is the picture",
113            kallo_core::FilmProcess::Monochrome => "monochrome — one record, no dyes",
114        }
115    );
116    println!(
117        "  print     {}",
118        if prepared.is_spectral_print() {
119            "spectral (dye absorption × paper layers)"
120        } else {
121            "scalar per channel"
122        }
123    );
124    println!(
125        "  scan      {}",
126        if prepared.is_spectral_scan() {
127            "spectral (reflectance × illuminant × CIE 1931)"
128        } else {
129            "scalar per channel"
130        }
131    );
132    println!(
133        "  exposure  {}",
134        if prepared.lut().is_some() {
135            "spectral, baked table"
136        } else if prepared.is_spectral() {
137            "spectral, live"
138        } else {
139            "scalar per channel"
140        }
141    );
142    match rendering.metering {
143        Some(metering) => println!(
144            "  ev        {:+.2}  auto — frame mean {:.3} → mid grey 0.180",
145            params.exposure_ev, metering.key
146        ),
147        None => println!("  ev        {:+.2}", params.exposure_ev),
148    }
149    if prepared.is_spectral() || prepared.lut().is_some() {
150        println!("  camera    {}", params.camera.name());
151    }
152    if let Some(interimage) = rendering.interimage {
153        println!(
154            "  interimage cyan ← the CL layer, gain {:.0} %  σ {:.0} µm  (placeholder)",
155            100.0 * interimage.gain,
156            interimage.sigma_um
157        );
158    }
159    if let Some(couplers) = rendering.couplers {
160        let [row0, row1, row2] = couplers.inhibition;
161        println!(
162            "  couplers  inhibition R←{:?} G←{:?} B←{:?}  σ {:.0} µm  (placeholder)",
163            [row0[0], row0[1], row0[2]],
164            [row1[0], row1[1], row1[2]],
165            [row2[0], row2[1], row2[2]],
166            couplers.sigma_um
167        );
168    }
169    if let Some(emulsion) = rendering.emulsion {
170        println!(
171            "  blur      σ {:.2}/{:.2}/{:.2} µm, {}  ({})",
172            emulsion.sigma_um[0],
173            emulsion.sigma_um[1],
174            emulsion.sigma_um[2],
175            if emulsion.is_noop() {
176                "below the pixel"
177            } else {
178                "applied"
179            },
180            if params.film.sigma_um.is_some() {
181                "the stock's own MTF"
182            } else {
183                "borrowed: VISION3 H-1-5219 p.3"
184            }
185        );
186    }
187    if let Some(halation) = rendering.halation {
188        println!(
189            "  halation  scatter σ {:.0} µm {:.1}/{:.1}/{:.1} %  reflection σ {:.0} µm {:.1}/{:.1}/{:.1} %  (placeholder)",
190            halation.scatter.sigma_um,
191            100.0 * halation.scatter.strength[0],
192            100.0 * halation.scatter.strength[1],
193            100.0 * halation.scatter.strength[2],
194            halation.reflection.sigma_um,
195            100.0 * halation.reflection.strength[0],
196            100.0 * halation.reflection.strength[1],
197            100.0 * halation.reflection.strength[2],
198        );
199    }
200    if let Some(grain) = rendering.grain {
201        // The clump is worth saying out loud: it is the difference between
202        // grain and coloured noise, and it is the one number here that a scan
203        // had to measure rather than a data sheet print.
204        let clump = if grain.has_kernel() {
205            format!(
206                ", clump {:.2}/{:.2}/{:.2} µm r{}",
207                grain.grain_um[0],
208                grain.grain_um[1],
209                grain.grain_um[2],
210                grain.radius.iter().copied().max().unwrap_or(0)
211            )
212        } else {
213            String::new()
214        };
215        println!(
216            "  grain     {:.2}/{:.2}/{:.2} per µm²  ({:.2} µm pixel, seed {}{})",
217            grain.grains_per_um2[0],
218            grain.grains_per_um2[1],
219            grain.grains_per_um2[2],
220            grain.pixel_area_um2.sqrt(),
221            grain.seed,
222            clump
223        );
224    }
225    if let Some(scanner) = rendering.scanner {
226        println!(
227            "  scanner   {:.2} µm texture, {:.2} % at 48 µm, {:.0} % common, optics {:.2} µm",
228            scanner.texture_um,
229            100.0 * scanner.texture_density,
230            100.0 * scanner.shared,
231            scanner.optics_um
232        );
233    }
234    println!(
235        "  develop   {:.2} min @ {:.1} °C  →  {:.2} effective min",
236        development.time_min,
237        development.temp_c,
238        development.effective_time_min()
239    );
240
241    let gammas = prepared.channels().map(|channel| channel.gamma);
242    println!(
243        "  contrast  ×{:.3}  (R {:.3}  G {:.3}  B {:.3})",
244        development.gamma_factor(),
245        gammas[0],
246        gammas[1],
247        gammas[2]
248    );
249    println!("  fog       {:+.3} density", development.fog_delta());
250    println!("  ceiling   {:+.3} density", development.d_max_delta());
251    println!("  toe       {:+.2} rate", development.toe_delta());
252    println!(
253        "  speed     {:+.3} log10 E  ({:+.2} stops)",
254        development.speed_delta(),
255        development.speed_delta() / 2.0_f64.log10()
256    );
257
258    if width > 0 {
259        println!("  image     {width}x{height}");
260    }
261}