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())
    }
}