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