def test_default_groups_only2(data_dir, caplog): """Test that the group difference warning is not triggered if non-default source groups are empty.""" d = designspaceLib.DesignSpaceDocument() d.addAxisDescriptor(name="Weight", tag="wght", minimum=300, default=300, maximum=900) d.addSourceDescriptor(location={"Weight": 300}, font=ufoLib2.Font()) d.addSourceDescriptor(location={"Weight": 900}, font=ufoLib2.Font()) d.addInstanceDescriptor(styleName="2", location={"Weight": 400}) d.findDefault() d.sources[0].font.groups["public.kern1.GRK_alpha_alt_LC_1ST"] = [ "alpha.alt", "alphatonos.alt", ] generator = fontmake.instantiator.Instantiator.from_designspace(d) assert "contains different groups than the default source" not in caplog.text instance = generator.generate_instance(d.instances[0]) assert instance.groups == { "public.kern1.GRK_alpha_alt_LC_1ST": ["alpha.alt", "alphatonos.alt"] }
def _make_designspace_with_axes(axes, ufo_module): doc = designspaceLib.DesignSpaceDocument() # Add a "Regular" source regular = doc.newSourceDescriptor() regular.font = ufo_module.Font() regular.location = {name: 0 for _, name in axes} doc.addSource(regular) for tag, name in axes: axis = doc.newAxisDescriptor() axis.tag = tag axis.name = name axis.minimum = 0 axis.default = 0 axis.maximum = 100 doc.addAxis(axis) extreme = doc.newSourceDescriptor() extreme.font = ufo_module.Font() extreme.location = { name_: 0 if name_ != name else 100 for _, name_ in axes } doc.addSource(extreme) return doc
def test_axis_with_no_mapping_does_not_error_in_roundtrip(ufo_module): """Tests that a custom axis without a mapping and without sources on its extremes does not generate an error during roundtrip. Also tests that during a to_glyphs, to_designspace roundtrip the min and max axis information is not lost. """ doc = designspaceLib.DesignSpaceDocument() # Add a "Regular" source regular = doc.newSourceDescriptor() regular.font = ufo_module.Font() regular.location = {"Style": 0} doc.addSource(regular) axis = doc.newAxisDescriptor() axis.tag = "styl" axis.name = "Style" doc.addAxis(axis) # This axis spans a range of 0 to 1 but only has a source at {"Style": 0} # and no explicit mapping. The point of this test is to see if the min and # max are still the same after round tripping. doc.axes[0].minimum = 0 doc.axes[0].maximum = 1 doc.axes[0].default = 0 doc.axes[0].map = [] doc2 = deepcopy(doc) font = to_glyphs(doc2) doc_rt = to_designspace(font) assert doc_rt.axes[0].serialize() == doc.axes[0].serialize()
def test_default_master_roundtrips(): """This test comes from a common scenario while using glyphsLib to go back and forth several times with "minimize diffs" in both directions. In the end we get UFOs that have information as below, and there was a bug that turned "Regular" into "Normal" and changed the default axis value. """ thin = defcon.Font() thin.info.familyName = "CustomFont" thin.info.styleName = "Thin" thin.lib["com.schriftgestaltung.customParameter.GSFont.Axes"] = [ {"Name": "Weight", "Tag": "wght"} ] regular = defcon.Font() regular.info.familyName = "CustomFont" regular.info.styleName = "Regular" regular.lib["com.schriftgestaltung.customParameter.GSFont.Axes"] = [ {"Name": "Weight", "Tag": "wght"} ] ds = designspaceLib.DesignSpaceDocument() weight = ds.newAxisDescriptor() weight.tag = "wght" weight.name = "Weight" weight.minimum = 300 weight.maximum = 700 weight.default = 400 weight.map = [(300, 58), (400, 85), (700, 145)] ds.addAxis(weight) thinSource = ds.newSourceDescriptor() thinSource.font = thin thinSource.location = {"Weight": 58} thinSource.familyName = "CustomFont" thinSource.styleName = "Thin" ds.addSource(thinSource) regularSource = ds.newSourceDescriptor() regularSource.font = regular regularSource.location = {"Weight": 85} regularSource.familyName = "CustomFont" regularSource.styleName = "Regular" regularSource.copyFeatures = True regularSource.copyGroups = True regularSource.copyInfo = True regularSource.copyLib = True ds.addSource(regularSource) font = to_glyphs(ds, minimize_ufo_diffs=True) doc = to_designspace(font, minimize_glyphs_diffs=True) reg = doc.sources[1] assert reg.styleName == "Regular" assert reg.font.info.styleName == "Regular" assert reg.copyFeatures is True assert reg.copyGroups is True assert reg.copyInfo is True assert reg.copyLib is True
def _search_instances(designspace_path, pattern): designspace = designspaceLib.DesignSpaceDocument() designspace.read(designspace_path) instances = OrderedDict() for instance in designspace.instances: # is 'name' optional? 'filename' certainly must not be if fullmatch(pattern, instance.name): instances[instance.name] = instance.filename if not instances: raise FontmakeError("No instance found with %r" % pattern) return instances
def main(argv): ufos = tuple(a for a in argv[1:] if a.endswith(".ufo")) config_file = None if FLAGS.config_file: config_file = Path(FLAGS.config_file) font_config = config.load(config_file) designspace = designspaceLib.DesignSpaceDocument() import pprint pp = pprint.PrettyPrinter() # define axes names, tags and min/default/max axis_defs = [ dict( tag=a.axisTag, name=a.name, minimum=min( p.position for m in font_config.masters for p in m.position if p.axisTag == a.axisTag ), default=a.default, maximum=max( p.position for m in font_config.masters for p in m.position if p.axisTag == a.axisTag ), ) for a in font_config.axes ] logging.info(pp.pformat(axis_defs)) for axis_def in axis_defs: designspace.addAxisDescriptor(**axis_def) axis_names = {a.axisTag: a.name for a in font_config.axes} for master in font_config.masters: ufo = ufoLib2.Font.open(master.output_ufo) ufo.info.styleName = master.style_name location = {axis_names[p.axisTag]: p.position for p in master.position} designspace.addSourceDescriptor( name=master.output_ufo, location=location, font=ufo ) # build a variable TTFont from the designspace document # TODO: Use ufo2ft.compileVariableCFF2 for CFF vf = ufo2ft.compileVariableTTF(designspace) vf.save(font_config.output_file)
def _designspace_locations(self, designspace_path): """Map font filenames to their locations in a designspace.""" maps = [] ds = designspaceLib.DesignSpaceDocument() ds.read(designspace_path) for elements in (ds.sources, ds.instances): location_map = {} for element in elements: path = _normpath(os.path.join( os.path.dirname(designspace_path), element.filename)) location_map[path] = element.location maps.append(location_map) return maps
def _fake_designspace(self, ufos): """Build a fake designspace with the given UFOs as sources, so that all builder functions can rely on the presence of a designspace. """ designspace = designspaceLib.DesignSpaceDocument() ufo_to_location = defaultdict(dict) # Make weight and width axis if relevant for info_key, axis_def in zip( ("openTypeOS2WeightClass", "openTypeOS2WidthClass"), (WEIGHT_AXIS_DEF, WIDTH_AXIS_DEF), ): axis = designspace.newAxisDescriptor() axis.tag = axis_def.tag axis.name = axis_def.name mapping = [] for ufo in ufos: user_loc = getattr(ufo.info, info_key) if user_loc is not None: design_loc = class_to_value(axis_def.tag, user_loc) mapping.append((user_loc, design_loc)) ufo_to_location[ufo][axis_def.name] = design_loc mapping = sorted(set(mapping)) if len(mapping) > 1: axis.map = mapping axis.minimum = min([user_loc for user_loc, _ in mapping]) axis.maximum = max([user_loc for user_loc, _ in mapping]) axis.default = min( axis.maximum, max(axis.minimum, axis_def.default_user_loc) ) designspace.addAxis(axis) for ufo in ufos: source = designspace.newSourceDescriptor() source.font = ufo source.familyName = ufo.info.familyName source.styleName = ufo.info.styleName # source.name = '%s %s' % (source.familyName, source.styleName) source.path = ufo.path source.location = ufo_to_location[ufo] designspace.addSource(source) return designspace
def designspace(layertestrgufo, layertestbdufo): ds = designspaceLib.DesignSpaceDocument() a1 = designspaceLib.AxisDescriptor() a1.tag = "wght" a1.name = "Weight" a1.default = a1.minimum = 350 a1.maximum = 625 ds.addAxis(a1) s1 = designspaceLib.SourceDescriptor() s1.name = "Layer Font Regular" s1.familyName = "Layer Font" s1.styleName = "Regular" s1.filename = "LayerFont-Regular.ufo" s1.location = {"Weight": 350} s1.font = layertestrgufo ds.addSource(s1) s2 = designspaceLib.SourceDescriptor() s2.name = "Layer Font Medium" s2.familyName = "Layer Font" s2.styleName = "Medium" s2.filename = "LayerFont-Regular.ufo" s2.layerName = "Medium" s2.location = {"Weight": 450} s2.font = layertestrgufo ds.addSource(s2) s3 = designspaceLib.SourceDescriptor() s3.name = "Layer Font Bold" s3.familyName = "Layer Font" s3.styleName = "Bold" s3.filename = "LayerFont-Bold.ufo" s3.location = {"Weight": 625} s3.font = layertestbdufo ds.addSource(s3) return ds
font.generate("build/masters_ufo/" + master[0] + "-Slanted" + ".ufo") all_masters.append( [master[0] + "-Slanted", master[1] + [2 * slant_angle], font]) master[1].append(slant_angle) if "monospace" in options or "all" in options: special_instances.append( ["Mono", toPos({ "weight": 400, "monospace": 1, "slant": 20 })]) os.system("rm -rf /tmp/font-generation") document = designspace.DesignSpaceDocument() for axis in axises: a = designspace.AxisDescriptor() a.tag = axis[0] a.name = axis[1] a.minimum = axis[2] a.maximum = axis[3] a.default = axis[4] if len(axis) > 5: a.map = axis[5] document.addAxis(a) for master in all_masters: s = designspace.SourceDescriptor() s.path = "build/masters_ufo/" + master[0] + ".ufo" s.familyName = family_name
def _fake_designspace(self, ufos): """Build a fake designspace with the given UFOs as sources, so that all builder functions can rely on the presence of a designspace. """ designspace = designspaceLib.DesignSpaceDocument() ufo_to_location = defaultdict(dict) # Make weight and width axis if relevant for info_key, axis_def in zip( ("openTypeOS2WeightClass", "openTypeOS2WidthClass"), (WEIGHT_AXIS_DEF, WIDTH_AXIS_DEF), ): axis = designspace.newAxisDescriptor() axis.tag = axis_def.tag axis.name = axis_def.name mapping = [] for ufo in ufos: user_loc = getattr(ufo.info, info_key) if user_loc is not None: design_loc = class_to_value(axis_def.tag, user_loc) mapping.append((user_loc, design_loc)) ufo_to_location[ufo][axis_def.name] = design_loc mapping = sorted(set(mapping)) if len(mapping) > 1: axis.map = mapping axis.minimum = min([user_loc for user_loc, _ in mapping]) axis.maximum = max([user_loc for user_loc, _ in mapping]) axis.default = min( axis.maximum, max(axis.minimum, axis_def.default_user_loc)) designspace.addAxis(axis) for ufo in ufos: source = designspace.newSourceDescriptor() source.font = ufo source.familyName = ufo.info.familyName source.styleName = ufo.info.styleName # source.name = '%s %s' % (source.familyName, source.styleName) source.path = ufo.path source.location = ufo_to_location[ufo] designspace.addSource(source) # UFO-level skip list lib keys are usually ignored, except when we don't have a # Designspace file to start from. If they exist in the UFOs, promote them to a # Designspace-level lib key. However, to avoid accidents, expect the list to # exist in none or be the same in all UFOs. if any("public.skipExportGlyphs" in ufo.lib for ufo in ufos): skip_export_glyphs = { frozenset(ufo.lib.get("public.skipExportGlyphs", [])) for ufo in ufos } if len(skip_export_glyphs) == 1: designspace.lib["public.skipExportGlyphs"] = sorted( next(iter(skip_export_glyphs))) else: raise ValueError( "The `public.skipExportGlyphs` list of all UFOs must either not " "exist or be the same in every UFO.") return designspace
def main(): # TODO: Could refactor args to use click, since we are already using it parser = argparse.ArgumentParser() parser.add_argument('ds', type=pathlib.Path) parser.add_argument( '--round', help="Round values to integers", action="store_true") parser.add_argument( '--ranges', help="Define locations with ranges", action="store_true") args = parser.parse_args() logging.getLogger().setLevel(logging.INFO) output = str(args.ds).replace(".designspace", ".stylespace") ds = dsLib.DesignSpaceDocument() ds.read(args.ds) axes = {} for a in ds.axes: a = a.serialize() axes[a["name"]] = { "name": a["name"], "tag": a["tag"] } locations = {} # For a single axis designspace we can use the defined instance's names to # set a location name if len(axes) == 1: axisName = list(axes.keys())[0] axis = [a for a in ds.axes if a.serialize()["name"] == axisName][0] vals = [get_axis_value(axis, i.location[axisName]) for i in ds.instances] locs = [] for i in ds.instances: for k, v in i.location.items(): loc = get_location(ds, k, v, args.round, args.ranges, vals, i.styleName) locs.append(loc) axes[axisName]["locations"] = locs # For designspaces with several axes the instance style names yield no # reliable location name, so mark all locations with names to be manually # overwritten else: for i in ds.instances: for k, v in i.location.items(): if k not in locations: locations[k] = [] locations[k].append(v) for k, v in locations.items(): vals = sorted(list(set(v))) locs = [] # Make vals a list of unique, user space values for val in vals: loc = get_location(ds, k, val, args.round, args.ranges, vals) locs.append(loc) axes[k]["locations"] = locs with open(output, "wb") as doc: plistlib.dump({"axes": list(axes.values())}, doc) logging.info("Stylespace template '%s' created" % output)
def doit(args): ficopyreq = ("ascender", "copyright", "descender", "familyName", "openTypeHheaAscender", "openTypeHheaDescender", "openTypeHheaLineGap", "openTypeNameDescription", "openTypeNameDesigner", "openTypeNameDesignerURL", "openTypeNameLicense", "openTypeNameLicenseURL", "openTypeNameManufacturer", "openTypeNameManufacturerURL", "openTypeNamePreferredFamilyName", "openTypeNameVersion", "openTypeOS2CodePageRanges", "openTypeOS2TypoAscender", "openTypeOS2TypoDescender", "openTypeOS2TypoLineGap", "openTypeOS2UnicodeRanges", "openTypeOS2VendorID", "openTypeOS2WinAscent", "openTypeOS2WinDescent", "versionMajor", "versionMinor") ficopyopt = ("openTypeNameSampleText", "postscriptFamilyBlues", "postscriptFamilyOtherBlues", "trademark", "woffMetadataCredits", "woffMetadataDescription") fispecial = ("italicAngle", "openTypeOS2WeightClass", "styleMapFamilyName", "styleMapStyleName", "styleName", "unitsPerEm") fiall = sorted(set(ficopyreq) | set(ficopyopt) | set(fispecial)) required = ficopyreq + ("openTypeOS2WeightClass", "styleName", "unitsPerEm") libcopy = ("com.schriftgestaltung.glyphOrder", "public.glyphOrder", "public.postscriptNames") logger = args.logger pds = DSD.DesignSpaceDocument() pds.read(args.primaryds) if args.secondds is not None: sds = DSD.DesignSpaceDocument() sds.read(args.secondds) else: sds = None # Process all the sources psource = None dsources = [] for source in pds.sources: if source.copyInfo: if psource: logger.log('Multiple fonts with <info copy="1" />', "S") psource = Dsource(pds, source, logger, frompds=True, psource=True, args=args) else: dsources.append( Dsource(pds, source, logger, frompds=True, psource=False, args=args)) if sds is not None: for source in sds.sources: dsources.append( Dsource(sds, source, logger, frompds=False, psource=False, args=args)) # Process values in psource complex = True if "master" in psource.source.filename.lower() else False fipval = {} libpval = {} changes = False reqmissing = False for field in fiall: pval = psource.fontinfo.getval( field) if field in psource.fontinfo else None oval = pval # Set values or do other checks for special cases if field == "italicAngle": if "italic" in psource.source.filename.lower(): if pval is None or pval == 0: logger.log( "Primary font: Italic angle must be non-zero for italic fonts", "E") else: if pval is not None and pval != 0: logger.log( "Primary font: Italic angle must be zero for non-italic fonts", "E") pval = None elif field == "openTypeOS2WeightClass": pval = int(psource.source.location["weight"]) elif field == "styleMapFamilyName": if not complex and pval is None: logger.log("styleMapFamilyName missing from primary font", "E") elif field == "styleMapStyleName": if not complex and pval not in ('regular', 'bold', 'italic', 'bold italic'): logger.log( "styleMapStyleName must be 'regular', 'bold', 'italic', 'bold italic'", "E") elif field == "styleName": pval = psource.source.styleName elif field == "unitsperem": if pval is None or pval <= 0: logger.log("unitsperem must be non-zero", "S") # After processing special cases, all required fields should have values if pval is None and field in required: reqmissing = True logger.log( "Required fontinfo field " + field + " missing from " + psource.source.filename, "E") elif oval != pval: changes = True if pval is None: if field in psource.fontinfo: psource.fontinfo.remove(field) else: psource.fontinfo[field][1].text = str(pval) logchange(logger, "Primary font: " + field + " updated:", oval, pval) fipval[field] = pval if reqmissing: logger.log( "Required fontinfo fields missing from " + psource.source.filename, "S") if changes: psource.fontinfo.setval( "openTypeHeadCreated", "string", datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")) psource.write("fontinfo") for field in libcopy: pval = psource.lib.getval(field) if field in psource.lib else None if pval is None: logtype = "W" if field[0:7] == "public." else "I" logger.log( "lib.plist field " + field + " missing from " + psource.source.filename, logtype) libpval[field] = pval # Now update values in other source fonts for dsource in dsources: logger.log("Processing " + dsource.ufodir, "I") fchanges = False for field in fiall: sval = dsource.fontinfo.getval( field) if field in dsource.fontinfo else None oval = sval pval = fipval[field] # Set values or do other checks for special cases if field == "italicAngle": if "italic" in dsource.source.filename.lower(): if sval is None or sval == 0: logger.log( dsource.source.filename + ": Italic angle must be non-zero for italic fonts", "E") else: if sval is not None and sval != 0: logger.log( dsource.source.filename + ": Italic angle must be zero for non-italic fonts", "E") sval = None elif field == "openTypeOS2WeightClass": sval = int(dsource.source.location["weight"]) elif field == "styleMapStyleName": if not complex and sval not in ('regular', 'bold', 'italic', 'bold italic'): logger.log( dsource.source.filename + ": styleMapStyleName must be 'regular', 'bold', 'italic', 'bold italic'", "E") elif field == "styleName": sval = dsource.source.styleName else: sval = pval if oval != sval: if field == "unitsPerEm": logger.log("unitsPerEm inconsistent across fonts", "S") fchanges = True if sval is None: dsource.fontinfo.remove(field) logmess = " removed: " else: logmess = " added: " if oval is None else " updated: " dsource.fontinfo.setelem( field, ET.fromstring(ET.tostring(psource.fontinfo[field][1]))) if field in ("italicAngle", "openTypeOS2WeightClass", "styleName"): dsource.fontinfo[field][1].text = str( sval) # Not a simple copy of pval logchange(logger, dsource.source.filename + " " + field + logmess, oval, sval) lchanges = False for field in libcopy: oval = dsource.lib.getval(field) if field in dsource.lib else None pval = libpval[field] if oval != pval: lchanges = True if pval is None: dsource.lib.remove(field) logmess = " removed: " else: dsource.lib.setelem( field, ET.fromstring(ET.tostring(psource.lib[field][1]))) logmess = " updated: " logchange(logger, dsource.source.filename + " " + field + logmess, oval, pval) if lchanges: dsource.write("lib") fchanges = True # Force fontinfo to update so openTypeHeadCreated is set if fchanges: dsource.fontinfo.setval( "openTypeHeadCreated", "string", datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")) dsource.write("fontinfo") logger.log("psfsyncmasters completed", "P")