Skip to main content

skrifa/outline/varc/
mod.rs

1//! Support for rendering variable composite glyphs from the VARC table.
2
3use read_fonts::{
4    tables::{
5        layout::Condition,
6        varc::{
7            DecomposedTransform, MultiItemVariationStore, SparseVariationRegionList, Varc,
8            VarcComponent, VarcFlags,
9        },
10        variations::NO_VARIATION_INDEX,
11    },
12    types::{F2Dot14, GlyphId, Matrix},
13    FontRef, ReadError, TableProvider,
14};
15
16use crate::{
17    collections::SmallVec,
18    instance::Size,
19    outline::{cff, glyf, metrics::GlyphHMetrics, pen::PathStyle, DrawError, OutlinePen},
20    provider::MetadataProvider,
21    GLYF_COMPOSITE_RECURSION_LIMIT, MAX_GRAPH_EDGES,
22};
23
24#[cfg(feature = "libm")]
25#[allow(unused_imports)]
26use core_maths::CoreFloat;
27
28use super::OutlineKind;
29
30type GlyphStack = SmallVec<GlyphId, 8>;
31type CoordVec = SmallVec<F2Dot14, 64>;
32type AxisIndexVec = SmallVec<u16, 64>;
33type AxisValueVec = SmallVec<f32, 64>;
34type DeltaVec = SmallVec<f32, 64>;
35type ScalarCacheVec = SmallVec<f32, 128>;
36type Affine = Matrix<f32>;
37
38struct Scratchpad {
39    deltas: DeltaVec,
40    axis_indices: AxisIndexVec,
41    axis_values: AxisValueVec,
42    edges_left: usize,
43}
44
45impl Scratchpad {
46    fn new() -> Self {
47        Self {
48            deltas: DeltaVec::new(),
49            axis_indices: AxisIndexVec::new(),
50            axis_values: AxisValueVec::new(),
51            edges_left: MAX_GRAPH_EDGES,
52        }
53    }
54}
55
56struct VarcSharedContext<'a, 'b> {
57    font_coords: &'b [F2Dot14],
58    size: Size,
59    path_style: PathStyle,
60    coverage: &'b read_fonts::tables::layout::CoverageTable<'a>,
61    var_store: Option<&'b MultiItemVariationStore<'a>>,
62    store_regions: Option<(
63        &'b MultiItemVariationStore<'a>,
64        &'b SparseVariationRegionList<'a>,
65    )>,
66}
67
68#[derive(Clone)]
69enum BaseOutlines<'a> {
70    Glyf(glyf::Outlines<'a>),
71    Cff(cff::Outlines<'a>),
72}
73
74impl<'a> BaseOutlines<'a> {
75    fn glyph_count(&self) -> u32 {
76        match self {
77            Self::Glyf(glyf) => glyf.glyph_count() as u32,
78            Self::Cff(cff) => cff.glyph_count() as u32,
79        }
80    }
81
82    fn font(&self) -> &FontRef<'a> {
83        match self {
84            Self::Glyf(glyf) => &glyf.font,
85            Self::Cff(cff) => &cff.font,
86        }
87    }
88
89    fn base_outline_kind(&self, glyph_id: GlyphId) -> Option<OutlineKind<'a>> {
90        match self {
91            Self::Glyf(glyf) => Some(OutlineKind::Glyf(
92                glyf.clone(),
93                glyf.outline(glyph_id).ok()?,
94            )),
95            Self::Cff(cff) => Some(OutlineKind::Cff(
96                cff.clone(),
97                glyph_id,
98                cff.subfont_index(glyph_id),
99            )),
100        }
101    }
102
103    fn base_outline_memory(&self, glyph_id: GlyphId) -> usize {
104        match self {
105            Self::Glyf(glyf) => glyf
106                .outline(glyph_id)
107                .ok()
108                .map(|outline| outline.required_buffer_size(super::Hinting::None))
109                .unwrap_or(0),
110            Self::Cff(..) => 0,
111        }
112    }
113}
114
115#[derive(Clone)]
116pub(crate) struct Outlines<'a> {
117    varc: Varc<'a>,
118    coverage: read_fonts::tables::layout::CoverageTable<'a>,
119    var_store: Option<MultiItemVariationStore<'a>>,
120    regions: Option<SparseVariationRegionList<'a>>,
121    base: BaseOutlines<'a>,
122    glyph_metrics: GlyphHMetrics<'a>,
123    units_per_em: u16,
124    axis_count: usize,
125}
126
127#[derive(Clone, Copy)]
128pub(crate) struct Outline {
129    pub(crate) glyph_id: GlyphId,
130    pub(crate) coverage_index: u16,
131    max_component_memory: usize,
132}
133
134impl Outline {
135    pub fn required_buffer_size(&self) -> usize {
136        self.max_component_memory
137    }
138}
139
140impl<'a> Outlines<'a> {
141    pub fn new(font: &FontRef<'a>) -> Option<Self> {
142        let varc = font.varc().ok()?;
143        if let Some(glyf) = glyf::Outlines::new(font) {
144            return Self::from_base(font, varc, BaseOutlines::Glyf(glyf));
145        }
146        if let Some(cff) = cff::Outlines::new(font) {
147            return Self::from_base(font, varc, BaseOutlines::Cff(cff));
148        }
149        None
150    }
151
152    fn from_base(font: &FontRef<'a>, varc: Varc<'a>, base: BaseOutlines<'a>) -> Option<Self> {
153        let glyph_metrics = GlyphHMetrics::new(font)?;
154        let units_per_em = font.head().ok()?.units_per_em();
155        let axis_count = font.axes().len();
156        let coverage = varc.coverage().ok()?;
157        let var_store = varc.multi_var_store().transpose().ok()?;
158        let regions = var_store
159            .as_ref()
160            .map(|s| s.region_list())
161            .transpose()
162            .ok()?;
163        Some(Self {
164            varc,
165            coverage,
166            var_store,
167            regions,
168            base,
169            glyph_metrics,
170            units_per_em,
171            axis_count,
172        })
173    }
174
175    pub fn units_per_em(&self) -> u16 {
176        self.units_per_em
177    }
178
179    pub fn glyph_count(&self) -> u32 {
180        self.base.glyph_count()
181    }
182
183    pub fn prefer_interpreter(&self) -> bool {
184        false
185    }
186
187    pub fn fractional_size_hinting(&self) -> bool {
188        false
189    }
190
191    pub fn font(&self) -> &FontRef<'a> {
192        self.base.font()
193    }
194
195    pub(crate) fn fallback_outline_kind(&self, glyph_id: GlyphId) -> Option<OutlineKind<'a>> {
196        self.base.base_outline_kind(glyph_id)
197    }
198
199    pub fn outline(&self, glyph_id: GlyphId) -> Result<Option<Outline>, ReadError> {
200        let Some(coverage_index) = self.coverage.get(glyph_id) else {
201            return Ok(None);
202        };
203        let max_component_memory = self.compute_max_component_memory(glyph_id, coverage_index)?;
204        Ok(Some(Outline {
205            glyph_id,
206            coverage_index,
207            max_component_memory,
208        }))
209    }
210
211    /// Lightweight coverage lookup without computing max_component_memory.
212    fn coverage_index(&self, glyph_id: GlyphId) -> Result<Option<u16>, ReadError> {
213        Ok(self.coverage.get(glyph_id))
214    }
215
216    fn compute_max_component_memory(
217        &self,
218        glyph_id: GlyphId,
219        coverage_index: u16,
220    ) -> Result<usize, ReadError> {
221        let mut stack = GlyphStack::new();
222        let mut edges_left = MAX_GRAPH_EDGES;
223        self.max_component_memory_for_glyph(glyph_id, coverage_index, &mut stack, &mut edges_left)
224    }
225
226    fn max_component_memory_for_glyph(
227        &self,
228        glyph_id: GlyphId,
229        coverage_index: u16,
230        stack: &mut GlyphStack,
231        edges_left: &mut usize,
232    ) -> Result<usize, ReadError> {
233        if stack.contains(&glyph_id) {
234            return Ok(0);
235        }
236        // HB returns success for both recursion and edge limits, so we do the same.
237        // See <https://github.com/harfbuzz/harfbuzz/blob/0fef675a5ea4973ce49d6dd00c02e70ec409eee3/src/OT/Var/VARC/VARC.cc#L386>
238        if stack.len() >= GLYF_COMPOSITE_RECURSION_LIMIT {
239            return Ok(0);
240        }
241        if *edges_left == 0 {
242            return Ok(0);
243        }
244        *edges_left -= 1;
245        stack.push(glyph_id);
246        let mut max_memory = 0usize;
247        let glyph = self.varc.glyph(coverage_index as usize)?;
248        for component in glyph.components() {
249            let component = component?;
250            let component_gid = component.gid();
251            let component_memory = if component_gid == glyph_id {
252                self.base.base_outline_memory(component_gid)
253            } else if let Some(coverage_index) = self.coverage_index(component_gid)? {
254                self.max_component_memory_for_glyph(
255                    component_gid,
256                    coverage_index,
257                    stack,
258                    edges_left,
259                )?
260            } else {
261                self.base.base_outline_memory(component_gid)
262            };
263            max_memory = max_memory.max(component_memory);
264        }
265        stack.pop();
266        Ok(max_memory)
267    }
268
269    pub fn draw(
270        &self,
271        outline: &Outline,
272        buf: &mut [u8],
273        size: Size,
274        coords: &[F2Dot14],
275        path_style: PathStyle,
276        pen: &mut impl OutlinePen,
277    ) -> Result<(), DrawError> {
278        let mut font_coords = CoordVec::new();
279        expand_coords(&mut font_coords, self.axis_count, coords);
280        let mut stack = GlyphStack::new();
281        let pen: &mut dyn OutlinePen = pen;
282        let mut scalar_cache = self.scalar_cache_from_store(self.var_store.as_ref())?;
283        let mut scratch = Scratchpad::new();
284        let ctx = VarcSharedContext {
285            font_coords: &font_coords,
286            size,
287            path_style,
288            coverage: &self.coverage,
289            var_store: self.var_store.as_ref(),
290            store_regions: self.var_store.as_ref().zip(self.regions.as_ref()),
291        };
292        self.draw_glyph(
293            outline.glyph_id,
294            outline.coverage_index,
295            &font_coords,
296            Affine::IDENTITY,
297            &ctx,
298            buf,
299            pen,
300            &mut stack,
301            &mut scalar_cache,
302            &mut scratch,
303        )
304    }
305
306    pub fn draw_unscaled(
307        &self,
308        outline: &Outline,
309        buf: &mut [u8],
310        coords: &[F2Dot14],
311        pen: &mut impl OutlinePen,
312    ) -> Result<i32, DrawError> {
313        let size = Size::unscaled();
314        self.draw(outline, buf, size, coords, PathStyle::default(), pen)?;
315        Ok(self.glyph_metrics.advance_width(outline.glyph_id, coords))
316    }
317
318    #[allow(clippy::too_many_arguments)]
319    fn draw_glyph(
320        &self,
321        glyph_id: GlyphId,
322        coverage_index: u16,
323        current_coords: &[F2Dot14],
324        parent_matrix: Affine,
325        ctx: &VarcSharedContext<'a, '_>,
326        buf: &mut [u8],
327        pen: &mut dyn OutlinePen,
328        stack: &mut GlyphStack,
329        scalar_cache: &mut ScalarCache,
330        scratch: &mut Scratchpad,
331    ) -> Result<(), DrawError> {
332        if stack.len() >= GLYF_COMPOSITE_RECURSION_LIMIT {
333            return Err(DrawError::RecursionLimitExceeded(glyph_id));
334        }
335        if scratch.edges_left == 0 {
336            return Err(DrawError::RecursionLimitExceeded(glyph_id));
337        }
338        scratch.edges_left -= 1;
339        let glyph = self.varc.glyph(coverage_index as usize)?;
340        stack.push(glyph_id);
341        let coverage = ctx.coverage;
342        let store_regions = ctx.store_regions;
343        let mut component_coords_buffer = CoordVec::new();
344        let mut child_scalar_cache: Option<ScalarCache> = None;
345        for component in glyph.components() {
346            let component = component?;
347            if !self.component_condition_met(
348                &component,
349                current_coords,
350                scalar_cache,
351                scratch,
352                store_regions,
353            )? {
354                continue;
355            }
356            let component_gid = component.gid();
357            let flags = component.flags();
358
359            let coords_the_same = !flags.contains(VarcFlags::HAVE_AXES)
360                && !flags.contains(VarcFlags::RESET_UNSPECIFIED_AXES);
361
362            let component_coords = if coords_the_same {
363                current_coords
364            } else {
365                self.component_coords(
366                    &component,
367                    current_coords,
368                    &mut component_coords_buffer,
369                    scalar_cache,
370                    scratch,
371                    ctx.font_coords,
372                    store_regions,
373                )?;
374                component_coords_buffer.as_slice()
375            };
376
377            let mut transform = *component.transform();
378            self.apply_transform_variations(
379                &component,
380                current_coords,
381                &mut transform,
382                scalar_cache,
383                scratch,
384                store_regions,
385            )?;
386            let scale = ctx.size.linear_scale(self.units_per_em);
387            let matrix = parent_matrix * scale_matrix(transform.matrix(), scale);
388            if component_gid != glyph_id {
389                if let Some(coverage_index) = coverage.get(component_gid) {
390                    if !stack.contains(&component_gid) {
391                        // Optimization: if coordinates haven't changed, we can reuse the scalar cache.
392                        if coords_the_same {
393                            self.draw_glyph(
394                                component_gid,
395                                coverage_index,
396                                current_coords,
397                                matrix,
398                                ctx,
399                                buf,
400                                pen,
401                                stack,
402                                scalar_cache,
403                                scratch,
404                            )?;
405                        } else {
406                            if let Some(ref mut cache) = child_scalar_cache {
407                                cache.values.fill(ScalarCache::INVALID);
408                            } else {
409                                child_scalar_cache =
410                                    Some(self.scalar_cache_from_store(ctx.var_store)?);
411                            }
412                            self.draw_glyph(
413                                component_gid,
414                                coverage_index,
415                                component_coords,
416                                matrix,
417                                ctx,
418                                buf,
419                                pen,
420                                stack,
421                                child_scalar_cache.as_mut().unwrap(),
422                                scratch,
423                            )?;
424                        }
425                        continue;
426                    }
427                }
428            }
429            let mut transform_pen = TransformPen::new(pen, matrix);
430            self.draw_base_glyph(
431                component_gid,
432                component_coords,
433                ctx.size,
434                ctx.path_style,
435                buf,
436                &mut transform_pen,
437            )?;
438        }
439        stack.pop();
440        Ok(())
441    }
442
443    fn draw_base_glyph(
444        &self,
445        glyph_id: GlyphId,
446        coords: &[F2Dot14],
447        size: Size,
448        path_style: PathStyle,
449        buf: &mut [u8],
450        pen: &mut impl OutlinePen,
451    ) -> Result<(), DrawError> {
452        let Some(kind) = self.base.base_outline_kind(glyph_id) else {
453            return Err(DrawError::GlyphNotFound(glyph_id));
454        };
455        let settings =
456            crate::outline::DrawSettings::unhinted(size, crate::instance::LocationRef::new(coords))
457                .with_path_style(path_style)
458                .with_memory(Some(buf));
459        crate::outline::OutlineGlyph { kind }.draw(settings, pen)?;
460        Ok(())
461    }
462
463    #[allow(clippy::too_many_arguments)]
464    fn component_coords(
465        &self,
466        component: &VarcComponent<'_>,
467        current_coords: &[F2Dot14],
468        coords: &mut CoordVec,
469        scalar_cache: &mut ScalarCache,
470        scratch: &mut Scratchpad,
471        font_coords: &[F2Dot14],
472        store_regions: Option<(&MultiItemVariationStore<'a>, &SparseVariationRegionList<'a>)>,
473    ) -> Result<(), DrawError> {
474        let flags = component.flags();
475        if flags.contains(VarcFlags::RESET_UNSPECIFIED_AXES) {
476            expand_coords(coords, font_coords.len(), font_coords);
477        } else {
478            expand_coords(coords, current_coords.len(), current_coords);
479        }
480
481        if !flags.contains(VarcFlags::HAVE_AXES) {
482            return Ok(());
483        }
484
485        let axis_indices_index = component
486            .axis_indices_index()
487            .ok_or(ReadError::MalformedData("Missing axisIndicesIndex"))?;
488        let num_axes = self.axis_indices(axis_indices_index as usize, &mut scratch.axis_indices)?;
489
490        self.axis_values(component, num_axes, &mut scratch.axis_values)?;
491        if let Some(var_idx) = component.axis_values_var_index() {
492            let (store, regions) = store_regions.ok_or(ReadError::NullOffset)?;
493            compute_tuple_deltas(
494                store,
495                regions,
496                var_idx,
497                current_coords,
498                scratch.axis_indices.len(),
499                scalar_cache,
500                &mut scratch.deltas,
501            )?;
502            for (value, delta) in scratch.axis_values.iter_mut().zip(scratch.deltas.iter()) {
503                *value += *delta;
504            }
505        }
506
507        for (axis_index, value) in scratch
508            .axis_indices
509            .iter()
510            .zip(scratch.axis_values.iter().copied())
511        {
512            let Some(slot) = coords.get_mut(*axis_index as usize) else {
513                return Err(DrawError::Read(ReadError::OutOfBounds));
514            };
515            let raw = value.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
516            *slot = F2Dot14::from_bits(raw);
517        }
518        Ok(())
519    }
520
521    fn axis_indices(&self, nth: usize, out: &mut AxisIndexVec) -> Result<usize, DrawError> {
522        let packed = self.varc.axis_indices(nth)?;
523        out.clear();
524        for value in packed.iter() {
525            out.push(value as u16);
526        }
527        Ok(out.len())
528    }
529
530    fn axis_values(
531        &self,
532        component: &VarcComponent<'_>,
533        count: usize,
534        out: &mut AxisValueVec,
535    ) -> Result<(), DrawError> {
536        let Some(packed) = component.axis_values() else {
537            out.clear();
538            return Ok(());
539        };
540        out.resize_and_fill(count, 0.0);
541        for (slot, value) in out.iter_mut().zip(packed.iter().by_ref().take(count)) {
542            *slot = value as f32;
543        }
544        Ok(())
545    }
546
547    fn apply_transform_variations(
548        &self,
549        component: &VarcComponent<'_>,
550        coords: &[F2Dot14],
551        transform: &mut DecomposedTransform,
552        scalar_cache: &mut ScalarCache,
553        scratch: &mut Scratchpad,
554        store_regions: Option<(&MultiItemVariationStore<'a>, &SparseVariationRegionList<'a>)>,
555    ) -> Result<(), DrawError> {
556        let Some(var_idx) = component.transform_var_index() else {
557            return Ok(());
558        };
559
560        let flags = component.flags();
561
562        // Count transform fields using a mask + count_ones
563        const TRANSFORM_MASK: VarcFlags = VarcFlags::from_bits_truncate(
564            VarcFlags::HAVE_TRANSLATE_X.bits()
565                | VarcFlags::HAVE_TRANSLATE_Y.bits()
566                | VarcFlags::HAVE_ROTATION.bits()
567                | VarcFlags::HAVE_SCALE_X.bits()
568                | VarcFlags::HAVE_SCALE_Y.bits()
569                | VarcFlags::HAVE_SKEW_X.bits()
570                | VarcFlags::HAVE_SKEW_Y.bits()
571                | VarcFlags::HAVE_TCENTER_X.bits()
572                | VarcFlags::HAVE_TCENTER_Y.bits(),
573        );
574        let field_count = (flags.bits() & TRANSFORM_MASK.bits()).count_ones() as usize;
575        if field_count == 0 {
576            return Ok(());
577        }
578
579        let (store, regions) = store_regions.ok_or(ReadError::NullOffset)?;
580        compute_tuple_deltas(
581            store,
582            regions,
583            var_idx,
584            coords,
585            field_count,
586            scalar_cache,
587            &mut scratch.deltas,
588        )?;
589
590        // Apply deltas in flag order, consuming from iterator
591        let mut delta_iter = scratch.deltas.iter().copied();
592
593        if flags.contains(VarcFlags::HAVE_TRANSLATE_X) {
594            let delta = delta_iter.next().unwrap_or(0.0);
595            transform.set_translate_x(transform.translate_x() + delta);
596        }
597        if flags.contains(VarcFlags::HAVE_TRANSLATE_Y) {
598            let delta = delta_iter.next().unwrap_or(0.0);
599            transform.set_translate_y(transform.translate_y() + delta);
600        }
601        if flags.contains(VarcFlags::HAVE_ROTATION) {
602            let delta = delta_iter.next().unwrap_or(0.0);
603            transform.set_rotation(transform.rotation() + delta / 4096.0);
604        }
605        if flags.contains(VarcFlags::HAVE_SCALE_X) {
606            let delta = delta_iter.next().unwrap_or(0.0);
607            transform.set_scale_x(transform.scale_x() + delta / 1024.0);
608        }
609        if flags.contains(VarcFlags::HAVE_SCALE_Y) {
610            let delta = delta_iter.next().unwrap_or(0.0);
611            transform.set_scale_y(transform.scale_y() + delta / 1024.0);
612        }
613        const SKEW_OR_CENTER: VarcFlags = VarcFlags::from_bits_truncate(
614            VarcFlags::HAVE_SKEW_X.bits()
615                | VarcFlags::HAVE_SKEW_Y.bits()
616                | VarcFlags::HAVE_TCENTER_X.bits()
617                | VarcFlags::HAVE_TCENTER_Y.bits(),
618        );
619        if flags.intersects(SKEW_OR_CENTER) {
620            if flags.contains(VarcFlags::HAVE_SKEW_X) {
621                let delta = delta_iter.next().unwrap_or(0.0);
622                transform.set_skew_x(transform.skew_x() + delta / 4096.0);
623            }
624            if flags.contains(VarcFlags::HAVE_SKEW_Y) {
625                let delta = delta_iter.next().unwrap_or(0.0);
626                transform.set_skew_y(transform.skew_y() + delta / 4096.0);
627            }
628            if flags.contains(VarcFlags::HAVE_TCENTER_X) {
629                let delta = delta_iter.next().unwrap_or(0.0);
630                transform.set_center_x(transform.center_x() + delta);
631            }
632            if flags.contains(VarcFlags::HAVE_TCENTER_Y) {
633                let delta = delta_iter.next().unwrap_or(0.0);
634                transform.set_center_y(transform.center_y() + delta);
635            }
636        }
637
638        if !flags.contains(VarcFlags::HAVE_SCALE_Y) {
639            transform.set_scale_y(transform.scale_x());
640        }
641        Ok(())
642    }
643
644    fn component_condition_met(
645        &self,
646        component: &VarcComponent<'_>,
647        coords: &[F2Dot14],
648        scalar_cache: &mut ScalarCache,
649        scratch: &mut Scratchpad,
650        store_regions: Option<(&MultiItemVariationStore<'a>, &SparseVariationRegionList<'a>)>,
651    ) -> Result<bool, DrawError> {
652        let Some(condition_index) = component.condition_index() else {
653            return Ok(true);
654        };
655        let Some(condition_list) = self.varc.condition_list() else {
656            return Err(DrawError::Read(ReadError::NullOffset));
657        };
658        let condition_list = condition_list?;
659        let condition = condition_list.conditions().get(condition_index as usize)?;
660        let (store, regions) = store_regions.ok_or(ReadError::NullOffset)?;
661        Self::eval_condition(&condition, coords, store, regions, scalar_cache, scratch, 0)
662    }
663
664    fn eval_condition(
665        condition: &Condition<'a>,
666        coords: &[F2Dot14],
667        var_store: &MultiItemVariationStore<'a>,
668        regions: &SparseVariationRegionList<'a>,
669        scalar_cache: &mut ScalarCache,
670        scratch: &mut Scratchpad,
671        depth: usize,
672    ) -> Result<bool, DrawError> {
673        // Format 3/4/5 conditions nest child conditions by offset, and the tree
674        // is fully attacker-controlled. Bound the recursion so a deeply nested
675        // (or degenerate) condition tree returns an error instead of overflowing
676        // the stack, mirroring the component-recursion guard in `draw_glyph`.
677        if depth >= GLYF_COMPOSITE_RECURSION_LIMIT {
678            return Err(DrawError::RecursionLimitExceeded(GlyphId::NOTDEF));
679        }
680        match condition {
681            Condition::Format1AxisRange(condition) => {
682                let axis_index = condition.axis_index() as usize;
683                let coord = coords.get(axis_index).copied().unwrap_or(F2Dot14::ZERO);
684                Ok(coord >= condition.filter_range_min_value()
685                    && coord <= condition.filter_range_max_value())
686            }
687            Condition::Format2VariableValue(condition) => {
688                let default_value = condition.default_value() as f32;
689                let var_idx = condition.var_index();
690                compute_tuple_deltas(
691                    var_store,
692                    regions,
693                    var_idx,
694                    coords,
695                    1,
696                    scalar_cache,
697                    &mut scratch.deltas,
698                )?;
699                let delta = scratch.deltas.first().copied().unwrap_or(0.0);
700                Ok(default_value + delta > 0.0)
701            }
702            Condition::Format3And(condition) => {
703                for nested in condition.conditions().iter() {
704                    let nested = nested?;
705                    if !Self::eval_condition(
706                        &nested,
707                        coords,
708                        var_store,
709                        regions,
710                        scalar_cache,
711                        scratch,
712                        depth + 1,
713                    )? {
714                        return Ok(false);
715                    }
716                }
717                Ok(true)
718            }
719            Condition::Format4Or(condition) => {
720                for nested in condition.conditions().iter() {
721                    let nested = nested?;
722                    if Self::eval_condition(
723                        &nested,
724                        coords,
725                        var_store,
726                        regions,
727                        scalar_cache,
728                        scratch,
729                        depth + 1,
730                    )? {
731                        return Ok(true);
732                    }
733                }
734                Ok(false)
735            }
736            Condition::Format5Negate(condition) => {
737                let nested = condition.condition()?;
738                Ok(!Self::eval_condition(
739                    &nested,
740                    coords,
741                    var_store,
742                    regions,
743                    scalar_cache,
744                    scratch,
745                    depth + 1,
746                )?)
747            }
748        }
749    }
750
751    fn scalar_cache_from_store(
752        &self,
753        store: Option<&MultiItemVariationStore<'a>>,
754    ) -> Result<ScalarCache, DrawError> {
755        // The VARC `multiVarStore` offset is nullable, so a valid font may omit the
756        // store entirely. In that case there are no variation regions and an empty
757        // cache is correct: any component that references variation data will fail
758        // gracefully with `ReadError::NullOffset` when it resolves the missing store,
759        // rather than us panicking here.
760        let region_count = match store {
761            Some(store) => store.region_list()?.region_count() as usize,
762            None => 0,
763        };
764        Ok(ScalarCache::new(region_count))
765    }
766}
767
768struct ScalarCache {
769    values: ScalarCacheVec,
770}
771
772impl ScalarCache {
773    const INVALID: f32 = 2.0; // Scalars are in [0,1], so 2.0 means "not cached"
774
775    fn new(count: usize) -> Self {
776        Self {
777            values: ScalarCacheVec::with_len(count, Self::INVALID),
778        }
779    }
780
781    fn get(&self, index: usize) -> f32 {
782        self.values.get(index).copied().unwrap_or(Self::INVALID)
783    }
784
785    fn set(&mut self, index: usize, value: f32) {
786        if let Some(slot) = self.values.get_mut(index) {
787            *slot = value;
788        }
789    }
790}
791
792fn expand_coords(out: &mut CoordVec, axis_count: usize, coords: &[F2Dot14]) {
793    out.resize_and_fill(axis_count, F2Dot14::ZERO);
794    for (slot, value) in out.iter_mut().zip(coords.iter().copied()) {
795        *slot = value;
796    }
797}
798
799fn compute_tuple_deltas(
800    store: &MultiItemVariationStore,
801    regions: &SparseVariationRegionList,
802    var_idx: u32,
803    coords: &[F2Dot14],
804    tuple_len: usize,
805    cache: &mut ScalarCache,
806    out: &mut DeltaVec,
807) -> Result<(), ReadError> {
808    out.resize_and_fill(tuple_len, 0.0);
809    if tuple_len == 0 || var_idx == NO_VARIATION_INDEX {
810        return Ok(());
811    }
812    let outer = (var_idx >> 16) as usize;
813    let inner = (var_idx & 0xFFFF) as usize;
814    let data = store
815        .variation_data()
816        .get(outer)
817        .map_err(|_| ReadError::InvalidCollectionIndex(outer as _))?;
818    let region_indices = data.region_indices();
819    let mut deltas = data.delta_set(inner)?.fetcher();
820    let regions = regions.regions();
821    let out_slice = out.as_mut_slice();
822
823    let mut skip = 0;
824    for region_index in region_indices.iter() {
825        let region_idx = region_index.get() as usize;
826        let mut scalar = cache.get(region_idx);
827        if scalar >= 2.0 {
828            scalar = regions.get(region_idx)?.compute_scalar_f32(coords);
829            cache.set(region_idx, scalar);
830        }
831        // We skip lazily. Reduces work at the tail end.
832        if scalar == 0.0 {
833            skip += out_slice.len();
834        } else {
835            if skip != 0 {
836                deltas.skip(skip)?;
837                skip = 0;
838            }
839            deltas.add_to_f32_scaled(out_slice, scalar)?;
840        }
841    }
842
843    Ok(())
844}
845
846#[inline(always)]
847fn scale_matrix(mut m: Affine, s: f32) -> Affine {
848    m.dx *= s;
849    m.dy *= s;
850    m
851}
852
853struct TransformPen<'a, P: OutlinePen + ?Sized> {
854    pen: &'a mut P,
855    matrix: Affine,
856}
857
858impl<'a, P: OutlinePen + ?Sized> TransformPen<'a, P> {
859    fn new(pen: &'a mut P, matrix: Affine) -> Self {
860        Self { pen, matrix }
861    }
862
863    #[inline(always)]
864    fn transform(&self, x: f32, y: f32) -> (f32, f32) {
865        self.matrix.transform(x, y)
866    }
867}
868
869impl<P: OutlinePen + ?Sized> OutlinePen for TransformPen<'_, P> {
870    fn move_to(&mut self, x: f32, y: f32) {
871        let (x, y) = self.transform(x, y);
872        self.pen.move_to(x, y);
873    }
874
875    fn line_to(&mut self, x: f32, y: f32) {
876        let (x, y) = self.transform(x, y);
877        self.pen.line_to(x, y);
878    }
879
880    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
881        let (cx0, cy0) = self.transform(cx0, cy0);
882        let (x, y) = self.transform(x, y);
883        self.pen.quad_to(cx0, cy0, x, y);
884    }
885
886    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
887        let (cx0, cy0) = self.transform(cx0, cy0);
888        let (cx1, cy1) = self.transform(cx1, cy1);
889        let (x, y) = self.transform(x, y);
890        self.pen.curve_to(cx0, cy0, cx1, cy1, x, y);
891    }
892
893    fn close(&mut self) {
894        self.pen.close();
895    }
896}
897
898#[cfg(test)]
899mod tests {
900    use super::*;
901    use crate::outline::pen::PathElement;
902    use read_fonts::{FontRef, TableProvider};
903
904    fn coord(value: f32) -> F2Dot14 {
905        F2Dot14::from_f32(value)
906    }
907
908    fn assert_close(actual: f32, expected: f32) {
909        let diff = (actual - expected).abs();
910        assert!(
911            diff <= 1e-6,
912            "expected {expected}, got {actual}, diff {diff}"
913        );
914    }
915
916    fn path_head_signature(path: &[PathElement], n: usize) -> Vec<String> {
917        path.iter()
918            .take(n)
919            .map(|el| match *el {
920                PathElement::MoveTo { x, y } => format!("M{:.2},{:.2}", x, y),
921                PathElement::LineTo { x, y } => format!("L{:.2},{:.2}", x, y),
922                PathElement::QuadTo { cx0, cy0, x, y } => {
923                    format!("Q{:.2},{:.2} {:.2},{:.2}", cx0, cy0, x, y)
924                }
925                PathElement::CurveTo {
926                    cx0,
927                    cy0,
928                    cx1,
929                    cy1,
930                    x,
931                    y,
932                } => format!(
933                    "C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
934                    cx0, cy0, cx1, cy1, x, y
935                ),
936                PathElement::Close => "Z".to_string(),
937            })
938            .collect()
939    }
940
941    #[test]
942    fn expand_coords_pads_and_truncates() {
943        let mut out = CoordVec::new();
944        expand_coords(&mut out, 4, &[coord(0.25), coord(-0.5)]);
945        assert_eq!(
946            out.as_slice(),
947            &[coord(0.25), coord(-0.5), F2Dot14::ZERO, F2Dot14::ZERO]
948        );
949
950        expand_coords(&mut out, 1, &[coord(0.25), coord(-0.5)]);
951        assert_eq!(out.as_slice(), &[coord(0.25)]);
952    }
953
954    #[test]
955    fn scale_matrix_only_scales_translation() {
956        let matrix = Matrix::from_elements([1.0, 2.0, 3.0, 4.0, 5.0, -6.0]);
957        assert_eq!(
958            scale_matrix(matrix, 10.0).elements(),
959            [1.0, 2.0, 3.0, 4.0, 50.0, -60.0]
960        );
961    }
962
963    #[test]
964    fn compute_tuple_deltas_no_variation_index_is_noop_after_resize() {
965        let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
966        let varc = font.varc().unwrap();
967        let store = varc.multi_var_store().unwrap().unwrap();
968        let regions = store.region_list().unwrap();
969        let mut cache = ScalarCache::new(regions.region_count() as usize);
970        let mut out = DeltaVec::new();
971        out.push(42.0);
972
973        compute_tuple_deltas(
974            &store,
975            &regions,
976            NO_VARIATION_INDEX,
977            &[coord(0.0)],
978            3,
979            &mut cache,
980            &mut out,
981        )
982        .unwrap();
983        assert_eq!(out.as_slice(), &[0.0, 0.0, 0.0]);
984
985        compute_tuple_deltas(
986            &store,
987            &regions,
988            NO_VARIATION_INDEX,
989            &[coord(0.0)],
990            0,
991            &mut cache,
992            &mut out,
993        )
994        .unwrap();
995        assert!(out.is_empty());
996    }
997
998    #[test]
999    fn compute_tuple_deltas_invalid_outer_index_errors() {
1000        let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
1001        let varc = font.varc().unwrap();
1002        let store = varc.multi_var_store().unwrap().unwrap();
1003        let regions = store.region_list().unwrap();
1004        let mut cache = ScalarCache::new(regions.region_count() as usize);
1005        let mut out = DeltaVec::new();
1006
1007        let err = compute_tuple_deltas(&store, &regions, 0xFFFF_0000, &[], 1, &mut cache, &mut out)
1008            .unwrap_err();
1009        assert!(matches!(err, ReadError::InvalidCollectionIndex(_)));
1010    }
1011
1012    #[test]
1013    fn compute_tuple_deltas_matches_manual_decode() {
1014        let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
1015        let varc = font.varc().unwrap();
1016        let store = varc.multi_var_store().unwrap().unwrap();
1017        let regions = store.region_list().unwrap();
1018        let region_list = regions.regions();
1019        let coords = [coord(0.5); 8];
1020
1021        let mut tried = 0usize;
1022        for (outer, data) in store.variation_data().iter().enumerate() {
1023            let data = data.unwrap();
1024            let region_count = data.region_indices().len();
1025            if region_count == 0 {
1026                continue;
1027            }
1028            let delta_set_count = data.delta_sets().unwrap().count() as usize;
1029            for inner in 0..delta_set_count.min(3) {
1030                let decoded = data.delta_set(inner).unwrap().iter().collect::<Vec<_>>();
1031                if decoded.is_empty() || decoded.len() % region_count != 0 {
1032                    continue;
1033                }
1034                let tuple_len = decoded.len() / region_count;
1035                let var_idx = ((outer as u32) << 16) | inner as u32;
1036
1037                let mut cache = ScalarCache::new(regions.region_count() as usize);
1038                let mut actual = DeltaVec::new();
1039                compute_tuple_deltas(
1040                    &store,
1041                    &regions,
1042                    var_idx,
1043                    &coords,
1044                    tuple_len,
1045                    &mut cache,
1046                    &mut actual,
1047                )
1048                .unwrap();
1049                let first = actual.as_slice().to_vec();
1050
1051                // Same cache after population should not alter results.
1052                compute_tuple_deltas(
1053                    &store,
1054                    &regions,
1055                    var_idx,
1056                    &coords,
1057                    tuple_len,
1058                    &mut cache,
1059                    &mut actual,
1060                )
1061                .unwrap();
1062                assert_eq!(actual.as_slice(), first.as_slice());
1063
1064                let mut expected = vec![0.0f32; tuple_len];
1065                for (region_order, region_idx) in data.region_indices().iter().enumerate() {
1066                    let scalar = region_list
1067                        .get(region_idx.get() as usize)
1068                        .unwrap()
1069                        .compute_scalar_f32(&coords);
1070                    if scalar == 0.0 {
1071                        continue;
1072                    }
1073                    let base = region_order * tuple_len;
1074                    for (i, slot) in expected.iter_mut().enumerate() {
1075                        *slot += decoded[base + i] as f32 * scalar;
1076                    }
1077                }
1078                assert_eq!(actual.len(), expected.len());
1079                for (a, e) in actual.iter().zip(expected.iter()) {
1080                    assert_close(*a, *e);
1081                }
1082                tried += 1;
1083            }
1084        }
1085        assert!(tried > 0, "expected at least one tuple to be exercised");
1086    }
1087
1088    fn apply_transform_variations_reference(
1089        component: &VarcComponent<'_>,
1090        coords: &[F2Dot14],
1091        transform: &mut DecomposedTransform,
1092        var_store: Option<&MultiItemVariationStore<'_>>,
1093        regions: Option<&SparseVariationRegionList<'_>>,
1094        scalar_cache: &mut ScalarCache,
1095        deltas: &mut DeltaVec,
1096    ) -> Result<(), DrawError> {
1097        let Some(var_idx) = component.transform_var_index() else {
1098            return Ok(());
1099        };
1100        let flags = component.flags();
1101        const TRANSFORM_MASK: VarcFlags = VarcFlags::from_bits_truncate(
1102            VarcFlags::HAVE_TRANSLATE_X.bits()
1103                | VarcFlags::HAVE_TRANSLATE_Y.bits()
1104                | VarcFlags::HAVE_ROTATION.bits()
1105                | VarcFlags::HAVE_SCALE_X.bits()
1106                | VarcFlags::HAVE_SCALE_Y.bits()
1107                | VarcFlags::HAVE_SKEW_X.bits()
1108                | VarcFlags::HAVE_SKEW_Y.bits()
1109                | VarcFlags::HAVE_TCENTER_X.bits()
1110                | VarcFlags::HAVE_TCENTER_Y.bits(),
1111        );
1112        let field_count = (flags.bits() & TRANSFORM_MASK.bits()).count_ones() as usize;
1113        if field_count == 0 {
1114            return Ok(());
1115        }
1116
1117        let store = var_store.ok_or(ReadError::NullOffset)?;
1118        let regions = regions.ok_or(ReadError::NullOffset)?;
1119        compute_tuple_deltas(
1120            store,
1121            regions,
1122            var_idx,
1123            coords,
1124            field_count,
1125            scalar_cache,
1126            deltas,
1127        )?;
1128
1129        let mut delta_iter = deltas.iter().copied();
1130        if flags.contains(VarcFlags::HAVE_TRANSLATE_X) {
1131            transform.set_translate_x(transform.translate_x() + delta_iter.next().unwrap_or(0.0));
1132        }
1133        if flags.contains(VarcFlags::HAVE_TRANSLATE_Y) {
1134            transform.set_translate_y(transform.translate_y() + delta_iter.next().unwrap_or(0.0));
1135        }
1136        if flags.contains(VarcFlags::HAVE_ROTATION) {
1137            transform
1138                .set_rotation(transform.rotation() + delta_iter.next().unwrap_or(0.0) / 4096.0);
1139        }
1140        if flags.contains(VarcFlags::HAVE_SCALE_X) {
1141            transform.set_scale_x(transform.scale_x() + delta_iter.next().unwrap_or(0.0) / 1024.0);
1142        }
1143        if flags.contains(VarcFlags::HAVE_SCALE_Y) {
1144            transform.set_scale_y(transform.scale_y() + delta_iter.next().unwrap_or(0.0) / 1024.0);
1145        }
1146        const SKEW_OR_CENTER: VarcFlags = VarcFlags::from_bits_truncate(
1147            VarcFlags::HAVE_SKEW_X.bits()
1148                | VarcFlags::HAVE_SKEW_Y.bits()
1149                | VarcFlags::HAVE_TCENTER_X.bits()
1150                | VarcFlags::HAVE_TCENTER_Y.bits(),
1151        );
1152        if flags.intersects(SKEW_OR_CENTER) {
1153            if flags.contains(VarcFlags::HAVE_SKEW_X) {
1154                transform
1155                    .set_skew_x(transform.skew_x() + delta_iter.next().unwrap_or(0.0) / 4096.0);
1156            }
1157            if flags.contains(VarcFlags::HAVE_SKEW_Y) {
1158                transform
1159                    .set_skew_y(transform.skew_y() + delta_iter.next().unwrap_or(0.0) / 4096.0);
1160            }
1161            if flags.contains(VarcFlags::HAVE_TCENTER_X) {
1162                transform.set_center_x(transform.center_x() + delta_iter.next().unwrap_or(0.0));
1163            }
1164            if flags.contains(VarcFlags::HAVE_TCENTER_Y) {
1165                transform.set_center_y(transform.center_y() + delta_iter.next().unwrap_or(0.0));
1166            }
1167        }
1168        if !flags.contains(VarcFlags::HAVE_SCALE_Y) {
1169            transform.set_scale_y(transform.scale_x());
1170        }
1171        Ok(())
1172    }
1173
1174    #[test]
1175    fn apply_transform_variations_matches_reference_path() {
1176        let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
1177        let outlines = Outlines::new(&font).unwrap();
1178        let coverage = outlines.varc.coverage().unwrap();
1179        let var_store = outlines.varc.multi_var_store().transpose().unwrap();
1180        let regions = var_store
1181            .as_ref()
1182            .map(|s| s.region_list())
1183            .transpose()
1184            .unwrap();
1185        let region_count = regions
1186            .as_ref()
1187            .map(|r| r.region_count() as usize)
1188            .unwrap_or(0);
1189
1190        let mut coords = CoordVec::new();
1191        coords.resize_and_fill(outlines.axis_count, F2Dot14::ZERO);
1192        for (i, c) in coords.iter_mut().enumerate() {
1193            *c = match i % 4 {
1194                0 => coord(0.5),
1195                1 => coord(-0.5),
1196                2 => coord(0.25),
1197                _ => coord(-0.25),
1198            };
1199        }
1200
1201        let mut tested = 0usize;
1202        for gid16 in coverage.iter() {
1203            let gid: GlyphId = gid16.into();
1204            let coverage_index = coverage.get(gid).unwrap() as usize;
1205            let glyph = outlines.varc.glyph(coverage_index).unwrap();
1206            for component in glyph.components() {
1207                let component = component.unwrap();
1208                if component.transform_var_index().is_none() {
1209                    continue;
1210                }
1211
1212                let mut transform_new = *component.transform();
1213                let mut transform_ref = *component.transform();
1214                let mut cache_new = ScalarCache::new(region_count);
1215                let mut cache_ref = ScalarCache::new(region_count);
1216                let mut deltas_ref = DeltaVec::new();
1217                let mut scratch = Scratchpad::new();
1218                let store_regions = var_store.as_ref().zip(regions.as_ref());
1219
1220                outlines
1221                    .apply_transform_variations(
1222                        &component,
1223                        &coords,
1224                        &mut transform_new,
1225                        &mut cache_new,
1226                        &mut scratch,
1227                        store_regions,
1228                    )
1229                    .unwrap();
1230                apply_transform_variations_reference(
1231                    &component,
1232                    &coords,
1233                    &mut transform_ref,
1234                    var_store.as_ref(),
1235                    regions.as_ref(),
1236                    &mut cache_ref,
1237                    &mut deltas_ref,
1238                )
1239                .unwrap();
1240
1241                assert_close(transform_new.translate_x(), transform_ref.translate_x());
1242                assert_close(transform_new.translate_y(), transform_ref.translate_y());
1243                assert_close(transform_new.rotation(), transform_ref.rotation());
1244                assert_close(transform_new.scale_x(), transform_ref.scale_x());
1245                assert_close(transform_new.scale_y(), transform_ref.scale_y());
1246                assert_close(transform_new.skew_x(), transform_ref.skew_x());
1247                assert_close(transform_new.skew_y(), transform_ref.skew_y());
1248                assert_close(transform_new.center_x(), transform_ref.center_x());
1249                assert_close(transform_new.center_y(), transform_ref.center_y());
1250                tested += 1;
1251                if tested >= 32 {
1252                    break;
1253                }
1254            }
1255            if tested >= 32 {
1256                break;
1257            }
1258        }
1259        assert!(tested > 0, "expected at least one transformed component");
1260    }
1261
1262    #[test]
1263    fn draw_varc_6868_freetype_path_head_snapshot() {
1264        let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
1265        let outlines = Outlines::new(&font).unwrap();
1266        let gid = font.cmap().unwrap().map_codepoint(0x6868_u32).unwrap();
1267        let outline = outlines.outline(gid).unwrap().unwrap();
1268        let mut memory = vec![0u8; outline.required_buffer_size()];
1269        let mut pen = Vec::<PathElement>::new();
1270        outlines
1271            .draw(
1272                &outline,
1273                &mut memory,
1274                Size::unscaled(),
1275                &[],
1276                PathStyle::FreeType,
1277                &mut pen,
1278            )
1279            .unwrap();
1280
1281        let head = path_head_signature(&pen, 8);
1282        assert_eq!(
1283            head,
1284            vec![
1285                "M454.56,574.77".to_string(),
1286                "Q477.58,585.56 499.80,598.25".to_string(),
1287                "Q522.02,610.95 543.36,625.16".to_string(),
1288                "Q564.71,639.37 584.54,655.19".to_string(),
1289                "Q604.38,671.02 623.03,688.41".to_string(),
1290                "Q641.67,705.80 658.41,724.46".to_string(),
1291                "Q675.14,743.12 689.79,763.21".to_string(),
1292                "Q704.43,783.31 717.03,804.56".to_string(),
1293            ]
1294        );
1295    }
1296
1297    // Build the store/regions needed by `eval_condition`'s signature. The
1298    // conditions exercised below never touch the variation store, so any valid
1299    // VARC store works here.
1300    fn condition_eval_env() -> (
1301        FontRef<'static>,
1302        MultiItemVariationStore<'static>,
1303        SparseVariationRegionList<'static>,
1304    ) {
1305        let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
1306        let varc = font.varc().unwrap();
1307        let store = varc.multi_var_store().unwrap().unwrap();
1308        let regions = store.region_list().unwrap();
1309        (font, store, regions)
1310    }
1311
1312    // A deeply nested condition tree must return an error instead of overflowing
1313    // the stack. Regression test for the unbounded recursion in `eval_condition`.
1314    #[test]
1315    fn eval_condition_bounds_recursion_depth() {
1316        use read_fonts::{FontData, FontRead};
1317        // Linear chain of Format5Negate tables: each is `format(u16 BE = 5)` +
1318        // `condition_offset(Offset24 BE = 5)`, so every table points 5 bytes
1319        // forward to the next -> an arbitrarily deep condition tree. `CHAIN_LEN`
1320        // is far larger than the limit, proving evaluation bails out early rather
1321        // than walking (and recursing into) the whole chain.
1322        const CHAIN_LEN: usize = 4096;
1323        let mut bytes = Vec::with_capacity(CHAIN_LEN * 5);
1324        for _ in 0..CHAIN_LEN {
1325            bytes.extend_from_slice(&[0x00, 0x05, 0x00, 0x00, 0x05]);
1326        }
1327        let cond = Condition::read(FontData::new(&bytes)).unwrap();
1328
1329        let (_font, store, regions) = condition_eval_env();
1330        let mut cache = ScalarCache::new(regions.region_count() as usize);
1331        let mut scratch = Scratchpad::new();
1332
1333        let result =
1334            Outlines::eval_condition(&cond, &[], &store, &regions, &mut cache, &mut scratch, 0);
1335        assert!(
1336            matches!(result, Err(DrawError::RecursionLimitExceeded(_))),
1337            "expected RecursionLimitExceeded, got {result:?}"
1338        );
1339    }
1340
1341    // A normal (shallow) condition tree must still evaluate correctly.
1342    #[test]
1343    fn eval_condition_shallow_is_correct() {
1344        use read_fonts::{FontData, FontRead};
1345        let (_font, store, regions) = condition_eval_env();
1346        let mut cache = ScalarCache::new(regions.region_count() as usize);
1347        let mut scratch = Scratchpad::new();
1348
1349        // ConditionFormat1: axis 0, filter range [-1.0, 1.0].
1350        let leaf: [u8; 8] = [0x00, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x40, 0x00];
1351        let cond = Condition::read(FontData::new(&leaf)).unwrap();
1352        assert!(Outlines::eval_condition(
1353            &cond,
1354            &[coord(0.5)],
1355            &store,
1356            &regions,
1357            &mut cache,
1358            &mut scratch,
1359            0,
1360        )
1361        .unwrap());
1362        assert!(!Outlines::eval_condition(
1363            &cond,
1364            &[coord(1.5)],
1365            &store,
1366            &regions,
1367            &mut cache,
1368            &mut scratch,
1369            0,
1370        )
1371        .unwrap());
1372
1373        // Format5Negate wrapping the same leaf must negate the result, proving a
1374        // shallow nested tree still evaluates through the recursion guard.
1375        let mut negate = vec![0x00, 0x05, 0x00, 0x00, 0x05];
1376        negate.extend_from_slice(&leaf);
1377        let cond = Condition::read(FontData::new(&negate)).unwrap();
1378        assert!(!Outlines::eval_condition(
1379            &cond,
1380            &[coord(0.5)],
1381            &store,
1382            &regions,
1383            &mut cache,
1384            &mut scratch,
1385            0,
1386        )
1387        .unwrap());
1388    }
1389
1390    /// The `multi_var_store_offset` field of the VARC table is `#[nullable]`, so a
1391    /// spec-valid font may omit the `MultiItemVariationStore`. Drawing such a glyph
1392    /// must not panic. Regression test for the `.unwrap()` on the `Option` returned
1393    /// by `scalar_cache_from_store`.
1394    #[test]
1395    fn draw_varc_with_null_var_store_does_not_panic() {
1396        use crate::{instance::LocationRef, outline::DrawSettings, MetadataProvider};
1397        use read_fonts::types::Tag;
1398
1399        let mut bytes = font_test_data::varc::CJK_6868.to_vec();
1400        // Find the VARC table via the public table directory rather than parsing the
1401        // sfnt header by hand.
1402        let varc_off = {
1403            let font = FontRef::new(&bytes).unwrap();
1404            font.table_directory()
1405                .table_records()
1406                .iter()
1407                .find(|rec| rec.tag() == Tag::new(b"VARC"))
1408                .expect("VARC table present")
1409                .offset() as usize
1410        };
1411        // VARC layout: version (4) + coverage_offset (4) + multi_var_store_offset (4) ...
1412        // Null the (nullable) multi_var_store_offset so `multi_var_store()` -> None.
1413        for b in &mut bytes[varc_off + 8..varc_off + 12] {
1414            *b = 0;
1415        }
1416
1417        let font = FontRef::new(&bytes).unwrap();
1418        let outlines = font.outline_glyphs();
1419        let gid = font.cmap().unwrap().map_codepoint(0x6868_u32).unwrap();
1420        let glyph = outlines.get(gid).expect("covered VARC glyph");
1421        let mut pen = Vec::<PathElement>::new();
1422        // Must complete without panicking (Ok, or a graceful DrawError).
1423        let _ = glyph.draw(
1424            DrawSettings::unhinted(Size::unscaled(), LocationRef::default()),
1425            &mut pen,
1426        );
1427    }
1428
1429    fn first_nested_varc_edge(outlines: &Outlines<'_>) -> Option<(GlyphId, u16)> {
1430        for gid16 in outlines.coverage.iter() {
1431            let gid: GlyphId = gid16.into();
1432            let coverage_index = outlines.coverage.get(gid)?;
1433            let glyph = outlines.varc.glyph(coverage_index as usize).ok()?;
1434            for component in glyph.components() {
1435                let component = component.ok()?;
1436                // Pick an unconditional child edge so the traversal always attempts
1437                // to recurse for this component.
1438                if component.condition_index().is_none()
1439                    && component.gid() != gid
1440                    && outlines.coverage.get(component.gid()).is_some()
1441                {
1442                    return Some((gid, coverage_index));
1443                }
1444            }
1445        }
1446        None
1447    }
1448
1449    #[test]
1450    fn draw_glyph_respects_total_edge_budget() {
1451        let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
1452        let outlines = Outlines::new(&font).unwrap();
1453        let (root_gid, root_cov_idx) = first_nested_varc_edge(&outlines)
1454            .expect("expected at least one VARC->VARC component edge in fixture font");
1455        let mut coords = CoordVec::new();
1456        expand_coords(&mut coords, outlines.axis_count, &[]);
1457        let ctx = VarcSharedContext {
1458            font_coords: &coords,
1459            size: Size::unscaled(),
1460            path_style: PathStyle::default(),
1461            coverage: &outlines.coverage,
1462            var_store: outlines.var_store.as_ref(),
1463            store_regions: outlines.var_store.as_ref().zip(outlines.regions.as_ref()),
1464        };
1465        let outline = outlines.outline(root_gid).unwrap().unwrap();
1466        let mut memory = vec![0u8; outline.required_buffer_size()];
1467        let mut pen = Vec::<PathElement>::new();
1468        let mut stack = GlyphStack::new();
1469        let mut scalar_cache = outlines
1470            .scalar_cache_from_store(outlines.var_store.as_ref())
1471            .unwrap();
1472        let mut scratch = Scratchpad::new();
1473        // Budget of one edge allows entering the root glyph only; any recursive
1474        // component edge must be rejected as over budget.
1475        scratch.edges_left = 1;
1476        let result = outlines.draw_glyph(
1477            root_gid,
1478            root_cov_idx,
1479            &coords,
1480            Affine::IDENTITY,
1481            &ctx,
1482            &mut memory,
1483            &mut pen,
1484            &mut stack,
1485            &mut scalar_cache,
1486            &mut scratch,
1487        );
1488        assert!(
1489            matches!(result, Err(DrawError::RecursionLimitExceeded(_))),
1490            "expected RecursionLimitExceeded when edge budget is exhausted, got {result:?}"
1491        );
1492    }
1493
1494    #[test]
1495    fn max_component_memory_respects_total_edge_budget() {
1496        let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
1497        let outlines = Outlines::new(&font).unwrap();
1498        let (root_gid, root_cov_idx) = first_nested_varc_edge(&outlines)
1499            .expect("expected at least one VARC->VARC component edge in fixture font");
1500        let mut stack = GlyphStack::new();
1501        let mut edges_left = 0;
1502        let memory = outlines
1503            .max_component_memory_for_glyph(root_gid, root_cov_idx, &mut stack, &mut edges_left)
1504            .unwrap();
1505        assert_eq!(memory, 0);
1506        assert_eq!(edges_left, 0);
1507    }
1508}