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