Skip to main content

adobe_font_metrics/
parser.rs

1use std::borrow::Cow;
2
3use crate::records::{
4    self, bbox, boolean, integer, keyword, malformed, number, source_record, vector,
5};
6use crate::{Direction, DirectionMetrics, FontMetrics, MetricsSets, ParseError, RecordContext};
7
8#[derive(Debug, Clone, Copy)]
9struct Count {
10    expected: usize,
11    seen: usize,
12    keyword: &'static str,
13}
14
15impl Count {
16    fn new(
17        value: &str,
18        keyword: &'static str,
19        line: usize,
20        input_len: usize,
21    ) -> Result<Self, ParseError> {
22        let expected = integer(value, keyword, line)?;
23        // A record consumes at least one input byte. Never allocate from an untrusted count.
24        if expected > input_len {
25            return Err(malformed(
26                line,
27                keyword,
28                "declared count exceeds input size",
29            ));
30        }
31        Ok(Self {
32            expected,
33            seen: 0,
34            keyword,
35        })
36    }
37    fn add(&mut self, line: usize) -> Result<(), ParseError> {
38        self.seen += 1;
39        if self.seen > self.expected {
40            return Err(malformed(line, self.keyword, "more records than declared"));
41        }
42        Ok(())
43    }
44    fn close(self, line: usize) -> Result<(), ParseError> {
45        if self.seen != self.expected {
46            return Err(malformed(
47                line,
48                self.keyword,
49                "record count does not match declaration",
50            ));
51        }
52        Ok(())
53    }
54}
55
56#[derive(Debug, Clone, Copy)]
57enum Section {
58    Font,
59    Direction(MetricsSets),
60    Characters(Count),
61    Pairs(Direction, Count),
62    Tracks(Count),
63    Composites(Count),
64}
65
66struct Reader<'a> {
67    font: FontMetrics<'a>,
68    section: Section,
69    header_seen: bool,
70    bbox_seen: bool,
71    kern_data: bool,
72    finished: bool,
73    input_len: usize,
74    direction_lines: [usize; 2],
75    vector_line: usize,
76}
77
78impl<'a> Reader<'a> {
79    fn new(input_len: usize) -> Self {
80        Self {
81            font: FontMetrics::default(),
82            section: Section::Font,
83            header_seen: false,
84            bbox_seen: false,
85            kern_data: false,
86            finished: false,
87            input_len,
88            direction_lines: [1; 2],
89            vector_line: 1,
90        }
91    }
92
93    fn context(&self) -> RecordContext {
94        match self.section {
95            Section::Font if self.kern_data => RecordContext::KernData,
96            Section::Font => RecordContext::Font,
97            Section::Direction(direction) => RecordContext::Direction(direction),
98            Section::Characters(_) => RecordContext::CharacterMetrics,
99            Section::Pairs(direction, _) => RecordContext::KernPairs(direction),
100            Section::Tracks(_) => RecordContext::TrackKern,
101            Section::Composites(_) => RecordContext::Composites,
102        }
103    }
104
105    fn retain(&mut self, line: usize, key: &'a str, value: &'a str) {
106        let context = self.context();
107        source_record(self.font.source_records.to_mut(), line, context, key, value);
108    }
109
110    fn line(&mut self, raw: &'a str, line: usize) -> Result<(), ParseError> {
111        let text = raw.trim();
112        if text.is_empty() {
113            return Ok(());
114        }
115        if self.finished {
116            return Err(malformed(
117                line,
118                "EndFontMetrics",
119                "unexpected trailing data",
120            ));
121        }
122        let (key, value) = keyword(text);
123        if key == "Comment" {
124            self.retain(line, key, value);
125            return Ok(());
126        }
127        if !self.header_seen {
128            if key != "StartFontMetrics" {
129                return Err(ParseError::MissingHeader { line });
130            }
131            if !value.split_once('.').is_some_and(|(major, minor)| {
132                major == "4" && !minor.is_empty() && minor.bytes().all(|b| b.is_ascii_digit())
133            }) {
134                return Err(ParseError::UnsupportedVersion {
135                    line,
136                    version: value.to_owned(),
137                });
138            }
139            self.header_seen = true;
140            self.font.afm_version = Cow::Borrowed(value);
141            return Ok(());
142        }
143        if self.control(key, value, line)? {
144            return Ok(());
145        }
146        match self.section {
147            Section::Font if !self.kern_data => {
148                if !self.global(key, value, line)?
149                    && !self.direction_metric(key, value, line, MetricsSets::Zero)?
150                {
151                    self.extra_or_misplaced(key, value, line)?;
152                }
153            }
154            Section::Direction(selector) => {
155                if !self.direction_metric(key, value, line, selector)? {
156                    self.extra_or_misplaced(key, value, line)?;
157                }
158            }
159            Section::Characters(_) => {
160                if character_line(text) {
161                    let character = records::character(
162                        text,
163                        line,
164                        self.font.character_metrics.len(),
165                        self.font.source_records.to_mut(),
166                    )?;
167                    self.font.character_metrics.to_mut().push(character);
168                    if let Section::Characters(count) = &mut self.section {
169                        count.add(line)?;
170                    }
171                } else {
172                    self.extra_or_misplaced(key, value, line)?;
173                }
174            }
175            Section::Pairs(direction, _) if matches!(key, "KP" | "KPX" | "KPY" | "KPH") => {
176                self.font
177                    .kerning_pairs
178                    .to_mut()
179                    .push(records::pair(key, value, line, direction)?);
180                if let Section::Pairs(_, count) = &mut self.section {
181                    count.add(line)?;
182                }
183            }
184            Section::Tracks(_) if key == "TrackKern" => {
185                self.font
186                    .track_kerns
187                    .to_mut()
188                    .push(records::track(value, line)?);
189                if let Section::Tracks(count) = &mut self.section {
190                    count.add(line)?;
191                }
192            }
193            Section::Composites(_) if key == "CC" => {
194                let composite = records::composite(
195                    text,
196                    line,
197                    self.font.composites.len(),
198                    self.font.source_records.to_mut(),
199                )?;
200                self.font.composites.to_mut().push(composite);
201                if let Section::Composites(count) = &mut self.section {
202                    count.add(line)?;
203                }
204            }
205            _ => self.extra_or_misplaced(key, value, line)?,
206        }
207        Ok(())
208    }
209
210    fn extra_or_misplaced(
211        &mut self,
212        key: &'a str,
213        value: &'a str,
214        line: usize,
215    ) -> Result<(), ParseError> {
216        if records::known_record(key) {
217            return Err(malformed(
218                line,
219                "section",
220                "modeled record in an incompatible section",
221            ));
222        }
223        self.retain(line, key, value);
224        Ok(())
225    }
226
227    fn require_font_section(
228        &self,
229        key: &'static str,
230        line: usize,
231        allow_kern: bool,
232    ) -> Result<(), ParseError> {
233        if !matches!(self.section, Section::Font) || (!allow_kern && self.kern_data) {
234            return Err(malformed(line, key, "incompatible or unclosed section"));
235        }
236        Ok(())
237    }
238
239    fn control(&mut self, key: &str, value: &str, line: usize) -> Result<bool, ParseError> {
240        match key {
241            "StartFontMetrics" => {
242                return Err(malformed(line, "StartFontMetrics", "duplicate header"));
243            }
244            "EndFontMetrics" => {
245                self.require_font_section("EndFontMetrics", line, false)?;
246                records::operands::<0>(value, "EndFontMetrics", line)?;
247                self.finished = true;
248            }
249            "StartDirection" => {
250                self.require_font_section("StartDirection", line, false)?;
251                self.section = Section::Direction(selector(value, "StartDirection", line)?);
252            }
253            "EndDirection" => {
254                if !matches!(self.section, Section::Direction(_)) {
255                    return Err(malformed(line, "EndDirection", "no open direction section"));
256                }
257                records::operands::<0>(value, "EndDirection", line)?;
258                self.section = Section::Font;
259            }
260            "StartCharMetrics" => {
261                self.require_font_section("StartCharMetrics", line, false)?;
262                self.section = Section::Characters(Count::new(
263                    value,
264                    "StartCharMetrics",
265                    line,
266                    self.input_len,
267                )?);
268            }
269            "EndCharMetrics" => {
270                let Section::Characters(count) = self.section else {
271                    return Err(malformed(
272                        line,
273                        "EndCharMetrics",
274                        "no open character section",
275                    ));
276                };
277                records::operands::<0>(value, "EndCharMetrics", line)?;
278                count.close(line)?;
279                self.section = Section::Font;
280            }
281            "StartKernData" => {
282                self.require_font_section("StartKernData", line, false)?;
283                records::operands::<0>(value, "StartKernData", line)?;
284                self.kern_data = true;
285            }
286            "EndKernData" => {
287                self.require_font_section("EndKernData", line, true)?;
288                if !self.kern_data {
289                    return Err(malformed(line, "EndKernData", "no open kerning container"));
290                }
291                records::operands::<0>(value, "EndKernData", line)?;
292                self.kern_data = false;
293            }
294            "StartKernPairs" | "StartKernPairs0" | "StartKernPairs1" => {
295                self.require_font_section("StartKernPairs", line, true)?;
296                let (direction, field) = match key {
297                    "StartKernPairs0" => (Direction::Zero, "StartKernPairs0"),
298                    "StartKernPairs1" => (Direction::One, "StartKernPairs1"),
299                    _ => (Direction::Zero, "StartKernPairs"),
300                };
301                self.section =
302                    Section::Pairs(direction, Count::new(value, field, line, self.input_len)?);
303            }
304            "EndKernPairs" => {
305                let Section::Pairs(_, count) = self.section else {
306                    return Err(malformed(line, "EndKernPairs", "no open pair section"));
307                };
308                records::operands::<0>(value, "EndKernPairs", line)?;
309                count.close(line)?;
310                self.section = Section::Font;
311            }
312            "StartTrackKern" => {
313                self.require_font_section("StartTrackKern", line, true)?;
314                self.section =
315                    Section::Tracks(Count::new(value, "StartTrackKern", line, self.input_len)?);
316            }
317            "EndTrackKern" => {
318                let Section::Tracks(count) = self.section else {
319                    return Err(malformed(line, "EndTrackKern", "no open track section"));
320                };
321                records::operands::<0>(value, "EndTrackKern", line)?;
322                count.close(line)?;
323                self.section = Section::Font;
324            }
325            "StartComposites" => {
326                self.require_font_section("StartComposites", line, false)?;
327                self.section = Section::Composites(Count::new(
328                    value,
329                    "StartComposites",
330                    line,
331                    self.input_len,
332                )?);
333            }
334            "EndComposites" => {
335                let Section::Composites(count) = self.section else {
336                    return Err(malformed(
337                        line,
338                        "EndComposites",
339                        "no open composite section",
340                    ));
341                };
342                records::operands::<0>(value, "EndComposites", line)?;
343                count.close(line)?;
344                self.section = Section::Font;
345            }
346            _ => return Ok(false),
347        }
348        Ok(true)
349    }
350
351    fn direction_metric(
352        &mut self,
353        key: &str,
354        value: &str,
355        line: usize,
356        selector: MetricsSets,
357    ) -> Result<bool, ParseError> {
358        let mut changes = DirectionMetrics::default();
359        match key {
360            "UnderlinePosition" => {
361                changes.underline_position = Some(number(value, "UnderlinePosition", line)?);
362            }
363            "UnderlineThickness" => {
364                changes.underline_thickness = Some(number(value, "UnderlineThickness", line)?);
365            }
366            "ItalicAngle" => changes.italic_angle = Some(number(value, "ItalicAngle", line)?),
367            "CharWidth" => changes.char_width = Some(vector(value, "CharWidth", line)?),
368            "IsFixedPitch" => changes.is_fixed_pitch = Some(boolean(value, "IsFixedPitch", line)?),
369            _ => return Ok(false),
370        }
371        for direction in [Direction::Zero, Direction::One] {
372            if matches!(
373                (selector, direction),
374                (MetricsSets::Zero, Direction::One) | (MetricsSets::One, Direction::Zero)
375            ) {
376                continue;
377            }
378            let metrics = &mut self.font.directions[direction.index()];
379            metrics.underline_position = changes.underline_position.or(metrics.underline_position);
380            metrics.underline_thickness =
381                changes.underline_thickness.or(metrics.underline_thickness);
382            metrics.italic_angle = changes.italic_angle.or(metrics.italic_angle);
383            metrics.char_width = changes.char_width.or(metrics.char_width);
384            metrics.is_fixed_pitch = changes.is_fixed_pitch.or(metrics.is_fixed_pitch);
385            self.direction_lines[direction.index()] = line;
386        }
387        Ok(true)
388    }
389
390    fn global(&mut self, key: &'a str, value: &'a str, line: usize) -> Result<bool, ParseError> {
391        match key {
392            "FontName" => self.font.font_name = Cow::Borrowed(value),
393            "FontBBox" => {
394                self.font.font_bbox = bbox(value, "FontBBox", line)?;
395                self.bbox_seen = true;
396            }
397            "MetricsSets" => self.font.metrics_sets = Some(selector(value, "MetricsSets", line)?),
398            "VVector" => {
399                self.font.v_vector = Some(vector(value, "VVector", line)?);
400                self.vector_line = line;
401            }
402            "FullName" => self.font.full_name = Some(Cow::Borrowed(value)),
403            "FamilyName" => self.font.family_name = Some(Cow::Borrowed(value)),
404            "Weight" => self.font.weight = Some(Cow::Borrowed(value)),
405            "Version" => self.font.version = Some(Cow::Borrowed(value)),
406            "Notice" => self.font.notice = Some(Cow::Borrowed(value)),
407            "EncodingScheme" => self.font.encoding_scheme = Some(Cow::Borrowed(value)),
408            "CharacterSet" => self.font.character_set = Some(Cow::Borrowed(value)),
409            "MappingScheme" => {
410                self.font.mapping_scheme = Some(integer(value, "MappingScheme", line)?);
411            }
412            "EscChar" => self.font.esc_char = Some(integer(value, "EscChar", line)?),
413            "Characters" => self.font.characters = Some(integer(value, "Characters", line)?),
414            "IsBaseFont" => self.font.is_base_font = Some(boolean(value, "IsBaseFont", line)?),
415            "IsCIDFont" => self.font.is_cid_font = Some(boolean(value, "IsCIDFont", line)?),
416            "IsFixedV" => self.font.is_fixed_v = Some(boolean(value, "IsFixedV", line)?),
417            "CapHeight" => self.font.cap_height = Some(number(value, "CapHeight", line)?),
418            "XHeight" => self.font.x_height = Some(number(value, "XHeight", line)?),
419            "Ascender" => self.font.ascender = Some(number(value, "Ascender", line)?),
420            "Descender" => self.font.descender = Some(number(value, "Descender", line)?),
421            "StdHW" => self.font.std_hw = Some(number(value, "StdHW", line)?),
422            "StdVW" => self.font.std_vw = Some(number(value, "StdVW", line)?),
423            _ => return Ok(false),
424        }
425        Ok(true)
426    }
427
428    fn finish(self, line: usize) -> Result<FontMetrics<'a>, ParseError> {
429        if !self.header_seen {
430            return Err(ParseError::MissingHeader { line: 1 });
431        }
432        if self.font.font_name.is_empty() {
433            return Err(ParseError::MissingRequiredField { field: "FontName" });
434        }
435        if !self.bbox_seen {
436            return Err(ParseError::MissingRequiredField { field: "FontBBox" });
437        }
438        if !self.finished {
439            return Err(malformed(
440                line,
441                "EndFontMetrics",
442                "missing closing font marker",
443            ));
444        }
445        if self.font.mapping_scheme == Some(3) && self.font.esc_char.is_none() {
446            return Err(ParseError::MissingRequiredField { field: "EscChar" });
447        }
448        if self.font.is_fixed_v == Some(false) && self.font.v_vector.is_some() {
449            return Err(malformed(
450                self.vector_line,
451                "VVector",
452                "global vector conflicts with IsFixedV false",
453            ));
454        }
455        for direction in [Direction::Zero, Direction::One] {
456            let metrics = self.font.direction(direction);
457            if metrics.char_width.is_some() && metrics.is_fixed_pitch == Some(false) {
458                return Err(malformed(
459                    self.direction_lines[direction.index()],
460                    "CharWidth",
461                    "global width conflicts with IsFixedPitch false",
462                ));
463            }
464        }
465        Ok(self.font)
466    }
467}
468
469fn selector(value: &str, field: &'static str, line: usize) -> Result<MetricsSets, ParseError> {
470    match integer::<u8>(value, field, line)? {
471        0 => Ok(MetricsSets::Zero),
472        1 => Ok(MetricsSets::One),
473        2 => Ok(MetricsSets::Both),
474        _ => Err(malformed(
475            line,
476            field,
477            "expected direction selector 0, 1, or 2",
478        )),
479    }
480}
481
482fn character_line(text: &str) -> bool {
483    text.split(';').any(|part| {
484        matches!(
485            keyword(part).0,
486            "C" | "CH"
487                | "N"
488                | "B"
489                | "WX"
490                | "WY"
491                | "W0X"
492                | "W0Y"
493                | "W1X"
494                | "W1Y"
495                | "W"
496                | "W0"
497                | "W1"
498                | "VV"
499                | "L"
500        )
501    })
502}
503
504fn validate_text(source: &[u8]) -> Result<(), ParseError> {
505    let mut line = 1;
506    for (offset, &value) in source.iter().enumerate() {
507        if !matches!(value, b'\t' | b'\n' | b'\r' | 0x1b | 0x20..=0x7e) {
508            return Err(ParseError::InvalidByte {
509                offset,
510                line,
511                value,
512            });
513        }
514        if value == b'\n' {
515            line += 1;
516        }
517    }
518    Ok(())
519}
520
521fn read(source: &str) -> Result<FontMetrics<'_>, ParseError> {
522    let mut reader = Reader::new(source.len());
523    let mut line = 0;
524    for raw in source.lines() {
525        line += 1;
526        reader.line(raw, line)?;
527    }
528    reader.finish(line + 1)
529}
530
531/// Parse AFM v4.x text into borrowed font metrics.
532///
533/// Accepts LF/CRLF ASCII text and preserves comments and uninterpreted records.
534/// Optional fields retain absence. See the crate documentation for coverage.
535///
536/// # Errors
537/// Returns a source-located [`ParseError`] for invalid text, unsupported versions,
538/// missing required fields, malformed modeled records, or invalid section boundaries/counts.
539#[must_use = "parsing may return an error"]
540pub fn parse(source: &str) -> Result<FontMetrics<'_>, ParseError> {
541    validate_text(source.as_bytes())?;
542    read(source)
543}
544
545/// Parse AFM file bytes without a caller-side decoding step or lossy conversion.
546/// Names and text borrow the byte slice through a validated ASCII view.
547///
548/// # Errors
549/// Returns the same errors as [`parse`]. Invalid bytes report their zero-based
550/// byte offset and one-based source line through [`ParseError::InvalidByte`].
551#[must_use = "parsing may return an error"]
552pub fn parse_bytes(source: &[u8]) -> Result<FontMetrics<'_>, ParseError> {
553    validate_text(source)?;
554    let text = std::str::from_utf8(source).map_err(|error| ParseError::InvalidByte {
555        offset: error.valid_up_to(),
556        line: 1,
557        value: source[error.valid_up_to()],
558    })?;
559    read(text)
560}