def _apply_solid_paint(el: etree.Element, paint: PaintSolid): if etree.QName(el.tag).localname == "g": assert paint.color.opaque() == Color.fromstring( "black"), "Unexpected color choice" if paint.color.opaque() != Color.fromstring("black"): el.attrib["fill"] = paint.color.opaque().to_string() if paint.color.alpha != 1.0: el.attrib["opacity"] = _ntos(paint.color.alpha)
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)
def _color_index_node(palette, color_index): color = Color.fromstring(palette[color_index.PaletteIndex].hex()) ci_alpha = color_index.Alpha.value node_id = f"{ci_alpha:.2f}.{color.opaque().to_string()}.{color.alpha:.2f}" return Node( node_id=node_id, node_label=color._replace(alpha=color.alpha * ci_alpha).to_string(), )
def _color_stop(stop_el): offset = stop_el.attrib.get("offset", "0") if offset.endswith("%"): offset = float(offset[:-1]) / 100 else: offset = float(offset) color = stop_el.attrib.get("stop-color", "black") if "stop-opacity" in stop_el.attrib: raise ValueError("<stop stop-opacity/> not supported") return ColorStop(stopOffset=offset, color=Color.fromstring(color))
def _paint(nsvg, shape): match = regex.match(r"^url[(]#([^)]+)[)]$", shape.fill) if shape.fill.startswith("url("): el = nsvg.resolve_url(shape.fill, "*") grad_type, grad_type_parser = _GRADIENT_INFO[etree.QName(el).localname] grad_args = _common_gradient_parts(el) grad_args.update(grad_type_parser(el)) return grad_type(**grad_args) return PaintSolid(color=Color.fromstring(shape.fill, alpha=shape.opacity))
class PaintSolid(Paint): format: ClassVar[int] = int(ot.PaintFormat.PaintSolid) color: Color = Color.fromstring("black") def colors(self): yield self.color def to_ufo_paint(self, colors): return { "Format": self.format, "PaletteIndex": colors.index(self.color.opaque()), "Alpha": self.color.alpha, }
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))
def _paint(self, shape): upem = self.ufo.info.unitsPerEm if shape.fill.startswith("url("): el = self.picosvg.resolve_url(shape.fill, "*") grad_type, grad_type_parser = _GRADIENT_INFO[etree.QName( el).localname] grad_args = _common_gradient_parts(el, shape.opacity) try: grad_args.update( grad_type_parser(el, shape.bounding_box(), self.picosvg.view_box(), upem)) except ValueError as e: raise ValueError( f"parse failed for {self.filename}, {etree.tostring(el)[:128]}" ) from e return grad_type(**grad_args) return PaintSolid( color=Color.fromstring(shape.fill, alpha=shape.opacity))
r1=round(paint.r1, prec), affine2x2=(tuple(round(v, prec) for v in paint.affine2x2) if paint.affine2x2 is not None else None), ) else: return paint @pytest.mark.parametrize( "svg_in, expected_paints", [ # solid ( "rect.svg", { PaintSolid(color=Color.fromstring("blue")), PaintSolid(color=Color.fromstring("blue", alpha=0.8)), }, ), # linear ( "linear_gradient_rect.svg", { PaintLinearGradient( stops=( ColorStop(stopOffset=0.1, color=Color.fromstring("blue")), ColorStop(stopOffset=0.9, color=Color.fromstring("cyan", 0.8)), ), p0=Point(200, 800),
def _painted_layers( debug_hint: str, config: FontConfig, picosvg: SVG, glyph_width: int, ) -> Tuple[Paint, ...]: defs_seen = False layers = [] # Reverse to get leaves first because that makes building Paint's easier # shapes *must* be leaves per picosvg for context in reversed(tuple(picosvg.depth_first())): if context.depth() == 0: continue # svg root # picosvg will deliver us exactly one defs if context.path == "/svg[0]/defs[0]": assert not defs_seen defs_seen = True continue # defs are pulled in by the consuming paints if context.is_shape(): while len(layers) < context.depth(): layers.append([]) assert len(layers) == context.depth() layers[context.depth() - 1].append( _paint_glyph(debug_hint, config, picosvg, context, glyph_width) ) if context.is_group(): # flush child shapes into a new group opacity = float(context.element.get("opacity")) assert ( 0.0 < opacity < 1.0 ), f"{debug_hint} {context.path} should be transparent" assert ( len(layers) == context.depth() + 1 ), "Should have a list of child nodes" child_nodes = layers.pop(context.depth()) assert ( len(child_nodes) > 1 ), f"{debug_hint} {context.path} should have 2+ children" assert {"opacity"} == set( context.element.attrib.keys() ), f"{debug_hint} {context.path} only attribute should be opacity. Found {context.element.attrib.keys()}" # insert reversed to undo the reversed at the top of loop paint = PaintComposite( mode=CompositeMode.SRC_IN, source=PaintColrLayers(tuple(reversed(child_nodes))), backdrop=PaintSolid(Color(0, 0, 0, opacity)), ) layers[context.depth() - 1].append(paint) assert defs_seen, f"{debug_hint} we never saw defs, what's up with that?!" if not layers: return () assert len(layers) == 1, f"Unexpected layers: {[len(l) for l in layers]}" # undo the reversed at the top of loop layers = reversed(layers[0]) return tuple(layers)
def _color_stop(stop_el, shape_opacity=1.0) -> ColorStop: offset = number_or_percentage(stop_el.attrib.get("offset", "0")) color = Color.fromstring(stop_el.attrib.get("stop-color", "black")) opacity = number_or_percentage(stop_el.attrib.get("stop-opacity", "1")) color = color._replace(alpha=color.alpha * opacity * shape_opacity) return ColorStop(stopOffset=offset, color=color)
) if is_transform(paint): return transformed(paint.gettransform().round(prec), paint.paint) return paint @pytest.mark.parametrize( "svg_in, expected_paints", [ # solid ( "rect.svg", ( PaintGlyph( glyph="M2,2 L8,2 L8,4 L2,4 L2,2 Z", paint=PaintSolid(color=Color.fromstring("blue")), ), PaintGlyph( glyph="M4,4 L10,4 L10,6 L4,6 L4,4 Z", paint=PaintSolid( color=Color.fromstring("blue", alpha=0.8)), ), ), ), # linear ( "linear_gradient_rect.svg", (PaintGlyph( glyph="M2,2 L8,2 L8,4 L2,4 L2,2 Z", paint=PaintLinearGradient( stops=(
class ColorStop: stopOffset: float = 0.0 color: Color = Color.fromstring("black")
) color_glyph = ColorGlyph.create( _ufo(upem), "duck", 1, [0x0042], SVG.fromstring(svg_str) ) assert color_glyph.transform_for_font_space() == expected_transform @pytest.mark.parametrize( "svg_in, expected_paints", [ # solid ( "rect.svg", { PaintSolid(color=Color.fromstring("blue")), PaintSolid(color=Color.fromstring("blue", alpha=0.8)), }, ), # linear ( "linear_gradient_rect.svg", { PaintLinearGradient( stops=( ColorStop(stopOffset=0.1, color=Color.fromstring("blue")), ColorStop(stopOffset=0.9, color=Color.fromstring("cyan")), ) ) }, ),
# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from nanoemoji.colors import Color import pytest @pytest.mark.parametrize( "color_string, expected_color", [ # 3-hex digits ("#BCD", Color(0xBB, 0xCC, 0xDD, 1.0)), # 4-hex digits ("#BCD3", Color(0xBB, 0xCC, 0xDD, 0.2)), # 6-hex digits ("#F1E2D3", Color(0xF1, 0xE2, 0xD3, 1.0)), # 8-hex digits ("#F1E2D366", Color(0xF1, 0xE2, 0xD3, 0.4)), # CSS named color ("wheat", Color(0xF5, 0xDE, 0xB3, 1.0)), # rgb(r,g,b) ("rgb(0, 256, -1)", Color(0, 255, 0, 1.0)), # rgb(r g b) ("rgb(42 101 43)", Color(42, 101, 43, 1.0)), # extra whitespace as found in the noto-emoji Luxembourg flag ("#00A1DE\n", Color(0, 161, 222, 1.0)), ],
def test_color_fromstring(color_string, expected_color): assert expected_color == Color.fromstring(color_string)
# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from nanoemoji.colors import Color import pytest @pytest.mark.parametrize( "color_string, expected_color", [ # 3-hex digits ("#BCD", Color(0xBB, 0xCC, 0xDD, 1.0)), # 4-hex digits ("#BCD3", Color(0xBB, 0xCC, 0xDD, 0.2)), # 6-hex digits ("#F1E2D3", Color(0xF1, 0xE2, 0xD3, 1.0)), # 8-hex digits ("#F1E2D366", Color(0xF1, 0xE2, 0xD3, 0.4)), # CSS named color ("wheat", Color(0xF5, 0xDE, 0xB3, 1.0)), # rgb(r,g,b) ("rgb(0, 256, -1)", Color(0, 255, 0, 1.0)), # rgb(r g b) ("rgb(42 101 43)", Color(42, 101, 43, 1.0)), ], ) def test_color_fromstring(color_string, expected_color):