use super::{Fit, ImageData};
use crate::color::Rgb;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Half {
Empty,
Top(Rgb),
Bottom(Rgb),
Both(Rgb, Rgb),
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Placement {
source: (f64, f64, f64, f64),
x: u32,
y: u32,
width: u32,
height: u32,
}
fn place(image: (u32, u32), area: (u32, u32), fit: Fit) -> Placement {
let (iw, ih) = (f64::from(image.0), f64::from(image.1));
let (aw, ah) = (f64::from(area.0), f64::from(area.1));
match fit {
Fit::Contain => {
let scale = (aw / iw).min(ah / ih);
let width = to_pixels(iw * scale).clamp(1, area.0);
let height = to_pixels(ih * scale).clamp(1, area.1);
Placement { source: (0.0, 0.0, iw, ih), x: (area.0 - width) / 2, y: (area.1 - height) / 2, width, height }
}
Fit::Cover => {
let scale = (aw / iw).max(ah / ih);
let (shown_w, shown_h) = ((aw / scale).min(iw), (ah / scale).min(ih));
Placement {
source: ((iw - shown_w) / 2.0, (ih - shown_h) / 2.0, shown_w, shown_h),
x: 0,
y: 0,
width: area.0,
height: area.1,
}
}
Fit::Center => {
let (width, height) = (image.0.min(area.0), image.1.min(area.1));
let left = f64::from((image.0 - width) / 2);
let top = f64::from((image.1 - height) / 2);
Placement {
source: (left, top, f64::from(width), f64::from(height)),
x: (area.0 - width) / 2,
y: (area.1 - height) / 2,
width,
height,
}
}
}
}
fn to_pixels(length: f64) -> u32 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let pixels = length.round().max(0.0) as u32;
pixels
}
pub(super) fn measure(data: &ImageData, room: (u16, u16), fit: Fit) -> (u16, u16) {
if room.0 == 0 || room.1 == 0 {
return (0, 0);
}
match fit {
Fit::Cover => room,
Fit::Contain | Fit::Center => {
let area = (u32::from(room.0), u32::from(room.1) * 2);
let placed = place((data.width(), data.height()), area, fit);
let width = u16::try_from(placed.width).unwrap_or(room.0);
let height = u16::try_from(placed.height.div_ceil(2)).unwrap_or(room.1);
(width.min(room.0), height.min(room.1))
}
}
}
pub(super) fn cells(data: &ImageData, columns: u16, rows: u16, fit: Fit) -> Vec<Half> {
let area = (u32::from(columns), u32::from(rows) * 2);
if area.0 == 0 || area.1 == 0 {
return Vec::new();
}
let placed = place((data.width(), data.height()), area, fit);
let pixels = resample(data, placed.source, placed.width, placed.height);
let at = |px: u32, py: u32| -> Option<Rgb> {
let (x, y) = (px.checked_sub(placed.x)?, py.checked_sub(placed.y)?);
if x >= placed.width || y >= placed.height {
return None;
}
pixels.get(usize::try_from(y * placed.width + x).ok()?).copied()
};
let mut out = Vec::with_capacity(usize::from(columns) * usize::from(rows));
for row in 0..u32::from(rows) {
for column in 0..u32::from(columns) {
out.push(match (at(column, row * 2), at(column, row * 2 + 1)) {
(Some(top), Some(bottom)) => Half::Both(top, bottom),
(Some(top), None) => Half::Top(top),
(None, Some(bottom)) => Half::Bottom(bottom),
(None, None) => Half::Empty,
});
}
}
out
}
fn resample(data: &ImageData, source: (f64, f64, f64, f64), width: u32, height: u32) -> Vec<Rgb> {
let columns = spans(source.0, source.2, width, data.width());
let rows = spans(source.1, source.3, height, data.height());
let pixels = data.pixels();
let stride = usize::try_from(data.width()).unwrap_or(usize::MAX);
let mut out = Vec::with_capacity(columns.len() * rows.len());
for row in &rows {
for column in &columns {
let (mut r, mut g, mut b, mut total) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
for &(y, wy) in row {
let line = &pixels[y * stride..(y + 1) * stride];
for &(x, wx) in column {
let weight = wy * wx;
let pixel = line[x];
r += weight * f64::from(pixel.r);
g += weight * f64::from(pixel.g);
b += weight * f64::from(pixel.b);
total += weight;
}
}
out.push(if total > 0.0 {
Rgb::new(channel(r / total), channel(g / total), channel(b / total))
} else {
Rgb::new(0, 0, 0)
});
}
}
out
}
fn channel(value: f64) -> u8 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let channel = value.round().clamp(0.0, 255.0) as u8;
channel
}
fn spans(start: f64, length: f64, count: u32, limit: u32) -> Vec<Vec<(usize, f64)>> {
let step = length / f64::from(count.max(1));
let last = limit.saturating_sub(1);
(0..count)
.map(|index| {
let from = start + step * f64::from(index);
let to = from + step;
let first = to_index(from.floor()).min(last);
let end = to_index(to.ceil()).clamp(first + 1, limit);
let mut span: Vec<(usize, f64)> = (first..end)
.filter_map(|pixel| {
let covered = to.min(f64::from(pixel) + 1.0) - from.max(f64::from(pixel));
(covered > 1e-9).then(|| (usize::try_from(pixel).unwrap_or(usize::MAX), covered))
})
.collect();
if span.is_empty() {
span.push((usize::try_from(first).unwrap_or(0), 1.0));
}
span
})
.collect()
}
fn to_index(value: f64) -> u32 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let index = value.max(0.0) as u32;
index
}