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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! Generate distance field bitmaps for rendering as pseudo-vector images in shaders.
//!
//! An example usecase for the library would be to automatically convert asset images. You can achieve this by having a `build.rs` similar to this:
//!
//! ```rust
//! use std::fs::File;
//! use distance_field::DistanceFieldExt;
//!
//! fn convert_image_to_dfield(input: &str, output: &str) {
//!     // Load the 'input' image
//!     let img = image::open(input).unwrap();
//!
//!     // Generate a distance field from the image
//!     let outbuf = img.grayscale().distance_field(distance_field::Options {
//!         size: (128, 128),
//!         max_distance: 256,
//!         ..Default::default()
//!     });
//!
//!     // Save it to 'output' as a PNG
//!     image::DynamicImage::ImageLuma8(outbuf).save(output).unwrap();
//! }
//!
//! fn main() {
//!     convert_image_to_dfield("img/input.png", "output.png");
//! }
//! ```

use bitvec::vec::BitVec;
use image::{DynamicImage, ImageBuffer, Luma};
use spiral::ManhattanIterator;

/// The options passed as the argument to the `distance_field` method.
pub struct Options {
    /// The dimensions of the output image (width, height). The default value is: (64,64).
    pub size: (usize, usize),

    /// The maximum distance for the projected point of the output image on the input image to
    /// search for the nearest point. The defaul value is: 512.
    pub max_distance: usize,

    /// The image value at which to apply the treshold. In a black-white vector image 127 is
    /// probably the best value. The default value is: 127.
    pub image_treshold: u8,
}

/// Returns:
/// ```Options {
///     size: (64, 64),
///     max_distance: 512,
///     image_treshold: 127
/// }```
impl Default for Options {
    fn default() -> Self {
        Options {
            size: (64, 64),
            max_distance: 512,
            image_treshold: 127,
        }
    }
}

/// A trait adding the `distance_field` function to image types.
pub trait DistanceFieldExt {
    /// Generates a grayscale output image with the dimensions as specified in the `Options`
    /// struct.
    fn distance_field(&self, options: Options) -> ImageBuffer<Luma<u8>, Vec<u8>>;
}

/// A implementation of the `distance_field` function for the `DynamicImage` type. To call this
/// from a normal RGB image use `image.grayscale().distance_field(options)`.
impl DistanceFieldExt for DynamicImage {
    fn distance_field(&self, options: Options) -> ImageBuffer<Luma<u8>, Vec<u8>> {
        let treshold = TresholdImage::from_dynamic_image(self, options.image_treshold);

        ImageBuffer::from_fn(options.size.0 as u32, options.size.1 as u32, |x, y| {
            Luma([get_nearest_pixel_distance(
                &treshold, x as usize, y as usize, &options,
            )])
        })
    }
}

/// Treshold image as a boolean vector.
struct TresholdImage {
    bits: BitVec,
    width: usize,
    height: usize,
}

impl TresholdImage {
    /// Convert a dynamic image to this type of image by applying a treshold.
    pub fn from_dynamic_image(image: &DynamicImage, treshold: u8) -> Self {
        // Get the image as grayscale
        let luma_img = image.to_luma8();

        // Convert the image to a boolean buffer, applying a treshold
        let bits = luma_img
            .pixels()
            .map(|pixel| pixel.0[0] > treshold)
            .collect::<BitVec>();

        Self {
            bits,
            width: luma_img.width() as usize,
            height: luma_img.height() as usize,
        }
    }

    /// Get a pixel as a boolean.
    pub fn get_pixel(&self, x: usize, y: usize) -> bool {
        let index = x + y * self.width;

        self.bits[index]
    }
}

fn get_nearest_pixel_distance(
    input: &TresholdImage,
    out_x: usize,
    out_y: usize,
    options: &Options,
) -> u8 {
    // Calcule the projected center of the output pixel on the source image
    let center = (
        ((out_x * input.width) / options.size.0) as isize,
        ((out_y * input.height) / options.size.1) as isize,
    );

    // Check if we are inside a filled area so we can get the 127-255 range
    let is_inside = input.get_pixel(center.0 as usize, center.1 as usize);

    let closest_distance =
        ManhattanIterator::new(center.0, center.1, options.max_distance as isize)
            // Ignore the boundary conditions
            .filter(|(x, y)| {
                *x >= 0 && *x < input.width as isize && *y >= 0 && *y < input.height as isize
            })
            // Continue if the center and this pixel are inside the filled area or
            // the pixels are both outside the filled area
            .find(|(x, y)| input.get_pixel(*x as usize, *y as usize) != is_inside)
            // We found the nearest pixel, calculate the distance
            .map(|(x, y)| {
                let dx = center.0.abs_diff(x);
                let dy = center.1.abs_diff(y);

                ((dx * dx + dy * dy) as f32).sqrt()
            })
            .unwrap_or(options.max_distance as f32);

    // Convert the outside to a 0.0-0.5 and the inside to a 0.5-1.0 range
    let distance_fraction = if is_inside {
        0.5 + (closest_distance / 2.0) / options.max_distance as f32
    } else {
        0.5 - (closest_distance / 2.0) / options.max_distance as f32
    };

    // Convert the 0.0-1.0 range to a u8
    (distance_fraction * u8::max_value() as f32) as u8
}