def _loadDesignspace(self, designspace): # Note: Only used for building variable fonts log.info("loading designspace sources") if isinstance(designspace, str): designspace = DesignSpaceDocument.fromfile(designspace) else: # copy that we can mess with designspace = DesignSpaceDocument.fromfile(designspace.path) masters = designspace.loadSourceFonts(opener=Font) # masters = [s.font for s in designspace.sources] # list of UFO font objects # Update the default source's full name to not include style name defaultFont = designspace.default.font defaultFont.info.openTypeNameCompatibleFullName = defaultFont.info.familyName log.info("Preprocessing glyphs") # find glyphs subject to decomposition and/or overlap removal # TODO: Find out why this loop is SO DAMN SLOW. It might just be so that defcon is # really slow when reading glyphs. Perhaps we can sidestep defcon and just # read & parse the .glif files ourselves. glyphNamesToDecompose = set() # glyph names glyphsToRemoveOverlaps = set() # glyph objects for ufo in masters: # Note: ufo is of type defcon.objects.font.Font # update font version updateFontVersion(ufo, dummy=False, isVF=True) componentReferences = set(ufo.componentReferences) for g in ufo: directives = findGlyphDirectives(g.note) if self._shouldDecomposeGlyph(g, directives, componentReferences): glyphNamesToDecompose.add(g.name) if 'removeoverlap' in directives: if g.components and len(g.components) > 0: glyphNamesToDecompose.add(g.name) glyphsToRemoveOverlaps.add(g) self._decompose(masters, glyphNamesToDecompose) # remove overlaps if glyphsToRemoveOverlaps: rmoverlapFilter = RemoveOverlapsFilter(backend='pathops') rmoverlapFilter.start() if log.isEnabledFor(logging.DEBUG): log.debug( 'Removing overlaps in glyphs:\n %s', "\n ".join(set([g.name for g in glyphsToRemoveOverlaps])), ) elif log.isEnabledFor(logging.INFO): log.info('Removing overlaps in %d glyphs', len(glyphsToRemoveOverlaps)) for g in glyphsToRemoveOverlaps: rmoverlapFilter.filter(g) # handle control back to fontmake return designspace
def build_masters(opts): """ Build master OTFs using supplied options. """ logger.info("Reading designspace file...") ds = DesignSpaceDocument.fromfile(opts.dsPath) validateDesignspaceDoc(ds) master_paths = [s.path for s in ds.sources] logger.info("Building local OTFs for master font paths...") curDir = os.getcwd() dsDir = os.path.dirname(opts.dsPath) for master_path in master_paths: master_path = os.path.join(dsDir, master_path) masterDir = os.path.dirname(master_path) ufoName = os.path.basename(master_path) otfName = os.path.splitext(ufoName)[0] otfName = f"{otfName}.otf" if masterDir: os.chdir(masterDir) makeotf(['-nshw', '-f', ufoName, '-o', otfName, '-r', '-nS'] + opts.mkot) logger.info(f"Built OTF font for {master_path}") generalizeCFF(otfName) os.chdir(curDir)
def test_default_featureWriters_in_designspace_lib(tmpdir, ufo_module): """Test that the glyphsLib custom featureWriters settings (with mode="append") are exported to the designspace lib whenever a GSFont contains a manual 'kern' feature. And that they are not imported back to GSFont.userData if they are the same as the default value. """ font = classes.GSFont() font.masters.append(classes.GSFontMaster()) kern = classes.GSFeature(name="kern", code="pos a b 100;") font.features.append(kern) designspace = to_designspace(font, ufo_module=ufo_module) path = str(tmpdir / "test.designspace") designspace.write(path) for source in designspace.sources: source.font.save(str(tmpdir / source.filename)) designspace2 = DesignSpaceDocument.fromfile(path) assert UFO2FT_FEATURE_WRITERS_KEY in designspace2.lib assert designspace2.lib[ UFO2FT_FEATURE_WRITERS_KEY] == DEFAULT_FEATURE_WRITERS font2 = to_glyphs(designspace2, ufo_module=ufo_module) assert not len(font2.userData) assert len([f for f in font2.features if f.name == "kern"]) == 1
def test_custom_featureWriters_in_designpace_lib(tmpdir, ufo_module): """Test that we can roundtrip custom user-defined ufo2ft featureWriters settings that are stored in the designspace lib or GSFont.userData. """ font = classes.GSFont() font.masters.append(classes.GSFontMaster()) kern = classes.GSFeature(name="kern", code="pos a b 100;") font.features.append(kern) customFeatureWriters = list(DEFAULT_FEATURE_WRITERS) + [{ "class": "MyCustomWriter", "module": "myCustomWriter" }] font.userData[UFO2FT_FEATURE_WRITERS_KEY] = customFeatureWriters designspace = to_designspace(font, ufo_module=ufo_module) path = str(tmpdir / "test.designspace") designspace.write(path) for source in designspace.sources: source.font.save(str(tmpdir / source.filename)) designspace2 = DesignSpaceDocument.fromfile(path) assert UFO2FT_FEATURE_WRITERS_KEY in designspace2.lib assert designspace2.lib[UFO2FT_FEATURE_WRITERS_KEY] == customFeatureWriters font2 = to_glyphs(designspace2, ufo_module=ufo_module) assert len(font2.userData) == 1 assert font2.userData[UFO2FT_FEATURE_WRITERS_KEY] == customFeatureWriters
def test_varlib_build_BASE(self): self.temp_dir() ds_path = self.get_test_input('TestBASE.designspace', copy=True) ttx_dir = self.get_test_input("master_base_test") expected_ttx_name = 'TestBASE' suffix = '.otf' for path in self.get_file_list(ttx_dir, '.ttx', 'TestBASE'): font, savepath = self.compile_font(path, suffix, self.tempdir) ds = DesignSpaceDocument.fromfile(ds_path) for source in ds.sources: source.path = os.path.join( self.tempdir, os.path.basename(source.filename).replace(".ufo", suffix) ) ds.updatePaths() varfont, _, _ = build(ds) varfont = reload_font(varfont) expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') tables = ["BASE"] self.expect_ttx(varfont, expected_ttx_path, tables) self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
def test_varlib_build_from_ds_object_in_memory_ttfonts(self): ds_path = self.get_test_input("Build.designspace") ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf") expected_ttx_path = self.get_test_output("BuildMain.ttx") self.temp_dir() for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'): self.compile_font(path, ".ttf", self.tempdir) ds = DesignSpaceDocument.fromfile(ds_path) for source in ds.sources: filename = os.path.join( self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf")) source.font = TTFont(filename, recalcBBoxes=False, recalcTimestamp=False, lazy=True) source.filename = None # Make sure no file path gets into build() varfont, _, _ = build(ds) varfont = reload_font(varfont) tables = [ table_tag for table_tag in varfont.keys() if table_tag != "head" ] self.expect_ttx(varfont, expected_ttx_path, tables)
def compileDSToFont(dsPath, ttFolder): doc = DesignSpaceDocument.fromfile(dsPath) doc.findDefault() ufoPathToTTPath = getTTPaths(doc, ttFolder) for source in doc.sources: if source.layerName is None: ttPath = ufoPathToTTPath[source.path] if not os.path.exists(ttPath): raise FileNotFoundError(ttPath) source.font = TTFont(ttPath, lazy=False) assert doc.default.font is not None if "name" not in doc.default.font: doc.default.font["name"] = newTable( "name") # This is the template for the VF, and needs a name table if any(s.layerName is not None for s in doc.sources): fb = FontBuilder(unitsPerEm=doc.default.font["head"].unitsPerEm) fb.setupGlyphOrder(doc.default.font.getGlyphOrder()) fb.setupPost() # This makes sure we store the glyph names font = fb.font for source in doc.sources: if source.font is None: source.font = font ttFont, masterModel, _ = varLib.build( doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT']) # Our client needs the masterModel, so we save a pickle into the font ttFont["MPcl"] = newTable("MPcl") ttFont["MPcl"].data = pickle.dumps(masterModel) return ttFont
def test_loadSourceFonts(): def opener(path): font = ttLib.TTFont() font.importXML(path) return font # this designspace file contains .TTX source paths path = os.path.join( os.path.dirname(os.path.dirname(__file__)), "varLib", "data", "SparseMasters.designspace" ) designspace = DesignSpaceDocument.fromfile(path) # force two source descriptors to have the same path designspace.sources[1].path = designspace.sources[0].path fonts = designspace.loadSourceFonts(opener) assert len(fonts) == 3 assert all(isinstance(font, ttLib.TTFont) for font in fonts) assert fonts[0] is fonts[1] # same path, identical font object fonts2 = designspace.loadSourceFonts(opener) for font1, font2 in zip(fonts, fonts2): assert font1 is font2
def __init__(self, filename=None): """Load a variable font from the given filename.""" self.masters = {} self.designspace = None self.master_order = [] if filename.endswith(".glyphs"): f = GSFont(filename) self.designspace = UFOBuilder(f).designspace self.masters = { master.name: _load_gsfont(master) for master in f.masters } self.master_order = [master.name for master in f.masters] elif filename.endswith(".designspace"): self.designspace = DesignSpaceDocument.fromfile(filename) self.designspace.loadSourceFonts(load_ufo) self.masters = { source.styleName: source.font for source in self.designspace.sources } self.master_order = [ source.styleName for source in self.designspace.sources ] if self.designspace: self._make_model()
def main(args=None): options = get_options(args) if os.path.exists(options.var_font_path): os.remove(options.var_font_path) designspace = DesignSpaceDocument.fromfile(options.design_space_path) ds_data = varLib.load_designspace(designspace) master_fonts = varLib.load_masters(designspace, otfFinder) logger.progress("Reading source fonts...") for i, master_font in enumerate(master_fonts): designspace.sources[i].font = master_font # Subset source fonts if options.include_glyphs_path: logger.progress("Subsetting source fonts...") subsetDict = getSubset(options.include_glyphs_path) subset_masters(designspace, subsetDict) if options.check_compatibility: logger.progress("Checking outline compatibility in source fonts...") font_list = [src.font for src in designspace.sources] default_font = designspace.sources[ds_data.base_idx].font vf = deepcopy(default_font) # We copy vf from default_font, because we use VF to hold # merged arguments from each source font charstring - this alters # the font, which we don't want to do to the default font. do_compatibility(vf, font_list, ds_data.base_idx) logger.progress("Building variable OTF (CFF2) font...") # Note that we now pass in the design space object, rather than a path to # the design space file, in order to pass in the modified source fonts # fonts without having to recompile and save them. try: varFont, _, _ = varLib.build(designspace, otfFinder) except VarLibCFFPointTypeMergeError: logger.error("The input set requires compatibilization. Please try " "again with the -c (--check-compat) option.") return 0 if not options.keep_glyph_names: suppress_glyph_names(varFont) if options.omit_mac_names: remove_mac_names(varFont) stat_file_path = os.path.join(os.path.dirname(options.var_font_path), STAT_FILENAME) if os.path.exists(stat_file_path): logger.progress("Importing STAT table override...") import_stat_override(varFont, stat_file_path) validate_stat_axes(varFont) validate_stat_values(varFont) update_stat_name_ids(varFont) varFont.save(options.var_font_path) logger.progress(f"Built variable font '{options.var_font_path}'")
def test_instance_getStatNames(datadir): doc = DesignSpaceDocument.fromfile(datadir / "test_v5_sourceserif.designspace") assert getStatNames(doc, doc.instances[0].getFullUserLocation(doc)) == StatNames( familyNames={"en": "Source Serif 4"}, styleNames={"en": "Caption ExtraLight"}, postScriptFontName="SourceSerif4-CaptionExtraLight", styleMapFamilyNames={"en": "Source Serif 4 Caption ExtraLight"}, styleMapStyleName="regular", )
def apply_instance_data(designspace, include_filenames=None, Font=None): """Open UFO instances referenced by designspace, apply Glyphs instance data if present, re-save UFOs and return updated UFO Font objects. Args: designspace: DesignSpaceDocument object or path (str or PathLike) to a designspace file. include_filenames: optional set of instance filenames (relative to the designspace path) to be included. By default all instaces are processed. Font: a callable(path: str) -> Font, used to load a UFO, such as defcon.Font class (default: ufoLib2.Font.open). Returns: List of opened and updated instance UFOs. """ from fontTools.designspaceLib import DesignSpaceDocument from os.path import normcase, normpath if Font is None: import ufoLib2 Font = ufoLib2.Font.open if hasattr(designspace, "__fspath__"): designspace = designspace.__fspath__() if isinstance(designspace, str): designspace = DesignSpaceDocument.fromfile(designspace) basedir = os.path.dirname(designspace.path) instance_ufos = [] if include_filenames is not None: include_filenames = {normcase(normpath(p)) for p in include_filenames} for designspace_instance in designspace.instances: fname = designspace_instance.filename assert fname is not None, "instance %r missing required filename" % getattr( designspace_instance, "name", designspace_instance ) if include_filenames is not None: fname = normcase(normpath(fname)) if fname not in include_filenames: continue logger.debug("Applying instance data to %s", fname) # fontmake <= 1.4.0 compares the ufo paths returned from this function # to the keys of a dict of designspace locations that have been passed # through normpath (but not normcase). We do the same. ufo = Font(normpath(os.path.join(basedir, fname))) apply_instance_data_to_ufo(ufo, designspace_instance, designspace) ufo.save() instance_ufos.append(ufo) return instance_ufos
def test_using_v5_features_upgrades_format(tmpdir, datadir): test_file = datadir / "test_v4_original.designspace" output_4_path = tmpdir / "test_v4.designspace" output_5_path = tmpdir / "test_v5.designspace" shutil.copy(test_file, output_4_path) doc = DesignSpaceDocument.fromfile(output_4_path) doc.write(output_4_path) assert 'format="4.1"' in output_4_path.read_text(encoding="utf-8") doc.addVariableFont(VariableFontDescriptor(name="TestVF")) doc.write(output_5_path) assert 'format="5.0"' in output_5_path.read_text(encoding="utf-8")
def test_read_v5_document_discrete(datadir): doc = DesignSpaceDocument.fromfile(datadir / "test_v5_discrete.designspace") assert not doc.locationLabels assert not doc.variableFonts assert_descriptors_equal( doc.axes, [ DiscreteAxisDescriptor( default=400, values=[400, 700, 900], name="Weight", tag="wght", axisLabels=[ AxisLabelDescriptor( name="Regular", userValue=400, elidable=True, linkedUserValue=700, ), AxisLabelDescriptor(name="Bold", userValue=700), AxisLabelDescriptor(name="Black", userValue=900), ], ), DiscreteAxisDescriptor( default=100, values=[75, 100], name="Width", tag="wdth", axisLabels=[ AxisLabelDescriptor(name="Narrow", userValue=75), AxisLabelDescriptor( name="Normal", userValue=100, elidable=True), ], ), DiscreteAxisDescriptor( default=0, values=[0, 1], name="Italic", tag="ital", axisLabels=[ AxisLabelDescriptor(name="Roman", userValue=0, elidable=True, linkedUserValue=1), AxisLabelDescriptor(name="Italic", userValue=1), ], ), ], )
def getSourcePathsFromDesignspace(): designspacePath = getFile("select designspace for variable font", allowsMultipleSelection=False, fileTypes=["designspace"])[0] designspace = DesignSpaceDocument.fromfile(designspacePath) inputFontPaths = [] for source in designspace.sources: inputFontPaths.append(source.path) return designspacePath, inputFontPaths
def copyFiles(designspacePath, outRoot): """ Copies the supplied designspace and all of it's sources to *outRoot* This updates the source paths in the the designspace file. *designspacePath* is a `string` of the path to a designspace file *outRoot* is a `string` of the root directory to copy files to """ ignore = shutil.ignore_patterns(".git", ".git*") if os.path.exists(outRoot): print("🛑 new folder path exists, stopping") raise ValueError os.mkdir(outRoot) newDesignspacePath = os.path.join(outRoot, os.path.split(designspacePath)[1]) shutil.copy(designspacePath, newDesignspacePath) ds = DesignSpaceDocument.fromfile(designspacePath) sources = [source.path for source in ds.sources] paths = {} for fontPath in sources: f = os.path.split(fontPath)[1] newPath = os.path.join(outRoot, f) paths[f] = newPath shutil.copytree(fontPath, newPath, ignore=ignore) ds = DesignSpaceDocument.fromfile(newDesignspacePath) for source in ds.sources: source.path = paths[os.path.split(source.path)[1]] ds.write(newDesignspacePath) return newDesignspacePath
def compileDSToFont(dsPath, ttFolder): doc = DesignSpaceDocument.fromfile(dsPath) doc.findDefault() ufoPathToTTPath = getTTPaths(doc, ttFolder) for source in doc.sources: if source.layerName is None: ttPath = ufoPathToTTPath[source.path] if not os.path.exists(ttPath): raise FileNotFoundError(ttPath) source.font = TTFont(ttPath, lazy=False) assert doc.default.font is not None if "name" not in doc.default.font: doc.default.font["name"] = newTable( "name") # This is the template for the VF, and needs a name table if any(s.layerName is not None for s in doc.sources): fb = FontBuilder(unitsPerEm=doc.default.font["head"].unitsPerEm) fb.setupGlyphOrder(doc.default.font.getGlyphOrder()) fb.setupPost() # This makes sure we store the glyph names font = fb.font for source in doc.sources: if source.font is None: source.font = font try: ttFont, masterModel, _ = varLib.build( doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT']) except VarLibError as e: if 'GSUB' in e.args: extraExclude = ['GSUB'] elif 'GPOS' in e.args: extraExclude = ['GPOS', 'GDEF'] else: raise print(f"{e!r}", file=sys.stderr) print( f"Error while building {extraExclude[0]} table, trying again without {' and '.join(extraExclude)}.", file=sys.stderr) ttFont, masterModel, _ = varLib.build( doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT'] + extraExclude) # Our client needs the masterModel, so we save a pickle into the font ttFont["MPcl"] = newTable("MPcl") ttFont["MPcl"].data = pickle.dumps(masterModel) return ttFont
def test_getStatNames_on_ds4_doesnt_make_up_bad_names(datadir): """See this issue on GitHub: https://github.com/googlefonts/ufo2ft/issues/630 When as in the example, there's no STAT data present, the getStatName shouldn't try making up a postscript name. """ doc = DesignSpaceDocument.fromfile(datadir / "DS5BreakTest.designspace") assert getStatNames(doc, {"Weight": 600, "Width": 125, "Italic": 1}) == StatNames( familyNames={"en": "DS5BreakTest"}, styleNames={}, postScriptFontName=None, styleMapFamilyNames={}, styleMapStyleName=None, )
def test_varlib_build_from_ttx_paths(self): ds_path = self.get_test_input("Build.designspace") ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf") expected_ttx_path = self.get_test_output("BuildMain.ttx") ds = DesignSpaceDocument.fromfile(ds_path) for source in ds.sources: source.path = os.path.join( ttx_dir, os.path.basename(source.filename).replace(".ufo", ".ttx") ) ds.updatePaths() varfont, _, _ = build(ds) varfont = reload_font(varfont) tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] self.expect_ttx(varfont, expected_ttx_path, tables)
def add_STAT(designspacePath, fontPath, stylespacePath=None): if stylespacePath is None: stylespacePath = getStyleSpacePath(designspacePath) ds = DesignSpaceDocument.fromfile(designspacePath) makeStyleSpace(ds, stylespacePath) if stylespacePath is not None: print("Adding STAT table") additional_locations = ds.lib.get("org.statmake.additionalLocations", {}) font = fontTools.ttLib.TTFont(fontPath) stylespace = Stylespace.from_file(stylespacePath) apply_stylespace_to_variable_font(stylespace, font, additional_locations) font.save(fontPath)
def copyDesignSpace(designspacePath, newFolderPath): # duplicate designspace into new folder inputDStail = os.path.split(designspacePath)[1] outputDSpath = newFolderPath + "/" + inputDStail shutil.copyfile(designspacePath, outputDSpath) # update source & instance paths in designspace as needed outputDS = DesignSpaceDocument.fromfile(outputDSpath) # updates path if sources were originally in a different directory than designspace file for source in outputDS.sources: newFontPath = newFolderPath + '/' + os.path.split(source.path)[1] source.path = newFontPath outputDS.write(outputDSpath)
def interpolate_layout(designspace, loc, master_finder=lambda s: s, mapped=False): """ Interpolate GPOS from a designspace file and location. If master_finder is set, it should be a callable that takes master filename as found in designspace file and map it to master font binary as to be opened (eg. .ttf or .otf). If mapped is False (default), then location is mapped using the map element of the axes in designspace file. If mapped is True, it is assumed that location is in designspace's internal space and no mapping is performed. """ if hasattr(designspace, "sources"): # Assume a DesignspaceDocument pass else: # Assume a file path from fontTools.designspaceLib import DesignSpaceDocument designspace = DesignSpaceDocument.fromfile(designspace) ds = load_designspace(designspace) log.info("Building interpolated font") log.info("Loading master fonts") master_fonts = load_masters(designspace, master_finder) font = deepcopy(master_fonts[ds.base_idx]) log.info("Location: %s", pformat(loc)) if not mapped: loc = {name: ds.axes[name].map_forward(v) for name, v in loc.items()} log.info("Internal location: %s", pformat(loc)) loc = models.normalizeLocation(loc, ds.internal_axis_supports) log.info("Normalized location: %s", pformat(loc)) # Assume single-model for now. model = models.VariationModel(ds.normalized_master_locs) assert 0 == model.mapping[ds.base_idx] merger = InstancerMerger(font, model, loc) log.info("Building interpolated tables") # TODO GSUB/GDEF merger.mergeTables(font, master_fonts, ['GPOS']) return font
def test_detect_ribbi_aktiv(datadir): doc = DesignSpaceDocument.fromfile(datadir / "test_v5_aktiv.designspace") assert getStatNames(doc, {"Weight": 600, "Width": 125, "Italic": 1}) == StatNames( familyNames={"en": "Aktiv Grotesk"}, styleNames={"en": "Ex SemiBold Italic"}, postScriptFontName="AktivGrotesk-ExSemiBoldItalic", styleMapFamilyNames={"en": "Aktiv Grotesk Ex SemiBold"}, styleMapStyleName="italic", ) assert getStatNames(doc, {"Weight": 700, "Width": 75, "Italic": 1}) == StatNames( familyNames={"en": "Aktiv Grotesk"}, styleNames={"en": "Cd Bold Italic"}, postScriptFontName="AktivGrotesk-CdBoldItalic", styleMapFamilyNames={"en": "Aktiv Grotesk Cd"}, styleMapStyleName="bold italic", )
def test_roundtrip(tmpdir, datadir, filename): test_file = datadir / filename output_path = tmpdir / filename # Move the file to the tmpdir so that the filenames stay the same # (they're relative to the file's path) shutil.copy(test_file, output_path) doc = DesignSpaceDocument.fromfile(output_path) doc.write(output_path) # The input XML has comments and empty lines for documentation purposes xml = test_file.read_text(encoding="utf-8") xml = re.sub( r"<!-- ROUNDTRIP_TEST_REMOVE_ME_BEGIN -->(.|\n)*?<!-- ROUNDTRIP_TEST_REMOVE_ME_END -->", "", xml, ) xml = re.sub(r"<!--(.|\n)*?-->", "", xml) xml = re.sub(r"\s*\n+", "\n", xml) assert output_path.read_text(encoding="utf-8") == xml
def build_variable( designspacePath, stylespacePath=None, out=None, verbose="ERROR", ): """ Builds a variable font from a designspace using fontmake. Post applies the STAT table using a stylespace if given. *designspacePath* a `string` of the path to the designspace *stylespacePath* a `string` of the path to the stylespace *out* a `string` of the path where the varible font should be saved *verbose* sets the verbosity level for fontmake. Defaults to "ERROR" """ if out is None: out = os.path.splitext( os.path.basename(designspacePath))[0] + "-VF.ttf" else: if not os.path.exists(os.path.split(out)[0]): os.mkdir(os.path.split(out)[0]) print("🏗 Constructing variable font") fp = FontProject(verbose=verbose) fp.build_variable_font(designspacePath, output_path=out, useProductionNames=True) if stylespacePath is not None: print("🏗 Adding STAT table") ds = DesignSpaceDocument.fromfile(designspacePath) additional_locations = ds.lib.get("org.statmake.additionalLocations", {}) font = fontTools.ttLib.TTFont(out) stylespace = Stylespace.from_file(stylespacePath) apply_stylespace_to_variable_font(stylespace, font, additional_locations) font.save(out) print("✅ Built variable font")
def test_varlib_build_lazy_masters(self): # See https://github.com/fonttools/fonttools/issues/1808 ds_path = self.get_test_input("SparseMasters.designspace") expected_ttx_path = self.get_test_output("SparseMasters.ttx") def _open_font(master_path, master_finder=lambda s: s): font = TTFont() font.importXML(master_path) buf = BytesIO() font.save(buf, reorderTables=False) buf.seek(0) font = TTFont(buf, lazy=True) # reopen in lazy mode, to reproduce #1808 return font ds = DesignSpaceDocument.fromfile(ds_path) ds.loadSourceFonts(_open_font) varfont, _, _ = build(ds) varfont = reload_font(varfont) tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] self.expect_ttx(varfont, expected_ttx_path, tables)
def test_convert5to4(datadir, tmpdir, test_ds, expected_vfs): data_in = datadir / test_ds temp_in = tmpdir / test_ds shutil.copy(data_in, temp_in) doc = DesignSpaceDocument.fromfile(temp_in) variable_fonts = convert5to4(doc) assert variable_fonts.keys() == expected_vfs for vf_name, vf in variable_fonts.items(): data_out = (datadir / "convert5to4_output" / vf_name).with_suffix(".designspace") temp_out = (Path(tmpdir) / "out" / vf_name).with_suffix(".designspace") temp_out.parent.mkdir(exist_ok=True) vf.write(temp_out) if UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING: data_out.write_text(temp_out.read_text(encoding="utf-8"), encoding="utf-8") else: assert data_out.read_text(encoding="utf-8") == temp_out.read_text( encoding="utf-8" )
def test_varlib_build_sparse_CFF2(self): ds_path = self.get_test_input('TestSparseCFF2VF.designspace') ttx_dir = self.get_test_input("master_sparse_cff2") expected_ttx_path = self.get_test_output("TestSparseCFF2VF.ttx") self.temp_dir() for path in self.get_file_list(ttx_dir, '.ttx', 'MasterSet_Kanji-'): self.compile_font(path, ".otf", self.tempdir) ds = DesignSpaceDocument.fromfile(ds_path) for source in ds.sources: source.path = os.path.join( self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf") ) ds.updatePaths() varfont, _, _ = build(ds) varfont = reload_font(varfont) tables = ["fvar", "CFF2"] self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_from_ttf_paths(self): self.temp_dir() ds_path = self.get_test_input("Build.designspace", copy=True) ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf") expected_ttx_path = self.get_test_output("BuildMain.ttx") for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'): self.compile_font(path, ".ttf", self.tempdir) ds = DesignSpaceDocument.fromfile(ds_path) for source in ds.sources: source.path = os.path.join( self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf") ) ds.updatePaths() varfont, _, _ = build(ds) varfont = reload_font(varfont) tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_vpal(self): ds_path = self.get_test_input('test_vpal.designspace') ttx_dir = self.get_test_input("master_vpal_test") expected_ttx_path = self.get_test_output("test_vpal.ttx") self.temp_dir() for path in self.get_file_list(ttx_dir, '.ttx', 'master_vpal_test_'): self.compile_font(path, ".otf", self.tempdir) ds = DesignSpaceDocument.fromfile(ds_path) for source in ds.sources: source.path = os.path.join( self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf")) ds.updatePaths() varfont, _, _ = build(ds) varfont = reload_font(varfont) tables = ["GPOS"] self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_sparse_CFF2(self): ds_path = self.get_test_input('TestSparseCFF2VF.designspace') ttx_dir = self.get_test_input("master_sparse_cff2") expected_ttx_path = self.get_test_output("TestSparseCFF2VF.ttx") self.temp_dir() for path in self.get_file_list(ttx_dir, '.ttx', 'MasterSet_Kanji-'): self.compile_font(path, ".otf", self.tempdir) ds = DesignSpaceDocument.fromfile(ds_path) for source in ds.sources: source.path = os.path.join( self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf")) ds.updatePaths() varfont, _, _ = build(ds) varfont = reload_font(varfont) tables = ["fvar", "CFF2"] self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_from_ds_object_in_memory_ttfonts(self): ds_path = self.get_test_input("Build.designspace") ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf") expected_ttx_path = self.get_test_output("BuildMain.ttx") self.temp_dir() for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'): self.compile_font(path, ".ttf", self.tempdir) ds = DesignSpaceDocument.fromfile(ds_path) for source in ds.sources: filename = os.path.join( self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf") ) source.font = TTFont( filename, recalcBBoxes=False, recalcTimestamp=False, lazy=True ) source.filename = None # Make sure no file path gets into build() varfont, _, _ = build(ds) varfont = reload_font(varfont) tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"] self.expect_ttx(varfont, expected_ttx_path, tables)
def test_varlib_build_VVAR_CFF2(self): ds_path = self.get_test_input('TestVVAR.designspace') ttx_dir = self.get_test_input("master_vvar_cff2") expected_ttx_name = 'TestVVAR' suffix = '.otf' self.temp_dir() for path in self.get_file_list(ttx_dir, '.ttx', 'TestVVAR'): font, savepath = self.compile_font(path, suffix, self.tempdir) ds = DesignSpaceDocument.fromfile(ds_path) for source in ds.sources: source.path = os.path.join( self.tempdir, os.path.basename(source.filename).replace(".ufo", suffix) ) ds.updatePaths() varfont, _, _ = build(ds) varfont = reload_font(varfont) expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') tables = ["VVAR"] self.expect_ttx(varfont, expected_ttx_path, tables) self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
def test_varlib_build_sparse_masters_MVAR(self): import fontTools.varLib.mvar ds_path = self.get_test_input("SparseMasters.designspace") ds = DesignSpaceDocument.fromfile(ds_path) load_masters(ds) # Trigger MVAR generation so varLib is forced to create deltas with a # sparse master inbetween. font_0_os2 = ds.sources[0].font["OS/2"] font_0_os2.sTypoAscender = 1 font_0_os2.sTypoDescender = 1 font_0_os2.sTypoLineGap = 1 font_0_os2.usWinAscent = 1 font_0_os2.usWinDescent = 1 font_0_os2.sxHeight = 1 font_0_os2.sCapHeight = 1 font_0_os2.ySubscriptXSize = 1 font_0_os2.ySubscriptYSize = 1 font_0_os2.ySubscriptXOffset = 1 font_0_os2.ySubscriptYOffset = 1 font_0_os2.ySuperscriptXSize = 1 font_0_os2.ySuperscriptYSize = 1 font_0_os2.ySuperscriptXOffset = 1 font_0_os2.ySuperscriptYOffset = 1 font_0_os2.yStrikeoutSize = 1 font_0_os2.yStrikeoutPosition = 1 font_0_vhea = newTable("vhea") font_0_vhea.ascent = 1 font_0_vhea.descent = 1 font_0_vhea.lineGap = 1 font_0_vhea.caretSlopeRise = 1 font_0_vhea.caretSlopeRun = 1 font_0_vhea.caretOffset = 1 ds.sources[0].font["vhea"] = font_0_vhea font_0_hhea = ds.sources[0].font["hhea"] font_0_hhea.caretSlopeRise = 1 font_0_hhea.caretSlopeRun = 1 font_0_hhea.caretOffset = 1 font_0_post = ds.sources[0].font["post"] font_0_post.underlineThickness = 1 font_0_post.underlinePosition = 1 font_2_os2 = ds.sources[2].font["OS/2"] font_2_os2.sTypoAscender = 800 font_2_os2.sTypoDescender = 800 font_2_os2.sTypoLineGap = 800 font_2_os2.usWinAscent = 800 font_2_os2.usWinDescent = 800 font_2_os2.sxHeight = 800 font_2_os2.sCapHeight = 800 font_2_os2.ySubscriptXSize = 800 font_2_os2.ySubscriptYSize = 800 font_2_os2.ySubscriptXOffset = 800 font_2_os2.ySubscriptYOffset = 800 font_2_os2.ySuperscriptXSize = 800 font_2_os2.ySuperscriptYSize = 800 font_2_os2.ySuperscriptXOffset = 800 font_2_os2.ySuperscriptYOffset = 800 font_2_os2.yStrikeoutSize = 800 font_2_os2.yStrikeoutPosition = 800 font_2_vhea = newTable("vhea") font_2_vhea.ascent = 800 font_2_vhea.descent = 800 font_2_vhea.lineGap = 800 font_2_vhea.caretSlopeRise = 800 font_2_vhea.caretSlopeRun = 800 font_2_vhea.caretOffset = 800 ds.sources[2].font["vhea"] = font_2_vhea font_2_hhea = ds.sources[2].font["hhea"] font_2_hhea.caretSlopeRise = 800 font_2_hhea.caretSlopeRun = 800 font_2_hhea.caretOffset = 800 font_2_post = ds.sources[2].font["post"] font_2_post.underlineThickness = 800 font_2_post.underlinePosition = 800 varfont, _, _ = build(ds) mvar_tags = [vr.ValueTag for vr in varfont["MVAR"].table.ValueRecord] assert all(tag in mvar_tags for tag in fontTools.varLib.mvar.MVAR_ENTRIES)
def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True): """ Build variation font from a designspace file. If master_finder is set, it should be a callable that takes master filename as found in designspace file and map it to master font binary as to be opened (eg. .ttf or .otf). """ if hasattr(designspace, "sources"): # Assume a DesignspaceDocument pass else: # Assume a file path designspace = DesignSpaceDocument.fromfile(designspace) ds = load_designspace(designspace) log.info("Building variable font") log.info("Loading master fonts") master_fonts = load_masters(designspace, master_finder) # TODO: 'master_ttfs' is unused except for return value, remove later master_ttfs = [] for master in master_fonts: try: master_ttfs.append(master.reader.file.name) except AttributeError: master_ttfs.append(None) # in-memory fonts have no path # Copy the base master to work from it vf = deepcopy(master_fonts[ds.base_idx]) # TODO append masters as named-instances as well; needs .designspace change. fvar = _add_fvar(vf, ds.axes, ds.instances) if 'STAT' not in exclude: _add_stat(vf, ds.axes) if 'avar' not in exclude: _add_avar(vf, ds.axes) # Map from axis names to axis tags... normalized_master_locs = [ {ds.axes[k].tag: v for k,v in loc.items()} for loc in ds.normalized_master_locs ] # From here on, we use fvar axes only axisTags = [axis.axisTag for axis in fvar.axes] # Assume single-model for now. model = models.VariationModel(normalized_master_locs, axisOrder=axisTags) assert 0 == model.mapping[ds.base_idx] log.info("Building variations tables") if 'MVAR' not in exclude: _add_MVAR(vf, model, master_fonts, axisTags) if 'HVAR' not in exclude: _add_HVAR(vf, model, master_fonts, axisTags) if 'GDEF' not in exclude or 'GPOS' not in exclude: _merge_OTL(vf, model, master_fonts, axisTags) if 'gvar' not in exclude and 'glyf' in vf: _add_gvar(vf, model, master_fonts, optimize=optimize) if 'cvar' not in exclude and 'glyf' in vf: _merge_TTHinting(vf, model, master_fonts) if 'GSUB' not in exclude and ds.rules: _add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules) if 'CFF2' not in exclude and 'CFF ' in vf: _add_CFF2(vf, model, master_fonts) for tag in exclude: if tag in vf: del vf[tag] # TODO: Only return vf for 4.0+, the rest is unused. return vf, model, master_ttfs
def load_designspace(designspace_filename): ds = DesignSpaceDocument.fromfile(designspace_filename) masters = ds.sources if not masters: raise VarLibError("no sources found in .designspace") instances = ds.instances standard_axis_map = OrderedDict([ ('weight', ('wght', {'en':'Weight'})), ('width', ('wdth', {'en':'Width'})), ('slant', ('slnt', {'en':'Slant'})), ('optical', ('opsz', {'en':'Optical Size'})), ]) # Setup axes axes = OrderedDict() for axis in ds.axes: axis_name = axis.name if not axis_name: assert axis.tag is not None axis_name = axis.name = axis.tag if axis_name in standard_axis_map: if axis.tag is None: axis.tag = standard_axis_map[axis_name][0] if not axis.labelNames: axis.labelNames.update(standard_axis_map[axis_name][1]) else: assert axis.tag is not None if not axis.labelNames: axis.labelNames["en"] = axis_name axes[axis_name] = axis log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()])) # Check all master and instance locations are valid and fill in defaults for obj in masters+instances: obj_name = obj.name or obj.styleName or '' loc = obj.location for axis_name in loc.keys(): assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name) for axis_name,axis in axes.items(): if axis_name not in loc: loc[axis_name] = axis.default else: v = axis.map_backward(loc[axis_name]) assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum) # Normalize master locations internal_master_locs = [o.location for o in masters] log.info("Internal master locations:\n%s", pformat(internal_master_locs)) # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar internal_axis_supports = {} for axis in axes.values(): triple = (axis.minimum, axis.default, axis.maximum) internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple] log.info("Internal axis supports:\n%s", pformat(internal_axis_supports)) normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs] log.info("Normalized master locations:\n%s", pformat(normalized_master_locs)) # Find base master base_idx = None for i,m in enumerate(normalized_master_locs): if all(v == 0 for v in m.values()): assert base_idx is None base_idx = i assert base_idx is not None, "Base master not found; no master at default location?" log.info("Index of base master: %s", base_idx) return _DesignSpaceData( axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances, ds.rules, )