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}