use crate::measure::SourcePixels;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DecodePixels {
pub width: u32,
pub height: u32,
}
impl DecodePixels {
pub fn rgba_bytes(self) -> u64 {
u64::from(self.width) * u64::from(self.height) * 4
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ViewportPixels {
pub width: u32,
pub height: u32,
}
impl ViewportPixels {
pub fn from_logical(width: f32, height: f32, scale: f32) -> ViewportPixels {
ViewportPixels {
width: (width * scale).ceil().max(1.0) as u32,
height: (height * scale).ceil().max(1.0) as u32,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Budget {
max_edge: u32,
max_source_edge: u32,
}
const ZOOM_HEADROOM: f32 = 2.0;
const MAX_SOURCE_EDGE: u32 = 65_536;
impl Budget {
pub fn for_viewport(viewport: ViewportPixels) -> Budget {
let longest = viewport.width.max(viewport.height).max(1);
Budget {
max_edge: ((longest as f32) * ZOOM_HEADROOM).ceil() as u32,
max_source_edge: MAX_SOURCE_EDGE,
}
}
pub fn for_edge(max_edge: u32) -> Budget {
Budget { max_edge: max_edge.max(1), max_source_edge: MAX_SOURCE_EDGE }
}
pub fn fit(&self, source: SourcePixels) -> DecodePixels {
let (w, h) = (source.width.max(1), source.height.max(1));
let longest = w.max(h);
if longest <= self.max_edge {
return DecodePixels { width: w, height: h };
}
let ratio = f64::from(self.max_edge) / f64::from(longest);
DecodePixels {
width: ((f64::from(w) * ratio).round() as u32).max(1),
height: ((f64::from(h) * ratio).round() as u32).max(1),
}
}
pub fn allows_source(&self, source: SourcePixels) -> bool {
source.width <= self.max_source_edge && source.height <= self.max_source_edge
}
pub fn limits(&self) -> image::Limits {
let mut limits = image::Limits::default();
limits.max_image_width = Some(self.max_source_edge);
limits.max_image_height = Some(self.max_source_edge);
limits
}
pub fn max_retained_bytes(&self) -> u64 {
let edge = u64::from(self.max_edge);
edge * edge * 4
}
}
#[cfg(test)]
mod tests {
use super::*;
fn source(width: u32, height: u32) -> SourcePixels {
SourcePixels { width, height }
}
#[test]
fn a_thirty_six_megapixel_photograph_is_never_retained_at_full_size() {
let budget = Budget::for_viewport(ViewportPixels { width: 2560, height: 1600 });
let fitted = budget.fit(source(8001, 4501));
assert!(fitted.width < 8001, "{fitted:?}");
assert!(fitted.rgba_bytes() < 60 * 1024 * 1024, "{} bytes", fitted.rgba_bytes());
assert!(fitted.rgba_bytes() <= budget.max_retained_bytes());
}
#[test]
fn fitting_preserves_the_shape_of_the_picture() {
let budget = Budget::for_edge(1000);
let fitted = budget.fit(source(4000, 2000));
assert_eq!(fitted, DecodePixels { width: 1000, height: 500 });
}
#[test]
fn a_picture_smaller_than_the_budget_is_never_scaled_up() {
let budget = Budget::for_edge(4000);
assert_eq!(budget.fit(source(64, 64)), DecodePixels { width: 64, height: 64 });
}
#[test]
fn an_extreme_panorama_keeps_at_least_one_pixel_on_its_short_edge() {
let budget = Budget::for_edge(100);
let fitted = budget.fit(source(20_000, 1));
assert_eq!(fitted.width, 100);
assert_eq!(fitted.height, 1);
}
#[test]
fn what_is_kept_depends_on_the_window_and_not_on_the_file() {
let budget = Budget::for_viewport(ViewportPixels { width: 1920, height: 1080 });
let huge = budget.fit(source(12_000, 8_000)).rgba_bytes();
let large = budget.fit(source(9_000, 6_000)).rgba_bytes();
assert_eq!(huge, large);
}
#[test]
fn a_scaled_display_gets_a_bigger_budget_than_its_logical_size() {
let logical = ViewportPixels::from_logical(1600.0, 1000.0, 1.0);
let scaled = ViewportPixels::from_logical(1600.0, 1000.0, 1.6);
assert_eq!(scaled.width, 2560);
assert!(
Budget::for_viewport(scaled).max_retained_bytes()
> Budget::for_viewport(logical).max_retained_bytes()
);
}
#[test]
fn a_file_claiming_an_impossible_size_is_refused_before_decoding() {
let budget = Budget::for_edge(2000);
assert!(budget.allows_source(source(8001, 4501)));
assert!(!budget.allows_source(source(200_000, 4)));
}
#[test]
fn a_zero_sized_viewport_still_yields_a_usable_budget() {
let budget = Budget::for_viewport(ViewportPixels { width: 0, height: 0 });
let fitted = budget.fit(source(4000, 3000));
assert!(fitted.width >= 1 && fitted.height >= 1);
}
}