1use 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
32pub fn resolve(stock: &StockArgs) -> Result<Selection, CliError> {
34 kallo_api::resolve(&StockChoice::from(stock))
35}
36
37pub 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
51pub(crate) fn camera_profile(value: Option<&str>) -> Result<CameraProfile, CliError> {
53 kallo_api::camera_profile(value)
54}
55
56pub struct Rendering<'a> {
61 pub grain: Option<&'a kallo_core::GrainField>,
63 pub emulsion: Option<&'a kallo_core::EmulsionBlur>,
65 pub halation: Option<&'a kallo_core::Halation>,
67 pub couplers: Option<&'a kallo_core::Couplers>,
69 pub interimage: Option<&'a kallo_core::InterImage>,
71 pub scanner: Option<&'a kallo_core::Scanner>,
73 pub metering: Option<kallo_core::Metering>,
75 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
94pub 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 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}