diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdf3fea1..c5414c22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: - uses: dtolnay/rust-toolchain@stable - run: cargo build name: Build + - run: cargo build --no-default-features + name: Build without default features - run: cargo test name: Run tests diff --git a/Cargo.lock b/Cargo.lock index bacc08a6..647b6900 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.4.0" @@ -78,6 +84,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "font-types" version = "0.8.3" @@ -87,6 +99,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "font-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7" +dependencies = [ + "bytemuck", +] + [[package]] name = "fuzz" version = "0.2.2" @@ -94,7 +115,7 @@ dependencies = [ "rand", "rand_distr", "rayon", - "skrifa", + "skrifa 0.29.2", "subsetter", "ttf-parser", "walkdir", @@ -112,6 +133,32 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "kurbo" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c" +dependencies = [ + "arrayvec", + "smallvec", +] + [[package]] name = "libc" version = "0.2.171" @@ -124,6 +171,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "num-traits" version = "0.2.19" @@ -234,7 +287,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f14974c88fb4fd0a7203719f98020209248c9dbebaf9d10d860337797a905097" dependencies = [ "bytemuck", - "font-types", + "font-types 0.8.3", +] + +[[package]] +name = "read-fonts" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb16bffd9221b97a7de1b048520122b3926cf11e778c42c4c7514a4ee9da9455" +dependencies = [ + "bytemuck", + "font-types 0.9.0", ] [[package]] @@ -259,16 +322,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c0ca53de9bb9bee1720c727606275148463cd938eb6bde249dcedeec4967747" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.27.5", +] + +[[package]] +name = "skrifa" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc1f79e5624f166718224ef883095bea315801aca75b62c64f3937755a586d" +dependencies = [ + "bytemuck", + "read-fonts 0.31.2", ] +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "subsetter" version = "0.2.2" dependencies = [ + "kurbo", "rustc-hash", - "skrifa", + "skrifa 0.33.2", "ttf-parser", + "write-fonts", ] [[package]] @@ -404,6 +485,19 @@ dependencies = [ "bitflags", ] +[[package]] +name = "write-fonts" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac89848aeba9b7e1877f1e89e3f9c95adb8771f640e1ef23c605aa734748c2b" +dependencies = [ + "font-types 0.9.0", + "indexmap", + "kurbo", + "log", + "read-fonts 0.31.2", +] + [[package]] name = "zerocopy" version = "0.8.24" diff --git a/Cargo.toml b/Cargo.toml index 91a6fc02..3694b43a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,16 @@ repository = { workspace = true } readme = { workspace = true } license = { workspace = true } +[features] +default = ["variable-fonts"] +variable-fonts = ["dep:skrifa", "dep:write-fonts", "dep:kurbo"] + [dependencies] rustc-hash = "2.1" +skrifa = { optional = true, version = "0.33" } +kurbo = { optional = true, version = "0.11" } +write-fonts = { optional = true, version = "0.39.1"} [dev-dependencies] -skrifa = "0.29.0" +skrifa = "0.33" ttf-parser = "0.25.1" diff --git a/NOTICE b/NOTICE index 92813ce8..8d921d26 100644 --- a/NOTICE +++ b/NOTICE @@ -66,7 +66,9 @@ SOFTWARE. The SIL OPEN FONT LICENSE Version 1.1 applies to the following fonts: - fonts/ClickerScript-Regular.ttf - fonts/MPLUS1p-Regular.ttf +- fonts/Cantarell-VF.otf - fonts/NotoSans-Regular.ttf +- fonts/NotoSans-Regular_var.ttf - fonts/NotoSansCJKsc-Bold-subset1.otf - fonts/NotoSansCJKsc-Regular.otf - fonts/Syne-Regular_subset.oft (Copyright 2017 The Syne Project Authors (https://gitlab.com/bonjour-monde/fonderie/syne-typeface)) diff --git a/fonts/Cantarell-VF.otf b/fonts/Cantarell-VF.otf new file mode 100644 index 00000000..c921a9bd Binary files /dev/null and b/fonts/Cantarell-VF.otf differ diff --git a/fonts/NotoSans-Regular_var.ttf b/fonts/NotoSans-Regular_var.ttf new file mode 100644 index 00000000..a5853d7e Binary files /dev/null and b/fonts/NotoSans-Regular_var.ttf differ diff --git a/src/cff2.rs b/src/cff2.rs new file mode 100644 index 00000000..79631363 --- /dev/null +++ b/src/cff2.rs @@ -0,0 +1,25 @@ +use crate::interjector::Interjector; +use crate::Error::MalformedFont; +use crate::{glyf, Context, MaxpData}; +use std::borrow::Cow; + +/// CFF2 fonts will currently be converted into TTF fonts. +pub fn subset(ctx: &mut Context) -> crate::Result<()> { + let mut maxp_data = MaxpData::default(); + + let result = glyf::subset_with(ctx, |old_gid, ctx| { + let data = match &ctx.interjector { + // We reject CFF2 fonts earlier if `variable-fonts` feature is not enabled. + Interjector::Dummy(_) => unreachable!(), + #[cfg(feature = "variable-fonts")] + Interjector::Skrifa(s) => { + Cow::Owned(s.glyph_data(&mut maxp_data, old_gid).ok_or(MalformedFont)?) + } + }; + + Ok(data) + }); + + ctx.custom_maxp_data = Some(maxp_data); + result +} diff --git a/src/glyf.rs b/src/glyf.rs index 9d5e2815..40cff8a4 100644 --- a/src/glyf.rs +++ b/src/glyf.rs @@ -46,7 +46,29 @@ pub fn closure(face: &Face, glyph_remapper: &mut GlyphRemapper) -> Result<()> { } pub fn subset(ctx: &mut Context) -> Result<()> { - let subsetted_entries = subset_glyf_entries(ctx)?; + let table = Table::new(&ctx.face).ok_or(MalformedFont)?; + let mut _maxp = MaxpData::default(); + + subset_with(ctx, |old_gid, ctx| { + let data = match &ctx.interjector { + Interjector::Dummy(_) => { + Cow::Borrowed(table.glyph_data(old_gid).ok_or(MalformedFont)?) + } + #[cfg(feature = "variable-fonts")] + Interjector::Skrifa(s) => { + Cow::Owned(s.glyph_data(&mut _maxp, old_gid).ok_or(MalformedFont)?) + } + }; + + Ok(data) + }) +} + +pub(crate) fn subset_with<'a>( + ctx: &mut Context<'a>, + glyph_data_fn: impl FnMut(u16, &Context<'a>) -> Result>, +) -> Result<()> { + let subsetted_entries = subset_glyf_entries(ctx, glyph_data_fn)?; let mut sub_glyf = Writer::new(); let mut sub_loca = Writer::new(); @@ -112,29 +134,30 @@ impl<'a> Table<'a> { } } -fn subset_glyf_entries<'a>(ctx: &mut Context<'a>) -> Result>> { - let table = Table::new(&ctx.face).ok_or(MalformedFont)?; - +fn subset_glyf_entries<'a>( + ctx: &mut Context<'a>, + mut glyph_data_fn: impl FnMut(u16, &Context<'a>) -> Result>, +) -> Result>> { let mut size = 0; let mut glyf_entries = vec![]; for old_gid in ctx.mapper.remapped_gids() { - let glyph_data = table.glyph_data(old_gid).ok_or(MalformedFont)?; + let glyph_data = glyph_data_fn(old_gid, ctx)?; // Empty glyph. if glyph_data.is_empty() { - glyf_entries.push(Cow::Borrowed(glyph_data)); + glyf_entries.push(glyph_data); continue; } - let mut r = Reader::new(glyph_data); + let mut r = Reader::new(&glyph_data); let num_contours = r.read::().ok_or(MalformedFont)?; let glyph_data = if num_contours < 0 { - Cow::Owned(remap_component_glyph(&ctx.mapper, glyph_data)?) + Cow::Owned(remap_component_glyph(&ctx.mapper, &glyph_data)?) } else { // Simple glyphs don't need any subsetting. - Cow::Borrowed(glyph_data) + glyph_data }; let mut len = glyph_data.len(); diff --git a/src/hmtx.rs b/src/hmtx.rs index 2c87d7e4..dd52dcad 100644 --- a/src/hmtx.rs +++ b/src/hmtx.rs @@ -19,39 +19,13 @@ pub fn subset(ctx: &mut Context) -> Result<()> { let new_metrics = { let mut new_metrics = vec![]; - // Extract the number of horizontal metrics from the `hhea` table. - let num_h_metrics = { - let hhea = ctx.expect_table(Tag::HHEA).ok_or(MalformedFont)?; - let mut r = Reader::new(hhea); - r.skip_bytes(34); - r.read::().ok_or(MalformedFont)? - }; - - let last_advance_width = { - let index = 4 * num_h_metrics.checked_sub(1).ok_or(OverflowError)? as usize; - let mut r = Reader::new(hmtx.get(index..).ok_or(MalformedFont)?); - r.read::().ok_or(MalformedFont)? - }; - - for old_gid in ctx.mapper.remapped_gids() { - let has_advance_width = old_gid < num_h_metrics; - - let offset = if has_advance_width { - old_gid as usize * 4 - } else { - let num_h_metrics = num_h_metrics as usize; - num_h_metrics * 4 + (old_gid as usize - num_h_metrics) * 2 - }; - - let mut r = Reader::new(hmtx.get(offset..).ok_or(MalformedFont)?); - - if has_advance_width { - let adv = r.read::().ok_or(MalformedFont)?; - let lsb = r.read::().ok_or(MalformedFont)?; - new_metrics.push((adv, lsb)); - } else { - new_metrics - .push((last_advance_width, r.read::().ok_or(MalformedFont)?)); + match &ctx.interjector { + Interjector::Dummy(_) => extract_metrics(hmtx, &mut new_metrics, ctx)?, + #[cfg(feature = "variable-fonts")] + Interjector::Skrifa(s) => { + for old_gid in ctx.mapper.remapped_gids() { + new_metrics.push(s.horizontal_metrics(old_gid).ok_or(MalformedFont)?); + } } } @@ -79,7 +53,7 @@ pub fn subset(ctx: &mut Context) -> Result<()> { sub_hmtx.write::(metric.0); } - sub_hmtx.write::(metric.1); + sub_hmtx.write::(metric.1); } ctx.push(Tag::HMTX, sub_hmtx.finish()); @@ -93,3 +67,46 @@ pub fn subset(ctx: &mut Context) -> Result<()> { Ok(()) } + +fn extract_metrics( + hmtx: &[u8], + new_metrics: &mut Vec<(u16, i16)>, + ctx: &mut Context, +) -> Result<()> { + // Extract the number of horizontal metrics from the `hhea` table. + let num_h_metrics = { + let hhea = ctx.expect_table(Tag::HHEA).ok_or(MalformedFont)?; + let mut r = Reader::new(hhea); + r.skip_bytes(34); + r.read::().ok_or(MalformedFont)? + }; + + let last_advance_width = { + let index = 4 * num_h_metrics.checked_sub(1).ok_or(OverflowError)? as usize; + let mut r = Reader::new(hmtx.get(index..).ok_or(MalformedFont)?); + r.read::().ok_or(MalformedFont)? + }; + + for old_gid in ctx.mapper.remapped_gids() { + let has_advance_width = old_gid < num_h_metrics; + + let offset = if has_advance_width { + old_gid as usize * 4 + } else { + let num_h_metrics = num_h_metrics as usize; + num_h_metrics * 4 + (old_gid as usize - num_h_metrics) * 2 + }; + + let mut r = Reader::new(hmtx.get(offset..).ok_or(MalformedFont)?); + + if has_advance_width { + let adv = r.read::().ok_or(MalformedFont)?; + let lsb = r.read::().ok_or(MalformedFont)?; + new_metrics.push((adv, lsb)); + } else { + new_metrics.push((last_advance_width, r.read::().ok_or(MalformedFont)?)); + } + } + + Ok(()) +} diff --git a/src/interjector.rs b/src/interjector.rs new file mode 100644 index 00000000..b930905e --- /dev/null +++ b/src/interjector.rs @@ -0,0 +1,164 @@ +use std::marker::PhantomData; + +pub(crate) enum Interjector<'a> { + Dummy(PhantomData<&'a ()>), + #[cfg(feature = "variable-fonts")] + Skrifa(skrifa::SkrifaInterjector<'a>), +} + +#[cfg(feature = "variable-fonts")] +pub(crate) mod skrifa { + use crate::{MaxpData, Tag}; + use kurbo::{BezPath, CubicBez}; + use skrifa::instance::Location; + use skrifa::outline::{DrawSettings, OutlinePen}; + use skrifa::prelude::Size; + use skrifa::{FontRef, GlyphId, MetadataProvider}; + use write_fonts::tables::glyf::SimpleGlyph; + use write_fonts::{dump_table, FontWrite, TableWriter}; + + pub(crate) struct SkrifaInterjector<'a> { + font_ref: FontRef<'a>, + location: Location, + } + + impl<'a> SkrifaInterjector<'a> { + pub(crate) fn new( + data: &'a [u8], + index: u32, + location: &[(Tag, f32)], + ) -> Option { + let font_ref = FontRef::from_index(data, index).ok()?; + let location = font_ref + .axes() + .location(location.iter().map(|i| (skrifa::Tag::new(i.0.get()), i.1))); + + Some(Self { font_ref, location }) + } + } + + impl<'a> SkrifaInterjector<'a> { + /// Return the advance width and left side bearing of the glyph. + pub(crate) fn horizontal_metrics(&self, glyph: u16) -> Option<(u16, i16)> { + let metrics = self.font_ref.glyph_metrics(Size::unscaled(), &self.location); + + let adv = metrics.advance_width(GlyphId::new(glyph as u32))?; + // Note that for variable fonts, our left side bearing points don't seem to + // match the ones from fonttools (they use some different technique for deriving + // it which isn't reflected in skrifa's API), but I _think_ that this shouldn't + // really be relevant in the context of PDF. + let lsb = metrics.left_side_bearing(GlyphId::new(glyph as u32))?; + + Some((adv.round() as u16, lsb.round() as i16)) + } + + /// Return the glyph description in the `glyf` outline format. + pub(crate) fn glyph_data<'b>( + &'b self, + maxp_data: &'b mut MaxpData, + glyph: u16, + ) -> Option> { + let outlines = self.font_ref.outline_glyphs(); + + let mut outline_builder = OutlinePath::new(); + let glyph = GlyphId::new(glyph as u32); + + if let Some(outline_glyph) = outlines.get(glyph) { + outline_glyph + .draw( + DrawSettings::unhinted(Size::unscaled(), &self.location), + &mut outline_builder, + ) + .ok()?; + } + + let path = outline_builder.path; + + if path.is_empty() { + return Some(vec![]); + } + + let simple_glyph = SimpleGlyph::from_bezpath(&path).ok()?; + + maxp_data.max_points = maxp_data + .max_points + .max(simple_glyph.contours.iter().map(|c| c.len() as u16).sum()); + maxp_data.max_contours = + maxp_data.max_contours.max(simple_glyph.contours.len() as u16); + + let mut writer = TableWriter::default(); + simple_glyph.write_into(&mut writer); + + dump_table(&simple_glyph).ok() + } + } + + pub(crate) struct OutlinePath { + last_move_to: (f32, f32), + last_point: (f32, f32), + path: BezPath, + } + + impl OutlinePath { + fn new() -> Self { + Self { + last_move_to: (0.0, 0.0), + last_point: (0.0, 0.0), + path: BezPath::new(), + } + } + } + + impl OutlinePen for OutlinePath { + #[inline] + fn move_to(&mut self, x: f32, y: f32) { + self.path.move_to((x, y)); + self.last_move_to = (x, y); + self.last_point = (x, y); + } + + #[inline] + fn line_to(&mut self, x: f32, y: f32) { + self.path.line_to((x, y)); + self.last_point = (x, y); + } + + #[inline] + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + // Only called by TrueType fonts. + self.path.quad_to((cx, cy), (x, y)); + self.last_point = (x, y); + } + + #[inline] + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + // Only called by CFF2 fonts. + let cubic = CubicBez::new( + (self.last_point.0 as f64, self.last_point.1 as f64), + (cx0 as f64, cy0 as f64), + (cx1 as f64, cy1 as f64), + (x as f64, y as f64), + ); + + // It is not entirely clear how small the `accuracy` parameter needs to be + // to produce sensible results. In `vello_cpu`, a value of around 0.025 is used + // (0.25 for the flattening accuracy and * 0.1 in the flattening code, so we choose + // the same value here. + for (_, _, quad) in cubic.to_quads(0.025) { + // Note that `quad.p2` is the same as `quad.p0` of the next point in the iterator. + self.quad_to( + quad.p1.x as f32, + quad.p1.y as f32, + quad.p2.x as f32, + quad.p2.y as f32, + ); + } + } + + #[inline] + fn close(&mut self) { + self.path.close_path(); + self.last_point = self.last_move_to; + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 68a9ded5..cff72616 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,9 +73,12 @@ resulting font is 1 KB. #![deny(missing_docs)] mod cff; +#[cfg(feature = "variable-fonts")] +mod cff2; mod glyf; mod head; mod hmtx; +mod interjector; mod maxp; mod name; mod post; @@ -83,44 +86,119 @@ mod read; mod remapper; mod write; +use crate::interjector::Interjector; +use crate::maxp::MaxpData; use crate::read::{Readable, Reader}; pub use crate::remapper::GlyphRemapper; use crate::write::{Writeable, Writer}; -use crate::Error::{MalformedFont, UnknownKind}; +use crate::Error::{MalformedFont, Unimplemented, UnknownKind}; use std::borrow::Cow; use std::fmt::{self, Debug, Display, Formatter}; +use std::marker::PhantomData; /// Subset the font face to include only the necessary glyphs and tables. /// /// - The `data` must be in the OpenType font format. /// - The `index` is only relevant if the data contains a font collection /// (`.ttc` or `.otc` file). Otherwise, it should be 0. +/// +/// CFF2 fonts are not supported. pub fn subset(data: &[u8], index: u32, mapper: &GlyphRemapper) -> Result> { + subset_inner(data, index, &[], false, mapper) +} + +/// Subset the font face to include only the necessary glyphs and tables, instantiated +/// to the given variation coordinates. +/// +/// This does the same as [`subset`], but allows you to specify variation coordinates. +/// +/// It is important to note that if you pass a CFF2 font, it will be converted to a TrueType +/// font. +#[cfg(feature = "variable-fonts")] +pub fn subset_with_variations( + data: &[u8], + index: u32, + variation_coordinates: &[(Tag, f32)], + mapper: &GlyphRemapper, +) -> Result> { + subset_inner(data, index, variation_coordinates, true, mapper) +} + +fn subset_inner( + data: &[u8], + index: u32, + variation_coordinates: &[(Tag, f32)], + allow_cff2: bool, + mapper: &GlyphRemapper, +) -> Result> { let mapper = mapper.clone(); - let context = prepare_context(data, index, mapper)?; + let context = + prepare_context(data, index, variation_coordinates, allow_cff2, mapper)?; _subset(context) } -fn prepare_context( - data: &[u8], +fn prepare_context<'a>( + data: &'a [u8], index: u32, + #[cfg_attr(not(feature = "variable-fonts"), allow(unused))] + variation_coordinates: &[(Tag, f32)], + allow_cff2: bool, mut gid_remapper: GlyphRemapper, -) -> Result { +) -> Result> { let face = parse(data, index)?; - let kind = match (face.table(Tag::GLYF), face.table(Tag::CFF)) { - (Some(_), _) => FontKind::TrueType, - (_, Some(_)) => FontKind::Cff, - _ => return Err(UnknownKind), + let flavor = if face.table(Tag::GLYF).is_some() { + FontFlavor::TrueType + } else if face.table(Tag::CFF).is_some() { + FontFlavor::Cff + } else if face.table(Tag::CFF2).is_some() { + // Only works if `variable-fonts` feature is enabled, as we need skrifa + // so we can convert CFF2 into TrueType. + if allow_cff2 { + FontFlavor::Cff2 + } else { + return Err(Unimplemented); + } + } else { + return Err(UnknownKind); }; - if kind == FontKind::TrueType { + if flavor == FontFlavor::TrueType { glyf::closure(&face, &mut gid_remapper)?; } + let _ = variation_coordinates; + + #[cfg(not(feature = "variable-fonts"))] + let interjector = Interjector::Dummy(PhantomData::default()); + // For CFF, we _always_ want to do normal subsetting, since CFF cannot have variations. + // For TrueType, we prefer normal subsetting in case no variation was requested. If we do have + // variations, we use `skrifa` to instance. + // For CFF2, we _always_ use `skrifa` to instance. + #[cfg(feature = "variable-fonts")] + let interjector = if (variation_coordinates.is_empty() + && flavor == FontFlavor::TrueType) + || flavor == FontFlavor::Cff + { + // For TrueType and CFF, we are still best off using the normal subsetting logic in case no variation coordinates + // have been passed. + Interjector::Dummy(PhantomData::default()) + } else { + Interjector::Skrifa( + interjector::skrifa::SkrifaInterjector::new( + data, + index, + variation_coordinates, + ) + .ok_or(MalformedFont)?, + ) + }; + Ok(Context { face, mapper: gid_remapper, - kind, + interjector, + custom_maxp_data: None, + flavor, tables: vec![], long_loca: false, }) @@ -138,16 +216,16 @@ fn _subset(mut ctx: Context) -> Result> { // - GASP: Not mandated by PDF specification, and ghostscript also seems to exclude them. // - OS2: Not mandated by PDF specification, and ghostscript also seems to exclude them. - if ctx.kind == FontKind::TrueType { + if ctx.flavor == FontFlavor::TrueType { // LOCA will be handled by GLYF ctx.process(Tag::GLYF)?; ctx.process(Tag::CVT)?; // won't be subsetted. ctx.process(Tag::FPGM)?; // won't be subsetted. ctx.process(Tag::PREP)?; // won't be subsetted. - } - - if ctx.kind == FontKind::Cff { + } else if ctx.flavor == FontFlavor::Cff { ctx.process(Tag::CFF)?; + } else if ctx.flavor == FontFlavor::Cff2 { + ctx.process(Tag::CFF2)?; } // Required tables. @@ -202,7 +280,7 @@ fn construct(mut ctx: Context) -> Vec { ctx.tables.sort_by_key(|&(tag, _)| tag); let mut w = Writer::new(); - w.write::(ctx.kind); + w.write(ctx.flavor); // Write table directory. let count = ctx.tables.len() as u16; @@ -280,10 +358,22 @@ struct Context<'a> { face: Face<'a>, /// A map from old gids to new gids, and the reverse mapper: GlyphRemapper, - /// The kind of face. - kind: FontKind, + /// The font flavor. + flavor: FontFlavor, /// Subsetted tables. tables: Vec<(Tag, Cow<'a, [u8]>)>, + /// An object that can _interject_ data during the subsetting process. + /// Normally, when subsetting CFF/TrueType fonts, we will simply read the corresponding + /// data from the old font and rewrite it to the new font, for example when writing the + /// `hmtx` table. + /// + /// However, in case we are subsetting with variation coordinates, we instead rely on skrifa + /// to apply the variation coordinates and interject that data during the subsetting process + /// instead of using the data from the old font. + interjector: Interjector<'a>, + /// Custom data that should be used for writing the `maxp` table. Only needed for CFF2, + /// where we need to synthesize a V1 table after converting. + pub(crate) custom_maxp_data: Option, /// Whether the long loca format was chosen. long_loca: bool, } @@ -305,6 +395,10 @@ impl<'a> Context<'a> { Tag::GLYF => glyf::subset(self)?, Tag::LOCA => panic!("handled by glyf"), Tag::CFF => cff::subset(self)?, + #[cfg(feature = "variable-fonts")] + Tag::CFF2 => cff2::subset(self)?, + #[cfg(not(feature = "variable-fonts"))] + Tag::CFF2 => return Err(Unimplemented), Tag::HEAD => head::subset(self)?, Tag::HHEA => panic!("handled by hmtx"), Tag::HMTX => hmtx::subset(self)?, @@ -343,15 +437,13 @@ impl<'a> Face<'a> { } } -/// What kind of contents the font has. +/// Whether the font is a font collection or a single font. #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum FontKind { - /// TrueType outlines. - TrueType, - /// CFF outlines - Cff, /// A font collection. Collection, + /// A single font. + Single, } impl Readable<'_> for FontKind { @@ -359,27 +451,63 @@ impl Readable<'_> for FontKind { fn read(r: &mut Reader) -> Option { match r.read::()? { - 0x00010000 | 0x74727565 => Some(FontKind::TrueType), - 0x4F54544F => Some(FontKind::Cff), + // TrueType + 0x00010000 | 0x74727565 => Some(FontKind::Single), + // CFF + 0x4F54544F => Some(FontKind::Single), 0x74746366 => Some(FontKind::Collection), _ => None, } } } -impl Writeable for FontKind { +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +/// The flavor of outlines used by the font. +enum FontFlavor { + /// TrueType fonts using the `glyf` table. + TrueType, + /// CFF fonts using the `CFF` table. + Cff, + /// CFF2 fonts using the `CFF2` table. + Cff2, +} + +impl Writeable for FontFlavor { fn write(&self, w: &mut Writer) { w.write::(match self { - FontKind::TrueType => 0x00010000, - FontKind::Cff => 0x4F54544F, - FontKind::Collection => 0x74746366, + // Important note: This is the magic for TrueType and not CFF2. + // However, CFF2 fonts will be converted to TrueType as part of the subsetting + // process, hence we write the same magic. + FontFlavor::TrueType | FontFlavor::Cff2 => 0x00010000, + FontFlavor::Cff => 0x4F54544F, }) } } /// A 4-byte OpenType tag. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Tag(pub [u8; 4]); +pub struct Tag([u8; 4]); + +impl Tag { + /// Create a new tag. + pub fn new(tag: &[u8; 4]) -> Self { + Self(*tag) + } + + /// Try to create a new tag from a string. + /// + /// Return `None` if the string is not 4 bytes in size. + pub fn from_str(s: &str) -> Option { + let tag: [u8; 4] = s.as_bytes().try_into().ok()?; + + Some(Self(tag)) + } + + /// Return the value of the tag. + pub fn get(&self) -> &[u8; 4] { + &self.0 + } +} #[allow(unused)] impl Tag { diff --git a/src/maxp.rs b/src/maxp.rs index 76d42c1f..3cf3e35d 100644 --- a/src/maxp.rs +++ b/src/maxp.rs @@ -5,20 +5,85 @@ use super::*; pub fn subset(ctx: &mut Context) -> Result<()> { + const POST_TRUETYPE_VERSION: u32 = 0x00010000; + const POST_CFF_VERSION: u32 = 0x00005000; + let maxp = ctx.expect_table(Tag::MAXP).ok_or(MalformedFont)?; let mut r = Reader::new(maxp); - let version = r.read::().ok_or(MalformedFont)?; + // Version + let _ = r.read::().ok_or(MalformedFont)?; // number of glyphs r.read::().ok_or(MalformedFont)?; + let version = match ctx.flavor { + FontFlavor::TrueType => POST_TRUETYPE_VERSION, + FontFlavor::Cff => POST_CFF_VERSION, + // Since we convert to TrueType. + FontFlavor::Cff2 => POST_TRUETYPE_VERSION, + }; + let mut sub_maxp = Writer::new(); sub_maxp.write::(version); sub_maxp.write::(ctx.mapper.num_gids()); - if version == 0x00010000 { - sub_maxp.extend(r.tail().ok_or(MalformedFont)?); + if version == POST_TRUETYPE_VERSION { + if let Some(custom_data) = &ctx.custom_maxp_data { + sub_maxp.write::(custom_data.max_points); + sub_maxp.write::(custom_data.max_contours); + sub_maxp.write::(custom_data.max_composite_points); + sub_maxp.write::(custom_data.max_composite_contours); + sub_maxp.write::(custom_data.max_zones); + sub_maxp.write::(custom_data.max_twilight_points); + sub_maxp.write::(custom_data.max_storage); + sub_maxp.write::(custom_data.max_function_defs); + sub_maxp.write::(custom_data.max_instruction_defs); + sub_maxp.write::(custom_data.max_stack_elements); + sub_maxp.write::(custom_data.max_size_of_instructions); + sub_maxp.write::(custom_data.max_component_elements); + sub_maxp.write::(custom_data.max_component_depth); + } else { + sub_maxp.extend(r.tail().ok_or(MalformedFont)?); + } } ctx.push(Tag::MAXP, sub_maxp.finish()); Ok(()) } + +// When converting CFF2 to TTF, we need to write a version 1.0 MAXP table. +pub(crate) struct MaxpData { + pub(crate) max_points: u16, + pub(crate) max_contours: u16, + max_composite_points: u16, + max_composite_contours: u16, + max_zones: u16, + max_twilight_points: u16, + max_storage: u16, + max_function_defs: u16, + max_instruction_defs: u16, + max_stack_elements: u16, + max_size_of_instructions: u16, + max_component_elements: u16, + max_component_depth: u16, +} + +impl Default for MaxpData { + fn default() -> Self { + Self { + max_points: 0, + max_contours: 0, + max_composite_points: 0, + max_composite_contours: 0, + max_zones: 1, + max_twilight_points: 0, + max_storage: 0, + max_function_defs: 0, + max_instruction_defs: 0, + max_stack_elements: 0, + max_size_of_instructions: 0, + max_component_elements: 0, + // Could probably be 0 too since we only use simple glyphs when converting? + max_component_depth: 1, + } + } +} diff --git a/tests/data/fonttools.tests b/tests/data/fonttools.tests index 9c7ec39b..8b797370 100644 --- a/tests/data/fonttools.tests +++ b/tests/data/fonttools.tests @@ -10,4 +10,10 @@ NotoSans-Regular.ttf;567-570,2345-2350 Roboto-Regular.ttf;456,460-463 NewCMMath-Regular.otf;803-806,950-952,5600-5602 NotoSansCJKsc-Bold-subset1.otf;1 +NotoSans-Regular_var.ttf;10,40,58,201-205;wght=400 +NotoSans-Regular_var.ttf;10,40,58,201-205;wght=900 +NotoSans-Regular_var.ttf;10,40,58,201-205;wght=800,wdth=70.0 +// Note that the lsb values currently doesn't match fonttools +Cantarell-VF.otf;1,15,30-35,40,103-105;wght=400 +Cantarell-VF.otf;1,15,30-35,40,103-105;wght=800 diff --git a/tests/scripts/gen-tests.py b/tests/scripts/gen-tests.py index f6eef09c..526388c1 100755 --- a/tests/scripts/gen-tests.py +++ b/tests/scripts/gen-tests.py @@ -19,12 +19,12 @@ def main(): def gen_font_tools_tests(): - cff_fonttools_impl("fonttools.tests", Path(FONT_TOOLS_PATH), "test_font_tools") + cff_fonttools_impl("fonttools.tests", Path(FONT_TOOLS_PATH), True, "test_font_tools") def gen_cff_tests(): - cff_fonttools_impl("cff.tests", Path(CFF_PATH), "test_cff_dump") + cff_fonttools_impl("cff.tests", Path(CFF_PATH), False, "test_cff_dump") -def cff_fonttools_impl(test_src, out_path, fn_name): +def cff_fonttools_impl(test_src, out_path, include_variations, fn_name): test_string = f"// This file was auto-generated by `{Path(__file__).name}`, do not edit manually.\n\n" test_string += "#![allow(non_snake_case)]\n\n" test_string += f"use crate::*;\n\n" @@ -40,6 +40,7 @@ def cff_fonttools_impl(test_src, out_path, fn_name): font_file = parts[0] gids = parts[1] + variations = parts[2] if len(parts) > 2 else "" if font_file not in counters: counters[font_file] = 1 @@ -50,7 +51,9 @@ def cff_fonttools_impl(test_src, out_path, fn_name): function_name = f"{font_name_to_function(font_file)}_{counter}" test_string += "#[test] " - test_string += f'fn {function_name}() {{{fn_name}("{font_file}", "{gids}", {counter})}}\n' + if include_variations and variations: + test_string += '#[cfg(feature = "variable-fonts")] ' + test_string += f'fn {function_name}() {{{fn_name}("{font_file}", "{gids}", "{variations}", {counter})}}\n' if include_variations else f'fn {function_name}() {{{fn_name}("{font_file}", "{gids}", {counter})}}\n' with open(out_path, "w+") as file: file.write(test_string) @@ -79,6 +82,9 @@ def gen_subset_tests(): counter = counters[font_file] counters[font_file] += 1 + # We don't check outlines with variations because with our current subsetting logic, it's very likely + # that the actual outline description will change (though visually, they will still be the same). We rely + # on manually checking the fonttools tests to ensure that the outlines look correct. functions = ["glyph_metrics", "glyph_outlines_ttf_parser", "glyph_outlines_skrifa"] for function in functions: diff --git a/tests/src/font_tools.rs b/tests/src/font_tools.rs index 511e779c..2c6c2ab2 100644 --- a/tests/src/font_tools.rs +++ b/tests/src/font_tools.rs @@ -4,12 +4,17 @@ use crate::*; -#[test] fn clicker_script_regular_1() {test_font_tools("ClickerScript-Regular.ttf", "5,8,10,100-104", 1)} -#[test] fn deja_vu_sans_mono_1() {test_font_tools("DejaVuSansMono.ttf", "140-155,100-105", 1)} -#[test] fn latin_modern_roman_regular_1() {test_font_tools("LatinModernRoman-Regular.otf", "307,309,314,221", 1)} -#[test] fn m_p_l_u_s1p_regular_1() {test_font_tools("MPLUS1p-Regular.ttf", "3,45-50", 1)} -#[test] fn noto_sans_c_j_ksc_regular_1() {test_font_tools("NotoSansCJKsc-Regular.otf", "6543-6550,371-375", 1)} -#[test] fn noto_sans_regular_1() {test_font_tools("NotoSans-Regular.ttf", "567-570,2345-2350", 1)} -#[test] fn roboto_regular_1() {test_font_tools("Roboto-Regular.ttf", "456,460-463", 1)} -#[test] fn new_c_m_math_regular_1() {test_font_tools("NewCMMath-Regular.otf", "803-806,950-952,5600-5602", 1)} -#[test] fn noto_sans_c_j_ksc_boldsubset1_1() {test_font_tools("NotoSansCJKsc-Bold-subset1.otf", "1", 1)} +#[test] fn clicker_script_regular_1() {test_font_tools("ClickerScript-Regular.ttf", "5,8,10,100-104", "", 1)} +#[test] fn deja_vu_sans_mono_1() {test_font_tools("DejaVuSansMono.ttf", "140-155,100-105", "", 1)} +#[test] fn latin_modern_roman_regular_1() {test_font_tools("LatinModernRoman-Regular.otf", "307,309,314,221", "", 1)} +#[test] fn m_p_l_u_s1p_regular_1() {test_font_tools("MPLUS1p-Regular.ttf", "3,45-50", "", 1)} +#[test] fn noto_sans_c_j_ksc_regular_1() {test_font_tools("NotoSansCJKsc-Regular.otf", "6543-6550,371-375", "", 1)} +#[test] fn noto_sans_regular_1() {test_font_tools("NotoSans-Regular.ttf", "567-570,2345-2350", "", 1)} +#[test] fn roboto_regular_1() {test_font_tools("Roboto-Regular.ttf", "456,460-463", "", 1)} +#[test] fn new_c_m_math_regular_1() {test_font_tools("NewCMMath-Regular.otf", "803-806,950-952,5600-5602", "", 1)} +#[test] fn noto_sans_c_j_ksc_boldsubset1_1() {test_font_tools("NotoSansCJKsc-Bold-subset1.otf", "1", "", 1)} +#[test] #[cfg(feature = "variable-fonts")] fn noto_sans_regular_var_1() {test_font_tools("NotoSans-Regular_var.ttf", "10,40,58,201-205", "wght=400", 1)} +#[test] #[cfg(feature = "variable-fonts")] fn noto_sans_regular_var_2() {test_font_tools("NotoSans-Regular_var.ttf", "10,40,58,201-205", "wght=900", 2)} +#[test] #[cfg(feature = "variable-fonts")] fn noto_sans_regular_var_3() {test_font_tools("NotoSans-Regular_var.ttf", "10,40,58,201-205", "wght=800,wdth=70.0", 3)} +#[test] #[cfg(feature = "variable-fonts")] fn cantarell_v_f_1() {test_font_tools("Cantarell-VF.otf", "1,15,30-35,40,103-105", "wght=400", 1)} +#[test] #[cfg(feature = "variable-fonts")] fn cantarell_v_f_2() {test_font_tools("Cantarell-VF.otf", "1,15,30-35,40,103-105", "wght=800", 2)} diff --git a/tests/src/main.rs b/tests/src/main.rs index 2eae2e7e..21ba43d0 100644 --- a/tests/src/main.rs +++ b/tests/src/main.rs @@ -5,7 +5,7 @@ use skrifa::MetadataProvider; use std::error::Error; use std::path::{Path, PathBuf}; use std::process::Command; -use subsetter::{subset, GlyphRemapper}; +use subsetter::{subset, subset_with_variations, GlyphRemapper, Tag}; use ttf_parser::GlyphId; #[rustfmt::skip] @@ -81,7 +81,7 @@ fn test_cff_dump(font_file: &str, gids: &str, num: u16) { } } -fn test_font_tools(font_file: &str, gids: &str, num: u16) { +fn test_font_tools(font_file: &str, gids: &str, variations: &str, num: u16) { let mut ttx_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); ttx_path.push("tests/ttx"); let _ = std::fs::create_dir_all(&ttx_path); @@ -111,25 +111,40 @@ fn test_font_tools(font_file: &str, gids: &str, num: u16) { let face = ttf_parser::Face::parse(&data, 0).unwrap(); let gids_vec: Vec<_> = parse_gids(gids, face.number_of_glyphs()); let remapper = GlyphRemapper::new_from_glyphs(gids_vec.as_slice()); - let subset = subset(&data, 0, &remapper).unwrap(); + let variations = parse_variations(variations); + let subset = subset_with_variations(&data, 0, &variations, &remapper).unwrap(); std::fs::write(otf_path.clone(), subset).unwrap(); // Optionally create the subset via fonttools, so that we can compare it to our subset. if FONT_TOOLS_REF { let font_path = get_font_path(font_file); + let mut input_path = font_path.to_str().unwrap(); + let output_path = otf_ref_path.to_str().unwrap(); + + if !variations.is_empty() { + let mut args = vec!["varLib.instancer".to_string(), input_path.to_string()]; + + args.extend(variations.iter().map(|(name, value)| format!("{name}={value}"))); + args.extend(["-o".to_string(), output_path.to_string()]); + + Command::new("fonttools").args(args).output().unwrap(); + + input_path = output_path; + } + Command::new("fonttools") .args([ "subset", - font_path.to_str().unwrap(), - "--drop-tables=GSUB,GPOS,GDEF,FFTM,vhea,vmtx,DSIG,VORG,hdmx,cmap,MATH", + input_path, + "--drop-tables=GSUB,GPOS,GDEF,FFTM,vhea,vmtx,DSIG,VORG,hdmx,cmap,MATH,HVAR,MVAR,STAT,avar,fvar,gvar", &format!("--gids={}", gids), "--glyph-names", "--desubroutinize", "--notdef-outline", "--no-prune-unicode-ranges", "--no-prune-codepage-ranges", - &format!("--output-file={}", otf_ref_path.to_str().unwrap()), + &format!("--output-file={output_path}", ), ]) .output() .unwrap(); @@ -194,10 +209,10 @@ fn get_test_context(font_file: &str, gids: &str) -> Result { let data = read_file(font_file); let face = ttf_parser::Face::parse(&data, 0).unwrap(); let gids: Vec<_> = parse_gids(gids, face.number_of_glyphs()); - let glyph_remapepr = GlyphRemapper::new_from_glyphs(gids.as_slice()); - let subset = subset(&data, 0, &glyph_remapepr)?; + let glyph_remapper = GlyphRemapper::new_from_glyphs(gids.as_slice()); + let subset = subset(&data, 0, &glyph_remapper)?; - Ok(TestContext { font: data, subset, mapper: glyph_remapepr, gids }) + Ok(TestContext { font: data, subset, mapper: glyph_remapper, gids }) } fn parse_gids(gids: &str, max: u16) -> Vec { @@ -223,6 +238,23 @@ fn parse_gids(gids: &str, max: u16) -> Vec { gids } +fn parse_variations(input: &str) -> Vec<(Tag, f32)> { + input + .split(',') + .filter_map(|pair| { + let parts: Vec<&str> = pair.split('=').collect(); + if parts.len() == 2 { + Some(( + Tag::from_str(parts[0].trim()).unwrap(), + parts[1].trim().parse().ok()?, + )) + } else { + None + } + }) + .collect() +} + fn glyph_metrics(font_file: &str, gids: &str) { let ctx = get_test_context(font_file, gids).unwrap(); let old_face = ttf_parser::Face::parse(&ctx.font, 0).unwrap(); diff --git a/tests/ttx/Cantarell-VF_1.ttx b/tests/ttx/Cantarell-VF_1.ttx new file mode 100644 index 00000000..27246f36 --- /dev/null +++ b/tests/ttx/Cantarell-VF_1.ttx @@ -0,0 +1,876 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2019 The Cantarell Project Authors (https://gitlab.gnome.org/GNOME/cantarell-fonts) + + + Cantarell + + + Regular + + + 0.303;ABAT;Cantarell-Regular + + + Cantarell Regular + + + Version 0.303 + + + Cantarell-Regular + + + Dave Crossland, Nikolaus Waxweiler, Florian Fecher, Jacques Le Bailly, Eben Sorkin, Alexei Vanyashin, Alexios Zavras, Emilios Theofanous, Irene Vlachou + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFL + + + http://scripts.sil.org/OFL + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ttx/Cantarell-VF_2.ttx b/tests/ttx/Cantarell-VF_2.ttx new file mode 100644 index 00000000..9a7c1672 --- /dev/null +++ b/tests/ttx/Cantarell-VF_2.ttx @@ -0,0 +1,928 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2019 The Cantarell Project Authors (https://gitlab.gnome.org/GNOME/cantarell-fonts) + + + Cantarell + + + Regular + + + 0.303;ABAT;Cantarell-Regular + + + Cantarell Regular + + + Version 0.303 + + + Cantarell-Regular + + + Dave Crossland, Nikolaus Waxweiler, Florian Fecher, Jacques Le Bailly, Eben Sorkin, Alexei Vanyashin, Alexios Zavras, Emilios Theofanous, Irene Vlachou + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFL + + + http://scripts.sil.org/OFL + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ttx/NotoSans-Regular_var_1.ttx b/tests/ttx/NotoSans-Regular_var_1.ttx new file mode 100644 index 00000000..5df87f5f --- /dev/null +++ b/tests/ttx/NotoSans-Regular_var_1.ttx @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + + + Noto Sans + + + Regular + + + 2.013;GOOG;NotoSans-Regular + + + Noto Sans Regular + + + Version 2.013 + + + NotoSans-Regular + + + Noto is a trademark of Google LLC. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://scripts.sil.org/OFL + + + https://scripts.sil.org/OFL + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ttx/NotoSans-Regular_var_2.ttx b/tests/ttx/NotoSans-Regular_var_2.ttx new file mode 100644 index 00000000..99f2f98d --- /dev/null +++ b/tests/ttx/NotoSans-Regular_var_2.ttx @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + + + Noto Sans + + + Regular + + + 2.013;GOOG;NotoSans-Regular + + + Noto Sans Regular + + + Version 2.013 + + + NotoSans-Regular + + + Noto is a trademark of Google LLC. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://scripts.sil.org/OFL + + + https://scripts.sil.org/OFL + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ttx/NotoSans-Regular_var_3.ttx b/tests/ttx/NotoSans-Regular_var_3.ttx new file mode 100644 index 00000000..34cede76 --- /dev/null +++ b/tests/ttx/NotoSans-Regular_var_3.ttx @@ -0,0 +1,671 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + + + Noto Sans + + + Regular + + + 2.013;GOOG;NotoSans-Regular + + + Noto Sans Regular + + + Version 2.013 + + + NotoSans-Regular + + + Noto is a trademark of Google LLC. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://scripts.sil.org/OFL + + + https://scripts.sil.org/OFL + + + + + + + + + + + + + + + + + + + + + + + + + +