예제 #1
0
    def _loadDesignspace(self, designspace):
        # Note: Only used for building variable fonts
        log.info("loading designspace sources")
        if isinstance(designspace, str):
            designspace = DesignSpaceDocument.fromfile(designspace)
        else:
            # copy that we can mess with
            designspace = DesignSpaceDocument.fromfile(designspace.path)

        masters = designspace.loadSourceFonts(opener=Font)
        # masters = [s.font for s in designspace.sources]  # list of UFO font objects

        # Update the default source's full name to not include style name
        defaultFont = designspace.default.font
        defaultFont.info.openTypeNameCompatibleFullName = defaultFont.info.familyName

        log.info("Preprocessing glyphs")
        # find glyphs subject to decomposition and/or overlap removal
        # TODO: Find out why this loop is SO DAMN SLOW. It might just be so that defcon is
        #       really slow when reading glyphs. Perhaps we can sidestep defcon and just
        #       read & parse the .glif files ourselves.
        glyphNamesToDecompose = set()  # glyph names
        glyphsToRemoveOverlaps = set()  # glyph objects
        for ufo in masters:
            # Note: ufo is of type defcon.objects.font.Font
            # update font version
            updateFontVersion(ufo, dummy=False, isVF=True)
            componentReferences = set(ufo.componentReferences)
            for g in ufo:
                directives = findGlyphDirectives(g.note)
                if self._shouldDecomposeGlyph(g, directives,
                                              componentReferences):
                    glyphNamesToDecompose.add(g.name)
                if 'removeoverlap' in directives:
                    if g.components and len(g.components) > 0:
                        glyphNamesToDecompose.add(g.name)
                    glyphsToRemoveOverlaps.add(g)

        self._decompose(masters, glyphNamesToDecompose)

        # remove overlaps
        if glyphsToRemoveOverlaps:
            rmoverlapFilter = RemoveOverlapsFilter(backend='pathops')
            rmoverlapFilter.start()
            if log.isEnabledFor(logging.DEBUG):
                log.debug(
                    'Removing overlaps in glyphs:\n  %s',
                    "\n  ".join(set([g.name for g in glyphsToRemoveOverlaps])),
                )
            elif log.isEnabledFor(logging.INFO):
                log.info('Removing overlaps in %d glyphs',
                         len(glyphsToRemoveOverlaps))
            for g in glyphsToRemoveOverlaps:
                rmoverlapFilter.filter(g)

        # handle control back to fontmake
        return designspace
예제 #2
0
def build_masters(opts):
    """
    Build master OTFs using supplied options.
    """
    logger.info("Reading designspace file...")
    ds = DesignSpaceDocument.fromfile(opts.dsPath)
    validateDesignspaceDoc(ds)
    master_paths = [s.path for s in ds.sources]

    logger.info("Building local OTFs for master font paths...")
    curDir = os.getcwd()
    dsDir = os.path.dirname(opts.dsPath)

    for master_path in master_paths:
        master_path = os.path.join(dsDir, master_path)
        masterDir = os.path.dirname(master_path)
        ufoName = os.path.basename(master_path)
        otfName = os.path.splitext(ufoName)[0]
        otfName = f"{otfName}.otf"

        if masterDir:
            os.chdir(masterDir)

        makeotf(['-nshw', '-f', ufoName, '-o', otfName,
                 '-r', '-nS'] + opts.mkot)
        logger.info(f"Built OTF font for {master_path}")
        generalizeCFF(otfName)
        os.chdir(curDir)
def test_default_featureWriters_in_designspace_lib(tmpdir, ufo_module):
    """Test that the glyphsLib custom featureWriters settings (with mode="append")
    are exported to the designspace lib whenever a GSFont contains a manual 'kern'
    feature. And that they are not imported back to GSFont.userData if they are
    the same as the default value.
    """
    font = classes.GSFont()
    font.masters.append(classes.GSFontMaster())
    kern = classes.GSFeature(name="kern", code="pos a b 100;")
    font.features.append(kern)

    designspace = to_designspace(font, ufo_module=ufo_module)
    path = str(tmpdir / "test.designspace")
    designspace.write(path)
    for source in designspace.sources:
        source.font.save(str(tmpdir / source.filename))

    designspace2 = DesignSpaceDocument.fromfile(path)

    assert UFO2FT_FEATURE_WRITERS_KEY in designspace2.lib
    assert designspace2.lib[
        UFO2FT_FEATURE_WRITERS_KEY] == DEFAULT_FEATURE_WRITERS

    font2 = to_glyphs(designspace2, ufo_module=ufo_module)

    assert not len(font2.userData)
    assert len([f for f in font2.features if f.name == "kern"]) == 1
def test_custom_featureWriters_in_designpace_lib(tmpdir, ufo_module):
    """Test that we can roundtrip custom user-defined ufo2ft featureWriters
    settings that are stored in the designspace lib or GSFont.userData.
    """
    font = classes.GSFont()
    font.masters.append(classes.GSFontMaster())
    kern = classes.GSFeature(name="kern", code="pos a b 100;")
    font.features.append(kern)
    customFeatureWriters = list(DEFAULT_FEATURE_WRITERS) + [{
        "class":
        "MyCustomWriter",
        "module":
        "myCustomWriter"
    }]
    font.userData[UFO2FT_FEATURE_WRITERS_KEY] = customFeatureWriters

    designspace = to_designspace(font, ufo_module=ufo_module)
    path = str(tmpdir / "test.designspace")
    designspace.write(path)
    for source in designspace.sources:
        source.font.save(str(tmpdir / source.filename))

    designspace2 = DesignSpaceDocument.fromfile(path)

    assert UFO2FT_FEATURE_WRITERS_KEY in designspace2.lib
    assert designspace2.lib[UFO2FT_FEATURE_WRITERS_KEY] == customFeatureWriters

    font2 = to_glyphs(designspace2, ufo_module=ufo_module)

    assert len(font2.userData) == 1
    assert font2.userData[UFO2FT_FEATURE_WRITERS_KEY] == customFeatureWriters
예제 #5
0
    def test_varlib_build_BASE(self):
        self.temp_dir()

        ds_path = self.get_test_input('TestBASE.designspace', copy=True)
        ttx_dir = self.get_test_input("master_base_test")
        expected_ttx_name = 'TestBASE'
        suffix = '.otf'

        for path in self.get_file_list(ttx_dir, '.ttx', 'TestBASE'):
            font, savepath = self.compile_font(path, suffix, self.tempdir)

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            source.path = os.path.join(
                self.tempdir, os.path.basename(source.filename).replace(".ufo", suffix)
            )
        ds.updatePaths()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)

        expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx')
        tables = ["BASE"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
        self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
예제 #6
0
    def test_varlib_build_from_ds_object_in_memory_ttfonts(self):
        ds_path = self.get_test_input("Build.designspace")
        ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf")
        expected_ttx_path = self.get_test_output("BuildMain.ttx")

        self.temp_dir()
        for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'):
            self.compile_font(path, ".ttf", self.tempdir)

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            filename = os.path.join(
                self.tempdir,
                os.path.basename(source.filename).replace(".ufo", ".ttf"))
            source.font = TTFont(filename,
                                 recalcBBoxes=False,
                                 recalcTimestamp=False,
                                 lazy=True)
            source.filename = None  # Make sure no file path gets into build()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)
        tables = [
            table_tag for table_tag in varfont.keys() if table_tag != "head"
        ]
        self.expect_ttx(varfont, expected_ttx_path, tables)
예제 #7
0
def compileDSToFont(dsPath, ttFolder):
    doc = DesignSpaceDocument.fromfile(dsPath)
    doc.findDefault()

    ufoPathToTTPath = getTTPaths(doc, ttFolder)

    for source in doc.sources:
        if source.layerName is None:
            ttPath = ufoPathToTTPath[source.path]
            if not os.path.exists(ttPath):
                raise FileNotFoundError(ttPath)
            source.font = TTFont(ttPath, lazy=False)

    assert doc.default.font is not None
    if "name" not in doc.default.font:
        doc.default.font["name"] = newTable(
            "name")  # This is the template for the VF, and needs a name table

    if any(s.layerName is not None for s in doc.sources):
        fb = FontBuilder(unitsPerEm=doc.default.font["head"].unitsPerEm)
        fb.setupGlyphOrder(doc.default.font.getGlyphOrder())
        fb.setupPost()  # This makes sure we store the glyph names
        font = fb.font
        for source in doc.sources:
            if source.font is None:
                source.font = font

    ttFont, masterModel, _ = varLib.build(
        doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT'])

    # Our client needs the masterModel, so we save a pickle into the font
    ttFont["MPcl"] = newTable("MPcl")
    ttFont["MPcl"].data = pickle.dumps(masterModel)

    return ttFont
예제 #8
0
def test_loadSourceFonts():

    def opener(path):
        font = ttLib.TTFont()
        font.importXML(path)
        return font

    # this designspace file contains .TTX source paths
    path = os.path.join(
        os.path.dirname(os.path.dirname(__file__)),
        "varLib",
        "data",
        "SparseMasters.designspace"
    )
    designspace = DesignSpaceDocument.fromfile(path)

    # force two source descriptors to have the same path
    designspace.sources[1].path = designspace.sources[0].path

    fonts = designspace.loadSourceFonts(opener)

    assert len(fonts) == 3
    assert all(isinstance(font, ttLib.TTFont) for font in fonts)
    assert fonts[0] is fonts[1]  # same path, identical font object

    fonts2 = designspace.loadSourceFonts(opener)

    for font1, font2 in zip(fonts, fonts2):
        assert font1 is font2
예제 #9
0
 def __init__(self, filename=None):
     """Load a variable font from the given filename."""
     self.masters = {}
     self.designspace = None
     self.master_order = []
     if filename.endswith(".glyphs"):
         f = GSFont(filename)
         self.designspace = UFOBuilder(f).designspace
         self.masters = {
             master.name: _load_gsfont(master)
             for master in f.masters
         }
         self.master_order = [master.name for master in f.masters]
     elif filename.endswith(".designspace"):
         self.designspace = DesignSpaceDocument.fromfile(filename)
         self.designspace.loadSourceFonts(load_ufo)
         self.masters = {
             source.styleName: source.font
             for source in self.designspace.sources
         }
         self.master_order = [
             source.styleName for source in self.designspace.sources
         ]
     if self.designspace:
         self._make_model()
예제 #10
0
def main(args=None):
    options = get_options(args)

    if os.path.exists(options.var_font_path):
        os.remove(options.var_font_path)

    designspace = DesignSpaceDocument.fromfile(options.design_space_path)
    ds_data = varLib.load_designspace(designspace)
    master_fonts = varLib.load_masters(designspace, otfFinder)
    logger.progress("Reading source fonts...")
    for i, master_font in enumerate(master_fonts):
        designspace.sources[i].font = master_font

    # Subset source fonts
    if options.include_glyphs_path:
        logger.progress("Subsetting source fonts...")
        subsetDict = getSubset(options.include_glyphs_path)
        subset_masters(designspace, subsetDict)

    if options.check_compatibility:
        logger.progress("Checking outline compatibility in source fonts...")
        font_list = [src.font for src in designspace.sources]
        default_font = designspace.sources[ds_data.base_idx].font
        vf = deepcopy(default_font)
        # We copy vf from default_font, because we use VF to hold
        # merged arguments from each source font charstring - this alters
        # the font, which we don't want to do to the default font.
        do_compatibility(vf, font_list, ds_data.base_idx)

    logger.progress("Building variable OTF (CFF2) font...")
    # Note that we now pass in the design space object, rather than a path to
    # the design space file, in order to pass in the modified source fonts
    # fonts without having to recompile and save them.
    try:
        varFont, _, _ = varLib.build(designspace, otfFinder)
    except VarLibCFFPointTypeMergeError:
        logger.error("The input set requires compatibilization. Please try "
                     "again with the -c (--check-compat) option.")
        return 0

    if not options.keep_glyph_names:
        suppress_glyph_names(varFont)

    if options.omit_mac_names:
        remove_mac_names(varFont)

    stat_file_path = os.path.join(os.path.dirname(options.var_font_path),
                                  STAT_FILENAME)
    if os.path.exists(stat_file_path):
        logger.progress("Importing STAT table override...")
        import_stat_override(varFont, stat_file_path)

    validate_stat_axes(varFont)
    validate_stat_values(varFont)
    update_stat_name_ids(varFont)

    varFont.save(options.var_font_path)
    logger.progress(f"Built variable font '{options.var_font_path}'")
예제 #11
0
def test_instance_getStatNames(datadir):
    doc = DesignSpaceDocument.fromfile(datadir / "test_v5_sourceserif.designspace")

    assert getStatNames(doc, doc.instances[0].getFullUserLocation(doc)) == StatNames(
        familyNames={"en": "Source Serif 4"},
        styleNames={"en": "Caption ExtraLight"},
        postScriptFontName="SourceSerif4-CaptionExtraLight",
        styleMapFamilyNames={"en": "Source Serif 4 Caption ExtraLight"},
        styleMapStyleName="regular",
    )
예제 #12
0
def apply_instance_data(designspace, include_filenames=None, Font=None):
    """Open UFO instances referenced by designspace, apply Glyphs instance
    data if present, re-save UFOs and return updated UFO Font objects.

    Args:
        designspace: DesignSpaceDocument object or path (str or PathLike) to
            a designspace file.
        include_filenames: optional set of instance filenames (relative to
            the designspace path) to be included. By default all instaces are
            processed.
        Font: a callable(path: str) -> Font, used to load a UFO, such as
            defcon.Font class (default: ufoLib2.Font.open).
    Returns:
        List of opened and updated instance UFOs.
    """
    from fontTools.designspaceLib import DesignSpaceDocument
    from os.path import normcase, normpath

    if Font is None:
        import ufoLib2

        Font = ufoLib2.Font.open

    if hasattr(designspace, "__fspath__"):
        designspace = designspace.__fspath__()
    if isinstance(designspace, str):
        designspace = DesignSpaceDocument.fromfile(designspace)

    basedir = os.path.dirname(designspace.path)
    instance_ufos = []
    if include_filenames is not None:
        include_filenames = {normcase(normpath(p)) for p in include_filenames}

    for designspace_instance in designspace.instances:
        fname = designspace_instance.filename
        assert fname is not None, "instance %r missing required filename" % getattr(
            designspace_instance, "name", designspace_instance
        )
        if include_filenames is not None:
            fname = normcase(normpath(fname))
            if fname not in include_filenames:
                continue

        logger.debug("Applying instance data to %s", fname)
        # fontmake <= 1.4.0 compares the ufo paths returned from this function
        # to the keys of a dict of designspace locations that have been passed
        # through normpath (but not normcase). We do the same.
        ufo = Font(normpath(os.path.join(basedir, fname)))

        apply_instance_data_to_ufo(ufo, designspace_instance, designspace)

        ufo.save()
        instance_ufos.append(ufo)
    return instance_ufos
예제 #13
0
def test_using_v5_features_upgrades_format(tmpdir, datadir):
    test_file = datadir / "test_v4_original.designspace"
    output_4_path = tmpdir / "test_v4.designspace"
    output_5_path = tmpdir / "test_v5.designspace"
    shutil.copy(test_file, output_4_path)
    doc = DesignSpaceDocument.fromfile(output_4_path)
    doc.write(output_4_path)
    assert 'format="4.1"' in output_4_path.read_text(encoding="utf-8")
    doc.addVariableFont(VariableFontDescriptor(name="TestVF"))
    doc.write(output_5_path)
    assert 'format="5.0"' in output_5_path.read_text(encoding="utf-8")
예제 #14
0
def test_read_v5_document_discrete(datadir):
    doc = DesignSpaceDocument.fromfile(datadir /
                                       "test_v5_discrete.designspace")

    assert not doc.locationLabels
    assert not doc.variableFonts

    assert_descriptors_equal(
        doc.axes,
        [
            DiscreteAxisDescriptor(
                default=400,
                values=[400, 700, 900],
                name="Weight",
                tag="wght",
                axisLabels=[
                    AxisLabelDescriptor(
                        name="Regular",
                        userValue=400,
                        elidable=True,
                        linkedUserValue=700,
                    ),
                    AxisLabelDescriptor(name="Bold", userValue=700),
                    AxisLabelDescriptor(name="Black", userValue=900),
                ],
            ),
            DiscreteAxisDescriptor(
                default=100,
                values=[75, 100],
                name="Width",
                tag="wdth",
                axisLabels=[
                    AxisLabelDescriptor(name="Narrow", userValue=75),
                    AxisLabelDescriptor(
                        name="Normal", userValue=100, elidable=True),
                ],
            ),
            DiscreteAxisDescriptor(
                default=0,
                values=[0, 1],
                name="Italic",
                tag="ital",
                axisLabels=[
                    AxisLabelDescriptor(name="Roman",
                                        userValue=0,
                                        elidable=True,
                                        linkedUserValue=1),
                    AxisLabelDescriptor(name="Italic", userValue=1),
                ],
            ),
        ],
    )
예제 #15
0
def getSourcePathsFromDesignspace():

    designspacePath = getFile("select designspace for variable font",
                              allowsMultipleSelection=False,
                              fileTypes=["designspace"])[0]

    designspace = DesignSpaceDocument.fromfile(designspacePath)

    inputFontPaths = []
    for source in designspace.sources:
        inputFontPaths.append(source.path)

    return designspacePath, inputFontPaths
예제 #16
0
def copyFiles(designspacePath, outRoot):
    """
    Copies the supplied designspace and all of it's sources to *outRoot*

    This updates the source paths in the the designspace file.

    *designspacePath* is a `string` of the path to a designspace file
    *outRoot* is a `string` of the root directory to copy files to
    """

    ignore = shutil.ignore_patterns(".git", ".git*")

    if os.path.exists(outRoot):
        print("🛑 new folder path exists, stopping")
        raise ValueError
    os.mkdir(outRoot)

    newDesignspacePath = os.path.join(outRoot,
                                      os.path.split(designspacePath)[1])

    shutil.copy(designspacePath, newDesignspacePath)

    ds = DesignSpaceDocument.fromfile(designspacePath)
    sources = [source.path for source in ds.sources]
    paths = {}
    for fontPath in sources:
        f = os.path.split(fontPath)[1]
        newPath = os.path.join(outRoot, f)
        paths[f] = newPath
        shutil.copytree(fontPath, newPath, ignore=ignore)

    ds = DesignSpaceDocument.fromfile(newDesignspacePath)
    for source in ds.sources:
        source.path = paths[os.path.split(source.path)[1]]
    ds.write(newDesignspacePath)

    return newDesignspacePath
예제 #17
0
def compileDSToFont(dsPath, ttFolder):
    doc = DesignSpaceDocument.fromfile(dsPath)
    doc.findDefault()

    ufoPathToTTPath = getTTPaths(doc, ttFolder)

    for source in doc.sources:
        if source.layerName is None:
            ttPath = ufoPathToTTPath[source.path]
            if not os.path.exists(ttPath):
                raise FileNotFoundError(ttPath)
            source.font = TTFont(ttPath, lazy=False)

    assert doc.default.font is not None
    if "name" not in doc.default.font:
        doc.default.font["name"] = newTable(
            "name")  # This is the template for the VF, and needs a name table

    if any(s.layerName is not None for s in doc.sources):
        fb = FontBuilder(unitsPerEm=doc.default.font["head"].unitsPerEm)
        fb.setupGlyphOrder(doc.default.font.getGlyphOrder())
        fb.setupPost()  # This makes sure we store the glyph names
        font = fb.font
        for source in doc.sources:
            if source.font is None:
                source.font = font

    try:
        ttFont, masterModel, _ = varLib.build(
            doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT'])
    except VarLibError as e:
        if 'GSUB' in e.args:
            extraExclude = ['GSUB']
        elif 'GPOS' in e.args:
            extraExclude = ['GPOS', 'GDEF']
        else:
            raise
        print(f"{e!r}", file=sys.stderr)
        print(
            f"Error while building {extraExclude[0]} table, trying again without {' and '.join(extraExclude)}.",
            file=sys.stderr)
        ttFont, masterModel, _ = varLib.build(
            doc, exclude=['MVAR', 'HVAR', 'VVAR', 'STAT'] + extraExclude)

    # Our client needs the masterModel, so we save a pickle into the font
    ttFont["MPcl"] = newTable("MPcl")
    ttFont["MPcl"].data = pickle.dumps(masterModel)

    return ttFont
예제 #18
0
def test_getStatNames_on_ds4_doesnt_make_up_bad_names(datadir):
    """See this issue on GitHub: https://github.com/googlefonts/ufo2ft/issues/630

    When as in the example, there's no STAT data present, the getStatName
    shouldn't try making up a postscript name.
    """
    doc = DesignSpaceDocument.fromfile(datadir / "DS5BreakTest.designspace")

    assert getStatNames(doc, {"Weight": 600, "Width": 125, "Italic": 1}) == StatNames(
        familyNames={"en": "DS5BreakTest"},
        styleNames={},
        postScriptFontName=None,
        styleMapFamilyNames={},
        styleMapStyleName=None,
    )
예제 #19
0
    def test_varlib_build_from_ttx_paths(self):
        ds_path = self.get_test_input("Build.designspace")
        ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf")
        expected_ttx_path = self.get_test_output("BuildMain.ttx")

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            source.path = os.path.join(
                ttx_dir, os.path.basename(source.filename).replace(".ufo", ".ttx")
            )
        ds.updatePaths()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)
        tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
예제 #20
0
    def test_varlib_build_from_ttx_paths(self):
        ds_path = self.get_test_input("Build.designspace")
        ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf")
        expected_ttx_path = self.get_test_output("BuildMain.ttx")

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            source.path = os.path.join(
                ttx_dir, os.path.basename(source.filename).replace(".ufo", ".ttx")
            )
        ds.updatePaths()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)
        tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
예제 #21
0
def add_STAT(designspacePath, fontPath, stylespacePath=None):
    if stylespacePath is None:
        stylespacePath = getStyleSpacePath(designspacePath)
    ds = DesignSpaceDocument.fromfile(designspacePath)

    makeStyleSpace(ds, stylespacePath)

    if stylespacePath is not None:
        print("Adding STAT table")
        additional_locations = ds.lib.get("org.statmake.additionalLocations",
                                          {})
        font = fontTools.ttLib.TTFont(fontPath)
        stylespace = Stylespace.from_file(stylespacePath)
        apply_stylespace_to_variable_font(stylespace, font,
                                          additional_locations)
        font.save(fontPath)
예제 #22
0
def copyDesignSpace(designspacePath, newFolderPath):

    # duplicate designspace into new folder
    inputDStail = os.path.split(designspacePath)[1]
    outputDSpath = newFolderPath + "/" + inputDStail

    shutil.copyfile(designspacePath, outputDSpath)

    # update source & instance paths in designspace as needed
    outputDS = DesignSpaceDocument.fromfile(outputDSpath)

    # updates path if sources were originally in a different directory than designspace file
    for source in outputDS.sources:
        newFontPath = newFolderPath + '/' + os.path.split(source.path)[1]
        source.path = newFontPath

    outputDS.write(outputDSpath)
def interpolate_layout(designspace,
                       loc,
                       master_finder=lambda s: s,
                       mapped=False):
    """
	Interpolate GPOS from a designspace file and location.

	If master_finder is set, it should be a callable that takes master
	filename as found in designspace file and map it to master font
	binary as to be opened (eg. .ttf or .otf).

	If mapped is False (default), then location is mapped using the
	map element of the axes in designspace file.  If mapped is True,
	it is assumed that location is in designspace's internal space and
	no mapping is performed.
	"""
    if hasattr(designspace, "sources"):  # Assume a DesignspaceDocument
        pass
    else:  # Assume a file path
        from fontTools.designspaceLib import DesignSpaceDocument
        designspace = DesignSpaceDocument.fromfile(designspace)

    ds = load_designspace(designspace)
    log.info("Building interpolated font")

    log.info("Loading master fonts")
    master_fonts = load_masters(designspace, master_finder)
    font = deepcopy(master_fonts[ds.base_idx])

    log.info("Location: %s", pformat(loc))
    if not mapped:
        loc = {name: ds.axes[name].map_forward(v) for name, v in loc.items()}
    log.info("Internal location: %s", pformat(loc))
    loc = models.normalizeLocation(loc, ds.internal_axis_supports)
    log.info("Normalized location: %s", pformat(loc))

    # Assume single-model for now.
    model = models.VariationModel(ds.normalized_master_locs)
    assert 0 == model.mapping[ds.base_idx]

    merger = InstancerMerger(font, model, loc)

    log.info("Building interpolated tables")
    # TODO GSUB/GDEF
    merger.mergeTables(font, master_fonts, ['GPOS'])
    return font
예제 #24
0
def test_detect_ribbi_aktiv(datadir):
    doc = DesignSpaceDocument.fromfile(datadir / "test_v5_aktiv.designspace")

    assert getStatNames(doc, {"Weight": 600, "Width": 125, "Italic": 1}) == StatNames(
        familyNames={"en": "Aktiv Grotesk"},
        styleNames={"en": "Ex SemiBold Italic"},
        postScriptFontName="AktivGrotesk-ExSemiBoldItalic",
        styleMapFamilyNames={"en": "Aktiv Grotesk Ex SemiBold"},
        styleMapStyleName="italic",
    )

    assert getStatNames(doc, {"Weight": 700, "Width": 75, "Italic": 1}) == StatNames(
        familyNames={"en": "Aktiv Grotesk"},
        styleNames={"en": "Cd Bold Italic"},
        postScriptFontName="AktivGrotesk-CdBoldItalic",
        styleMapFamilyNames={"en": "Aktiv Grotesk Cd"},
        styleMapStyleName="bold italic",
    )
예제 #25
0
def test_roundtrip(tmpdir, datadir, filename):
    test_file = datadir / filename
    output_path = tmpdir / filename
    # Move the file to the tmpdir so that the filenames stay the same
    # (they're relative to the file's path)
    shutil.copy(test_file, output_path)
    doc = DesignSpaceDocument.fromfile(output_path)
    doc.write(output_path)
    # The input XML has comments and empty lines for documentation purposes
    xml = test_file.read_text(encoding="utf-8")
    xml = re.sub(
        r"<!-- ROUNDTRIP_TEST_REMOVE_ME_BEGIN -->(.|\n)*?<!-- ROUNDTRIP_TEST_REMOVE_ME_END -->",
        "",
        xml,
    )
    xml = re.sub(r"<!--(.|\n)*?-->", "", xml)
    xml = re.sub(r"\s*\n+", "\n", xml)
    assert output_path.read_text(encoding="utf-8") == xml
예제 #26
0
def build_variable(
    designspacePath,
    stylespacePath=None,
    out=None,
    verbose="ERROR",
):
    """
    Builds a variable font from a designspace using fontmake.
    Post applies the STAT table using a stylespace if given.

    *designspacePath* a `string` of the path to the designspace
    *stylespacePath* a `string` of the path to the stylespace
    *out* a `string` of the path where the varible font should be saved
    *verbose* sets the verbosity level for fontmake. Defaults to "ERROR"
    """

    if out is None:
        out = os.path.splitext(
            os.path.basename(designspacePath))[0] + "-VF.ttf"

    else:
        if not os.path.exists(os.path.split(out)[0]):
            os.mkdir(os.path.split(out)[0])

    print("🏗  Constructing variable font")
    fp = FontProject(verbose=verbose)
    fp.build_variable_font(designspacePath,
                           output_path=out,
                           useProductionNames=True)

    if stylespacePath is not None:
        print("🏗  Adding STAT table")
        ds = DesignSpaceDocument.fromfile(designspacePath)
        additional_locations = ds.lib.get("org.statmake.additionalLocations",
                                          {})
        font = fontTools.ttLib.TTFont(out)
        stylespace = Stylespace.from_file(stylespacePath)
        apply_stylespace_to_variable_font(stylespace, font,
                                          additional_locations)
        font.save(out)

    print("✅ Built variable font")
예제 #27
0
    def test_varlib_build_lazy_masters(self):
        # See https://github.com/fonttools/fonttools/issues/1808
        ds_path = self.get_test_input("SparseMasters.designspace")
        expected_ttx_path = self.get_test_output("SparseMasters.ttx")

        def _open_font(master_path, master_finder=lambda s: s):
            font = TTFont()
            font.importXML(master_path)
            buf = BytesIO()
            font.save(buf, reorderTables=False)
            buf.seek(0)
            font = TTFont(buf, lazy=True)  # reopen in lazy mode, to reproduce #1808
            return font

        ds = DesignSpaceDocument.fromfile(ds_path)
        ds.loadSourceFonts(_open_font)
        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)
        tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
예제 #28
0
def test_convert5to4(datadir, tmpdir, test_ds, expected_vfs):
    data_in = datadir / test_ds
    temp_in = tmpdir / test_ds
    shutil.copy(data_in, temp_in)
    doc = DesignSpaceDocument.fromfile(temp_in)

    variable_fonts = convert5to4(doc)

    assert variable_fonts.keys() == expected_vfs
    for vf_name, vf in variable_fonts.items():
        data_out = (datadir / "convert5to4_output" / vf_name).with_suffix(".designspace")
        temp_out = (Path(tmpdir) / "out" / vf_name).with_suffix(".designspace")
        temp_out.parent.mkdir(exist_ok=True)
        vf.write(temp_out)

        if UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING:
            data_out.write_text(temp_out.read_text(encoding="utf-8"), encoding="utf-8")
        else:
            assert data_out.read_text(encoding="utf-8") == temp_out.read_text(
                encoding="utf-8"
            )
예제 #29
0
    def test_varlib_build_sparse_CFF2(self):
        ds_path = self.get_test_input('TestSparseCFF2VF.designspace')
        ttx_dir = self.get_test_input("master_sparse_cff2")
        expected_ttx_path = self.get_test_output("TestSparseCFF2VF.ttx")

        self.temp_dir()
        for path in self.get_file_list(ttx_dir, '.ttx', 'MasterSet_Kanji-'):
            self.compile_font(path, ".otf", self.tempdir)

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            source.path = os.path.join(
                self.tempdir, os.path.basename(source.filename).replace(".ufo", ".otf")
            )
        ds.updatePaths()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)

        tables = ["fvar", "CFF2"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
예제 #30
0
    def test_varlib_build_from_ttf_paths(self):
        self.temp_dir()

        ds_path = self.get_test_input("Build.designspace", copy=True)
        ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf")
        expected_ttx_path = self.get_test_output("BuildMain.ttx")

        for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'):
            self.compile_font(path, ".ttf", self.tempdir)

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            source.path = os.path.join(
                self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf")
            )
        ds.updatePaths()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)
        tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
예제 #31
0
    def test_varlib_build_vpal(self):
        ds_path = self.get_test_input('test_vpal.designspace')
        ttx_dir = self.get_test_input("master_vpal_test")
        expected_ttx_path = self.get_test_output("test_vpal.ttx")

        self.temp_dir()
        for path in self.get_file_list(ttx_dir, '.ttx', 'master_vpal_test_'):
            self.compile_font(path, ".otf", self.tempdir)

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            source.path = os.path.join(
                self.tempdir,
                os.path.basename(source.filename).replace(".ufo", ".otf"))
        ds.updatePaths()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)

        tables = ["GPOS"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
예제 #32
0
    def test_varlib_build_sparse_CFF2(self):
        ds_path = self.get_test_input('TestSparseCFF2VF.designspace')
        ttx_dir = self.get_test_input("master_sparse_cff2")
        expected_ttx_path = self.get_test_output("TestSparseCFF2VF.ttx")

        self.temp_dir()
        for path in self.get_file_list(ttx_dir, '.ttx', 'MasterSet_Kanji-'):
            self.compile_font(path, ".otf", self.tempdir)

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            source.path = os.path.join(
                self.tempdir,
                os.path.basename(source.filename).replace(".ufo", ".otf"))
        ds.updatePaths()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)

        tables = ["fvar", "CFF2"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
예제 #33
0
    def test_varlib_build_from_ds_object_in_memory_ttfonts(self):
        ds_path = self.get_test_input("Build.designspace")
        ttx_dir = self.get_test_input("master_ttx_interpolatable_ttf")
        expected_ttx_path = self.get_test_output("BuildMain.ttx")

        self.temp_dir()
        for path in self.get_file_list(ttx_dir, '.ttx', 'TestFamily-'):
            self.compile_font(path, ".ttf", self.tempdir)

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            filename = os.path.join(
                self.tempdir, os.path.basename(source.filename).replace(".ufo", ".ttf")
            )
            source.font = TTFont(
                filename, recalcBBoxes=False, recalcTimestamp=False, lazy=True
            )
            source.filename = None  # Make sure no file path gets into build()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)
        tables = [table_tag for table_tag in varfont.keys() if table_tag != "head"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
예제 #34
0
    def test_varlib_build_VVAR_CFF2(self):
        ds_path = self.get_test_input('TestVVAR.designspace')
        ttx_dir = self.get_test_input("master_vvar_cff2")
        expected_ttx_name = 'TestVVAR'
        suffix = '.otf'

        self.temp_dir()
        for path in self.get_file_list(ttx_dir, '.ttx', 'TestVVAR'):
            font, savepath = self.compile_font(path, suffix, self.tempdir)

        ds = DesignSpaceDocument.fromfile(ds_path)
        for source in ds.sources:
            source.path = os.path.join(
                self.tempdir, os.path.basename(source.filename).replace(".ufo", suffix)
            )
        ds.updatePaths()

        varfont, _, _ = build(ds)
        varfont = reload_font(varfont)

        expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx')
        tables = ["VVAR"]
        self.expect_ttx(varfont, expected_ttx_path, tables)
        self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix)
예제 #35
0
    def test_varlib_build_sparse_masters_MVAR(self):
        import fontTools.varLib.mvar

        ds_path = self.get_test_input("SparseMasters.designspace")
        ds = DesignSpaceDocument.fromfile(ds_path)
        load_masters(ds)

        # Trigger MVAR generation so varLib is forced to create deltas with a
        # sparse master inbetween.
        font_0_os2 = ds.sources[0].font["OS/2"]
        font_0_os2.sTypoAscender = 1
        font_0_os2.sTypoDescender = 1
        font_0_os2.sTypoLineGap = 1
        font_0_os2.usWinAscent = 1
        font_0_os2.usWinDescent = 1
        font_0_os2.sxHeight = 1
        font_0_os2.sCapHeight = 1
        font_0_os2.ySubscriptXSize = 1
        font_0_os2.ySubscriptYSize = 1
        font_0_os2.ySubscriptXOffset = 1
        font_0_os2.ySubscriptYOffset = 1
        font_0_os2.ySuperscriptXSize = 1
        font_0_os2.ySuperscriptYSize = 1
        font_0_os2.ySuperscriptXOffset = 1
        font_0_os2.ySuperscriptYOffset = 1
        font_0_os2.yStrikeoutSize = 1
        font_0_os2.yStrikeoutPosition = 1
        font_0_vhea = newTable("vhea")
        font_0_vhea.ascent = 1
        font_0_vhea.descent = 1
        font_0_vhea.lineGap = 1
        font_0_vhea.caretSlopeRise = 1
        font_0_vhea.caretSlopeRun = 1
        font_0_vhea.caretOffset = 1
        ds.sources[0].font["vhea"] = font_0_vhea
        font_0_hhea = ds.sources[0].font["hhea"]
        font_0_hhea.caretSlopeRise = 1
        font_0_hhea.caretSlopeRun = 1
        font_0_hhea.caretOffset = 1
        font_0_post = ds.sources[0].font["post"]
        font_0_post.underlineThickness = 1
        font_0_post.underlinePosition = 1

        font_2_os2 = ds.sources[2].font["OS/2"]
        font_2_os2.sTypoAscender = 800
        font_2_os2.sTypoDescender = 800
        font_2_os2.sTypoLineGap = 800
        font_2_os2.usWinAscent = 800
        font_2_os2.usWinDescent = 800
        font_2_os2.sxHeight = 800
        font_2_os2.sCapHeight = 800
        font_2_os2.ySubscriptXSize = 800
        font_2_os2.ySubscriptYSize = 800
        font_2_os2.ySubscriptXOffset = 800
        font_2_os2.ySubscriptYOffset = 800
        font_2_os2.ySuperscriptXSize = 800
        font_2_os2.ySuperscriptYSize = 800
        font_2_os2.ySuperscriptXOffset = 800
        font_2_os2.ySuperscriptYOffset = 800
        font_2_os2.yStrikeoutSize = 800
        font_2_os2.yStrikeoutPosition = 800
        font_2_vhea = newTable("vhea")
        font_2_vhea.ascent = 800
        font_2_vhea.descent = 800
        font_2_vhea.lineGap = 800
        font_2_vhea.caretSlopeRise = 800
        font_2_vhea.caretSlopeRun = 800
        font_2_vhea.caretOffset = 800
        ds.sources[2].font["vhea"] = font_2_vhea
        font_2_hhea = ds.sources[2].font["hhea"]
        font_2_hhea.caretSlopeRise = 800
        font_2_hhea.caretSlopeRun = 800
        font_2_hhea.caretOffset = 800
        font_2_post = ds.sources[2].font["post"]
        font_2_post.underlineThickness = 800
        font_2_post.underlinePosition = 800

        varfont, _, _ = build(ds)
        mvar_tags = [vr.ValueTag for vr in varfont["MVAR"].table.ValueRecord]
        assert all(tag in mvar_tags for tag in fontTools.varLib.mvar.MVAR_ENTRIES)
예제 #36
0
def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
	"""
	Build variation font from a designspace file.

	If master_finder is set, it should be a callable that takes master
	filename as found in designspace file and map it to master font
	binary as to be opened (eg. .ttf or .otf).
	"""
	if hasattr(designspace, "sources"):  # Assume a DesignspaceDocument
		pass
	else:  # Assume a file path
		designspace = DesignSpaceDocument.fromfile(designspace)

	ds = load_designspace(designspace)
	log.info("Building variable font")

	log.info("Loading master fonts")
	master_fonts = load_masters(designspace, master_finder)

	# TODO: 'master_ttfs' is unused except for return value, remove later
	master_ttfs = []
	for master in master_fonts:
		try:
			master_ttfs.append(master.reader.file.name)
		except AttributeError:
			master_ttfs.append(None)  # in-memory fonts have no path

	# Copy the base master to work from it
	vf = deepcopy(master_fonts[ds.base_idx])

	# TODO append masters as named-instances as well; needs .designspace change.
	fvar = _add_fvar(vf, ds.axes, ds.instances)
	if 'STAT' not in exclude:
		_add_stat(vf, ds.axes)
	if 'avar' not in exclude:
		_add_avar(vf, ds.axes)

	# Map from axis names to axis tags...
	normalized_master_locs = [
		{ds.axes[k].tag: v for k,v in loc.items()} for loc in ds.normalized_master_locs
	]
	# From here on, we use fvar axes only
	axisTags = [axis.axisTag for axis in fvar.axes]

	# Assume single-model for now.
	model = models.VariationModel(normalized_master_locs, axisOrder=axisTags)
	assert 0 == model.mapping[ds.base_idx]

	log.info("Building variations tables")
	if 'MVAR' not in exclude:
		_add_MVAR(vf, model, master_fonts, axisTags)
	if 'HVAR' not in exclude:
		_add_HVAR(vf, model, master_fonts, axisTags)
	if 'GDEF' not in exclude or 'GPOS' not in exclude:
		_merge_OTL(vf, model, master_fonts, axisTags)
	if 'gvar' not in exclude and 'glyf' in vf:
		_add_gvar(vf, model, master_fonts, optimize=optimize)
	if 'cvar' not in exclude and 'glyf' in vf:
		_merge_TTHinting(vf, model, master_fonts)
	if 'GSUB' not in exclude and ds.rules:
		_add_GSUB_feature_variations(vf, ds.axes, ds.internal_axis_supports, ds.rules)
	if 'CFF2' not in exclude and 'CFF ' in vf:
		_add_CFF2(vf, model, master_fonts)

	for tag in exclude:
		if tag in vf:
			del vf[tag]

	# TODO: Only return vf for 4.0+, the rest is unused.
	return vf, model, master_ttfs
예제 #37
0
def load_designspace(designspace_filename):

	ds = DesignSpaceDocument.fromfile(designspace_filename)
	masters = ds.sources
	if not masters:
		raise VarLibError("no sources found in .designspace")
	instances = ds.instances

	standard_axis_map = OrderedDict([
		('weight',  ('wght', {'en':'Weight'})),
		('width',   ('wdth', {'en':'Width'})),
		('slant',   ('slnt', {'en':'Slant'})),
		('optical', ('opsz', {'en':'Optical Size'})),
		])

	# Setup axes
	axes = OrderedDict()
	for axis in ds.axes:
		axis_name = axis.name
		if not axis_name:
			assert axis.tag is not None
			axis_name = axis.name = axis.tag

		if axis_name in standard_axis_map:
			if axis.tag is None:
				axis.tag = standard_axis_map[axis_name][0]
			if not axis.labelNames:
				axis.labelNames.update(standard_axis_map[axis_name][1])
		else:
			assert axis.tag is not None
			if not axis.labelNames:
				axis.labelNames["en"] = axis_name

		axes[axis_name] = axis
	log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()]))

	# Check all master and instance locations are valid and fill in defaults
	for obj in masters+instances:
		obj_name = obj.name or obj.styleName or ''
		loc = obj.location
		for axis_name in loc.keys():
			assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name)
		for axis_name,axis in axes.items():
			if axis_name not in loc:
				loc[axis_name] = axis.default
			else:
				v = axis.map_backward(loc[axis_name])
				assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum)

	# Normalize master locations

	internal_master_locs = [o.location for o in masters]
	log.info("Internal master locations:\n%s", pformat(internal_master_locs))

	# TODO This mapping should ideally be moved closer to logic in _add_fvar/avar
	internal_axis_supports = {}
	for axis in axes.values():
		triple = (axis.minimum, axis.default, axis.maximum)
		internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple]
	log.info("Internal axis supports:\n%s", pformat(internal_axis_supports))

	normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs]
	log.info("Normalized master locations:\n%s", pformat(normalized_master_locs))

	# Find base master
	base_idx = None
	for i,m in enumerate(normalized_master_locs):
		if all(v == 0 for v in m.values()):
			assert base_idx is None
			base_idx = i
	assert base_idx is not None, "Base master not found; no master at default location?"
	log.info("Index of base master: %s", base_idx)

	return _DesignSpaceData(
		axes,
		internal_axis_supports,
		base_idx,
		normalized_master_locs,
		masters,
		instances,
		ds.rules,
	)