def main(args): from fontTools import configLogger args = args[1:] # TODO: allow user to configure logging via command-line options configLogger(level="INFO") if len(args) < 1: print("usage: fonttools varLib.models source.designspace", file=sys.stderr) print(" or") print("usage: fonttools varLib.models location1 location2 ...", file=sys.stderr) sys.exit(1) from pprint import pprint if len(args) == 1 and args[0].endswith('.designspace'): from fontTools.designspaceLib import DesignSpaceDocument doc = DesignSpaceDocument() doc.read(args[0]) locs = [s.location for s in doc.sources] print("Original locations:") pprint(locs) doc.normalize() print("Normalized locations:") pprint(locs) else: axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args] model = VariationModel(locs) print("Sorted locations:") pprint(model.locations) print("Supports:") pprint(model.supports)
def test_with_with_path_object(tmpdir): import pathlib tmpdir = str(tmpdir) dest = pathlib.Path(tmpdir) / "test.designspace" doc = DesignSpaceDocument() doc.write(dest) assert dest.exists()
def main(args=None): from fontTools import configLogger if args is None: args = sys.argv[1:] # configure the library logger (for >= WARNING) configLogger() # comment this out to enable debug messages from logger # log.setLevel(logging.DEBUG) if len(args) < 1: print("usage: fonttools varLib.plot source.designspace", file=sys.stderr) print(" or") print("usage: fonttools varLib.plot location1 location2 ...", file=sys.stderr) sys.exit(1) fig = pyplot.figure() if len(args) == 1 and args[0].endswith('.designspace'): doc = DesignSpaceDocument() doc.read(args[0]) plotDocument(doc, fig) else: axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args] plotLocationsSurfaces(locs, fig) pyplot.show()
def test_load_masters_layerName_without_required_font(): ds = DesignSpaceDocument() s = SourceDescriptor() s.font = None s.layerName = "Medium" ds.addSource(s) with pytest.raises( AttributeError, match="specified a layer name but lacks the required TTFont object", ): load_masters(ds)
def test_findDefault_axis_mapping(): designspace_string = """\ <?xml version='1.0' encoding='UTF-8'?> <designspace format="4.0"> <axes> <axis tag="wght" name="Weight" minimum="100" maximum="800" default="400"> <map input="100" output="20"/> <map input="300" output="40"/> <map input="400" output="80"/> <map input="700" output="126"/> <map input="800" output="170"/> </axis> <axis tag="ital" name="Italic" minimum="0" maximum="1" default="1"/> </axes> <sources> <source filename="Font-Light.ufo"> <location> <dimension name="Weight" xvalue="20"/> <dimension name="Italic" xvalue="0"/> </location> </source> <source filename="Font-Regular.ufo"> <location> <dimension name="Weight" xvalue="80"/> <dimension name="Italic" xvalue="0"/> </location> </source> <source filename="Font-Bold.ufo"> <location> <dimension name="Weight" xvalue="170"/> <dimension name="Italic" xvalue="0"/> </location> </source> <source filename="Font-LightItalic.ufo"> <location> <dimension name="Weight" xvalue="20"/> <dimension name="Italic" xvalue="1"/> </location> </source> <source filename="Font-Italic.ufo"> <location> <dimension name="Weight" xvalue="80"/> <dimension name="Italic" xvalue="1"/> </location> </source> <source filename="Font-BoldItalic.ufo"> <location> <dimension name="Weight" xvalue="170"/> <dimension name="Italic" xvalue="1"/> </location> </source> </sources> </designspace> """ designspace = DesignSpaceDocument.fromstring(designspace_string) assert designspace.findDefault().filename == "Font-Italic.ufo" designspace.axes[1].default = 0 assert designspace.findDefault().filename == "Font-Regular.ufo"
def test_normalise4(): # normalisation with a map doc = DesignSpaceDocument() # write some axes a4 = AxisDescriptor() a4.minimum = 0 a4.maximum = 1000 a4.default = 0 a4.name = "ddd" a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] doc.addAxis(a4) doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.map)) r.sort() assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])]
def test_axisMapping(): # note: because designspance lib does not do any actual # processing of the mapping data, we can only check if there data is there. doc = DesignSpaceDocument() # write some axes a4 = AxisDescriptor() a4.minimum = 0 a4.maximum = 1000 a4.default = 0 a4.name = "ddd" a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] doc.addAxis(a4) doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.map)) r.sort() assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])]
def test_documentLib(tmpdir): # roundtrip test of the document lib with some nested data tmpdir = str(tmpdir) testDocPath1 = os.path.join(tmpdir, "testDocumentLibTest.designspace") doc = DesignSpaceDocument() a1 = AxisDescriptor() a1.tag = "TAGA" a1.name = "axisName_a" a1.minimum = 0 a1.maximum = 1000 a1.default = 0 doc.addAxis(a1) dummyData = dict(a=123, b=u"äbc", c=[1,2,3], d={'a':123}) dummyKey = "org.fontTools.designspaceLib" doc.lib = {dummyKey: dummyData} doc.write(testDocPath1) new = DesignSpaceDocument() new.read(testDocPath1) assert dummyKey in new.lib assert new.lib[dummyKey] == dummyData
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 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_CFF2(self): ds_path = self.get_test_input('TestCFF2.designspace') ttx_dir = self.get_test_input("master_cff2") expected_ttx_path = self.get_test_output("BuildTestCFF2.ttx") self.temp_dir() for path in self.get_file_list(ttx_dir, '.ttx', 'TestCFF2_'): 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 getVFUserRegion(doc: DesignSpaceDocument, vf: VariableFontDescriptor) -> Region: vfUserRegion: Region = {} # For each axis, 2 cases: # - it has a range = it's an axis in the VF DS # - it's a single location = use it to know which rules should apply in the VF for axisSubset in vf.axisSubsets: axis = doc.getAxis(axisSubset.name) if axis is None: raise DesignSpaceDocumentError( f"Cannot find axis named '{axisSubset.name}' for variable font '{vf.name}'." ) if hasattr(axisSubset, "userMinimum"): # Mypy doesn't support narrowing union types via hasattr() # TODO(Python 3.10): use TypeGuard # https://mypy.readthedocs.io/en/stable/type_narrowing.html axisSubset = cast(RangeAxisSubsetDescriptor, axisSubset) if not hasattr(axis, "minimum"): raise DesignSpaceDocumentError( f"Cannot select a range over '{axis.name}' for variable font '{vf.name}' " "because it's a discrete axis, use only 'userValue' instead." ) axis = cast(AxisDescriptor, axis) vfUserRegion[axis.name] = Range( max(axisSubset.userMinimum, axis.minimum), min(axisSubset.userMaximum, axis.maximum), axisSubset.userDefault or axis.default, ) else: axisSubset = cast(ValueAxisSubsetDescriptor, axisSubset) vfUserRegion[axis.name] = axisSubset.userValue # Any axis not mentioned explicitly has a single location = default value for axis in doc.axes: if axis.name not in vfUserRegion: assert isinstance( axis.default, (int, float)), f"Axis '{axis.name}' has no valid default value." vfUserRegion[axis.name] = axis.default return vfUserRegion
def buildFiles(sources=True, static=True, variable=True, ds="recursive-MONO_CASL_wght_slnt_ital--full_gsub.designspace", version="0.000"): print("🚚 Building files for mastering") paths = getFolders(ds) if sources: print("\n🚚 Generating sources") if os.path.exists(paths["root"]): shutil.rmtree(paths["root"]) os.mkdir(paths["root"]) os.mkdir(paths["static"]) os.mkdir(paths["var"]) makeSources(ds, paths["src"], version) ds = DesignSpaceDocument.fromfile(paths["designspace"]) if static: print("\n🚚 Making files for static font mastering") name_map = buildNameMap() buildFolders(ds, paths["cff"], name_map) buildFontMenuDB(ds, paths["cff"], name_map) buildGlyphOrderAndAlias(ds.sources[0].path, paths["cff"]) buildFamilyFeatures(paths["cff"], os.path.join(paths["src"], 'features.fea'), version) buildInstances(paths["designspace"], paths["cff"], name_map) if variable: print("\n🚚 Making files for varible font mastering") makeSTAT(paths["stylespace"], ds) return paths
def test_varlib_build_vpal(self): self.temp_dir() ds_path = self.get_test_input('test_vpal.designspace', copy=True) ttx_dir = self.get_test_input("master_vpal_test") expected_ttx_path = self.get_test_output("test_vpal.ttx") 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 __init__(self, name, path): """Answers a PreVarFamily instance from the defined design space path.""" self.designSpace = ds = DesignSpaceDocument() ds.read(path) self.axes = {} for axis in self.designSpace.axes: self.axes[axis.tag] = axis fonts = {} for source in ds.sources: fonts[source.path] = Font(source.path) Family.__init__(self, name=name, fonts=fonts) self._parametricAxisFonts = {} # Key is parametric axis name self._parametricAxisMetrics = { } # Collection of font metrics and calculated parameters. self._metrics = None # Initialized on property call self._defaultFont = None # Initialized on property call self._glyphNames = None # Set of all unique glyph names in all design space fonts self.baseGlyphName = self.BASE_GLYPH_NAME
def test_normalise3(): # normalisation of negative values, with default == maximum doc = DesignSpaceDocument() # write some axes a3 = AxisDescriptor() a3.minimum = -1000 a3.maximum = 0 a3.default = 0 a3.name = "ccc" doc.addAxis(a3) assert doc.normalizeLocation(dict(ccc=0)) == {'ccc': 0.0} assert doc.normalizeLocation(dict(ccc=1)) == {'ccc': 0.0} assert doc.normalizeLocation(dict(ccc=-1000)) == {'ccc': -1.0} assert doc.normalizeLocation(dict(ccc=-1001)) == {'ccc': -1.0} doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.minimum, axis.default, axis.maximum)) r.sort() assert r == [('ccc', -1.0, 0.0, 0.0)]
def _set_path(self, path): self._path = path self._ds = None self.axes = {} self.axisList = [] self.masters = {} self.masterList = [] self.instances = {} self.instanceList = [] if path is not None: self._ds = ds = DesignSpaceDocument.fromfile( path) # Raw eTree from file. for a in ds['axes']: # Maintain order by index axis = Axis(tag=a['tag'], name=a['name'], minimum=a['minimum'], default=a['default'], maximum=a['maximum']) self.appendAxes(axis) for m in ds['sources']: masterPath = self.getFontPath(m['filename']) master = FontInfo(name=m['name'], familyName=m['familyname'], styleName=m['stylename'], path=masterPath, location=self.asTagLocation(m['location'])) self.appendMasters(master) for i in ds.get('instances', []): instancePath = self.getFontPath(i['filename']) instance = FontInfo(name=i['name'], familyName=i['familyname'], styleName=i['stylename'], path=masterPath, location=self.asTagLocation(i['location'])) self.appendInstances(instance)
def test_varlib_build_from_ttf_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") 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: 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_split(datadir, tmpdir, test_ds, expected_interpolable_spaces): data_in = datadir / test_ds temp_in = Path(tmpdir) / test_ds shutil.copy(data_in, temp_in) doc = DesignSpaceDocument.fromfile(temp_in) for i, (location, sub_doc) in enumerate(splitInterpolable(doc)): expected_location, expected_vf_names = expected_interpolable_spaces[i] assert location == expected_location vfs = list(splitVariableFonts(sub_doc)) assert expected_vf_names == set(vf[0] for vf in vfs) loc_str = "_".join(f"{name}_{value}"for name, value in sorted(location.items())) data_out = datadir / "split_output" / f"{temp_in.stem}_{loc_str}.designspace" temp_out = Path(tmpdir) / "out" / f"{temp_in.stem}_{loc_str}.designspace" temp_out.parent.mkdir(exist_ok=True) sub_doc.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" ) for vf_name, vf_doc in vfs: data_out = (datadir / "split_output" / vf_name).with_suffix(".designspace") temp_out = (Path(tmpdir) / "out" / vf_name).with_suffix(".designspace") temp_out.parent.mkdir(exist_ok=True) vf_doc.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_normalise4(): # normalisation with a map doc = DesignSpaceDocument() # write some axes a4 = AxisDescriptor() a4.minimum = 0 a4.maximum = 1000 a4.default = 0 a4.name = "ddd" a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] doc.addAxis(a4) doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.map)) r.sort() assert r == [('ddd', [(0, 0.0), (300, 0.5), (600, 0.5), (1000, 1.0)])]
def buildDesignSpace(sources, instances, axes): doc = DesignSpaceDocument() # build source descriptors from source list for source in sources: s = SourceDescriptor() s.path = source["path"] s.name = source["name"] s.copyInfo = source["copyInfo"] s.location = source["location"] s.familyName = source["familyName"] s.styleName = source["styleName"] doc.addSource(s) # build instance descriptors from instance list for instance in instances: i = InstanceDescriptor() i.location = instance["location"] i.familyName = instance["familyName"] i.styleName = instance["styleName"] i.path = instance["path"] i.postScriptFontName = instance["postScriptFontName"] i.styleMapFamilyName = instance["styleMapFamilyName"] i.styleMapStyleName = instance["styleMapStyleName"] doc.addInstance(i) # build axis descriptors from axis list for axis in axes: a = AxisDescriptor() a.minimum = axis["minimum"] a.maximum = axis["maximum"] a.default = axis["default"] a.name = axis["name"] a.tag = axis["tag"] for languageCode, labelName in axis["labelNames"].items(): a.labelNames[languageCode] = labelName a.map = axis["map"] doc.addAxis(a) return doc
def test_getStatLocations(datadir): doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace") assert getStatLocations(doc, { "Italic": 0, "Width": Range(50, 150), "Weight": Range(200, 900) }) == [ { "flags": 0, "location": { "ital": 0.0, "wdth": 50.0, "wght": 300.0 }, "name": { "en": "Some Style", "fr": "Un Style" }, }, ] assert getStatLocations(doc, { "Italic": 1, "Width": Range(50, 150), "Weight": Range(200, 900) }) == [ { "flags": 0, "location": { "ital": 1.0, "wdth": 100.0, "wght": 700.0 }, "name": { "en": "Other" }, }, ]
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_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_not_all_ordering_specified_and_translations(datadir): doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace") assert getStatNames(doc, {"Weight": 200, "Width": 125, "Italic": 1}) == StatNames( familyNames={ "en": "MasterFamilyName", "fr": "Montserrat", "ja": "モンセラート", }, styleNames={ "fr": "Wide Extra léger Italic", "de": "Wide Extraleicht Italic", "en": "Wide Extra Light Italic", }, postScriptFontName="MasterFamilyName-WideExtraLightItalic", styleMapFamilyNames={ "en": "MasterFamilyName Wide Extra Light", "fr": "Montserrat Wide Extra léger", "de": "MasterFamilyName Wide Extraleicht", "ja": "モンセラート Wide Extra Light", }, styleMapStyleName="italic", )
def test_axisMapping(): # note: because designspance lib does not do any actual # processing of the mapping data, we can only check if there data is there. doc = DesignSpaceDocument() # write some axes a4 = AxisDescriptor() a4.minimum = 0 a4.maximum = 1000 a4.default = 0 a4.name = "ddd" a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] doc.addAxis(a4) doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.map)) r.sort() assert r == [('ddd', [(0, 0.0), (300, 0.5), (600, 0.5), (1000, 1.0)])]
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_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 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...") dsDir = os.path.dirname(opts.dsPath) for master_path in master_paths: master_path = os.path.join(dsDir, master_path) otf_path = f"{os.path.splitext(master_path)[0]}.otf" result = makeotf(['-nshw', '-f', master_path, '-o', otf_path, '-r', '-nS'] + opts.mkot) if result: raise Exception(f'makeotf return value: {result}') logger.info(f"Built OTF font for {master_path}") generalizeCFF(otf_path)
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 main(): parser = argparse.ArgumentParser(description="Build Reem Kufi fonts.") parser.add_argument("file", metavar="FILE", help="input font to process") parser.add_argument("--out-file", metavar="FILE", help="output font to write", required=True) args = parser.parse_args() with TemporaryDirectory() as tempdir: ufos, designspace = build_masters(args.file, tempdir, tempdir) doc = DesignSpaceDocument() doc.read(designspace) doc.instances = [i for i in doc.instances if i.styleName == "Regular"] assert len(doc.instances) == 1 instance = doc.instances[0] instance.location = dict(Weight=108) instance.path = args.out_file doc.write(designspace) build(designspace, outputUFOFormatVersion=3)
def main(args=None): """Normalize locations on a given designspace""" from fontTools import configLogger import argparse parser = argparse.ArgumentParser( "fonttools varLib.models", description=main.__doc__, ) parser.add_argument('--loglevel', metavar='LEVEL', default="INFO", help="Logging level (defaults to INFO)") group = parser.add_mutually_exclusive_group(required=True) group.add_argument('-d', '--designspace',metavar="DESIGNSPACE",type=str) group.add_argument('-l', '--locations', metavar='LOCATION', nargs='+', help="Master locations as comma-separate coordinates. One must be all zeros.") args = parser.parse_args(args) configLogger(level=args.loglevel) from pprint import pprint if args.designspace: from fontTools.designspaceLib import DesignSpaceDocument doc = DesignSpaceDocument() doc.read(args.designspace) locs = [s.location for s in doc.sources] print("Original locations:") pprint(locs) doc.normalize() print("Normalized locations:") locs = [s.location for s in doc.sources] pprint(locs) else: axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args.locations] model = VariationModel(locs) print("Sorted locations:") pprint(model.locations) print("Supports:") pprint(model.supports)
def main(args): from fontTools import configLogger args = args[1:] # TODO: allow user to configure logging via command-line options configLogger(level="INFO") if len(args) < 1: print("usage: fonttools varLib.models source.designspace", file=sys.stderr) print(" or") print("usage: fonttools varLib.models location1 location2 ...", file=sys.stderr) sys.exit(1) from pprint import pprint if len(args) == 1 and args[0].endswith('.designspace'): from fontTools.designspaceLib import DesignSpaceDocument doc = DesignSpaceDocument() doc.read(args[0]) locs = [s.location for s in doc.sources] print("Original locations:") pprint(locs) doc.normalize() print("Normalized locations:") locs = [s.location for s in doc.sources] pprint(locs) else: axes = [chr(c) for c in range(ord('A'), ord('Z') + 1)] locs = [ dict(zip(axes, (float(v) for v in s.split(',')))) for s in args ] model = VariationModel(locs) print("Sorted locations:") pprint(model.locations) print("Supports:") pprint(model.supports)
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, )
def test_fill_document(tmpdir): tmpdir = str(tmpdir) testDocPath = os.path.join(tmpdir, "test.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") doc = DesignSpaceDocument() # write some axes a1 = AxisDescriptor() a1.minimum = 0 a1.maximum = 1000 a1.default = 0 a1.name = "weight" a1.tag = "wght" # note: just to test the element language, not an actual label name recommendations. a1.labelNames[u'fa-IR'] = u"قطر" a1.labelNames[u'en'] = u"Wéíght" doc.addAxis(a1) a2 = AxisDescriptor() a2.minimum = 0 a2.maximum = 1000 a2.default = 20 a2.name = "width" a2.tag = "wdth" a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] a2.hidden = True a2.labelNames[u'fr'] = u"Chasse" doc.addAxis(a2) # add master 1 s1 = SourceDescriptor() s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) assert s1.font is None s1.name = "master.ufo1" s1.copyLib = True s1.copyInfo = True s1.copyFeatures = True s1.location = dict(weight=0) s1.familyName = "MasterFamilyName" s1.styleName = "MasterStyleNameOne" s1.mutedGlyphNames.append("A") s1.mutedGlyphNames.append("Z") doc.addSource(s1) # add master 2 s2 = SourceDescriptor() s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s2.name = "master.ufo2" s2.copyLib = False s2.copyInfo = False s2.copyFeatures = False s2.muteKerning = True s2.location = dict(weight=1000) s2.familyName = "MasterFamilyName" s2.styleName = "MasterStyleNameTwo" doc.addSource(s2) # add master 3 from a different layer s3 = SourceDescriptor() s3.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s3.name = "master.ufo2" s3.copyLib = False s3.copyInfo = False s3.copyFeatures = False s3.muteKerning = False s3.layerName = "supports" s3.location = dict(weight=1000) s3.familyName = "MasterFamilyName" s3.styleName = "Supports" doc.addSource(s3) # add instance 1 i1 = InstanceDescriptor() i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) i1.familyName = "InstanceFamilyName" i1.styleName = "InstanceStyleName" i1.name = "instance.ufo1" i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined. i1.postScriptFontName = "InstancePostscriptName" i1.styleMapFamilyName = "InstanceStyleMapFamilyName" i1.styleMapStyleName = "InstanceStyleMapStyleName" glyphData = dict(name="arrow", mute=True, unicodes=[0x123, 0x124, 0x125]) i1.glyphs['arrow'] = glyphData i1.lib['com.coolDesignspaceApp.binaryData'] = plistlib.Data(b'<binary gunk>') i1.lib['com.coolDesignspaceApp.specimenText'] = "Hamburgerwhatever" doc.addInstance(i1) # add instance 2 i2 = InstanceDescriptor() i2.filename = os.path.relpath(instancePath2, os.path.dirname(testDocPath)) i2.familyName = "InstanceFamilyName" i2.styleName = "InstanceStyleName" i2.name = "instance.ufo2" # anisotropic location i2.location = dict(weight=500, width=(400,300)) i2.postScriptFontName = "InstancePostscriptName" i2.styleMapFamilyName = "InstanceStyleMapFamilyName" i2.styleMapStyleName = "InstanceStyleMapStyleName" glyphMasters = [dict(font="master.ufo1", glyphName="BB", location=dict(width=20,weight=20)), dict(font="master.ufo2", glyphName="CC", location=dict(width=900,weight=900))] glyphData = dict(name="arrow", unicodes=[101, 201, 301]) glyphData['masters'] = glyphMasters glyphData['note'] = "A note about this glyph" glyphData['instanceLocation'] = dict(width=100, weight=120) i2.glyphs['arrow'] = glyphData i2.glyphs['arrow2'] = dict(mute=False) doc.addInstance(i2) doc.filename = "suggestedFileName.designspace" doc.lib['com.coolDesignspaceApp.previewSize'] = 30 # write some rules r1 = RuleDescriptor() r1.name = "named.rule.1" r1.conditionSets.append([ dict(name='axisName_a', minimum=0, maximum=1), dict(name='axisName_b', minimum=2, maximum=3) ]) r1.subs.append(("a", "a.alt")) doc.addRule(r1) # write the document doc.write(testDocPath) assert os.path.exists(testDocPath) assert_equals_test_file(testDocPath, 'data/test.designspace') # import it again new = DesignSpaceDocument() new.read(testDocPath) assert new.default.location == {'width': 20.0, 'weight': 0.0} assert new.filename == 'test.designspace' assert new.lib == doc.lib assert new.instances[0].lib == doc.instances[0].lib # test roundtrip for the axis attributes and data axes = {} for axis in doc.axes: if axis.tag not in axes: axes[axis.tag] = [] axes[axis.tag].append(axis.serialize()) for axis in new.axes: if axis.tag[0] == "_": continue if axis.tag not in axes: axes[axis.tag] = [] axes[axis.tag].append(axis.serialize()) for v in axes.values(): a, b = v assert a == b
def main(argv): designspace_file = argv[1] designspace = DesignSpaceDocument.fromfile(designspace_file) designspace = fix_opsz_maximum(designspace) designspace = update_sources(designspace) designspace.write(designspace_file)
def test_read_with_path_object(): import pathlib source = (pathlib.Path(__file__) / "../data/test.designspace").resolve() assert source.exists() doc = DesignSpaceDocument() doc.read(source)
from fontTools.designspaceLib import DesignSpaceDocument from vanilla import * import inspect import defcon f = CurrentFont() thisGlyph = CurrentGlyph() doc = DesignSpaceDocument() doc.read("Crispy[SRIF,wdth,wght].designspace") referenceFileName = doc.sources[ 0].familyName #this assumed a well-made designspace file with correctly named font masters. coordinateslist = [] fontsList = doc.loadSourceFonts(defcon.Font) for i in range(len(fontsList)): nameTag = str(fontsList[i].info.styleName) for glyph in fontsList[i]: compatibilityCounter = 0 if glyph.name == thisGlyph.name: pointCount = 0 for contour in glyph: for segment in contour.segments: for points in segment: pointCount += 1 for refFonts in fontslist: compatible = glyph.isCompatible(refFont[glyph.name]) if compatible: compatibilityCounter += 1 print(compatibilityCounter)
def test_rulesDocument(tmpdir): # tests of rules in a document, roundtripping. tmpdir = str(tmpdir) testDocPath = os.path.join(tmpdir, "testRules.designspace") testDocPath2 = os.path.join(tmpdir, "testRules_roundtrip.designspace") doc = DesignSpaceDocument() a1 = AxisDescriptor() a1.minimum = 0 a1.maximum = 1000 a1.default = 0 a1.name = "axisName_a" a1.tag = "TAGA" b1 = AxisDescriptor() b1.minimum = 2000 b1.maximum = 3000 b1.default = 2000 b1.name = "axisName_b" b1.tag = "TAGB" doc.addAxis(a1) doc.addAxis(b1) r1 = RuleDescriptor() r1.name = "named.rule.1" r1.conditionSets.append([ dict(name='axisName_a', minimum=0, maximum=1000), dict(name='axisName_b', minimum=0, maximum=3000) ]) r1.subs.append(("a", "a.alt")) # rule with minium and maximum doc.addRule(r1) assert len(doc.rules) == 1 assert len(doc.rules[0].conditionSets) == 1 assert len(doc.rules[0].conditionSets[0]) == 2 assert _axesAsDict(doc.axes) == {'axisName_a': {'map': [], 'name': 'axisName_a', 'default': 0, 'minimum': 0, 'maximum': 1000, 'tag': 'TAGA'}, 'axisName_b': {'map': [], 'name': 'axisName_b', 'default': 2000, 'minimum': 2000, 'maximum': 3000, 'tag': 'TAGB'}} assert doc.rules[0].conditionSets == [[ {'minimum': 0, 'maximum': 1000, 'name': 'axisName_a'}, {'minimum': 0, 'maximum': 3000, 'name': 'axisName_b'}]] assert doc.rules[0].subs == [('a', 'a.alt')] doc.normalize() assert doc.rules[0].name == 'named.rule.1' assert doc.rules[0].conditionSets == [[ {'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_a'}, {'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_b'}]] # still one conditionset assert len(doc.rules[0].conditionSets) == 1 doc.write(testDocPath) # add a stray conditionset _addUnwrappedCondition(testDocPath) doc2 = DesignSpaceDocument() doc2.read(testDocPath) assert len(doc2.axes) == 2 assert len(doc2.rules) == 1 assert len(doc2.rules[0].conditionSets) == 2 doc2.write(testDocPath2) # verify these results # make sure the stray condition is now neatly wrapped in a conditionset. doc3 = DesignSpaceDocument() doc3.read(testDocPath2) assert len(doc3.rules) == 1 assert len(doc3.rules[0].conditionSets) == 2
def test_localisedNames(tmpdir): tmpdir = str(tmpdir) testDocPath = os.path.join(tmpdir, "testLocalisedNames.designspace") testDocPath2 = os.path.join(tmpdir, "testLocalisedNames_roundtrip.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") doc = DesignSpaceDocument() # add master 1 s1 = SourceDescriptor() s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) s1.name = "master.ufo1" s1.copyInfo = True s1.location = dict(weight=0) doc.addSource(s1) # add master 2 s2 = SourceDescriptor() s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s2.name = "master.ufo2" s2.location = dict(weight=1000) doc.addSource(s2) # add instance 1 i1 = InstanceDescriptor() i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) i1.familyName = "Montserrat" i1.styleName = "SemiBold" i1.styleMapFamilyName = "Montserrat SemiBold" i1.styleMapStyleName = "Regular" i1.setFamilyName("Montserrat", "fr") i1.setFamilyName(u"モンセラート", "ja") i1.setStyleName("Demigras", "fr") i1.setStyleName(u"半ば", "ja") i1.setStyleMapStyleName(u"Standard", "de") i1.setStyleMapFamilyName("Montserrat Halbfett", "de") i1.setStyleMapFamilyName(u"モンセラート SemiBold", "ja") i1.name = "instance.ufo1" i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined. i1.postScriptFontName = "InstancePostscriptName" glyphData = dict(name="arrow", mute=True, unicodes=[0x123]) i1.glyphs['arrow'] = glyphData doc.addInstance(i1) # now we have sources and instances, but no axes yet. doc.axes = [] # clear the axes # write some axes a1 = AxisDescriptor() a1.minimum = 0 a1.maximum = 1000 a1.default = 0 a1.name = "weight" a1.tag = "wght" # note: just to test the element language, not an actual label name recommendations. a1.labelNames[u'fa-IR'] = u"قطر" a1.labelNames[u'en'] = u"Wéíght" doc.addAxis(a1) a2 = AxisDescriptor() a2.minimum = 0 a2.maximum = 1000 a2.default = 0 a2.name = "width" a2.tag = "wdth" a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] a2.labelNames[u'fr'] = u"Poids" doc.addAxis(a2) # add an axis that is not part of any location to see if that works a3 = AxisDescriptor() a3.minimum = 333 a3.maximum = 666 a3.default = 444 a3.name = "spooky" a3.tag = "spok" a3.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] #doc.addAxis(a3) # uncomment this line to test the effects of default axes values # write some rules r1 = RuleDescriptor() r1.name = "named.rule.1" r1.conditionSets.append([ dict(name='weight', minimum=200, maximum=500), dict(name='width', minimum=0, maximum=150) ]) r1.subs.append(("a", "a.alt")) doc.addRule(r1) # write the document doc.write(testDocPath) assert os.path.exists(testDocPath) # import it again new = DesignSpaceDocument() new.read(testDocPath) new.write(testDocPath2) with open(testDocPath, 'r', encoding='utf-8') as f1: t1 = f1.read() with open(testDocPath2, 'r', encoding='utf-8') as f2: t2 = f2.read() assert t1 == t2
def test_handleNoAxes(tmpdir): tmpdir = str(tmpdir) # test what happens if the designspacedocument has no axes element. testDocPath = os.path.join(tmpdir, "testNoAxes_source.designspace") testDocPath2 = os.path.join(tmpdir, "testNoAxes_recontructed.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") # Case 1: No axes element in the document, but there are sources and instances doc = DesignSpaceDocument() for name, value in [('One', 1),('Two', 2),('Three', 3)]: a = AxisDescriptor() a.minimum = 0 a.maximum = 1000 a.default = 0 a.name = "axisName%s" % (name) a.tag = "ax_%d" % (value) doc.addAxis(a) # add master 1 s1 = SourceDescriptor() s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) s1.name = "master.ufo1" s1.copyLib = True s1.copyInfo = True s1.copyFeatures = True s1.location = dict(axisNameOne=-1000, axisNameTwo=0, axisNameThree=1000) s1.familyName = "MasterFamilyName" s1.styleName = "MasterStyleNameOne" doc.addSource(s1) # add master 2 s2 = SourceDescriptor() s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s2.name = "master.ufo1" s2.copyLib = False s2.copyInfo = False s2.copyFeatures = False s2.location = dict(axisNameOne=1000, axisNameTwo=1000, axisNameThree=0) s2.familyName = "MasterFamilyName" s2.styleName = "MasterStyleNameTwo" doc.addSource(s2) # add instance 1 i1 = InstanceDescriptor() i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) i1.familyName = "InstanceFamilyName" i1.styleName = "InstanceStyleName" i1.name = "instance.ufo1" i1.location = dict(axisNameOne=(-1000,500), axisNameTwo=100) i1.postScriptFontName = "InstancePostscriptName" i1.styleMapFamilyName = "InstanceStyleMapFamilyName" i1.styleMapStyleName = "InstanceStyleMapStyleName" doc.addInstance(i1) doc.write(testDocPath) verify = DesignSpaceDocument() verify.read(testDocPath) verify.write(testDocPath2)
def test_normalise2(): # normalisation with minimum > 0 doc = DesignSpaceDocument() # write some axes a2 = AxisDescriptor() a2.minimum = 100 a2.maximum = 1000 a2.default = 100 a2.name = "axisName_b" doc.addAxis(a2) assert doc.normalizeLocation(dict(axisName_b=0)) == {'axisName_b': 0.0} assert doc.normalizeLocation(dict(axisName_b=1000)) == {'axisName_b': 1.0} # clipping beyond max values: assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0} assert doc.normalizeLocation(dict(axisName_b=500)) == {'axisName_b': 0.4444444444444444} assert doc.normalizeLocation(dict(axisName_b=-1000)) == {'axisName_b': 0.0} assert doc.normalizeLocation(dict(axisName_b=-1001)) == {'axisName_b': 0.0} # anisotropic coordinates normalise to isotropic assert doc.normalizeLocation(dict(axisName_b=(1000,-1000))) == {'axisName_b': 1.0} assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0} doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.minimum, axis.default, axis.maximum)) r.sort() assert r == [('axisName_b', 0.0, 0.0, 1.0)]
def test_normalise1(): # normalisation of anisotropic locations, clipping doc = DesignSpaceDocument() # write some axes a1 = AxisDescriptor() a1.minimum = -1000 a1.maximum = 1000 a1.default = 0 a1.name = "axisName_a" a1.tag = "TAGA" doc.addAxis(a1) assert doc.normalizeLocation(dict(axisName_a=0)) == {'axisName_a': 0.0} assert doc.normalizeLocation(dict(axisName_a=1000)) == {'axisName_a': 1.0} # clipping beyond max values: assert doc.normalizeLocation(dict(axisName_a=1001)) == {'axisName_a': 1.0} assert doc.normalizeLocation(dict(axisName_a=500)) == {'axisName_a': 0.5} assert doc.normalizeLocation(dict(axisName_a=-1000)) == {'axisName_a': -1.0} assert doc.normalizeLocation(dict(axisName_a=-1001)) == {'axisName_a': -1.0} # anisotropic coordinates normalise to isotropic assert doc.normalizeLocation(dict(axisName_a=(1000, -1000))) == {'axisName_a': 1.0} doc.normalize() r = [] for axis in doc.axes: r.append((axis.name, axis.minimum, axis.default, axis.maximum)) r.sort() assert r == [('axisName_a', -1.0, 0.0, 1.0)]
def from_designspace( cls, designspace: designspaceLib.DesignSpaceDocument, round_geometry: bool = True, ): """Instantiates a new data class from a Designspace object.""" if designspace.default is None: raise ValueError( "Can't generate UFOs from this designspace: no default font." ) glyph_names: Set[str] = set() for source in designspace.sources: if source.font is None: if not Path(source.path).exists(): raise ValueError(f"Source at path '{source.path}' not found.") source.font = ufoLib2.Font.open(source.path, lazy=False) glyph_names.update(source.font.keys()) # Construct Variators axis_bounds: Dict[str, Tuple[float, float, float]] = {} axis_by_name: Dict[str, designspaceLib.AxisDescriptor] = {} weight_width_axes = {} for axis in designspace.axes: axis_by_name[axis.name] = axis axis_bounds[axis.name] = (axis.minimum, axis.default, axis.maximum) if axis.tag in ("wght", "wdth"): weight_width_axes[axis.tag] = axis masters_info = collect_info_masters(designspace) info_mutator = Variator.from_masters(masters_info, axis_by_name, axis_bounds) masters_kerning = collect_kerning_masters(designspace) kerning_mutator = Variator.from_masters( masters_kerning, axis_by_name, axis_bounds ) glyph_mutators: Dict[str, Variator] = {} for glyph_name in glyph_names: items = collect_glyph_masters(designspace, glyph_name) mutator = Variator.from_masters(items, axis_by_name, axis_bounds) glyph_mutators[glyph_name] = mutator # Construct defaults to copy over default_source = designspace.findDefault() copy_feature_text: str = next( (s.font.features.text for s in designspace.sources if s.copyFeatures), default_source.font.features.text, ) copy_groups: Mapping[str, List[str]] = next( (s.font.groups for s in designspace.sources if s.copyGroups), default_source.font.groups, ) copy_info: ufoLib2.objects.Info = next( (s.font.info for s in designspace.sources if s.copyInfo), default_source.font.info, ) copy_lib: Mapping[str, Any] = next( (s.font.lib for s in designspace.sources if s.copyLib), default_source.font.lib, ) # The list of glyphs not to export and decompose where used as a component is # supposed to be taken from the Designspace when a Designspace is used as the # starting point of the compilation process. It should be exported to all # instance libs, where the ufo2ft compilation functions will pick it up. skip_export_glyphs = designspace.lib.get("public.skipExportGlyphs", []) return cls( copy_feature_text, copy_groups, copy_info, copy_lib, designspace.rules, glyph_mutators, info_mutator, kerning_mutator, round_geometry, skip_export_glyphs, weight_width_axes, )
def test_updatePaths(tmpdir): doc = DesignSpaceDocument() doc.path = str(tmpdir / "foo" / "bar" / "MyDesignspace.designspace") s1 = SourceDescriptor() doc.addSource(s1) doc.updatePaths() # expect no changes assert s1.path is None assert s1.filename is None name1 = "../masters/Source1.ufo" path1 = posix(str(tmpdir / "foo" / "masters" / "Source1.ufo")) s1.path = path1 s1.filename = None doc.updatePaths() assert s1.path == path1 assert s1.filename == name1 # empty filename updated name2 = "../masters/Source2.ufo" s1.filename = name2 doc.updatePaths() # conflicting filename discarded, path always gets precedence assert s1.path == path1 assert s1.filename == "../masters/Source1.ufo" s1.path = None s1.filename = name2 doc.updatePaths() # expect no changes assert s1.path is None assert s1.filename == name2
def test_loadSourceFonts_no_required_path(): designspace = DesignSpaceDocument() designspace.sources.append(SourceDescriptor()) with pytest.raises(DesignSpaceDocumentError, match="no 'path' attribute"): designspace.loadSourceFonts(lambda p: p)
def from_designspace( cls, designspace: designspaceLib.DesignSpaceDocument, round_geometry: bool = True, ): """Instantiates a new data class from a Designspace object.""" if designspace.default is None: raise ValueError( "Can't generate UFOs from this designspace: no default font." ) designspace.loadSourceFonts(ufoLib2.Font.open) glyph_names: Set[str] = set() for source in designspace.sources: glyph_names.update(source.font.keys()) # Construct Variators axis_bounds: Dict[str, Tuple[float, float, float]] = {} # Design space! axis_order: List[str] = [] weight_width_axes = {} for axis in designspace.axes: axis_order.append(axis.name) axis_bounds[axis.name] = ( axis.map_forward(axis.minimum), axis.map_forward(axis.default), axis.map_forward(axis.maximum), ) if axis.tag in ("wght", "wdth"): weight_width_axes[axis.tag] = axis masters_info = collect_info_masters(designspace, axis_bounds) info_mutator = Variator.from_masters(masters_info, axis_order) masters_kerning = collect_kerning_masters(designspace, axis_bounds) kerning_mutator = Variator.from_masters(masters_kerning, axis_order) default_font = designspace.findDefault().font glyph_mutators: Dict[str, Variator] = {} glyph_name_to_unicodes: Dict[str, List[int]] = {} for glyph_name in glyph_names: items = collect_glyph_masters(designspace, glyph_name, axis_bounds) glyph_mutators[glyph_name] = Variator.from_masters(items, axis_order) glyph_name_to_unicodes[glyph_name] = default_font[glyph_name].unicodes # Construct defaults to copy over copy_feature_text: str = default_font.features.text copy_groups: Mapping[str, List[str]] = default_font.groups copy_info: ufoLib2.objects.Info = default_font.info copy_lib: Mapping[str, Any] = default_font.lib # The list of glyphs not to export and decompose where used as a component is # supposed to be taken from the Designspace when a Designspace is used as the # starting point of the compilation process. It should be exported to all # instance libs, where the ufo2ft compilation functions will pick it up. skip_export_glyphs = designspace.lib.get("public.skipExportGlyphs", []) return cls( axis_bounds, copy_feature_text, copy_groups, copy_info, copy_lib, designspace.rules, glyph_mutators, glyph_name_to_unicodes, info_mutator, kerning_mutator, round_geometry, skip_export_glyphs, weight_width_axes, )
async def load(self, outputWriter): if self.doc is None: self.doc = DesignSpaceDocument.fromfile(self.fontPath) self.doc.findDefault() with tempfile.TemporaryDirectory( prefix="fontgoggles_temp") as ttFolder: sourcePathToTTPath = getTTPaths(self.doc, ttFolder) ufosToCompile = [] ttPaths = [] outputs = [] coros = [] self._sourceFiles = defaultdict(list) self._includedFeatureFiles = defaultdict(list) previousUFOs = self._ufos self._ufos = {} previousSourceData = self._sourceFontData self._sourceFontData = {} for source in self.doc.sources: sourceKey = (source.path, source.layerName) self._sourceFiles[pathlib.Path(source.path)].append(sourceKey) ufoState = previousUFOs.get(sourceKey) if ufoState is None: reader = UFOReader(source.path, validate=False) glyphSet = reader.getGlyphSet(layerName=source.layerName) glyphSet.glyphClass = Glyph if source.layerName is None: includedFeatureFiles = extractIncludedFeatureFiles( source.path, reader) getUnicodesAndAnchors = functools.partial( self._getUnicodesAndAnchors, source.path) else: includedFeatureFiles = [] # We're not compiling features nor do we need cmaps for these sparse layers, # so we don't need need proper anchor or unicode data def getUnicodesAndAnchors(): return ({}, {}) ufoState = UFOState( reader, glyphSet, getUnicodesAndAnchors=getUnicodesAndAnchors, includedFeatureFiles=includedFeatureFiles) for includedFeaFile in ufoState.includedFeatureFiles: self._includedFeatureFiles[includedFeaFile].append( sourceKey) self._ufos[sourceKey] = ufoState if source.layerName is not None: continue if source.path in ufosToCompile: continue ttPath = sourcePathToTTPath[source.path] if source.path in previousSourceData: with open(ttPath, "wb") as f: f.write(previousSourceData[source.path]) self._sourceFontData[source.path] = previousSourceData[ source.path] else: ufosToCompile.append(source.path) ttPaths.append(ttPath) output = io.StringIO() outputs.append(output) coros.append( compileUFOToPath(source.path, ttPath, output.write)) # print(f"compiling {len(coros)} fonts") errors = await asyncio.gather(*coros, return_exceptions=True) for sourcePath, exc, output in zip(ufosToCompile, errors, outputs): output = output.getvalue() if output or exc is not None: outputWriter(f"compile output for {sourcePath}:\n") if output: outputWriter(output) if exc is not None: outputWriter(f"{exc!r}\n") if any(errors): raise DesignSpaceSourceError( f"Could not build '{os.path.basename(self.fontPath)}': " "some sources did not successfully compile") for sourcePath, ttPath in zip(ufosToCompile, ttPaths): # Store compiled tt data so we can reuse it to rebuild ourselves # without recompiling the source. with open(ttPath, "rb") as f: self._sourceFontData[sourcePath] = f.read() if not ufosToCompile and not self._needsVFRebuild: # self.ttFont and self.shaper are still up-to-date return vfFontData = await compileDSToBytes(self.fontPath, ttFolder, outputWriter) f = io.BytesIO(vfFontData) self.ttFont = TTFont(f, lazy=True) # Nice cookie for us from the worker self.masterModel = pickle.loads(self.ttFont["MPcl"].data) assert len(self.masterModel.deltaWeights) == len(self.doc.sources) self.shaper = HBShape(vfFontData, getHorizontalAdvance=self._getHorizontalAdvance, getVerticalAdvance=self._getVerticalAdvance, getVerticalOrigin=self._getVerticalOrigin, ttFont=self.ttFont) self._needsVFRebuild = False
def test_pathNameResolve(tmpdir): tmpdir = str(tmpdir) # test how descriptor.path and descriptor.filename are resolved testDocPath1 = os.path.join(tmpdir, "testPathName_case1.designspace") testDocPath2 = os.path.join(tmpdir, "testPathName_case2.designspace") testDocPath3 = os.path.join(tmpdir, "testPathName_case3.designspace") testDocPath4 = os.path.join(tmpdir, "testPathName_case4.designspace") testDocPath5 = os.path.join(tmpdir, "testPathName_case5.designspace") testDocPath6 = os.path.join(tmpdir, "testPathName_case6.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") a1 = AxisDescriptor() a1.tag = "TAGA" a1.name = "axisName_a" a1.minimum = 0 a1.maximum = 1000 a1.default = 0 # Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file. doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = None s.path = None s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath1) verify = DesignSpaceDocument() verify.read(testDocPath1) assert verify.sources[0].filename == None assert verify.sources[0].path == None # Case 2: filename is empty, path points somewhere: calculate a new filename. doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = None s.path = masterPath1 s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath2) verify = DesignSpaceDocument() verify.read(testDocPath2) assert verify.sources[0].filename == "masters/masterTest1.ufo" assert verify.sources[0].path == posix(masterPath1) # Case 3: the filename is set, the path is None. doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = "../somewhere/over/the/rainbow.ufo" s.path = None s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath3) verify = DesignSpaceDocument() verify.read(testDocPath3) assert verify.sources[0].filename == "../somewhere/over/the/rainbow.ufo" # make the absolute path for filename so we can see if it matches the path p = os.path.abspath(os.path.join(os.path.dirname(testDocPath3), verify.sources[0].filename)) assert verify.sources[0].path == posix(p) # Case 4: the filename points to one file, the path points to another. The path takes precedence. doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = "../somewhere/over/the/rainbow.ufo" s.path = masterPath1 s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath4) verify = DesignSpaceDocument() verify.read(testDocPath4) assert verify.sources[0].filename == "masters/masterTest1.ufo" # Case 5: the filename is None, path has a value, update the filename doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = None s.path = masterPath1 s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.addSource(s) doc.write(testDocPath5) # so that the document has a path doc.updateFilenameFromPath() assert doc.sources[0].filename == "masters/masterTest1.ufo" # Case 6: the filename has a value, path has a value, update the filenames with force doc = DesignSpaceDocument() doc.addAxis(a1) s = SourceDescriptor() s.filename = "../somewhere/over/the/rainbow.ufo" s.path = masterPath1 s.copyInfo = True s.location = dict(weight=0) s.familyName = "MasterFamilyName" s.styleName = "MasterStyleNameOne" doc.write(testDocPath5) # so that the document has a path doc.addSource(s) assert doc.sources[0].filename == "../somewhere/over/the/rainbow.ufo" doc.updateFilenameFromPath(force=True) assert doc.sources[0].filename == "masters/masterTest1.ufo"
def test_designspace_source_locations(tmpdir, ufo_module): """Check that opening UFOs from their source descriptor works with both the filename and the path attributes. """ designspace_path = os.path.join(str(tmpdir), "test.designspace") light_ufo_path = os.path.join(str(tmpdir), "light.ufo") bold_ufo_path = os.path.join(str(tmpdir), "bold.ufo") designspace = DesignSpaceDocument() wght = AxisDescriptor() wght.minimum = 100 wght.maximum = 700 wght.default = 100 wght.name = "Weight" wght.tag = "wght" designspace.addAxis(wght) light_source = designspace.newSourceDescriptor() light_source.filename = "light.ufo" light_source.location = {"Weight": 100} designspace.addSource(light_source) bold_source = designspace.newSourceDescriptor() bold_source.path = bold_ufo_path bold_source.location = {"Weight": 700} designspace.addSource(bold_source) designspace.write(designspace_path) light = ufo_module.Font() light.info.ascender = 30 light.save(light_ufo_path) bold = ufo_module.Font() bold.info.ascender = 40 bold.save(bold_ufo_path) designspace = DesignSpaceDocument() designspace.read(designspace_path) font = to_glyphs(designspace, ufo_module=ufo_module) assert len(font.masters) == 2 assert font.masters[0].ascender == 30 assert font.masters[1].ascender == 40
def test_unicodes(tmpdir): tmpdir = str(tmpdir) testDocPath = os.path.join(tmpdir, "testUnicodes.designspace") testDocPath2 = os.path.join(tmpdir, "testUnicodes_roundtrip.designspace") masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") doc = DesignSpaceDocument() # add master 1 s1 = SourceDescriptor() s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) s1.name = "master.ufo1" s1.copyInfo = True s1.location = dict(weight=0) doc.addSource(s1) # add master 2 s2 = SourceDescriptor() s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) s2.name = "master.ufo2" s2.location = dict(weight=1000) doc.addSource(s2) # add instance 1 i1 = InstanceDescriptor() i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) i1.name = "instance.ufo1" i1.location = dict(weight=500) glyphData = dict(name="arrow", mute=True, unicodes=[100, 200, 300]) i1.glyphs['arrow'] = glyphData doc.addInstance(i1) # now we have sources and instances, but no axes yet. doc.axes = [] # clear the axes # write some axes a1 = AxisDescriptor() a1.minimum = 0 a1.maximum = 1000 a1.default = 0 a1.name = "weight" a1.tag = "wght" doc.addAxis(a1) # write the document doc.write(testDocPath) assert os.path.exists(testDocPath) # import it again new = DesignSpaceDocument() new.read(testDocPath) new.write(testDocPath2) # compare the file contents with open(testDocPath, 'r', encoding='utf-8') as f1: t1 = f1.read() with open(testDocPath2, 'r', encoding='utf-8') as f2: t2 = f2.read() assert t1 == t2 # check the unicode values read from the document assert new.instances[0].glyphs['arrow']['unicodes'] == [100,200,300]