1#![doc(
50 html_logo_url = "https://mosaiclang.dev/assets/A4.svg",
51 html_favicon_url = "https://mosaiclang.dev/assets/A4.svg"
52)]
53#![deny(missing_docs)]
54
55use std::borrow::Cow;
56use std::error::Error;
57use std::fmt;
58
59#[derive(Debug, Clone, Copy, Default, PartialEq)]
80pub struct BBox {
81 pub llx: f32,
83 pub lly: f32,
85 pub urx: f32,
87 pub ury: f32,
89}
90
91#[derive(Debug, Clone, PartialEq)]
115pub struct CharacterMetric<'a> {
116 pub code: i32,
120 pub name: Cow<'a, str>,
122 pub width_x: f32,
124 pub bbox: Option<BBox>,
126}
127
128#[derive(Debug, Clone, PartialEq)]
146pub struct KerningPair<'a> {
147 pub left: Cow<'a, str>,
149 pub right: Cow<'a, str>,
151 pub adjust: f32,
155}
156
157#[derive(Debug, Clone, PartialEq)]
177pub struct FontMetrics<'a> {
178 pub font_name: Cow<'a, str>,
180 pub full_name: Cow<'a, str>,
182 pub family_name: Cow<'a, str>,
184 pub weight: Cow<'a, str>,
186 pub italic_angle: f32,
188 pub is_fixed_pitch: bool,
190 pub font_bbox: BBox,
192 pub underline_position: f32,
194 pub underline_thickness: f32,
196 pub cap_height: f32,
198 pub x_height: f32,
200 pub ascender: f32,
202 pub descender: f32,
204 pub encoding_scheme: Cow<'a, str>,
206 pub character_metrics: Cow<'a, [CharacterMetric<'a>]>,
211 pub kerning_pairs: Cow<'a, [KerningPair<'a>]>,
216}
217
218pub type OwnedFontMetrics = FontMetrics<'static>;
234
235#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum ParseError {
248 MissingHeader {
250 line: usize,
252 },
253 UnsupportedVersion {
255 line: usize,
257 version: String,
259 },
260 MissingRequiredField {
263 field: &'static str,
265 },
266 InvalidNumber {
268 line: usize,
270 field: &'static str,
272 value: String,
274 },
275 MalformedRecord {
278 line: usize,
280 keyword: &'static str,
282 reason: &'static str,
284 },
285}
286
287impl fmt::Display for ParseError {
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 match self {
290 Self::MissingHeader { line } => {
291 write!(f, "line {line}: expected StartFontMetrics header")
292 }
293 Self::UnsupportedVersion { line, version } => {
294 write!(
295 f,
296 "line {line}: unsupported AFM version {version:?} (need 4.x)"
297 )
298 }
299 Self::MissingRequiredField { field } => {
300 write!(f, "missing required field {field}")
301 }
302 Self::InvalidNumber { line, field, value } => {
303 write!(f, "line {line}: invalid number {value:?} for {field}")
304 }
305 Self::MalformedRecord {
306 line,
307 keyword,
308 reason,
309 } => {
310 write!(f, "line {line}: malformed {keyword} record: {reason}")
311 }
312 }
313 }
314}
315
316impl Error for ParseError {}
317
318impl CharacterMetric<'_> {
321 #[must_use]
341 pub fn into_owned(self) -> CharacterMetric<'static> {
342 CharacterMetric {
343 code: self.code,
344 name: Cow::Owned(self.name.into_owned()),
345 width_x: self.width_x,
346 bbox: self.bbox,
347 }
348 }
349}
350
351impl KerningPair<'_> {
352 #[must_use]
371 pub fn into_owned(self) -> KerningPair<'static> {
372 KerningPair {
373 left: Cow::Owned(self.left.into_owned()),
374 right: Cow::Owned(self.right.into_owned()),
375 adjust: self.adjust,
376 }
377 }
378}
379
380impl FontMetrics<'_> {
381 #[must_use]
399 pub fn into_owned(self) -> OwnedFontMetrics {
400 let chars: Vec<CharacterMetric<'static>> = self
401 .character_metrics
402 .into_owned()
403 .into_iter()
404 .map(CharacterMetric::into_owned)
405 .collect();
406 let kerns: Vec<KerningPair<'static>> = self
407 .kerning_pairs
408 .into_owned()
409 .into_iter()
410 .map(KerningPair::into_owned)
411 .collect();
412 FontMetrics {
413 font_name: Cow::Owned(self.font_name.into_owned()),
414 full_name: Cow::Owned(self.full_name.into_owned()),
415 family_name: Cow::Owned(self.family_name.into_owned()),
416 weight: Cow::Owned(self.weight.into_owned()),
417 italic_angle: self.italic_angle,
418 is_fixed_pitch: self.is_fixed_pitch,
419 font_bbox: self.font_bbox,
420 underline_position: self.underline_position,
421 underline_thickness: self.underline_thickness,
422 cap_height: self.cap_height,
423 x_height: self.x_height,
424 ascender: self.ascender,
425 descender: self.descender,
426 encoding_scheme: Cow::Owned(self.encoding_scheme.into_owned()),
427 character_metrics: Cow::Owned(chars),
428 kerning_pairs: Cow::Owned(kerns),
429 }
430 }
431}
432
433#[derive(Copy, Clone, Eq, PartialEq, Debug)]
436enum State {
437 Top,
438 CharMetrics,
439 KernPairs,
440 SkipKernPairs,
444}
445
446#[derive(Copy, Clone, Eq, PartialEq, Debug)]
447enum HeaderState {
448 Pending,
449 Seen,
450}
451
452#[derive(Copy, Clone, Eq, PartialEq, Debug)]
453enum DirectionState {
454 Reading,
455 Skipping,
456}
457
458#[derive(Copy, Clone, Eq, PartialEq, Debug)]
459enum FinishState {
460 Reading,
461 Done,
462}
463
464#[derive(Copy, Clone, Eq, PartialEq, Debug)]
465enum Presence {
466 Missing,
467 Present,
468}
469
470struct ParseAccumulator<'a> {
471 header: HeaderState,
472 state: State,
473 composites_depth: u32,
474 direction: DirectionState,
475 finish: FinishState,
476 font_name: Cow<'a, str>,
477 full_name: Cow<'a, str>,
478 family_name: Cow<'a, str>,
479 weight: Cow<'a, str>,
480 encoding_scheme: Cow<'a, str>,
481 italic_angle: f32,
482 is_fixed_pitch: bool,
483 font_bbox: BBox,
484 font_bbox_presence: Presence,
485 underline_position: f32,
486 underline_thickness: f32,
487 cap_height: f32,
488 x_height: f32,
489 ascender: f32,
490 descender: f32,
491 chars: Vec<CharacterMetric<'a>>,
492 kerns: Vec<KerningPair<'a>>,
493}
494
495impl<'a> ParseAccumulator<'a> {
496 fn new() -> Self {
497 Self {
498 header: HeaderState::Pending,
499 state: State::Top,
500 composites_depth: 0,
501 direction: DirectionState::Reading,
502 finish: FinishState::Reading,
503 font_name: Cow::Borrowed(""),
504 full_name: Cow::Borrowed(""),
505 family_name: Cow::Borrowed(""),
506 weight: Cow::Borrowed(""),
507 encoding_scheme: Cow::Borrowed(""),
508 italic_angle: 0.0,
509 is_fixed_pitch: false,
510 font_bbox: BBox::default(),
511 font_bbox_presence: Presence::Missing,
512 underline_position: 0.0,
513 underline_thickness: 0.0,
514 cap_height: 0.0,
515 x_height: 0.0,
516 ascender: 0.0,
517 descender: 0.0,
518 chars: Vec::new(),
519 kerns: Vec::new(),
520 }
521 }
522
523 fn parse_line(&mut self, raw: &'a str, lineno: usize) -> Result<(), ParseError> {
524 let line = raw.trim();
525 if line.is_empty() || self.is_done() {
526 return Ok(());
527 }
528 let (kw, rest) = split_keyword(line);
529
530 if self.skip_block_line(kw) || self.parse_header_line(kw, rest, lineno)? {
531 return Ok(());
532 }
533
534 self.parse_body_line(line, kw, rest, lineno)
535 }
536
537 fn finish(self) -> Result<FontMetrics<'a>, ParseError> {
538 if self.header == HeaderState::Pending {
539 return Err(ParseError::MissingHeader { line: 1 });
540 }
541 if self.font_name.is_empty() {
542 return Err(ParseError::MissingRequiredField { field: "FontName" });
543 }
544 if self.font_bbox_presence == Presence::Missing {
545 return Err(ParseError::MissingRequiredField { field: "FontBBox" });
546 }
547
548 Ok(FontMetrics {
549 font_name: self.font_name,
550 full_name: self.full_name,
551 family_name: self.family_name,
552 weight: self.weight,
553 italic_angle: self.italic_angle,
554 is_fixed_pitch: self.is_fixed_pitch,
555 font_bbox: self.font_bbox,
556 underline_position: self.underline_position,
557 underline_thickness: self.underline_thickness,
558 cap_height: self.cap_height,
559 x_height: self.x_height,
560 ascender: self.ascender,
561 descender: self.descender,
562 encoding_scheme: self.encoding_scheme,
563 character_metrics: Cow::Owned(self.chars),
564 kerning_pairs: Cow::Owned(self.kerns),
565 })
566 }
567
568 fn skip_block_line(&mut self, kw: &str) -> bool {
569 if self.composites_depth > 0 {
570 if kw == "EndComposites" {
571 self.composites_depth -= 1;
572 }
573 return true;
574 }
575 if self.direction == DirectionState::Skipping {
576 if kw == "EndDirection" {
577 self.direction = DirectionState::Reading;
578 }
579 return true;
580 }
581 false
582 }
583
584 fn parse_header_line(
585 &mut self,
586 kw: &str,
587 rest: &str,
588 lineno: usize,
589 ) -> Result<bool, ParseError> {
590 if self.header == HeaderState::Seen {
591 return Ok(false);
592 }
593 if kw == "Comment" {
594 return Ok(true);
595 }
596 if kw != "StartFontMetrics" {
597 return Err(ParseError::MissingHeader { line: lineno });
598 }
599 let version = rest.trim();
600 let is_v4 = version.split_once('.').is_some_and(|(major, minor)| {
601 major == "4" && !minor.is_empty() && minor.bytes().all(|b| b.is_ascii_digit())
602 });
603 if !is_v4 {
604 return Err(ParseError::UnsupportedVersion {
605 line: lineno,
606 version: version.to_owned(),
607 });
608 }
609 self.header = HeaderState::Seen;
610 Ok(true)
611 }
612
613 fn is_done(&self) -> bool {
614 self.finish == FinishState::Done
615 }
616
617 fn parse_body_line(
618 &mut self,
619 line: &'a str,
620 kw: &str,
621 rest: &'a str,
622 lineno: usize,
623 ) -> Result<(), ParseError> {
624 match kw {
625 "EndFontMetrics" => self.finish = FinishState::Done,
626 "StartComposites" => self.composites_depth = 1,
627 "FontName" => self.font_name = Cow::Borrowed(rest.trim()),
628 "FullName" => self.full_name = Cow::Borrowed(rest.trim()),
629 "FamilyName" => self.family_name = Cow::Borrowed(rest.trim()),
630 "Weight" => self.weight = Cow::Borrowed(rest.trim()),
631 "EncodingScheme" => self.encoding_scheme = Cow::Borrowed(rest.trim()),
632 "ItalicAngle" => self.italic_angle = parse_f32(rest, "ItalicAngle", lineno)?,
633 "IsFixedPitch" => self.is_fixed_pitch = parse_bool(rest, lineno)?,
634 "UnderlinePosition" => {
635 self.underline_position = parse_f32(rest, "UnderlinePosition", lineno)?;
636 }
637 "UnderlineThickness" => {
638 self.underline_thickness = parse_f32(rest, "UnderlineThickness", lineno)?;
639 }
640 "CapHeight" => self.cap_height = parse_f32(rest, "CapHeight", lineno)?,
641 "XHeight" => self.x_height = parse_f32(rest, "XHeight", lineno)?,
642 "Ascender" => self.ascender = parse_f32(rest, "Ascender", lineno)?,
643 "Descender" => self.descender = parse_f32(rest, "Descender", lineno)?,
644 "FontBBox" => {
645 self.font_bbox = parse_bbox(rest, "FontBBox", lineno)?;
646 self.font_bbox_presence = Presence::Present;
647 }
648 "StartCharMetrics" => self.start_char_metrics(rest, lineno)?,
649 "EndCharMetrics" | "EndKernPairs" | "EndKernData" => self.state = State::Top,
650 "StartKernData" => self.state = State::KernPairs,
651 "StartKernPairs" | "StartKernPairs0" => self.start_kern_pairs(rest, lineno)?,
652 "StartKernPairs1" => self.state = State::SkipKernPairs,
653 "StartDirection" => self.start_direction(rest, lineno)?,
654 "C" | "CH" if self.state == State::CharMetrics => {
655 self.chars.push(parse_char_metric_line(line, lineno)?);
656 }
657 "KPX" | "KPY" | "KP" if self.state == State::KernPairs => {
658 if let Some(pair) = parse_kern_record(kw, rest, lineno)? {
659 self.kerns.push(pair);
660 }
661 }
662 "KPH" if self.state == State::KernPairs => {}
663 _ => {}
664 }
665 Ok(())
666 }
667
668 fn start_char_metrics(&mut self, rest: &str, lineno: usize) -> Result<(), ParseError> {
669 let n = parse_declared_count(rest, "StartCharMetrics", lineno)?;
670 self.chars
671 .try_reserve(n)
672 .map_err(|_err| ParseError::MalformedRecord {
673 line: lineno,
674 keyword: "StartCharMetrics",
675 reason: "declared count exceeds allocatable capacity",
676 })?;
677 self.state = State::CharMetrics;
678 Ok(())
679 }
680
681 fn start_kern_pairs(&mut self, rest: &str, lineno: usize) -> Result<(), ParseError> {
682 let n = parse_declared_count(rest, "StartKernPairs", lineno)?;
683 self.kerns
684 .try_reserve(n)
685 .map_err(|_err| ParseError::MalformedRecord {
686 line: lineno,
687 keyword: "StartKernPairs",
688 reason: "declared count exceeds allocatable capacity",
689 })?;
690 self.state = State::KernPairs;
691 Ok(())
692 }
693
694 fn start_direction(&mut self, rest: &str, lineno: usize) -> Result<(), ParseError> {
695 let value = rest.trim();
696 let direction = value
697 .parse::<u8>()
698 .map_err(|_err| ParseError::InvalidNumber {
699 line: lineno,
700 field: "StartDirection",
701 value: value.to_owned(),
702 })?;
703 match direction {
704 0 | 2 => {}
705 1 => self.direction = DirectionState::Skipping,
706 _ => {
707 return Err(ParseError::MalformedRecord {
708 line: lineno,
709 keyword: "StartDirection",
710 reason: "expected direction selector 0, 1, or 2",
711 });
712 }
713 }
714 Ok(())
715 }
716}
717
718fn parse_declared_count(
719 rest: &str,
720 field: &'static str,
721 lineno: usize,
722) -> Result<usize, ParseError> {
723 let value = rest.trim();
724 value
725 .parse::<usize>()
726 .map_err(|_err| ParseError::InvalidNumber {
727 line: lineno,
728 field,
729 value: value.to_owned(),
730 })
731}
732
733#[must_use = "discarding the parsed FontMetrics also discards any parse error"]
758pub fn parse(src: &str) -> Result<FontMetrics<'_>, ParseError> {
759 let mut accumulator = ParseAccumulator::new();
760
761 for (idx, raw) in src.lines().enumerate() {
762 accumulator.parse_line(raw, idx + 1)?;
763 if accumulator.is_done() {
764 break;
765 }
766 }
767
768 accumulator.finish()
769}
770
771fn split_keyword(line: &str) -> (&str, &str) {
774 line.find(|c: char| c.is_ascii_whitespace())
775 .map_or((line, ""), |i| (&line[..i], &line[i..]))
776}
777
778fn parse_f32(s: &str, field: &'static str, lineno: usize) -> Result<f32, ParseError> {
779 let trimmed = s.trim();
780 trimmed
781 .parse::<f32>()
782 .map_err(|_e| ParseError::InvalidNumber {
783 line: lineno,
784 field,
785 value: trimmed.to_owned(),
786 })
787}
788
789fn parse_i32(s: &str, field: &'static str, lineno: usize) -> Result<i32, ParseError> {
790 let trimmed = s.trim();
791 trimmed
792 .parse::<i32>()
793 .map_err(|_e| ParseError::InvalidNumber {
794 line: lineno,
795 field,
796 value: trimmed.to_owned(),
797 })
798}
799
800fn parse_bool(s: &str, lineno: usize) -> Result<bool, ParseError> {
801 match s.trim() {
802 "true" => Ok(true),
803 "false" => Ok(false),
804 _ => Err(ParseError::MalformedRecord {
805 line: lineno,
806 keyword: "IsFixedPitch",
807 reason: "expected `true` or `false`",
808 }),
809 }
810}
811
812fn parse_bbox(s: &str, field: &'static str, lineno: usize) -> Result<BBox, ParseError> {
813 let mut toks = s.split_ascii_whitespace();
814 let llx = next_f32(&mut toks, field, lineno)?;
815 let lly = next_f32(&mut toks, field, lineno)?;
816 let urx = next_f32(&mut toks, field, lineno)?;
817 let ury = next_f32(&mut toks, field, lineno)?;
818 if toks.next().is_some() {
819 return Err(ParseError::MalformedRecord {
820 line: lineno,
821 keyword: field,
822 reason: "too many numbers",
823 });
824 }
825 Ok(BBox { llx, lly, urx, ury })
826}
827
828fn next_f32(
829 toks: &mut std::str::SplitAsciiWhitespace<'_>,
830 field: &'static str,
831 lineno: usize,
832) -> Result<f32, ParseError> {
833 let t = toks.next().ok_or(ParseError::MalformedRecord {
834 line: lineno,
835 keyword: field,
836 reason: "expected number",
837 })?;
838 parse_f32(t, field, lineno)
839}
840
841fn parse_char_metric_line(line: &str, lineno: usize) -> Result<CharacterMetric<'_>, ParseError> {
842 let mut code: i32 = -1;
843 let mut name: &str = "";
844 let mut width_x: f32 = 0.0;
845 let mut bbox: Option<BBox> = None;
846
847 for seg in line.split(';') {
848 let seg = seg.trim();
849 if seg.is_empty() {
850 continue;
851 }
852 let (tok, rest) = split_keyword(seg);
853 let rest = rest.trim();
854 match tok {
855 "C" => code = parse_i32(rest, "C", lineno)?,
856 "CH" => {
857 let hex = rest.trim_start_matches('<').trim_end_matches('>').trim();
858 code = i32::from_str_radix(hex, 16).map_err(|_e| ParseError::InvalidNumber {
859 line: lineno,
860 field: "CH",
861 value: rest.to_owned(),
862 })?;
863 }
864 "WX" | "W0X" => width_x = parse_f32(rest, "WX", lineno)?,
865 "W" | "W0" => {
866 let x =
867 rest.split_ascii_whitespace()
868 .next()
869 .ok_or(ParseError::MalformedRecord {
870 line: lineno,
871 keyword: "W",
872 reason: "missing x advance",
873 })?;
874 width_x = parse_f32(x, "W", lineno)?;
875 }
876 "N" => name = rest,
877 "B" => bbox = Some(parse_bbox(rest, "B", lineno)?),
878 _ => {} }
880 }
881
882 Ok(CharacterMetric {
883 code,
884 name: Cow::Borrowed(name),
885 width_x,
886 bbox,
887 })
888}
889
890fn parse_kern_record<'a>(
891 kw: &str,
892 rest: &'a str,
893 lineno: usize,
894) -> Result<Option<KerningPair<'a>>, ParseError> {
895 let keyword = match kw {
898 "KPX" => "KPX",
899 "KPY" => "KPY",
900 "KP" => "KP",
901 _ => return Ok(None),
902 };
903 let mut toks = rest.split_ascii_whitespace();
904 let left = toks.next().ok_or(ParseError::MalformedRecord {
905 line: lineno,
906 keyword,
907 reason: "missing left glyph name",
908 })?;
909 let right = toks.next().ok_or(ParseError::MalformedRecord {
910 line: lineno,
911 keyword,
912 reason: "missing right glyph name",
913 })?;
914 let first_num = toks.next().ok_or(ParseError::MalformedRecord {
915 line: lineno,
916 keyword,
917 reason: "missing kern adjustment",
918 })?;
919 let adjust = match keyword {
920 "KPX" => parse_f32(first_num, "KPX", lineno)?,
921 "KPY" => {
922 let _ = parse_f32(first_num, "KPY", lineno)?;
925 0.0
926 }
927 "KP" => {
928 let x = parse_f32(first_num, "KP", lineno)?;
930 let y = toks.next().ok_or(ParseError::MalformedRecord {
931 line: lineno,
932 keyword,
933 reason: "missing y kern adjustment",
934 })?;
935 let _ = parse_f32(y, "KP", lineno)?;
936 x
937 }
938 _ => return Ok(None),
940 };
941 if toks.next().is_some() {
942 return Err(ParseError::MalformedRecord {
943 line: lineno,
944 keyword,
945 reason: "too many operands",
946 });
947 }
948 Ok(Some(KerningPair {
949 left: Cow::Borrowed(left),
950 right: Cow::Borrowed(right),
951 adjust,
952 }))
953}