1use std::path::Path;
4
5use kallo_core::{
6 Couplers, EmulsionBlur, Finishing, GrainField, Halation, InterImage, Metering, RenderParams,
7 Scanner, render_pixels_finished,
8};
9use kallo_io::{Image, save};
10
11use crate::error::Error;
12use crate::selection::Selection;
13
14#[derive(Debug, Clone, Copy)]
16pub enum Exposure {
17 Stops(f64),
19 Auto,
21}
22
23impl std::str::FromStr for Exposure {
24 type Err = String;
25
26 fn from_str(text: &str) -> Result<Self, Self::Err> {
27 if text.eq_ignore_ascii_case("auto") {
28 return Ok(Self::Auto);
29 }
30 text.parse::<f64>()
31 .map(Self::Stops)
32 .map_err(|error| format!("expected a number of stops or `auto`: {error}"))
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct RenderOptions {
39 pub ev: Exposure,
41 pub print_ev: f64,
43 pub balance_from: Option<f64>,
50 pub frame_mm: f64,
52 pub emulsion: bool,
54 pub halation: bool,
56 pub couplers: bool,
58 pub interimage: bool,
60 pub grain: bool,
62 pub grain_um: Option<f64>,
64 pub grain_seed: u64,
66 pub scanner: bool,
68 pub scan_optics_um: Option<f64>,
70 pub gpu: bool,
72}
73
74impl Default for RenderOptions {
75 fn default() -> Self {
76 Self {
77 ev: Exposure::Auto,
78 print_ev: 0.0,
79 balance_from: None,
80 frame_mm: 36.0,
81 emulsion: true,
82 halation: false,
83 couplers: false,
84 interimage: false,
85 grain: false,
86 grain_um: None,
87 grain_seed: 0,
88 scanner: false,
89 scan_optics_um: None,
90 gpu: true,
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
97pub enum Backend {
98 Cpu,
100 Gpu(String),
102}
103
104impl std::fmt::Display for Backend {
105 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match self {
107 Self::Cpu => write!(formatter, "cpu"),
108 Self::Gpu(name) => write!(formatter, "gpu — {name}"),
109 }
110 }
111}
112
113#[derive(Debug, Default)]
115pub struct OwnedFinishing {
116 pub emulsion: Option<EmulsionBlur>,
118 pub halation: Option<Halation>,
120 pub grain: Option<GrainField>,
122 pub couplers: Option<Couplers>,
124 pub interimage: Option<InterImage>,
126}
127
128impl OwnedFinishing {
129 #[must_use]
131 pub fn as_finishing(&self) -> Finishing<'_> {
132 Finishing {
133 emulsion: self.emulsion.as_ref(),
134 halation: self.halation.as_ref(),
135 grain: self.grain.as_ref(),
136 couplers: self.couplers.as_ref(),
137 interimage: self.interimage.as_ref(),
138 }
139 }
140}
141
142pub struct RenderOutcome {
144 pub width: u32,
146 pub height: u32,
148 pub backend: Backend,
150 pub params: RenderParams,
152 pub prepared: kallo_core::PreparedRender,
154 pub finishing: OwnedFinishing,
156 pub scanner: Option<Scanner>,
158 pub metering: Option<Metering>,
160 pub warnings: Vec<String>,
162}
163
164pub fn render(
172 image: &mut Image,
173 film: Option<&'static kallo_core::FilmStock>,
174 selection: &Selection,
175 options: &RenderOptions,
176 output: &Path,
177) -> Result<RenderOutcome, Error> {
178 let outcome = render_image(image, film, selection, options)?;
179 ensure_parent(output)?;
180 save(output, image).map_err(|source| Error::Write {
181 path: output.to_path_buf(),
182 source,
183 })?;
184 Ok(outcome)
185}
186
187pub fn render_image(
194 image: &mut Image,
195 film: Option<&'static kallo_core::FilmStock>,
196 selection: &Selection,
197 options: &RenderOptions,
198) -> Result<RenderOutcome, Error> {
199 let film = film.unwrap_or(selection.film);
200 let (width, height) = (image.width, image.height);
201 let (width, height) = (width as usize, height as usize);
202
203 let (exposure_ev, metering) = match options.ev {
206 Exposure::Stops(ev) => (ev, None),
207 Exposure::Auto => {
208 let metered = Metering::of(&image.pixels);
209 (metered.ev, Some(metered))
210 }
211 };
212
213 let interimage = options.interimage && film.cl_layer.is_some();
216 let prepared = selection.prepare_with_film(exposure_ev, options.print_ev, interimage, film)?;
217 let prepared = match options.balance_from {
218 Some(minutes) => {
219 if !(minutes.is_finite() && minutes > 0.0) {
220 return Err(Error::InvalidNumber {
221 flag: "--balance-from",
222 value: minutes,
223 expected: "a positive number of minutes",
224 });
225 }
226 let mut reference = selection.clone();
227 reference.development.time_min = minutes;
228 let reference =
229 reference.prepare_with_film(exposure_ev, options.print_ev, interimage, film)?;
230 prepared.printed_like(&reference)
231 }
232 None => prepared,
233 };
234 let params = selection.render_params(exposure_ev, options.print_ev, interimage, film);
235
236 let pixel_um = options.frame_mm * 1000.0 / width.max(1) as f64;
237 let grain = options.grain.then(|| {
241 let field =
242 GrainField::for_stock(width, height, options.frame_mm, options.grain_seed, film);
243 match options.grain_um {
244 Some(diameter) => field.with_grain_sizes([diameter; 3]),
245 None => field,
246 }
247 });
248 let emulsion = options
249 .emulsion
250 .then(|| EmulsionBlur::for_stock(film, pixel_um));
251 let halation = options.halation.then(|| Halation::typical(pixel_um));
252 let couplers = options.couplers.then(|| Couplers::typical(pixel_um));
253 let interimage_field = interimage.then(|| InterImage::typical(pixel_um));
254 let finishing = OwnedFinishing {
255 emulsion,
256 halation,
257 grain,
258 couplers,
259 interimage: interimage_field,
260 };
261
262 let mut warnings = Vec::new();
263 let backend = render_pixels(
264 &mut image.pixels,
265 &prepared,
266 width,
267 height,
268 &finishing.as_finishing(),
269 options.gpu,
270 &mut warnings,
271 )?;
272
273 let scanner = options.scanner.then(|| {
277 let mut scanner = Scanner::measured();
278 if let Some(optics) = options.scan_optics_um {
279 scanner.optics_um = optics;
280 }
281 scanner
282 });
283 if let Some(scanner) = scanner.as_ref() {
284 scanner.apply(&mut image.pixels, width, height, pixel_um);
285 }
286
287 Ok(RenderOutcome {
288 width: width as u32,
289 height: height as u32,
290 backend,
291 params,
292 prepared,
293 finishing,
294 scanner,
295 metering,
296 warnings,
297 })
298}
299
300#[cfg(feature = "gpu")]
307fn render_pixels(
308 pixels: &mut [[f64; 3]],
309 prepared: &kallo_core::PreparedRender,
310 width: usize,
311 height: usize,
312 finishing: &Finishing,
313 on_gpu: bool,
314 warnings: &mut Vec<String>,
315) -> Result<Backend, Error> {
316 if !on_gpu {
317 render_pixels_finished(pixels, prepared, width, height, finishing);
318 return Ok(Backend::Cpu);
319 }
320
321 let gpu = match kallo_gpu::Gpu::new() {
322 Ok(gpu) => gpu,
323 Err(error) => {
324 warnings.push(format!("{error}; rendering on the cpu"));
325 render_pixels_finished(pixels, prepared, width, height, finishing);
326 return Ok(Backend::Cpu);
327 }
328 };
329
330 match gpu.render_finished(pixels, prepared, width, height, finishing) {
331 Ok(rendered) => {
332 pixels.copy_from_slice(&rendered);
333 Ok(Backend::Gpu(gpu.describe()))
334 }
335 Err(error) => {
341 warnings.push(format!("{error}; rendering on the cpu"));
342 render_pixels_finished(pixels, prepared, width, height, finishing);
343 Ok(Backend::Cpu)
344 }
345 }
346}
347
348#[cfg(not(feature = "gpu"))]
352fn render_pixels(
353 pixels: &mut [[f64; 3]],
354 prepared: &kallo_core::PreparedRender,
355 width: usize,
356 height: usize,
357 finishing: &Finishing,
358 on_gpu: bool,
359 warnings: &mut Vec<String>,
360) -> Result<Backend, Error> {
361 if on_gpu {
362 warnings.push("this build has no GPU backend; rendering on the cpu".to_owned());
363 }
364 render_pixels_finished(pixels, prepared, width, height, finishing);
365 Ok(Backend::Cpu)
366}
367
368pub(crate) fn ensure_parent(path: &Path) -> Result<(), Error> {
370 let Some(parent) = path.parent() else {
371 return Ok(());
372 };
373 if parent.as_os_str().is_empty() {
374 return Ok(());
375 }
376 std::fs::create_dir_all(parent).map_err(|source| Error::CreateDir {
377 path: parent.to_path_buf(),
378 source,
379 })
380}