Skip to main content

read_fonts/model/metrics/
style.rs

1//! Metrics used to style text and decorations.
2
3use super::metric;
4use crate::model::variation::NormalizedCoord;
5use crate::{
6    ps::{cff::CffFontRef, type1::Type1Font},
7    tables::mvar::tags,
8    TableProvider,
9};
10use types::{F48Dot16, Fixed};
11
12/// The size and placement of a subscript or superscript em box.
13///
14/// Values are in font units unless a caller names another unit.
15#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
16pub struct ScriptMetrics<T = F48Dot16> {
17    /// Suggested width of the script em box.
18    pub x_size: T,
19    /// Suggested height of the script em box.
20    pub y_size: T,
21    /// Horizontal offset from the base glyph.
22    pub x_offset: T,
23    /// Vertical offset from the baseline. A positive subscript offset is below
24    /// it; a positive superscript offset is above it, as in `OS/2`.
25    pub y_offset: T,
26}
27
28/// The position and thickness of an underline or strikethrough.
29///
30/// Position is measured from the baseline to the top of the stroke.
31/// Values are in font units unless a caller names another unit.
32#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
33pub struct Decoration<T = F48Dot16> {
34    /// Position of the top of the stroke relative to the baseline.
35    pub position: T,
36    /// Suggested stroke thickness.
37    pub thickness: T,
38}
39
40/// Font-wide style measurements from `OS/2` and `post`, or Type 1 font info.
41///
42/// Unlike [`super::Metrics`], reading these may require the `post` table.
43/// Each group is `None` if its source table is missing or unreadable. At a
44/// non-default location, `MVAR` deltas are applied without rounding. The italic
45/// angle remains the value in `post`; normalized coordinates alone do not
46/// provide the user-space `slnt` value needed to adjust it.
47#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
48pub struct StyleMetrics {
49    /// Design units per em, which the measurements use.
50    ///
51    /// The same as [`Metrics::units_per_em`](super::Metrics::units_per_em).
52    pub units_per_em: u16,
53    /// Suggested subscript em box and placement, from `OS/2`.
54    pub subscript: Option<ScriptMetrics>,
55    /// Suggested superscript em box and placement, from `OS/2`.
56    pub superscript: Option<ScriptMetrics>,
57    /// Suggested underline, from `post`.
58    pub underline: Option<Decoration>,
59    /// Suggested strikethrough, from `OS/2`.
60    pub strike_through: Option<Decoration>,
61    /// Italic angle in degrees, from `post`. This is not a length.
62    pub italic_angle: Option<Fixed>,
63    /// Whether `post` reports a fixed pitch.
64    pub is_fixed_pitch: Option<bool>,
65}
66
67impl StyleMetrics {
68    /// Reads style measurements from `tables` at `normalized_coords`.
69    ///
70    /// Pass an empty location for the font's defaults. Missing or unreadable
71    /// tables leave their measurements unset.
72    pub fn from_sfnt<'a>(
73        tables: &impl TableProvider<'a>,
74        normalized_coords: &[NormalizedCoord],
75    ) -> Self {
76        let coords = normalized_coords;
77        let mvar = (!coords.is_empty()).then(|| tables.mvar().ok()).flatten();
78        let deltas = mvar.as_ref().and_then(|mvar| mvar.at(coords));
79        let deltas = deltas.as_ref();
80        let mut metrics = Self {
81            units_per_em: super::units_per_em(tables.head().map_or(0, |head| head.units_per_em())),
82            ..Self::default()
83        };
84        if let Ok(os2) = tables.os2() {
85            metrics.subscript = Some(ScriptMetrics {
86                x_size: metric(os2.y_subscript_x_size() as i32, deltas, tags::SBXS),
87                y_size: metric(os2.y_subscript_y_size() as i32, deltas, tags::SBYS),
88                x_offset: metric(os2.y_subscript_x_offset() as i32, deltas, tags::SBXO),
89                y_offset: metric(os2.y_subscript_y_offset() as i32, deltas, tags::SBYO),
90            });
91            metrics.superscript = Some(ScriptMetrics {
92                x_size: metric(os2.y_superscript_x_size() as i32, deltas, tags::SPXS),
93                y_size: metric(os2.y_superscript_y_size() as i32, deltas, tags::SPYS),
94                x_offset: metric(os2.y_superscript_x_offset() as i32, deltas, tags::SPXO),
95                y_offset: metric(os2.y_superscript_y_offset() as i32, deltas, tags::SPYO),
96            });
97            metrics.strike_through = Some(Decoration {
98                position: metric(os2.y_strikeout_position() as i32, deltas, tags::STRO),
99                thickness: metric(os2.y_strikeout_size() as i32, deltas, tags::STRS),
100            });
101        }
102        if let Ok(post) = tables.post() {
103            metrics.underline = Some(Decoration {
104                position: metric(
105                    post.underline_position().to_i16() as i32,
106                    deltas,
107                    tags::UNDO,
108                ),
109                thickness: metric(
110                    post.underline_thickness().to_i16() as i32,
111                    deltas,
112                    tags::UNDS,
113                ),
114            });
115            metrics.italic_angle = Some(post.italic_angle());
116            metrics.is_fixed_pitch = Some(post.is_fixed_pitch() != 0);
117        }
118        metrics
119    }
120
121    /// Reads the style measurements from a Type 1 font.
122    ///
123    /// Its underline center is converted to the top edge used by
124    /// [`Decoration`]. Type 1 has no subscript, superscript or strikethrough.
125    pub fn from_type1(font: &Type1Font) -> Self {
126        // Type 1 locates the underline by its center, whereas `post` locates
127        // its top edge. Keep the public decoration convention consistent.
128        let underline_top = F48Dot16::from_i32(font.underline_position())
129            + F48Dot16::from_bits((font.underline_thickness() as i64) << 15);
130        Self {
131            units_per_em: super::units_per_em(font.upem().clamp(0, u16::MAX as i32) as u16),
132            underline: Some(Decoration {
133                position: underline_top,
134                thickness: F48Dot16::from_i32(font.underline_thickness()),
135            }),
136            italic_angle: Some(Fixed::from_i32(font.italic_angle())),
137            is_fixed_pitch: Some(font.is_fixed_pitch()),
138            ..Default::default()
139        }
140    }
141
142    /// Reads style measurements from a standalone CFF font.
143    pub fn from_cff(font: &CffFontRef<'_>) -> Self {
144        let units_per_em = super::units_per_em(font.upem().clamp(0, u16::MAX as i32) as u16);
145        let Some(metadata) = font.metadata() else {
146            return Self {
147                units_per_em,
148                ..Self::default()
149            };
150        };
151        let thickness = metadata.underline_thickness().to_f48dot16();
152        Self {
153            units_per_em,
154            underline: Some(Decoration {
155                position: metadata.underline_position().to_f48dot16()
156                    + F48Dot16::from_bits(thickness.to_bits() >> 1),
157                thickness,
158            }),
159            italic_angle: Some(metadata.italic_angle()),
160            is_fixed_pitch: Some(metadata.is_fixed_pitch()),
161            ..Default::default()
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::FontRef;
170
171    #[test]
172    fn reads_os2_and_post_without_losing_stated_zeroes() {
173        let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
174        let os2 = font.os2().unwrap();
175        let post = font.post().unwrap();
176        let metrics = StyleMetrics::from_sfnt(&font, &[]);
177        let subscript = metrics.subscript.unwrap();
178        assert_eq!(
179            subscript.x_size,
180            F48Dot16::from_i32(os2.y_subscript_x_size() as i32)
181        );
182        assert_eq!(
183            subscript.y_size,
184            F48Dot16::from_i32(os2.y_subscript_y_size() as i32)
185        );
186        assert_eq!(
187            subscript.x_offset,
188            F48Dot16::from_i32(os2.y_subscript_x_offset() as i32)
189        );
190        assert_eq!(
191            subscript.y_offset,
192            F48Dot16::from_i32(os2.y_subscript_y_offset() as i32)
193        );
194        let superscript = metrics.superscript.unwrap();
195        assert_eq!(
196            superscript.x_size,
197            F48Dot16::from_i32(os2.y_superscript_x_size() as i32)
198        );
199        assert_eq!(
200            superscript.y_size,
201            F48Dot16::from_i32(os2.y_superscript_y_size() as i32)
202        );
203        assert_eq!(
204            superscript.x_offset,
205            F48Dot16::from_i32(os2.y_superscript_x_offset() as i32)
206        );
207        assert_eq!(
208            superscript.y_offset,
209            F48Dot16::from_i32(os2.y_superscript_y_offset() as i32)
210        );
211        assert_eq!(
212            metrics.strike_through.unwrap().position,
213            F48Dot16::from_i32(os2.y_strikeout_position() as i32)
214        );
215        assert_eq!(
216            metrics.strike_through.unwrap().thickness,
217            F48Dot16::from_i32(os2.y_strikeout_size() as i32)
218        );
219        assert_eq!(
220            metrics.underline.unwrap().position,
221            F48Dot16::from_i32(post.underline_position().to_i16() as i32)
222        );
223        assert_eq!(
224            metrics.underline.unwrap().thickness,
225            F48Dot16::from_i32(post.underline_thickness().to_i16() as i32)
226        );
227        assert_eq!(metrics.italic_angle, Some(post.italic_angle()));
228        assert_eq!(metrics.is_fixed_pitch, Some(post.is_fixed_pitch() != 0));
229    }
230
231    #[test]
232    fn absent_tables_leave_style_metrics_unset() {
233        let font = FontRef::new(font_test_data::NAMES_ONLY).unwrap();
234        // Without `head`, the measurements are in a 1000 unit em.
235        let expected = StyleMetrics {
236            units_per_em: 1000,
237            ..StyleMetrics::default()
238        };
239        assert_eq!(StyleMetrics::from_sfnt(&font, &[]), expected);
240    }
241
242    // Scaling to a size uses the font's own units per em.
243    #[test]
244    fn scaling_to_a_size_uses_the_units_per_em() {
245        use crate::model::{
246            metrics::{Scale26Dot6, ScaleF32},
247            Font,
248        };
249        use types::GlyphId;
250        let font = Font::new(font_test_data::TINOS_SUBSET, 0).unwrap();
251        let upem = font.units_per_em();
252        let glyph = GlyphId::new(1);
253        let (f32_scale, fixed_scale) = (
254            ScaleF32::from_ppem(16.0, upem),
255            Scale26Dot6::from_ppem(16.0, upem),
256        );
257        let metrics = font.metrics();
258        assert_eq!(
259            metrics.scaled_f32(16.0).h_line(),
260            metrics.scaled(f32_scale).h_line()
261        );
262        assert_eq!(
263            metrics.scaled_26dot6(16.0).h_line(),
264            metrics.scaled(fixed_scale).h_line()
265        );
266        let style = font.style_metrics();
267        assert_eq!(style.units_per_em, upem);
268        assert_eq!(
269            style.scaled_f32(16.0).underline(),
270            style.scaled(f32_scale).underline()
271        );
272        assert_eq!(
273            style.scaled_26dot6(16.0).underline(),
274            style.scaled(fixed_scale).underline()
275        );
276        let glyphs = font.glyph_metrics();
277        assert_eq!(
278            glyphs.scaled_f32(16.0).h_advance(glyph),
279            glyphs.scaled(f32_scale).h_advance(glyph)
280        );
281        assert_eq!(
282            glyphs.scaled_26dot6(16.0).h_advance(glyph),
283            glyphs.scaled(fixed_scale).h_advance(glyph)
284        );
285    }
286
287    #[test]
288    fn mvar_moves_the_strikethrough_position() {
289        let font = FontRef::new(font_test_data::AMSTELVAR_AVAR2_A).unwrap();
290        let coords = [NormalizedCoord::from_f32(1.0); 12];
291        let default = StyleMetrics::from_sfnt(&font, &[]);
292        let varied = StyleMetrics::from_sfnt(&font, &coords);
293        let delta = font
294            .mvar()
295            .unwrap()
296            .at(&coords)
297            .unwrap()
298            .get(tags::STRO)
299            .unwrap();
300        assert_ne!(delta, F48Dot16::ZERO);
301        assert_eq!(
302            varied.strike_through.unwrap().position - default.strike_through.unwrap().position,
303            delta
304        );
305        assert_eq!(varied.italic_angle, default.italic_angle);
306    }
307
308    #[test]
309    fn type1_exposes_its_font_info() {
310        let font = Type1Font::new(font_test_data::type1::NOTO_SERIF_REGULAR_SUBSET_PFB).unwrap();
311        let metrics = StyleMetrics::from_type1(&font);
312        assert_eq!(
313            metrics.underline.unwrap().position,
314            F48Dot16::from_i32(font.underline_position())
315                + F48Dot16::from_bits((font.underline_thickness() as i64) << 15)
316        );
317        assert_eq!(
318            metrics.underline.unwrap().thickness,
319            F48Dot16::from_i32(font.underline_thickness())
320        );
321        assert_eq!(
322            metrics.italic_angle,
323            Some(Fixed::from_i32(font.italic_angle()))
324        );
325        assert_eq!(metrics.is_fixed_pitch, Some(font.is_fixed_pitch()));
326        assert!(metrics.subscript.is_none());
327    }
328
329    #[test]
330    #[cfg(feature = "experimental_font_api")]
331    fn scales_lengths_on_their_own_axes_but_not_the_angle() {
332        use crate::model::metrics::{ScaleF32, ScaledStyleMetrics};
333        let metrics = StyleMetrics {
334            subscript: Some(ScriptMetrics {
335                x_size: F48Dot16::from_i32(100),
336                y_size: F48Dot16::from_i32(200),
337                x_offset: F48Dot16::from_i32(30),
338                y_offset: F48Dot16::from_i32(-40),
339            }),
340            underline: Some(Decoration {
341                position: F48Dot16::from_i32(-50),
342                thickness: F48Dot16::from_i32(20),
343            }),
344            italic_angle: Some(Fixed::from_i32(-12)),
345            ..Default::default()
346        };
347        let scaled: ScaledStyleMetrics<'_, _> = metrics.scaled(ScaleF32::new(100.0, 200.0, 1000));
348        let subscript = scaled.subscript().unwrap();
349        assert_eq!((subscript.x_size, subscript.y_size), (10.0, 40.0));
350        assert_eq!((subscript.x_offset, subscript.y_offset), (3.0, -8.0));
351        assert_eq!(scaled.underline().unwrap().position, -10.0);
352        assert_eq!(scaled.underline().unwrap().thickness, 4.0);
353        assert_eq!(scaled.italic_angle(), metrics.italic_angle);
354    }
355}