Example #1
0
def _tidy_use_elements(svg: SVG):
    use_els = sorted(svg.xpath("//use"), key=_use_href)
    targets = {}

    for use_el in use_els:
        ref = _use_href(use_el)
        reused_el = svg.xpath_one(f'//svg:*[@id="{ref}"]')
        targets[ref] = reused_el

        duplicate_attrs = {
            attr_name
            for attr_name, attr_value in use_el.attrib.items()
            if attr_name in reused_el.attrib
            and attr_value == reused_el.attrib.get(attr_name)
        }
        for duplicate_attr in duplicate_attrs:
            del use_el.attrib[duplicate_attr]

    # If all <use> have the same paint attr migrate it from use to target
    for ref, uses in groupby(use_els, key=_use_href):
        uses = list(uses)
        target = targets[ref]
        for attr_name in sorted(_PAINT_ATTRIB_APPLY_PAINT_MAY_SET):
            values = [
                use.attrib[attr_name] for use in uses
                if attr_name in use.attrib
            ]
            unique_values = set(values)
            if len(values) == len(uses) and len(unique_values) == 1:
                target.attrib[attr_name] = values[0]
                for use in uses:
                    del use.attrib[attr_name]
Example #2
0
def _paint_glyph(
    debug_hint: str,
    config: FontConfig,
    picosvg: SVG,
    context: SVGTraverseContext,
    glyph_width: int,
) -> Paint:
    shape = context.shape()

    if shape.fill.startswith("url("):
        fill_el = picosvg.resolve_url(shape.fill, "*")
        try:
            glyph_paint = _GRADIENT_INFO[etree.QName(fill_el).localname](
                config,
                fill_el,
                shape.bounding_box(),
                picosvg.view_box(),
                glyph_width,
                shape.opacity,
            )
        except ValueError as e:
            raise ValueError(
                f"parse failed for {debug_hint}, {etree.tostring(fill_el)[:128]}"
            ) from e
    else:
        glyph_paint = PaintSolid(
            color=Color.fromstring(shape.fill, alpha=shape.opacity)
        )

    return PaintGlyph(glyph=shape.as_path().d, paint=glyph_paint)
Example #3
0
def _run(argv):
    try:
        input_file = argv[1]
    except IndexError:
        input_file = None

    if input_file:
        svg = SVG.parse(input_file).topicosvg()
    else:
        svg = SVG.fromstring(sys.stdin.read()).topicosvg()

    if FLAGS.clip_to_viewbox:
        svg.clip_to_viewbox(inplace=True)

    tree = svg.toetree()

    # lxml really likes to retain whitespace
    for e in tree.iter("*"):
        e.text = _reduce_text(e.text)
        e.tail = _reduce_text(e.tail)

    output = etree.tostring(tree, pretty_print=True).decode("utf-8")

    if FLAGS.output_file == "-":
        print(output)
    else:
        with open(FLAGS.output_file, "w") as f:
            f.write(output)
Example #4
0
def test_shapes_to_paths(shape: str, expected_path: str):
    actual = SVG.fromstring(
        svg_string(shape)).shapes_to_paths(inplace=True).toetree()
    expected_result = SVG.fromstring(
        svg_string(f"<path {expected_path}/>")).toetree()
    print(f"A: {pretty_print(actual)}")
    print(f"E: {pretty_print(expected_result)}")
    assert etree.tostring(actual) == etree.tostring(expected_result)
Example #5
0
def test_svg_to_colr_to_svg(svg_in, expected_svg_out, config_overrides):
    config, glyph_inputs = test_helper.color_font_config(
        config_overrides,
        (svg_in, ),
    )
    _, ttfont = write_font._generate_color_font(config, glyph_inputs)
    svg_before = SVG.parse(str(test_helper.locate_test_file(svg_in)))
    svgs_from_font = tuple(
        colr_to_svg(svg_before.view_box(), ttfont,
                    rounding_ndigits=3).values())
    assert len(svgs_from_font) == 1
    svg_expected = SVG.parse(
        str(test_helper.locate_test_file(expected_svg_out)))
    test_helper.svg_diff(svgs_from_font[0], svg_expected)
Example #6
0
def _rawsvg_docs(
        config: FontConfig, ttfont: ttLib.TTFont,
        color_glyphs: Sequence[ColorGlyph]) -> Sequence[Tuple[str, int, int]]:
    doc_list = []
    for color_glyph in color_glyphs:
        svg = (
            SVG.parse(color_glyph.filename)
            # all the scaling and positioning happens in "transform" below
            .remove_attributes(("width", "height", "viewBox"), inplace=True)
            # Firefox likes to render blank if present
            .remove_attributes(("enable-background", ), inplace=True))
        g = etree.Element(
            "g",
            {
                # Map gid => svg doc
                "id": f"glyph{color_glyph.glyph_id}",
                # map viewBox to OT-SVG space (+x,-y)
                "transform": _svg_matrix(
                    color_glyph.transform_for_otsvg_space()),
            },
        )
        # move all the elements under the new group
        g.extend(svg.svg_root)
        svg.svg_root.append(g)

        doc_list.append((
            svg.tostring(pretty_print=config.pretty_print),
            color_glyph.glyph_id,
            color_glyph.glyph_id,
        ))
    return doc_list
Example #7
0
def _migrate_to_defs(
    svg: SVG,
    reused_el: etree.Element,
    reuse_cache: ReuseCache,
    reuse_result: ReuseResult,
):
    svg_defs = svg.xpath_one("//svg:defs")

    if reused_el in svg_defs:
        return  # nop

    tag = etree.QName(reused_el.tag).localname
    assert tag == "path", f"expected 'path', found '{tag}'"

    svg_use = etree.Element("use", nsmap=svg.svg_root.nsmap)
    svg_use.attrib[_XLINK_HREF_ATTR_NAME] = f"#{reuse_result.glyph_name}"
    # if reused_el hasn't been given a parent yet just let the <use> replace it
    # otherwise move it from current to new parent
    if reused_el.getparent() is None:
        reuse_cache.glyph_elements[reuse_result.glyph_name] = svg_use
    else:
        reused_el.addnext(svg_use)

    svg_defs.append(reused_el)  # append moves

    # sorted for diff stability
    for attr_name in sorted(_attrib_apply_paint_uses(reused_el)):
        svg_use.attrib[attr_name] = reused_el.attrib.pop(attr_name)

    return svg_use
Example #8
0
def _picosvg_docs(
        config: FontConfig, ttfont: ttLib.TTFont,
        color_glyphs: Sequence[ColorGlyph]) -> Sequence[Tuple[str, int, int]]:
    reuse_cache = ReuseCache(config.reuse_tolerance,
                             GlyphReuseCache(config.reuse_tolerance))
    reuse_groups = _glyph_groups(config, color_glyphs, reuse_cache)
    color_glyph_order = [c.glyph_name for c in color_glyphs]
    color_glyphs = {c.glyph_name: c for c in color_glyphs}
    _ensure_groups_grouped_in_glyph_order(color_glyphs, color_glyph_order,
                                          ttfont, reuse_groups)

    doc_list = []
    for group in reuse_groups:
        reuse_cache.gradient_ids = {}  # don't share gradients across groups

        # establish base svg, defs
        root = etree.Element(
            f"{{{svg_meta.svgns()}}}svg",
            {"version": "1.1"},
            nsmap={
                None: svg_meta.svgns(),
                "xlink": svg_meta.xlinkns()
            },
        )
        defs = etree.SubElement(root,
                                f"{{{svg_meta.svgns()}}}defs",
                                nsmap=root.nsmap)
        svg = SVG(root)

        for color_glyph in (color_glyphs[g] for g in group):
            _add_glyph(svg, color_glyph, reuse_cache)

        # tidy use elements, they may emerge from _add_glyph with unnecessary attributes
        _tidy_use_elements(svg)

        # sort <defs> by @id to increase diff stability
        defs[:] = sorted(defs, key=lambda e: e.attrib["id"])

        # strip <defs/> if empty
        if len(defs) == 0:
            root.remove(defs)

        gids = tuple(color_glyphs[g].glyph_id for g in group)
        doc_list.append((svg.tostring(pretty_print=config.pretty_print),
                         min(gids), max(gids)))

    return doc_list
Example #9
0
def _colr_v0_to_svgs(view_box: Rect, ttfont: ttLib.TTFont) -> Dict[str, SVG]:
    glyph_set = ttfont.getGlyphSet()
    return {
        g: SVG.fromstring(
            etree.tostring(
                _colr_v0_glyph_to_svg(ttfont, glyph_set, view_box, g)))
        for g in ttfont["COLR"].ColorLayers
    }
Example #10
0
def _colr_v1_to_svgs(view_box: Rect, ttfont: ttLib.TTFont) -> Dict[str, SVG]:
    glyph_set = ttfont.getGlyphSet()
    return {
        g.BaseGlyph: SVG.fromstring(
            etree.tostring(
                _colr_v1_glyph_to_svg(ttfont, glyph_set, view_box, g)))
        for g in ttfont["COLR"].table.BaseGlyphList.BaseGlyphPaintRecord
    }
Example #11
0
def _inputs(
    glyph_mappings: Sequence[GlyphMapping],
) -> Generator[InputGlyph, None, None]:
    for g in glyph_mappings:
        try:
            picosvg = SVG.parse(str(g.svg_file))
        except etree.ParseError as e:
            raise IOError(f"Unable to parse {g.svg_file}") from e
        yield InputGlyph(g.svg_file, g.codepoints, g.glyph_name, picosvg)
Example #12
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)
Example #13
0
def main():
    try:
        input_file = sys.argv[1]
    except IndexError:
        input_file = None

    if input_file:
        svg = SVG.parse(input_file).topicosvg()
    else:
        svg = SVG.fromstring(sys.stdin.read()).topicosvg()

    tree = svg.toetree()

    # lxml really likes to retain whitespace
    for e in tree.iter("*"):
        e.text = _reduce_text(e.text)
        e.tail = _reduce_text(e.tail)

    print(etree.tostring(tree, pretty_print=True).decode("utf-8"))
Example #14
0
def test_apply_gradient_translation(gradient_string, expected_result):
    svg = SVG.fromstring(
        svg_string(gradient_string))._apply_gradient_translation()
    el = svg.xpath_one("//svg:linearGradient | //svg:radialGradient")

    for node in svg.svg_root.getiterator():
        node.tag = etree.QName(node).localname
    etree.cleanup_namespaces(svg.svg_root)

    assert etree.tostring(el).decode("utf-8") == expected_result
Example #15
0
def test_common_attrib(shape, expected_fields):
    svg = SVG.fromstring(shape)
    field_values = dataclasses.asdict(svg.shapes()[0])
    for field_name, expected_value in expected_fields.items():
        assert field_values.get(field_name, "") == expected_value, field_name

    svg = svg.shapes_to_paths()
    field_values = dataclasses.asdict(svg.shapes()[0])
    for field_name, expected_value in expected_fields.items():
        assert field_values.get(field_name, "") == expected_value, field_name
Example #16
0
def _paint(
    debug_hint: str, config: FontConfig, picosvg: SVG, shape: SVGPath, glyph_width: int
) -> Paint:
    if shape.fill.startswith("url("):
        el = picosvg.resolve_url(shape.fill, "*")
        try:
            return _GRADIENT_INFO[etree.QName(el).localname](
                config,
                el,
                shape.bounding_box(),
                picosvg.view_box(),
                glyph_width,
                shape.opacity,
            )
        except ValueError as e:
            raise ValueError(
                f"parse failed for {debug_hint}, {etree.tostring(el)[:128]}"
            ) from e

    return PaintSolid(color=Color.fromstring(shape.fill, alpha=shape.opacity))
Example #17
0
def _inputs(codepoints: Mapping[str, Tuple[int, ...]],
            svg_files: Iterable[str]) -> Generator[InputGlyph, None, None]:
    for svg_file in svg_files:
        rgi = codepoints.get(os.path.basename(svg_file), None)
        if not rgi:
            raise ValueError(f"No codepoint sequence for {svg_file}")
        try:
            picosvg = SVG.parse(svg_file)
        except etree.ParseError as e:
            raise IOError(f"Unable to parse {svg_file}") from e
        yield InputGlyph(svg_file, rgi, picosvg)
Example #18
0
    def create(
        font_config: FontConfig,
        ufo: ufoLib2.Font,
        filename: str,
        glyph_id: int,
        glyph_name: str,
        codepoints: Tuple[int, ...],
        svg: SVG,
    ) -> "ColorGlyph":
        logging.debug(" ColorGlyph for %s (%s)", filename, codepoints)
        base_glyph = ufo.newGlyph(glyph_name)

        # non-square aspect ratio == proportional width; square == monospace
        view_box = svg.view_box()
        if view_box is not None:
            base_glyph.width = _color_glyph_advance_width(view_box, font_config)
        else:
            base_glyph.width = font_config.width

        # Setup direct access to the glyph if possible
        if len(codepoints) == 1:
            base_glyph.unicode = next(iter(codepoints))

        # Grab the transform + (color, glyph) layers unless they aren't to be touched
        # or cannot possibly paint
        painted_layers = ()
        if not font_config.transform.is_degenerate():
            if font_config.has_picosvgs:
                painted_layers = tuple(
                    _painted_layers(
                        filename,
                        font_config,
                        svg,
                        base_glyph.width,
                    )
                )

        return ColorGlyph(
            ufo,
            filename,
            glyph_name,
            glyph_id,
            codepoints,
            painted_layers,
            svg,
            font_config.transform,
        )
Example #19
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
Example #20
0
def _rawsvg_docs(
        ttfont: ttLib.TTFont,
        color_glyphs: Sequence[ColorGlyph]) -> Sequence[Tuple[str, int, int]]:
    doc_list = []
    for color_glyph in color_glyphs:
        svg = (
            SVG.parse(color_glyph.filename)
            # dumb sizing isn't useful
            .remove_attributes(("width", "height"), inplace=True)
            # Firefox likes to render blank if present
            .remove_attributes(("enable-background", ), inplace=True)
            # Map gid => svg doc
            .set_attributes((("id", f"glyph{color_glyph.glyph_id}"), )))
        svg.svg_root.attrib[
            "transform"] = f"translate(0, {-color_glyph.ufo.info.unitsPerEm})"
        doc_list.append(
            (svg.tostring(), color_glyph.glyph_id, color_glyph.glyph_id))
    return doc_list
def main(argv):
    if len(argv) > 2:
        sys.exit("Expected Only 1 non-flag Argument.")
    symbol = Symbol()
    pico = SVG.parse(argv[1]).topicosvg()
    main_svg = pico.xpath_one("//svg:svg")
    symbol.write_icon(
        _REQUIRED_SYMBOL,
        svgLib.SVGPath.fromstring(pico.tostring()),
        SVGPathPen(None),
        Rect(
            0,
            0,
            parse_float(main_svg.get("width")),
            parse_float(main_svg.get("height")),
        ),
    )
    symbol.drop_empty_icons()
    symbol.write_to(FLAGS.out)
Example #22
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"]}'
Example #23
0
def _picosvg_docs(
        ttfont: ttLib.TTFont,
        color_glyphs: Sequence[ColorGlyph]) -> Sequence[Tuple[str, int, int]]:
    reuse_groups = _glyph_groups(color_glyphs)
    color_glyphs = {c.glyph_name: c for c in color_glyphs}
    _ensure_groups_grouped_in_glyph_order(color_glyphs, ttfont, reuse_groups)

    doc_list = []
    reuse_cache = ReuseCache()
    layers = {}  # reusable layers
    for group in reuse_groups:
        # establish base svg, defs
        svg = SVG.fromstring(
            r'<svg version="1.1" xmlns="http://www.w3.org/2000/svg"><defs/></svg>'
        )

        svg_defs = svg.xpath_one("//svg:defs")
        for color_glyph in (color_glyphs[g] for g in group):
            _add_unique_gradients(svg_defs, color_glyph, reuse_cache)
            _add_glyph(svg, color_glyph, reuse_cache)

        gids = tuple(color_glyphs[g].glyph_id for g in group)
        doc_list.append((svg.tostring(), min(gids), max(gids)))
    return doc_list
Example #24
0
def test_default_for_blank(svg_content, expected_result):
    assert tuple(SVG.fromstring(
        svg_string(svg_content)).shapes()) == expected_result
Example #25
0
def test_tolerance(svg_string, expected_result):
    assert round(SVG.fromstring(svg_string).tolerance, 4) == expected_result
Example #26
0
def test_remove_attributes(svg_string, names, expected_result):
    assert (SVG.fromstring(svg_string).remove_attributes(names).tostring()
            ) == expected_result
Example #27
0
def test_iter(shape, expected_cmds):
    svg_path = SVG.fromstring(svg_string(shape)).shapes_to_paths().shapes()[0]
    actual_cmds = [t for t in svg_path]
    print(f"A: {actual_cmds}")
    print(f"E: {expected_cmds}")
    assert actual_cmds == expected_cmds
Example #28
0
def _nsvg(filename):
    return SVG.parse(_test_file(filename)).topicosvg()
Example #29
0
def test_viewbox(svg_string, expected_result):
    assert SVG.fromstring(svg_string).view_box() == expected_result
Example #30
0
def _new_symbol():
    return SVG.parse(
        os.path.join(os.path.dirname(__file__), "symbol_template.svg"))