Skip to main content

fmd_font/
outline.rs

1//! Glyph outline decoding: `glyf` point data → quadratic-Bézier contours.
2//!
3//! TrueType outlines are already quadratic, so decoding is zero-loss: every
4//! on-curve point becomes an anchor, every off-curve point a control point,
5//! and runs of consecutive off-curve points get their implied on-curve
6//! midpoints synthesized exactly as the rasterizer contract specifies.
7//! Composite glyphs are assembled with their full transform semantics —
8//! F2Dot14 scale / 2×2 matrices, XY offsets (scaled only when
9//! `SCALED_COMPONENT_OFFSET` asks for it), and anchor-point matching — and
10//! metrics honor `USE_MY_METRICS` plus the `hmtx` phantom-point rules
11//! (left side bearing from `hmtx`, right side bearing derived from the
12//! advance and the `glyf` header bbox).
13//!
14//! Fonts are untrusted input: every read is bounds-checked, composite
15//! recursion is depth-limited, and total decoded points are budgeted, so a
16//! hostile font errors quickly instead of hanging or ballooning memory.
17
18use crate::{Font, be_i16, be_u16};
19
20/// Composite glyphs deeper than this are rejected. Real fonts nest two or
21/// three levels at most; the cap only exists to bound hostile recursion
22/// (including self-referential component cycles, which exhaust depth).
23pub const MAX_COMPOSITE_DEPTH: usize = 8;
24
25/// Hard ceiling on the points a single decoded glyph may accumulate across
26/// all its components. A well-formed glyph never exceeds the format's
27/// 65 536-point space; the budget stops quadratic blow-ups from aliased
28/// composite records.
29pub const MAX_OUTLINE_POINTS: usize = 65_536;
30
31/// Ceiling on component records walked while decoding one glyph (across
32/// the whole recursion), bounding work on hostile `MORE_COMPONENTS` chains.
33pub const MAX_COMPONENTS: usize = 512;
34
35/// A point in font design units. Simple glyphs decode to exact integer
36/// coordinates; composite transforms (F2Dot14 fractions) and synthesized
37/// midpoints introduce the fractional values `f64` carries exactly.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct Point {
40    /// X in font design units (advance direction).
41    pub x: f64,
42    /// Y in font design units (baseline-relative, y-up).
43    pub y: f64,
44}
45
46impl Point {
47    #[inline(always)]
48    fn midpoint(self, other: Point) -> Point {
49        Point {
50            x: (self.x + other.x) * 0.5,
51            y: (self.y + other.y) * 0.5,
52        }
53    }
54}
55
56/// One segment of a closed contour, starting from the previous segment's
57/// endpoint (or [`Contour::start`] for the first).
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum Segment {
60    /// A straight edge to `to`.
61    Line {
62        /// The segment endpoint.
63        to: Point,
64    },
65    /// A quadratic Bézier through control point `ctrl` to `to`.
66    Quad {
67        /// The off-curve control point.
68        ctrl: Point,
69        /// The on-curve endpoint.
70        to: Point,
71    },
72}
73
74impl Segment {
75    /// The segment's endpoint.
76    #[inline(always)]
77    #[must_use]
78    pub fn to(&self) -> Point {
79        match self {
80            Self::Line { to } | Self::Quad { to, .. } => *to,
81        }
82    }
83}
84
85/// A closed contour: `segments` walk from `start` back around to `start`
86/// (the final segment's endpoint always equals `start`). Winding direction
87/// is preserved from the font (TrueType fills non-zero).
88#[derive(Debug, Clone, PartialEq)]
89pub struct Contour {
90    /// The first on-curve anchor (synthesized as a midpoint when the raw
91    /// contour opens off-curve).
92    pub start: Point,
93    /// The closed segment loop.
94    pub segments: Vec<Segment>,
95}
96
97/// A decoded glyph: quadratic contours plus phantom-point-correct metrics.
98#[derive(Debug, Clone, PartialEq)]
99pub struct GlyphOutline {
100    /// The closed contours, in font order. Empty for blank glyphs (space).
101    pub contours: Vec<Contour>,
102    /// Advance width in design units, from `hmtx` — or from the flagged
103    /// component's `hmtx` entry when a composite sets `USE_MY_METRICS`.
104    pub advance: u16,
105    /// Left side bearing in design units, same source as `advance`.
106    pub lsb: i16,
107    /// Right side bearing in design units: `advance − lsb − (xMax − xMin)`
108    /// over the `glyf` header bbox (the phantom-point identity). For a
109    /// blank glyph this degenerates to `advance − lsb`.
110    pub rsb: i32,
111    /// The `glyf` header bounding box `[xMin, yMin, xMax, yMax]`, when the
112    /// glyph has one (blank glyphs do not).
113    pub bbox: Option<[i16; 4]>,
114}
115
116impl GlyphOutline {
117    /// Exact extents `[x_min, y_min, x_max, y_max]` of the decoded points
118    /// (anchors and control points — the same point set the `glyf` header
119    /// bbox covers), or `None` when there are no contours.
120    #[must_use]
121    pub fn extents(&self) -> Option<[f64; 4]> {
122        let mut ext: Option<[f64; 4]> = None;
123        let mut fold = |p: Point| {
124            ext = Some(match ext {
125                None => [p.x, p.y, p.x, p.y],
126                Some([x0, y0, x1, y1]) => [x0.min(p.x), y0.min(p.y), x1.max(p.x), y1.max(p.y)],
127            });
128        };
129        for c in &self.contours {
130            fold(c.start);
131            for s in &c.segments {
132                if let Segment::Quad { ctrl, .. } = s {
133                    fold(*ctrl);
134                }
135                fold(s.to());
136            }
137        }
138        ext
139    }
140}
141
142/// Why a glyph failed to decode.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum OutlineError {
145    /// The font has no `glyf`/`loca` tables (CFF outlines are tiered out).
146    NoGlyfOutlines,
147    /// The glyph id is out of range or its `loca` entry is unreadable.
148    BadGlyphId,
149    /// The glyph data is structurally invalid (truncated arrays, offsets
150    /// past the record, anchor point numbers out of range, …).
151    Malformed,
152    /// A resource budget tripped: composite depth, component count, or the
153    /// decoded-point ceiling. Well-formed fonts never hit these.
154    BudgetExceeded,
155}
156
157impl core::fmt::Display for OutlineError {
158    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159        match self {
160            Self::NoGlyfOutlines => write!(f, "font has no TrueType (glyf) outlines"),
161            Self::BadGlyphId => write!(f, "glyph id out of range"),
162            Self::Malformed => write!(f, "glyph outline data is malformed"),
163            Self::BudgetExceeded => write!(f, "glyph outline exceeds decode budgets"),
164        }
165    }
166}
167
168impl std::error::Error for OutlineError {}
169
170/// A raw outline point (before quadratic conversion).
171#[derive(Debug, Clone, Copy)]
172struct RawPoint {
173    x: f64,
174    y: f64,
175    on_curve: bool,
176}
177
178/// A flattened raw outline: `points` in composition order (anchor-point
179/// numbering indexes into this), `contour_ends` the inclusive end index of
180/// each contour.
181#[derive(Debug, Default)]
182struct RawGlyph {
183    points: Vec<RawPoint>,
184    contour_ends: Vec<usize>,
185}
186
187/// Shared decode budgets, threaded through composite recursion.
188struct Budget {
189    points_left: usize,
190    components_left: usize,
191}
192
193// Component flag bits (OpenType `glyf` composite description).
194const ARG_1_AND_2_ARE_WORDS: u16 = 0x0001;
195const ARGS_ARE_XY_VALUES: u16 = 0x0002;
196const WE_HAVE_A_SCALE: u16 = 0x0008;
197const MORE_COMPONENTS: u16 = 0x0020;
198const X_AND_Y_SCALE: u16 = 0x0040;
199const TWO_BY_TWO: u16 = 0x0080;
200const USE_MY_METRICS: u16 = 0x0200;
201const SCALED_COMPONENT_OFFSET: u16 = 0x0800;
202const UNSCALED_COMPONENT_OFFSET: u16 = 0x1000;
203
204#[inline(always)]
205fn f2dot14(v: i16) -> f64 {
206    f64::from(v) / 16384.0
207}
208
209impl Font {
210    /// Decode glyph `gid` to quadratic contours with phantom-point-correct
211    /// metrics. Blank glyphs (space) succeed with empty `contours`.
212    ///
213    /// # Errors
214    ///
215    /// [`OutlineError::NoGlyfOutlines`] for CFF-only fonts,
216    /// [`OutlineError::BadGlyphId`] for out-of-range ids, and
217    /// [`OutlineError::Malformed`] / [`OutlineError::BudgetExceeded`] for
218    /// structurally invalid or hostile glyph data.
219    pub fn glyph_outline(&self, gid: u16) -> Result<GlyphOutline, OutlineError> {
220        if !self.has_glyf_outlines() {
221            return Err(OutlineError::NoGlyfOutlines);
222        }
223        if gid >= self.num_glyphs {
224            return Err(OutlineError::BadGlyphId);
225        }
226        let mut budget = Budget {
227            points_left: MAX_OUTLINE_POINTS,
228            components_left: MAX_COMPONENTS,
229        };
230        let raw = decode_raw(self, gid, 0, &mut budget)?;
231        let (advance, lsb) = resolve_metrics(self, gid, 0);
232        let bbox = self.glyph_bbox(gid);
233        let rsb = match bbox {
234            Some([x_min, _, x_max, _]) => {
235                i32::from(advance) - i32::from(lsb) - (i32::from(x_max) - i32::from(x_min))
236            }
237            None => i32::from(advance) - i32::from(lsb),
238        };
239        Ok(GlyphOutline {
240            contours: to_contours(&raw),
241            advance,
242            lsb,
243            rsb,
244            bbox,
245        })
246    }
247}
248
249/// Metrics for `gid`, honoring `USE_MY_METRICS`: a composite flagging a
250/// component takes that component's `hmtx` advance and lsb (recursively).
251fn resolve_metrics(font: &Font, gid: u16, depth: usize) -> (u16, i16) {
252    if depth < MAX_COMPOSITE_DEPTH
253        && let Some(data) = font.glyph_data(gid)
254        && let Some(metrics_gid) = use_my_metrics_component(data)
255        && metrics_gid != gid
256    {
257        return resolve_metrics(font, metrics_gid, depth + 1);
258    }
259    (font.advance_width(gid), font.left_side_bearing(gid))
260}
261
262/// The component gid flagged `USE_MY_METRICS` in a composite glyph, if any.
263fn use_my_metrics_component(data: &[u8]) -> Option<u16> {
264    let num_contours = be_i16(data, 0)?;
265    if num_contours >= 0 {
266        return None;
267    }
268    let mut p = 10usize;
269    for _ in 0..MAX_COMPONENTS {
270        let flags = be_u16(data, p)?;
271        let comp = be_u16(data, p.checked_add(2)?)?;
272        if flags & USE_MY_METRICS != 0 {
273            return Some(comp);
274        }
275        p = p.checked_add(component_record_len(flags))?;
276        if flags & MORE_COMPONENTS == 0 {
277            return None;
278        }
279    }
280    None
281}
282
283/// Byte length of one component record with the given flags (flags word +
284/// glyph index + args + transform).
285fn component_record_len(flags: u16) -> usize {
286    let args = if flags & ARG_1_AND_2_ARE_WORDS != 0 {
287        4
288    } else {
289        2
290    };
291    let xform = if flags & WE_HAVE_A_SCALE != 0 {
292        2
293    } else if flags & X_AND_Y_SCALE != 0 {
294        4
295    } else if flags & TWO_BY_TWO != 0 {
296        8
297    } else {
298        0
299    };
300    4 + args + xform
301}
302
303/// Decode `gid` (simple or composite) to raw points, recursing through
304/// composite components with transforms and anchor matching applied.
305fn decode_raw(
306    font: &Font,
307    gid: u16,
308    depth: usize,
309    budget: &mut Budget,
310) -> Result<RawGlyph, OutlineError> {
311    if depth > MAX_COMPOSITE_DEPTH {
312        return Err(OutlineError::BudgetExceeded);
313    }
314    let data = font.glyph_data(gid).ok_or(OutlineError::BadGlyphId)?;
315    if data.is_empty() {
316        return Ok(RawGlyph::default()); // blank glyph (space)
317    }
318    let num_contours = be_i16(data, 0).ok_or(OutlineError::Malformed)?;
319    if num_contours >= 0 {
320        decode_simple(data, num_contours as usize, budget)
321    } else {
322        decode_composite(font, data, depth, budget)
323    }
324}
325
326/// Decode a simple glyph's flag/coordinate arrays into raw points.
327fn decode_simple(
328    data: &[u8],
329    num_contours: usize,
330    budget: &mut Budget,
331) -> Result<RawGlyph, OutlineError> {
332    // endPtsOfContours follows the 10-byte header.
333    let mut contour_ends = Vec::with_capacity(num_contours);
334    let mut prev_end: Option<usize> = None;
335    for i in 0..num_contours {
336        let off = 10usize
337            .checked_add(i.checked_mul(2).ok_or(OutlineError::Malformed)?)
338            .ok_or(OutlineError::Malformed)?;
339        let end = be_u16(data, off).ok_or(OutlineError::Malformed)? as usize;
340        // endPts must be non-decreasing; a decreasing run would desync the
341        // point count below.
342        if prev_end.is_some_and(|p| end < p) {
343            return Err(OutlineError::Malformed);
344        }
345        prev_end = Some(end);
346        contour_ends.push(end);
347    }
348    let num_points = match contour_ends.last() {
349        Some(&last) => last + 1,
350        None => {
351            return Ok(RawGlyph::default()); // zero contours: blank
352        }
353    };
354    if num_points > budget.points_left {
355        return Err(OutlineError::BudgetExceeded);
356    }
357    budget.points_left -= num_points;
358
359    let instr_off = 10 + num_contours * 2;
360    let instr_len = be_u16(data, instr_off).ok_or(OutlineError::Malformed)? as usize;
361    let mut p = instr_off
362        .checked_add(2)
363        .and_then(|v| v.checked_add(instr_len))
364        .ok_or(OutlineError::Malformed)?;
365
366    // Flags, run-length encoded via the REPEAT bit.
367    const ON_CURVE: u8 = 0x01;
368    const X_SHORT: u8 = 0x02;
369    const Y_SHORT: u8 = 0x04;
370    const REPEAT: u8 = 0x08;
371    const X_SAME_OR_POS: u8 = 0x10;
372    const Y_SAME_OR_POS: u8 = 0x20;
373    let mut flags = Vec::with_capacity(num_points);
374    while flags.len() < num_points {
375        let f = *data.get(p).ok_or(OutlineError::Malformed)?;
376        p += 1;
377        flags.push(f);
378        if f & REPEAT != 0 {
379            let n = *data.get(p).ok_or(OutlineError::Malformed)? as usize;
380            p += 1;
381            if flags.len() + n > num_points {
382                return Err(OutlineError::Malformed);
383            }
384            for _ in 0..n {
385                flags.push(f);
386            }
387        }
388    }
389
390    // X deltas, then Y deltas, each accumulated to absolute coordinates.
391    let mut points = Vec::with_capacity(num_points);
392    let mut x = 0i32;
393    for &f in &flags {
394        let dx = if f & X_SHORT != 0 {
395            let b = i32::from(*data.get(p).ok_or(OutlineError::Malformed)?);
396            p += 1;
397            if f & X_SAME_OR_POS != 0 { b } else { -b }
398        } else if f & X_SAME_OR_POS != 0 {
399            0
400        } else {
401            let v = i32::from(be_i16(data, p).ok_or(OutlineError::Malformed)?);
402            p += 2;
403            v
404        };
405        x += dx;
406        points.push(RawPoint {
407            x: f64::from(x),
408            y: 0.0,
409            on_curve: f & ON_CURVE != 0,
410        });
411    }
412    let mut y = 0i32;
413    for (i, &f) in flags.iter().enumerate() {
414        let dy = if f & Y_SHORT != 0 {
415            let b = i32::from(*data.get(p).ok_or(OutlineError::Malformed)?);
416            p += 1;
417            if f & Y_SAME_OR_POS != 0 { b } else { -b }
418        } else if f & Y_SAME_OR_POS != 0 {
419            0
420        } else {
421            let v = i32::from(be_i16(data, p).ok_or(OutlineError::Malformed)?);
422            p += 2;
423            v
424        };
425        y += dy;
426        if let Some(pt) = points.get_mut(i) {
427            pt.y = f64::from(y);
428        }
429    }
430
431    Ok(RawGlyph {
432        points,
433        contour_ends,
434    })
435}
436
437/// Decode a composite glyph by recursively decoding each component and
438/// appending its transformed points (anchor-point numbering stays flat
439/// across components, which is what anchor matching indexes into).
440fn decode_composite(
441    font: &Font,
442    data: &[u8],
443    depth: usize,
444    budget: &mut Budget,
445) -> Result<RawGlyph, OutlineError> {
446    let mut out = RawGlyph::default();
447    let mut p = 10usize;
448    loop {
449        if budget.components_left == 0 {
450            return Err(OutlineError::BudgetExceeded);
451        }
452        budget.components_left -= 1;
453
454        let flags = be_u16(data, p).ok_or(OutlineError::Malformed)?;
455        let comp_gid = be_u16(data, p.checked_add(2).ok_or(OutlineError::Malformed)?)
456            .ok_or(OutlineError::Malformed)?;
457        let record_end = p
458            .checked_add(component_record_len(flags))
459            .ok_or(OutlineError::Malformed)?;
460        if record_end > data.len() {
461            return Err(OutlineError::Malformed);
462        }
463
464        // Arguments: either an (dx, dy) offset or (parent, child) anchor
465        // point numbers, in words or bytes.
466        let arg_base = p + 4;
467        let (arg1, arg2) = if flags & ARG_1_AND_2_ARE_WORDS != 0 {
468            (
469                i32::from(be_i16(data, arg_base).ok_or(OutlineError::Malformed)?),
470                i32::from(be_i16(data, arg_base + 2).ok_or(OutlineError::Malformed)?),
471            )
472        } else {
473            let a = *data.get(arg_base).ok_or(OutlineError::Malformed)?;
474            let b = *data.get(arg_base + 1).ok_or(OutlineError::Malformed)?;
475            if flags & ARGS_ARE_XY_VALUES != 0 {
476                (i32::from(a as i8), i32::from(b as i8))
477            } else {
478                (i32::from(a), i32::from(b))
479            }
480        };
481
482        // Transform: 2×2 matrix in F2Dot14, defaulting to identity.
483        let xf_base = arg_base
484            + if flags & ARG_1_AND_2_ARE_WORDS != 0 {
485                4
486            } else {
487                2
488            };
489        let (a, b, c, d) = if flags & WE_HAVE_A_SCALE != 0 {
490            let s = f2dot14(be_i16(data, xf_base).ok_or(OutlineError::Malformed)?);
491            (s, 0.0, 0.0, s)
492        } else if flags & X_AND_Y_SCALE != 0 {
493            (
494                f2dot14(be_i16(data, xf_base).ok_or(OutlineError::Malformed)?),
495                0.0,
496                0.0,
497                f2dot14(be_i16(data, xf_base + 2).ok_or(OutlineError::Malformed)?),
498            )
499        } else if flags & TWO_BY_TWO != 0 {
500            (
501                f2dot14(be_i16(data, xf_base).ok_or(OutlineError::Malformed)?),
502                f2dot14(be_i16(data, xf_base + 2).ok_or(OutlineError::Malformed)?),
503                f2dot14(be_i16(data, xf_base + 4).ok_or(OutlineError::Malformed)?),
504                f2dot14(be_i16(data, xf_base + 6).ok_or(OutlineError::Malformed)?),
505            )
506        } else {
507            (1.0, 0.0, 0.0, 1.0)
508        };
509        let apply = |pt: RawPoint, dx: f64, dy: f64| RawPoint {
510            x: a * pt.x + c * pt.y + dx,
511            y: b * pt.x + d * pt.y + dy,
512            on_curve: pt.on_curve,
513        };
514
515        let child = decode_raw(font, comp_gid, depth + 1, budget)?;
516
517        let (dx, dy) = if flags & ARGS_ARE_XY_VALUES != 0 {
518            // An (dx, dy) offset. Per the spec's dominant (Microsoft/
519            // FreeType) interpretation the offset is in the parent's space,
520            // unscaled, unless SCALED_COMPONENT_OFFSET explicitly asks for
521            // the child transform to apply (UNSCALED_COMPONENT_OFFSET wins
522            // when both are set).
523            let (dx, dy) = (f64::from(arg1), f64::from(arg2));
524            if flags & SCALED_COMPONENT_OFFSET != 0 && flags & UNSCALED_COMPONENT_OFFSET == 0 {
525                (a * dx + c * dy, b * dx + d * dy)
526            } else {
527                (dx, dy)
528            }
529        } else {
530            // Anchor matching: align child point `arg2` (after the matrix,
531            // before translation) onto parent point `arg1` (numbered across
532            // the points composed so far).
533            let parent_ix = usize::try_from(arg1).map_err(|_| OutlineError::Malformed)?;
534            let child_ix = usize::try_from(arg2).map_err(|_| OutlineError::Malformed)?;
535            let parent_pt = *out.points.get(parent_ix).ok_or(OutlineError::Malformed)?;
536            let child_pt = *child.points.get(child_ix).ok_or(OutlineError::Malformed)?;
537            let placed = apply(child_pt, 0.0, 0.0);
538            (parent_pt.x - placed.x, parent_pt.y - placed.y)
539        };
540
541        let base = out.points.len();
542        out.points
543            .extend(child.points.iter().map(|&pt| apply(pt, dx, dy)));
544        out.contour_ends
545            .extend(child.contour_ends.iter().map(|&e| base + e));
546
547        if flags & MORE_COMPONENTS == 0 {
548            break;
549        }
550        p = record_end;
551    }
552    Ok(out)
553}
554
555/// Convert raw contours (on/off-curve points) into closed quadratic
556/// contours, synthesizing the implied on-curve midpoints between
557/// consecutive off-curve points.
558fn to_contours(raw: &RawGlyph) -> Vec<Contour> {
559    let mut contours = Vec::with_capacity(raw.contour_ends.len());
560    let mut start_ix = 0usize;
561    for &end in &raw.contour_ends {
562        let Some(pts) = raw.points.get(start_ix..=end) else {
563            break; // composite budget paths guarantee validity; belt & braces
564        };
565        start_ix = end + 1;
566        if let Some(c) = contour_to_quads(pts) {
567            contours.push(c);
568        }
569    }
570    contours
571}
572
573/// One raw contour → one closed quadratic contour. Returns `None` for
574/// degenerate contours (fewer than two points), which draw nothing.
575fn contour_to_quads(pts: &[RawPoint]) -> Option<Contour> {
576    if pts.len() < 2 {
577        return None;
578    }
579    let n = pts.len();
580    // Choose the starting anchor: the first on-curve point, or (all points
581    // off-curve) the midpoint of the last and first control points.
582    let first_on = pts.iter().position(|p| p.on_curve);
583    let (start, mut pending_ctrl, seq_start) = match first_on {
584        Some(i) => {
585            let p = pts[i];
586            (Point { x: p.x, y: p.y }, None, i + 1)
587        }
588        None => {
589            let last = pts[n - 1];
590            let first = pts[0];
591            let mid = Point {
592                x: (last.x + first.x) / 2.0,
593                y: (last.y + first.y) / 2.0,
594            };
595            (mid, None, 0)
596        }
597    };
598
599    let mut segments = Vec::with_capacity(n);
600    let mut cursor = start;
601    // Walk every raw point exactly once, starting after the anchor (or at
602    // the first control point in the all-off-curve case), wrapping around.
603    let count = if first_on.is_some() { n - 1 } else { n };
604    let mut emit = |ctrl: Option<Point>, to: Point, segments: &mut Vec<Segment>| {
605        match ctrl {
606            Some(ctrl) => segments.push(Segment::Quad { ctrl, to }),
607            None => {
608                // Skip zero-length line segments (repeated on-curve points);
609                // they add nothing and degrade downstream stroke joins.
610                if to != cursor {
611                    segments.push(Segment::Line { to });
612                }
613            }
614        }
615        cursor = to;
616    };
617    for k in 0..count {
618        let p = pts[(seq_start + k) % n];
619        let point = Point { x: p.x, y: p.y };
620        if p.on_curve {
621            emit(pending_ctrl.take(), point, &mut segments);
622        } else if let Some(prev_ctrl) = pending_ctrl.replace(point) {
623            // Two consecutive off-curve points: the implied on-curve
624            // midpoint closes the previous quad.
625            emit(Some(prev_ctrl), prev_ctrl.midpoint(point), &mut segments);
626            pending_ctrl = Some(point);
627        }
628    }
629    // Close the contour back to the start.
630    match pending_ctrl.take() {
631        Some(ctrl) => segments.push(Segment::Quad { ctrl, to: start }),
632        None => {
633            if cursor != start {
634                segments.push(Segment::Line { to: start });
635            }
636        }
637    }
638    if segments.is_empty() {
639        return None;
640    }
641    Some(Contour { start, segments })
642}
643
644#[cfg(test)]
645#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
646mod hostile_outline_tests {
647    //! Synthetic-font tests for the decoder's untrusted-input posture:
648    //! malformed structures error (never panic, never hang), and the
649    //! recursion/point budgets trip on hostile composites.
650
651    use super::*;
652
653    fn push16(v: &mut Vec<u8>, x: u16) {
654        v.extend_from_slice(&x.to_be_bytes());
655    }
656    fn push_i16(v: &mut Vec<u8>, x: i16) {
657        v.extend_from_slice(&x.to_be_bytes());
658    }
659    fn push32(v: &mut Vec<u8>, x: u32) {
660        v.extend_from_slice(&x.to_be_bytes());
661    }
662
663    fn sfnt(tables: &[(&[u8; 4], Vec<u8>)]) -> Vec<u8> {
664        let mut out = Vec::new();
665        push32(&mut out, 0x0001_0000);
666        push16(&mut out, u16::try_from(tables.len()).unwrap());
667        out.extend_from_slice(&[0u8; 6]);
668        let mut offset = 12 + tables.len() * 16;
669        let mut body = Vec::new();
670        for (tag, bytes) in tables {
671            out.extend_from_slice(&tag[..]);
672            push32(&mut out, 0);
673            push32(&mut out, u32::try_from(offset).unwrap());
674            push32(&mut out, u32::try_from(bytes.len()).unwrap());
675            offset += bytes.len();
676            body.extend_from_slice(bytes);
677        }
678        out.extend_from_slice(&body);
679        out
680    }
681
682    /// A minimal parseable font whose `glyf` holds exactly `glyphs` (raw
683    /// per-glyph bytes), with a long `loca` and one hmtx metric per glyph.
684    fn font_with_glyphs(glyphs: &[Vec<u8>]) -> Font {
685        let n = u16::try_from(glyphs.len()).unwrap();
686        let mut head = vec![0u8; 54];
687        head[18..20].copy_from_slice(&1000u16.to_be_bytes());
688        head[50..52].copy_from_slice(&1u16.to_be_bytes()); // long loca
689        let mut maxp = vec![0u8; 6];
690        maxp[4..6].copy_from_slice(&n.to_be_bytes());
691        let mut hhea = vec![0u8; 36];
692        hhea[4..6].copy_from_slice(&700i16.to_be_bytes());
693        hhea[6..8].copy_from_slice(&(-200i16).to_be_bytes());
694        hhea[34..36].copy_from_slice(&n.to_be_bytes());
695        let mut hmtx = Vec::new();
696        for _ in 0..n {
697            push16(&mut hmtx, 600);
698            push_i16(&mut hmtx, 50);
699        }
700        // Minimal format-4 cmap: just the mandatory 0xFFFF closing segment.
701        let mut cmap = Vec::new();
702        push16(&mut cmap, 0);
703        push16(&mut cmap, 1);
704        push16(&mut cmap, 3);
705        push16(&mut cmap, 1);
706        push32(&mut cmap, 12);
707        push16(&mut cmap, 4); // format
708        push16(&mut cmap, 32); // length
709        push16(&mut cmap, 0); // language
710        push16(&mut cmap, 2); // segCountX2
711        push16(&mut cmap, 0);
712        push16(&mut cmap, 0);
713        push16(&mut cmap, 0);
714        push16(&mut cmap, 0xFFFF); // endCode
715        push16(&mut cmap, 0); // reservedPad
716        push16(&mut cmap, 0xFFFF); // startCode
717        push16(&mut cmap, 1); // idDelta
718        push16(&mut cmap, 0); // idRangeOffset
719        let mut loca = Vec::new();
720        let mut glyf = Vec::new();
721        push32(&mut loca, 0);
722        for g in glyphs {
723            glyf.extend_from_slice(g);
724            push32(&mut loca, u32::try_from(glyf.len()).unwrap());
725        }
726        Font::parse(sfnt(&[
727            (b"head", head),
728            (b"maxp", maxp),
729            (b"hhea", hhea),
730            (b"hmtx", hmtx),
731            (b"cmap", cmap),
732            (b"loca", loca),
733            (b"glyf", glyf),
734        ]))
735        .expect("synthetic font parses")
736    }
737
738    /// A well-formed one-contour triangle: (0,0) → (500,0) → (250,400).
739    fn triangle_glyph() -> Vec<u8> {
740        let mut g = Vec::new();
741        push_i16(&mut g, 1); // numberOfContours
742        push_i16(&mut g, 0); // xMin
743        push_i16(&mut g, 0); // yMin
744        push_i16(&mut g, 500); // xMax
745        push_i16(&mut g, 400); // yMax
746        push16(&mut g, 2); // endPtsOfContours[0]
747        push16(&mut g, 0); // instructionLength
748        g.extend_from_slice(&[0x01, 0x01, 0x01]); // flags: 3 on-curve points
749        push_i16(&mut g, 0); // x deltas (16-bit)
750        push_i16(&mut g, 500);
751        push_i16(&mut g, -250);
752        push_i16(&mut g, 0); // y deltas
753        push_i16(&mut g, 0);
754        push_i16(&mut g, 400);
755        g
756    }
757
758    /// A composite glyph with one component record (XY offsets, word args).
759    fn composite_glyph(component_gid: u16, dx: i16, dy: i16, more: bool) -> Vec<u8> {
760        let mut g = Vec::new();
761        push_i16(&mut g, -1);
762        for _ in 0..4 {
763            push_i16(&mut g, 0); // bbox: unread by the decoder
764        }
765        let mut flags = ARG_1_AND_2_ARE_WORDS | ARGS_ARE_XY_VALUES;
766        if more {
767            flags |= MORE_COMPONENTS;
768        }
769        push16(&mut g, flags);
770        push16(&mut g, component_gid);
771        push_i16(&mut g, dx);
772        push_i16(&mut g, dy);
773        g
774    }
775
776    #[test]
777    fn triangle_decodes_closed_with_exact_extents() {
778        let font = font_with_glyphs(&[triangle_glyph()]);
779        let o = font.glyph_outline(0).expect("triangle decodes");
780        assert_eq!(o.contours.len(), 1);
781        let c = &o.contours[0];
782        assert_eq!(c.segments.last().unwrap().to(), c.start, "contour closes");
783        assert_eq!(o.extents(), Some([0.0, 0.0, 500.0, 400.0]));
784        assert_eq!(o.bbox, Some([0, 0, 500, 400]));
785        assert_eq!((o.advance, o.lsb), (600, 50));
786        assert_eq!(o.rsb, i32::from(600u16) - 50 - 500);
787    }
788
789    #[test]
790    fn blank_glyph_decodes_empty() {
791        let font = font_with_glyphs(&[Vec::new()]);
792        let o = font.glyph_outline(0).expect("blank glyph decodes");
793        assert!(o.contours.is_empty());
794        assert_eq!(o.bbox, None);
795        assert_eq!(o.rsb, 600 - 50);
796    }
797
798    #[test]
799    fn out_of_range_gid_is_rejected() {
800        let font = font_with_glyphs(&[triangle_glyph()]);
801        assert_eq!(font.glyph_outline(7), Err(OutlineError::BadGlyphId));
802    }
803
804    #[test]
805    fn composite_offsets_translate_the_component() {
806        let font = font_with_glyphs(&[triangle_glyph(), composite_glyph(0, 100, -50, false)]);
807        let o = font.glyph_outline(1).expect("composite decodes");
808        assert_eq!(o.extents(), Some([100.0, -50.0, 600.0, 350.0]));
809    }
810
811    #[test]
812    fn self_referential_composite_trips_the_depth_budget() {
813        let font = font_with_glyphs(&[composite_glyph(0, 10, 10, false)]);
814        assert_eq!(font.glyph_outline(0), Err(OutlineError::BudgetExceeded));
815    }
816
817    #[test]
818    fn mutually_recursive_composites_trip_the_depth_budget() {
819        let font = font_with_glyphs(&[
820            composite_glyph(1, 0, 0, false),
821            composite_glyph(0, 0, 0, false),
822        ]);
823        assert_eq!(font.glyph_outline(0), Err(OutlineError::BudgetExceeded));
824        assert_eq!(font.glyph_outline(1), Err(OutlineError::BudgetExceeded));
825    }
826
827    #[test]
828    fn anchor_args_out_of_range_are_malformed() {
829        // Anchor matching (no ARGS_ARE_XY_VALUES) with point numbers far
830        // beyond both glyphs' point counts.
831        let mut g = Vec::new();
832        push_i16(&mut g, -1);
833        for _ in 0..4 {
834            push_i16(&mut g, 0);
835        }
836        push16(&mut g, ARG_1_AND_2_ARE_WORDS); // words, anchors
837        push16(&mut g, 0); // component: the triangle
838        push_i16(&mut g, 999);
839        push_i16(&mut g, 999);
840        let font = font_with_glyphs(&[triangle_glyph(), g]);
841        assert_eq!(font.glyph_outline(1), Err(OutlineError::Malformed));
842    }
843
844    #[test]
845    fn truncated_simple_glyph_is_malformed() {
846        let full = triangle_glyph();
847        // Every proper prefix (past the header's contour count) must error
848        // cleanly — flags, deltas, endPts all truncate somewhere in here.
849        for cut in 2..full.len() {
850            let font = font_with_glyphs(&[full[..cut].to_vec()]);
851            let r = font.glyph_outline(0);
852            assert!(r.is_err(), "prefix of {cut} bytes must not decode");
853        }
854    }
855
856    #[test]
857    fn decreasing_end_pts_are_malformed() {
858        let mut g = Vec::new();
859        push_i16(&mut g, 2); // two contours
860        for _ in 0..4 {
861            push_i16(&mut g, 0);
862        }
863        push16(&mut g, 5); // endPts[0]
864        push16(&mut g, 2); // endPts[1] decreasing: desyncs the point count
865        push16(&mut g, 0);
866        let font = font_with_glyphs(&[g]);
867        assert_eq!(font.glyph_outline(0), Err(OutlineError::Malformed));
868    }
869
870    #[test]
871    fn flag_repeat_overflow_is_malformed() {
872        let mut g = Vec::new();
873        push_i16(&mut g, 1);
874        for _ in 0..4 {
875            push_i16(&mut g, 0);
876        }
877        push16(&mut g, 2); // 3 points
878        push16(&mut g, 0); // no instructions
879        g.extend_from_slice(&[0x09, 0xFF]); // on-curve + REPEAT × 255: overflows
880        let font = font_with_glyphs(&[g]);
881        assert_eq!(font.glyph_outline(0), Err(OutlineError::Malformed));
882    }
883
884    #[test]
885    fn overlong_component_chain_trips_the_component_budget() {
886        // One composite whose records all reference the triangle and chain
887        // MORE_COMPONENTS far past the budget.
888        let mut g = Vec::new();
889        push_i16(&mut g, -1);
890        for _ in 0..4 {
891            push_i16(&mut g, 0);
892        }
893        for i in 0..(MAX_COMPONENTS + 8) {
894            let last = i == MAX_COMPONENTS + 7;
895            let mut flags = ARG_1_AND_2_ARE_WORDS | ARGS_ARE_XY_VALUES;
896            if !last {
897                flags |= MORE_COMPONENTS;
898            }
899            push16(&mut g, flags);
900            push16(&mut g, 0);
901            push_i16(&mut g, 0);
902            push_i16(&mut g, 0);
903        }
904        let font = font_with_glyphs(&[triangle_glyph(), g]);
905        assert_eq!(font.glyph_outline(1), Err(OutlineError::BudgetExceeded));
906    }
907
908    #[test]
909    fn all_off_curve_contour_closes_with_quads() {
910        // Four off-curve points forming a diamond-ish TrueType "dot": every
911        // anchor is synthesized.
912        let mut g = Vec::new();
913        push_i16(&mut g, 1);
914        for _ in 0..4 {
915            push_i16(&mut g, 0);
916        }
917        push16(&mut g, 3); // 4 points
918        push16(&mut g, 0);
919        g.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // all off-curve, 16-bit deltas
920        // Absolute points: (0,100), (100,200), (200,100), (100,0) — a diamond
921        // of control points with every anchor synthesized as a midpoint.
922        for dx in [0i16, 100, 100, -100] {
923            push_i16(&mut g, dx);
924        }
925        for dy in [100i16, 100, -100, -100] {
926            push_i16(&mut g, dy);
927        }
928        let font = font_with_glyphs(&[g]);
929        let o = font
930            .glyph_outline(0)
931            .expect("all-off-curve contour decodes");
932        assert_eq!(o.contours.len(), 1);
933        let c = &o.contours[0];
934        assert_eq!(c.segments.len(), 4);
935        assert!(
936            c.segments.iter().all(|s| matches!(s, Segment::Quad { .. })),
937            "an all-off-curve contour is pure quads"
938        );
939        assert_eq!(c.segments.last().unwrap().to(), c.start);
940    }
941}