Ejemplo n.º 1
0
def _make_designspace_rule(glyph_names, axis_name, range_min, range_max, reverse=False):
    rule_name = f"BRACKET.{range_min}.{range_max}"
    rule = designspaceLib.RuleDescriptor()
    rule.name = rule_name
    rule.conditionSets.append(
        [{"name": axis_name, "minimum": range_min, "maximum": range_max}]
    )
    location = range_max if reverse else range_min
    for glyph_name in glyph_names:
        sub_glyph_name = _bracket_glyph_name(glyph_name, reverse, location)
        rule.subs.append((glyph_name, sub_glyph_name))
    return rule
Ejemplo n.º 2
0
    def _apply_bracket_layers(self):
        """Extract bracket layers in a GSGlyph into free-standing UFO glyphs with
        Designspace substitution rules.

        As of Glyphs.app 2.6, only single axis bracket layers are supported, we
        assume the axis to be the first axis in the Designspace. Bracket layer
        backgrounds are not round-tripped.

        A glyph can have more than one bracket layer but Designspace
        rule/OpenType variation condition sets apply all substitutions in a rule
        in a range, so we have to potentially sort bracket layers into rule
        buckets. Example: if a glyph "x" has two bracket layers [300] and [600]
        and glyph "a" has bracket layer [300] and the bracket axis tops out at
        1000, we need the following Designspace rules:

        - BRACKET.300.600  # min 300, max 600 on the bracket axis.
          - x -> x.BRACKET.300
        - BRACKET.600.1000
          - x -> x.BRACKET.600
        - BRACKET.300.1000
          - a -> a.BRACKET.300
        """
        if not self._designspace.axes:
            raise ValueError(
                "Cannot apply bracket layers unless at least one axis is defined."
            )
        bracket_axis = self._designspace.axes[0]

        # Determine the axis scale in design space because crossovers/locations are
        # in design space (axis.default/minimum/maximum may be user space).
        if bracket_axis.map:
            axis_scale = [design_location for _, design_location in bracket_axis.map]
            bracket_axis_min = min(axis_scale)
            bracket_axis_max = max(axis_scale)
        else:  # No mapping means user and design space are the same.
            bracket_axis_min = bracket_axis.minimum
            bracket_axis_max = bracket_axis.maximum

        # 1. bracket_layer_map: Organize all bracket layers by crossover value, so
        #    we can go through the layers by location and copy them to free-standing
        #    glyphs.
        # 2. glyph_crossovers: Keep track of the crossover values of a single glyph, so
        #    we can easily sort them into rule buckets.
        # 3. glyph_sanity_counter: Count the number of master layers providing
        #    bracket layers per glyph and crossover value. We currently only support
        #    the situation where there is a bracket layer for _all_ masters, what the
        #    Glyphs.app tutorial calls 'Changing All Masters'.
        bracket_layer_map = defaultdict(list)  # type: Dict[int, List[classes.GSLayer]]
        glyph_crossovers = defaultdict(set)  # type: Dict[str, Set[int]]
        glyph_sanity_counter = defaultdict(
            list
        )  # type: Dict[Tuple[str, int], List[str]]
        for layer in self.bracket_layers:
            glyph_name = layer.parent.name
            n = layer.name
            try:
                bracket_crossover = int(n[n.index("[") + 1 : n.index("]")])
            except ValueError:
                raise ValueError(
                    "Only bracket layers with one numerical (design space) location "
                    "(meaning the first axis in the designspace file) are currently "
                    "supported."
                )
            if not bracket_axis_min <= bracket_crossover <= bracket_axis_max:
                raise ValueError(
                    "Glyph {glyph_name}: Bracket layer {layer_name} must be within the "
                    "design space bounds of the {bracket_axis_name} axis: minimum "
                    "{bracket_axis_minimum}, maximum {bracket_axis_maximum}.".format(
                        glyph_name=glyph_name,
                        layer_name=n,
                        bracket_axis_name=bracket_axis.name,
                        bracket_axis_minimum=bracket_axis_min,
                        bracket_axis_maximum=bracket_axis_max,
                    )
                )
            bracket_layer_map[bracket_crossover].append(layer)
            glyph_crossovers[glyph_name].add(bracket_crossover)
            glyph_sanity_counter[(glyph_name, bracket_crossover)].append(
                layer.associatedMasterId
            )

        # Check that each bracket layer is present in all master layers.
        unbalanced_bracket_layers = []
        n_masters = len(list(self.masters))
        for ((glyph_name, _), master_layer_ids) in glyph_sanity_counter.items():
            if not len(master_layer_ids) == n_masters:
                unbalanced_bracket_layers.append(glyph_name)
        if unbalanced_bracket_layers:
            raise ValueError(
                "Currently, we only support bracket layers that are present on all "
                "masters, i.e. what the Glyphs.app tutorial calls 'Changing All "
                "Masters'. There is a/are bracket layer(s) missing "
                "for glyph(s) {unbalanced_glyphs}.".format(
                    unbalanced_glyphs=unbalanced_bracket_layers
                )
            )

        # Sort crossovers into buckets.
        rule_bucket = defaultdict(list)  # type: Dict[Tuple[int, int], List[int]]
        for glyph_name, crossovers in sorted(glyph_crossovers.items()):
            for crossover_min, crossover_max in util.pairwise(
                sorted(crossovers) + [bracket_axis_max]
            ):
                rule_bucket[(int(crossover_min), int(crossover_max))].append(glyph_name)

        # Generate rules for the bracket layers.
        for (axis_range_min, axis_range_max), glyph_names in sorted(
            rule_bucket.items()
        ):
            rule_name = "BRACKET.{}.{}".format(axis_range_min, axis_range_max)
            glyph_sub_suffix = ".BRACKET.{}".format(axis_range_min)
            rule = designspaceLib.RuleDescriptor()
            rule.name = rule_name
            rule.conditionSets.append(
                [
                    {
                        "name": bracket_axis.name,
                        "minimum": axis_range_min,
                        "maximum": axis_range_max,
                    }
                ]
            )
            rule.subs.extend(
                [
                    (glyph_name, glyph_name + glyph_sub_suffix)
                    for glyph_name in glyph_names
                ]
            )
            self._designspace.addRule(rule)

        # Finally, copy bracket layers to their own glyphs.
        for location, layers in bracket_layer_map.items():
            for layer in layers:
                ufo_font = self._sources[
                    layer.associatedMasterId or layer.layerId
                ].font.layers.defaultLayer
                ufo_glyph_name = "{glyph_name}.BRACKET.{location}".format(
                    glyph_name=layer.parent.name, location=location
                )
                ufo_glyph = ufo_font.newGlyph(ufo_glyph_name)
                self.to_ufo_glyph(ufo_glyph, layer, layer.parent)
                ufo_glyph.unicodes = []  # Avoid cmap interference
                ufo_glyph.lib[GLYPHLIB_PREFIX + "_originalLayerName"] = layer.name
Ejemplo n.º 3
0
    def _apply_bracket_layers(self):
        """Extract bracket layers in a GSGlyph into free-standing UFO glyphs with
        Designspace substitution rules.

        As of Glyphs.app 2.6, only single axis bracket layers are supported, we assume
        the axis to be the first axis in the Designspace.
        """
        if not self._designspace.axes:
            raise ValueError(
                "Cannot apply bracket layers unless at least one axis is defined."
            )
        bracket_axis = self._designspace.axes[0]

        # Determine the axis scale in design space because crossovers/locations are
        # in design space (axis.default/minimum/maximum may be user space).
        if bracket_axis.map:
            axis_scale = [design_location for _, design_location in bracket_axis.map]
            bracket_axis_min = min(axis_scale)
            bracket_axis_max = max(axis_scale)
        else:  # No mapping means user and design space are the same.
            bracket_axis_min = bracket_axis.minimum
            bracket_axis_max = bracket_axis.maximum

        # First, organize all bracket layers by crossover value.
        bracket_layer_map = defaultdict(list)  # type: Dict[int, List[classes.GSLayer]]
        for layer in self.bracket_layers:
            n = layer.name
            try:
                bracket_crossover = int(n[n.index("[") + 1 : n.index("]")])
            except ValueError:
                raise ValueError(
                    "Only bracket layers with one numerical (design space) location "
                    "(meaning the first axis in the designspace file) are currently "
                    "supported."
                )
            if not bracket_axis_min <= bracket_crossover <= bracket_axis_max:
                raise ValueError(
                    "Glyph {glyph_name}: Bracket layer {layer_name} must be within the "
                    "design space bounds of the {bracket_axis_name} axis: minimum "
                    "{bracket_axis_minimum}, maximum {bracket_axis_maximum}.".format(
                        glyph_name=layer.parent.name,
                        layer_name=layer.name,
                        bracket_axis_name=bracket_axis.name,
                        bracket_axis_minimum=bracket_axis_min,
                        bracket_axis_maximum=bracket_axis_max,
                    )
                )
            bracket_layer_map[bracket_crossover].append(layer)

        # Map crossovers to glyph names, i.e. if glyph "x" and "y" have the bracket
        # layers "[300]" and "[600]", crossovers will be
        # {"x": [300, 600], "y": [300, 600]}. This helps with defining overlaps in the
        # replacment rules below.
        # Note: sort by location to ensure value list appending sortedness for
        # roundtrip stability on Python 2.
        crossovers = defaultdict(list)  # type: Dict[str, List[int]]
        for location, layers in sorted(bracket_layer_map.items()):
            if not len(layers) % len(list(self.masters)) == 0:
                raise ValueError(
                    "Currently, we only support bracket layers that are present on all "
                    "masters, i.e. what the Glyphs.app tutorial calls 'Changing All "
                    "Masters'. There is a [{location}] bracket layer missing for a "
                    "glyph.".format(location=location)
                )
            glyph_name_set = {l.parent.name for l in layers}
            for glyph_name in glyph_name_set:
                crossovers[glyph_name].append(location)

        # Copy bracket layers to their own glyphs.
        for location, layers in bracket_layer_map.items():
            for layer in layers:
                ufo_font = self._sources[
                    layer.associatedMasterId or layer.layerId
                ].font.layers.defaultLayer
                ufo_glyph_name = "{glyph_name}.BRACKET.{location}".format(
                    glyph_name=layer.parent.name, location=location
                )
                ufo_glyph = ufo_font.newGlyph(ufo_glyph_name)
                self.to_ufo_glyph(ufo_glyph, layer, layer.parent)
                ufo_glyph.unicodes = []  # Avoid cmap interference
                ufo_glyph.lib[GLYPHLIB_PREFIX + "_originalLayerName"] = layer.name

        # Generate rules for the bracket layers.
        for glyph_name, axis_crossovers in crossovers.items():
            for crossover_min, crossover_max in util.pairwise(
                axis_crossovers + [bracket_axis_max]
            ):
                glyph_name_substitution = "{glyph_name}.BRACKET.{crossover_min}".format(
                    glyph_name=glyph_name, crossover_min=crossover_min
                )
                rule = designspaceLib.RuleDescriptor()
                rule.name = glyph_name_substitution
                rule.conditionSets.append(
                    [
                        {
                            "name": bracket_axis.name,
                            "minimum": crossover_min,
                            "maximum": crossover_max,
                        }
                    ]
                )
                rule.subs.append((glyph_name, glyph_name_substitution))
                self._designspace.addRule(rule)