Esempio n. 1
0
def _generate_color_font(config: ColorFontConfig,
                         inputs: Iterable[InputGlyph]):
    """Make a UFO and optionally a TTFont from svgs."""
    ufo = _ufo(config.family, config.upem, config.keep_glyph_names)
    _ensure_codepoints_will_have_glyphs(ufo, inputs)

    base_gid = len(ufo.glyphOrder)
    color_glyphs = [
        ColorGlyph.create(ufo, filename, base_gid + idx, codepoints, psvg)
        for idx, (filename, codepoints, psvg) in enumerate(inputs)
    ]
    ufo.glyphOrder = ufo.glyphOrder + [g.glyph_name for g in color_glyphs]
    for g in color_glyphs:
        assert g.glyph_id == ufo.glyphOrder.index(g.glyph_name)

    _COLOR_FORMAT_GENERATORS[config.color_format].apply_ufo(ufo, color_glyphs)

    with open(config.fea_file) as f:
        ufo.features.text = f.read()
    logging.debug("fea:\n%s\n" % ufo.features.text)

    ttfont = _make_ttfont(config, ufo, color_glyphs)

    # TODO may wish to nuke 'post' glyph names

    return ufo, ttfont
Esempio n. 2
0
def _generate_color_font(config: FontConfig, inputs: Iterable[InputGlyph]):
    """Make a UFO and optionally a TTFont from svgs."""
    ufo = _ufo(config)
    _ensure_codepoints_will_have_glyphs(ufo, inputs)
    base_gid = len(ufo.glyphOrder)
    color_glyphs = tuple(
        ColorGlyph.create(
            config,
            ufo,
            str(glyph_input.svg_file),
            base_gid + idx,
            glyph_input.glyph_name,
            glyph_input.codepoints,
            glyph_input.svg,
        ) for idx, glyph_input in enumerate(inputs))
    # TODO: Optimize glyphOrder so that color glyphs sharing the same clip box
    # values are placed next to one another in continuous ranges, to minimize number
    # of COLRv1 ClipRecords
    ufo.glyphOrder = ufo.glyphOrder + [g.glyph_name for g in color_glyphs]
    for g in color_glyphs:
        assert g.glyph_id == ufo.glyphOrder.index(g.glyph_name)

    _COLOR_FORMAT_GENERATORS[config.color_format].apply_ufo(
        config, ufo, color_glyphs)

    with open(config.fea_file) as f:
        ufo.features.text = f.read()
    logging.debug("fea:\n%s\n" % ufo.features.text)

    ttfont = _make_ttfont(config, ufo, color_glyphs)

    # TODO may wish to nuke 'post' glyph names

    return ufo, ttfont
Esempio n. 3
0
def _generate_color_font(config, glyph_inputs):
    """Make a UFO and optionally a TTFont from svgs.

    Args:
        color_font_config: ColorFontConfig
        glyph_inputs: sequence of (filename, codepoints, nanosvg) tuples
    """
    ufo = _ufo(config.family, config.upem)
    _ensure_codepoints_will_have_glyphs(ufo, glyph_inputs)

    base_gid = len(ufo.glyphOrder)
    color_glyphs = [
        ColorGlyph.create(ufo, filename, base_gid + idx, codepoints, nsvg)
        for idx, (filename, codepoints, nsvg) in enumerate(glyph_inputs)
    ]
    ufo.glyphOrder = ufo.glyphOrder + [g.glyph_name for g in color_glyphs]
    for g in color_glyphs:
        assert g.glyph_id == ufo.glyphOrder.index(g.glyph_name)

    _COLOR_FORMAT_GENERATORS[config.color_format].apply_ufo(ufo, color_glyphs)

    ufo.features.text = _generate_fea([(c.codepoints, c.glyph_name)
                                       for c in color_glyphs])
    logging.debug("fea:\n%s\n" % ufo.features.text)

    ttfont = _make_ttfont(config, ufo, color_glyphs)

    # TODO may wish to nuke 'post' glyph names

    return ufo, ttfont
Esempio n. 4
0
def test_paint_from_shape(svg_in, expected_paints):
    color_glyph = ColorGlyph.create(_ufo(1000), "duck", 1, [0x0042],
                                    _nsvg(svg_in))
    assert {
        _round_gradient_coordinates(paint)
        for paint in color_glyph.paints()
    } == expected_paints
Esempio n. 5
0
def test_transform(view_box, upem, expected_transform):
    svg_str = ('<svg version="1.1"'
               ' xmlns="http://www.w3.org/2000/svg"'
               f' viewBox="{view_box}"'
               "/>")
    color_glyph = ColorGlyph.create(_ufo(upem), "duck", 1, [0x0042],
                                    SVG.fromstring(svg_str))

    assert color_glyph.transform_for_font_space() == pytest.approx(
        expected_transform)
Esempio n. 6
0
def _add_glyph(svg: SVG, color_glyph: ColorGlyph, reuse_cache: ReuseCache):
    # each glyph gets a group of its very own
    svg_g = svg.append_to("/svg:svg", etree.Element("g"))
    svg_g.attrib["id"] = f"glyph{color_glyph.glyph_id}"
    # https://github.com/googlefonts/nanoemoji/issues/58: group needs transform
    svg_g.attrib["transform"] = _svg_matrix(
        color_glyph.transform_for_otsvg_space())

    # copy the shapes into our svg
    for painted_layer in color_glyph.as_painted_layers():
        view_box = color_glyph.picosvg.view_box()
        if view_box is None:
            raise ValueError(f"{color_glyph.filename} must declare view box")
        reuse_key = _inter_glyph_reuse_key(view_box, painted_layer)
        if reuse_key not in reuse_cache.shapes:
            el = to_element(painted_layer.path)
            match = regex.match(r"url\(#([^)]+)*\)", el.attrib.get("fill", ""))
            if match:
                el.attrib[
                    "fill"] = f"url(#{reuse_cache.old_to_new_id.get(match.group(1), match.group(1))})"
            svg_g.append(el)
            reuse_cache.shapes[reuse_key] = el
            for reuse in painted_layer.reuses:
                _ensure_has_id(el)
                svg_use = etree.SubElement(svg_g, "use")
                svg_use.attrib["href"] = f'#{el.attrib["id"]}'
                tx, ty = reuse.gettranslate()
                if tx:
                    svg_use.attrib["x"] = _ntos(tx)
                if ty:
                    svg_use.attrib["y"] = _ntos(ty)
                transform = reuse.translate(-tx, -ty)
                if transform != Affine2D.identity():
                    # TODO apply scale and rotation. Just slap a transform on the <use>?
                    raise NotImplementedError(
                        "TODO apply scale & rotation to use")

        else:
            el = reuse_cache.shapes[reuse_key]
            _ensure_has_id(el)
            svg_use = etree.SubElement(svg_g, "use")
            svg_use.attrib["href"] = f'#{el.attrib["id"]}'
Esempio n. 7
0
def test_color_glyph_layers(svg_in, expected_paints):
    config = FontConfig(upem=1000, ascender=1000, descender=0, width=1000)
    color_glyph = ColorGlyph.create(
        config, _ufo(config), "duck", 1, "g_name", [0x0042],
        _nsvg(svg_in)).mutating_traverse(_round_coords)

    actual_paints = color_glyph.painted_layers
    if actual_paints != expected_paints:
        print("A:")
        print(_pprint(actual_paints))
        print("E:")
        print(_pprint(expected_paints))
    assert actual_paints == expected_paints
Esempio n. 8
0
def test_transform_and_width(view_box, upem, width, ascender, descender,
                             expected_transform, expected_width):
    svg_str = ('<svg version="1.1"'
               ' xmlns="http://www.w3.org/2000/svg"'
               f' viewBox="{view_box}"'
               "><defs/></svg>")
    config = FontConfig(upem=upem,
                        width=width,
                        ascender=ascender,
                        descender=descender).validate()
    ufo = _ufo(config)
    color_glyph = ColorGlyph.create(config, ufo, "duck", 1, "glyph_name",
                                    [0x0042], SVG.fromstring(svg_str))

    assert color_glyph.transform_for_font_space() == pytest.approx(
        expected_transform)
    assert ufo[color_glyph.glyph_name].width == expected_width
Esempio n. 9
0
def _create_glyph(color_glyph: ColorGlyph,
                  painted_layer: PaintedLayer) -> Glyph:
    ufo = color_glyph.ufo

    glyph = ufo.newGlyph(
        _next_name(ufo, lambda i: f"{color_glyph.glyph_name}.{i}"))
    glyph_names = [glyph.name]
    glyph.width = ufo.info.unitsPerEm

    svg_units_to_font_units = color_glyph.transform_for_font_space()

    if painted_layer.reuses:
        # Shape repeats, form a composite
        base_glyph = ufo.newGlyph(
            _next_name(ufo, lambda i: f"{glyph.name}.component.{i}"))
        glyph_names.append(base_glyph.name)

        draw_svg_path(painted_layer.path, base_glyph.getPen(),
                      svg_units_to_font_units)

        glyph.components.append(
            Component(baseGlyph=base_glyph.name,
                      transformation=Affine2D.identity()))

        for transform in painted_layer.reuses:
            # We already redrew the component into font space, don't redo it
            # scale x/y translation and flip y movement to match font space
            transform = transform._replace(
                e=transform.e * svg_units_to_font_units.a,
                f=transform.f * svg_units_to_font_units.d,
            )
            glyph.components.append(
                Component(baseGlyph=base_glyph.name, transformation=transform))
    else:
        # Not a composite, just draw directly on the glyph
        draw_svg_path(painted_layer.path, glyph.getPen(),
                      svg_units_to_font_units)

    ufo.glyphOrder += glyph_names

    return glyph
Esempio n. 10
0
def _migrate_paths_to_ufo_glyphs(color_glyph: ColorGlyph,
                                 glyph_cache: GlyphReuseCache) -> ColorGlyph:
    svg_units_to_font_units = color_glyph.transform_for_font_space()

    # Walk through the color glyph, where we see a PaintGlyph take the path out of it,
    # move the path into font coordinates, generate a ufo glyph, and push the name of
    # the ufo glyph into the PaintGlyph
    def _update_paint_glyph(paint):
        if paint.format != PaintGlyph.format:
            return paint

        if glyph_cache.is_known_glyph(paint.glyph):
            return paint

        assert paint.glyph.startswith(
            "M"), f"{paint.glyph} doesn't look like a path"
        path_in_font_space = (SVGPath(
            d=paint.glyph).apply_transform(svg_units_to_font_units).d)

        reuse_result = glyph_cache.try_reuse(path_in_font_space)
        if reuse_result is not None:
            # TODO: when is it more compact to use a new transforming glyph?
            child_transform = Affine2D.identity()
            child_paint = paint.paint
            if is_transform(child_paint):
                child_transform = child_paint.gettransform()
                child_paint = child_paint.paint

            # sanity check: GlyphReuseCache.try_reuse would return None if overflowed
            assert fixed_safe(*reuse_result.transform)
            overflows = False

            # TODO: handle gradient anywhere in subtree, not only as direct child of
            # PaintGlyph or PaintTransform
            if is_gradient(child_paint):
                # We have a gradient so we need to reverse the effect of the
                # reuse_result.transform. First we try to apply the combined transform
                # to the gradient's geometry; but this may overflow OT integer bounds,
                # in which case we pass through gradient unscaled
                transform = Affine2D.compose_ltr(
                    (child_transform, reuse_result.transform.inverse()))
                # skip reuse if combined transform overflows OT int bounds
                overflows = not fixed_safe(*transform)
                if not overflows:
                    try:
                        child_paint = child_paint.apply_transform(transform)
                    except OverflowError:
                        child_paint = transformed(transform, child_paint)

            if not overflows:
                return transformed(
                    reuse_result.transform,
                    PaintGlyph(
                        glyph=reuse_result.glyph_name,
                        paint=child_paint,
                    ),
                )

        glyph = _create_glyph(color_glyph, paint, path_in_font_space)
        glyph_cache.add_glyph(glyph.name, path_in_font_space)

        return dataclasses.replace(paint, glyph=glyph.name)

    return color_glyph.mutating_traverse(_update_paint_glyph)
Esempio n. 11
0
def _add_glyph(svg: SVG, color_glyph: ColorGlyph, reuse_cache: ReuseCache):
    svg_defs = svg.xpath_one("//svg:defs")

    # each glyph gets a group of its very own
    svg_g = svg.append_to("/svg:svg", etree.Element("g"))
    svg_g.attrib["id"] = f"glyph{color_glyph.glyph_id}"

    view_box = color_glyph.svg.view_box()
    if view_box is None:
        raise ValueError(f"{color_glyph.filename} must declare view box")

    # https://github.com/googlefonts/nanoemoji/issues/58: group needs transform
    svg_g.attrib["transform"] = _svg_matrix(
        color_glyph.transform_for_otsvg_space())

    vbox_to_upem = color_glyph.transform_for_font_space()
    upem_to_vbox = vbox_to_upem.inverse()

    # copy the shapes into our svg
    el_by_path = {(): svg_g}
    complete_paths = set()
    nth_paint_glyph = 0

    for root in color_glyph.painted_layers:
        for context in root.breadth_first():
            if any(c == context.path[:len(c)] for c in complete_paths):
                continue

            parent_el = svg_g
            path = context.path
            while path:
                if path in el_by_path:
                    parent_el = el_by_path[path]
                    break
                path = path[:-1]

            if isinstance(context.paint, PaintGlyph):
                glyph_name = _paint_glyph_name(color_glyph, nth_paint_glyph)
                assert (glyph_name in reuse_cache.glyph_elements
                        ), f"Missing entry for {glyph_name}"

                reuse_result = reuse_cache.reuse_results.get(glyph_name, None)

                if reuse_result:
                    reused_glyph_name = reuse_result.glyph_name
                    reused_el = reuse_cache.glyph_elements[reused_glyph_name]
                    reused_el_tag = etree.QName(reused_el.tag).localname
                    if reused_el_tag == "use":
                        # if reused_el is a <use> it means _migrate_to_defs has already
                        # replaced a parent-less <path> with a <use> pointing to it, and
                        # has appended the reused path to <defs>. Assert that's the case
                        assert _use_href(reused_el) == reused_glyph_name
                        reused_el = svg.xpath_one(
                            f'//svg:defs/svg:path[@id="{reused_glyph_name}"]',
                        )
                    elif reused_el_tag == "path":
                        # we need to refer to you, it's important you have identity
                        reused_el.attrib["id"] = reused_glyph_name
                    else:
                        raise AssertionError(reused_el_tag)

                    svg_use = _create_use_element(svg, parent_el, reuse_result)
                    _apply_paint(
                        svg_defs,
                        svg_use,
                        context.paint.paint,  # pytype: disable=attribute-error
                        upem_to_vbox,
                        reuse_cache,
                    )

                    # In two cases, we need to push the reused element to the outer
                    # <defs> and replace its first occurence with a <use>:
                    # 1) If reuse spans multiple glyphs, as Adobe Illustrator
                    #    doesn't support direct references between glyphs:
                    #    https://github.com/googlefonts/nanoemoji/issues/264
                    # 2) If the reused_el has attributes <use> cannot override
                    #    https://github.com/googlefonts/nanoemoji/issues/337
                    if color_glyph.glyph_name != _color_glyph_name(
                            reused_glyph_name) or _attrib_apply_paint_uses(
                                reused_el):
                        _migrate_to_defs(svg, reused_el, reuse_cache,
                                         reuse_result)

                else:
                    el = reuse_cache.glyph_elements[glyph_name]
                    _apply_paint(
                        svg_defs,
                        el,
                        context.paint.paint,  # pytype: disable=attribute-error
                        upem_to_vbox,
                        reuse_cache,
                    )
                    parent_el.append(el)  # pytype: disable=attribute-error

                # don't update el_by_path because we're declaring this path complete
                complete_paths.add(context.path + (context.paint, ))
                nth_paint_glyph += 1

            elif isinstance(context.paint, PaintColrLayers):
                pass

            elif isinstance(context.paint, PaintSolid):
                _apply_solid_paint(parent_el, context.paint)

            elif _is_svg_supported_composite(context.paint):
                el = etree.SubElement(parent_el, f"{{{svg_meta.svgns()}}}g")
                el_by_path[context.path + (context.paint, )] = el

            # TODO: support transform types, either by introducing <g> or by applying context.transform to Paint

            else:
                raise ValueError(f"What do we do with {context}")
Esempio n. 12
0
def test_paint_from_shape(svg_in, expected_paints):
    color_glyph = ColorGlyph.create(_ufo(256), "duck", 1, [0x0042], _nsvg(svg_in))
    assert color_glyph.paints() == expected_paints