def _apply_paint( svg_defs: etree.Element, el: etree.Element, paint: Paint, upem_to_vbox: Affine2D, reuse_cache: ReuseCache, transform: Affine2D = Affine2D.identity(), ): # If you modify the attributes _apply_paint can set also modify _PAINT_ATTRIB_APPLY_PAINT_MAY_SET if isinstance(paint, PaintSolid): _apply_solid_paint(el, paint) elif isinstance(paint, (PaintLinearGradient, PaintRadialGradient)): # Gradient paint coordinates are in UPEM space, we want them in SVG viewBox # so that they match the SVGPath.d coordinates (that we copy unmodified). paint = _map_gradient_coordinates(paint, upem_to_vbox) # Likewise transforms refer to UPEM so they must be adjusted for SVG if transform != Affine2D.identity(): transform = Affine2D.compose_ltr( (upem_to_vbox.inverse(), transform, upem_to_vbox)) _apply_gradient_paint(svg_defs, el, paint, reuse_cache, transform) elif is_transform(paint): transform @= paint.gettransform() child = paint.paint # pytype: disable=attribute-error _apply_paint(svg_defs, el, child, upem_to_vbox, reuse_cache, transform) else: raise NotImplementedError(type(paint))
def _apply_gradient_common_parts( gradient: etree.Element, paint: _GradientPaint, transform: Affine2D = Affine2D.identity(), ): gradient.attrib["gradientUnits"] = "userSpaceOnUse" for stop in paint.stops: stop_el = etree.SubElement(gradient, "stop") stop_el.attrib["offset"] = _ntos(stop.stopOffset) stop_el.attrib["stop-color"] = stop.color.opaque().to_string() if stop.color.alpha != 1.0: stop_el.attrib["stop-opacity"] = _ntos(stop.color.alpha) if paint.extend != Extend.PAD: gradient.attrib["spreadMethod"] = paint.extend.name.lower() transform = transform.round(_DEFAULT_ROUND_NDIGITS) if transform != Affine2D.identity(): # Safari has a bug which makes it reject a gradient if gradientTransform # contains an 'involutory matrix' (i.e. matrix whose inverse equals itself, # such that M @ M == Identity, e.g. reflection), hence the following hack: # https://github.com/googlefonts/nanoemoji/issues/268 # https://en.wikipedia.org/wiki/Involutory_matrix # TODO: Remove once the bug gets fixed if transform @ transform == Affine2D.identity(): transform = transform._replace(a=transform.a + 0.00001) assert transform.inverse() != transform gradient.attrib["gradientTransform"] = transform.tostring()
def _radial_gradient_paint( svg_defs: etree.Element, svg_path: etree.Element, ttfont: ttLib.TTFont, view_box: Rect, stops: Sequence[_ColorStop], extend: Extend, c0: Point, c1: Point, r0: int, r1: int, transform: Affine2D, ): # map centres and radii from UPEM to SVG space upem_to_vbox = _emsquare_to_viewbox(ttfont["head"].unitsPerEm, view_box) c0 = upem_to_vbox.map_point(c0) c1 = upem_to_vbox.map_point(c1) # _emsquare_to_viewbox guarantees view_box is square so scaling radii is ok r0 = upem_to_vbox.map_point((r0, 0)).x r1 = upem_to_vbox.map_point((r1, 0)).x # COLRv1 centre points aren't affected by the gradient Affine2x2, whereas in SVG # gradientTransform applies to everything; to prevent that, we must also map # the centres with the inverse of gradientTransform, so they won't move. inverse_transform = transform.inverse() fx, fy = inverse_transform.map_point(c0) cx, cy = inverse_transform.map_point(c1) gradient = etree.SubElement(svg_defs, "radialGradient") gradient_id = gradient.attrib["id"] = f"g{len(svg_defs)}" gradient.attrib["gradientUnits"] = "userSpaceOnUse" gradient.attrib["fx"] = _ntos(fx) gradient.attrib["fy"] = _ntos(fy) gradient.attrib["fr"] = _ntos(r0) gradient.attrib["cx"] = _ntos(cx) gradient.attrib["cy"] = _ntos(cy) gradient.attrib["r"] = _ntos(r1) if transform != Affine2D.identity(): gradient.attrib["gradientTransform"] = _svg_matrix(transform) if extend != Extend.PAD: gradient.attrib["spreadMethod"] = extend.name.lower() palette = ttfont["CPAL"].palettes[0] for stop in stops: stop_el = etree.SubElement(gradient, "stop") stop_el.attrib["offset"] = _ntos(stop.offset) cpal_color = palette[stop.palette_index] svg_color, svg_opacity = _svg_color_and_opacity(cpal_color, stop.alpha) stop_el.attrib["stop-color"] = svg_color if svg_opacity: stop_el.attrib["stop-opacity"] = svg_opacity svg_path.attrib["fill"] = f"url(#{gradient_id})"
def _colr_v1_paint_to_svg( ttfont: ttLib.TTFont, glyph_set: Mapping[str, Any], parent_el: etree.Element, svg_defs: etree.Element, font_to_vbox: Affine2D, ot_paint: otTables.Paint, reuse_cache: ReuseCache, transform: Affine2D = Affine2D.identity(), ): def descend(parent: etree.Element, paint: otTables.Paint): _colr_v1_paint_to_svg( ttfont, glyph_set, parent, svg_defs, font_to_vbox, paint, reuse_cache, transform=transform, ) if ot_paint.Format == PaintSolid.format: _apply_solid_ot_paint(parent_el, ttfont, ot_paint) elif ot_paint.Format in _GRADIENT_PAINT_FORMATS: _apply_gradient_ot_paint(svg_defs, parent_el, ttfont, font_to_vbox, ot_paint, reuse_cache, transform) elif ot_paint.Format == PaintGlyph.format: layer_glyph = ot_paint.Glyph svg_path = etree.SubElement(parent_el, "path") # This only occurs if path is reused; we could wire up use. But for now ... not. if transform != Affine2D.identity(): svg_transform = Affine2D.compose_ltr( (font_to_vbox.inverse(), transform, font_to_vbox)) svg_path.attrib["transform"] = _svg_matrix(svg_transform) # we must reset the current user space when setting the 'transform' # attribute on a <path>, since that already affects the gradients used # and we don't want the transform to be applied twice to gradients: # https://github.com/googlefonts/nanoemoji/issues/334 transform = Affine2D.identity() descend(svg_path, ot_paint.Paint) _draw_svg_path(svg_path, glyph_set, layer_glyph, font_to_vbox) elif is_transform(ot_paint.Format): paint = Paint.from_ot(ot_paint) transform @= paint.gettransform() descend(parent_el, ot_paint.Paint) elif ot_paint.Format == PaintColrLayers.format: layerList = ttfont["COLR"].table.LayerList.Paint assert layerList, "Paint layers without a layer list :(" for child_paint in layerList[ot_paint. FirstLayerIndex:ot_paint.FirstLayerIndex + ot_paint.NumLayers]: descend(parent_el, child_paint) elif ot_paint.Format == PaintComposite.format and ( ot_paint.CompositeMode == CompositeMode.SRC_IN and ot_paint.BackdropPaint.Format == PaintSolid.format): # Only simple group opacity for now color = _color( ttfont, ot_paint.BackdropPaint.PaletteIndex, ot_paint.BackdropPaint.Alpha, ) if color[:3] != (0, 0, 0): raise NotImplementedError(color) g = etree.SubElement(parent_el, "g") g.attrib["opacity"] = ntos(color.alpha) descend(g, ot_paint.SourcePaint) else: raise NotImplementedError(ot_paint.Format)