Skip to main content

figlet_rs/
shared.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::fs;
4use std::io::{Cursor, Read};
5
6pub(crate) const SM_EQUAL: i32 = 1;
7pub(crate) const SM_LOWLINE: i32 = 2;
8pub(crate) const SM_HIERARCHY: i32 = 4;
9pub(crate) const SM_PAIR: i32 = 8;
10pub(crate) const SM_BIGX: i32 = 16;
11pub(crate) const SM_HARDBLANK: i32 = 32;
12pub(crate) const SM_KERN: i32 = 64;
13pub(crate) const SM_SMUSH: i32 = 128;
14
15pub(crate) struct FontData {
16    pub header_line: HeaderLine,
17    pub comments: String,
18    pub fonts: HashMap<u32, FIGcharacter>,
19}
20
21pub(crate) fn load_font_file(filename: &str) -> Result<FontData, String> {
22    let bytes = fs::read(filename).map_err(|e| format!("{e:?}"))?;
23    parse_font_bytes(&bytes)
24}
25
26pub(crate) fn parse_font_bytes(bytes: &[u8]) -> Result<FontData, String> {
27    let contents = decode_font_bytes(bytes)?;
28    parse_font_content(&contents)
29}
30
31pub(crate) fn parse_font_content(contents: &str) -> Result<FontData, String> {
32    let lines: Vec<&str> = contents.lines().collect();
33
34    if lines.is_empty() {
35        return Err("can not generate FIGlet font from empty string".to_string());
36    }
37
38    let header_line = read_header_line(lines.first().unwrap())?;
39    let comments = read_comments(&lines, header_line.comment_lines)?;
40    let fonts = read_fonts(&lines, &header_line)?;
41
42    Ok(FontData {
43        header_line,
44        comments,
45        fonts,
46    })
47}
48
49fn decode_font_bytes(bytes: &[u8]) -> Result<String, String> {
50    if bytes.starts_with(b"PK\x03\x04") {
51        let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| format!("{e:?}"))?;
52        if archive.is_empty() {
53            return Err("zip font archive is empty".to_string());
54        }
55
56        let mut file = archive.by_index(0).map_err(|e| format!("{e:?}"))?;
57        let mut contents = String::new();
58        file.read_to_string(&mut contents)
59            .map_err(|e| format!("{e:?}"))?;
60        Ok(contents)
61    } else {
62        String::from_utf8(bytes.to_vec()).map_err(|e| format!("{e:?}"))
63    }
64}
65
66fn read_header_line(header_line: &str) -> Result<HeaderLine, String> {
67    HeaderLine::try_from(header_line)
68}
69
70fn read_comments(lines: &[&str], comment_count: i32) -> Result<String, String> {
71    let length = lines.len() as i32;
72    if length < comment_count + 1 {
73        Err("can't get comments from font".to_string())
74    } else {
75        Ok(lines[1..(1 + comment_count) as usize].join("\n"))
76    }
77}
78
79fn extract_one_line(
80    lines: &[&str],
81    index: usize,
82    height: usize,
83    is_last_index: bool,
84) -> Result<String, String> {
85    let line = lines
86        .get(index)
87        .ok_or_else(|| format!("can't get line at specified index:{index}"))?;
88
89    let trimmed = line.trim_end_matches(' ');
90    let mut chars: Vec<char> = trimmed.chars().collect();
91    let endmark = chars
92        .pop()
93        .ok_or_else(|| format!("can't parse endmark at specified index:{index}"))?;
94    if is_last_index && height != 1 && chars.last().copied() == Some(endmark) {
95        chars.pop();
96    }
97
98    Ok(chars.into_iter().collect())
99}
100
101fn extract_one_font(
102    lines: &[&str],
103    code: u32,
104    start_index: usize,
105    height: usize,
106) -> Result<FIGcharacter, String> {
107    let mut characters = vec![];
108    for i in 0..height {
109        let index = start_index + i;
110        let is_last_index = i == height - 1;
111        characters.push(extract_one_line(lines, index, height, is_last_index)?);
112    }
113
114    Ok(FIGcharacter {
115        code,
116        width: characters[0].chars().count() as u32,
117        height: height as u32,
118        characters,
119    })
120}
121
122fn read_required_font(
123    lines: &[&str],
124    headerline: &HeaderLine,
125    map: &mut HashMap<u32, FIGcharacter>,
126) -> Result<(), String> {
127    let offset = (1 + headerline.comment_lines) as usize;
128    let height = headerline.height as usize;
129    let size = lines.len();
130
131    for i in 0..=94 {
132        let code = (i + 32) as u32;
133        let start_index = offset + i * height;
134        if start_index >= size {
135            break;
136        }
137
138        let font = extract_one_font(lines, code, start_index, height)?;
139        map.insert(code, font);
140    }
141
142    let offset = offset + 95 * height;
143    let required_deutsch_characters_codes: [u32; 7] = [196, 214, 220, 228, 246, 252, 223];
144    for (i, code) in required_deutsch_characters_codes.iter().enumerate() {
145        let start_index = offset + i * height;
146        if start_index >= size {
147            break;
148        }
149
150        let font = extract_one_font(lines, *code, start_index, height)?;
151        map.insert(*code, font);
152    }
153
154    Ok(())
155}
156
157fn extract_codetag_font_code(lines: &[&str], index: usize) -> Result<Option<u32>, String> {
158    let line = lines
159        .get(index)
160        .ok_or_else(|| "get codetag line error".to_string())?;
161
162    let infos: Vec<&str> = line.split_whitespace().collect();
163    if infos.is_empty() {
164        return Err("extract code for codetag font error".to_string());
165    }
166
167    let code = infos[0].trim();
168    let is_negative = code.starts_with('-');
169    let unsigned = code.trim_start_matches(['-', '+']);
170
171    let parsed = if let Some(s) = unsigned.strip_prefix("0x") {
172        i64::from_str_radix(s, 16)
173    } else if let Some(s) = unsigned.strip_prefix("0X") {
174        i64::from_str_radix(s, 16)
175    } else if unsigned.len() > 1 && unsigned.starts_with('0') {
176        i64::from_str_radix(&unsigned[1..], 8)
177    } else {
178        unsigned.parse()
179    }
180    .map_err(|e| format!("{e:?}"))?;
181
182    if is_negative {
183        Ok(None)
184    } else {
185        u32::try_from(parsed)
186            .map(Some)
187            .map_err(|e| format!("{e:?}"))
188    }
189}
190
191fn read_codetag_font(
192    lines: &[&str],
193    headerline: &HeaderLine,
194    map: &mut HashMap<u32, FIGcharacter>,
195) -> Result<(), String> {
196    let offset = (1 + headerline.comment_lines + 102 * headerline.height) as usize;
197    if offset >= lines.len() {
198        return Ok(());
199    }
200
201    let codetag_height = (headerline.height + 1) as usize;
202    let codetag_lines = lines.len() - offset;
203
204    if codetag_lines % codetag_height != 0 {
205        return Err("codetag font is illegal.".to_string());
206    }
207
208    let size = codetag_lines / codetag_height;
209
210    for i in 0..size {
211        let start_index = offset + i * codetag_height;
212        if start_index >= lines.len() {
213            break;
214        }
215
216        let Some(code) = extract_codetag_font_code(lines, start_index)? else {
217            continue;
218        };
219        let font = extract_one_font(lines, code, start_index + 1, headerline.height as usize)?;
220        map.insert(code, font);
221    }
222
223    Ok(())
224}
225
226fn read_fonts(
227    lines: &[&str],
228    headerline: &HeaderLine,
229) -> Result<HashMap<u32, FIGcharacter>, String> {
230    let mut map = HashMap::new();
231    read_required_font(lines, headerline, &mut map)?;
232    read_codetag_font(lines, headerline, &mut map)?;
233    Ok(map)
234}
235
236pub(crate) fn render<'a>(
237    header_line: &'a HeaderLine,
238    fonts: &'a HashMap<u32, FIGcharacter>,
239    message: &str,
240) -> Option<FIGure<'a>> {
241    if message.is_empty() {
242        return None;
243    }
244
245    let mut characters: Vec<&FIGcharacter> = vec![];
246    for ch in message.chars() {
247        let code = ch as u32;
248        if let Some(character) = fonts.get(&code) {
249            characters.push(character);
250        }
251    }
252
253    if characters.is_empty() {
254        return None;
255    }
256
257    let rendered_lines = Renderer::new(header_line, fonts).render(&characters);
258
259    Some(FIGure {
260        characters,
261        height: header_line.height as u32,
262        lines: rendered_lines,
263    })
264}
265
266#[derive(Debug, Clone)]
267pub struct HeaderLine {
268    pub header_line: String,
269    pub signature: String,
270    pub hardblank: char,
271    pub height: i32,
272    pub baseline: i32,
273    pub max_length: i32,
274    pub old_layout: i32,
275    pub comment_lines: i32,
276    pub print_direction: Option<i32>,
277    pub full_layout: Option<i32>,
278    pub codetag_count: Option<i32>,
279}
280
281impl HeaderLine {
282    fn extract_signature_with_hardblank(
283        signature_with_hardblank: &str,
284    ) -> Result<(String, char), String> {
285        if signature_with_hardblank.len() < 6 {
286            Err("can't get signature with hardblank from first line of font".to_string())
287        } else {
288            let hardblank_index = signature_with_hardblank.len() - 1;
289            let signature = &signature_with_hardblank[..hardblank_index];
290            let hardblank = signature_with_hardblank[hardblank_index..]
291                .chars()
292                .next()
293                .unwrap();
294
295            Ok((String::from(signature), hardblank))
296        }
297    }
298
299    fn extract_required_info(infos: &[&str], index: usize, field: &str) -> Result<i32, String> {
300        let val = infos.get(index).ok_or_else(|| {
301            format!(
302                "can't get field:{field} index:{index} from {}",
303                infos.join(",")
304            )
305        })?;
306
307        val.parse()
308            .map_err(|_| format!("can't parse required field:{field} of {val} to i32"))
309    }
310
311    fn extract_optional_info(infos: &[&str], index: usize) -> Option<i32> {
312        infos.get(index).and_then(|val| val.parse().ok())
313    }
314
315    pub(crate) fn effective_layout(&self) -> i32 {
316        match self.full_layout {
317            Some(layout) => layout,
318            None if self.old_layout == 0 => SM_KERN,
319            None if self.old_layout < 0 => 0,
320            None => (self.old_layout & 31) | SM_SMUSH,
321        }
322    }
323
324    fn is_right_to_left(&self) -> bool {
325        self.print_direction == Some(1)
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.split_whitespace().collect();
334
335        if infos.len() < 6 {
336            return Err("headerline is illegal".to_string());
337        }
338
339        let (signature, hardblank) =
340            HeaderLine::extract_signature_with_hardblank(infos.first().unwrap())?;
341
342        Ok(HeaderLine {
343            header_line: String::from(header_line),
344            signature,
345            hardblank,
346            height: HeaderLine::extract_required_info(&infos, 1, "height")?,
347            baseline: HeaderLine::extract_required_info(&infos, 2, "baseline")?,
348            max_length: HeaderLine::extract_required_info(&infos, 3, "max length")?,
349            old_layout: HeaderLine::extract_required_info(&infos, 4, "old layout")?,
350            comment_lines: HeaderLine::extract_required_info(&infos, 5, "comment lines")?,
351            print_direction: HeaderLine::extract_optional_info(&infos, 6),
352            full_layout: HeaderLine::extract_optional_info(&infos, 7),
353            codetag_count: HeaderLine::extract_optional_info(&infos, 8),
354        })
355    }
356}
357
358#[derive(Debug, Clone)]
359pub struct FIGcharacter {
360    pub code: u32,
361    pub characters: Vec<String>,
362    pub width: u32,
363    pub height: u32,
364}
365
366impl fmt::Display for FIGcharacter {
367    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
368        write!(f, "{}", self.characters.join("\n"))
369    }
370}
371
372#[derive(Debug)]
373pub struct FIGure<'a> {
374    pub characters: Vec<&'a FIGcharacter>,
375    pub height: u32,
376    lines: Vec<String>,
377}
378
379impl<'a> FIGure<'a> {
380    pub(crate) fn is_not_empty(&self) -> bool {
381        !self.characters.is_empty() && self.height > 0
382    }
383
384    pub fn as_str(&self) -> String {
385        self.to_string()
386    }
387}
388
389impl<'a> fmt::Display for FIGure<'a> {
390    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
391        if self.is_not_empty() {
392            for line in &self.lines {
393                writeln!(f, "{}", line)?;
394            }
395            Ok(())
396        } else {
397            write!(f, "")
398        }
399    }
400}
401
402struct Renderer<'a> {
403    header_line: &'a HeaderLine,
404    prev_char_width: usize,
405    cur_char_width: usize,
406    max_smush: usize,
407}
408
409impl<'a> Renderer<'a> {
410    fn new(header_line: &'a HeaderLine, _fonts: &'a HashMap<u32, FIGcharacter>) -> Self {
411        Self {
412            header_line,
413            prev_char_width: 0,
414            cur_char_width: 0,
415            max_smush: 0,
416        }
417    }
418
419    fn render(mut self, characters: &[&FIGcharacter]) -> Vec<String> {
420        let mut buffer = vec![String::new(); self.header_line.height as usize];
421        for character in characters {
422            self.cur_char_width = character.width as usize;
423            self.max_smush = self.smush_amount(&buffer, character);
424
425            for (row, buffer_row) in buffer.iter_mut().enumerate() {
426                self.add_char_row_to_buffer_row(buffer_row, &character.characters[row]);
427            }
428
429            self.prev_char_width = self.cur_char_width;
430        }
431
432        buffer
433            .into_iter()
434            .map(|line| line.replace(self.header_line.hardblank, " "))
435            .collect()
436    }
437
438    fn add_char_row_to_buffer_row(&self, buffer_row: &mut String, char_row: &str) {
439        let (mut left, right) = if self.header_line.is_right_to_left() {
440            (
441                char_row.chars().collect::<Vec<_>>(),
442                buffer_row.chars().collect::<Vec<_>>(),
443            )
444        } else {
445            (
446                buffer_row.chars().collect::<Vec<_>>(),
447                char_row.chars().collect::<Vec<_>>(),
448            )
449        };
450
451        for i in 0..self.max_smush {
452            let idx = left.len() as isize - self.max_smush as isize + i as isize;
453            let left_ch = if idx >= 0 {
454                left.get(idx as usize).copied().unwrap_or('\0')
455            } else {
456                '\0'
457            };
458            let right_ch = right.get(i).copied().unwrap_or('\0');
459            if let Some(smushed) = self.smush_chars(left_ch, right_ch) {
460                if idx >= 0 {
461                    left[idx as usize] = smushed;
462                }
463            }
464        }
465
466        left.extend(right.into_iter().skip(self.max_smush));
467        *buffer_row = left.into_iter().collect();
468    }
469
470    fn smush_amount(&self, buffer: &[String], character: &FIGcharacter) -> usize {
471        let layout = self.header_line.effective_layout();
472        if (layout & (SM_SMUSH | SM_KERN)) == 0 {
473            return 0;
474        }
475
476        let mut max_smush = self.cur_char_width;
477        for (row, buffer_row) in buffer
478            .iter()
479            .enumerate()
480            .take(self.header_line.height as usize)
481        {
482            let (line_left, line_right) = if self.header_line.is_right_to_left() {
483                (&character.characters[row], buffer_row)
484            } else {
485                (buffer_row, &character.characters[row])
486            };
487
488            let left_chars: Vec<char> = line_left.chars().collect();
489            let right_chars: Vec<char> = line_right.chars().collect();
490
491            let trimmed_left_len = left_chars
492                .iter()
493                .rposition(|ch| *ch != ' ')
494                .map_or(0, |idx| idx + 1);
495            let linebd = trimmed_left_len.saturating_sub(1);
496            let ch1 = if trimmed_left_len == 0 {
497                '\0'
498            } else {
499                left_chars[linebd]
500            };
501
502            let charbd = right_chars
503                .iter()
504                .position(|ch| *ch != ' ')
505                .unwrap_or(right_chars.len());
506            let ch2 = if charbd < right_chars.len() {
507                right_chars[charbd]
508            } else {
509                '\0'
510            };
511
512            let mut amount = charbd as isize + left_chars.len() as isize - 1 - linebd as isize;
513            if ch1 == '\0' || ch1 == ' ' || (ch2 != '\0' && self.smush_chars(ch1, ch2).is_some()) {
514                amount += 1;
515            }
516
517            max_smush = max_smush.min(amount.max(0) as usize);
518        }
519
520        max_smush
521    }
522
523    pub(crate) fn smush_chars(&self, left: char, right: char) -> Option<char> {
524        if left == ' ' {
525            return Some(right);
526        }
527        if right == ' ' {
528            return Some(left);
529        }
530        if left == '\0' || right == '\0' {
531            return None;
532        }
533        if self.prev_char_width < 2 || self.cur_char_width < 2 {
534            return None;
535        }
536
537        let layout = self.header_line.effective_layout();
538        if (layout & SM_SMUSH) == 0 {
539            return None;
540        }
541
542        if (layout & 63) == 0 {
543            if left == self.header_line.hardblank {
544                return Some(right);
545            }
546            if right == self.header_line.hardblank {
547                return Some(left);
548            }
549
550            return if self.header_line.is_right_to_left() {
551                Some(left)
552            } else {
553                Some(right)
554            };
555        }
556
557        if (layout & SM_HARDBLANK) != 0
558            && left == self.header_line.hardblank
559            && right == self.header_line.hardblank
560        {
561            return Some(left);
562        }
563        if left == self.header_line.hardblank || right == self.header_line.hardblank {
564            return None;
565        }
566        if (layout & SM_EQUAL) != 0 && left == right {
567            return Some(left);
568        }
569        if (layout & SM_LOWLINE) != 0 {
570            if left == '_' && "|/\\[]{}()<>".contains(right) {
571                return Some(right);
572            }
573            if right == '_' && "|/\\[]{}()<>".contains(left) {
574                return Some(left);
575            }
576        }
577        if (layout & SM_HIERARCHY) != 0 {
578            for (a, b) in [
579                ("|", "/\\[]{}()<>"),
580                ("/\\", "[]{}()<>"),
581                ("[]", "{}()<>"),
582                ("{}", "()<>"),
583                ("()", "<>"),
584            ] {
585                if a.contains(left) && b.contains(right) {
586                    return Some(right);
587                }
588                if a.contains(right) && b.contains(left) {
589                    return Some(left);
590                }
591            }
592        }
593        if (layout & SM_PAIR) != 0 {
594            let pair = [left, right];
595            let reversed = [right, left];
596            if pair == ['[', ']']
597                || pair == ['{', '}']
598                || pair == ['(', ')']
599                || reversed == ['[', ']']
600                || reversed == ['{', '}']
601                || reversed == ['(', ')']
602            {
603                return Some('|');
604            }
605        }
606        if (layout & SM_BIGX) != 0 {
607            if left == '/' && right == '\\' {
608                return Some('|');
609            }
610            if left == '\\' && right == '/' {
611                return Some('Y');
612            }
613            if left == '>' && right == '<' {
614                return Some('X');
615            }
616        }
617
618        None
619    }
620}