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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
#![crate_name = "kodak"]
#![deny(
    missing_docs,
    missing_copy_implementations,
    missing_debug_implementations,
    unsafe_code,
    unstable_features,
    trivial_casts,
    trivial_numeric_casts
)]

//! Kodak is a crate for image creation and manipulation.
//! It aims to be easy to use, fast and well-documented.

use std::ops::Add;

/// This struct is used to indicate locations on an image.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Loc {
    x: u32,
    y: u32,
}

impl Loc {
    /// Returns the location from a one-dimensional index given dimensions.
    pub fn from_index(idx: usize, dimension: Dim) -> Self {
        // TODO: rewrite this so that images larger than 2^16 by 2^16 work too
        let i: u32 = idx
            .try_into()
            .expect("An index too large to fit into a u32 was encountered.");
        Loc {
            x: i % dimension.w,
            y: i / dimension.w,
        }
    }

    /// Returns the one-dimensional index from a location given dimensions.
    pub fn as_index(&self, dimension: Dim) -> usize {
        (self.x + self.y * dimension.w).try_into().unwrap()
    }

    /// Checks if a location falls inside of a region.
    ///
    /// Note that this function is left-inclusive, but right-exclusive.
    pub fn inside_region(&self, region: Region) -> bool {
        let x = self.x;
        let y = self.y;

        let cx = region.l.x;
        let cy = region.l.y;
        let w = region.d.w;
        let h = region.d.h;

        x >= cx && x < cx + w && y >= cy && y < cy + h
    }
}

impl Add<Dim> for Loc {
    type Output = Self;
    fn add(self, rhs: Dim) -> Self::Output {
        Loc {
            x: self.x + rhs.w,
            y: self.y + rhs.h,
        }
    }
}

/// This struct is used to indicate dimensions of an image.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Dim {
    w: u32,
    h: u32,
}

/// This struct is used to indicate a region, specified by a top-left Loc and a Dim.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Region {
    l: Loc,
    d: Dim,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
/// A struct to represent colours
///
/// Note that it is assumed that all colours are three-channel, 8 bit per pixel and in sRGB colour space.
/// It is outside of the scope of this crate to support colour representations that differ from this.
pub struct Colour {
    /// The red channel of the colour.
    r: u8,
    /// The green channel of the colour.
    g: u8,
    /// The blue channel of the colour.
    b: u8,
}

impl Colour {
    /// The colour black.
    pub const BLACK: Colour = Colour { r: 0, g: 0, b: 0 };
    /// The colour white.
    pub const WHITE: Colour = Colour {
        r: 255,
        g: 255,
        b: 255,
    };
}

/// The Image struct is at the heart of Kodak. You'll be using functions on this 95% of the time.
///
/// Note that the maximum image size is 2^32 - 1 by 2^32 - 1 pixels. This limit was chosen because it is also the maximum of the PNG format.
#[derive(Debug, Clone)]
pub struct Image {
    /// The width of the image in pixels.
    width: u32,
    /// The height of the image in pixels.
    height: u32,
    /// A vector containing all pixels one-dimensionally.
    pixels: Vec<Colour>,
}

// The following impl block defines constructing functions for Images.
impl Image {
    /// Creates a new blank image.
    ///
    /// The default colour used is black.
    ///
    /// # Arguments
    ///
    /// * `dimension` - the dimensions of the image to be created
    ///
    /// # Returns
    ///
    /// The newly created image.
    pub fn blank(dimension: Dim) -> Self {
        let width = dimension.w;
        let height = dimension.h;

        Image {
            width,
            height,
            pixels: vec![Colour::BLACK; (width * height).try_into().unwrap()],
        }
    }
}

// The following impl block defines functions that give information about Images.
impl Image {
    /// Returns the dimensions of the image.
    pub fn get_dimensions(&self) -> Dim {
        Dim {
            w: self.width,
            h: self.height,
        }
    }
    /// Returns the entire image as a region.
    pub fn as_region(&self) -> Region {
        Region {
            l: Loc { x: 0, y: 0 },
            d: self.get_dimensions(),
        }
    }
    /// Tries to look up the colour of a specific pixel; returns an Err<&str>
    /// if the location is out of bounds and an Ok<Colour> if not.
    pub fn get_pixel(&self, loc: Loc) -> Result<Colour, &'static str> {
        if !loc.inside_region(self.as_region()) {
            return Err("The specified location falls outside of the image.");
        }

        Ok(self.pixels[loc.as_index(self.get_dimensions())])
    }
}

// The following impl block defines modifying functions for Images.
impl Image {
    /// Fill the entire image with a given colour.
    ///
    /// # Arguments
    ///
    /// * `colour`: the colour to fill with.
    ///
    /// # Examples
    ///
    /// ```
    /// let img = Image::blank(30, 20).fill(Colour::WHITE);
    /// assert_eq!(img.pixels[0], Colour::WHITE);
    /// ```
    pub fn fill(self, colour: Colour) -> Image {
        let new_pixels = vec![colour; self.pixels.len()];
        Image {
            pixels: new_pixels,
            ..self
        }
    }

    /// Fills a region.
    pub fn fill_region(self, region: Region, colour: Colour) -> Image {
        let new_pixels = self
            .pixels
            .clone()
            .iter_mut()
            .enumerate()
            .map(|c| {
                if Loc::from_index(c.0, self.get_dimensions()).inside_region(region) {
                    colour
                } else {
                    *c.1 // pass through
                }
            })
            .collect::<Vec<_>>();
        Image {
            pixels: new_pixels,
            ..self
        }
    }

    /// Crop a region out of the image and return it. This method is mainly used internally and panics; the safer `crop()` should be used instead.
    ///
    /// The corner from which to crop is assumed to be the top left corner.
    ///
    /// # Panics
    ///
    /// * if the corner from which to crop is outside of the image,
    /// * if the cropped image would reach outside of the image (by specifying new dimensions which are too large).
    pub fn crop_unclamped(self, region: Region) -> Image {
        let new_width = region.d.w;
        let new_height = region.d.h;

        assert!(
            region.l.inside_region(self.as_region()),
            "The corner from which to crop is outside of the image."
        );
        assert!((region.l + region.d).inside_region(self.as_region()));

        Image {
            width: new_width,
            height: new_height,
            pixels: self
                .pixels
                .iter()
                .enumerate()
                .filter(|x| Loc::from_index(x.0, self.get_dimensions()).inside_region(region))
                .map(|x| *x.1)
                .collect(),
        }
    }

    /// Crop a region out of the image and return it. This method (unlike `crop_unclamped()`) will adjust the
    /// size of the region if it is too big. It will return an `Err<&str>` if the corner to begin with falls outside of the image.
    ///
    /// # Arguments
    ///
    /// * `region` - the region to crop out (_inclusive_).
    ///
    /// # Examples
    ///
    /// ```
    /// let img = Image::blank( Dim { w: 20, h: 30 } )
    ///     .crop( Region { l: Loc { x: 10, y: 10 }, d: Dim { w: 10, h: 10 } } )
    ///     .unwrap();
    /// assert_eq!(img.width, 10);
    /// ```
    pub fn crop(self, region: Region) -> Result<Image, &'static str> {
        if !region.l.inside_region(self.as_region()) {
            return Err("The corner from which to crop falls outside of the image.");
        }

        if !(region.l + region.d).inside_region(self.as_region()) {
            // We clamp the area to be cropped.
            let new_width = self.width - region.d.w;
            let new_height = self.height - region.d.h;

            return Ok(self.crop_unclamped(Region {
                d: Dim {
                    w: new_width,
                    h: new_height,
                },
                ..region
            }));
        }

        Ok(self.crop_unclamped(region))
    }
}

#[cfg(test)]
mod kodak_tests {
    use super::*;

    #[test]
    fn loc_in_region() {
        let location1 = Loc { x: 10, y: 10 };
        let location2 = Loc { x: 10, y: 11 };
        assert!(location1.inside_region(Region {
            l: Loc { x: 0, y: 0 },
            d: Dim { w: 20, h: 20 }
        }));

        assert!(location2.inside_region(Region {
            l: Loc { x: 0, y: 0 },
            d: Dim { w: 20, h: 20 }
        }));
    }

    #[test]
    fn fill_region() {
        let img = Image::blank(Dim { w: 20, h: 10 }).fill_region(
            Region {
                l: Loc { x: 10, y: 0 },
                d: Dim { w: 10, h: 10 },
            },
            Colour::WHITE,
        );

        assert_eq!(img.get_pixel(Loc { x: 5, y: 5 }).unwrap(), Colour::BLACK);
        assert_eq!(img.get_pixel(Loc { x: 15, y: 5 }).unwrap(), Colour::WHITE);
    }
}