use crate::core::{Pix, PixelDepth, pixel};
use crate::transform::{TransformError, TransformResult};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AffineFill {
#[default]
White,
Black,
Color(u32),
}
impl AffineFill {
pub fn to_value(self, depth: PixelDepth) -> u32 {
match self {
AffineFill::White => match depth {
PixelDepth::Bit1 => 0, PixelDepth::Bit2 => 3,
PixelDepth::Bit4 => 15,
PixelDepth::Bit8 => 255,
PixelDepth::Bit16 => 65535,
PixelDepth::Bit32 => 0xFFFFFF00,
},
AffineFill::Black => match depth {
PixelDepth::Bit1 => 1, PixelDepth::Bit32 => 0x00000000,
_ => 0,
},
AffineFill::Color(val) => val,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AffineMatrix {
coeffs: [f32; 6],
}
impl Default for AffineMatrix {
fn default() -> Self {
Self::identity()
}
}
impl AffineMatrix {
pub fn identity() -> Self {
Self {
coeffs: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
}
}
pub fn from_coeffs(coeffs: [f32; 6]) -> Self {
Self { coeffs }
}
pub fn coeffs(&self) -> &[f32; 6] {
&self.coeffs
}
pub fn translation(tx: f32, ty: f32) -> Self {
Self {
coeffs: [1.0, 0.0, tx, 0.0, 1.0, ty],
}
}
pub fn scale(sx: f32, sy: f32) -> Self {
Self {
coeffs: [sx, 0.0, 0.0, 0.0, sy, 0.0],
}
}
pub fn rotation(center_x: f32, center_y: f32, angle: f32) -> Self {
let cosa = angle.cos();
let sina = angle.sin();
Self {
coeffs: [
cosa,
-sina,
center_x * (1.0 - cosa) + center_y * sina,
sina,
cosa,
center_y * (1.0 - cosa) - center_x * sina,
],
}
}
pub fn from_three_points(src_pts: [Point; 3], dst_pts: [Point; 3]) -> TransformResult<Self> {
let x1 = src_pts[0].x;
let y1 = src_pts[0].y;
let x2 = src_pts[1].x;
let y2 = src_pts[1].y;
let x3 = src_pts[2].x;
let y3 = src_pts[2].y;
let mut a = [
[x1, y1, 1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, x1, y1, 1.0],
[x2, y2, 1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, x2, y2, 1.0],
[x3, y3, 1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, x3, y3, 1.0],
];
let mut b = [
dst_pts[0].x,
dst_pts[0].y,
dst_pts[1].x,
dst_pts[1].y,
dst_pts[2].x,
dst_pts[2].y,
];
gauss_jordan(&mut a, &mut b, 6)?;
Ok(Self {
coeffs: [b[0], b[1], b[2], b[3], b[4], b[5]],
})
}
pub fn inverse(&self) -> TransformResult<Self> {
let [a, b, tx, c, d, ty] = self.coeffs;
let det = a * d - b * c;
if det.abs() < 1e-10 {
return Err(TransformError::SingularMatrix);
}
let inv_det = 1.0 / det;
let a_inv = d * inv_det;
let b_inv = -b * inv_det;
let c_inv = -c * inv_det;
let d_inv = a * inv_det;
let tx_inv = -(a_inv * tx + b_inv * ty);
let ty_inv = -(c_inv * tx + d_inv * ty);
Ok(Self {
coeffs: [a_inv, b_inv, tx_inv, c_inv, d_inv, ty_inv],
})
}
pub fn compose(&self, other: &Self) -> Self {
let [a1, b1, tx1, c1, d1, ty1] = self.coeffs;
let [a2, b2, tx2, c2, d2, ty2] = other.coeffs;
Self {
coeffs: [
a2 * a1 + b2 * c1,
a2 * b1 + b2 * d1,
a2 * tx1 + b2 * ty1 + tx2,
c2 * a1 + d2 * c1,
c2 * b1 + d2 * d1,
c2 * tx1 + d2 * ty1 + ty2,
],
}
}
pub fn transform_point(&self, pt: Point) -> Point {
let [a, b, tx, c, d, ty] = self.coeffs;
Point {
x: a * pt.x + b * pt.y + tx,
y: c * pt.x + d * pt.y + ty,
}
}
pub fn transform_point_sampled(&self, x: i32, y: i32) -> (i32, i32) {
let [a, b, tx, c, d, ty] = self.coeffs;
let xf = x as f32;
let yf = y as f32;
let xp = (a * xf + b * yf + tx + 0.5).floor() as i32;
let yp = (c * xf + d * yf + ty + 0.5).floor() as i32;
(xp, yp)
}
pub fn transform_point_float(&self, x: f32, y: f32) -> (f32, f32) {
let [a, b, tx, c, d, ty] = self.coeffs;
let xp = a * x + b * y + tx;
let yp = c * x + d * y + ty;
(xp, yp)
}
}
fn gauss_jordan(a: &mut [[f32; 6]; 6], b: &mut [f32; 6], n: usize) -> TransformResult<()> {
let mut index_c = [0usize; 6];
let mut index_r = [0usize; 6];
let mut ipiv = [0i32; 6];
for i in 0..n {
let mut max_val = 0.0f32;
let mut irow = 0;
let mut icol = 0;
for j in 0..n {
if ipiv[j] != 1 {
for k in 0..n {
if ipiv[k] == 0 {
let abs_val = a[j][k].abs();
if abs_val >= max_val {
max_val = abs_val;
irow = j;
icol = k;
}
} else if ipiv[k] > 1 {
return Err(TransformError::SingularMatrix);
}
}
}
}
ipiv[icol] += 1;
if irow != icol {
a.swap(irow, icol);
b.swap(irow, icol);
}
index_r[i] = irow;
index_c[i] = icol;
if a[icol][icol] == 0.0 {
return Err(TransformError::SingularMatrix);
}
let pivinv = 1.0 / a[icol][icol];
a[icol][icol] = 1.0;
for item in a[icol].iter_mut().take(n) {
*item *= pivinv;
}
b[icol] *= pivinv;
for row in 0..n {
if row != icol {
let val = a[row][icol];
a[row][icol] = 0.0;
#[allow(clippy::needless_range_loop)]
for col in 0..n {
a[row][col] -= a[icol][col] * val;
}
b[row] -= b[icol] * val;
}
}
}
for col in (0..n).rev() {
if index_r[col] != index_c[col] {
for row_arr in a.iter_mut().take(n) {
row_arr.swap(index_r[col], index_c[col]);
}
}
}
Ok(())
}
pub fn affine_sampled(pix: &Pix, matrix: &AffineMatrix, fill: AffineFill) -> TransformResult<Pix> {
let inv_matrix = matrix.inverse()?;
affine_sampled_with_inverse(pix, &inv_matrix, fill)
}
fn affine_sampled_with_inverse(
pix: &Pix,
inv_matrix: &AffineMatrix,
fill: AffineFill,
) -> TransformResult<Pix> {
let w = pix.width();
let h = pix.height();
let depth = pix.depth();
let fill_value = fill.to_value(depth);
let out_pix = Pix::new(w, h, depth)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
if let Some(cmap) = pix.colormap() {
let _ = out_mut.set_colormap(Some(cmap.clone()));
}
fill_image(&mut out_mut, fill_value);
let wi = w as i32;
let hi = h as i32;
for j in 0..h {
for i in 0..w {
let (sx, sy) = inv_matrix.transform_point_sampled(i as i32, j as i32);
if sx >= 0 && sx < wi && sy >= 0 && sy < hi {
let val = pix.get_pixel_unchecked(sx as u32, sy as u32);
out_mut.set_pixel_unchecked(i, j, val);
}
}
}
Ok(out_mut.into())
}
pub fn affine_sampled_pta(
pix: &Pix,
src_pts: [Point; 3],
dst_pts: [Point; 3],
fill: AffineFill,
) -> TransformResult<Pix> {
let inv_matrix = AffineMatrix::from_three_points(dst_pts, src_pts)?;
affine_sampled_with_inverse(pix, &inv_matrix, fill)
}
pub fn affine(pix: &Pix, matrix: &AffineMatrix, fill: AffineFill) -> TransformResult<Pix> {
let depth = pix.depth();
if depth == PixelDepth::Bit1 {
return affine_sampled(pix, matrix, fill);
}
let inv_matrix = matrix.inverse()?;
match depth {
PixelDepth::Bit8 if pix.colormap().is_none() => affine_gray(pix, &inv_matrix, fill),
PixelDepth::Bit32 => affine_color(pix, &inv_matrix, fill),
_ => {
affine_sampled_with_inverse(pix, &inv_matrix, fill)
}
}
}
pub fn affine_pta(
pix: &Pix,
src_pts: [Point; 3],
dst_pts: [Point; 3],
fill: AffineFill,
) -> TransformResult<Pix> {
let depth = pix.depth();
if depth == PixelDepth::Bit1 {
return affine_sampled_pta(pix, src_pts, dst_pts, fill);
}
let inv_matrix = AffineMatrix::from_three_points(dst_pts, src_pts)?;
match depth {
PixelDepth::Bit8 if pix.colormap().is_none() => affine_gray(pix, &inv_matrix, fill),
PixelDepth::Bit32 => affine_color(pix, &inv_matrix, fill),
_ => affine_sampled_with_inverse(pix, &inv_matrix, fill),
}
}
fn affine_gray(pix: &Pix, inv_matrix: &AffineMatrix, fill: AffineFill) -> TransformResult<Pix> {
let w = pix.width();
let h = pix.height();
let depth = pix.depth();
let fill_value = fill.to_value(depth) as u8;
let out_pix = Pix::new(w, h, depth)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
fill_image(&mut out_mut, fill_value as u32);
let wi = w as i32;
let hi = h as i32;
let wm2 = wi - 2;
let hm2 = hi - 2;
let [a, b, tx, c, d, ty] = *inv_matrix.coeffs();
let a16 = 16.0 * a;
let b16 = 16.0 * b;
let c16 = 16.0 * c;
let d16 = 16.0 * d;
for j in 0..h {
let jf = j as f32;
for i in 0..w {
let if_ = i as f32;
let xpm = (a16 * if_ + b16 * jf + 16.0 * tx) as i32;
let ypm = (c16 * if_ + d16 * jf + 16.0 * ty) as i32;
let xp = xpm >> 4;
let yp = ypm >> 4;
let xf = xpm & 0x0f;
let yf = ypm & 0x0f;
if xp < 0 || yp < 0 || xp > wm2 || yp > hm2 {
continue;
}
let v00 = pix.get_pixel_unchecked(xp as u32, yp as u32) as i32;
let v10 = pix.get_pixel_unchecked((xp + 1) as u32, yp as u32) as i32;
let v01 = pix.get_pixel_unchecked(xp as u32, (yp + 1) as u32) as i32;
let v11 = pix.get_pixel_unchecked((xp + 1) as u32, (yp + 1) as u32) as i32;
let val = ((16 - xf) * (16 - yf) * v00
+ xf * (16 - yf) * v10
+ (16 - xf) * yf * v01
+ xf * yf * v11
+ 128)
/ 256;
out_mut.set_pixel_unchecked(i, j, val as u32);
}
}
Ok(out_mut.into())
}
fn affine_color(pix: &Pix, inv_matrix: &AffineMatrix, fill: AffineFill) -> TransformResult<Pix> {
let w = pix.width();
let h = pix.height();
let depth = pix.depth();
let fill_value = fill.to_value(depth);
let out_pix = Pix::new(w, h, depth)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(pix.spp());
fill_image(&mut out_mut, fill_value);
let wi = w as i32;
let hi = h as i32;
let wm2 = wi - 2;
let hm2 = hi - 2;
let [a, b, tx, c, d, ty] = *inv_matrix.coeffs();
let a16 = 16.0 * a;
let b16 = 16.0 * b;
let c16 = 16.0 * c;
let d16 = 16.0 * d;
for j in 0..h {
let jf = j as f32;
for i in 0..w {
let if_ = i as f32;
let xpm = (a16 * if_ + b16 * jf + 16.0 * tx) as i32;
let ypm = (c16 * if_ + d16 * jf + 16.0 * ty) as i32;
let xp = xpm >> 4;
let yp = ypm >> 4;
let xf = xpm & 0x0f;
let yf = ypm & 0x0f;
if xp < 0 || yp < 0 || xp > wm2 || yp > hm2 {
continue;
}
let p00 = pix.get_pixel_unchecked(xp as u32, yp as u32);
let p10 = pix.get_pixel_unchecked((xp + 1) as u32, yp as u32);
let p01 = pix.get_pixel_unchecked(xp as u32, (yp + 1) as u32);
let p11 = pix.get_pixel_unchecked((xp + 1) as u32, (yp + 1) as u32);
let (r00, g00, b00, a00) = pixel::extract_rgba(p00);
let (r10, g10, b10, a10) = pixel::extract_rgba(p10);
let (r01, g01, b01, a01) = pixel::extract_rgba(p01);
let (r11, g11, b11, a11) = pixel::extract_rgba(p11);
let r = area_interp(r00, r10, r01, r11, xf, yf);
let g = area_interp(g00, g10, g01, g11, xf, yf);
let b = area_interp(b00, b10, b01, b11, xf, yf);
let av = area_interp(a00, a10, a01, a11, xf, yf);
let pixel = pixel::compose_rgba(r, g, b, av);
out_mut.set_pixel_unchecked(i, j, pixel);
}
}
Ok(out_mut.into())
}
#[inline]
fn area_interp(v00: u8, v10: u8, v01: u8, v11: u8, xf: i32, yf: i32) -> u8 {
let val = ((16 - xf) * (16 - yf) * v00 as i32
+ xf * (16 - yf) * v10 as i32
+ (16 - xf) * yf * v01 as i32
+ xf * yf * v11 as i32
+ 128)
/ 256;
val.clamp(0, 255) as u8
}
fn fill_image(pix: &mut crate::core::PixMut, value: u32) {
let w = pix.width();
let h = pix.height();
for y in 0..h {
for x in 0..w {
pix.set_pixel_unchecked(x, y, value);
}
}
}
pub fn affine_pta_with_alpha(
pix: &Pix,
src_pts: [Point; 3],
dst_pts: [Point; 3],
alpha_mask: Option<&Pix>,
opacity: f32,
border: u32,
) -> TransformResult<Pix> {
with_alpha_transform(
pix,
alpha_mask,
opacity,
border,
&src_pts,
&dst_pts,
|img, src, dst| {
let s: [Point; 3] = [src[0], src[1], src[2]];
let d: [Point; 3] = [dst[0], dst[1], dst[2]];
affine_pta(img, s, d, AffineFill::Black)
},
)
}
pub(crate) fn with_alpha_transform<F>(
pix: &Pix,
alpha_mask: Option<&Pix>,
opacity: f32,
border: u32,
src_pts: &[Point],
dst_pts: &[Point],
transform_fn: F,
) -> TransformResult<Pix>
where
F: Fn(&Pix, &[Point], &[Point]) -> TransformResult<Pix>,
{
use crate::core::pix::RgbComponent;
if pix.depth() != PixelDepth::Bit32 {
return Err(TransformError::UnsupportedDepth(
"WithAlpha requires 32bpp input".into(),
));
}
let opacity = opacity.clamp(0.0, 1.0);
if let Some(mask) = alpha_mask
&& mask.depth() != PixelDepth::Bit8
{
return Err(TransformError::InvalidParameters(
"alpha_mask must be 8bpp".into(),
));
}
let ws = pix.width();
let hs = pix.height();
let pixb1 = pix.add_border(border, 0)?;
let border_f = border as f32;
let adj_src: Vec<Point> = src_pts
.iter()
.map(|p| Point::new(p.x + border_f, p.y + border_f))
.collect();
let adj_dst: Vec<Point> = dst_pts
.iter()
.map(|p| Point::new(p.x + border_f, p.y + border_f))
.collect();
let pixd = transform_fn(&pixb1, &adj_src, &adj_dst)?;
let use_custom_mask = alpha_mask.is_some();
let pix_alpha = if let Some(mask) = alpha_mask {
if mask.width() != ws || mask.height() != hs {
mask.resize_to_match(None, ws, hs)?
} else {
mask.deep_clone()
}
} else {
let alpha_val = (255.0 * opacity) as u32;
let alpha_pix = Pix::new(ws, hs, PixelDepth::Bit8)?;
let mut am = alpha_pix.try_into_mut().unwrap();
for y in 0..hs {
for x in 0..ws {
am.set_pixel_unchecked(x, y, alpha_val);
}
}
am.into()
};
let pix_alpha = if !use_custom_mask && ws > 10 && hs > 10 {
let mut am = pix_alpha.try_into_mut().unwrap();
am.set_border_val(1, 1, 1, 1, 0)
.map_err(TransformError::Core)?;
if ws > 12 && hs > 12 {
let ring2_val = (255.0 * opacity * 0.5) as u32;
for x in 1..(ws - 1) {
am.set_pixel_unchecked(x, 1, ring2_val);
am.set_pixel_unchecked(x, hs - 2, ring2_val);
}
for y in 1..(hs - 1) {
am.set_pixel_unchecked(1, y, ring2_val);
am.set_pixel_unchecked(ws - 2, y, ring2_val);
}
}
am.into()
} else {
pix_alpha
};
let pixb2 = pix_alpha.add_border(border, 0)?;
let pixga = transform_fn(&pixb2, &adj_src, &adj_dst)?;
let mut pixd_mut = pixd.try_into_mut().unwrap();
pixd_mut
.set_rgb_component(&pixga, RgbComponent::Alpha)
.map_err(TransformError::Core)?;
Ok(pixd_mut.into())
}
pub fn translate(pix: &Pix, tx: f32, ty: f32) -> TransformResult<Pix> {
let matrix = AffineMatrix::translation(tx, ty);
affine_sampled(pix, &matrix, AffineFill::White)
}
pub fn affine_scale(pix: &Pix, sx: f32, sy: f32) -> TransformResult<Pix> {
if sx <= 0.0 || sy <= 0.0 {
return Err(TransformError::InvalidParameters(
"scale factors must be positive".to_string(),
));
}
let matrix = AffineMatrix::scale(sx, sy);
affine(pix, &matrix, AffineFill::White)
}
pub fn affine_rotate(pix: &Pix, center_x: f32, center_y: f32, angle: f32) -> TransformResult<Pix> {
let matrix = AffineMatrix::rotation(center_x, center_y, angle);
affine(pix, &matrix, AffineFill::White)
}
pub fn pta_affine_transform(pta: &crate::core::Pta, matrix: &AffineMatrix) -> crate::core::Pta {
let c = matrix.coeffs();
pta.iter()
.map(|(x, y)| (c[0] * x + c[1] * y + c[2], c[3] * x + c[4] * y + c[5]))
.collect()
}
pub fn boxa_affine_transform(boxa: &crate::core::Boxa, matrix: &AffineMatrix) -> crate::core::Boxa {
use crate::core::Pta;
let n = boxa.len();
let mut flat_pta = Pta::with_capacity(n * 4);
for b in boxa.iter() {
let x0 = b.x as f32;
let y0 = b.y as f32;
let x1 = (b.x + b.w) as f32;
let y1 = (b.y + b.h) as f32;
flat_pta.push(x0, y0);
flat_pta.push(x1, y0);
flat_pta.push(x0, y1);
flat_pta.push(x1, y1);
}
let transformed = pta_affine_transform(&flat_pta, matrix);
let mut result = crate::core::Boxa::with_capacity(n);
for i in 0..n {
let base = i * 4;
let xmin = (0..4)
.map(|k| transformed.get(base + k).unwrap().0)
.fold(f32::INFINITY, f32::min);
let ymin = (0..4)
.map(|k| transformed.get(base + k).unwrap().1)
.fold(f32::INFINITY, f32::min);
let xmax = (0..4)
.map(|k| transformed.get(base + k).unwrap().0)
.fold(f32::NEG_INFINITY, f32::max);
let ymax = (0..4)
.map(|k| transformed.get(base + k).unwrap().1)
.fold(f32::NEG_INFINITY, f32::max);
result.push(crate::core::Box::new_unchecked(
xmin.round() as i32,
ymin.round() as i32,
(xmax - xmin).round() as i32,
(ymax - ymin).round() as i32,
));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point_new() {
let pt = Point::new(1.5, 2.5);
assert_eq!(pt.x, 1.5);
assert_eq!(pt.y, 2.5);
}
#[test]
fn test_identity_matrix() {
let m = AffineMatrix::identity();
let pt = Point::new(10.0, 20.0);
let transformed = m.transform_point(pt);
assert!((transformed.x - pt.x).abs() < 1e-5);
assert!((transformed.y - pt.y).abs() < 1e-5);
}
#[test]
fn test_translation_matrix() {
let m = AffineMatrix::translation(5.0, -3.0);
let pt = Point::new(10.0, 20.0);
let transformed = m.transform_point(pt);
assert!((transformed.x - 15.0).abs() < 1e-5);
assert!((transformed.y - 17.0).abs() < 1e-5);
}
#[test]
fn test_scale_matrix() {
let m = AffineMatrix::scale(2.0, 0.5);
let pt = Point::new(10.0, 20.0);
let transformed = m.transform_point(pt);
assert!((transformed.x - 20.0).abs() < 1e-5);
assert!((transformed.y - 10.0).abs() < 1e-5);
}
#[test]
fn test_rotation_matrix_90_deg() {
let m = AffineMatrix::rotation(0.0, 0.0, std::f32::consts::FRAC_PI_2);
let pt = Point::new(1.0, 0.0);
let transformed = m.transform_point(pt);
assert!((transformed.x - 0.0).abs() < 1e-5);
assert!((transformed.y - 1.0).abs() < 1e-5);
}
#[test]
fn test_rotation_about_center() {
let m = AffineMatrix::rotation(5.0, 5.0, std::f32::consts::PI);
let pt = Point::new(10.0, 5.0);
let transformed = m.transform_point(pt);
assert!((transformed.x - 0.0).abs() < 1e-4);
assert!((transformed.y - 5.0).abs() < 1e-4);
}
#[test]
fn test_inverse_identity() {
let m = AffineMatrix::identity();
let inv = m.inverse().unwrap();
let pt = Point::new(10.0, 20.0);
let transformed = inv.transform_point(pt);
assert!((transformed.x - pt.x).abs() < 1e-5);
assert!((transformed.y - pt.y).abs() < 1e-5);
}
#[test]
fn test_inverse_translation() {
let m = AffineMatrix::translation(5.0, -3.0);
let inv = m.inverse().unwrap();
let pt = Point::new(10.0, 20.0);
let transformed = m.transform_point(pt);
let back = inv.transform_point(transformed);
assert!((back.x - pt.x).abs() < 1e-5);
assert!((back.y - pt.y).abs() < 1e-5);
}
#[test]
fn test_inverse_scale() {
let m = AffineMatrix::scale(2.0, 3.0);
let inv = m.inverse().unwrap();
let pt = Point::new(10.0, 20.0);
let transformed = m.transform_point(pt);
let back = inv.transform_point(transformed);
assert!((back.x - pt.x).abs() < 1e-5);
assert!((back.y - pt.y).abs() < 1e-5);
}
#[test]
fn test_inverse_rotation() {
let m = AffineMatrix::rotation(5.0, 5.0, 0.5);
let inv = m.inverse().unwrap();
let pt = Point::new(10.0, 20.0);
let transformed = m.transform_point(pt);
let back = inv.transform_point(transformed);
assert!((back.x - pt.x).abs() < 1e-4);
assert!((back.y - pt.y).abs() < 1e-4);
}
#[test]
fn test_singular_matrix() {
let m = AffineMatrix::from_coeffs([1.0, 2.0, 0.0, 2.0, 4.0, 0.0]);
let result = m.inverse();
assert!(matches!(result, Err(TransformError::SingularMatrix)));
}
#[test]
fn test_compose_identity() {
let m = AffineMatrix::translation(5.0, 3.0);
let id = AffineMatrix::identity();
let composed = m.compose(&id);
let pt = Point::new(10.0, 20.0);
let t1 = m.transform_point(pt);
let t2 = composed.transform_point(pt);
assert!((t1.x - t2.x).abs() < 1e-5);
assert!((t1.y - t2.y).abs() < 1e-5);
}
#[test]
fn test_compose_translations() {
let m1 = AffineMatrix::translation(5.0, 3.0);
let m2 = AffineMatrix::translation(2.0, -1.0);
let composed = m1.compose(&m2);
let pt = Point::new(0.0, 0.0);
let transformed = composed.transform_point(pt);
assert!((transformed.x - 7.0).abs() < 1e-5);
assert!((transformed.y - 2.0).abs() < 1e-5);
}
#[test]
fn test_from_three_points_identity() {
let src = [
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.0, 1.0),
];
let dst = src;
let m = AffineMatrix::from_three_points(src, dst).unwrap();
let pt = Point::new(5.0, 7.0);
let transformed = m.transform_point(pt);
assert!((transformed.x - pt.x).abs() < 1e-4);
assert!((transformed.y - pt.y).abs() < 1e-4);
}
#[test]
fn test_from_three_points_translation() {
let src = [
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(0.0, 10.0),
];
let dst = [
Point::new(5.0, 3.0),
Point::new(15.0, 3.0),
Point::new(5.0, 13.0),
];
let m = AffineMatrix::from_three_points(src, dst).unwrap();
let pt = Point::new(0.0, 0.0);
let transformed = m.transform_point(pt);
assert!((transformed.x - 5.0).abs() < 1e-4);
assert!((transformed.y - 3.0).abs() < 1e-4);
}
#[test]
fn test_from_three_points_scale() {
let src = [
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(0.0, 10.0),
];
let dst = [
Point::new(0.0, 0.0),
Point::new(20.0, 0.0),
Point::new(0.0, 30.0),
];
let m = AffineMatrix::from_three_points(src, dst).unwrap();
let pt = Point::new(5.0, 5.0);
let transformed = m.transform_point(pt);
assert!((transformed.x - 10.0).abs() < 1e-4);
assert!((transformed.y - 15.0).abs() < 1e-4);
}
#[test]
fn test_collinear_points() {
let src = [
Point::new(0.0, 0.0),
Point::new(1.0, 1.0),
Point::new(2.0, 2.0), ];
let dst = [
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.0, 1.0),
];
let result = AffineMatrix::from_three_points(src, dst);
assert!(matches!(result, Err(TransformError::SingularMatrix)));
}
#[test]
fn test_affine_fill_values() {
assert_eq!(AffineFill::White.to_value(PixelDepth::Bit1), 0);
assert_eq!(AffineFill::Black.to_value(PixelDepth::Bit1), 1);
assert_eq!(AffineFill::White.to_value(PixelDepth::Bit8), 255);
assert_eq!(AffineFill::Black.to_value(PixelDepth::Bit8), 0);
assert_eq!(AffineFill::Color(128).to_value(PixelDepth::Bit8), 128);
}
#[test]
fn test_affine_sampled_identity() {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
pix_mut.set_pixel_unchecked(x, y, x + y * 10);
}
}
let pix: Pix = pix_mut.into();
let m = AffineMatrix::identity();
let result = affine_sampled(&pix, &m, AffineFill::White).unwrap();
for y in 0..10 {
for x in 0..10 {
let orig = pix.get_pixel_unchecked(x, y);
let trans = result.get_pixel_unchecked(x, y);
assert_eq!(orig, trans);
}
}
}
#[test]
fn test_affine_sampled_translation() {
let pix = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
pix_mut.set_pixel_unchecked(5, 5, 100);
let pix: Pix = pix_mut.into();
let m = AffineMatrix::translation(3.0, 2.0);
let result = affine_sampled(&pix, &m, AffineFill::White).unwrap();
assert_eq!(result.get_pixel_unchecked(8, 7), 100);
}
#[test]
fn test_affine_interpolated_8bpp() {
let pix = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
let m = AffineMatrix::identity();
let result = affine(&pix, &m, AffineFill::White);
assert!(result.is_ok());
}
#[test]
fn test_affine_interpolated_32bpp() {
let pix = Pix::new(20, 20, PixelDepth::Bit32).unwrap();
let m = AffineMatrix::identity();
let result = affine(&pix, &m, AffineFill::White);
assert!(result.is_ok());
}
#[test]
fn test_affine_1bpp_falls_back_to_sampling() {
let pix = Pix::new(20, 20, PixelDepth::Bit1).unwrap();
let m = AffineMatrix::identity();
let result = affine(&pix, &m, AffineFill::White);
assert!(result.is_ok());
}
#[test]
fn test_translate_function() {
let pix = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
let result = translate(&pix, 5.0, 3.0);
assert!(result.is_ok());
}
#[test]
fn test_affine_scale_function() {
let pix = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
let result = affine_scale(&pix, 1.5, 1.5);
assert!(result.is_ok());
}
#[test]
fn test_affine_scale_invalid() {
let pix = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
let result = affine_scale(&pix, 0.0, 1.0);
assert!(matches!(result, Err(TransformError::InvalidParameters(_))));
}
#[test]
fn test_affine_rotate_function() {
let pix = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
let result = affine_rotate(&pix, 10.0, 10.0, 0.5);
assert!(result.is_ok());
}
#[test]
fn test_affine_sampled_pta() {
let pix = Pix::new(50, 50, PixelDepth::Bit8).unwrap();
let src = [
Point::new(0.0, 0.0),
Point::new(49.0, 0.0),
Point::new(0.0, 49.0),
];
let dst = [
Point::new(5.0, 5.0),
Point::new(44.0, 5.0),
Point::new(5.0, 44.0),
];
let result = affine_sampled_pta(&pix, src, dst, AffineFill::White);
assert!(result.is_ok());
}
#[test]
fn test_affine_pta() {
let pix = Pix::new(50, 50, PixelDepth::Bit8).unwrap();
let src = [
Point::new(0.0, 0.0),
Point::new(49.0, 0.0),
Point::new(0.0, 49.0),
];
let dst = [
Point::new(5.0, 5.0),
Point::new(44.0, 5.0),
Point::new(5.0, 44.0),
];
let result = affine_pta(&pix, src, dst, AffineFill::White);
assert!(result.is_ok());
}
#[test]
fn test_affine_pta_with_alpha_basic() {
let pix = Pix::new(50, 50, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
pm.set_spp(4);
for y in 0..50u32 {
for x in 0..50u32 {
let pixel = pixel::compose_rgba((x * 5) as u8, (y * 5) as u8, 128, 255);
pm.set_pixel_unchecked(x, y, pixel);
}
}
let pix: Pix = pm.into();
let src = [
Point::new(0.0, 0.0),
Point::new(49.0, 0.0),
Point::new(0.0, 49.0),
];
let dst = [
Point::new(2.0, 2.0),
Point::new(47.0, 2.0),
Point::new(2.0, 47.0),
];
let result = affine_pta_with_alpha(&pix, src, dst, None, 1.0, 10).unwrap();
assert_eq!(result.depth(), PixelDepth::Bit32);
assert_eq!(result.spp(), 4);
assert_eq!(result.width(), 70); assert_eq!(result.height(), 70);
assert_eq!(pixel::alpha(result.get_pixel_unchecked(0, 0)), 0);
let center_pixel = result.get_pixel_unchecked(35, 35);
assert!(pixel::alpha(center_pixel) > 0);
}
#[test]
fn test_affine_pta_with_alpha_opacity() {
let pix = Pix::new(30, 30, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..30u32 {
for x in 0..30u32 {
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(100, 150, 200));
}
}
let pix: Pix = pm.into();
let src = [
Point::new(0.0, 0.0),
Point::new(29.0, 0.0),
Point::new(0.0, 29.0),
];
let dst = src;
let result = affine_pta_with_alpha(&pix, src, dst, None, 0.5, 5).unwrap();
assert_eq!(result.spp(), 4);
let interior = result.get_pixel_unchecked(20, 20);
let alpha = pixel::alpha(interior);
assert!(
(alpha as i32 - 127).abs() <= 2,
"Expected alpha ~127, got {}",
alpha
);
}
#[test]
fn test_affine_pta_with_alpha_custom_mask() {
let pix = Pix::new(30, 30, PixelDepth::Bit32).unwrap();
let mut pm = pix.try_into_mut().unwrap();
for y in 0..30u32 {
for x in 0..30u32 {
pm.set_pixel_unchecked(x, y, pixel::compose_rgb(100, 150, 200));
}
}
let pix: Pix = pm.into();
let mask = Pix::new(30, 30, PixelDepth::Bit8).unwrap();
let mut mm = mask.try_into_mut().unwrap();
for y in 0..30u32 {
for x in 0..15u32 {
mm.set_pixel_unchecked(x, y, 255);
}
for x in 15..30u32 {
mm.set_pixel_unchecked(x, y, 0);
}
}
let mask: Pix = mm.into();
let src = [
Point::new(0.0, 0.0),
Point::new(29.0, 0.0),
Point::new(0.0, 29.0),
];
let dst = src;
let result = affine_pta_with_alpha(&pix, src, dst, Some(&mask), 1.0, 5).unwrap();
assert_eq!(result.spp(), 4);
let opaque_pixel = result.get_pixel(7, 15).unwrap();
let transparent_pixel = result.get_pixel(27, 15).unwrap();
let opaque_alpha = (opaque_pixel & 0xff) as u8;
let transparent_alpha = (transparent_pixel & 0xff) as u8;
assert!(
opaque_alpha > 200,
"expected high alpha in masked-opaque region, got {opaque_alpha}"
);
assert!(
transparent_alpha < 50,
"expected low alpha in masked-transparent region, got {transparent_alpha}"
);
}
#[test]
fn test_affine_pta_with_alpha_invalid_depth() {
let pix = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
let src = [
Point::new(0.0, 0.0),
Point::new(19.0, 0.0),
Point::new(0.0, 19.0),
];
let result = affine_pta_with_alpha(&pix, src, src, None, 1.0, 5);
assert!(result.is_err());
}
#[test]
fn test_affine_preserves_colormap() {
use crate::core::PixColormap;
let pix = Pix::new(20, 20, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
let mut cmap = PixColormap::new(8).unwrap();
cmap.add_rgb(255, 0, 0).unwrap(); cmap.add_rgb(0, 255, 0).unwrap(); let _ = pix_mut.set_colormap(Some(cmap));
let pix: Pix = pix_mut.into();
let m = AffineMatrix::identity();
let result = affine_sampled(&pix, &m, AffineFill::White).unwrap();
assert!(result.colormap().is_some());
}
#[test]
fn test_pta_affine_transform_identity() {
use crate::core::Pta;
let mut pta = Pta::new();
pta.push(1.0, 2.0);
pta.push(3.0, 4.0);
let matrix = AffineMatrix::identity();
let result = pta_affine_transform(&pta, &matrix);
assert_eq!(result.len(), 2);
assert!((result.get(0).unwrap().0 - 1.0).abs() < 1e-5);
assert!((result.get(0).unwrap().1 - 2.0).abs() < 1e-5);
}
#[test]
fn test_pta_affine_transform_translation() {
use crate::core::Pta;
let mut pta = Pta::new();
pta.push(1.0, 2.0);
let matrix = AffineMatrix::translation(10.0, 20.0);
let result = pta_affine_transform(&pta, &matrix);
assert!((result.get(0).unwrap().0 - 11.0).abs() < 1e-5);
assert!((result.get(0).unwrap().1 - 22.0).abs() < 1e-5);
}
#[test]
fn test_boxa_affine_transform_identity() {
use crate::core::{Box as LBox, Boxa};
let mut boxa = Boxa::new();
boxa.push(LBox::new(10, 20, 30, 40).unwrap());
let matrix = AffineMatrix::identity();
let result = boxa_affine_transform(&boxa, &matrix);
assert_eq!(result.len(), 1);
let b = result.get(0).unwrap();
assert_eq!(b.x, 10);
assert_eq!(b.y, 20);
assert_eq!(b.w, 30);
assert_eq!(b.h, 40);
}
#[test]
fn test_boxa_affine_transform_translation() {
use crate::core::{Box as LBox, Boxa};
let mut boxa = Boxa::new();
boxa.push(LBox::new(0, 0, 10, 10).unwrap());
let matrix = AffineMatrix::translation(5.0, 3.0);
let result = boxa_affine_transform(&boxa, &matrix);
let b = result.get(0).unwrap();
assert_eq!(b.x, 5);
assert_eq!(b.y, 3);
assert_eq!(b.w, 10);
assert_eq!(b.h, 10);
}
}