Beispiel #1
0
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"]
    }
Beispiel #2
0
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
Beispiel #3
0
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()
Beispiel #4
0
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
Beispiel #5
0
 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)
Beispiel #7
0
    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
Beispiel #8
0
    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
Beispiel #9
0
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
Beispiel #10
0
        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
Beispiel #11
0
    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
Beispiel #12
0
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)
Beispiel #13
0
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")