def collect_glyph_masters( designspace: designspaceLib.DesignSpaceDocument, glyph_name: str, axis_bounds: AxisBounds, ) -> List[Tuple[Location, FontMathObject]]: """Return master glyph objects for glyph_name wrapped by MathGlyph. Note: skips empty source glyphs if the default glyph is not empty to almost match what ufoProcessor is doing. In e.g. Mutator Sans, the 'S.closed' glyph is left empty in one source layer. One could treat this as a source error, but ufoProcessor specifically has code to skip that empty glyph and carry on. """ locations_and_masters = [] default_glyph_empty = False other_glyph_empty = False for source in designspace.sources: if source.layerName is None: # Source font. source_layer = source.font.layers.defaultLayer else: # Source layer. source_layer = source.font.layers[source.layerName] # Sparse fonts do not and layers may not contain every glyph. if glyph_name not in source_layer: continue source_glyph = source_layer[glyph_name] if not (source_glyph.contours or source_glyph.components): if source is designspace.findDefault(): default_glyph_empty = True else: other_glyph_empty = True normalized_location = varLib.models.normalizeLocation( source.location, axis_bounds ) locations_and_masters.append( (normalized_location, fontMath.MathGlyph(source_glyph)) ) # Filter out empty glyphs if the default glyph is not empty. if not default_glyph_empty and other_glyph_empty: locations_and_masters = [ (loc, master) for loc, master in locations_and_masters if master.contours or master.components ] return locations_and_masters
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 InstantiatorError( "Can't generate UFOs from this designspace: no default font.") if any( anisotropic(instance.location) for instance in designspace.instances): raise InstantiatorError( "The Designspace contains anisotropic instance locations, which are " "not supported by varLib.") 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: AxisBounds = {} # Design space! axis_order: List[str] = [] special_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), ) # Some axes relate to existing OpenType fields and get special attention. if axis.tag in {"wght", "wdth", "slnt"}: special_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_nonkerning_groups: Mapping[str, List[str]] = { key: glyph_names for key, glyph_names in default_font.groups.items() if not key.startswith(("public.kern1.", "public.kern2.")) } # Kerning groups are taken care of by the kerning Variator. 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_nonkerning_groups, copy_info, copy_lib, designspace.default.location, designspace.rules, glyph_mutators, glyph_name_to_unicodes, info_mutator, kerning_mutator, round_geometry, skip_export_glyphs, special_axes, )
def getStatNames(doc: DesignSpaceDocument, userLocation: SimpleLocationDict) -> StatNames: """Compute the family, style, PostScript names of the given ``userLocation`` using the document's STAT information. Also computes localizations. If not enough STAT data is available for a given name, either its dict of localized names will be empty (family and style names), or the name will be None (PostScript name). .. versionadded:: 5.0 """ familyNames: Dict[str, str] = {} defaultSource: Optional[SourceDescriptor] = doc.findDefault() if defaultSource is None: LOGGER.warning( "Cannot determine default source to look up family name.") elif defaultSource.familyName is None: LOGGER.warning( "Cannot look up family name, assign the 'familyname' attribute to the default source." ) else: familyNames = { "en": defaultSource.familyName, **defaultSource.localisedFamilyName, } styleNames: Dict[str, str] = {} # If a free-standing label matches the location, use it for name generation. label = doc.labelForUserLocation(userLocation) if label is not None: styleNames = {"en": label.name, **label.labelNames} # Otherwise, scour the axis labels for matches. else: # Gather all languages in which at least one translation is provided # Then build names for all these languages, but fallback to English # whenever a translation is missing. labels = _getAxisLabelsForUserLocation(doc.axes, userLocation) if labels: languages = set(language for label in labels for language in label.labelNames) languages.add("en") for language in languages: styleName = " ".join( label.labelNames.get(language, label.defaultName) for label in labels if not label.elidable) if not styleName and doc.elidedFallbackName is not None: styleName = doc.elidedFallbackName styleNames[language] = styleName if "en" not in familyNames or "en" not in styleNames: # Not enough information to compute PS names of styleMap names return StatNames( familyNames=familyNames, styleNames=styleNames, postScriptFontName=None, styleMapFamilyNames={}, styleMapStyleName=None, ) postScriptFontName = f"{familyNames['en']}-{styleNames['en']}".replace( " ", "") styleMapStyleName, regularUserLocation = _getRibbiStyle(doc, userLocation) styleNamesForStyleMap = styleNames if regularUserLocation != userLocation: regularStatNames = getStatNames(doc, regularUserLocation) styleNamesForStyleMap = regularStatNames.styleNames styleMapFamilyNames = {} for language in set(familyNames).union(styleNames.keys()): familyName = familyNames.get(language, familyNames["en"]) styleName = styleNamesForStyleMap.get(language, styleNamesForStyleMap["en"]) styleMapFamilyNames[language] = (familyName + " " + styleName).strip() return StatNames( familyNames=familyNames, styleNames=styleNames, postScriptFontName=postScriptFontName, styleMapFamilyNames=styleMapFamilyNames, styleMapStyleName=styleMapStyleName, )
def _extractSubSpace( doc: DesignSpaceDocument, userRegion: Region, *, keepVFs: bool, makeNames: bool, expandLocations: bool, makeInstanceFilename: MakeInstanceFilenameCallable, ) -> DesignSpaceDocument: subDoc = DesignSpaceDocument() # Don't include STAT info # FIXME: (Jany) let's think about it. Not include = OK because the point of # the splitting is to build VFs and we'll use the STAT data of the full # document to generate the STAT of the VFs, so "no need" to have STAT data # in sub-docs. Counterpoint: what if someone wants to split this DS for # other purposes? Maybe for that it would be useful to also subset the STAT # data? # subDoc.elidedFallbackName = doc.elidedFallbackName def maybeExpandDesignLocation(object): if expandLocations: return object.getFullDesignLocation(doc) else: return object.designLocation for axis in doc.axes: range = userRegion[axis.name] if isinstance(range, Range) and hasattr(axis, "minimum"): # Mypy doesn't support narrowing union types via hasattr() # TODO(Python 3.10): use TypeGuard # https://mypy.readthedocs.io/en/stable/type_narrowing.html axis = cast(AxisDescriptor, axis) subDoc.addAxis( AxisDescriptor( # Same info tag=axis.tag, name=axis.name, labelNames=axis.labelNames, hidden=axis.hidden, # Subset range minimum=max(range.minimum, axis.minimum), default=range.default or axis.default, maximum=min(range.maximum, axis.maximum), map=[ (user, design) for user, design in axis.map if range.minimum <= user <= range.maximum ], # Don't include STAT info axisOrdering=None, axisLabels=None, ) ) # Don't include STAT info # subDoc.locationLabels = doc.locationLabels # Rules: subset them based on conditions designRegion = userRegionToDesignRegion(doc, userRegion) subDoc.rules = _subsetRulesBasedOnConditions(doc.rules, designRegion) subDoc.rulesProcessingLast = doc.rulesProcessingLast # Sources: keep only the ones that fall within the kept axis ranges for source in doc.sources: if not locationInRegion(doc.map_backward(source.designLocation), userRegion): continue subDoc.addSource( SourceDescriptor( filename=source.filename, path=source.path, font=source.font, name=source.name, designLocation=_filterLocation( userRegion, maybeExpandDesignLocation(source) ), layerName=source.layerName, familyName=source.familyName, styleName=source.styleName, muteKerning=source.muteKerning, muteInfo=source.muteInfo, mutedGlyphNames=source.mutedGlyphNames, ) ) # Copy family name translations from the old default source to the new default vfDefault = subDoc.findDefault() oldDefault = doc.findDefault() if vfDefault is not None and oldDefault is not None: vfDefault.localisedFamilyName = oldDefault.localisedFamilyName # Variable fonts: keep only the ones that fall within the kept axis ranges if keepVFs: # Note: call getVariableFont() to make the implicit VFs explicit for vf in doc.getVariableFonts(): vfUserRegion = getVFUserRegion(doc, vf) if regionInRegion(vfUserRegion, userRegion): subDoc.addVariableFont( VariableFontDescriptor( name=vf.name, filename=vf.filename, axisSubsets=[ axisSubset for axisSubset in vf.axisSubsets if isinstance(userRegion[axisSubset.name], Range) ], lib=vf.lib, ) ) # Instances: same as Sources + compute missing names for instance in doc.instances: if not locationInRegion(instance.getFullUserLocation(doc), userRegion): continue if makeNames: statNames = getStatNames(doc, instance.getFullUserLocation(doc)) familyName = instance.familyName or statNames.familyNames.get("en") styleName = instance.styleName or statNames.styleNames.get("en") subDoc.addInstance( InstanceDescriptor( filename=instance.filename or makeInstanceFilename(doc, instance, statNames), path=instance.path, font=instance.font, name=instance.name or f"{familyName} {styleName}", userLocation={} if expandLocations else instance.userLocation, designLocation=_filterLocation( userRegion, maybeExpandDesignLocation(instance) ), familyName=familyName, styleName=styleName, postScriptFontName=instance.postScriptFontName or statNames.postScriptFontName, styleMapFamilyName=instance.styleMapFamilyName or statNames.styleMapFamilyNames.get("en"), styleMapStyleName=instance.styleMapStyleName or statNames.styleMapStyleName, localisedFamilyName=instance.localisedFamilyName or statNames.familyNames, localisedStyleName=instance.localisedStyleName or statNames.styleNames, localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName or statNames.styleMapFamilyNames, localisedStyleMapStyleName=instance.localisedStyleMapStyleName or {}, lib=instance.lib, ) ) else: subDoc.addInstance( InstanceDescriptor( filename=instance.filename, path=instance.path, font=instance.font, name=instance.name, userLocation={} if expandLocations else instance.userLocation, designLocation=_filterLocation( userRegion, maybeExpandDesignLocation(instance) ), familyName=instance.familyName, styleName=instance.styleName, postScriptFontName=instance.postScriptFontName, styleMapFamilyName=instance.styleMapFamilyName, styleMapStyleName=instance.styleMapStyleName, localisedFamilyName=instance.localisedFamilyName, localisedStyleName=instance.localisedStyleName, localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName, localisedStyleMapStyleName=instance.localisedStyleMapStyleName, lib=instance.lib, ) ) subDoc.lib = doc.lib return subDoc
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, )
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, )