1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
use image::{DynamicImage, GenericImageView, Pixel, Rgb, Rgba, RgbaImage}; use std::str::FromStr; pub type Error = Box<dyn std::error::Error>; pub struct Color(pub Rgb<u8>); impl FromStr for Color { type Err = Error; fn from_str(s: &str) -> Result<Self, Self::Err> { if let Some(rem) = s.strip_prefix("#") { let mut rem = rem; let mut parse_component = || { rem.get(..2) .and_then(|s| { rem = &rem[2..]; u8::from_str_radix(s, 16).ok() }) .ok_or_else(|| "Invalid format for color, expected #rrggbb") }; let r = parse_component()?; let g = parse_component()?; let b = parse_component()?; if rem.is_empty() { Ok(Color(Rgb([r, g, b]))) } else { Err("Invalid format for color, expected #rrggbb".into()) } } else { Err("Invalid format for color, expected #rrggbb".into()) } } } fn color_lerp(zero: Rgb<u8>, one: Rgb<u8>, luma: f32, alpha: f32) -> Rgba<u8> { let luma = luma.max(0.0).min(1.0); let alpha = alpha.max(0.0).min(1.0); let zero_r = zero.channels()[0] as f32; let zero_g = zero.channels()[1] as f32; let zero_b = zero.channels()[2] as f32; let one_r = one.channels()[0] as f32; let one_g = one.channels()[1] as f32; let one_b = one.channels()[2] as f32; Rgba([ (zero_r * (1.0 - luma) + one_r * luma) as u8, (zero_g * (1.0 - luma) + one_g * luma) as u8, (zero_b * (1.0 - luma) + one_b * luma) as u8, (alpha * 255.0) as u8, ]) } pub fn convert( dark_image: &DynamicImage, light_image: &DynamicImage, dark_color: Rgb<u8>, light_color: Rgb<u8>, ) -> Result<RgbaImage, Error> { if dark_image.dimensions() != light_image.dimensions() { return Err("dark and light image dimensions must be the same".into()); } let mut output = RgbaImage::new(dark_image.width(), dark_image.height()); for y in 0..output.height() { for x in 0..output.width() { let dark_value = dark_image.get_pixel(x, y).to_luma().channels()[0] as f32 / 255.0; let light_value = light_image.get_pixel(x, y).to_luma().channels()[0] as f32 / 255.0; let output_alpha = (dark_value - light_value + 1.0) / 2.0; let output_luma = if output_alpha == 0.0 { 0.0 } else { dark_value / 2.0 / output_alpha }; *output.get_pixel_mut(x, y) = color_lerp(dark_color, light_color, output_luma, output_alpha); } } Ok(output) } #[cfg(target_arch = "wasm32")] mod wasm { use crate::{Color, Error}; use image::{DynamicImage, ImageFormat}; use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn convert( dark_image: &[u8], light_image: &[u8], dark_color: &str, light_color: &str, ) -> Result<Box<[u8]>, JsValue> { || -> Result<Box<[u8]>, Error> { let dark_image = image::load_from_memory(dark_image)?; let light_image = image::load_from_memory(light_image)?; let dark_color = dark_color.parse::<Color>()?.0; let light_color = light_color.parse::<Color>()?.0; let output = crate::convert(&dark_image, &light_image, dark_color, light_color)?; let mut output_png = Vec::new(); DynamicImage::ImageRgba8(output).write_to(&mut output_png, ImageFormat::Png)?; Ok(output_png.into_boxed_slice()) }() .map_err(|e| e.to_string().into()) } }