use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::sync::{Mutex, MutexGuard};
use kallo_core::Metering;
use kallo_io::{Image, InputEncoding, resize_to_width, to_rgba8};
use crate::error::Error;
use crate::input::read_input;
use crate::render::{Exposure, RenderOptions, render_image};
use crate::selection::{Selection, StockChoice, resolve};
const CACHE: usize = 12;
#[derive(Debug, Clone)]
pub struct PreviewRequest {
pub input: PathBuf,
pub encoding: InputEncoding,
pub film: String,
pub paper: String,
pub temp_c: f64,
pub width: u32,
pub spectral: bool,
pub frame_mm: f64,
pub grain: bool,
pub halation: bool,
pub couplers: bool,
pub gpu: bool,
}
impl PreviewRequest {
fn choice(&self) -> StockChoice {
StockChoice {
film: self.film.clone(),
paper: self.paper.clone(),
temp: Some(self.temp_c),
spectral: self.spectral,
spectral_print: self.spectral,
spectral_scan: self.spectral,
..StockChoice::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreviewFrame {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Key {
minutes: u64,
print_ev: u64,
balance_from: Option<u64>,
filters: String,
heat_glass: Option<String>,
}
impl Key {
fn new(
minutes: f64,
print_ev: f64,
balance_from: Option<f64>,
filters: &str,
heat_glass: Option<&str>,
) -> Self {
Self {
minutes: minutes.to_bits(),
print_ev: print_ev.to_bits(),
balance_from: balance_from.map(f64::to_bits),
filters: filters.to_owned(),
heat_glass: heat_glass.map(str::to_owned),
}
}
}
#[derive(Debug, Default)]
struct Cache {
frames: HashMap<Key, PreviewFrame>,
order: VecDeque<Key>,
}
impl Cache {
fn get(&self, key: &Key) -> Option<PreviewFrame> {
self.frames.get(key).cloned()
}
fn put(&mut self, key: Key, frame: PreviewFrame) {
if let Some(slot) = self.frames.get_mut(&key) {
*slot = frame;
return;
}
if self.order.len() >= CACHE {
if let Some(oldest) = self.order.pop_front() {
self.frames.remove(&oldest);
}
}
self.order.push_back(key.clone());
self.frames.insert(key, frame);
}
}
#[derive(Debug)]
pub struct PreviewSession {
choice: StockChoice,
image: Image,
metered_ev: f64,
frame_mm: f64,
grain: bool,
halation: bool,
couplers: bool,
gpu: bool,
cache: Mutex<Cache>,
}
impl PreviewSession {
pub fn open(request: PreviewRequest) -> Result<Self, Error> {
if request.width == 0 {
return Err(Error::InvalidNumber {
flag: "--size",
value: 0.0,
expected: "a positive width in pixels",
});
}
let choice = request.choice();
resolve(&choice)?;
let source = read_input(&request.input, request.encoding)?;
let image = resize_to_width(&source, request.width);
let metered_ev = Metering::of(&image.pixels).ev;
Ok(Self {
choice,
image,
metered_ev,
frame_mm: request.frame_mm,
grain: request.grain,
halation: request.halation,
couplers: request.couplers,
gpu: request.gpu,
cache: Mutex::new(Cache::default()),
})
}
pub fn frame(
&self,
minutes: f64,
print_ev: f64,
balance_from: Option<f64>,
filters: &str,
heat_glass: Option<&str>,
) -> Result<PreviewFrame, Error> {
if !(minutes.is_finite() && minutes > 0.0) {
return Err(Error::InvalidNumber {
flag: "--dev",
value: minutes,
expected: "a positive number of minutes",
});
}
if !print_ev.is_finite() {
return Err(Error::InvalidNumber {
flag: "--print-ev",
value: print_ev,
expected: "a finite number of stops",
});
}
let key = Key::new(minutes, print_ev, balance_from, filters, heat_glass);
if let Some(frame) = self.cache().get(&key) {
return Ok(frame);
}
let mut choice = self.choice.clone();
choice.dev_time = Some(minutes);
choice.filters = filters.to_owned();
choice.heat_glass = heat_glass.map(str::to_owned);
let selection: Selection = resolve(&choice)?;
let mut image = self.image.clone();
let options = RenderOptions {
ev: Exposure::Stops(self.metered_ev),
print_ev,
balance_from,
frame_mm: self.frame_mm,
grain: self.grain,
halation: self.halation,
couplers: self.couplers,
gpu: self.gpu,
..RenderOptions::default()
};
render_image(&mut image, None, &selection, &options)?;
let frame = PreviewFrame {
width: image.width,
height: image.height,
rgba: to_rgba8(&image),
};
self.cache().put(key, frame.clone());
Ok(frame)
}
#[cfg(test)]
fn held(&self) -> usize {
self.cache().frames.len()
}
fn cache(&self) -> MutexGuard<'_, Cache> {
self.cache.lock().unwrap_or_else(|error| error.into_inner())
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use kallo_io::{Image, save};
use super::{PreviewRequest, PreviewSession};
use crate::render::{Exposure, RenderOptions, render_image};
use crate::selection::resolve;
static NEXT: AtomicU64 = AtomicU64::new(0);
fn photograph() -> (PathBuf, Image) {
let image = Image {
width: 5,
height: 4,
pixels: (0..20)
.map(|index| {
let value = 0.02 + f64::from(index) * 0.04;
[value, value * 0.8, value * 0.6]
})
.collect(),
};
let path = std::env::temp_dir().join(format!(
"kallo-preview-{}-{}.png",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
save(&path, &image).expect("the test photograph");
(path, image)
}
fn request(path: &Path) -> PreviewRequest {
PreviewRequest {
input: path.to_path_buf(),
encoding: kallo_io::InputEncoding::Srgb,
film: "vision3_500t".to_owned(),
paper: "portra_endura".to_owned(),
temp_c: 20.0,
width: 5,
spectral: false,
frame_mm: 36.0,
grain: false,
halation: false,
couplers: false,
gpu: false,
}
}
fn expected(
session: &PreviewSession,
minutes: f64,
print_ev: f64,
balance_from: Option<f64>,
filters: &str,
) -> Image {
let mut choice = session.choice.clone();
choice.dev_time = Some(minutes);
choice.filters = filters.to_owned();
let selection = resolve(&choice).expect("the test selection");
let mut image = session.image.clone();
let options = RenderOptions {
ev: Exposure::Stops(session.metered_ev),
print_ev,
balance_from,
frame_mm: session.frame_mm,
grain: session.grain,
halation: session.halation,
couplers: session.couplers,
gpu: false,
..RenderOptions::default()
};
render_image(&mut image, None, &selection, &options).expect("the expected render");
image
}
#[test]
fn a_session_frame_is_the_print_at_that_time() {
let (path, _) = photograph();
let session = PreviewSession::open(request(&path)).expect("the session");
let frame = session.frame(6.0, 0.0, None, "", None).expect("the frame");
let image = expected(&session, 6.0, 0.0, None, "");
assert_eq!(frame.width, image.width);
assert_eq!(frame.height, image.height);
assert_eq!(frame.rgba, kallo_io::to_rgba8(&image));
assert_eq!(
frame.rgba.len(),
(image.width * image.height * 4) as usize,
"four channels per pixel"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn a_second_frame_at_the_same_setting_is_the_cached_one() {
let (path, _) = photograph();
let session = PreviewSession::open(request(&path)).expect("the session");
let first = session.frame(6.0, 0.0, None, "", None).expect("the frame");
let second = session
.frame(6.0, 0.0, None, "", None)
.expect("the cached frame");
assert_eq!(first, second);
assert_eq!(session.held(), 1, "one setting, one frame held");
let other = session
.frame(9.0, 0.0, None, "", None)
.expect("another frame");
assert_ne!(first, other, "nine minutes is not six");
assert_eq!(session.held(), 2);
let _ = std::fs::remove_file(path);
}
#[test]
fn a_strip_frame_borrows_the_first_prints_balance() {
let (path, _) = photograph();
let session = PreviewSession::open(request(&path)).expect("the session");
let frame = session
.frame(12.0, 0.0, Some(6.0), "", None)
.expect("the frame");
let image = expected(&session, 12.0, 0.0, Some(6.0), "");
assert_eq!(frame.rgba, kallo_io::to_rgba8(&image));
let _ = std::fs::remove_file(path);
}
#[test]
fn filters_are_part_of_the_frame() {
let (path, _) = photograph();
let session = PreviewSession::open(request(&path)).expect("the session");
let plain = session
.frame(6.0, 0.0, None, "", None)
.expect("the plain frame");
let filtered = session
.frame(6.0, 0.0, None, "30M", None)
.expect("the filtered frame");
assert_ne!(plain, filtered, "a colour pack changes the print");
assert_eq!(session.held(), 2);
let _ = std::fs::remove_file(path);
}
}