read_fonts/model/metrics/
style.rs1use 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#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
16pub struct ScriptMetrics<T = F48Dot16> {
17 pub x_size: T,
19 pub y_size: T,
21 pub x_offset: T,
23 pub y_offset: T,
26}
27
28#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
33pub struct Decoration<T = F48Dot16> {
34 pub position: T,
36 pub thickness: T,
38}
39
40#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
48pub struct StyleMetrics {
49 pub units_per_em: u16,
53 pub subscript: Option<ScriptMetrics>,
55 pub superscript: Option<ScriptMetrics>,
57 pub underline: Option<Decoration>,
59 pub strike_through: Option<Decoration>,
61 pub italic_angle: Option<Fixed>,
63 pub is_fixed_pitch: Option<bool>,
65}
66
67impl StyleMetrics {
68 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 pub fn from_type1(font: &Type1Font) -> Self {
126 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 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 let expected = StyleMetrics {
236 units_per_em: 1000,
237 ..StyleMetrics::default()
238 };
239 assert_eq!(StyleMetrics::from_sfnt(&font, &[]), expected);
240 }
241
242 #[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}