Skip to main content

kallo_api/
roll.rs

1//! A roll: one photograph, one decode, five developments and a proof.
2//!
3//! The bench used to ask the model six times — five prints and a proof — and
4//! each of those calls decoded the photograph again. It was the largest single
5//! cost of making a roll, and it bought nothing: the negative is the same
6//! negative at every development time. This module is that knowledge in one
7//! place: read the file once, develop it five ways, print the proof, write the
8//! lot.
9
10use std::path::PathBuf;
11
12use kallo_core::FilmStock;
13use kallo_io::resize_to_width;
14
15use crate::contact::{ContactOptions, ContactOutcome, contact};
16use crate::error::Error;
17use crate::input::read_input;
18use crate::render::{Exposure, RenderOptions, RenderOutcome, render};
19use crate::selection::Selection;
20
21/// What a roll is asked for.
22#[derive(Debug, Clone)]
23pub struct RollRequest {
24    /// The photograph.
25    pub input: PathBuf,
26    /// How to read an ordinary image (a raw carries its own encoding).
27    pub input_encoding: kallo_io::InputEncoding,
28    /// One output per development, in the ladder's order.
29    pub frames: Vec<PathBuf>,
30    /// Where the proof goes, if one is wanted.
31    pub contact: Option<PathBuf>,
32    /// Where its hit-testing record goes.
33    pub record: Option<PathBuf>,
34    /// The development times, in minutes.
35    pub times: Vec<f64>,
36    /// The width the bench shows the frames at.
37    pub size: u32,
38    /// The proof's resolution.
39    pub dpi: f64,
40    /// Exposure offset, or metering.
41    pub ev: Exposure,
42    /// Extra print exposure in stops.
43    pub print_ev: f64,
44    /// Width of the film frame the image represents, in millimetres.
45    pub frame_mm: f64,
46    /// Add grain.
47    pub grain: bool,
48    /// Override the grain's correlation length.
49    pub grain_um: Option<f64>,
50    /// Grain seed.
51    pub grain_seed: u64,
52    /// Add halation.
53    pub halation: bool,
54    /// Add DIR couplers.
55    pub couplers: bool,
56    /// Render on the GPU when there is one.
57    pub gpu: bool,
58}
59
60impl Default for RollRequest {
61    fn default() -> Self {
62        Self {
63            input: PathBuf::new(),
64            input_encoding: kallo_io::InputEncoding::Srgb,
65            frames: Vec::new(),
66            contact: None,
67            record: None,
68            times: Vec::new(),
69            size: 1200,
70            dpi: 600.0,
71            ev: Exposure::Auto,
72            print_ev: 0.0,
73            frame_mm: 36.0,
74            grain: true,
75            grain_um: None,
76            grain_seed: 0,
77            halation: true,
78            couplers: true,
79            gpu: true,
80        }
81    }
82}
83
84/// Where a roll has got to.
85#[derive(Debug, Clone, Copy)]
86pub enum Progress {
87    /// A frame has been developed and written.
88    Frame {
89        /// How many are done, one-based.
90        index: usize,
91        /// How many there are.
92        total: usize,
93    },
94    /// The proof has been printed and written.
95    Contact,
96    /// Everything is written.
97    Done,
98}
99
100/// What a roll produced.
101pub struct RollOutcome {
102    /// One outcome per frame, in the ladder's order.
103    pub frames: Vec<RenderOutcome>,
104    /// The proof, if one was asked for.
105    pub contact: Option<ContactOutcome>,
106    /// The chart: the same wedge through every development, for the interface.
107    pub chart: Vec<crate::chart::FrameChart>,
108    /// The size of the photograph as it was read.
109    pub source: (u32, u32),
110}
111
112/// Develop a photograph into a roll: one read, five prints, one proof.
113///
114/// `film` overrides the stock the selection resolved to, for a `.kalofilm`
115/// file. `progress` is called as each piece lands, so an interface can show the
116/// bench filling up without polling the file system.
117pub fn roll(
118    request: &RollRequest,
119    selection: &Selection,
120    film: Option<&'static FilmStock>,
121    progress: &mut dyn FnMut(Progress),
122) -> Result<RollOutcome, Error> {
123    if request.frames.len() != request.times.len() {
124        return Err(Error::Contact {
125            message: format!(
126                "{} development times but {} output frames",
127                request.times.len(),
128                request.frames.len()
129            ),
130        });
131    }
132    if request.size == 0 {
133        return Err(Error::InvalidNumber {
134            flag: "--size",
135            value: f64::from(request.size),
136            expected: "a positive width in pixels",
137        });
138    }
139    if let Some(bad) = request
140        .times
141        .iter()
142        .copied()
143        .find(|time| !(time.is_finite() && *time > 0.0))
144    {
145        return Err(Error::InvalidNumber {
146            flag: "--dev",
147            value: bad,
148            expected: "a positive number of minutes",
149        });
150    }
151
152    let source = read_input(&request.input, request.input_encoding)?;
153    let source_size = (source.width, source.height);
154    // The bench is one size for every frame; the proof is sampled from the
155    // photograph itself, at the frame the film carries.
156    let bench = resize_to_width(&source, request.size);
157
158    let options = RenderOptions {
159        ev: request.ev,
160        print_ev: request.print_ev,
161        frame_mm: request.frame_mm,
162        grain: request.grain,
163        grain_um: request.grain_um,
164        grain_seed: request.grain_seed,
165        halation: request.halation,
166        couplers: request.couplers,
167        gpu: request.gpu,
168        ..RenderOptions::default()
169    };
170
171    let total = request.times.len();
172    let mut frames = Vec::with_capacity(total);
173    for (index, output) in request.frames.iter().enumerate() {
174        // The render turns the pixels into a print in place, so every frame
175        // starts from the bench again — and it is a copy of the same decode,
176        // not another one. The selection is copied too, because the only thing
177        // that changes between the frames is the development time.
178        let mut image = bench.clone();
179        let mut frame_selection = selection.clone();
180        frame_selection.development.time_min = request.times[index];
181        let outcome = render(&mut image, film, &frame_selection, &options, output)?;
182        frames.push(outcome);
183        progress(Progress::Frame {
184            index: index + 1,
185            total,
186        });
187    }
188
189    let proof = if request.contact.is_some() || request.record.is_some() {
190        let sheet = request.contact.clone().unwrap_or_else(|| {
191            let mut path = request.input.clone();
192            path.set_extension("contact.png");
193            path
194        });
195        let options = ContactOptions {
196            ev: request.ev,
197            print_ev: request.print_ev,
198            dpi: request.dpi,
199            grain: request.grain,
200            halation: request.halation,
201            couplers: request.couplers,
202            edge_codes: true,
203            record: request.record.clone(),
204        };
205        let outcome = contact(&source, film, selection, &request.times, &options, &sheet)?;
206        progress(Progress::Contact);
207        Some(outcome)
208    } else {
209        None
210    };
211
212    // The chart is the stock and the development, not the photograph: it is
213    // the same wedge the committed strip was drawn from, so a roll the app
214    // developed can be read against the one it ships with.
215    let chart = crate::chart::chart(
216        film.unwrap_or(selection.film),
217        &request.times,
218        selection.development.temp_c,
219    );
220
221    progress(Progress::Done);
222    Ok(RollOutcome {
223        frames,
224        contact: proof,
225        chart,
226        source: source_size,
227    })
228}