1use std::collections::HashMap;
27use std::{fmt, fs};
28
29#[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 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 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 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 pub fn standard() -> Result<FIGfont, String> {
238 let contents = std::include_str!("standard.flf");
239 FIGfont::from_content(contents)
240 }
241
242 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#[derive(Debug)]
271pub struct HeaderLine {
272 pub header_line: String,
273
274 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, pub comment_lines: i32,
282
283 pub print_direction: Option<i32>,
285 pub full_layout: Option<i32>, 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#[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#[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}