diff --git a/Lib/gftools/builder/recipeproviders/googlefonts.py b/Lib/gftools/builder/recipeproviders/googlefonts.py index 70069fc5..772f2420 100644 --- a/Lib/gftools/builder/recipeproviders/googlefonts.py +++ b/Lib/gftools/builder/recipeproviders/googlefonts.py @@ -3,10 +3,13 @@ import os import re from tempfile import NamedTemporaryFile +from typing import Optional, Tuple +from glyphsLib.builder import UFOBuilder import yaml from strictyaml import load, YAMLValidationError +from gftools.builder.file import File from gftools.builder.recipeproviders import RecipeProviderBase from gftools.builder.schema import ( GOOGLEFONTS_SCHEMA, @@ -17,6 +20,8 @@ logger = logging.getLogger("GFBuilder") +Italic = Optional[Tuple[str, float, float]] # tag, regular value, italic value + # Things not ported from old builder: # - cleanup # - stylespace (old, replaced by stat) @@ -50,6 +55,7 @@ "useMutatorMath": False, "checkCompatibility": True, "overlaps": "booleanOperations", + "splitItalic": True, } @@ -88,9 +94,7 @@ def write_recipe(self): for font in list(self.config["stat"].keys()): scfont = re.sub(r"((?:-Italic)?\[)", r"SC\1", font) self.config["stat"][scfont] = self.config["stat"][font] - yaml.dump(self.config["stat"], self.statfile) - self.statfile.close() else: self.statfile = None # Find variable fonts @@ -99,7 +103,37 @@ def write_recipe(self): self.build_all_statics() return self.recipe - def _vf_filename(self, source, suffix="", extension="ttf"): + def _has_slant_ital(self, source: File) -> Italic: + if source.is_glyphs: + tags = [ax.axisTag for ax in source.gsfont.axes] + elif source.is_designspace: + tags = [ax.tag for ax in source.designspace.axes] + else: + return + if "ital" in tags: + slanty_axis = "ital" + elif "slnt" in tags: + slanty_axis = "slnt" + else: + return + if source.is_glyphs: + gsfont = source.gsfont + builder = UFOBuilder(gsfont, minimal=True) + builder.to_designspace_axes() + axes = builder.designspace.axes + else: + axes = source.designspace.axes + wanted = [axis for axis in axes if axis.tag == slanty_axis] + if slanty_axis == "ital": + return (slanty_axis, wanted[0].minimum, wanted[0].maximum) + else: + # We expect the italic value to have negative slant, so it + # turns out as the minimum. + return (slanty_axis, wanted[0].maximum, wanted[0].minimum) + + def _vf_filename( + self, source, suffix="", extension="ttf", italic_ds=None, roman=False + ): """Determine the file name for a variable font.""" sourcebase = os.path.splitext(source.basename)[0] if source.is_glyphs: @@ -109,6 +143,10 @@ def _vf_filename(self, source, suffix="", extension="ttf"): else: raise ValueError("Unknown source type") + if italic_ds: + if not roman: + sourcebase += "-Italic" + tags.remove(italic_ds[0]) axis_tags = ",".join(sorted(tags)) directory = self.config["vfDir"] if extension == "woff2": @@ -192,7 +230,16 @@ def build_all_variables(self): or (source.is_designspace and len(source.designspace.sources) < 2) ): continue - self.build_a_variable(source) + italic_ds = None + if self.config["splitItalic"]: + italic_ds = self._has_slant_ital(source) + if italic_ds: + self.build_a_variable(source, italic_ds=italic_ds, roman=True) + self.build_a_variable(source, italic_ds=italic_ds, roman=False) + if self.statfile: + self._italicize_stat_file(source, italic_ds) + else: + self.build_a_variable(source) self.build_STAT() def build_STAT(self): @@ -204,6 +251,7 @@ def build_STAT(self): if len(all_variables) > 0: last_target = all_variables[-1] if self.statfile: + self.statfile.close() args = {"args": "--src " + self.statfile.name} else: args = {} @@ -216,7 +264,7 @@ def build_STAT(self): build_stat_step["needs"] = other_variables self.recipe[last_target].append(build_stat_step) - def _vtt_steps(self, target): + def _vtt_steps(self, target: str): if os.path.basename(target) in self.config.get("vttSources", {}): return [ { @@ -226,19 +274,33 @@ def _vtt_steps(self, target): ] return [] - def build_a_variable(self, source): - target = self._vf_filename(source) - steps = ( - [ - {"source": source.path}, - { - "operation": "buildVariable", - "args": self.fontmake_args(source, variable=True), - }, - ] - + self._vtt_steps(target) - + self._fix_step() - ) + def build_a_variable( + self, source: File, italic_ds: Italic = None, roman: bool = False + ): + if roman: + target = self._vf_filename(source, italic_ds=italic_ds, roman=True) + else: + target = self._vf_filename(source, italic_ds=italic_ds, roman=False) + steps = [ + {"source": source.path}, + { + "operation": "buildVariable", + "args": self.fontmake_args(source, variable=True), + }, + ] + self._vtt_steps(target) + if italic_ds: + desired_slice = italic_ds[0] + "=" + if roman: + desired_slice += str(italic_ds[1]) + steps += [{"operation": "subspace", "axes": desired_slice}] + else: + desired_slice += str(italic_ds[2]) + steps += [ + {"operation": "subspace", "axes": desired_slice}, + ] + self._italic_fixup() + + steps += self._fix_step() + self.recipe[target] = steps self.build_a_webfont(target, self._vf_filename(source, extension="woff2")) if self._do_smallcap(source): @@ -333,3 +395,63 @@ def _smallcap_steps(self, source, original): }, {"operation": "fix", "args": self.fix_args()}, ] + + def _italicize_stat_file(self, source: File, italic_ds: Italic): + # In this situation we have a stat file, and we have a font with + # either an ital or a slnt axis that we have split into two subspaced + # VFs. We now need to rewrite the stat file to remove the slnt axis, + # and potentially to copy the STAT table to the newly created file. + + # What kind of STAT file are we? A global one or a font-specific one? + if isinstance(self.config["stat"], dict): + old_font = self._vf_filename(source) + new_font = self._vf_filename(source, italic_ds=italic_ds, roman=False) + if old_font in self.config["stat"] and new_font not in self.config["stat"]: + raise ValueError( + f"We are splitting the font on the {italic_ds[0]} axis, " + "but the stat: entry in the config file does not contain " + f"an entry for {new_font}. Please add one and try again." + ) + # Presume the user has done the right thing + return + # This is easy, just drop slnt + self.config["stat"] = [ + axis for axis in self.config["stat"] if axis["tag"] != "slnt" + ] + # Rewrite the stat file + self.statfile.seek(0) + self.statfile.truncate(0) + yaml.dump(self.config["stat"], self.statfile) + + def _italic_fixup(self): + # We have a font created by subspacing the ital or slnt axis, but its + # name table is not italic yet (and we can't use --update-name-table + # because we don't have a STAT table eyt). So we need to make this font + # "italic enough" to convince gftools-fix-font to apply all its italic + # font fixes (post.italicAngle etc.) when we call it with + # --include-source-fixes. + configfile = NamedTemporaryFile(delete=False, mode="w+") + family_name = self.sources[0].family_name.replace(" ", "") + # Since this is mad YAML, we can't use the normal YAML library + # to write this. We'll just write it out manually. + configfile.write( + f""" +OS/2->fsSelection: 129 +head->macStyle: "|= 0x02" +name->setName: ["{family_name}Italic", 25, 3, 1, 0x409] +name->setName: ["Italic", 2, 3, 1, 0x409] +name->setName: ["Italic", 17, 3, 1, 0x409] + """ + ) + configfile.close() + return [ + { + "operation": "exec", + "exe": "gftools-fontsetter", + "args": "-o $out $in " + configfile.name, + }, + { + "operation": "fix", + "args": "--include-source-fixes", + }, + ] diff --git a/Lib/gftools/builder/schema.py b/Lib/gftools/builder/schema.py index 8a9740db..64f2b0e1 100644 --- a/Lib/gftools/builder/schema.py +++ b/Lib/gftools/builder/schema.py @@ -108,5 +108,6 @@ Optional("extraVariableFontmakeArgs"): Str(), Optional("extraStaticFontmakeArgs"): Str(), Optional("buildSmallCap"): Bool(), + Optional("splitItalic"): Bool(), } ) diff --git a/docs/gftools-builder/README.md b/docs/gftools-builder/README.md index 5416b97b..ec22228d 100644 --- a/docs/gftools-builder/README.md +++ b/docs/gftools-builder/README.md @@ -200,6 +200,10 @@ The build can be customized by adding the following keys to the YAML file: - `buildSmallCap`: Automatically build smallcap families from source with a `smcp` feature. Defaults to true. +- `splitItalic`: For variable fonts containing a `slnt` or `ital` axis, + subspace them into separate roman and italic VF files to comply with + Google Fonts' specification. Defaults to true. + ## *Really* customizing the build process If the options above aren't enough for you - let's say you want to run your