use crate::binary::{be_f64_at, be_u32_at};
use crate::error::{Result, bail};
use crate::image::RgbaImage;
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct Placement {
pub width: u32,
pub height: u32,
pub scale: (f64, f64),
pub center: (f64, f64),
pub corners: [(f64, f64); 4],
}
const CORNERS_AT: usize = 120;
impl Placement {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < CORNERS_AT + 64 {
bail!("ResizableImageInfo of {} bytes, expected at least 184", data.len());
}
let f = |at: usize| be_f64_at(data, at);
let corners = [0, 1, 2, 3].map(|i| (f(CORNERS_AT + 16 * i), f(CORNERS_AT + 16 * i + 8)));
let placement = Placement {
width: be_u32_at(data, 32),
height: be_u32_at(data, 36),
scale: (f(40), f(48)),
center: (f(64), f(72)),
corners,
};
if placement.width == 0
|| placement.height == 0
|| corners.iter().any(|(x, y)| !x.is_finite() || !y.is_finite())
{
bail!("ResizableImageInfo without a usable size or corners");
}
Ok(placement)
}
fn inverse(&self, offset: (f64, f64)) -> Option<[f64; 9]> {
let (w, h) = (self.width as f64, self.height as f64);
let source = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)];
let target = self.corners.map(|(x, y)| (x + offset.0, y + offset.1));
homography(target, source)
}
pub fn draw(
&self,
original: &RgbaImage,
level_scale: f64,
offset: (f64, f64),
width: u32,
height: u32,
scale: f64,
) -> Option<RgbaImage> {
self.draw_region(original, level_scale, offset, (0, 0, width, height), scale)
}
pub fn bounds(&self, offset: (f64, f64), scale: f64) -> (i64, i64, i64, i64) {
let xs = self.corners.map(|c| (c.0 + offset.0) * scale);
let ys = self.corners.map(|c| (c.1 + offset.1) * scale);
let min = |v: [f64; 4]| v.iter().copied().fold(f64::INFINITY, f64::min).floor() as i64;
let max = |v: [f64; 4]| v.iter().copied().fold(f64::NEG_INFINITY, f64::max).ceil() as i64;
(min(xs), min(ys), max(xs), max(ys))
}
pub fn draw_region(
&self,
original: &RgbaImage,
level_scale: f64,
offset: (f64, f64),
region: (u32, u32, u32, u32),
scale: f64,
) -> Option<RgbaImage> {
let (left, top, width, height) = region;
let m = self.inverse(offset)?;
let premultiplied: Vec<[f32; 4]> = original
.pixels
.as_chunks::<4>()
.0
.iter()
.map(|p| {
let a = p[3] as f32 / 255.0;
[p[0] as f32 * a, p[1] as f32 * a, p[2] as f32 * a, p[3] as f32]
})
.collect();
let (ow, oh) = (original.width as i64, original.height as i64);
let texel = |x: i64, y: i64| premultiplied[(y.clamp(0, oh - 1) * ow + x.clamp(0, ow - 1)) as usize];
let (bx0, by0, bx1, by1) = self.bounds(offset, scale);
let clip = |value: i64, start: u32, size: u32| (value - start as i64).clamp(0, size as i64) as u32;
let (x0, x1) = (clip(bx0, left, width), clip(bx1, left, width));
let (y0, y1) = (clip(by0, top, height), clip(by1, top, height));
let mut out = RgbaImage::new(width, height);
for y in y0..y1 {
for x in x0..x1 {
let cx = ((x + left) as f64 + 0.5) / scale;
let cy = ((y + top) as f64 + 0.5) / scale;
let d = m[6] * cx + m[7] * cy + m[8];
if d.abs() < 1e-12 {
continue;
}
let sx = (m[0] * cx + m[1] * cy + m[2]) / d * level_scale - 0.5;
let sy = (m[3] * cx + m[4] * cy + m[5]) / d * level_scale - 0.5;
if sx < -0.5 || sy < -0.5 || sx >= ow as f64 - 0.5 || sy >= oh as f64 - 0.5 {
continue;
}
let (fx, fy) = (sx.floor(), sy.floor());
let (tx, ty) = ((sx - fx) as f32, (sy - fy) as f32);
let (ix, iy) = (fx as i64, fy as i64);
let (a, b, c, e) = (texel(ix, iy), texel(ix + 1, iy), texel(ix, iy + 1), texel(ix + 1, iy + 1));
let mut p = [0f32; 4];
for k in 0..4 {
let top = a[k] + (b[k] - a[k]) * tx;
let bottom = c[k] + (e[k] - c[k]) * tx;
p[k] = top + (bottom - top) * ty;
}
if p[3] <= 0.0 {
continue;
}
let alpha = p[3] / 255.0;
let at = (y as usize * width as usize + x as usize) * 4;
let straight = [p[0] / alpha, p[1] / alpha, p[2] / alpha, p[3]];
for (target, value) in out.pixels[at..at + 4].iter_mut().zip(straight) {
*target = value.round().clamp(0.0, 255.0) as u8;
}
}
}
Some(out)
}
pub fn magnification(&self) -> f64 {
let length = |a: (f64, f64), b: (f64, f64)| ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt();
let x = length(self.corners[0], self.corners[1]) / self.width as f64;
let y = length(self.corners[0], self.corners[2]) / self.height as f64;
x.max(y)
}
}
fn homography(from: [(f64, f64); 4], to: [(f64, f64); 4]) -> Option<[f64; 9]> {
let mut a = [[0f64; 9]; 8];
for (i, ((x, y), (u, v))) in from.iter().zip(to.iter()).enumerate() {
a[2 * i] = [*x, *y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, *u];
a[2 * i + 1] = [0.0, 0.0, 0.0, *x, *y, 1.0, -v * x, -v * y, *v];
}
for col in 0..8 {
let pivot = (col..8).max_by(|&r, &s| a[r][col].abs().total_cmp(&a[s][col].abs()))?;
if a[pivot][col].abs() < 1e-12 {
return None;
}
a.swap(col, pivot);
let pivot_row = a[col];
for (index, row) in a.iter_mut().enumerate() {
if index != col {
let factor = row[col] / pivot_row[col];
for (value, pivot_value) in row.iter_mut().zip(pivot_row).skip(col) {
*value -= factor * pivot_value;
}
}
}
}
let mut h = [0f64; 9];
for (i, (value, row)) in h.iter_mut().zip(&a).enumerate() {
*value = row[8] / row[i];
}
h[8] = 1.0;
Some(h)
}
#[cfg(test)]
mod tests {
use super::*;
fn blob(width: u32, height: u32, corners: [(f64, f64); 4]) -> Vec<u8> {
let mut data = vec![0u8; 184];
data[32..36].copy_from_slice(&width.to_be_bytes());
data[36..40].copy_from_slice(&height.to_be_bytes());
for (i, (x, y)) in corners.iter().enumerate() {
data[120 + 16 * i..128 + 16 * i].copy_from_slice(&x.to_be_bytes());
data[128 + 16 * i..136 + 16 * i].copy_from_slice(&y.to_be_bytes());
}
data
}
#[test]
fn scaled_and_moved_image_lands_on_its_corners() {
let mut original = RgbaImage::new(4, 2);
original.fill([200, 10, 20, 255]);
let placement = Placement::parse(&blob(4, 2, [(10.0, 6.0), (18.0, 6.0), (10.0, 10.0), (18.0, 10.0)])).unwrap();
assert!((placement.magnification() - 2.0).abs() < 1e-9);
let image = placement.draw(&original, 1.0, (2.0, 1.0), 32, 16, 1.0).unwrap();
assert_eq!(image.pixel(13, 9), [200, 10, 20, 255]);
assert_eq!(image.pixel(11, 6)[3], 0);
assert_eq!(image.pixel(20, 9)[3], 0);
assert_eq!(image.pixel(19, 10), [200, 10, 20, 255]);
}
#[test]
fn flipped_corners_mirror_the_image() {
let mut original = RgbaImage::new(2, 1);
original.set_pixel(0, 0, [255, 0, 0, 255]);
original.set_pixel(1, 0, [0, 0, 255, 255]);
let placement = Placement::parse(&blob(2, 1, [(20.0, 0.0), (0.0, 0.0), (20.0, 10.0), (0.0, 10.0)])).unwrap();
let image = placement.draw(&original, 1.0, (0.0, 0.0), 20, 10, 1.0).unwrap();
assert_eq!(image.pixel(2, 5), [0, 0, 255, 255]);
assert_eq!(image.pixel(17, 5), [255, 0, 0, 255]);
}
#[test]
fn a_region_is_the_same_part_of_the_whole() {
let mut original = RgbaImage::new(3, 3);
for (i, pixel) in original.pixels.chunks_mut(4).enumerate() {
pixel.copy_from_slice(&[i as u8 * 20, 100, 200 - i as u8 * 10, 255]);
}
let placement = Placement::parse(&blob(3, 3, [(3.5, 2.0), (15.0, 4.0), (2.0, 13.0), (14.0, 16.5)])).unwrap();
let whole = placement.draw(&original, 1.0, (1.0, 1.0), 24, 20, 1.0).unwrap();
let part = placement.draw_region(&original, 1.0, (1.0, 1.0), (5, 4, 8, 9), 1.0).unwrap();
for y in 0..9 {
for x in 0..8 {
assert_eq!(part.pixel(x, y), whole.pixel(x + 5, y + 4));
}
}
assert_eq!(placement.bounds((1.0, 1.0), 1.0), (3, 3, 16, 18));
}
}