figlet_rs/
lib.rs

1//! you can visit [`figlet`] and [`figfont`] to find more details.
2//! you can visit [`fongdb`] to find more font.
3//!
4//! # Examples
5//!
6//! download [`small.flf`] and place it to the `resources` folder.
7//!
8//! convert string literal using standard or specified font:
9//!
10//! ```
11//! use figlet_rs::FIGfont;
12//!
13//! let standard_font = FIGfont::standard().unwrap();
14//! let figure = standard_font.convert("FIGlet");
15//! assert!(figure.is_some());
16//!
17//! let small_font = FIGfont::from_file("resources/small.flf").unwrap();
18//! let figure = small_font.convert("FIGlet");
19//! assert!(figure.is_some());
20//! ```
21//! [`figlet`]: http://www.figlet.org
22//! [`figfont`]: http://www.jave.de/figlet/figfont.html
23//! [`fongdb`]: http://www.figlet.org/fontdb.cgi
24//! [`small.flf`]: http://www.figlet.org/fonts/small.flf
25
26use std::collections::HashMap;
27use std::{fmt, fs};
28
29/// FIGlet font, which will hold the mapping from u32 code to FIGcharacter
30#[derive(Debug)]
31pub struct FIGfont {
32    pub header_line: HeaderLine,
33    pub comments: String,
34    pub fonts: HashMap<u32, FIGcharacter>,
35}
36
37impl FIGfont {
38    fn read_font_file(filename: &str) -> Result<String, String> {
39        fs::read_to_string(filename).map_err(|e| format!("{e:?}"))
40    }
41
42    fn read_header_line(header_line: &str) -> Result<HeaderLine, String> {
43        HeaderLine::try_from(header_line)
44    }
45
46    fn read_comments(lines: &[&str], comment_count: i32) -> Result<String, String> {
47        let length = lines.len() as i32;
48        if length < comment_count + 1 {
49            Err("can't get comments from font".to_string())
50        } else {
51            let comment = lines[1..(1 + comment_count) as usize].join("\n");
52            Ok(comment)
53        }
54    }
55
56    fn extract_one_line(
57        lines: &[&str],
58        index: usize,
59        height: usize,
60        hardblank: char,
61        is_last_index: bool,
62    ) -> Result<String, String> {
63        let line = lines
64            .get(index)
65            .ok_or(format!("can't get line at specified index:{index}"))?;
66
67        let mut width = line.len() - 1;
68        if is_last_index && height != 1 {
69            width -= 1;
70        }
71
72        Ok(line[..width].replace(hardblank, " "))
73    }
74
75    fn extract_one_font(
76        lines: &[&str],
77        code: u32,
78        start_index: usize,
79        height: usize,
80        hardblank: char,
81    ) -> Result<FIGcharacter, String> {
82        let mut characters = vec![];
83        for i in 0..height {
84            let index = start_index + i as usize;
85            let is_last_index = i == height - 1;
86            let one_line_character =
87                FIGfont::extract_one_line(lines, index, height, hardblank, is_last_index)?;
88            characters.push(one_line_character);
89        }
90        let width = characters[0].len() as u32;
91        let height = height as u32;
92
93        Ok(FIGcharacter {
94            code,
95            characters,
96            width,
97            height,
98        })
99    }
100
101    // 32-126, 196, 214, 220, 228, 246, 252, 223
102    fn read_required_font(
103        lines: &[&str],
104        headerline: &HeaderLine,
105        map: &mut HashMap<u32, FIGcharacter>,
106    ) -> Result<(), String> {
107        let offset = (1 + headerline.comment_lines) as usize;
108        let height = headerline.height as usize;
109        let size = lines.len();
110
111        for i in 0..=94 {
112            let code = (i + 32) as u32;
113            let start_index = offset + i * height;
114            if start_index >= size {
115                break;
116            }
117
118            let font =
119                FIGfont::extract_one_font(lines, code, start_index, height, headerline.hardblank)?;
120            map.insert(code, font);
121        }
122
123        let offset = offset + 95 * height;
124        let required_deutsch_characters_codes: [u32; 7] = [196, 214, 220, 228, 246, 252, 223];
125        for (i, code) in required_deutsch_characters_codes.iter().enumerate() {
126            let start_index = offset + i * height;
127            if start_index >= size {
128                break;
129            }
130
131            let font =
132                FIGfont::extract_one_font(lines, *code, start_index, height, headerline.hardblank)?;
133            map.insert(*code, font);
134        }
135
136        Ok(())
137    }
138
139    fn extract_codetag_font_code(lines: &[&str], index: usize) -> Result<u32, String> {
140        let line = lines
141            .get(index)
142            .ok_or_else(|| "get codetag line error".to_string())?;
143
144        let infos: Vec<&str> = line.trim().split(' ').collect();
145        if infos.is_empty() {
146            return Err("extract code for codetag font error".to_string());
147        }
148
149        let code = infos[0].trim();
150
151        let code = if let Some(s) = code.strip_prefix("0x") {
152            u32::from_str_radix(s, 16)
153        } else if let Some(s) = code.strip_prefix("0X") {
154            u32::from_str_radix(s, 16)
155        } else if let Some(s) = code.strip_prefix('0') {
156            u32::from_str_radix(s, 8)
157        } else {
158            code.parse()
159        };
160
161        code.map_err(|e| format!("{e:?}"))
162    }
163
164    fn read_codetag_font(
165        lines: &[&str],
166        headerline: &HeaderLine,
167        map: &mut HashMap<u32, FIGcharacter>,
168    ) -> Result<(), String> {
169        let offset = (1 + headerline.comment_lines + 102 * headerline.height) as usize;
170        let codetag_height = (headerline.height + 1) as usize;
171        let codetag_lines = lines.len() - offset;
172
173        if codetag_lines % codetag_height != 0 {
174            return Err("codetag font is illegal.".to_string());
175        }
176
177        let size = codetag_lines / codetag_height;
178
179        for i in 0..size {
180            let start_index = offset + i * codetag_height;
181            if start_index >= lines.len() {
182                break;
183            }
184
185            let code = FIGfont::extract_codetag_font_code(lines, start_index)?;
186            let font = FIGfont::extract_one_font(
187                lines,
188                code,
189                start_index + 1,
190                headerline.height as usize,
191                headerline.hardblank,
192            )?;
193            map.insert(code, font);
194        }
195
196        Ok(())
197    }
198
199    fn read_fonts(
200        lines: &[&str],
201        headerline: &HeaderLine,
202    ) -> Result<HashMap<u32, FIGcharacter>, String> {
203        let mut map = HashMap::new();
204        FIGfont::read_required_font(lines, headerline, &mut map)?;
205        FIGfont::read_codetag_font(lines, headerline, &mut map)?;
206        Ok(map)
207    }
208
209    /// generate FIGlet font from string literal
210    pub fn from_content(contents: &str) -> Result<FIGfont, String> {
211        let lines: Vec<&str> = contents.lines().collect();
212
213        if lines.is_empty() {
214            return Err("can not generate FIGlet font from empty string".to_string());
215        }
216
217        let header_line = FIGfont::read_header_line(lines.first().unwrap())?;
218        let comments = FIGfont::read_comments(&lines, header_line.comment_lines)?;
219        let fonts = FIGfont::read_fonts(&lines, &header_line)?;
220
221        Ok(FIGfont {
222            header_line,
223            comments,
224            fonts,
225        })
226    }
227
228    /// generate FIGlet font from specified file
229    pub fn from_file(fontname: &str) -> Result<FIGfont, String> {
230        let contents = FIGfont::read_font_file(fontname)?;
231        FIGfont::from_content(&contents)
232    }
233
234    /// the standard FIGlet font, which you can find [`fontdb`]
235    ///
236    /// [`fontdb`]: http://www.figlet.org/fontdb.cgi
237    pub fn standard() -> Result<FIGfont, String> {
238        let contents = std::include_str!("standard.flf");
239        FIGfont::from_content(contents)
240    }
241
242    /// convert string literal to FIGure
243    pub fn convert(&self, message: &str) -> Option<FIGure> {
244        if message.is_empty() {
245            return None;
246        }
247
248        let mut characters: Vec<&FIGcharacter> = vec![];
249        for ch in message.chars() {
250            let code = ch as u32;
251            if let Some(character) = self.fonts.get(&code) {
252                characters.push(character);
253            }
254        }
255
256        if characters.is_empty() {
257            return None;
258        }
259
260        Some(FIGure {
261            characters,
262            height: self.header_line.height as u32,
263        })
264    }
265}
266
267/// the first line in FIGlet font, which you can find the documentation [`headerline`]
268///
269/// [`headerline`]: http://www.jave.de/figlet/figfont.html#headerline
270#[derive(Debug)]
271pub struct HeaderLine {
272    pub header_line: String,
273
274    // required
275    pub signature: String,
276    pub hardblank: char,
277    pub height: i32,
278    pub baseline: i32,
279    pub max_length: i32,
280    pub old_layout: i32, // Legal values -1 to 63
281    pub comment_lines: i32,
282
283    // optional
284    pub print_direction: Option<i32>,
285    pub full_layout: Option<i32>, // Legal values 0 to 32767
286    pub codetag_count: Option<i32>,
287}
288
289impl HeaderLine {
290    fn extract_signature_with_hardblank(
291        signature_with_hardblank: &str,
292    ) -> Result<(String, char), String> {
293        if signature_with_hardblank.len() < 6 {
294            Err("can't get signature with hardblank from first line of font".to_string())
295        } else {
296            let hardblank_index = signature_with_hardblank.len() - 1;
297            let signature = &signature_with_hardblank[..hardblank_index];
298            let hardblank = signature_with_hardblank[hardblank_index..]
299                .chars()
300                .next()
301                .unwrap();
302
303            Ok((String::from(signature), hardblank))
304        }
305    }
306
307    fn extract_required_info(infos: &[&str], index: usize, field: &str) -> Result<i32, String> {
308        let val = match infos.get(index) {
309            Some(val) => Ok(val),
310            None => Err(format!(
311                "can't get field:{field} index:{index} from {}",
312                infos.join(",")
313            )),
314        }?;
315
316        val.parse()
317            .map_err(|_| format!("can't parse required field:{field} of {val} to i32"))
318    }
319
320    fn extract_optional_info(infos: &[&str], index: usize, _field: &str) -> Option<i32> {
321        if let Some(val) = infos.get(index) {
322            val.parse().ok()
323        } else {
324            None
325        }
326    }
327}
328
329impl TryFrom<&str> for HeaderLine {
330    type Error = String;
331
332    fn try_from(header_line: &str) -> Result<Self, Self::Error> {
333        let infos: Vec<&str> = header_line.trim().split(' ').collect();
334
335        if infos.len() < 6 {
336            return Err("headerline is illegal".to_string());
337        }
338
339        let signature_with_hardblank =
340            HeaderLine::extract_signature_with_hardblank(infos.first().unwrap())?;
341
342        let height = HeaderLine::extract_required_info(&infos, 1, "height")?;
343        let baseline = HeaderLine::extract_required_info(&infos, 2, "baseline")?;
344        let max_length = HeaderLine::extract_required_info(&infos, 3, "max length")?;
345        let old_layout = HeaderLine::extract_required_info(&infos, 4, "old layout")?;
346        let comment_lines = HeaderLine::extract_required_info(&infos, 5, "comment lines")?;
347
348        let print_direction = HeaderLine::extract_optional_info(&infos, 6, "print direction");
349        let full_layout = HeaderLine::extract_optional_info(&infos, 7, "full layout");
350        let codetag_count = HeaderLine::extract_optional_info(&infos, 8, "codetag count");
351
352        Ok(HeaderLine {
353            header_line: String::from(header_line),
354            signature: signature_with_hardblank.0,
355            hardblank: signature_with_hardblank.1,
356            height,
357            baseline,
358            max_length,
359            old_layout,
360            comment_lines,
361            print_direction,
362            full_layout,
363            codetag_count,
364        })
365    }
366}
367
368/// the matched ascii art of one character
369#[derive(Debug)]
370pub struct FIGcharacter {
371    pub code: u32,
372    pub characters: Vec<String>,
373    pub width: u32,
374    pub height: u32,
375}
376
377impl fmt::Display for FIGcharacter {
378    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
379        write!(f, "{}", self.characters.join("\n"))
380    }
381}
382
383/// the matched ascii arts of string literal
384#[derive(Debug)]
385pub struct FIGure<'a> {
386    pub characters: Vec<&'a FIGcharacter>,
387    pub height: u32,
388}
389
390impl<'a> FIGure<'a> {
391    fn is_not_empty(&self) -> bool {
392        !self.characters.is_empty() && self.height > 0
393    }
394}
395
396impl<'a> fmt::Display for FIGure<'a> {
397    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
398        if self.is_not_empty() {
399            let mut rs: Vec<&'a str> = vec![];
400            for i in 0..self.height {
401                for character in &self.characters {
402                    if let Some(line) = character.characters.get(i as usize) {
403                        rs.push(line);
404                    }
405                }
406                rs.push("\n");
407            }
408
409            write!(f, "{}", rs.join(""))
410        } else {
411            write!(f, "")
412        }
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    #[test]
421    fn test_new_headerline() {
422        let line = "flf2a$ 6 5 20 15 3 0 143 229";
423        let headerline = HeaderLine::try_from(line);
424        assert!(headerline.is_ok());
425        let headerline = headerline.unwrap();
426
427        assert_eq!(line, headerline.header_line);
428        assert_eq!("flf2a", headerline.signature);
429        assert_eq!('$', headerline.hardblank);
430        assert_eq!(6, headerline.height);
431        assert_eq!(5, headerline.baseline);
432        assert_eq!(20, headerline.max_length);
433        assert_eq!(15, headerline.old_layout);
434        assert_eq!(3, headerline.comment_lines);
435        assert_eq!(Some(0), headerline.print_direction);
436        assert_eq!(Some(143), headerline.full_layout);
437        assert_eq!(Some(229), headerline.codetag_count);
438    }
439
440    #[test]
441    fn test_new_figfont() {
442        let font = FIGfont::standard();
443        assert!(font.is_ok());
444        let font = font.unwrap();
445
446        let headerline = font.header_line;
447        assert_eq!("flf2a$ 6 5 16 15 11 0 24463", headerline.header_line);
448        assert_eq!("flf2a", headerline.signature);
449        assert_eq!('$', headerline.hardblank);
450        assert_eq!(6, headerline.height);
451        assert_eq!(5, headerline.baseline);
452        assert_eq!(16, headerline.max_length);
453        assert_eq!(15, headerline.old_layout);
454        assert_eq!(11, headerline.comment_lines);
455        assert_eq!(Some(0), headerline.print_direction);
456        assert_eq!(Some(24463), headerline.full_layout);
457        assert_eq!(None, headerline.codetag_count);
458
459        assert_eq!(
460            "Standard by Glenn Chappell & Ian Chai 3/93 -- based on Frank's .sig
461Includes ISO Latin-1
462figlet release 2.1 -- 12 Aug 1994
463Modified for figlet 2.2 by John Cowan <cowan@ccil.org>
464  to add Latin-{2,3,4,5} support (Unicode U+0100-017F).
465Permission is hereby given to modify this font, as long as the
466modifier's name is placed on a comment line.
467
468Modified by Paul Burton <solution@earthlink.net> 12/96 to include new parameter
469supported by FIGlet and FIGWin.  May also be slightly modified for better use
470of new full-width/kern/smush alternatives, but default output is NOT changed.",
471            font.comments
472        );
473
474        let one_font = font.fonts.get(&('F' as u32));
475        assert!(one_font.is_some());
476
477        let one_font = one_font.unwrap();
478        assert_eq!(70, one_font.code);
479        assert_eq!(8, one_font.width);
480        assert_eq!(6, one_font.height);
481
482        assert_eq!(6, one_font.characters.len());
483        assert_eq!("  _____ ", one_font.characters.get(0).unwrap());
484        assert_eq!(" |  ___|", one_font.characters.get(1).unwrap());
485        assert_eq!(" | |_   ", one_font.characters.get(2).unwrap());
486        assert_eq!(" |  _|  ", one_font.characters.get(3).unwrap());
487        assert_eq!(" |_|    ", one_font.characters.get(4).unwrap());
488        assert_eq!("        ", one_font.characters.get(5).unwrap());
489    }
490
491    #[test]
492    fn test_convert() {
493        let standard_font = FIGfont::standard();
494        assert!(standard_font.is_ok());
495        let standard_font = standard_font.unwrap();
496
497        let figure = standard_font.convert("FIGlet");
498        assert!(figure.is_some());
499
500        let figure = figure.unwrap();
501        assert_eq!(6, figure.height);
502        assert_eq!(6, figure.characters.len());
503
504        let f = figure.characters.get(0).unwrap();
505        assert_eq!(figure.height, f.height);
506        assert_eq!(8, f.width);
507        assert_eq!("  _____ ", f.characters.get(0).unwrap());
508        assert_eq!(" |  ___|", f.characters.get(1).unwrap());
509        assert_eq!(" | |_   ", f.characters.get(2).unwrap());
510        assert_eq!(" |  _|  ", f.characters.get(3).unwrap());
511        assert_eq!(" |_|    ", f.characters.get(4).unwrap());
512        assert_eq!("        ", f.characters.get(5).unwrap());
513
514        let i = figure.characters.get(1).unwrap();
515        assert_eq!(figure.height, i.height);
516        assert_eq!(6, i.width);
517        assert_eq!("  ___ ", i.characters.get(0).unwrap());
518        assert_eq!(" |_ _|", i.characters.get(1).unwrap());
519        assert_eq!("  | | ", i.characters.get(2).unwrap());
520        assert_eq!("  | | ", i.characters.get(3).unwrap());
521        assert_eq!(" |___|", i.characters.get(4).unwrap());
522        assert_eq!("      ", i.characters.get(5).unwrap());
523
524        let g = figure.characters.get(2).unwrap();
525        assert_eq!(figure.height, g.height);
526        assert_eq!(8, g.width);
527        assert_eq!(r"   ____ ", g.characters.get(0).unwrap());
528        assert_eq!(r"  / ___|", g.characters.get(1).unwrap());
529        assert_eq!(r" | |  _ ", g.characters.get(2).unwrap());
530        assert_eq!(r" | |_| |", g.characters.get(3).unwrap());
531        assert_eq!(r"  \____|", g.characters.get(4).unwrap());
532        assert_eq!(r"        ", g.characters.get(5).unwrap());
533
534        let l = figure.characters.get(3).unwrap();
535        assert_eq!(figure.height, l.height);
536        assert_eq!(4, l.width);
537        assert_eq!("  _ ", l.characters.get(0).unwrap());
538        assert_eq!(" | |", l.characters.get(1).unwrap());
539        assert_eq!(" | |", l.characters.get(2).unwrap());
540        assert_eq!(" | |", l.characters.get(3).unwrap());
541        assert_eq!(" |_|", l.characters.get(4).unwrap());
542        assert_eq!("    ", l.characters.get(5).unwrap());
543
544        let e = figure.characters.get(4).unwrap();
545        assert_eq!(figure.height, e.height);
546        assert_eq!(7, e.width);
547        assert_eq!(r"       ", e.characters.get(0).unwrap());
548        assert_eq!(r"   ___ ", e.characters.get(1).unwrap());
549        assert_eq!(r"  / _ \", e.characters.get(2).unwrap());
550        assert_eq!(r" |  __/", e.characters.get(3).unwrap());
551        assert_eq!(r"  \___|", e.characters.get(4).unwrap());
552        assert_eq!(r"       ", e.characters.get(5).unwrap());
553
554        let t = figure.characters.get(5).unwrap();
555        assert_eq!(figure.height, t.height);
556        assert_eq!(6, t.width);
557        assert_eq!(r"  _   ", t.characters.get(0).unwrap());
558        assert_eq!(r" | |_ ", t.characters.get(1).unwrap());
559        assert_eq!(r" | __|", t.characters.get(2).unwrap());
560        assert_eq!(r" | |_ ", t.characters.get(3).unwrap());
561        assert_eq!(r"  \__|", t.characters.get(4).unwrap());
562        assert_eq!(r"      ", t.characters.get(5).unwrap());
563    }
564}