use std::collections::HashMap;
use std::{fmt, fs};
#[derive(Debug)]
struct Layout {
layout: i32,
}
impl Layout {
fn new(full_layout: Option<i32>, old_layout: i32) -> Layout {
if let Some(full_layout) = full_layout {
Self {
layout: full_layout,
}
} else {
Self { layout: old_layout }
}
}
fn has_rule_1(&self) -> bool {
return self.layout & 1 != 0;
}
fn has_rule_2(&self) -> bool {
return self.layout & 2 != 0;
}
fn has_rule_3(&self) -> bool {
return self.layout & 4 != 0;
}
fn has_rule_4(&self) -> bool {
return self.layout & 8 != 0;
}
}
#[derive(Debug)]
pub struct FIGfont {
pub header_line: HeaderLine,
pub comments: String,
pub fonts: HashMap<u32, FIGcharacter>,
}
impl FIGfont {
fn read_font_file(filename: &str) -> Result<String, String> {
fs::read_to_string(filename).map_err(|e| format!("{e:?}"))
}
fn read_header_line(header_line: &str) -> Result<HeaderLine, String> {
HeaderLine::try_from(header_line)
}
fn read_comments(lines: &[&str], comment_count: i32) -> Result<String, String> {
let length = lines.len() as i32;
if length < comment_count + 1 {
Err("can't get comments from font".to_string())
} else {
let comment = lines[1..(1 + comment_count) as usize].join("\n");
Ok(comment)
}
}
fn extract_one_line(
lines: &[&str],
index: usize,
height: usize,
is_last_index: bool,
) -> Result<String, String> {
let line = lines
.get(index)
.ok_or(format!("can't get line at specified index:{index}"))?;
let mut width = line.len() - 1;
if is_last_index && height != 1 {
width -= 1;
}
Ok(line[..width].to_string())
}
fn extract_one_font(
lines: &[&str],
code: u32,
start_index: usize,
height: usize,
) -> Result<FIGcharacter, String> {
let mut characters = vec![];
for i in 0..height {
let index = start_index + i as usize;
let is_last_index = i == height - 1;
let one_line_character =
FIGfont::extract_one_line(lines, index, height, is_last_index)?;
characters.push(one_line_character);
}
let width = characters[0].len() as u32;
let height = height as u32;
Ok(FIGcharacter {
code,
characters,
width,
height,
})
}
fn read_required_font(
lines: &[&str],
headerline: &HeaderLine,
map: &mut HashMap<u32, FIGcharacter>,
) -> Result<(), String> {
let offset = (1 + headerline.comment_lines) as usize;
let height = headerline.height as usize;
let size = lines.len();
for i in 0..=94 {
let code = (i + 32) as u32;
let start_index = offset + i * height;
if start_index >= size {
break;
}
let font = FIGfont::extract_one_font(lines, code, start_index, height)?;
map.insert(code, font);
}
let offset = offset + 95 * height;
let required_deutsch_characters_codes: [u32; 7] = [196, 214, 220, 228, 246, 252, 223];
for (i, code) in required_deutsch_characters_codes.iter().enumerate() {
let start_index = offset + i * height;
if start_index >= size {
break;
}
let font = FIGfont::extract_one_font(lines, *code, start_index, height)?;
map.insert(*code, font);
}
Ok(())
}
fn extract_codetag_font_code(lines: &[&str], index: usize) -> Result<u32, String> {
let line = lines
.get(index)
.ok_or_else(|| "get codetag line error".to_string())?;
let infos: Vec<&str> = line.trim().split(' ').collect();
if infos.is_empty() {
return Err("extract code for codetag font error".to_string());
}
let code = infos[0].trim();
let code = if let Some(s) = code.strip_prefix("0x") {
u32::from_str_radix(s, 16)
} else if let Some(s) = code.strip_prefix("0X") {
u32::from_str_radix(s, 16)
} else if let Some(s) = code.strip_prefix('0') {
u32::from_str_radix(s, 8)
} else {
code.parse()
};
code.map_err(|e| format!("{e:?}"))
}
fn read_codetag_font(
lines: &[&str],
headerline: &HeaderLine,
map: &mut HashMap<u32, FIGcharacter>,
) -> Result<(), String> {
let offset = (1 + headerline.comment_lines + 102 * headerline.height) as usize;
let codetag_height = (headerline.height + 1) as usize;
let codetag_lines = lines.len() - offset;
if codetag_lines % codetag_height != 0 {
return Err("codetag font is illegal.".to_string());
}
let size = codetag_lines / codetag_height;
for i in 0..size {
let start_index = offset + i * codetag_height;
if start_index >= lines.len() {
break;
}
let code = FIGfont::extract_codetag_font_code(lines, start_index)?;
let font = FIGfont::extract_one_font(
lines,
code,
start_index + 1,
headerline.height as usize,
)?;
map.insert(code, font);
}
Ok(())
}
fn read_fonts(
lines: &[&str],
headerline: &HeaderLine,
) -> Result<HashMap<u32, FIGcharacter>, String> {
let mut map = HashMap::new();
FIGfont::read_required_font(lines, headerline, &mut map)?;
FIGfont::read_codetag_font(lines, headerline, &mut map)?;
Ok(map)
}
pub fn from_content(contents: &str) -> Result<FIGfont, String> {
let lines: Vec<&str> = contents.lines().collect();
if lines.is_empty() {
return Err("can not generate FIGlet font from empty string".to_string());
}
let header_line = FIGfont::read_header_line(lines.first().unwrap())?;
let comments = FIGfont::read_comments(&lines, header_line.comment_lines)?;
let fonts = FIGfont::read_fonts(&lines, &header_line)?;
Ok(FIGfont {
header_line,
comments,
fonts,
})
}
pub fn from_file(fontname: &str) -> Result<FIGfont, String> {
let contents = FIGfont::read_font_file(fontname)?;
FIGfont::from_content(&contents)
}
pub fn standard() -> Result<FIGfont, String> {
let contents = std::include_str!("standard.flf");
FIGfont::from_content(contents)
}
pub fn convert(&self, message: &str) -> Option<FIGure> {
if message.is_empty() {
return None;
}
let mut characters: Vec<&FIGcharacter> = vec![];
for ch in message.chars() {
let code = ch as u32;
if let Some(character) = self.fonts.get(&code) {
characters.push(character);
}
}
if characters.is_empty() {
return None;
}
Some(FIGure {
characters,
height: self.header_line.height as u32,
layout: Layout::new(self.header_line.full_layout, self.header_line.old_layout),
hardblank: self.header_line.hardblank,
})
}
}
#[derive(Debug)]
pub struct HeaderLine {
pub header_line: String,
pub signature: String,
pub hardblank: char,
pub height: i32,
pub baseline: i32,
pub max_length: i32,
pub old_layout: i32, pub comment_lines: i32,
pub print_direction: Option<i32>,
pub full_layout: Option<i32>, pub codetag_count: Option<i32>,
}
impl HeaderLine {
fn extract_signature_with_hardblank(
signature_with_hardblank: &str,
) -> Result<(String, char), String> {
if signature_with_hardblank.len() < 6 {
Err("can't get signature with hardblank from first line of font".to_string())
} else {
let hardblank_index = signature_with_hardblank.len() - 1;
let signature = &signature_with_hardblank[..hardblank_index];
let hardblank = signature_with_hardblank[hardblank_index..]
.chars()
.next()
.unwrap();
Ok((String::from(signature), hardblank))
}
}
fn extract_required_info(infos: &[&str], index: usize, field: &str) -> Result<i32, String> {
let val = match infos.get(index) {
Some(val) => Ok(val),
None => Err(format!(
"can't get field:{field} index:{index} from {}",
infos.join(",")
)),
}?;
val.parse()
.map_err(|_| format!("can't parse required field:{field} of {val} to i32"))
}
fn extract_optional_info(infos: &[&str], index: usize, _field: &str) -> Option<i32> {
if let Some(val) = infos.get(index) {
val.parse().ok()
} else {
None
}
}
}
impl TryFrom<&str> for HeaderLine {
type Error = String;
fn try_from(header_line: &str) -> Result<Self, Self::Error> {
let infos: Vec<&str> = header_line.trim().split(' ').collect();
if infos.len() < 6 {
return Err("headerline is illegal".to_string());
}
let signature_with_hardblank =
HeaderLine::extract_signature_with_hardblank(infos.first().unwrap())?;
let height = HeaderLine::extract_required_info(&infos, 1, "height")?;
let baseline = HeaderLine::extract_required_info(&infos, 2, "baseline")?;
let max_length = HeaderLine::extract_required_info(&infos, 3, "max length")?;
let old_layout = HeaderLine::extract_required_info(&infos, 4, "old layout")?;
let comment_lines = HeaderLine::extract_required_info(&infos, 5, "comment lines")?;
let print_direction = HeaderLine::extract_optional_info(&infos, 6, "print direction");
let full_layout = HeaderLine::extract_optional_info(&infos, 7, "full layout");
let codetag_count = HeaderLine::extract_optional_info(&infos, 8, "codetag count");
Ok(HeaderLine {
header_line: String::from(header_line),
signature: signature_with_hardblank.0,
hardblank: signature_with_hardblank.1,
height,
baseline,
max_length,
old_layout,
comment_lines,
print_direction,
full_layout,
codetag_count,
})
}
}
#[derive(Debug)]
pub struct FIGcharacter {
pub code: u32,
pub characters: Vec<String>,
pub width: u32,
pub height: u32,
}
impl fmt::Display for FIGcharacter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.characters.join("\n"))
}
}
#[derive(Debug)]
pub struct FIGure<'a> {
pub characters: Vec<&'a FIGcharacter>,
pub height: u32,
layout: Layout,
hardblank: char,
}
impl<'a> FIGure<'a> {
fn is_not_empty(&self) -> bool {
!self.characters.is_empty() && self.height > 0
}
}
fn first_non_space(a: impl Iterator<Item = char>, len: usize) -> (char, usize) {
for (idx, c) in a.enumerate() {
if !c.is_whitespace() {
return (c, idx);
}
}
(' ', len)
}
fn rule_2_char(c: char) -> bool {
return c == '|'
|| c == '/'
|| c == '\\'
|| c == '['
|| c == ']'
|| c == '{'
|| c == '}'
|| c == '('
|| c == ')'
|| c == '<'
|| c == '>';
}
fn rule_3_klass(c: char) -> u32 {
match c {
'|' => 1,
'/' => 2,
'\\' => 2,
'[' => 3,
']' => 3,
'{' => 4,
'}' => 4,
'(' => 5,
')' => 5,
'<' => 6,
'>' => 6,
_ => 0,
}
}
fn rule_4_chars(a: char, b: char) -> bool {
match a {
'(' => b == ')',
')' => b == '(',
'[' => b == ']',
']' => b == '[',
'{' => b == '}',
'}' => b == '{',
_ => false,
}
}
fn compute_kerning(a: &String, b: &String, layout: &Layout) -> (usize, usize) {
let (a_char, a_k) = first_non_space(a.chars().rev(), a.len());
let (b_char, b_k) = first_non_space(b.chars(), b.len());
if layout.has_rule_1() && a_char == b_char {
return (a_k + 1, b_k);
}
if layout.has_rule_2() && a_char == '_' && rule_2_char(b_char) {
return (a_k, b_k + 1);
}
if layout.has_rule_2() && b_char == '_' && rule_2_char(a_char) {
return (a_k, b_k + 1);
}
let a_3_klass = rule_3_klass(a_char);
let b_3_klass = rule_3_klass(b_char);
if layout.has_rule_3() && a_3_klass != b_3_klass && a_3_klass > 0 && b_3_klass > 0 {
return (a_k + 1, b_k);
}
if layout.has_rule_4() && rule_4_chars(a_char, b_char) {
return (a_k + 1, b_k);
}
return (a_k + 1, b_k - 1);
}
fn merge_string(a: &String, b: &String, left_kernel: usize, right_kernel: usize) -> String {
if a.len() <= left_kernel {
b.to_owned()
} else {
let (char_a, a_k) = first_non_space(a.chars().rev(), a.len());
let (char_b, b_k) = first_non_space(b.chars(), b.len());
let (middle_c, a_off, b_off) = if a_k < left_kernel
&& rule_3_klass(char_a) >= rule_3_klass(char_b)
&& !rule_4_chars(char_a, char_b)
{
(None, a_k, left_kernel + right_kernel - a_k)
} else if b_k < right_kernel
&& rule_3_klass(char_a) <= rule_3_klass(char_b)
&& !rule_4_chars(char_a, char_b)
{
(None, left_kernel + right_kernel - b_k, b_k)
} else if left_kernel == 0 {
(None, 0, right_kernel)
} else {
let a_c = a.chars().nth(a.len() - left_kernel).unwrap();
let b_c = b.chars().nth(right_kernel).unwrap();
if a_c == ' ' || rule_3_klass(a_c) < rule_3_klass(b_c) && rule_3_klass(a_c) > 0 {
(None, left_kernel, right_kernel)
} else if rule_4_chars(a_c, b_c) {
(Some("|"), left_kernel, right_kernel + 1)
} else {
(None, left_kernel - 1, right_kernel + 1)
}
};
match middle_c {
None => a[..a.len() - a_off].to_string() + &b[b_off..],
Some(middle_c) => a[..a.len() - a_off].to_string() + middle_c + &b[b_off..],
}
}
}
impl<'a> fmt::Display for FIGure<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.is_not_empty() {
let mut kerning: Vec<(usize, usize)> = vec![(0, 0)];
for i in 1..self.characters.len() {
kerning.push(
self.characters[i - 1]
.characters
.iter()
.zip(self.characters[i].characters.iter())
.map(|(lc, rc)| {
let k = compute_kerning(lc, rc, &self.layout);
k
})
.reduce(|acc, c| if acc.0 + acc.1 < c.0 + c.1 { acc } else { c })
.unwrap_or((0, 0)),
);
}
let mut rs: Vec<String> = vec![];
rs.resize(self.height as usize, String::new());
for (character, (left_kerning, right_kerning)) in
self.characters.iter().zip(kerning.iter())
{
rs = rs
.into_iter()
.zip(character.characters.iter())
.map(|(r, c)| merge_string(&r, &c, *left_kerning, *right_kerning))
.collect();
}
write!(f, "{}", rs.join("\n").replace(self.hardblank, " "))
} else {
write!(f, "")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_headerline() {
let line = "flf2a$ 6 5 20 15 3 0 143 229";
let headerline = HeaderLine::try_from(line);
assert!(headerline.is_ok());
let headerline = headerline.unwrap();
assert_eq!(line, headerline.header_line);
assert_eq!("flf2a", headerline.signature);
assert_eq!('$', headerline.hardblank);
assert_eq!(6, headerline.height);
assert_eq!(5, headerline.baseline);
assert_eq!(20, headerline.max_length);
assert_eq!(15, headerline.old_layout);
assert_eq!(3, headerline.comment_lines);
assert_eq!(Some(0), headerline.print_direction);
assert_eq!(Some(143), headerline.full_layout);
assert_eq!(Some(229), headerline.codetag_count);
}
#[test]
fn test_new_figfont() {
let font = FIGfont::standard();
assert!(font.is_ok());
let font = font.unwrap();
let headerline = font.header_line;
assert_eq!("flf2a$ 6 5 16 15 11 0 24463", headerline.header_line);
assert_eq!("flf2a", headerline.signature);
assert_eq!('$', headerline.hardblank);
assert_eq!(6, headerline.height);
assert_eq!(5, headerline.baseline);
assert_eq!(16, headerline.max_length);
assert_eq!(15, headerline.old_layout);
assert_eq!(11, headerline.comment_lines);
assert_eq!(Some(0), headerline.print_direction);
assert_eq!(Some(24463), headerline.full_layout);
assert_eq!(None, headerline.codetag_count);
assert_eq!(
"Standard by Glenn Chappell & Ian Chai 3/93 -- based on Frank's .sig
Includes ISO Latin-1
figlet release 2.1 -- 12 Aug 1994
Modified for figlet 2.2 by John Cowan <cowan@ccil.org>
to add Latin-{2,3,4,5} support (Unicode U+0100-017F).
Permission is hereby given to modify this font, as long as the
modifier's name is placed on a comment line.
Modified by Paul Burton <solution@earthlink.net> 12/96 to include new parameter
supported by FIGlet and FIGWin. May also be slightly modified for better use
of new full-width/kern/smush alternatives, but default output is NOT changed.",
font.comments
);
let one_font = font.fonts.get(&('F' as u32));
assert!(one_font.is_some());
let one_font = one_font.unwrap();
assert_eq!(70, one_font.code);
assert_eq!(8, one_font.width);
assert_eq!(6, one_font.height);
assert_eq!(6, one_font.characters.len());
assert_eq!(" _____ ", one_font.characters.get(0).unwrap());
assert_eq!(" | ___|", one_font.characters.get(1).unwrap());
assert_eq!(" | |_ ", one_font.characters.get(2).unwrap());
assert_eq!(" | _| ", one_font.characters.get(3).unwrap());
assert_eq!(" |_| ", one_font.characters.get(4).unwrap());
assert_eq!(" ", one_font.characters.get(5).unwrap());
}
#[test]
fn test_convert() {
let standard_font = FIGfont::standard();
assert!(standard_font.is_ok());
let standard_font = standard_font.unwrap();
let figure = standard_font.convert("FIGlet");
assert!(figure.is_some());
let figure = figure.unwrap();
assert_eq!(6, figure.height);
assert_eq!(6, figure.characters.len());
let f = figure.characters.get(0).unwrap();
assert_eq!(figure.height, f.height);
assert_eq!(8, f.width);
assert_eq!(" _____ ", f.characters.get(0).unwrap());
assert_eq!(" | ___|", f.characters.get(1).unwrap());
assert_eq!(" | |_ ", f.characters.get(2).unwrap());
assert_eq!(" | _| ", f.characters.get(3).unwrap());
assert_eq!(" |_| ", f.characters.get(4).unwrap());
assert_eq!(" ", f.characters.get(5).unwrap());
let i = figure.characters.get(1).unwrap();
assert_eq!(figure.height, i.height);
assert_eq!(6, i.width);
assert_eq!(" ___ ", i.characters.get(0).unwrap());
assert_eq!(" |_ _|", i.characters.get(1).unwrap());
assert_eq!(" | | ", i.characters.get(2).unwrap());
assert_eq!(" | | ", i.characters.get(3).unwrap());
assert_eq!(" |___|", i.characters.get(4).unwrap());
assert_eq!(" ", i.characters.get(5).unwrap());
let g = figure.characters.get(2).unwrap();
assert_eq!(figure.height, g.height);
assert_eq!(8, g.width);
assert_eq!(r" ____ ", g.characters.get(0).unwrap());
assert_eq!(r" / ___|", g.characters.get(1).unwrap());
assert_eq!(r" | | _ ", g.characters.get(2).unwrap());
assert_eq!(r" | |_| |", g.characters.get(3).unwrap());
assert_eq!(r" \____|", g.characters.get(4).unwrap());
assert_eq!(r" ", g.characters.get(5).unwrap());
let l = figure.characters.get(3).unwrap();
assert_eq!(figure.height, l.height);
assert_eq!(4, l.width);
assert_eq!(" _ ", l.characters.get(0).unwrap());
assert_eq!(" | |", l.characters.get(1).unwrap());
assert_eq!(" | |", l.characters.get(2).unwrap());
assert_eq!(" | |", l.characters.get(3).unwrap());
assert_eq!(" |_|", l.characters.get(4).unwrap());
assert_eq!(" ", l.characters.get(5).unwrap());
let e = figure.characters.get(4).unwrap();
assert_eq!(figure.height, e.height);
assert_eq!(7, e.width);
assert_eq!(r" ", e.characters.get(0).unwrap());
assert_eq!(r" ___ ", e.characters.get(1).unwrap());
assert_eq!(r" / _ \", e.characters.get(2).unwrap());
assert_eq!(r" | __/", e.characters.get(3).unwrap());
assert_eq!(r" \___|", e.characters.get(4).unwrap());
assert_eq!(r" ", e.characters.get(5).unwrap());
let t = figure.characters.get(5).unwrap();
assert_eq!(figure.height, t.height);
assert_eq!(6, t.width);
assert_eq!(r" _ ", t.characters.get(0).unwrap());
assert_eq!(r" | |_ ", t.characters.get(1).unwrap());
assert_eq!(r" | __|", t.characters.get(2).unwrap());
assert_eq!(r" | |_ ", t.characters.get(3).unwrap());
assert_eq!(r" \__|", t.characters.get(4).unwrap());
assert_eq!(r" ", t.characters.get(5).unwrap());
}
#[test]
fn test_convert_with_kerning() {
let standard_font = FIGfont::standard();
assert!(standard_font.is_ok());
let standard_font = standard_font.unwrap();
let figure = standard_font.convert("FIGlet");
assert!(figure.is_some());
assert_eq!(
format!("{}", figure.unwrap()),
r" _____ ___ ____ _ _
| ___|_ _/ ___| | ___| |_
| |_ | | | _| |/ _ \ __|
| _| | | |_| | | __/ |_
|_| |___\____|_|\___|\__|
"
);
let figure = standard_font.convert("oVo Tr oTo");
assert!(figure.is_some());
assert_eq!(
format!("{}", figure.unwrap()),
r" __ __ _____ _____
__\ \ / /__ |_ _| __ __|_ _|__
/ _ \ \ / / _ \ | || '__| / _ \| |/ _ \
| (_) \ V / (_) | | || | | (_) | | (_) |
\___/ \_/ \___/ |_||_| \___/|_|\___/
"
);
}
}