use crate::hat::geometry::{mul, trans_pt, ttrans, IDENT};
use crate::hat::tiles::{
collect_hats, construct_metatiles, construct_patch, construct_prototiles, DrawContext, HatTile,
MetaTile, Tile,
};
use image::{ImageBuffer, ImageError, Rgb, RgbImage};
use nalgebra::Vector2;
use std::path::Path;
use std::rc::Rc;
use svg::node::element::Polygon;
use svg::node::element::SVG;
use svg::Document;
#[derive(Debug, Clone)]
pub struct AMosaic {
pub width: u32,
pub height: u32,
pub input_data: Vec<u8>,
}
impl AMosaic {
pub fn new(width: u32, height: u32, input_data: Vec<u8>) -> Self {
AMosaic {
width,
height,
input_data,
}
}
fn compute_required_level(&self, scale: f64) -> i32 {
let initial_diameter = 5.0; let growth_factor = 2.0;
let canvas_diagonal =
(self.width as f64 * self.width as f64 + self.height as f64 * self.height as f64).sqrt();
let required_diameter = canvas_diagonal / scale;
let level = (required_diameter / initial_diameter)
.log(growth_factor)
.ceil()
.max(0.0) as i32;
level
}
pub fn to_svg(&self, scale: f64) -> String {
let level = self.compute_required_level(scale);
let root = self
.create_tile_patch(level)
.expect("failed to build patch");
let to_screen = [scale, 0.0, 0.0, 0.0, -scale, 0.0];
let t_center = ttrans(self.width as f64 / 2.0, self.height as f64 / 2.0);
let transform = mul(&t_center, &to_screen);
let mut svg_canvas = Document::new()
.set("width", self.width)
.set("height", self.height)
.set("viewBox", (0, 0, self.width, self.height))
.add(SVG::new().set("xmlns", "http://www.w3.org/2000/svg"));
let input = self.decode_input_image();
for (hat, t_abs) in collect_hats(&root, transform) {
let poly_f: Vec<_> = hat.shape.iter().map(|p| trans_pt(&t_abs, *p)).collect();
let poly_in: Vec<(i32, i32)> = poly_f
.iter()
.map(|p| {
let ix = (p.x + 0.5).floor() as i32;
let iy = (p.y + 0.5).floor() as i32;
(
ix.clamp(0, self.width as i32 - 1),
iy.clamp(0, self.height as i32 - 1),
)
})
.collect();
let (r, g, b) = average_color_scanline(&input, &poly_in);
let points = poly_f
.iter()
.map(|p| format!("{:.2},{:.2}", p.x, p.y))
.collect::<Vec<_>>()
.join(" ");
let polygon = Polygon::new()
.set("points", points)
.set("fill", format!("rgb({},{},{})", r, g, b));
svg_canvas = svg_canvas.add(polygon);
}
svg_canvas.to_string()
}
pub fn draw(&self, scale: f64, output: &mut RgbImage) -> Result<(), ImageError> {
let level = self.compute_required_level(scale);
let root = self.create_tile_patch(level)?;
let grow_px = scale.ceil() as u32;
let pad_w = self.width + 2 * grow_px;
let pad_h = self.height + 2 * grow_px;
let to_screen = [scale, 0.0, 0.0, 0.0, -scale, 0.0];
let t_center_pad = ttrans(pad_w as f64 / 2.0, pad_h as f64 / 2.0);
let transform = mul(&t_center_pad, &to_screen);
let mut padded = RgbImage::from_pixel(pad_w, pad_h, Rgb([255, 255, 255]));
let input_img = self.decode_input_image();
for (hat, t_abs) in collect_hats(&root, transform) {
let poly_f: Vec<Vector2<f64>> = hat.shape.iter().map(|p| trans_pt(&t_abs, *p)).collect();
let (min_x_f, max_x_f) = poly_f
.iter()
.map(|p| p.x)
.fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), x| {
(mn.min(x), mx.max(x))
});
let (min_y_f, max_y_f) = poly_f
.iter()
.map(|p| p.y)
.fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), y| {
(mn.min(y), mx.max(y))
});
if max_x_f < -0.5
|| min_x_f > (self.width as f64 - 0.5)
|| max_y_f < -0.5
|| min_y_f > (self.height as f64 - 0.5)
{
continue;
}
let poly_in: Vec<(i32, i32)> = poly_f
.iter()
.map(|p| {
let ix = (p.x + 0.5).floor() as i32;
let iy = (p.y + 0.5).floor() as i32;
(
ix.clamp(0, self.width as i32 - 1),
iy.clamp(0, self.height as i32 - 1),
)
})
.collect();
if poly_in.len() < 3 {
continue;
}
let color = average_color_scanline(&input_img, &poly_in);
let poly_pad: Vec<(i32, i32)> = poly_f
.iter()
.map(|p| {
let ix = (p.x + 0.5).floor() as i32 + grow_px as i32;
let iy = (p.y + 0.5).floor() as i32 + grow_px as i32;
(ix, iy)
})
.collect();
fill_polygon_gapless(&mut padded, &poly_pad, color);
}
for y in 0..self.height {
for x in 0..self.width {
let p = padded.get_pixel(x + grow_px, y + grow_px);
output.put_pixel(x, y, *p);
}
}
Ok(())
}
fn decode_input_image(&self) -> RgbImage {
ImageBuffer::from_raw(self.width, self.height, self.input_data.clone())
.unwrap_or_else(|| RgbImage::from_pixel(self.width, self.height, Rgb([255, 255, 255])))
}
pub fn create_tile_patch(&self, level: i32) -> Result<Rc<dyn Tile>, ImageError> {
let (mt0, mt1, mt2, mt3) = construct_prototiles();
let mut patch = construct_patch(&mt0, &mt1, &mt2, &mt3);
for _ in 0..level {
let (nmt0, nmt1, nmt2, nmt3) = construct_metatiles(&patch);
patch = construct_patch(&nmt0, &nmt1, &nmt2, &nmt3);
}
Ok(Rc::new(patch) as Rc<dyn Tile>)
}
}
pub struct ImageDrawContext<'a> {
pub img: &'a mut RgbImage,
}
impl<'a> DrawContext for ImageDrawContext<'a> {
fn polygon(
&mut self,
points: &[(i32, i32)],
fill: Option<(u8, u8, u8)>,
outline: Option<(u8, u8, u8)>,
) {
if let Some(col) = fill {
fill_polygon_gapless(self.img, points, col);
}
}
}
fn draw_edge(
img: &mut RgbImage,
(mut x0, mut y0): (i32, i32),
(x1, y1): (i32, i32),
color: (u8, u8, u8),
) {
let dx = (x1 - x0).abs();
let dy = (y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx - dy;
loop {
if let (Some(xx), Some(yy)) = (
(0..img.width() as i32).contains(&x0).then(|| x0 as u32),
(0..img.height() as i32).contains(&y0).then(|| y0 as u32),
) {
img.put_pixel(xx, yy, Rgb([color.0, color.1, color.2]));
}
if x0 == x1 && y0 == y1 {
break;
}
let e2 = err * 2;
if e2 > -dy {
err -= dy;
x0 += sx;
}
if e2 < dx {
err += dx;
y0 += sy;
}
}
}
fn fill_polygon_gapless(img: &mut RgbImage, poly: &[(i32, i32)], color: (u8, u8, u8)) {
struct Edge {
y_min: i32,
y_max: i32,
x: f64,
inv_slope: f64,
}
let n = poly.len();
if n < 3 {
return;
}
let mut edges = Vec::with_capacity(n);
for i in 0..n {
let (x0, y0) = poly[i];
let (x1, y1) = poly[(i + 1) % n];
if y0 == y1 {
continue;
}
let (y_min, y_max, x_at_ymin, dy, dx) = if y0 < y1 {
(y0, y1, x0 as f64, (y1 - y0) as f64, (x1 - x0) as f64)
} else {
(y1, y0, x1 as f64, (y0 - y1) as f64, (x0 - x1) as f64)
};
edges.push(Edge {
y_min,
y_max,
x: x_at_ymin,
inv_slope: dx / dy,
});
}
let h = img.height() as i32;
let w = img.width() as i32;
let y_start = poly.iter().map(|&(_, y)| y).min().unwrap().clamp(0, h - 1);
let y_end = poly.iter().map(|&(_, y)| y).max().unwrap().clamp(0, h - 1);
for y in y_start..=y_end {
let mut xs: Vec<f64> = edges
.iter()
.filter_map(|e| {
if (e.y_min..e.y_max).contains(&y) {
Some(e.x + (y - e.y_min) as f64 * e.inv_slope)
} else {
None
}
})
.collect();
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
match xs.len() {
0 => continue, 1 => xs.push(xs[0]), _ => {} }
for chunk in xs.chunks(2) {
if let [x0, x1] = chunk {
let x_start = (x0.ceil() as i32).max(0);
let x_end = (x1.floor() as i32).min(w - 1);
if x_end >= x_start {
let yy = y as u32;
for xx in x_start as u32..=x_end as u32 {
img.put_pixel(xx, yy, Rgb([color.0, color.1, color.2]));
}
}
}
}
}
for i in 0..n {
let p0 = poly[i];
let p1 = poly[(i + 1) % n];
draw_edge(img, p0, p1, color);
}
}
pub fn average_color_scanline(img: &RgbImage, poly: &[(i32, i32)]) -> (u8, u8, u8) {
let n = poly.len();
if n < 3 {
return (0, 0, 0);
}
let min_y = poly
.iter()
.map(|&(_, y)| y)
.min()
.unwrap()
.max(0)
.min((img.height() as i32) - 1);
let max_y = poly
.iter()
.map(|&(_, y)| y)
.max()
.unwrap()
.max(0)
.min((img.height() as i32) - 1);
let mut r_sum = 0u64;
let mut g_sum = 0u64;
let mut b_sum = 0u64;
let mut count = 0u64;
for y in min_y..=max_y {
let mut xs = Vec::with_capacity(n);
for i in 0..n {
let (x0, y0) = poly[i];
let (x1, y1) = poly[(i + 1) % n];
if (y0 <= y && y1 > y) || (y1 <= y && y0 > y) {
let t = ((y as f64) + 0.5 - (y0 as f64)) / ((y1 as f64) - (y0 as f64));
let x = (x0 as f64) + t * ((x1 as f64) - (x0 as f64));
xs.push(x);
}
}
if xs.len() < 2 {
continue;
}
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
for pair in xs.chunks_exact(2) {
let x_start = pair[0].ceil() as i32;
let x_end = pair[1].floor() as i32;
for x in x_start..=x_end {
if x >= 0 && (x as u32) < img.width() {
let pix = img.get_pixel(x as u32, y as u32);
r_sum += pix[0] as u64;
g_sum += pix[1] as u64;
b_sum += pix[2] as u64;
count += 1;
}
}
}
}
if count == 0 {
(0, 0, 0)
} else {
(
(r_sum / count) as u8,
(g_sum / count) as u8,
(b_sum / count) as u8,
)
}
}
pub fn average_color(img: &RgbImage, poly: &[(i32, i32)]) -> (u8, u8, u8) {
if poly.is_empty() {
return (0, 0, 0);
}
let width = img.width();
let height = img.height();
let mut r_sum: u64 = 0;
let mut g_sum: u64 = 0;
let mut b_sum: u64 = 0;
let mut count: u64 = 0;
let min_x = poly.iter().map(|p| p.0).min().unwrap_or(0).max(0) as u32;
let max_x = poly
.iter()
.map(|p| p.0)
.max()
.unwrap_or(0)
.min(width as i32 - 1) as u32;
let min_y = poly.iter().map(|p| p.1).min().unwrap_or(0).max(0) as u32;
let max_y = poly
.iter()
.map(|p| p.1)
.max()
.unwrap_or(0)
.min(height as i32 - 1) as u32;
for x in min_x..max_x {
for y in min_y..max_y {
if point_in_polygon(x as i32, y as i32, poly) {
let pixel = img.get_pixel(x, y);
r_sum += pixel[0] as u64;
g_sum += pixel[1] as u64;
b_sum += pixel[2] as u64;
count += 1;
}
}
}
if count == 0 {
eprintln!("average_color: No pixels inside polygon, using vertex colors");
for &(x, y) in poly {
if x >= 0 && x < width as i32 && y >= 0 && y < height as i32 {
let pixel = img.get_pixel(x as u32, y as u32);
r_sum += pixel[0] as u64;
g_sum += pixel[1] as u64;
b_sum += pixel[2] as u64;
count += 1;
}
}
}
if count == 0 {
eprintln!("No valid pixels found for polygon, returning gray");
return (128, 128, 128); }
(
(r_sum / count) as u8,
(g_sum / count) as u8,
(b_sum / count) as u8,
)
}
fn point_in_polygon(x: i32, y: i32, poly: &[(i32, i32)]) -> bool {
let mut inside = false;
let n = poly.len();
for i in 0..n {
let (x1, y1) = poly[i];
let (x2, y2) = poly[(i + 1) % n];
let intersects = ((y1 > y) != (y2 > y))
&& ((x as f64) < (x2 - x1) as f64 * (y - y1) as f64 / (y2 - y1) as f64 + x1 as f64);
if intersects {
inside = !inside;
}
}
inside
}
#[cfg(test)]
mod tests {
use super::*;
use image::Rgb;
#[test]
fn test_amosaic_new() {
let input_data = vec![137, 80, 78, 71]; let mosaic = AMosaic::new(800, 600, input_data.clone());
assert_eq!(mosaic.width, 800);
assert_eq!(mosaic.height, 600);
assert_eq!(mosaic.input_data, input_data);
}
#[test]
fn test_average_color_empty_polygon() {
let img = RgbImage::from_pixel(100, 100, Rgb([255, 255, 255]));
let poly: Vec<(i32, i32)> = vec![];
let color = average_color(&img, &poly);
assert_eq!(color, (0, 0, 0), "Empty polygon should return black");
}
#[test]
fn test_average_color_single_pixel() {
let img = RgbImage::from_pixel(1, 1, Rgb([100, 150, 200]));
let poly = vec![(0, 0)];
let color = average_color(&img, &poly);
assert_eq!(
color,
(100, 150, 200),
"Single pixel polygon should return its color"
);
}
}