1 //! Axes of variation in a variable font. 2 3 use read_fonts::{ 4 tables::avar::Avar, 5 tables::fvar::{self, Fvar}, 6 types::{Fixed, Tag}, 7 TableProvider, 8 }; 9 10 use crate::{ 11 instance::{Location, NormalizedCoord}, 12 setting::VariationSetting, 13 small_array::SmallArray, 14 string::StringId, 15 }; 16 17 /// Axis of variation in a variable font. 18 /// 19 /// In variable fonts, an axis usually refers to a single aspect of a 20 /// typeface's design that can be altered by the user. 21 /// 22 /// See <https://fonts.google.com/knowledge/glossary/axis_in_variable_fonts> 23 #[derive(Clone)] 24 pub struct Axis { 25 index: usize, 26 record: fvar::VariationAxisRecord, 27 } 28 29 impl Axis { 30 /// Returns the tag that identifies the axis. tag(&self) -> Tag31 pub fn tag(&self) -> Tag { 32 self.record.axis_tag() 33 } 34 35 /// Returns the index of the axis in its owning collection. index(&self) -> usize36 pub fn index(&self) -> usize { 37 self.index 38 } 39 40 /// Returns the localized string identifier for the name of the axis. name_id(&self) -> StringId41 pub fn name_id(&self) -> StringId { 42 self.record.axis_name_id() 43 } 44 45 /// Returns true if the axis should be hidden in user interfaces. is_hidden(&self) -> bool46 pub fn is_hidden(&self) -> bool { 47 const AXIS_HIDDEN_FLAG: u16 = 0x1; 48 self.record.flags() & AXIS_HIDDEN_FLAG != 0 49 } 50 51 /// Returns the minimum value of the axis. min_value(&self) -> f3252 pub fn min_value(&self) -> f32 { 53 self.record.min_value().to_f64() as _ 54 } 55 56 /// Returns the default value of the axis. default_value(&self) -> f3257 pub fn default_value(&self) -> f32 { 58 self.record.default_value().to_f64() as _ 59 } 60 61 /// Returns the maximum value of the axis. max_value(&self) -> f3262 pub fn max_value(&self) -> f32 { 63 self.record.max_value().to_f64() as _ 64 } 65 66 /// Returns a normalized coordinate for the given user coordinate. 67 /// 68 /// The value will be clamped to the range specified by the minimum 69 /// and maximum values. 70 /// 71 /// This does not apply any axis variation remapping. normalize(&self, coord: f32) -> NormalizedCoord72 pub fn normalize(&self, coord: f32) -> NormalizedCoord { 73 self.record 74 .normalize(Fixed::from_f64(coord as _)) 75 .to_f2dot14() 76 } 77 } 78 79 /// Collection of axes in a variable font. 80 /// 81 /// See the [`Axis`] type for more detail. 82 #[derive(Clone)] 83 pub struct AxisCollection<'a> { 84 fvar: Option<Fvar<'a>>, 85 avar: Option<Avar<'a>>, 86 } 87 88 impl<'a> AxisCollection<'a> { 89 /// Creates a new axis collection from the given font. new(font: &impl TableProvider<'a>) -> Self90 pub fn new(font: &impl TableProvider<'a>) -> Self { 91 let fvar = font.fvar().ok(); 92 let avar = font.avar().ok(); 93 Self { fvar, avar } 94 } 95 96 /// Returns the number of variation axes in the font. len(&self) -> usize97 pub fn len(&self) -> usize { 98 self.fvar 99 .as_ref() 100 .map(|fvar| fvar.axis_count() as usize) 101 .unwrap_or(0) 102 } 103 104 /// Returns true if the collection is empty. is_empty(&self) -> bool105 pub fn is_empty(&self) -> bool { 106 self.len() == 0 107 } 108 109 /// Returns the axis at the given index. get(&self, index: usize) -> Option<Axis>110 pub fn get(&self, index: usize) -> Option<Axis> { 111 let raw = self.fvar.as_ref()?.axes().ok()?.get(index)?.clone(); 112 Some(Axis { index, record: raw }) 113 } 114 115 /// Returns the axis with the given tag. 116 /// 117 /// # Examples 118 /// 119 /// ```rust 120 /// # use skrifa::prelude::*; 121 /// # fn wrapper(font: &FontRef) { 122 /// let opsz = Tag::new(b"opsz"); 123 /// assert_eq!(font.axes().get_by_tag(opsz).unwrap().tag(), opsz); 124 /// # } 125 /// ``` get_by_tag(&self, tag: Tag) -> Option<Axis>126 pub fn get_by_tag(&self, tag: Tag) -> Option<Axis> { 127 self.iter().find(|axis| axis.tag() == tag) 128 } 129 130 /// Given an iterator of variation settings in user space, computes an 131 /// ordered sequence of normalized coordinates. 132 /// 133 /// * Setting selectors that don't match an axis are ignored. 134 /// * Setting values are clamped to the range of their associated axis 135 /// before normalization. 136 /// * If more than one setting for an axis is provided, the last one is 137 /// used. 138 /// * Omitted settings are set to 0.0, representing the default position 139 /// in variation space. 140 /// 141 /// # Examples 142 /// 143 /// ```rust 144 /// # use skrifa::prelude::*; 145 /// # fn wrapper(font: &FontRef) { 146 /// let location = font.axes().location([("wght", 250.0), ("wdth", 75.0)]); 147 /// # } 148 /// ``` location<I>(&self, settings: I) -> Location where I: IntoIterator, I::Item: Into<VariationSetting>,149 pub fn location<I>(&self, settings: I) -> Location 150 where 151 I: IntoIterator, 152 I::Item: Into<VariationSetting>, 153 { 154 let mut location = Location::new(self.len()); 155 self.location_to_slice(settings, location.coords_mut()); 156 location 157 } 158 159 /// Given an iterator of variation settings in user space, computes an 160 /// ordered sequence of normalized coordinates and stores them in the 161 /// target slice. 162 /// 163 /// * Setting selectors that don't match an axis are ignored. 164 /// * Setting values are clamped to the range of their associated axis 165 /// before normalization. 166 /// * If more than one setting for an axis is provided, the last one is 167 /// used. 168 /// * If no setting for an axis is provided, the associated coordinate is 169 /// set to the normalized value 0.0, representing the default position 170 /// in variation space. 171 /// 172 /// # Examples 173 /// 174 /// ```rust 175 /// # use skrifa::prelude::*; 176 /// # fn wrapper(font: &FontRef) { 177 /// let axes = font.axes(); 178 /// let mut location = vec![NormalizedCoord::default(); axes.len()]; 179 /// axes.location_to_slice([("wght", 250.0), ("wdth", 75.0)], &mut location); 180 /// # } 181 /// ``` location_to_slice<I>(&self, settings: I, location: &mut [NormalizedCoord]) where I: IntoIterator, I::Item: Into<VariationSetting>,182 pub fn location_to_slice<I>(&self, settings: I, location: &mut [NormalizedCoord]) 183 where 184 I: IntoIterator, 185 I::Item: Into<VariationSetting>, 186 { 187 for coord in location.iter_mut() { 188 *coord = NormalizedCoord::default(); 189 } 190 let avar_mappings = self.avar.as_ref().map(|avar| avar.axis_segment_maps()); 191 for setting in settings.into_iter() { 192 let setting = setting.into(); 193 // To permit non-linear interpolation, iterate over all axes to ensure we match 194 // multiple axes with the same tag: 195 // https://github.com/PeterConstable/OT_Drafts/blob/master/NLI/UnderstandingNLI.md 196 // We accept quadratic behavior here to avoid dynamic allocation and with the assumption 197 // that fonts contain a relatively small number of axes. 198 for (i, axis) in self 199 .iter() 200 .enumerate() 201 .filter(|v| v.1.tag() == setting.selector) 202 { 203 if let Some(target_coord) = location.get_mut(i) { 204 let coord = axis.record.normalize(Fixed::from_f64(setting.value as f64)); 205 *target_coord = avar_mappings 206 .as_ref() 207 .and_then(|mappings| mappings.get(i).transpose().ok()) 208 .flatten() 209 .map(|mapping| mapping.apply(coord)) 210 .unwrap_or(coord) 211 .to_f2dot14(); 212 } 213 } 214 } 215 } 216 217 /// Given an iterator of variation settings in user space, returns a 218 /// new iterator yielding those settings that are valid for this axis 219 /// collection. 220 /// 221 /// * Setting selectors that don't match an axis are dropped. 222 /// * If more than one setting for an axis is provided, the last one is 223 /// retained. 224 /// * Setting values are clamped to the range of their associated axis. 225 /// 226 /// # Examples 227 /// 228 /// ```rust 229 /// # use skrifa::prelude::*; 230 /// # fn wrapper(font: &FontRef) { 231 /// // Assuming a font contains a single "wght" (weight) axis with range 232 /// // 100-900: 233 /// let axes = font.axes(); 234 /// let filtered: Vec<_> = axes 235 /// .filter([("wght", 400.0), ("opsz", 100.0), ("wght", 1200.0)]) 236 /// .collect(); 237 /// // The first "wght" and "opsz" settings are dropped and the final 238 /// // "wght" axis is clamped to the maximum value of 900. 239 /// assert_eq!(&filtered, &[("wght", 900.0).into()]); 240 /// # } 241 /// ``` filter<I>(&self, settings: I) -> impl Iterator<Item = VariationSetting> + Clone where I: IntoIterator, I::Item: Into<VariationSetting>,242 pub fn filter<I>(&self, settings: I) -> impl Iterator<Item = VariationSetting> + Clone 243 where 244 I: IntoIterator, 245 I::Item: Into<VariationSetting>, 246 { 247 #[derive(Copy, Clone, Default)] 248 struct Entry { 249 tag: Tag, 250 min: f32, 251 max: f32, 252 value: f32, 253 present: bool, 254 } 255 let mut results = SmallArray::<_, 8>::new(Entry::default(), self.len()); 256 for (axis, result) in self.iter().zip(results.as_mut_slice()) { 257 result.tag = axis.tag(); 258 result.min = axis.min_value(); 259 result.max = axis.max_value(); 260 result.value = axis.default_value(); 261 } 262 for setting in settings { 263 let setting = setting.into(); 264 for entry in results.as_mut_slice() { 265 if entry.tag == setting.selector { 266 entry.value = setting.value.max(entry.min).min(entry.max); 267 entry.present = true; 268 } 269 } 270 } 271 results 272 .into_iter() 273 .filter(|entry| entry.present) 274 .map(|entry| VariationSetting::new(entry.tag, entry.value)) 275 } 276 277 /// Returns an iterator over the axes in the collection. iter(&self) -> impl Iterator<Item = Axis> + 'a + Clone278 pub fn iter(&self) -> impl Iterator<Item = Axis> + 'a + Clone { 279 let copy = self.clone(); 280 (0..self.len()).filter_map(move |i| copy.get(i)) 281 } 282 } 283 284 /// Named instance of a variation. 285 /// 286 /// A set of fixed axis positions selected by the type designer and assigned a 287 /// name. 288 /// 289 /// See <https://fonts.google.com/knowledge/glossary/instance> 290 #[derive(Clone)] 291 pub struct NamedInstance<'a> { 292 axes: AxisCollection<'a>, 293 record: fvar::InstanceRecord<'a>, 294 } 295 296 impl<'a> NamedInstance<'a> { 297 /// Returns the string identifier for the subfamily name of the instance. subfamily_name_id(&self) -> StringId298 pub fn subfamily_name_id(&self) -> StringId { 299 self.record.subfamily_name_id 300 } 301 302 /// Returns the string identifier for the PostScript name of the instance. postscript_name_id(&self) -> Option<StringId>303 pub fn postscript_name_id(&self) -> Option<StringId> { 304 self.record.post_script_name_id 305 } 306 307 /// Returns an iterator over the ordered sequence of user space coordinates 308 /// that define the instance, one coordinate per axis. user_coords(&self) -> impl Iterator<Item = f32> + 'a + Clone309 pub fn user_coords(&self) -> impl Iterator<Item = f32> + 'a + Clone { 310 self.record 311 .coordinates 312 .iter() 313 .map(|coord| coord.get().to_f64() as _) 314 } 315 316 /// Computes a location in normalized variation space for this instance. 317 /// 318 /// # Examples 319 /// 320 /// ```rust 321 /// # use skrifa::prelude::*; 322 /// # fn wrapper(font: &FontRef) { 323 /// let location = font.named_instances().get(0).unwrap().location(); 324 /// # } 325 /// ``` location(&self) -> Location326 pub fn location(&self) -> Location { 327 let mut location = Location::new(self.axes.len()); 328 self.location_to_slice(location.coords_mut()); 329 location 330 } 331 332 /// Computes a location in normalized variation space for this instance and 333 /// stores the result in the given slice. 334 /// 335 /// # Examples 336 /// 337 /// ```rust 338 /// # use skrifa::prelude::*; 339 /// # fn wrapper(font: &FontRef) { 340 /// let instance = font.named_instances().get(0).unwrap(); 341 /// let mut location = vec![NormalizedCoord::default(); instance.user_coords().count()]; 342 /// instance.location_to_slice(&mut location); 343 /// # } 344 /// ``` location_to_slice(&self, location: &mut [NormalizedCoord])345 pub fn location_to_slice(&self, location: &mut [NormalizedCoord]) { 346 let settings = self 347 .axes 348 .iter() 349 .map(|axis| axis.tag()) 350 .zip(self.user_coords()); 351 self.axes.location_to_slice(settings, location); 352 } 353 } 354 355 /// Collection of named instances in a variable font. 356 /// 357 /// See the [`NamedInstance`] type for more detail. 358 #[derive(Clone)] 359 pub struct NamedInstanceCollection<'a> { 360 axes: AxisCollection<'a>, 361 } 362 363 impl<'a> NamedInstanceCollection<'a> { 364 /// Creates a new instance collection from the given font. new(font: &impl TableProvider<'a>) -> Self365 pub fn new(font: &impl TableProvider<'a>) -> Self { 366 Self { 367 axes: AxisCollection::new(font), 368 } 369 } 370 371 /// Returns the number of instances in the collection. len(&self) -> usize372 pub fn len(&self) -> usize { 373 self.axes 374 .fvar 375 .as_ref() 376 .map(|fvar| fvar.instance_count() as usize) 377 .unwrap_or(0) 378 } 379 380 /// Returns true if the collection is empty. is_empty(&self) -> bool381 pub fn is_empty(&self) -> bool { 382 self.len() == 0 383 } 384 385 /// Returns the instance at the given index. get(&self, index: usize) -> Option<NamedInstance<'a>>386 pub fn get(&self, index: usize) -> Option<NamedInstance<'a>> { 387 let record = self.axes.fvar.as_ref()?.instances().ok()?.get(index).ok()?; 388 Some(NamedInstance { 389 axes: self.axes.clone(), 390 record, 391 }) 392 } 393 394 /// Returns an iterator over the instances in the colletion. iter(&self) -> impl Iterator<Item = NamedInstance<'a>> + 'a + Clone395 pub fn iter(&self) -> impl Iterator<Item = NamedInstance<'a>> + 'a + Clone { 396 let copy = self.clone(); 397 (0..self.len()).filter_map(move |i| copy.get(i)) 398 } 399 } 400 401 #[cfg(test)] 402 mod tests { 403 use super::*; 404 use crate::MetadataProvider as _; 405 use font_test_data::VAZIRMATN_VAR; 406 use read_fonts::FontRef; 407 use std::str::FromStr; 408 409 #[test] axis()410 fn axis() { 411 let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap(); 412 let axis = font.axes().get(0).unwrap(); 413 assert_eq!(axis.index(), 0); 414 assert_eq!(axis.tag(), Tag::new(b"wght")); 415 assert_eq!(axis.min_value(), 100.0); 416 assert_eq!(axis.default_value(), 400.0); 417 assert_eq!(axis.max_value(), 900.0); 418 assert_eq!(axis.name_id(), StringId::new(257)); 419 assert_eq!( 420 font.localized_strings(axis.name_id()) 421 .english_or_first() 422 .unwrap() 423 .to_string(), 424 "Weight" 425 ); 426 } 427 428 #[test] named_instances()429 fn named_instances() { 430 let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap(); 431 let named_instances = font.named_instances(); 432 let thin = named_instances.get(0).unwrap(); 433 assert_eq!(thin.subfamily_name_id(), StringId::new(258)); 434 assert_eq!( 435 font.localized_strings(thin.subfamily_name_id()) 436 .english_or_first() 437 .unwrap() 438 .to_string(), 439 "Thin" 440 ); 441 assert_eq!(thin.location().coords(), &[NormalizedCoord::from_f32(-1.0)]); 442 let regular = named_instances.get(3).unwrap(); 443 assert_eq!(regular.subfamily_name_id(), StringId::new(261)); 444 assert_eq!( 445 font.localized_strings(regular.subfamily_name_id()) 446 .english_or_first() 447 .unwrap() 448 .to_string(), 449 "Regular" 450 ); 451 assert_eq!( 452 regular.location().coords(), 453 &[NormalizedCoord::from_f32(0.0)] 454 ); 455 let bold = named_instances.get(6).unwrap(); 456 assert_eq!(bold.subfamily_name_id(), StringId::new(264)); 457 assert_eq!( 458 font.localized_strings(bold.subfamily_name_id()) 459 .english_or_first() 460 .unwrap() 461 .to_string(), 462 "Bold" 463 ); 464 assert_eq!( 465 bold.location().coords(), 466 &[NormalizedCoord::from_f32(0.6776123)] 467 ); 468 } 469 470 #[test] location()471 fn location() { 472 let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap(); 473 let axes = font.axes(); 474 let axis = axes.get_by_tag(Tag::from_str("wght").unwrap()).unwrap(); 475 assert_eq!( 476 axes.location([("wght", -1000.0)]).coords(), 477 &[NormalizedCoord::from_f32(-1.0)] 478 ); 479 assert_eq!( 480 axes.location([("wght", 100.0)]).coords(), 481 &[NormalizedCoord::from_f32(-1.0)] 482 ); 483 assert_eq!( 484 axes.location([("wght", 200.0)]).coords(), 485 &[NormalizedCoord::from_f32(-0.5)] 486 ); 487 assert_eq!( 488 axes.location([("wght", 400.0)]).coords(), 489 &[NormalizedCoord::from_f32(0.0)] 490 ); 491 // avar table maps 0.8 to 0.83875 492 assert_eq!( 493 axes.location(&[( 494 "wght", 495 axis.default_value() + (axis.max_value() - axis.default_value()) * 0.8, 496 )]) 497 .coords(), 498 &[NormalizedCoord::from_f32(0.83875)] 499 ); 500 assert_eq!( 501 axes.location([("wght", 900.0)]).coords(), 502 &[NormalizedCoord::from_f32(1.0)] 503 ); 504 assert_eq!( 505 axes.location([("wght", 1251.5)]).coords(), 506 &[NormalizedCoord::from_f32(1.0)] 507 ); 508 } 509 510 #[test] filter()511 fn filter() { 512 let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap(); 513 // This font contains one wght axis with the range 100-900 and default 514 // value of 400. 515 let axes = font.axes(); 516 // Drop axes that are not present in the font 517 let drop_missing: Vec<_> = axes.filter(&[("slnt", 25.0), ("wdth", 50.0)]).collect(); 518 assert_eq!(&drop_missing, &[]); 519 // Clamp an out of range value 520 let clamp: Vec<_> = axes.filter(&[("wght", 50.0)]).collect(); 521 assert_eq!(&clamp, &[("wght", 100.0).into()]); 522 // Combination of the above two: drop the missing axis and clamp out of range value 523 let drop_missing_and_clamp: Vec<_> = 524 axes.filter(&[("slnt", 25.0), ("wght", 1000.0)]).collect(); 525 assert_eq!(&drop_missing_and_clamp, &[("wght", 900.0).into()]); 526 // Ensure we take the later value in the case of duplicates 527 let drop_duplicate_and_missing: Vec<_> = axes 528 .filter(&[("wght", 400.0), ("opsz", 100.0), ("wght", 120.5)]) 529 .collect(); 530 assert_eq!(&drop_duplicate_and_missing, &[("wght", 120.5).into()]); 531 } 532 } 533