diff --git a/font-test-data/src/lib.rs b/font-test-data/src/lib.rs
index 407ecff28..0b6242363 100644
--- a/font-test-data/src/lib.rs
+++ b/font-test-data/src/lib.rs
@@ -87,6 +87,9 @@ pub static AHEM: &[u8] = include_bytes!("../test_data/ttf/ahem.ttf");
pub static AVAR2_CHECKER: &[u8] = include_bytes!("../test_data/ttf/avar2checker.ttf");
+pub static MATERIAL_ICONS_SUBSET: &[u8] =
+ include_bytes!("../test_data/ttf/material_icons_subset.ttf");
+
pub mod varc {
pub static CJK_6868: &[u8] = include_bytes!("../test_data/ttf/varc-6868.ttf");
pub static CONDITIONALS: &[u8] = include_bytes!("../test_data/ttf/varc-ac01-conditional.ttf");
diff --git a/font-test-data/test_data/README.md b/font-test-data/test_data/README.md
index 3a9431bdc..59b391006 100644
--- a/font-test-data/test_data/README.md
+++ b/font-test-data/test_data/README.md
@@ -73,6 +73,16 @@ Describes the provenance, usage and generation procedures for font data used for
pyftsubset notoserif-regular.ttf --layout-features+=c2sc --no-hinting --text=Hfix
```
+* _material_icons_subset_
+ * font: Google Material Icons Regular
+ * source: https://fonts.googleapis.com/icon?family=Material+Icons
+ * license: [Apache 2][Apache2]
+ * usage: testing empty Private DICT and scaling with upem=512, ppem=8
+ * subset: just a single glyph to check scaling
+ ```shell
+ pyftsubset material_icons.otf --gids=2
+ ```
+
* _avar2checker_
* font: avar2 checker
* source: https://github.com/Lorp/fencer/tree/main/src/fonts
diff --git a/font-test-data/test_data/ttf/material_icons_subset.ttf b/font-test-data/test_data/ttf/material_icons_subset.ttf
new file mode 100644
index 000000000..ab473c789
Binary files /dev/null and b/font-test-data/test_data/ttf/material_icons_subset.ttf differ
diff --git a/font-test-data/test_data/ttx/material_icons_subset.ttx b/font-test-data/test_data/ttx/material_icons_subset.ttx
new file mode 100644
index 000000000..5bf1a20df
--- /dev/null
+++ b/font-test-data/test_data/ttx/material_icons_subset.ttx
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Copyright 2019-2023 Google LLC. All Rights Reserved.
+
+
+ Google Material Icons
+
+
+ Regular
+
+
+ Google Material Icons 2024-06-24T07:13:04.639094
+
+
+ Google Material Icons Regular
+
+
+ 2024-06-24T07:13:04.639094
+
+
+ GoogleMaterialIcons-Regular
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 512 endchar
+
+
+ 512 405 448 rmoveto
+ -298 hlineto
+ -24 -19 -19 -24 hvcurveto
+ -298 vlineto
+ -24 19 -19 24 vhcurveto
+ 298 hlineto
+ 24 19 19 24 hvcurveto
+ 298 vlineto
+ 24 -19 19 -24 vhcurveto
+ -128 vmoveto
+ -213 -298 181 21 -96 32 128 -53 85 298 -85 vlineto
+ -74 -48 rmoveto
+ 48 -32 -128 32 48 vlineto
+ 37 -48 37 0 -48 64 48 64 -37 0 -37 -48 rlineto
+ -128 -80 rmoveto
+ 53 hlineto
+ 12 9 10 11 hvcurveto
+ 86 vlineto
+ 11 -9 10 -12 vhcurveto
+ -53 hlineto
+ -12 -10 -10 -11 hvcurveto
+ -86 vlineto
+ -11 10 -10 12 vhcurveto
+ 10 96 rmoveto
+ 32 -64 -32 64 hlineto
+ endchar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/skrifa/src/outline/cff/mod.rs b/skrifa/src/outline/cff/mod.rs
index 52dfc0caa..02c812c88 100644
--- a/skrifa/src/outline/cff/mod.rs
+++ b/skrifa/src/outline/cff/mod.rs
@@ -169,13 +169,16 @@ impl<'a> Outlines<'a> {
Some(ppem) if self.units_per_em > 0 => {
// Note: we do an intermediate scale to 26.6 to ensure we
// match FreeType
- Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(self.units_per_em as i32)
+ Some(
+ Fixed::from_bits((ppem * 64.) as i32)
+ / Fixed::from_bits(self.units_per_em as i32),
+ )
}
- _ => Fixed::ONE,
+ _ => None,
};
// When hinting, use a modified scale factor
//
- let hint_scale = Fixed::from_bits((scale.to_bits() + 32) / 64);
+ let hint_scale = Fixed::from_bits((scale.unwrap_or(Fixed::ONE).to_bits() + 32) / 64);
let hint_state = HintState::new(&hint_params, hint_scale);
Ok(Subfont {
is_cff2: self.is_cff2(),
@@ -210,7 +213,8 @@ impl<'a> Outlines<'a> {
let blend_state = subfont.blend_state(self, coords)?;
let mut pen_sink = PenSink::new(pen);
let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink);
- if hint {
+ // Only apply hinting if we have a scale
+ if hint && subfont.scale.is_some() {
let mut hinting_adapter =
HintingSink::new(&subfont.hint_state, &mut simplifying_adapter);
charstring::evaluate(
@@ -250,14 +254,12 @@ impl<'a> Outlines<'a> {
}
range
} else {
- // Last chance, use the private dict range from the top dict if
- // available.
+ // Use the private dict range from the top dict.
+ // Note: "A Private DICT is required but may be specified as having
+ // a length of 0 if there are no non-default values to be stored."
+ //
let range = self.top_dict.private_dict_range.clone();
- if !range.is_empty() {
- Some(range.start as usize..range.end as usize)
- } else {
- None
- }
+ Some(range.start as usize..range.end as usize)
}
.ok_or(Error::MissingPrivateDict)
}
@@ -273,7 +275,7 @@ impl<'a> Outlines<'a> {
#[derive(Clone)]
pub(crate) struct Subfont {
is_cff2: bool,
- scale: Fixed,
+ scale: Option,
subrs_offset: Option,
pub(crate) hint_state: HintState,
store_index: u16,
@@ -396,11 +398,11 @@ where
/// scaling process.
struct ScalingSink26Dot6<'a, S> {
inner: &'a mut S,
- scale: Fixed,
+ scale: Option,
}
impl<'a, S> ScalingSink26Dot6<'a, S> {
- fn new(sink: &'a mut S, scale: Fixed) -> Self {
+ fn new(sink: &'a mut S, scale: Option) -> Self {
Self { scale, inner: sink }
}
@@ -419,11 +421,11 @@ impl<'a, S> ScalingSink26Dot6<'a, S> {
// converts to font units.
//
let b = Fixed::from_bits(a.to_bits() >> 10);
- if self.scale != Fixed::ONE {
+ if let Some(scale) = self.scale {
// Scaled case:
// 3. Multiply by the original scale factor (to 26.6)
//
- let c = b * self.scale;
+ let c = b * scale;
// 4. Convert from 26.6 to 16.16
Fixed::from_bits(c.to_bits() << 10)
} else {
@@ -591,7 +593,7 @@ mod tests {
#[test]
fn unscaled_scaling_sink_produces_integers() {
let nothing = &mut ();
- let sink = ScalingSink26Dot6::new(nothing, Fixed::ONE);
+ let sink = ScalingSink26Dot6::new(nothing, None);
for coord in [50.0, 50.1, 50.125, 50.5, 50.9] {
assert_eq!(sink.scale(Fixed::from_f64(coord)).to_f32(), 50.0);
}
@@ -604,7 +606,7 @@ mod tests {
// match FreeType scaling with intermediate conversion to 26.6
let scale = Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem as i32);
let nothing = &mut ();
- let sink = ScalingSink26Dot6::new(nothing, scale);
+ let sink = ScalingSink26Dot6::new(nothing, Some(scale));
let inputs = [
// input coord, expected scaled output
(0.0, 0.0),
@@ -707,6 +709,36 @@ mod tests {
assert!(svg.to_string().ends_with('Z'));
}
+ /// Ensure we don't reject an empty Private DICT
+ #[test]
+ fn empty_private_dict() {
+ let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
+ let common = OutlinesCommon::new(&font).unwrap();
+ let outlines = super::Outlines::new(&common).unwrap();
+ assert!(outlines.top_dict.private_dict_range.is_empty());
+ assert!(outlines.private_dict_range(0).unwrap().is_empty());
+ }
+
+ /// Actually apply a scale when the computed scale factor is
+ /// equal to Fixed::ONE.
+ ///
+ /// Specifically, when upem = 512 and ppem = 8, this results in
+ /// a scale factor of 65536 which was being interpreted as an
+ /// unscaled draw request.
+ #[test]
+ fn proper_scaling_when_factor_equals_fixed_one() {
+ let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
+ assert_eq!(font.head().unwrap().units_per_em(), 512);
+ let glyphs = font.outline_glyphs();
+ let glyph = glyphs.get(GlyphId::new(1)).unwrap();
+ let mut svg = SvgPen::with_precision(6);
+ glyph
+ .draw((Size::new(8.0), LocationRef::default()), &mut svg)
+ .unwrap();
+ // This was initially producing unscaled values like M405.000...
+ assert!(svg.starts_with("M6.328125,7.000000 L1.671875,7.000000"));
+ }
+
/// For the given font data and extracted outlines, parse the extracted
/// outline data into a set of expected values and compare these with the
/// results generated by the scaler.