Skip to main content

read_fonts/model/glyph/color/
bounds.rs

1//! Color glyph extents computed from what the glyph paints.
2//!
3//! Provides a [`PaintSink`] that tracks the transform and clip stacks and
4//! takes the union of the clips of all fills, as HarfBuzz's
5//! `hb-paint-extents` does. A fill clipped to an outline is measured by the
6//! control box of that outline.
7
8use super::{Brush, CompositeMode, PaintSink};
9use crate::{
10    model::glyph::outline::OutlineGlyphs,
11    types::{BoundingBox, GlyphId, Matrix},
12};
13use smallvec::SmallVec;
14
15/// The area covered by a clip or by the paint inside it.
16#[derive(Clone, Copy)]
17enum Bounds {
18    /// The whole plane, because no clip has been pushed.
19    Unbounded,
20    /// A box with nonzero area.
21    Bounded(BoundingBox<f32>),
22    /// No area.
23    Empty,
24}
25
26impl Bounds {
27    /// Creates bounds from a box, or `Empty` if the box has no area.
28    ///
29    /// HarfBuzz treats a box with no width or height as empty.
30    fn of(bounds: BoundingBox<f32>) -> Self {
31        if bounds.x_min < bounds.x_max && bounds.y_min < bounds.y_max {
32            Bounds::Bounded(bounds)
33        } else {
34            Bounds::Empty
35        }
36    }
37
38    /// Expands to also cover `other`.
39    fn union(&mut self, other: Bounds) {
40        match (*self, other) {
41            (_, Bounds::Unbounded) => *self = Bounds::Unbounded,
42            (Bounds::Empty, Bounds::Bounded(_)) => *self = other,
43            (Bounds::Bounded(a), Bounds::Bounded(b)) => {
44                *self = Bounds::Bounded(BoundingBox {
45                    x_min: a.x_min.min(b.x_min),
46                    y_min: a.y_min.min(b.y_min),
47                    x_max: a.x_max.max(b.x_max),
48                    y_max: a.y_max.max(b.y_max),
49                })
50            }
51            _ => {}
52        }
53    }
54
55    /// Shrinks to the area also covered by `other`.
56    fn intersect(&mut self, other: Bounds) {
57        match (*self, other) {
58            (_, Bounds::Empty) => *self = Bounds::Empty,
59            (Bounds::Unbounded, Bounds::Bounded(_)) => *self = other,
60            (Bounds::Bounded(a), Bounds::Bounded(b)) => {
61                *self = Bounds::of(BoundingBox {
62                    x_min: a.x_min.max(b.x_min),
63                    y_min: a.y_min.max(b.y_min),
64                    x_max: a.x_max.min(b.x_max),
65                    y_max: a.y_max.min(b.y_max),
66                });
67            }
68            _ => {}
69        }
70    }
71}
72
73/// Returns the bounding box of the corners of `bounds` after applying
74/// `transform`.
75fn transform_bounds(transform: &Matrix<f32>, bounds: BoundingBox<f32>) -> BoundingBox<f32> {
76    let corners = [
77        transform.transform(bounds.x_min, bounds.y_min),
78        transform.transform(bounds.x_max, bounds.y_min),
79        transform.transform(bounds.x_min, bounds.y_max),
80        transform.transform(bounds.x_max, bounds.y_max),
81    ];
82    let (first, rest) = corners.split_first().unwrap();
83    rest.iter().fold(
84        BoundingBox {
85            x_min: first.0,
86            y_min: first.1,
87            x_max: first.0,
88            y_max: first.1,
89        },
90        |bounds, (x, y)| BoundingBox {
91            x_min: bounds.x_min.min(*x),
92            y_min: bounds.y_min.min(*y),
93            x_max: bounds.x_max.max(*x),
94            y_max: bounds.y_max.max(*y),
95        },
96    )
97}
98
99/// A sink that paints nothing and measures what would have been painted.
100pub(super) struct BoundsSink<'a> {
101    outlines: OutlineGlyphs<'a>,
102    /// The transformation matrix at each depth. The last one is current.
103    transforms: SmallVec<[Matrix<f32>; 8]>,
104    /// The clip at each depth, in root coordinates. The last one is current.
105    clips: SmallVec<[Bounds; 8]>,
106    /// The area painted by each open layer. The last one is current.
107    groups: SmallVec<[Bounds; 8]>,
108}
109
110impl<'a> BoundsSink<'a> {
111    pub(super) fn new(outlines: OutlineGlyphs<'a>) -> Self {
112        let mut sink = Self {
113            outlines,
114            transforms: SmallVec::new(),
115            clips: SmallVec::new(),
116            groups: SmallVec::new(),
117        };
118        sink.transforms.push(Matrix::IDENTITY);
119        sink.clips.push(Bounds::Unbounded);
120        sink.groups.push(Bounds::Empty);
121        sink
122    }
123
124    /// Returns the bounding box of everything painted, or an empty box at
125    /// the origin if nothing was painted.
126    pub(super) fn bounds(&self) -> BoundingBox<f32> {
127        match self.groups.last() {
128            Some(Bounds::Bounded(bounds)) => *bounds,
129            _ => BoundingBox::default(),
130        }
131    }
132
133    fn current_transform(&self) -> &Matrix<f32> {
134        self.transforms.last().unwrap_or(&Matrix::IDENTITY)
135    }
136
137    fn current_clip(&self) -> Bounds {
138        self.clips.last().copied().unwrap_or(Bounds::Unbounded)
139    }
140
141    /// Clips to `bounds`, given in the current coordinate space.
142    fn push_clip(&mut self, bounds: Bounds) {
143        let mut clip = self.current_clip();
144        clip.intersect(bounds);
145        self.clips.push(clip);
146    }
147
148    /// Records a fill of the current clip.
149    fn paint(&mut self) {
150        let clip = self.current_clip();
151        if let Some(group) = self.groups.last_mut() {
152            group.union(clip);
153        }
154    }
155}
156
157impl PaintSink for BoundsSink<'_> {
158    fn push_transform(&mut self, transform: Matrix<f32>) {
159        let transform = *self.current_transform() * transform;
160        self.transforms.push(transform);
161    }
162
163    fn pop_transform(&mut self) {
164        if self.transforms.len() > 1 {
165            self.transforms.pop();
166        }
167    }
168
169    fn push_clip_glyph(&mut self, glyph: GlyphId) {
170        // The outline's control box, as a drawn glyph is measured.
171        let bounds = match self
172            .outlines
173            .get(glyph)
174            .and_then(|glyph| glyph.control_box().ok())
175        {
176            Some(Some(bounds)) => Bounds::of(transform_bounds(self.current_transform(), bounds)),
177            _ => Bounds::Empty,
178        };
179        self.push_clip(bounds);
180    }
181
182    fn push_clip_box(&mut self, clip_box: BoundingBox<f32>) {
183        let bounds = Bounds::of(transform_bounds(self.current_transform(), clip_box));
184        self.push_clip(bounds);
185    }
186
187    fn pop_clip(&mut self) {
188        if self.clips.len() > 1 {
189            self.clips.pop();
190        }
191    }
192
193    fn fill(&mut self, _brush: Brush<'_>) {
194        self.paint();
195    }
196
197    fn push_layer(&mut self, _composite_mode: CompositeMode) {
198        self.groups.push(Bounds::Empty);
199    }
200
201    fn pop_layer_with_mode(&mut self, composite_mode: CompositeMode) {
202        if self.groups.len() < 2 {
203            return;
204        }
205        let source = self.groups.pop().unwrap_or(Bounds::Empty);
206        let Some(backdrop) = self.groups.last_mut() else {
207            return;
208        };
209        match composite_mode {
210            CompositeMode::Clear => *backdrop = Bounds::Empty,
211            CompositeMode::Src | CompositeMode::SrcOut => *backdrop = source,
212            CompositeMode::Dest | CompositeMode::DestOut => {}
213            CompositeMode::SrcIn | CompositeMode::DestIn => backdrop.intersect(source),
214            _ => backdrop.union(source),
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    fn boxed(x_min: f32, y_min: f32, x_max: f32, y_max: f32) -> Bounds {
224        Bounds::Bounded(BoundingBox {
225            x_min,
226            y_min,
227            x_max,
228            y_max,
229        })
230    }
231
232    #[test]
233    fn union_and_intersection() {
234        let mut a = boxed(0.0, 0.0, 10.0, 10.0);
235        a.union(boxed(5.0, -5.0, 20.0, 5.0));
236        assert!(
237            matches!(a, Bounds::Bounded(b) if b == BoundingBox { x_min: 0.0, y_min: -5.0, x_max: 20.0, y_max: 10.0 })
238        );
239        a.intersect(boxed(15.0, 0.0, 30.0, 30.0));
240        assert!(
241            matches!(a, Bounds::Bounded(b) if b == BoundingBox { x_min: 15.0, y_min: 0.0, x_max: 20.0, y_max: 10.0 })
242        );
243        a.intersect(boxed(100.0, 100.0, 200.0, 200.0));
244        assert!(matches!(a, Bounds::Empty));
245        let mut unbounded = Bounds::Unbounded;
246        unbounded.intersect(boxed(1.0, 2.0, 3.0, 4.0));
247        assert!(matches!(unbounded, Bounds::Bounded(_)));
248        let mut empty = Bounds::Empty;
249        empty.union(Bounds::Unbounded);
250        assert!(matches!(empty, Bounds::Unbounded));
251    }
252
253    // A box with no width or height covers nothing, as in HarfBuzz.
254    #[test]
255    fn a_box_without_area_is_empty() {
256        let line = BoundingBox {
257            x_min: 5.0,
258            y_min: 0.0,
259            x_max: 5.0,
260            y_max: 10.0,
261        };
262        assert!(matches!(Bounds::of(line), Bounds::Empty));
263        // Boxes that only touch share no area.
264        let mut a = boxed(0.0, 0.0, 5.0, 10.0);
265        a.intersect(boxed(5.0, 0.0, 10.0, 10.0));
266        assert!(matches!(a, Bounds::Empty));
267        // A fill inside a clip without area adds nothing.
268        let font = crate::model::Font::new(font_test_data::COLRV0V1_VARIABLE, 0).unwrap();
269        let mut sink = BoundsSink::new(font.outline_glyphs());
270        sink.push_clip_box(line);
271        sink.fill(Brush::Solid {
272            palette_index: 0,
273            alpha: 1.0,
274        });
275        sink.pop_clip();
276        assert_eq!(sink.bounds(), BoundingBox::default());
277    }
278
279    #[test]
280    fn a_transformed_box_is_the_box_around_its_corners() {
281        // Rotate a quarter turn: x becomes y.
282        let rotate = Matrix {
283            xx: 0.0,
284            yx: 1.0,
285            xy: -1.0,
286            yy: 0.0,
287            dx: 0.0,
288            dy: 0.0,
289        };
290        let bounds = transform_bounds(
291            &rotate,
292            BoundingBox {
293                x_min: 1.0,
294                y_min: 2.0,
295                x_max: 3.0,
296                y_max: 4.0,
297            },
298        );
299        assert_eq!(
300            bounds,
301            BoundingBox {
302                x_min: -4.0,
303                y_min: 1.0,
304                x_max: -2.0,
305                y_max: 3.0,
306            }
307        );
308    }
309}