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
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
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
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
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)
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"]}'
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
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
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
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)
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}")
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