use jpeg_decoder::Error as JpegError;
use png::ColorType;
use std::fs;
use std::fs::File;
use std::io::{BufWriter, Read};
use std::path::PathBuf;
use thiserror::Error;
use visioncortex::PathSimplifyMode;
use vtracer::Config;
const LOW_PASS_THRESHOLD: u8 = 75;
const HIGH_PASS_THRESHOLD: u8 = 120;
const HIGH_PASS_RESTORATION_THRESHOLD_FG: u8 = 90;
const HIGH_PASS_RESTORATION_THRESHOLD_BG: u8 = 45;
const REPLACEMENT_PIXEL: Pixel = Pixel::ALPHA;
#[derive(Error, Debug)]
pub enum ConversionError {
#[error("error decoding jpeg: {0}")]
Decode(JpegError),
#[error("the input image does not have any metadata. resolution cannot be read.")]
NoMetadata,
#[error("invalid pixel count. is the image in the RGB pixel format?")]
InvalidPixelCount,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Pixel {
r: u8,
g: u8,
b: u8,
a: u8,
}
pub fn jpeg_to_svg<T>(reader: T) -> Result<Vec<u8>, ConversionError>
where
T: Read,
{
let mut decoder = jpeg_decoder::Decoder::new(reader);
let raw_pixels = decoder.decode().map_err(|e| ConversionError::Decode(e))?;
#[cfg(feature = "debug")]
println!("pixels length: {}", raw_pixels.len());
#[cfg(feature = "debug")]
println!("metadata: {:#?}", decoder.info());
let Some(metadata) = decoder.info() else {
return Err(ConversionError::NoMetadata);
};
let pixels = parse_pixels(raw_pixels)?;
let average_bg_pixel = compute_average_pixel(&pixels);
#[cfg(feature = "debug")]
println!("average bg pixel: {:#?}", average_bg_pixel);
let mut low_pass_pixels = pixels.clone();
filter_pixels_by_threshold(
&mut low_pass_pixels,
&average_bg_pixel,
&REPLACEMENT_PIXEL,
LOW_PASS_THRESHOLD,
);
let mut high_pass_pixels = pixels.clone();
let pixels_result = second_pass(
&low_pass_pixels,
&pixels,
&mut high_pass_pixels,
&average_bg_pixel,
metadata.width,
metadata.height,
);
#[cfg(feature = "debug")]
save_debug_png(
"debug-low-pass.png",
&low_pass_pixels,
metadata.width as u32,
metadata.height as u32,
);
#[cfg(feature = "debug")]
save_debug_png(
"debug-high-pass.png",
&high_pass_pixels,
metadata.width as u32,
metadata.height as u32,
);
save_debug_png(
"debug-final.png",
&pixels_result,
metadata.width as u32,
metadata.height as u32,
);
let output_path = PathBuf::from("output.svg");
let debug_final_path = PathBuf::from("debug-final.png");
vtracer::convert_image_to_svg(Config {
input_path: debug_final_path.clone(),
output_path: output_path.clone(),
mode: PathSimplifyMode::Spline,
..Default::default()
})
.unwrap();
fs::remove_file(debug_final_path).unwrap();
let bytes = fs::read(output_path.as_path()).unwrap();
fs::remove_file(output_path).unwrap();
Ok(bytes)
}
fn second_pass(
low_pass_pixels: &Vec<Pixel>,
pixels: &Vec<Pixel>,
high_pass_pixels: &mut Vec<Pixel>,
average_bg_pixel: &Pixel,
width: u16,
height: u16,
) -> Vec<Pixel> {
let average_fg_pixel = compute_average_pixel_ignoring_exact_and_threshold(
low_pass_pixels,
&REPLACEMENT_PIXEL,
average_bg_pixel,
HIGH_PASS_THRESHOLD,
);
#[cfg(feature = "debug")]
println!("average fg pixel: {:#?}", average_fg_pixel);
filter_pixels_by_threshold(
high_pass_pixels,
&average_fg_pixel,
&REPLACEMENT_PIXEL,
HIGH_PASS_THRESHOLD,
);
let mut result = low_pass_pixels
.iter()
.enumerate()
.map(|(i, p)| {
let is_high_pass = high_pass_pixels[i] != REPLACEMENT_PIXEL;
if !is_high_pass
&& !pixels[i]
.exceeds_colour_threshold(&average_fg_pixel, HIGH_PASS_RESTORATION_THRESHOLD_FG)
&& pixels[i]
.exceeds_colour_threshold(&average_bg_pixel, HIGH_PASS_RESTORATION_THRESHOLD_BG)
{
average_fg_pixel.clone()
} else if is_high_pass {
REPLACEMENT_PIXEL
} else {
p.clone()
}
})
.collect::<Vec<_>>();
repeat_filter_by_neighbour_count(&mut result, 1, width, height, &REPLACEMENT_PIXEL, 50);
result
}
fn repeat_filter_by_neighbour_count(
pixels: &mut Vec<Pixel>,
threshold: u8,
width: u16,
height: u16,
replacement: &Pixel,
max_count: usize,
) {
let mut last_filtered_count;
for _ in 0..max_count {
last_filtered_count =
filter_by_neighbour_count(pixels, threshold, width, height, replacement);
if last_filtered_count == 0 {
return;
}
}
}
fn filter_by_neighbour_count(
pixels: &mut Vec<Pixel>,
threshold: u8,
width: u16,
height: u16,
replacement: &Pixel,
) -> u32 {
let mut filtered_count: u32 = 0;
let pixels_read = pixels.clone();
for (i, p) in pixels.iter_mut().enumerate() {
if p == replacement {
continue;
}
let neighbour_indices = get_pixel_neighbour_indices(i, width, height);
let actual_neighbour_count = neighbour_indices.iter().fold(0, |count, i| {
if pixels_read[*i] == *replacement {
count
} else {
count + 1
}
});
if actual_neighbour_count <= (threshold as usize) {
*p = replacement.clone();
filtered_count += 1;
}
}
#[cfg(feature = "debug")]
println!("filtered by neighbour count: {}", filtered_count);
filtered_count
}
fn get_pixel_neighbour_indices(pixel_i: usize, width: u16, height: u16) -> Vec<usize> {
let x = (pixel_i % width as usize) as i16;
let y = (pixel_i / width as usize) as i16;
let mut neighbours = Vec::new();
for dx in -1..=1 {
for dy in -1..=1 {
if dx == 0 && dy == 0 {
continue;
}
let nx = x + dx;
let ny = y + dy;
if nx >= 0 && nx < width as i16 && ny >= 0 && ny < height as i16 {
let neighbour_i = (ny as usize * width as usize) + nx as usize;
neighbours.push(neighbour_i);
}
}
}
neighbours
}
fn filter_pixels_by_threshold(
pixels: &mut Vec<Pixel>,
reference: &Pixel,
replacement: &Pixel,
threshold: u8,
) -> u32 {
let filtered_count = pixels.iter_mut().fold(0, |c, p| {
if p.exceeds_colour_threshold(&reference, threshold) {
c
} else {
*p = replacement.clone();
c + 1
}
});
#[cfg(feature = "debug")]
let filtered_percent = ((filtered_count as f32) * 100.0 / (pixels.len() as f32)) as u32;
#[cfg(feature = "debug")]
println!(
"filtered pixel count: {} ({}%)",
filtered_count, filtered_percent
);
filtered_count
}
fn save_debug_png(path: &str, pixels: &Vec<Pixel>, width: u32, height: u32) {
let file = File::create(path).unwrap();
let w = &mut BufWriter::new(file);
let mut encoder = png::Encoder::new(w, width, height);
encoder.set_color(ColorType::Rgba);
let mut writer = encoder.write_header().unwrap();
let data = pixels
.iter()
.flat_map(|p| [p.r, p.g, p.b, p.a])
.collect::<Vec<_>>();
writer.write_image_data(&data).unwrap();
}
fn parse_pixels(raw_pixels: Vec<u8>) -> Result<Vec<Pixel>, ConversionError> {
let mut pixels = Vec::new();
let count = raw_pixels.len();
if count % 3 != 0 {
return Err(ConversionError::InvalidPixelCount);
}
for pixel_n in 0..(count / 3) {
let i = pixel_n * 3;
pixels.push(Pixel {
r: raw_pixels[i],
g: raw_pixels[i + 1],
b: raw_pixels[i + 2],
a: 255,
})
}
Ok(pixels)
}
fn compute_average_pixel(pixels: &Vec<Pixel>) -> Pixel {
let count = pixels.len() as u128;
let avg_r = (pixels.iter().fold(0, |a, b| a + (b.r as u128)) / count) as u8;
let avg_g = (pixels.iter().fold(0, |a, b| a + (b.g as u128)) / count) as u8;
let avg_b = (pixels.iter().fold(0, |a, b| a + (b.b as u128)) / count) as u8;
Pixel {
r: avg_r,
g: avg_g,
b: avg_b,
a: 255,
}
}
fn compute_average_pixel_ignoring_exact_and_threshold(
pixels: &Vec<Pixel>,
ignore_exact: &Pixel,
ignore_threshold: &Pixel,
threshold: u8,
) -> Pixel {
let filtered = pixels
.iter()
.filter(|p| *p != ignore_exact)
.filter(|p| p.exceeds_colour_threshold(ignore_threshold, threshold))
.cloned()
.collect::<Vec<_>>();
compute_average_pixel(&filtered)
}
impl Pixel {
pub const BLACK: Pixel = Pixel {
r: 0,
g: 0,
b: 0,
a: 255,
};
pub const WHITE: Pixel = Pixel {
r: 255,
g: 255,
b: 255,
a: 255,
};
pub const ALPHA: Pixel = Pixel {
r: 0,
g: 0,
b: 0,
a: 0,
};
pub fn exceeds_colour_threshold(&self, reference: &Pixel, threshold: u8) -> bool {
let diff_magnitude = self.diff(reference).magnitude();
diff_magnitude >= threshold
}
pub fn magnitude(&self) -> u8 {
let to_sq = |v: u8| (v as f32).powi(2);
let r_sq = to_sq(self.r);
let g_sq = to_sq(self.g);
let b_sq = to_sq(self.b);
f32::sqrt(r_sq + g_sq + b_sq) as u8
}
pub fn diff(&self, other: &Pixel) -> Pixel {
let calc_diff = |a: u8, b: u8| if a >= b { a - b } else { b - a };
Pixel {
r: calc_diff(self.r, other.r),
g: calc_diff(self.g, other.g),
b: calc_diff(self.b, other.b),
a: 255,
}
}
}