1 //! Support for scaling CFF outlines. 2 3 mod hint; 4 5 use std::ops::Range; 6 7 use read_fonts::{ 8 tables::{ 9 cff::Cff, 10 cff2::Cff2, 11 postscript::{ 12 charstring::{self, CommandSink}, 13 dict, BlendState, Error, FdSelect, Index, 14 }, 15 variations::ItemVariationStore, 16 }, 17 types::{F2Dot14, Fixed, GlyphId, Pen}, 18 FontData, FontRead, TableProvider, 19 }; 20 21 use hint::{HintParams, HintState, HintingSink}; 22 23 /// Type for loading, scaling and hinting outlines in CFF/CFF2 tables. 24 /// 25 /// The skrifa crate provides a higher level interface for this that handles 26 /// caching and abstracting over the different outline formats. Consider using 27 /// that if detailed control over resources is not required. 28 /// 29 /// # Subfonts 30 /// 31 /// CFF tables can contain multiple logical "subfonts" which determine the 32 /// state required for processing some subset of glyphs. This state is 33 /// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28) 34 /// operators to select an appropriate subfont for any given glyph identifier. 35 /// This process is exposed on this type with the 36 /// [`subfont_index`](Self::subfont_index) method to retrieve the subfont 37 /// index for the requested glyph followed by using the 38 /// [`subfont`](Self::subfont) method to create an appropriately configured 39 /// subfont for that glyph. 40 #[derive(Clone)] 41 pub(crate) struct Outlines<'a> { 42 version: Version<'a>, 43 top_dict: TopDict<'a>, 44 units_per_em: u16, 45 } 46 47 impl<'a> Outlines<'a> { 48 /// Creates a new scaler for the given font. 49 /// 50 /// This will choose an underyling CFF2 or CFF table from the font, in that 51 /// order. new(font: &impl TableProvider<'a>) -> Result<Self, Error>52 pub fn new(font: &impl TableProvider<'a>) -> Result<Self, Error> { 53 let units_per_em = font.head()?.units_per_em(); 54 if let Ok(cff2) = font.cff2() { 55 Self::from_cff2(cff2, units_per_em) 56 } else { 57 // "The Name INDEX in the CFF data must contain only one entry; 58 // that is, there must be only one font in the CFF FontSet" 59 // So we always pass 0 for Top DICT index when reading from an 60 // OpenType font. 61 // <https://learn.microsoft.com/en-us/typography/opentype/spec/cff> 62 Self::from_cff(font.cff()?, 0, units_per_em) 63 } 64 } 65 from_cff( cff1: Cff<'a>, top_dict_index: usize, units_per_em: u16, ) -> Result<Self, Error>66 pub fn from_cff( 67 cff1: Cff<'a>, 68 top_dict_index: usize, 69 units_per_em: u16, 70 ) -> Result<Self, Error> { 71 let top_dict_data = cff1.top_dicts().get(top_dict_index)?; 72 let top_dict = TopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false)?; 73 Ok(Self { 74 version: Version::Version1(cff1), 75 top_dict, 76 units_per_em, 77 }) 78 } 79 from_cff2(cff2: Cff2<'a>, units_per_em: u16) -> Result<Self, Error>80 pub fn from_cff2(cff2: Cff2<'a>, units_per_em: u16) -> Result<Self, Error> { 81 let table_data = cff2.offset_data().as_bytes(); 82 let top_dict = TopDict::new(table_data, cff2.top_dict_data(), true)?; 83 Ok(Self { 84 version: Version::Version2(cff2), 85 top_dict, 86 units_per_em, 87 }) 88 } 89 is_cff2(&self) -> bool90 pub fn is_cff2(&self) -> bool { 91 matches!(self.version, Version::Version2(_)) 92 } 93 94 /// Returns the number of available glyphs. glyph_count(&self) -> usize95 pub fn glyph_count(&self) -> usize { 96 self.top_dict 97 .charstrings 98 .as_ref() 99 .map(|cs| cs.count() as usize) 100 .unwrap_or_default() 101 } 102 103 /// Returns the number of available subfonts. subfont_count(&self) -> u32104 pub fn subfont_count(&self) -> u32 { 105 self.top_dict 106 .font_dicts 107 .as_ref() 108 .map(|font_dicts| font_dicts.count()) 109 // All CFF fonts have at least one logical subfont. 110 .unwrap_or(1) 111 } 112 113 /// Returns the subfont (or Font DICT) index for the given glyph 114 /// identifier. subfont_index(&self, glyph_id: GlyphId) -> u32115 pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 { 116 // For CFF tables, an FDSelect index will be present for CID-keyed 117 // fonts. Otherwise, the Top DICT will contain an entry for the 118 // "global" Private DICT. 119 // See <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=27> 120 // 121 // CFF2 tables always contain a Font DICT and an FDSelect is only 122 // present if the size of the DICT is greater than 1. 123 // See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#10-font-dict-index-font-dicts-and-fdselect> 124 // 125 // In both cases, we return a subfont index of 0 when FDSelect is missing. 126 self.top_dict 127 .fd_select 128 .as_ref() 129 .and_then(|select| select.font_index(glyph_id)) 130 .unwrap_or(0) as u32 131 } 132 133 /// Creates a new subfont for the given index, size, normalized 134 /// variation coordinates and hinting state. 135 /// 136 /// The index of a subfont for a particular glyph can be retrieved with 137 /// the [`subfont_index`](Self::subfont_index) method. subfont( &self, index: u32, size: Option<f32>, coords: &[F2Dot14], ) -> Result<Subfont, Error>138 pub fn subfont( 139 &self, 140 index: u32, 141 size: Option<f32>, 142 coords: &[F2Dot14], 143 ) -> Result<Subfont, Error> { 144 let private_dict_range = self.private_dict_range(index)?; 145 let private_dict_data = self.offset_data().read_array(private_dict_range.clone())?; 146 let mut hint_params = HintParams::default(); 147 let mut subrs_offset = None; 148 let mut store_index = 0; 149 let blend_state = self 150 .top_dict 151 .var_store 152 .clone() 153 .map(|store| BlendState::new(store, coords, store_index)) 154 .transpose()?; 155 for entry in dict::entries(private_dict_data, blend_state) { 156 use dict::Entry::*; 157 match entry? { 158 BlueValues(values) => hint_params.blues = values, 159 FamilyBlues(values) => hint_params.family_blues = values, 160 OtherBlues(values) => hint_params.other_blues = values, 161 FamilyOtherBlues(values) => hint_params.family_blues = values, 162 BlueScale(value) => hint_params.blue_scale = value, 163 BlueShift(value) => hint_params.blue_shift = value, 164 BlueFuzz(value) => hint_params.blue_fuzz = value, 165 LanguageGroup(group) => hint_params.language_group = group, 166 // Subrs offset is relative to the private DICT 167 SubrsOffset(offset) => subrs_offset = Some(private_dict_range.start + offset), 168 VariationStoreIndex(index) => store_index = index, 169 _ => {} 170 } 171 } 172 let scale = match size { 173 Some(ppem) if self.units_per_em > 0 => { 174 // Note: we do an intermediate scale to 26.6 to ensure we 175 // match FreeType 176 Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(self.units_per_em as i32) 177 } 178 _ => Fixed::ONE, 179 }; 180 // When hinting, use a modified scale factor 181 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L279> 182 let hint_scale = Fixed::from_bits((scale.to_bits() + 32) / 64); 183 let hint_state = HintState::new(&hint_params, hint_scale); 184 Ok(Subfont { 185 is_cff2: self.is_cff2(), 186 scale, 187 subrs_offset, 188 hint_state, 189 store_index, 190 }) 191 } 192 193 /// Loads and scales an outline for the given subfont instance, glyph 194 /// identifier and normalized variation coordinates. 195 /// 196 /// Before calling this method, use [`subfont_index`](Self::subfont_index) 197 /// to retrieve the subfont index for the desired glyph and then 198 /// [`subfont`](Self::subfont) to create an instance of the subfont for a 199 /// particular size and location in variation space. 200 /// Creating subfont instances is not free, so this process is exposed in 201 /// discrete steps to allow for caching. 202 /// 203 /// The result is emitted to the specified pen. draw( &self, subfont: &Subfont, glyph_id: GlyphId, coords: &[F2Dot14], hint: bool, pen: &mut impl Pen, ) -> Result<(), Error>204 pub fn draw( 205 &self, 206 subfont: &Subfont, 207 glyph_id: GlyphId, 208 coords: &[F2Dot14], 209 hint: bool, 210 pen: &mut impl Pen, 211 ) -> Result<(), Error> { 212 let charstring_data = self 213 .top_dict 214 .charstrings 215 .as_ref() 216 .ok_or(Error::MissingCharstrings)? 217 .get(glyph_id.to_u16() as usize)?; 218 let subrs = subfont.subrs(self)?; 219 let blend_state = subfont.blend_state(self, coords)?; 220 let mut pen_sink = charstring::PenSink::new(pen); 221 let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink); 222 if hint { 223 let mut hinting_adapter = 224 HintingSink::new(&subfont.hint_state, &mut simplifying_adapter); 225 charstring::evaluate( 226 charstring_data, 227 self.global_subrs(), 228 subrs, 229 blend_state, 230 &mut hinting_adapter, 231 )?; 232 hinting_adapter.finish(); 233 } else { 234 let mut scaling_adapter = 235 ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale); 236 charstring::evaluate( 237 charstring_data, 238 self.global_subrs(), 239 subrs, 240 blend_state, 241 &mut scaling_adapter, 242 )?; 243 } 244 simplifying_adapter.finish(); 245 Ok(()) 246 } 247 offset_data(&self) -> FontData<'a>248 fn offset_data(&self) -> FontData<'a> { 249 match &self.version { 250 Version::Version1(cff1) => cff1.offset_data(), 251 Version::Version2(cff2) => cff2.offset_data(), 252 } 253 } 254 global_subrs(&self) -> Index<'a>255 fn global_subrs(&self) -> Index<'a> { 256 match &self.version { 257 Version::Version1(cff1) => cff1.global_subrs().into(), 258 Version::Version2(cff2) => cff2.global_subrs().into(), 259 } 260 } 261 private_dict_range(&self, subfont_index: u32) -> Result<Range<usize>, Error>262 fn private_dict_range(&self, subfont_index: u32) -> Result<Range<usize>, Error> { 263 if let Some(font_dicts) = &self.top_dict.font_dicts { 264 // If we have a font dict array, extract the private dict range 265 // from the font dict at the given index. 266 let font_dict_data = font_dicts.get(subfont_index as usize)?; 267 let mut range = None; 268 for entry in dict::entries(font_dict_data, None) { 269 if let dict::Entry::PrivateDictRange(r) = entry? { 270 range = Some(r); 271 break; 272 } 273 } 274 range 275 } else { 276 // Last chance, use the private dict range from the top dict if 277 // available. 278 self.top_dict.private_dict_range.clone() 279 } 280 .ok_or(Error::MissingPrivateDict) 281 } 282 } 283 284 #[derive(Clone)] 285 enum Version<'a> { 286 /// <https://learn.microsoft.com/en-us/typography/opentype/spec/cff> 287 Version1(Cff<'a>), 288 /// <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2> 289 Version2(Cff2<'a>), 290 } 291 292 /// Specifies local subroutines and hinting parameters for some subset of 293 /// glyphs in a CFF or CFF2 table. 294 /// 295 /// This type is designed to be cacheable to avoid re-evaluating the private 296 /// dict every time a charstring is processed. 297 /// 298 /// For variable fonts, this is dependent on a location in variation space. 299 #[derive(Clone)] 300 pub(crate) struct Subfont { 301 is_cff2: bool, 302 scale: Fixed, 303 subrs_offset: Option<usize>, 304 pub(crate) hint_state: HintState, 305 store_index: u16, 306 } 307 308 impl Subfont { 309 /// Returns the local subroutine index. subrs<'a>(&self, scaler: &Outlines<'a>) -> Result<Option<Index<'a>>, Error>310 pub fn subrs<'a>(&self, scaler: &Outlines<'a>) -> Result<Option<Index<'a>>, Error> { 311 if let Some(subrs_offset) = self.subrs_offset { 312 let offset_data = scaler.offset_data().as_bytes(); 313 let index_data = offset_data.get(subrs_offset..).unwrap_or_default(); 314 Ok(Some(Index::new(index_data, self.is_cff2)?)) 315 } else { 316 Ok(None) 317 } 318 } 319 320 /// Creates a new blend state for the given normalized variation 321 /// coordinates. blend_state<'a>( &self, scaler: &Outlines<'a>, coords: &'a [F2Dot14], ) -> Result<Option<BlendState<'a>>, Error>322 pub fn blend_state<'a>( 323 &self, 324 scaler: &Outlines<'a>, 325 coords: &'a [F2Dot14], 326 ) -> Result<Option<BlendState<'a>>, Error> { 327 if let Some(var_store) = scaler.top_dict.var_store.clone() { 328 Ok(Some(BlendState::new(var_store, coords, self.store_index)?)) 329 } else { 330 Ok(None) 331 } 332 } 333 } 334 335 /// Entries that we parse from the Top DICT that are required to support 336 /// charstring evaluation. 337 #[derive(Clone, Default)] 338 struct TopDict<'a> { 339 charstrings: Option<Index<'a>>, 340 font_dicts: Option<Index<'a>>, 341 fd_select: Option<FdSelect<'a>>, 342 private_dict_range: Option<Range<usize>>, 343 var_store: Option<ItemVariationStore<'a>>, 344 } 345 346 impl<'a> TopDict<'a> { new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result<Self, Error>347 fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result<Self, Error> { 348 let mut items = TopDict::default(); 349 for entry in dict::entries(top_dict_data, None) { 350 match entry? { 351 dict::Entry::CharstringsOffset(offset) => { 352 items.charstrings = Some(Index::new( 353 table_data.get(offset..).unwrap_or_default(), 354 is_cff2, 355 )?); 356 } 357 dict::Entry::FdArrayOffset(offset) => { 358 items.font_dicts = Some(Index::new( 359 table_data.get(offset..).unwrap_or_default(), 360 is_cff2, 361 )?); 362 } 363 dict::Entry::FdSelectOffset(offset) => { 364 items.fd_select = Some(FdSelect::read(FontData::new( 365 table_data.get(offset..).unwrap_or_default(), 366 ))?); 367 } 368 dict::Entry::PrivateDictRange(range) => { 369 items.private_dict_range = Some(range); 370 } 371 dict::Entry::VariationStoreOffset(offset) if is_cff2 => { 372 items.var_store = Some(ItemVariationStore::read(FontData::new( 373 // IVS is preceded by a 2 byte length 374 table_data.get(offset + 2..).unwrap_or_default(), 375 ))?); 376 } 377 _ => {} 378 } 379 } 380 Ok(items) 381 } 382 } 383 384 /// Command sink adapter that applies a scaling factor. 385 /// 386 /// This assumes a 26.6 scaling factor packed into a Fixed and thus, 387 /// this is not public and exists only to match FreeType's exact 388 /// scaling process. 389 struct ScalingSink26Dot6<'a, S> { 390 inner: &'a mut S, 391 scale: Fixed, 392 } 393 394 impl<'a, S> ScalingSink26Dot6<'a, S> { new(sink: &'a mut S, scale: Fixed) -> Self395 fn new(sink: &'a mut S, scale: Fixed) -> Self { 396 Self { scale, inner: sink } 397 } 398 scale(&self, coord: Fixed) -> Fixed399 fn scale(&self, coord: Fixed) -> Fixed { 400 // The following dance is necessary to exactly match FreeType's 401 // application of scaling factors. This seems to be the result 402 // of merging the contributed Adobe code while not breaking the 403 // FreeType public API. 404 // 405 // The first two steps apply to both scaled and unscaled outlines: 406 // 407 // 1. Multiply by 1/64 408 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L284> 409 let a = coord * Fixed::from_bits(0x0400); 410 // 2. Truncate the bottom 10 bits. Combined with the division by 64, 411 // converts to font units. 412 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psobjs.c#L2219> 413 let b = Fixed::from_bits(a.to_bits() >> 10); 414 if self.scale != Fixed::ONE { 415 // Scaled case: 416 // 3. Multiply by the original scale factor (to 26.6) 417 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/cff/cffgload.c#L721> 418 let c = b * self.scale; 419 // 4. Convert from 26.6 to 16.16 420 Fixed::from_bits(c.to_bits() << 10) 421 } else { 422 // Unscaled case: 423 // 3. Convert from integer to 16.16 424 Fixed::from_bits(b.to_bits() << 16) 425 } 426 } 427 } 428 429 impl<'a, S: CommandSink> CommandSink for ScalingSink26Dot6<'a, S> { hstem(&mut self, y: Fixed, dy: Fixed)430 fn hstem(&mut self, y: Fixed, dy: Fixed) { 431 self.inner.hstem(y, dy); 432 } 433 vstem(&mut self, x: Fixed, dx: Fixed)434 fn vstem(&mut self, x: Fixed, dx: Fixed) { 435 self.inner.vstem(x, dx); 436 } 437 hint_mask(&mut self, mask: &[u8])438 fn hint_mask(&mut self, mask: &[u8]) { 439 self.inner.hint_mask(mask); 440 } 441 counter_mask(&mut self, mask: &[u8])442 fn counter_mask(&mut self, mask: &[u8]) { 443 self.inner.counter_mask(mask); 444 } 445 move_to(&mut self, x: Fixed, y: Fixed)446 fn move_to(&mut self, x: Fixed, y: Fixed) { 447 self.inner.move_to(self.scale(x), self.scale(y)); 448 } 449 line_to(&mut self, x: Fixed, y: Fixed)450 fn line_to(&mut self, x: Fixed, y: Fixed) { 451 self.inner.line_to(self.scale(x), self.scale(y)); 452 } 453 curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed)454 fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { 455 self.inner.curve_to( 456 self.scale(cx1), 457 self.scale(cy1), 458 self.scale(cx2), 459 self.scale(cy2), 460 self.scale(x), 461 self.scale(y), 462 ); 463 } 464 close(&mut self)465 fn close(&mut self) { 466 self.inner.close(); 467 } 468 } 469 470 /// Command sink adapter that supresses degenerate move and line commands. 471 /// 472 /// FreeType avoids emitting empty contours and zero length lines to prevent 473 /// artifacts when stem darkening is enabled. We don't support stem darkening 474 /// because it's not enabled by any of our clients but we remove the degenerate 475 /// elements regardless to match the output. 476 /// 477 /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L1786> 478 struct NopFilteringSink<'a, S> { 479 start: Option<(Fixed, Fixed)>, 480 last: Option<(Fixed, Fixed)>, 481 pending_move: Option<(Fixed, Fixed)>, 482 inner: &'a mut S, 483 } 484 485 impl<'a, S> NopFilteringSink<'a, S> 486 where 487 S: CommandSink, 488 { new(inner: &'a mut S) -> Self489 fn new(inner: &'a mut S) -> Self { 490 Self { 491 start: None, 492 last: None, 493 pending_move: None, 494 inner, 495 } 496 } 497 flush_pending_move(&mut self)498 fn flush_pending_move(&mut self) { 499 if let Some((x, y)) = self.pending_move.take() { 500 if let Some((last_x, last_y)) = self.start { 501 if self.last != self.start { 502 self.inner.line_to(last_x, last_y); 503 } 504 } 505 self.start = Some((x, y)); 506 self.last = None; 507 self.inner.move_to(x, y); 508 } 509 } 510 finish(&mut self)511 pub fn finish(&mut self) { 512 match self.start { 513 Some((x, y)) if self.last != self.start => { 514 self.inner.line_to(x, y); 515 } 516 _ => {} 517 } 518 } 519 } 520 521 impl<'a, S> CommandSink for NopFilteringSink<'a, S> 522 where 523 S: CommandSink, 524 { hstem(&mut self, y: Fixed, dy: Fixed)525 fn hstem(&mut self, y: Fixed, dy: Fixed) { 526 self.inner.hstem(y, dy); 527 } 528 vstem(&mut self, x: Fixed, dx: Fixed)529 fn vstem(&mut self, x: Fixed, dx: Fixed) { 530 self.inner.vstem(x, dx); 531 } 532 hint_mask(&mut self, mask: &[u8])533 fn hint_mask(&mut self, mask: &[u8]) { 534 self.inner.hint_mask(mask); 535 } 536 counter_mask(&mut self, mask: &[u8])537 fn counter_mask(&mut self, mask: &[u8]) { 538 self.inner.counter_mask(mask); 539 } 540 move_to(&mut self, x: Fixed, y: Fixed)541 fn move_to(&mut self, x: Fixed, y: Fixed) { 542 self.pending_move = Some((x, y)); 543 } 544 line_to(&mut self, x: Fixed, y: Fixed)545 fn line_to(&mut self, x: Fixed, y: Fixed) { 546 if self.pending_move == Some((x, y)) { 547 return; 548 } 549 self.flush_pending_move(); 550 if self.last == Some((x, y)) || (self.last.is_none() && self.start == Some((x, y))) { 551 return; 552 } 553 self.inner.line_to(x, y); 554 self.last = Some((x, y)); 555 } 556 curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed)557 fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { 558 self.flush_pending_move(); 559 self.last = Some((x, y)); 560 self.inner.curve_to(cx1, cy1, cx2, cy2, x, y); 561 } 562 close(&mut self)563 fn close(&mut self) { 564 if self.pending_move.is_none() { 565 self.inner.close(); 566 self.start = None; 567 self.last = None; 568 } 569 } 570 } 571 572 #[cfg(test)] 573 mod tests { 574 use super::*; 575 use read_fonts::FontRef; 576 577 #[test] unscaled_scaling_sink_produces_integers()578 fn unscaled_scaling_sink_produces_integers() { 579 let nothing = &mut (); 580 let sink = ScalingSink26Dot6::new(nothing, Fixed::ONE); 581 for coord in [50.0, 50.1, 50.125, 50.5, 50.9] { 582 assert_eq!(sink.scale(Fixed::from_f64(coord)).to_f32(), 50.0); 583 } 584 } 585 586 #[test] scaled_scaling_sink()587 fn scaled_scaling_sink() { 588 let ppem = 20.0; 589 let upem = 1000.0; 590 // match FreeType scaling with intermediate conversion to 26.6 591 let scale = Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem as i32); 592 let nothing = &mut (); 593 let sink = ScalingSink26Dot6::new(nothing, scale); 594 let inputs = [ 595 // input coord, expected scaled output 596 (0.0, 0.0), 597 (8.0, 0.15625), 598 (16.0, 0.3125), 599 (32.0, 0.640625), 600 (72.0, 1.4375), 601 (128.0, 2.5625), 602 ]; 603 for (coord, expected) in inputs { 604 assert_eq!( 605 sink.scale(Fixed::from_f64(coord)).to_f32(), 606 expected, 607 "scaling coord {coord}" 608 ); 609 } 610 } 611 612 #[test] read_cff_static()613 fn read_cff_static() { 614 let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap(); 615 let cff = Outlines::new(&font).unwrap(); 616 assert!(!cff.is_cff2()); 617 assert!(cff.top_dict.var_store.is_none()); 618 assert!(cff.top_dict.font_dicts.is_none()); 619 assert!(cff.top_dict.private_dict_range.is_some()); 620 assert!(cff.top_dict.fd_select.is_none()); 621 assert_eq!(cff.subfont_count(), 1); 622 assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); 623 assert_eq!(cff.global_subrs().count(), 17); 624 } 625 626 #[test] read_cff2_static()627 fn read_cff2_static() { 628 let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); 629 let cff = Outlines::new(&font).unwrap(); 630 assert!(cff.is_cff2()); 631 assert!(cff.top_dict.var_store.is_some()); 632 assert!(cff.top_dict.font_dicts.is_some()); 633 assert!(cff.top_dict.private_dict_range.is_none()); 634 assert!(cff.top_dict.fd_select.is_none()); 635 assert_eq!(cff.subfont_count(), 1); 636 assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); 637 assert_eq!(cff.global_subrs().count(), 0); 638 } 639 640 #[test] read_example_cff2_table()641 fn read_example_cff2_table() { 642 let cff = Outlines::from_cff2( 643 Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap(), 644 1000, 645 ) 646 .unwrap(); 647 assert!(cff.is_cff2()); 648 assert!(cff.top_dict.var_store.is_some()); 649 assert!(cff.top_dict.font_dicts.is_some()); 650 assert!(cff.top_dict.private_dict_range.is_none()); 651 assert!(cff.top_dict.fd_select.is_none()); 652 assert_eq!(cff.subfont_count(), 1); 653 assert_eq!(cff.subfont_index(GlyphId::new(1)), 0); 654 assert_eq!(cff.global_subrs().count(), 0); 655 } 656 657 #[test] cff2_variable_outlines_match_freetype()658 fn cff2_variable_outlines_match_freetype() { 659 compare_glyphs( 660 font_test_data::CANTARELL_VF_TRIMMED, 661 font_test_data::CANTARELL_VF_TRIMMED_GLYPHS, 662 ); 663 } 664 665 #[test] cff_static_outlines_match_freetype()666 fn cff_static_outlines_match_freetype() { 667 compare_glyphs( 668 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED, 669 font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS, 670 ); 671 } 672 673 /// For the given font data and extracted outlines, parse the extracted 674 /// outline data into a set of expected values and compare these with the 675 /// results generated by the scaler. 676 /// 677 /// This will compare all outlines at various sizes and (for variable 678 /// fonts), locations in variation space. compare_glyphs(font_data: &[u8], expected_outlines: &str)679 fn compare_glyphs(font_data: &[u8], expected_outlines: &str) { 680 let font = FontRef::new(font_data).unwrap(); 681 let expected_outlines = read_fonts::scaler_test::parse_glyph_outlines(expected_outlines); 682 let outlines = super::Outlines::new(&font).unwrap(); 683 let mut path = read_fonts::scaler_test::Path::default(); 684 for expected_outline in &expected_outlines { 685 if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() { 686 continue; 687 } 688 let size = (expected_outline.size != 0.0).then_some(expected_outline.size); 689 path.elements.clear(); 690 let subfont = outlines 691 .subfont( 692 outlines.subfont_index(expected_outline.glyph_id), 693 size, 694 &expected_outline.coords, 695 ) 696 .unwrap(); 697 outlines 698 .draw( 699 &subfont, 700 expected_outline.glyph_id, 701 &expected_outline.coords, 702 false, 703 &mut path, 704 ) 705 .unwrap(); 706 if path.elements != expected_outline.path { 707 panic!( 708 "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}", 709 expected_outline.glyph_id, 710 expected_outline.size, 711 expected_outline.coords, 712 &path.elements, 713 &expected_outline.path 714 ); 715 } 716 } 717 } 718 } 719