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
//! Main functions doing actual work.
//!
//! Use `guess_format()` to get the image format from a path,
//! then read the image using `load_image()` to the size given by `image_resized_size()`,
//! resize it to terminal size with `resize_image()`
//! and display it with `write_[no_]ansi[_truecolor]()`,
//! or display it yourself with approximations from `create_colourtable()`.


use self::super::util::{ANSI_BG_COLOUR_ESCAPES, ANSI_RESET_ATTRIBUTES, ANSI_COLOUR_ESCAPES, JPEG_MAGIC, BMP_MAGIC, ICO_MAGIC, GIF_MAGIC, PNG_MAGIC,
                        closest_colour, bg_colours_for};
use image::{self, GenericImageView, DynamicImage, ImageFormat, Pixel};
use std::io::{BufReader, Write, Read};
use image::imageops::FilterType;
use self::super::Error;
use std::path::PathBuf;
use std::ops::Index;
use std::fs::File;

mod no_ansi;

pub use self::no_ansi::write_no_ansi;


/// Guess the image format from its extension or magic.
///
/// # Examples
///
/// Correct:
///
/// ```
/// # extern crate image;
/// # extern crate termimage;
/// # use image::ImageFormat;
/// # use std::path::PathBuf;
/// # use termimage::ops::guess_format;
/// # fn main() {
/// assert_eq!(guess_format(&(String::new(), PathBuf::from("img.png"))), Ok(ImageFormat::Png));
/// assert_eq!(guess_format(&(String::new(), PathBuf::from("img.jpg"))), Ok(ImageFormat::Jpeg));
/// assert_eq!(guess_format(&(String::new(), PathBuf::from("img.gif"))), Ok(ImageFormat::Gif));
/// assert_eq!(guess_format(&(String::new(), PathBuf::from("img.bmp"))), Ok(ImageFormat::Bmp));
/// assert_eq!(guess_format(&(String::new(), PathBuf::from("img.ico"))), Ok(ImageFormat::Ico));
/// # }
/// ```
///
/// Incorrect:
///
/// ```
/// # use std::path::PathBuf;
/// # use termimage::Error;
/// # use termimage::ops::guess_format;
/// assert_eq!(guess_format(&("src/ops.rs".to_string(), PathBuf::from("src/ops/mod.rs"))),
/// Err(Error::GuessingFormatFailed("src/ops.rs".to_string())));
/// ```
pub fn guess_format(file: &(String, PathBuf)) -> Result<ImageFormat, Error> {
    file.1
        .extension()
        .and_then(|ext| match &ext.to_str().unwrap().to_lowercase()[..] {
            "png" => Some(Ok(ImageFormat::Png)),
            "jpg" | "jpeg" | "jpe" | "jif" | "jfif" | "jfi" => Some(Ok(ImageFormat::Jpeg)),
            "gif" => Some(Ok(ImageFormat::Gif)),
            "webp" => Some(Ok(ImageFormat::WebP)),
            "ppm" => Some(Ok(ImageFormat::Pnm)),
            "tiff" | "tif" => Some(Ok(ImageFormat::Tiff)),
            "tga" => Some(Ok(ImageFormat::Tga)),
            "bmp" | "dib" => Some(Ok(ImageFormat::Bmp)),
            "ico" => Some(Ok(ImageFormat::Ico)),
            "hdr" => Some(Ok(ImageFormat::Hdr)),
            _ => None,
        })
        .unwrap_or_else(|| {
            let mut buf = [0; 32];
            let read = File::open(&file.1).map_err(|_| Error::OpeningImageFailed(file.0.clone()))?.read(&mut buf).unwrap();
            let buf = &buf[..read];

            if buf.len() >= PNG_MAGIC.len() && &buf[..PNG_MAGIC.len()] == PNG_MAGIC {
                Ok(ImageFormat::Png)
            } else if buf.len() >= JPEG_MAGIC.len() && &buf[..JPEG_MAGIC.len()] == JPEG_MAGIC {
                Ok(ImageFormat::Jpeg)
            } else if buf.len() >= GIF_MAGIC.len() && &buf[..GIF_MAGIC.len()] == GIF_MAGIC {
                Ok(ImageFormat::Gif)
            } else if buf.len() >= BMP_MAGIC.len() && &buf[..BMP_MAGIC.len()] == BMP_MAGIC {
                Ok(ImageFormat::Bmp)
            } else if buf.len() >= ICO_MAGIC.len() && &buf[..ICO_MAGIC.len()] == ICO_MAGIC {
                Ok(ImageFormat::Ico)
            } else {
                Err(Error::GuessingFormatFailed(file.0.clone()))
            }
        })
}

/// Load an image from the specified file as the specified format.
///
/// Get the image fromat with `guess_format()`.
pub fn load_image(file: &(String, PathBuf), format: ImageFormat) -> Result<DynamicImage, Error> {
    Ok(image::load(BufReader::new(File::open(&file.1).map_err(|_| Error::OpeningImageFailed(file.0.clone()))?),
                   format)
        .unwrap())
}

/// Get the image size to downscale to, given its size, the terminal's size and whether to preserve its aspect.
///
/// The resulting image size is twice as tall as the terminal size because we print two pixels per cell (height-wise).
pub fn image_resized_size(size: (u32, u32), term_size: (u32, u32), preserve_aspect: bool) -> (u32, u32) {
    if !preserve_aspect {
        return (term_size.0, term_size.1 * 2);
    }

    let nwidth = term_size.0;
    let nheight = term_size.1 * 2;
    let (width, height) = size;

    let ratio = width as f32 / height as f32;
    let nratio = nwidth as f32 / nheight as f32;

    let scale = if nratio > ratio {
        nheight as f32 / height as f32
    } else {
        nwidth as f32 / width as f32
    };

    ((width as f32 * scale) as u32, (height as f32 * scale) as u32)
}

/// Resize the specified image to the specified size.
pub fn resize_image(img: &DynamicImage, size: (u32, u32)) -> DynamicImage {
    img.resize_exact(size.0, size.1, FilterType::Nearest)
}

/// Create a line-major table of (upper, lower) colour approximation indices given the supported colours therefor.
///
/// # Examples
///
/// Approximate `img` to ANSI and display it to stdout.
///
/// ```
/// # extern crate termimage;
/// # extern crate image;
/// # use termimage::util::{ANSI_COLOURS_WHITE_BG, ANSI_COLOUR_ESCAPES, ANSI_BG_COLOUR_ESCAPES, bg_colours_for};
/// # use termimage::ops::create_colourtable;
/// # fn main() {
/// # let img = image::DynamicImage::new_rgb8(16, 16);
/// for line in create_colourtable(&img, &ANSI_COLOURS_WHITE_BG, &bg_colours_for(&ANSI_COLOURS_WHITE_BG)) {
///     for (upper_clr, lower_clr) in line {
///         print!("{}{}\u{2580}", // ▀
///                ANSI_COLOUR_ESCAPES[upper_clr],
///                ANSI_BG_COLOUR_ESCAPES[lower_clr]);
///     }
///     println!("{}{}", ANSI_COLOUR_ESCAPES[15], ANSI_BG_COLOUR_ESCAPES[0]);
/// }
/// # }
/// ```
pub fn create_colourtable<C: Index<usize, Output = u8>>(img: &DynamicImage, upper_colours: &[C], lower_colours: &[C]) -> Vec<Vec<(usize, usize)>> {
    let (width, height) = img.dimensions();
    let term_h = height / 2;

    (0..term_h)
        .map(|y| {
            let upper_y = y * 2;
            let lower_y = upper_y + 1;

            (0..width)
                .map(|x| (closest_colour(img.get_pixel(x, upper_y).to_rgb(), upper_colours), closest_colour(img.get_pixel(x, lower_y).to_rgb(), lower_colours)))
                .collect()
        })
        .collect()
}

/// Display the specified image approximating it to the specified colours in the default console using ANSI escape codes.
pub fn write_ansi<W: Write, C: Index<usize, Output = u8>>(out: &mut W, img: &DynamicImage, foreground_colours: &[C]) {
    for line in create_colourtable(img, foreground_colours, &bg_colours_for(foreground_colours)) {
        for (upper_clr, lower_clr) in line {
            write!(out,
                   "{}{}\u{2580}", // ▀
                   ANSI_COLOUR_ESCAPES[upper_clr],
                   ANSI_BG_COLOUR_ESCAPES[lower_clr])
                .unwrap();
        }
        writeln!(out, "{}{}", ANSI_COLOUR_ESCAPES[15], ANSI_BG_COLOUR_ESCAPES[0]).unwrap();
    }
    write!(out, "{}", ANSI_RESET_ATTRIBUTES).unwrap();
}

/// Display the specified image in the default console using ANSI 24-bit escape colour codes.
pub fn write_ansi_truecolor<W: Write>(out: &mut W, img: &DynamicImage) {
    let (width, height) = img.dimensions();
    let term_h = height / 2;

    for y in 0..term_h {
        let upper_y = y * 2;
        let lower_y = upper_y + 1;

        for x in 0..width {
            let upper_pixel = img.get_pixel(x, upper_y).to_rgb();
            let lower_pixel = img.get_pixel(x, lower_y).to_rgb();

            write!(out,
                   "\x1B[38;2;{};{};{}m\
                    \x1B[48;2;{};{};{}m\u{2580}", // ▀
                   upper_pixel[0],
                   upper_pixel[1],
                   upper_pixel[2],
                   lower_pixel[0],
                   lower_pixel[1],
                   lower_pixel[2])
                .unwrap();
        }
        writeln!(out, "{}", ANSI_BG_COLOUR_ESCAPES[0]).unwrap();
    }
    write!(out, "{}", ANSI_RESET_ATTRIBUTES).unwrap();
}