use std::path::Path;
use kallo_core::{
Couplers, EmulsionBlur, Finishing, GrainField, Halation, InterImage, Metering, RenderParams,
Scanner, render_pixels_finished,
};
use kallo_io::{Image, save};
use crate::error::Error;
use crate::selection::Selection;
#[derive(Debug, Clone, Copy)]
pub enum Exposure {
Stops(f64),
Auto,
}
impl std::str::FromStr for Exposure {
type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> {
if text.eq_ignore_ascii_case("auto") {
return Ok(Self::Auto);
}
text.parse::<f64>()
.map(Self::Stops)
.map_err(|error| format!("expected a number of stops or `auto`: {error}"))
}
}
#[derive(Debug, Clone)]
pub struct RenderOptions {
pub ev: Exposure,
pub print_ev: f64,
pub balance_from: Option<f64>,
pub frame_mm: f64,
pub emulsion: bool,
pub halation: bool,
pub couplers: bool,
pub interimage: bool,
pub grain: bool,
pub grain_um: Option<f64>,
pub grain_seed: u64,
pub scanner: bool,
pub scan_optics_um: Option<f64>,
pub gpu: bool,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
ev: Exposure::Auto,
print_ev: 0.0,
balance_from: None,
frame_mm: 36.0,
emulsion: true,
halation: false,
couplers: false,
interimage: false,
grain: false,
grain_um: None,
grain_seed: 0,
scanner: false,
scan_optics_um: None,
gpu: true,
}
}
}
#[derive(Debug, Clone)]
pub enum Backend {
Cpu,
Gpu(String),
}
impl std::fmt::Display for Backend {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Cpu => write!(formatter, "cpu"),
Self::Gpu(name) => write!(formatter, "gpu — {name}"),
}
}
}
#[derive(Debug, Default)]
pub struct OwnedFinishing {
pub emulsion: Option<EmulsionBlur>,
pub halation: Option<Halation>,
pub grain: Option<GrainField>,
pub couplers: Option<Couplers>,
pub interimage: Option<InterImage>,
}
impl OwnedFinishing {
#[must_use]
pub fn as_finishing(&self) -> Finishing<'_> {
Finishing {
emulsion: self.emulsion.as_ref(),
halation: self.halation.as_ref(),
grain: self.grain.as_ref(),
couplers: self.couplers.as_ref(),
interimage: self.interimage.as_ref(),
}
}
}
pub struct RenderOutcome {
pub width: u32,
pub height: u32,
pub backend: Backend,
pub params: RenderParams,
pub prepared: kallo_core::PreparedRender,
pub finishing: OwnedFinishing,
pub scanner: Option<Scanner>,
pub metering: Option<Metering>,
pub warnings: Vec<String>,
}
pub fn render(
image: &mut Image,
film: Option<&'static kallo_core::FilmStock>,
selection: &Selection,
options: &RenderOptions,
output: &Path,
) -> Result<RenderOutcome, Error> {
let outcome = render_image(image, film, selection, options)?;
ensure_parent(output)?;
save(output, image).map_err(|source| Error::Write {
path: output.to_path_buf(),
source,
})?;
Ok(outcome)
}
pub fn render_image(
image: &mut Image,
film: Option<&'static kallo_core::FilmStock>,
selection: &Selection,
options: &RenderOptions,
) -> Result<RenderOutcome, Error> {
let film = film.unwrap_or(selection.film);
let (width, height) = (image.width, image.height);
let (width, height) = (width as usize, height as usize);
let (exposure_ev, metering) = match options.ev {
Exposure::Stops(ev) => (ev, None),
Exposure::Auto => {
let metered = Metering::of(&image.pixels);
(metered.ev, Some(metered))
}
};
let interimage = options.interimage && film.cl_layer.is_some();
let prepared = selection.prepare_with_film(exposure_ev, options.print_ev, interimage, film)?;
let prepared = match options.balance_from {
Some(minutes) => {
if !(minutes.is_finite() && minutes > 0.0) {
return Err(Error::InvalidNumber {
flag: "--balance-from",
value: minutes,
expected: "a positive number of minutes",
});
}
let mut reference = selection.clone();
reference.development.time_min = minutes;
let reference =
reference.prepare_with_film(exposure_ev, options.print_ev, interimage, film)?;
prepared.printed_like(&reference)
}
None => prepared,
};
let params = selection.render_params(exposure_ev, options.print_ev, interimage, film);
let pixel_um = options.frame_mm * 1000.0 / width.max(1) as f64;
let grain = options.grain.then(|| {
let field =
GrainField::for_stock(width, height, options.frame_mm, options.grain_seed, film);
match options.grain_um {
Some(diameter) => field.with_grain_sizes([diameter; 3]),
None => field,
}
});
let emulsion = options
.emulsion
.then(|| EmulsionBlur::for_stock(film, pixel_um));
let halation = options.halation.then(|| Halation::typical(pixel_um));
let couplers = options.couplers.then(|| Couplers::typical(pixel_um));
let interimage_field = interimage.then(|| InterImage::typical(pixel_um));
let finishing = OwnedFinishing {
emulsion,
halation,
grain,
couplers,
interimage: interimage_field,
};
let mut warnings = Vec::new();
let backend = render_pixels(
&mut image.pixels,
&prepared,
width,
height,
&finishing.as_finishing(),
options.gpu,
&mut warnings,
)?;
let scanner = options.scanner.then(|| {
let mut scanner = Scanner::measured();
if let Some(optics) = options.scan_optics_um {
scanner.optics_um = optics;
}
scanner
});
if let Some(scanner) = scanner.as_ref() {
scanner.apply(&mut image.pixels, width, height, pixel_um);
}
Ok(RenderOutcome {
width: width as u32,
height: height as u32,
backend,
params,
prepared,
finishing,
scanner,
metering,
warnings,
})
}
#[cfg(feature = "gpu")]
fn render_pixels(
pixels: &mut [[f64; 3]],
prepared: &kallo_core::PreparedRender,
width: usize,
height: usize,
finishing: &Finishing,
on_gpu: bool,
warnings: &mut Vec<String>,
) -> Result<Backend, Error> {
if !on_gpu {
render_pixels_finished(pixels, prepared, width, height, finishing);
return Ok(Backend::Cpu);
}
let gpu = match kallo_gpu::Gpu::new() {
Ok(gpu) => gpu,
Err(error) => {
warnings.push(format!("{error}; rendering on the cpu"));
render_pixels_finished(pixels, prepared, width, height, finishing);
return Ok(Backend::Cpu);
}
};
match gpu.render_finished(pixels, prepared, width, height, finishing) {
Ok(rendered) => {
pixels.copy_from_slice(&rendered);
Ok(Backend::Gpu(gpu.describe()))
}
Err(error) => {
warnings.push(format!("{error}; rendering on the cpu"));
render_pixels_finished(pixels, prepared, width, height, finishing);
Ok(Backend::Cpu)
}
}
}
#[cfg(not(feature = "gpu"))]
fn render_pixels(
pixels: &mut [[f64; 3]],
prepared: &kallo_core::PreparedRender,
width: usize,
height: usize,
finishing: &Finishing,
on_gpu: bool,
warnings: &mut Vec<String>,
) -> Result<Backend, Error> {
if on_gpu {
warnings.push("this build has no GPU backend; rendering on the cpu".to_owned());
}
render_pixels_finished(pixels, prepared, width, height, finishing);
Ok(Backend::Cpu)
}
pub(crate) fn ensure_parent(path: &Path) -> Result<(), Error> {
let Some(parent) = path.parent() else {
return Ok(());
};
if parent.as_os_str().is_empty() {
return Ok(());
}
std::fs::create_dir_all(parent).map_err(|source| Error::CreateDir {
path: parent.to_path_buf(),
source,
})
}