Esempio n. 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"]
    }
Esempio n. 2
0
def _ufo(config: FontConfig) -> ufoLib2.Font:
    ufo = ufoLib2.Font()
    ufo.info.familyName = config.family
    # set various font metadata; see the full list of fontinfo attributes at
    # https://unifiedfontobject.org/versions/ufo3/fontinfo.plist/#generic-dimension-information
    ufo.info.unitsPerEm = config.upem
    # we just use a simple scheme that makes all sets of vertical metrics the same;
    # if one needs more fine-grained control they can fix up post build
    ufo.info.ascender = (ufo.info.openTypeHheaAscender
                         ) = ufo.info.openTypeOS2TypoAscender = config.ascender
    ufo.info.descender = (
        ufo.info.openTypeHheaDescender
    ) = ufo.info.openTypeOS2TypoDescender = config.descender
    ufo.info.openTypeHheaLineGap = ufo.info.openTypeOS2TypoLineGap = config.linegap
    # set USE_TYPO_METRICS flag (OS/2.fsSelection bit 7) to make sure OS/2 Typo* metrics
    # are preferred to define Windows line spacing over legacy WinAscent/WinDescent:
    # https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fsselection
    ufo.info.openTypeOS2Selection = [7]

    # version
    ufo.info.versionMajor = config.version_major
    ufo.info.versionMinor = config.version_minor

    # Must have .notdef and Win 10 Chrome likes a blank gid1 so make gid1 space
    ufo.newGlyph(".notdef")
    space = ufo.newGlyph(".space")
    space.unicodes = [0x0020]
    space.width = config.width
    ufo.glyphOrder = [".notdef", ".space"]

    # use 'post' format 3.0 for TTFs, shaving a kew KBs of unneeded glyph names
    ufo.lib[ufo2ft.constants.KEEP_GLYPH_NAMES] = config.keep_glyph_names

    return ufo
Esempio n. 3
0
def main(args=None):
    import argparse
    parser = argparse.ArgumentParser(
        description=
        "Generates I matra glyphs and pres feature lookup in FEA syntax")
    parser.add_argument(
        'threshold',
        type=int,
        nargs=2,
        help="Left and right threshold to find a match.",
    )
    parser.add_argument("output", help="Output for FEA lookup.")
    parser.add_argument('ufos', nargs="+", help="UFOs to process.")
    options = parser.parse_args(args)

    fonts = []
    for path in options.ufos:
        fonts.append(ufoLib2.Font(path))

    matra_maker = MatraMaker(fonts, options.threshold[0], options.threshold[1])

    matra_maker.generate_matra_variants(matra_maker.bucketed_matra_widths)

    for font in fonts:
        font.save()

    with open(options.output, "w") as fp:
        fp.write(matra_maker.feature_text(matra_maker.bucketed_matra_widths))
Esempio n. 4
0
def test_constructor_from_path(datadir):
    path = datadir / "UbuTestData.ufo"
    font = ufoLib2.Font(path)

    assert font._path == path
    assert font._lazy is True
    assert font._validate is True
    assert font._reader is not None

    font2 = ufoLib2.Font(path, lazy=False, validate=False)

    assert font2._path == path
    assert font2._lazy is False
    assert font2._validate is False
    assert font2._reader is None

    assert font == font2
Esempio n. 5
0
def test_LayerSet_load_layers_on_iteration(tmp_path):
    ufo = ufoLib2.Font()
    ufo.layers.newLayer("test")
    ufo_save_path = tmp_path / "test.ufo"
    ufo.save(ufo_save_path)
    ufo = ufoLib2.Font.open(ufo_save_path)
    assert set(ufo.layers.keys()) == {"public.default", "test"}
    for layer in ufo.layers:
        assert layer is not _NOT_LOADED
Esempio n. 6
0
def test_build_GDEF_incomplete_glyphOrder():
    import ufoLib2

    font = ufoLib2.Font()
    font.lib["public.glyphOrder"] = ["b", "c"]
    for name in ("d", "c", "b", "a"):
        glyph = font.newGlyph(name)
        glyph.appendAnchor({"name": "top", "x": 0, "y": 0})

    assert "[b c a d], # Base" in _build_gdef(font)
Esempio n. 7
0
def _font_to_quadratic(input_path, output_path=None, **kwargs):
    ufo = ufo_module.Font(input_path)
    logger.info('Converting curves for %s', input_path)
    if font_to_quadratic(ufo, **kwargs):
        logger.info("Saving %s", output_path)
        if output_path:
            ufo.save(output_path)
        else:
            ufo.save()  # save in-place
    elif output_path:
        _copytree(input_path, output_path)
Esempio n. 8
0
def empty_UFO(style_name: str) -> ufoLib2.Font:
    ufo = ufoLib2.Font()
    ufo.info.familyName = "Test"
    ufo.info.styleName = style_name
    ufo.info.unitsPerEm = 1000
    ufo.info.ascender = 800
    ufo.info.descender = -200
    ufo.info.xHeight = 500
    ufo.info.capHeight = 700
    ufo.info.postscriptUnderlineThickness = 50
    ufo.info.postscriptUnderlinePosition = -75
    g = ufo.newGlyph("a")
    g.width = 500
    return ufo
Esempio n. 9
0
def _ufo(family, upem):
    ufo = ufoLib2.Font()
    ufo.info.familyName = family
    # set various font metadata; see the full list of fontinfo attributes at
    # http://unifiedfontobject.org/versions/ufo3/fontinfo.plist/
    ufo.info.unitsPerEm = upem

    # Must have .notdef and Win 10 Chrome likes a blank gid1 so make gid1 space
    ufo.newGlyph(".notdef")
    space = ufo.newGlyph(".space")
    space.unicodes = [0x0020]
    space.width = upem
    ufo.glyphOrder = [".notdef", ".space"]

    return ufo
Esempio n. 10
0
    def __init__(self, filename, features):
        self._font = font = ufoLib2.Font(validate=False)

        parser = SFDParser(filename, font, ignore_uvs=False, ufo_anchors=False,
            ufo_kerning=False, minimal=True)
        parser.parse()

        if features:
            preprocessor = Preprocessor()
            for d in ("italic", "sans", "display", "math"):
                if d in filename.lower():
                    preprocessor.define(d.upper())
            with open(features) as f:
                preprocessor.parse(f)
            feafile = StringIO()
            preprocessor.write(feafile)
            feafile.write(font.features.text)
            font.features.text = feafile.getvalue()
Esempio n. 11
0
def _ufo(family: str,
         upem: int,
         keep_glyph_names: bool = False) -> ufoLib2.Font:
    ufo = ufoLib2.Font()
    ufo.info.familyName = family
    # set various font metadata; see the full list of fontinfo attributes at
    # http://unifiedfontobject.org/versions/ufo3/fontinfo.plist/
    ufo.info.unitsPerEm = upem

    # Must have .notdef and Win 10 Chrome likes a blank gid1 so make gid1 space
    ufo.newGlyph(".notdef")
    space = ufo.newGlyph(".space")
    space.unicodes = [0x0020]
    space.width = upem
    ufo.glyphOrder = [".notdef", ".space"]

    # use 'post' format 3.0 for TTFs, shaving a kew KBs of unneeded glyph names
    ufo.lib[ufo2ft.constants.KEEP_GLYPH_NAMES] = keep_glyph_names

    return ufo
Esempio n. 12
0
def test_guidelines():
    font = ufoLib2.Font()

    # accept either a mapping or a Guideline object
    font.appendGuideline({"x": 100, "y": 50, "angle": 315})
    font.appendGuideline(ufoLib2.objects.Guideline(x=30))

    assert len(font.guidelines) == 2
    assert font.guidelines == [
        ufoLib2.objects.Guideline(x=100, y=50, angle=315),
        ufoLib2.objects.Guideline(x=30),
    ]

    # setter should clear existing guidelines
    font.guidelines = [{"x": 100}, ufoLib2.objects.Guideline(y=20)]

    assert len(font.guidelines) == 2
    assert font.guidelines == [
        ufoLib2.objects.Guideline(x=100),
        ufoLib2.objects.Guideline(y=20),
    ]
Esempio n. 13
0
def _ufo(config):
    ufo = ufoLib2.Font()
    ufo.info.unitsPerEm = config.upem
    ufo.info.ascender = config.ascender
    ufo.info.descender = config.descender
    return ufo
Esempio n. 14
0
 def open(self, path, font=None):
     if font is None:
         font = Font()
     ufo = ufoLib2.Font(path)
     # font
     info = ufo.info
     if info.openTypeHeadCreated:
         try:
             font.date = datetime.strptime(
                 info.openTypeHeadCreated, "%Y/%m/%d %H:%M:%S")
         except ValueError:
             pass
     if info.familyName:
         font.familyName = info.familyName
     if info.copyright:
         font.copyright = info.copyright
     if info.openTypeNameDesigner:
         font.designer = info.openTypeNameDesigner
     if info.openTypeNameDesignerURL:
         font.designerURL = info.openTypeNameDesignerURL
     if info.openTypeNameManufacturer:
         font.manufacturer = info.openTypeNameManufacturer
     if info.openTypeNameManufacturerURL:
         font.manufacturerURL = info.openTypeNameManufacturerURL
     if info.unitsPerEm:
         font.unitsPerEm = info.unitsPerEm
     if info.versionMajor:
         font.versionMajor = info.versionMajor
     if info.versionMinor:
         font.versionMinor = info.versionMinor
     if ufo.lib:
         font._extraData = ufo.lib
     # features
     if ufo.features:
         font.featureHeaders.append(FeatureHeader("fea", ufo.features))
     # master
     master = font.selectedMaster
     if info.styleName:
         master.name = info.styleName
     for blues in (info.postscriptBlueValues, info.postscriptOtherBlues):
         for yMin, yMax in zip(blues[::2], blues[1::2]):
             master.alignmentZones.append(AlignmentZone(yMin, yMax-yMin))
     if info.postscriptStemSnapH:
         master.hStems = info.postscriptStemSnapH
     if info.postscriptStemSnapV:
         master.vStems = info.postscriptStemSnapV
     for g in ufo.guidelines:
         guideline = Guideline()
         if g.x:
             guideline.x = g.x
         if g.y:
             guideline.y = g.y
         if g.angle:
             guideline.angle = g.angle
         if g.name:
             guideline.name = g.name
         # ufo color and identifier are skipped
         master.guidelines.append(guideline)
     # note: unlike ufo, we store kerning in visual order. hard to convert
     # between the two (given that ltr and rtl pairs can be mixed)
     if ufo.kerning:
         master.hKerning = ufo.kerning
     if info.ascender:
         master.ascender = info.ascender
     if info.capHeight:
         master.capHeight = info.capHeight
     if info.descender:
         master.descender = info.descender
     if info.italicAngle:
         master.italicAngle = info.italicAngle
     if info.xHeight:
         master.xHeight = info.xHeight
     # glyphs
     font._glyphs.clear()
     glyphs = font.glyphs
     for g in ufo:
         glyph = Glyph(g.name)
         glyphs.append(glyph)
         if g.unicodes:
             glyph.unicodes = ["%04X" % uniValue for uniValue in g.unicodes]
         # TODO assign kerning groups
         # layer
         layer = glyph.layerForMaster(None)
         layer.width = g.width
         layer.height = g.height
         lib = g.lib
         vertOrigin = lib.pop("public.verticalOrigin", None)
         if vertOrigin:
             layer.yOrigin = vertOrigin
         if lib:
             layer._extraData = lib
         # anchors
         anchors = layer.anchors
         for a in g.anchors:
             if not a.name:
                 continue
             anchors[a.name] = Anchor(a.x or 0, a.y or 0)
             # ufo color and identifier are skipped
         # components
         components = layer.components
         for c in g.components:
             component = Component(c.baseGlyph)
             if c.transformation:
                 component.transformation = Transformation(
                     *tuple(c.transformation))
             # ufo identifier is skipped
             components.append(component)
         # guidelines
         guidelines = layer.guidelines
         for g_ in g.guidelines:
             guideline = Guideline(g_.x or 0, g_.y or 0, g_.angle or 0)
             if g_.name:
                 guideline.name = g_.name
             # ufo color and identifier are skipped
             guidelines.append(guideline)
         # paths
         paths = layer.paths
         for c in self.unstructure(g.contours):
             pts = c.pop("_points")
             for p in pts:
                 name = p.pop("name", None)
                 ident = p.pop("identifier", None)
                 if name or ident:
                     p["extraData"] = d = {}
                     if name:
                         d["name"] = name
                     if ident:
                         d["id"] = ident
             while pts[-1]["type"] is None:
                 pts.insert(0, pts.pop())
             c["points"] = pts
             ident = c.pop("identifier", None)
             if ident:
                 c["id"] = ident
             path = self.structure(c, Path)
             paths.append(path)
         glyph._lastModified = None
     return font
Esempio n. 15
0
    def generate_instance(
            self, instance: designspaceLib.InstanceDescriptor) -> ufoLib2.Font:
        """Generate an interpolated instance font object for an
        InstanceDescriptor."""
        if anisotropic(instance.location):
            raise InstantiatorError(
                f"Instance {instance.familyName}-"
                f"{instance.styleName}: Anisotropic location "
                f"{instance.location} not supported by varLib.")

        font = ufoLib2.Font()

        # Instances may leave out locations that match the default source, so merge
        # default location with the instance's location.
        location = {**self.default_design_location, **instance.location}
        location_normalized = varLib.models.normalizeLocation(
            location, self.axis_bounds)

        # Kerning
        kerning_instance = self.kerning_mutator.instance_at(
            location_normalized)
        if self.round_geometry:
            kerning_instance.round()
        kerning_instance.extractKerning(font)

        # Info
        self._generate_instance_info(instance, location_normalized, location,
                                     font)

        # Non-kerning groups. Kerning groups have been taken care of by the kerning
        # instance.
        for key, glyph_names in self.copy_nonkerning_groups.items():
            font.groups[key] = [name for name in glyph_names]

        # Features
        font.features.text = self.copy_feature_text

        # Lib
        #  1. Copy the default lib to the instance.
        font.lib = typing.cast(dict, copy.deepcopy(self.copy_lib))
        #  2. Copy the Designspace's skipExportGlyphs list over to the UFO to
        #     make sure it wins over the default UFO one.
        font.lib["public.skipExportGlyphs"] = [
            name for name in self.skip_export_glyphs
        ]
        #  3. Write _design_ location to instance's lib.
        font.lib["designspace.location"] = [loc for loc in location.items()]

        # Glyphs
        for glyph_name, glyph_mutator in self.glyph_mutators.items():
            glyph = font.newGlyph(glyph_name)

            try:
                glyph_instance = glyph_mutator.instance_at(location_normalized)

                if self.round_geometry:
                    glyph_instance = glyph_instance.round()

                # onlyGeometry=True does not set name and unicodes, in ufoLib2 we can't
                # modify a glyph's name. Copy unicodes from default font.
                glyph_instance.extractGlyph(glyph, onlyGeometry=True)
            except Exception as e:
                # TODO: Figure out what exceptions fontMath/varLib can throw.
                # By default, explode if we cannot generate a glyph instance for
                # whatever reason (usually outline incompatibility)...
                if glyph_name not in self.skip_export_glyphs:
                    raise InstantiatorError(
                        f"Failed to generate instance of glyph '{glyph_name}'."
                    ) from e

                # ...except if the glyph is in public.skipExportGlyphs and would
                # therefore be removed from the compiled font anyway. There's not much
                # we can do except leave it empty in the instance and tell the user.
                logger.warning(
                    "Failed to generate instance of glyph '%s', which is marked as "
                    "non-exportable. Glyph will be left empty. Failure reason: %s",
                    glyph_name,
                    e,
                )

            glyph.unicodes = [
                uv for uv in self.glyph_name_to_unicodes[glyph_name]
            ]

        # Process rules
        glyph_names_list = self.glyph_mutators.keys()
        glyph_names_list_renamed = designspaceLib.processRules(
            self.designspace_rules, location, glyph_names_list)
        for name_old, name_new in zip(glyph_names_list,
                                      glyph_names_list_renamed):
            if name_old != name_new:
                swap_glyph_names(font, name_old, name_new)

        return font
Esempio n. 16
0
def main(args=None):
    """Convert a UFO font from cubic to quadratic curves"""
    parser = argparse.ArgumentParser(prog="cu2qu")
    parser.add_argument("--version",
                        action="version",
                        version=fontTools.__version__)
    parser.add_argument("infiles",
                        nargs="+",
                        metavar="INPUT",
                        help="one or more input UFO source file(s).")
    parser.add_argument("-v", "--verbose", action="count", default=0)
    parser.add_argument(
        "-e",
        "--conversion-error",
        type=float,
        metavar="ERROR",
        default=None,
        help="maxiumum approximation error measured in EM (default: 0.001)")
    parser.add_argument("--keep-direction",
                        dest="reverse_direction",
                        action="store_false",
                        help="do not reverse the contour direction")

    mode_parser = parser.add_mutually_exclusive_group()
    mode_parser.add_argument(
        "-i",
        "--interpolatable",
        action="store_true",
        help="whether curve conversion should keep interpolation compatibility"
    )
    mode_parser.add_argument(
        "-j",
        "--jobs",
        type=int,
        nargs="?",
        default=1,
        const=_cpu_count(),
        metavar="N",
        help="Convert using N multiple processes (default: %(default)s)")

    output_parser = parser.add_mutually_exclusive_group()
    output_parser.add_argument(
        "-o",
        "--output-file",
        default=None,
        metavar="OUTPUT",
        help=("output filename for the converted UFO. By default fonts are "
              "modified in place. This only works with a single input."))
    output_parser.add_argument(
        "-d",
        "--output-dir",
        default=None,
        metavar="DIRECTORY",
        help="output directory where to save converted UFOs")

    options = parser.parse_args(args)

    if ufo_module is None:
        parser.error(
            "Either ufoLib2 or defcon are required to run this script.")

    if not options.verbose:
        level = "WARNING"
    elif options.verbose == 1:
        level = "INFO"
    else:
        level = "DEBUG"
    logging.basicConfig(level=level)

    if len(options.infiles) > 1 and options.output_file:
        parser.error("-o/--output-file can't be used with multile inputs")

    if options.output_dir:
        output_dir = options.output_dir
        if not os.path.exists(output_dir):
            os.mkdir(output_dir)
        elif not os.path.isdir(output_dir):
            parser.error("'%s' is not a directory" % output_dir)
        output_paths = [
            os.path.join(output_dir, os.path.basename(p))
            for p in options.infiles
        ]
    elif options.output_file:
        output_paths = [options.output_file]
    else:
        # save in-place
        output_paths = [None] * len(options.infiles)

    kwargs = dict(dump_stats=options.verbose > 0,
                  max_err_em=options.conversion_error,
                  reverse_direction=options.reverse_direction)

    if options.interpolatable:
        logger.info('Converting curves compatibly')
        ufos = [ufo_module.Font(infile) for infile in options.infiles]
        if fonts_to_quadratic(ufos, **kwargs):
            for ufo, output_path in zip(ufos, output_paths):
                logger.info("Saving %s", output_path)
                if output_path:
                    ufo.save(output_path)
                else:
                    ufo.save()
        else:
            for input_path, output_path in zip(options.infiles, output_paths):
                if output_path:
                    _copytree(input_path, output_path)
    else:
        jobs = min(len(options.infiles),
                   options.jobs) if options.jobs > 1 else 1
        if jobs > 1:
            func = partial(_font_to_quadratic, **kwargs)
            logger.info('Running %d parallel processes', jobs)
            with closing(mp.Pool(jobs)) as pool:
                pool.starmap(func, zip(options.infiles, output_paths))
        else:
            for input_path, output_path in zip(options.infiles, output_paths):
                _font_to_quadratic(input_path, output_path, **kwargs)
Esempio n. 17
0
def _ufo(upem):
    ufo = ufoLib2.Font()
    ufo.info.unitsPerEm = upem
    return ufo
Esempio n. 18
0
def open_ufo(path):
    if hasattr(ufo_module.Font, "open"):  # ufoLib2
        return ufo_module.Font.open(path)
    return ufo_module.Font(path)  # defcon