image2ascii/
lib.rs

1//! convert image file or string to ascii art
2
3use image::imageops::FilterType;
4use rusttype::{point, Font, Scale, PositionedGlyph};
5use std::fs::File;
6use std::io::{self, BufWriter, Write, Read};
7use std::cmp;
8use image::GenericImageView;
9use rand::{thread_rng, Rng};
10
11/// characters that is used to make ascii art 
12pub static ASCIIS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!\"#$%&'()-^\\=~|@[`{;:]+*},./_<>?_     ";
13//pub static ASCIIS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!\"#$%&'()-^\\=~|@[`{;:]+*},./_<>?_ ";
14
15/// 2-d array of chars
16pub struct Char2DArray {
17    /// 2-d array of chars by Vec 
18    pub buffer: Vec<Vec<char>>,
19    height: usize,
20    width: usize,
21}
22
23#[derive(Debug, Copy, Clone)]
24pub struct CharPosition {
25    pub x: i32,
26    pub y: i32,
27}
28
29impl Char2DArray {
30    /// create Char2DArray
31    /// create Char2DArray and initialize value by ' '
32    pub fn new(width: usize, height: usize) -> Char2DArray {
33        let mut array2d = Char2DArray{
34            buffer: Vec::with_capacity(height),
35            height: height,
36            width: width,
37        };
38        for _ in 0..height {
39            let mut line: Vec<char> = Vec::with_capacity(width);
40            for _ in 0..width {
41                line.push(' ');
42            }
43            array2d.buffer.push(line);
44        }        
45        array2d
46    }
47
48    pub fn debug_print(&self) {
49        let lines = self.to_lines();
50        for line in lines {
51            println!("{}", line);
52        }
53    }
54
55    ///create Char2DArray from Vec<Vec<char>>
56    pub fn from(src: Vec<Vec<char>>) -> Char2DArray {
57        let mut c2d = Char2DArray::new(src[0].len(), src.len());
58        for y in 0..c2d.height() {
59            for x in 0..c2d.width() {
60                c2d.buffer[y][x] = src[y][x];
61            }
62        }
63        c2d
64    }
65
66    /// conver to Vec<String>
67    pub fn to_lines(&self) -> Vec<String> {
68        let mut lines: Vec<String> = Vec::new();
69        for chars in self.buffer.iter() {
70            let line: String = chars.into_iter().collect();
71            lines.push(line); 
72        }
73        lines
74    }
75
76    /// save to file
77    pub fn save(&self, save_file: &str) -> io::Result<()>{
78        let file = File::create(save_file)?;
79        let mut writer = BufWriter::new(file);
80        let lines = self.to_lines();
81        for line in lines.iter() {
82            writer.write_all(line.as_bytes())?;
83            writer.write(&[10u8])?; // LF
84        }
85        writer.flush()?;
86        Ok(())
87    }
88
89    /// get number of lines
90    pub fn height(&self) -> usize {
91        self.height
92    }
93
94    /// get number of rows
95    pub fn width(&self) -> usize {
96        self.width
97    }
98
99    /// overwrite data by another Char2DArray
100    pub fn overwrite_rect(&mut self, rect: &Char2DArray, position: CharPosition, transparent: Option<char>) {
101        let y_start = cmp::max(0, position.y);
102        let y_end = cmp::min(self.height() as i32, position.y + rect.height() as i32);
103        let x_start = cmp::max(0, position.x);
104        let x_end = cmp::min(self.width() as i32, position.x + rect.width() as i32);
105
106        for y in y_start..y_end {
107            for x in x_start..x_end {
108                match transparent {
109                    Some(ch) => {
110                        if rect.buffer[(y - position.y) as usize][(x - position.x) as usize] == ch {
111                            continue;
112                        }
113                    },
114                    _ => {}
115                }
116                self.buffer[y as usize][x as usize] = rect.buffer[(y - position.y) as usize][(x - position.x) as usize];
117            }
118        }
119    }
120 
121
122    pub fn overwrite_rect_center(&mut self, rect: &Char2DArray, position: CharPosition, transparent: Option<char>) {
123        let center_x = (self.width() / 2) - (rect.width() / 2);    
124        let center_y = (self.height() / 2) - (rect.height() / 2);
125        let offset_posi = CharPosition {
126            x: position.x + center_x as i32,
127            y: position.y + center_y as i32,
128        };
129        self.overwrite_rect(rect, offset_posi, transparent)
130    }
131    
132
133    pub fn overwrite_line(&mut self, ch: char, line_index: usize) {
134        for x in 0..self.width() {
135            self.buffer[line_index][x] = ch;
136        }
137    }
138    
139    pub fn overwrite_char(&mut self, ch: char, position: CharPosition){
140        self.buffer[position.y as usize][position.x as usize] = ch;
141    }
142
143    pub fn overwrite_char_all(&mut self, ch: char) {
144        for y in 0..self.height() {
145            for x in 0..self.width() {
146                self.buffer[y][x] = ch;
147            }
148        }
149    }
150    
151    pub fn copy_from(&mut self, src: &Char2DArray) {
152        self.overwrite_rect(src, CharPosition{x:0,y:0}, Option::None);
153    }
154
155    pub fn overwrite_fn<F: Fn(usize, usize, char) -> bool>(&mut self, ch: char, f: F){
156        for y in 0..self.height() {
157            for x in 0..self.width() {
158                if f(x, y, self.buffer[y as usize][x as usize]) {
159                    self.buffer[y as usize][x as usize] = ch;
160                }
161            }
162        }
163    }
164
165    pub fn overwrite_random_fn<F: Fn(usize, usize, char) -> bool>(&mut self, chars: Vec<char>, f: F){
166        let mut rng = thread_rng();
167        for y in 0..self.height() {
168            for x in 0..self.width() {
169                if f(x, y, self.buffer[y as usize][x as usize]) {
170                    let index: usize = rng.gen_range(0, chars.len());
171                    self.buffer[y as usize][x as usize] = chars[index];
172                }
173            }
174        }
175    }
176    
177
178}
179
180/// pad String to 256 characters
181fn pad(ascii_list: &str, target_size: usize) -> String {
182    let mut padded_list = String::from(ascii_list);
183    let list_size = ascii_list.len();
184    let diff = target_size - list_size;
185    let n = diff / list_size;
186    let m = diff % list_size;
187    for _ in 0..n {
188        padded_list.push_str(ascii_list);
189    }
190    padded_list.push_str(&ascii_list[0..m]);
191    padded_list
192}
193
194/// calculate density of each ascii character
195fn ascii2density(ascii_list: &String) -> Vec<(u32, char)> {
196    let font = Vec::from(include_bytes!("../font/OpenSans-Regular.ttf") as &[u8]);
197    let font = Font::try_from_vec(font).unwrap();
198
199    let scale = Scale {
200        x: 20.0,
201        y: 20.0,
202    };
203
204    let mut density_ascii: Vec<(u32,char)> = Vec::new();
205    for ch in ascii_list.chars() {
206        let v_metrics = font.v_metrics(scale);
207        let offset = point(0.0, v_metrics.ascent);
208        let glyphs: Vec<PositionedGlyph<'_>> = font.layout(&ch.to_string(), scale, offset).collect();
209
210        for g in glyphs {
211            let mut target_pixels = 0;
212            g.draw(|_, _, _|{
213                target_pixels = target_pixels + 1;
214            });
215            density_ascii.push((target_pixels, ch));
216        }
217    }
218    density_ascii.sort_by(|a, b| (b.0).cmp(&a.0));    
219    density_ascii
220}
221
222/// convert string to ascii art 
223pub fn string2ascii(message: &str, height: f32, ch: char, ch2nd: Option<(usize, char)>, font_file: Option<&str>) -> Result<Char2DArray, String> {
224    let font = if let Some(file) = font_file {
225        let font_data = std::fs::read(&file).unwrap();
226        Font::try_from_vec(font_data).unwrap()
227    } else {
228        let font_data = include_bytes!("../font/OpenSans-Regular.ttf");
229        Font::try_from_bytes(font_data as &[u8]).unwrap()
230    };
231    let pixel_height = height.ceil() as usize;
232    let scale = Scale {
233        x: height * 2.0,
234        y: height,
235    };
236    let v_metrics = font.v_metrics(scale);
237    let offset = point(0.0, v_metrics.ascent);
238
239    let glyphs: Vec<_> = font.layout(&message, scale, offset).collect();
240
241    let width = glyphs
242        .iter()
243        .rev()
244        .map(|g| g.position().x as f32 + g.unpositioned().h_metrics().advance_width)
245        .next()
246        .unwrap_or(0.0)
247        .ceil() as usize;
248    
249    let mut c2d = Char2DArray::new(width, pixel_height);
250    let latter = ch2nd.unwrap_or((0, ch));
251    for (i, g) in glyphs.iter().enumerate() {
252        if let Some(bb) = g.pixel_bounding_box() {
253            g.draw(|x, y, v| {
254                let mut c = ' ';
255                if v > 0.5 {
256                    if i < latter.0 {
257                        c = ch;
258                    } else {
259                        c = latter.1;
260                    }
261                }
262                let x = x as i32 + bb.min.x;
263                let y = y as i32 + bb.min.y;
264                if x >= 0 && x < width as i32 && y >= 0 && y < pixel_height as i32 {
265                    c2d.buffer[y as usize][x as usize] = c;
266                }
267            });
268        }
269    }
270    Ok(c2d)
271}
272
273
274/// convert image file to ascii art
275pub fn image2ascii(image_file: &str, target_width: u32, contrast: Option<f32>, characters: Option<&str>) -> Result<Char2DArray, String> {
276    let density_ascii = ascii2density(&pad(characters.unwrap_or(ASCIIS), 256));
277    if let Ok(img) = image::open(image_file){
278        // adjust contrast 
279        let img = img.adjust_contrast(contrast.unwrap_or(30.0));
280
281        // resize
282        let height = img.height();
283        let width = img.width();
284        let scale: f32 = target_width as f32 / width as f32;
285        let target_height: f32 = (height as f32 * scale) / 2.0;
286        let target_height = target_height as u32;
287        let resized_img = img.resize_exact(target_width, target_height, FilterType::Lanczos3);
288
289        // grayscale
290        let luma_img = resized_img.to_luma();
291
292        // pack to Char2DArray
293        let mut char2d: Char2DArray = Char2DArray::new(target_width as usize, target_height as usize); 
294        for (x, y, pixel) in luma_img.enumerate_pixels() {
295            let index = pixel[0] as usize;
296            char2d.buffer[y as usize][x as usize] = density_ascii[index].1;
297        }
298        
299        Ok(char2d)
300    } else {
301        Err(format!("can not open file {}",image_file))
302    }
303}
304
305#[test]
306fn char2darray_new_works() {
307    let w = 10;
308    let h = 20;
309    let c2d = Char2DArray::new(w, h);
310    assert_eq!(c2d.buffer.len(), h);
311    for line in c2d.buffer.iter() {
312        assert_eq!(line.len(), w);
313        for ch in line.iter() {
314            assert_eq!(ch, &' ');
315        }
316    }
317}
318
319#[test]
320fn char2darray_to_lines_works() {
321    let w = 3;
322    let h = 2;
323    let mut c2d = Char2DArray::new(w, h);
324
325    c2d.buffer[0][0] = 'A';
326    c2d.buffer[0][1] = 'B';
327    c2d.buffer[0][2] = 'C';
328    
329    c2d.buffer[1][0] = 'X';
330    c2d.buffer[1][1] = 'Y';
331    c2d.buffer[1][2] = 'Z';
332
333    let lines = c2d.to_lines();
334    assert_eq!(lines[0], String::from("ABC"));
335    assert_eq!(lines[1], String::from("XYZ"));
336}
337
338#[test]
339fn char2darray_save() {
340    let w = 3;
341    let h = 2;
342    let mut c2d = Char2DArray::new(w, h);
343
344    c2d.buffer[0][0] = 'A';
345    c2d.buffer[0][1] = 'B';
346    c2d.buffer[0][2] = 'C';
347    
348    c2d.buffer[1][0] = 'X';
349    c2d.buffer[1][1] = 'Y';
350    c2d.buffer[1][2] = 'Z';
351
352    let tmp = tempfile::NamedTempFile::new().unwrap();
353    let ret = c2d.save(tmp.path().to_str().unwrap());
354    assert!(ret.is_ok());
355
356    let mut save_file = tmp.reopen().unwrap();
357    //let reader = BufReader::new(save_file);
358    let mut data = String::new();
359    let ret = save_file.read_to_string(&mut data);
360    assert!(ret.is_ok());
361    assert_eq!(data, "ABC\nXYZ\n");
362    
363}
364
365#[test]
366fn pad_works() {
367    let text = String::from("ABC");
368    let padded = pad(&text, 10);
369    assert_eq!(padded, "ABCABCABCA");
370}
371
372#[test]
373fn ascii2density_works() {
374    let asciis = String::from("A.@=");
375    let density_ascii = ascii2density(&asciis);
376    assert_eq!(density_ascii.len(), 4);
377    assert_eq!(density_ascii[0].1, '@');
378    assert_eq!(density_ascii[1].1, 'A');
379    assert_eq!(density_ascii[2].1, '=');
380    assert_eq!(density_ascii[3].1, '.');
381}
382#[test]
383fn char2darray_height_width_works() {
384    let c2d = Char2DArray::new(5, 10);
385    assert_eq!(c2d.height(), 10);
386    assert_eq!(c2d.width(), 5);
387}
388
389#[test]
390fn char2darray_from_works() {
391    let c2d = Char2DArray::from(
392        vec![
393            vec!['A', 'B', 'C'],
394            vec!['X', 'Y', 'Z'],
395        ]
396    );
397    assert_eq!(c2d.width(), 3);
398    assert_eq!(c2d.height(), 2);
399    assert_eq!(c2d.buffer[0], ['A', 'B', 'C']);
400    assert_eq!(c2d.buffer[1], ['X', 'Y', 'Z']);
401}
402
403#[test]
404fn char2darray_overwrite_rect_works() {
405    let mut base = Char2DArray::from(vec![
406        vec!['A', 'B', 'C'],
407        vec!['X', 'Y', 'Z'],
408    ]);
409    let rect = Char2DArray::from(vec![
410        vec!['1', '2'],
411        vec!['3', '4'],
412    ]);
413 
414    let pos = CharPosition{x: 0, y: 0};
415    base.overwrite_rect(&rect, pos, Option::None);
416
417    assert_eq!(base.buffer[0], ['1', '2', 'C']);
418    assert_eq!(base.buffer[1], ['3', '4', 'Z']);
419
420    let mut base = Char2DArray::from(vec![
421        vec!['A', 'B', 'C'],
422        vec!['X', 'Y', 'Z'],
423    ]);
424 
425    let pos = CharPosition{x: -1, y: -1};
426    base.overwrite_rect(&rect, pos, Option::None);
427
428    assert_eq!(base.buffer[0], ['4', 'B', 'C']);
429    assert_eq!(base.buffer[1], ['X', 'Y', 'Z']);
430
431    let mut base = Char2DArray::from(vec![
432        vec!['A', 'B', 'C'],
433        vec!['X', 'Y', 'Z'],
434    ]);
435 
436    let pos = CharPosition{x: 1, y: 1};
437    base.overwrite_rect(&rect, pos, Option::None);
438
439    assert_eq!(base.buffer[0], ['A', 'B', 'C']);
440    assert_eq!(base.buffer[1], ['X', '1', '2']);
441
442}
443#[test]
444fn char2darray_overwrite_rect_center_works() {
445    let mut base = Char2DArray::from(vec![
446        vec!['A', 'B', 'C', 'D'],
447        vec!['E', 'F', 'G', 'H'],
448        vec!['I', 'J', 'K', 'L'],
449        vec!['M', 'N', 'O', 'P'],
450    ]);
451    let rect = Char2DArray::from(vec![
452        vec!['1', '2'],
453        vec!['3', '4'],
454    ]);
455 
456    let pos = CharPosition{x: 0, y: 0};
457    base.overwrite_rect_center(&rect, pos, Option::None);
458
459    assert_eq!(base.buffer[0], ['A', 'B', 'C', 'D']);
460    assert_eq!(base.buffer[1], ['E', '1', '2', 'H']);
461    assert_eq!(base.buffer[2], ['I', '3', '4', 'L']);
462    assert_eq!(base.buffer[3], ['M', 'N', 'O', 'P']);
463    
464}
465
466#[test]
467fn char2darray_overwrite_rect_center_trans_works() {
468    let mut base = Char2DArray::from(vec![
469        vec!['A', 'B', 'C', 'D'],
470        vec!['E', 'F', 'G', 'H'],
471        vec!['I', 'J', 'K', 'L'],
472        vec!['M', 'N', 'O', 'P'],
473    ]);
474    let rect = Char2DArray::from(vec![
475        vec!['\u{0000}', '2'],
476        vec!['3', '4'],
477    ]);
478 
479    let pos = CharPosition{x: 0, y: 0};
480    base.overwrite_rect_center(&rect, pos, Some('\u{0000}'));
481
482    assert_eq!(base.buffer[0], ['A', 'B', 'C', 'D']);
483    assert_eq!(base.buffer[1], ['E', 'F', '2', 'H']);
484    assert_eq!(base.buffer[2], ['I', '3', '4', 'L']);
485    assert_eq!(base.buffer[3], ['M', 'N', 'O', 'P']);
486    
487}
488#[test]
489fn char2darray_overwrite_line_works() {
490    let mut base = Char2DArray::from(vec![
491        vec!['A', 'B', 'C', 'D'],
492        vec!['E', 'F', 'G', 'H'],
493        vec!['I', 'J', 'K', 'L'],
494        vec!['M', 'N', 'O', 'P'],
495    ]);
496    base.overwrite_line('@', 1);
497    assert_eq!(base.buffer[0], ['A', 'B', 'C', 'D']);
498    assert_eq!(base.buffer[1], ['@', '@', '@', '@']);
499    assert_eq!(base.buffer[2], ['I', 'J', 'K', 'L']);
500    assert_eq!(base.buffer[3], ['M', 'N', 'O', 'P']);
501}
502#[test]
503fn char2darray_overwrite_char_works(){
504    let mut base = Char2DArray::from(vec![
505        vec!['A', 'B', 'C', 'D'],
506        vec!['E', 'F', 'G', 'H'],
507        vec!['I', 'J', 'K', 'L'],
508        vec!['M', 'N', 'O', 'P'],
509    ]);
510    base.overwrite_char('@', CharPosition{x: 2, y:1});
511    assert_eq!(base.buffer[0], ['A', 'B', 'C', 'D']);
512    assert_eq!(base.buffer[1], ['E', 'F', '@', 'H']);
513    assert_eq!(base.buffer[2], ['I', 'J', 'K', 'L']);
514    assert_eq!(base.buffer[3], ['M', 'N', 'O', 'P']);
515}
516
517#[test]
518fn char2darray_overwrite_fn_works(){
519    let mut base = Char2DArray::from(vec![
520        vec!['A', 'B', 'C', 'D'],
521        vec!['E', 'F', 'G', 'H'],
522        vec!['I', 'J', 'K', 'L'],
523        vec!['M', 'N', 'O', 'P'],
524    ]);
525    let threshold = 2;
526    base.overwrite_fn('@', |_, y,_| {
527        if y >= threshold {
528            true
529        }else {
530            false
531        }
532    });
533    assert_eq!(base.buffer[0], ['A', 'B', 'C', 'D']);
534    assert_eq!(base.buffer[1], ['E', 'F', 'G', 'H']);
535    assert_eq!(base.buffer[2], ['@', '@', '@', '@']);
536    assert_eq!(base.buffer[3], ['@', '@', '@', '@']);
537
538}