예제 #1
0
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
예제 #2
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 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,
        )
예제 #3
0
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,
    )
예제 #4
0
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
예제 #5
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."
            )

        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,
        )
예제 #6
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,
        )