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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
//! A simple solution for encoding common icon file-formats, such as `.ico` and `.icns`. 
//! 
//! This crate is mostly a wrapper for other libraries, unifying existing APIs into a single, cohesive 
//! interface.
//! 
//! **IcoWriter** serves as **[Iconiic's](https://github.com/warrengalyen/iconiic)** internal library.
//! 
//! # Overview
//! 
//! An _icon_ consists of a set of _entries_. An _entry_ is simply an image that has a particular size.
//! **IcoWriter** simply automates the process of re-scaling pictures and combining them into an _icon_.
//! 
//! Pictures are scaled using resampling filters, which are represented by _functions that take a source_ 
//! _image and a size and return a re-scaled image_.
//! 
//! This allows the users of this crate to provide their custom resampling filters. Common resampling 
//! filters are provided in the 
//! [`resample`](https://docs.rs/iconwriter/1.6.0/iconwriter/resample/index.html) module.
//! 
//! # Examples
//! 
//! ## General Usage
//! 
//! ```rust, ignore
//! use iconwriter::{Ico, SourceImage, Icon};
//! use iconwriter::Error as IconError;
//!  
//! fn example() -> Result<(), IconError> {
//!     let icon = Ico::new();
//! 
//!     match SourceImage::from_path("image.svg") {
//!         Some(img) => icon.add_entry(resample::linear, &img, 32),
//!         None      => Ok(())
//!     }
//! }
//! ```
//! 
//! ## Writing to a File
//! 
//! ```rust, ignore
//! use iconwriter::*;
//! use std::{io, fs::File};
//!  
//! fn example() -> io::Result<()> {
//!     let icon = PngSequence::new();
//! 
//!     /* Process the icon */
//! 
//!     let file = File::create("out.icns")?;
//!     icon.write(file)
//! }
//! ```
//! 

pub extern crate image;
pub extern crate resvg;

pub use resvg::{usvg, raqote};
use std::{error, convert::From, path::{Path, PathBuf}, io::{self, Write}, fs::File, fmt::{self, Display, Debug}};
use image::{DynamicImage, GenericImageView};
use crate::usvg::Tree;

pub use crate::ico::Ico;
pub use crate::icns::Icns;
pub use crate::png_sequence::PngSequence;

#[cfg(test)]
mod test;
mod ico;
mod icns;
mod png_sequence;
pub mod resample;

const STD_CAPACITY: usize = 7;
const INVALID_DIM_ERR: &str = "a resampling filter returned images of invalid resolution";

/// A generic representation of an icon encoder.
pub trait Icon<E: AsRef<u32> + Debug + Eq> {
    /// Creates a new icon.
    /// 
    /// # Example
    /// ```rust, ignore
    /// let icon = Ico::new();
    /// ```
    fn new() -> Self;

    /// Adds an individual entry to the icon.
    /// 
    /// # Arguments
    /// * `filter` The resampling filter that will be used to re-scale `source`.
    /// * `source` A reference to the source image this entry will be based on.
    /// * `size` The target size of the entry in pixels.
    /// 
    /// # Return Value
    /// * Returns `Err(Error::InvalidSize(_))` if the dimensions provided in the
    ///  `size` argument are not supported.
    /// * Returns `Err(Error::Image(ImageError::DimensionError))`
    ///  if the resampling filter provided in the `filter` argument produces
    ///  results of dimensions other than the ones specified by `size`.
    /// * Otherwise return `Ok(())`.
    /// 
    /// # Example
    /// 
    /// ```rust, ignore
    /// use iconwriter::{Ico, SourceImage, Icon};
    /// use iconwriter::Error as IconError;
    ///  
    /// fn example() -> Result<(), IconError> {
    ///     let icon = Ico::new();
    /// 
    ///     match SourceImage::from_path("image.svg") {
    ///         Some(img) => icon.add_entry(resample::linear, &img, 32),
    ///         None      => Ok(())
    ///     }
    /// }
    /// ```
    fn add_entry<F: FnMut(&SourceImage, u32) -> DynamicImage>(
        &mut self,
        filter: F,
        source: &SourceImage,
        entry: E
    ) -> Result<(), Error<E>>;

    /// Adds a series of entries to the icon.
    /// # Arguments
    /// * `filter` The resampling filter that will be used to re-scale `source`.
    /// * `source` A reference to the source image this entry will be based on.
    /// * `size` A container for the target sizes of the entries in pixels.
    /// 
    /// # Return Value
    /// * Returns `Err(Error::InvalidSize(_))` if the dimensions provided in the
    ///  `size` argument are not supported.
    /// * Returns `Err(Error::Image(ImageError::DimensionError))`
    ///  if the resampling filter provided in the `filter` argument produces
    ///  results of dimensions other than the ones specified by `size`.
    /// * Otherwise return `Ok(())`.
    /// 
    /// # Example
    /// 
    /// ```rust, ignore
    /// use iconwriter::{Icns, SourceImage, Icon};
    /// use iconwriter::Error as IconError;
    ///  
   /// fn example() -> Result<(), IconError> {
    ///     let icon = Icns::new();
    /// 
    ///     match SourceImage::from_path("image.svg") {
    ///         Some(img) => icon.add_entries(
    ///             resample::linear,
    ///             &img,
    ///             vec![32, 64, 128]
    ///         ),
    ///         None => Ok(())
    ///     }
    /// }
    /// ```
    fn add_entries<F: FnMut(&SourceImage, u32) -> DynamicImage, I: IntoIterator<Item = E>>(
        &mut self,
        mut filter: F,
        source: &SourceImage,
        entries: I
    ) -> Result<(), Error<E>> {
        for entry in entries {
            self.add_entry(|src, size| filter(src, size), source, entry)?;
        }

        Ok(())
    }

    /// Writes the contents of the icon to `w`.
    /// 
    /// # Example
    /// 
    /// ```rust, ignore
    /// use iconwriter::*;
    /// use std::{io, fs::File};
    ///  
    /// fn example() -> io::Result<()> {
    ///     let icon = PngSequence::new();
    /// 
    ///     /* Process the icon */
    /// 
    ///     let file = File::create("out.icns")?;
    ///     icon.write(file)
    /// }
    /// ```
    fn write<W: Write>(&mut self, w: &mut W) -> io::Result<()>;

    /// Writes the contents of the icon to a file on disk.
    /// 
    /// # Example
    /// 
    /// ```rust
    /// use iconwriter::*;
    /// use std::{io, fs::File};
    ///  
    /// fn example() -> io::Result<()> {
    ///     let icon = Ico::new();
    /// 
    ///     /* Process the icon */
    /// 
    ///     icon.save("./output/out.ico")
    /// }
    /// ```
    fn save<P: AsRef<Path>>(&mut self, path: &P) -> io::Result<()> {
        let mut file = File::create(path.as_ref())?;
        self.write(&mut file)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Entry(u32);

#[derive(Clone, Debug, Eq, Hash)]
pub struct NamedEntry(u32, PathBuf);

#[derive(Clone)]
/// A representation of a source image.
pub enum SourceImage {
    /// A generic raster image.
    Raster(DynamicImage),
    /// A svg-encoded vector image.
    Svg(Tree)
}

#[derive(Debug)]
/// The error type for operations of the `Icon` trait.
pub enum Error<E: AsRef<u32> + Debug + Eq> {
    /// The `Icon` instance already includes this entry.
    AlreadyIncluded(E),
    /// The return value of a resampling filter has invalid
    /// dimensions: the dimensions do not match the ones
    /// specified in the application of the filter.
    InvalidDimensions(u32, (u32, u32)),
    /// An unsupported size was suplied to an `Icon` operation.
    InvalidSize(u32),
    /// Generic I/O error.
    Io(io::Error)
}

impl AsRef<u32> for Entry {
    fn as_ref(&self) -> &u32 {
        &self.0
    }
}

impl NamedEntry {
    /// Creates a `NamedEntry` from a reference to a `Path`.
    /// # Example
    /// ```rust
    /// let entry = NamedEntry::from(32, &"icons/32/icon.png");
    /// ```
    pub fn from<P: AsRef<Path>>(size: u32, path: &P) -> Self {
        NamedEntry(size, PathBuf::from(path.as_ref()))
    }
}

impl AsRef<u32> for NamedEntry {
    fn as_ref(&self) -> &u32 {
        &self.0
    }
}

impl PartialEq for NamedEntry {
    fn eq(&self, other: &NamedEntry) -> bool {
        self.1 == other.1
    }
}

impl SourceImage {
    /// Attempts to create a `SourceImage` from a given path.
    /// 
    /// The `SourceImage::from::<image::DynamicImage>` and `SourceImage::from::<usvg::Tree>`
    /// methods should always be preferred.
    /// 
    /// # Return Value
    /// * Returns `Some(src)` if the file indicated by the `path` argument could be 
    ///   successfully parsed into an image.
    /// * Returns `None` otherwise.
    /// 
    /// # Example
    /// ```rust, ignore
    /// let img = SourceImage::open("source.png")?;
    /// ```
    pub fn open<P: AsRef<Path>>(path: P) -> Option<Self> {
        if let Ok(ras) = image::open(&path) {
            return Some(SourceImage::from(ras));
        }

        Tree::from_file(&path, &usvg::Options::default())
        .ok().map(|svg| SourceImage::from(svg))
    }

    /// Returns the width of the original image in pixels.
    pub fn width(&self) -> f64 {
        match self {
            SourceImage::Raster(ras) => ras.width() as f64,
            SourceImage::Svg(svg)    => svg.svg_node().view_box.rect.width()
        }
    }

    /// Returns the height of the original image in pixels.
    pub fn height(&self) -> f64 {
        match self {
            SourceImage::Raster(ras) => ras.height() as f64,
            SourceImage::Svg(svg)    => svg.svg_node().view_box.rect.height()
        }
    }

    /// Returns the dimensions of the original image in pixels.
    pub fn dimensions(&self) -> (f64, f64) {
        (self.width(), self.height())
    }
}

impl From<Tree> for SourceImage {
    fn from(svg: Tree) -> Self {
        SourceImage::Svg(svg)
    }
}

impl From<DynamicImage> for SourceImage {
    fn from(bit: DynamicImage) -> Self {
        SourceImage::Raster(bit)
    }
}

impl<E: AsRef<u32> + Debug + Eq> Display for Error<E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::AlreadyIncluded(_) => write!(f, "the icon already includes this entry"),
            Error::InvalidSize(s)     => write!(f, "{0}x{0} icons are not supported", s),
            Error::Io(err)            => write!(f, "{}", err),
            Error::InvalidDimensions(s, (w, h))
                => write!(f, "{0}: expected {1}x{1} and got {2}x{3}", INVALID_DIM_ERR, s, w, h)
        }
    }
}

impl<E: AsRef<u32> + Debug + Eq> error::Error for Error<E> {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        if let Error::Io(ref err) = self {
            Some(err)
        } else {
            None
        }
    }
}

impl<E: AsRef<u32> + Debug + Eq> From<io::Error> for Error<E> {
    fn from(err: io::Error) -> Self {
        Error::Io(err)
    }
}

impl<E: AsRef<u32> + Debug + Eq> Into<io::Error> for Error<E> {
    fn into(self) -> io::Error {
        match self {
            Error::Io(err)                 => err,
            Error::InvalidDimensions(_, _) => io::Error::from(io::ErrorKind::InvalidData),
            _                              => io::Error::from(io::ErrorKind::InvalidInput)
        }
    }
}