diff --git a/doc/api/typing_api.rst b/doc/api/typing_api.rst index 0eafe849a0d0..8cd4023167e6 100644 --- a/doc/api/typing_api.rst +++ b/doc/api/typing_api.rst @@ -25,6 +25,7 @@ Artist styles ============= .. autodata:: matplotlib.typing.BlendModeType +.. autodata:: matplotlib.typing.FillRuleType .. autodata:: matplotlib.typing.LineStyleType .. autodata:: matplotlib.typing.DrawStyleType .. autodata:: matplotlib.typing.MarkEveryType diff --git a/doc/release/next_whats_new/fill_rules.rst b/doc/release/next_whats_new/fill_rules.rst new file mode 100644 index 000000000000..ccbc3669d535 --- /dev/null +++ b/doc/release/next_whats_new/fill_rules.rst @@ -0,0 +1,10 @@ +Option to use the even-odd fill rule for patches +------------------------------------------------ + +By default, patches such as `~.patches.Polygon` are filled according to the +`non-zero winding fill rule `__. +There is now the option to instead use the +`even-odd fill rule `__, +which is specified by setting the patch's ``fill_rule`` property to "evenodd". +See :doc:`/gallery/shapes_and_collections/fill_rule_demo` for more details and +an illustration of the difference. diff --git a/galleries/examples/shapes_and_collections/fill_rule_demo.py b/galleries/examples/shapes_and_collections/fill_rule_demo.py new file mode 100644 index 000000000000..09782401d1b0 --- /dev/null +++ b/galleries/examples/shapes_and_collections/fill_rule_demo.py @@ -0,0 +1,67 @@ +""" +============== +Fill rule demo +============== + +By default, patches such as `~.patches.Polygon` are filled according to the +`non-zero winding fill rule `__. +Any given point has a winding number, which is the number of times the path +wraps around the point in the clockwise direction. For this fill rule, the +filled regions are where the winding number is non-zero. See +:doc:`/gallery/shapes_and_collections/donut` for an example of how to leverage +the winding directions in multiple segments of a path under this fill rule. + +The other option for fill rule is the +`even-odd fill rule `__, +which is specified by setting the patch's ``fill_rule`` property to "evenodd". +For this fill rule, the filled regions are where the winding number is an odd +number. This fill rule allows for the construction of patterns of fill +regions that would otherwise take many more vertices to construct under the +non-zero winding fill rule. + +This example demonstrates the difference between the two fill rules for a single +`~.patches.Polygon` that intersects itself multiple times. The winding number +for each closed region is labeled. + +""" + +import matplotlib.pyplot as plt +import numpy as np + +from matplotlib.patches import Polygon + +fig, axs = plt.subplots(1, 2) + +vertices = np.array([[0, 0, 6, 6, 1, 1, 5, 5, 2, 2, 4, 4, 3, 3, 5, 5], + [2, 5, 5, 0, 0, 7, 7, 3, 3, 4, 4, 6, 6, 1, 1, 2]]).T + +labels = ['1', '0', '1', '2', '3', '2', '1', '1', '0'] +label_xys = np.array([[2.0, 4.0, 0.5, 1.5, 2.5, 4.0, 3.5, 2.0, 3.5], + [1.5, 1.5, 3.5, 3.5, 3.5, 3.5, 4.5, 5.5, 5.5]]).T + +for ax, fill_rule in zip(axs, ['nonzero', 'evenodd']): + polygon = Polygon(vertices, facecolor='green', edgecolor='red', + fill_rule=fill_rule) + ax.add_patch(polygon) + + ax.plot(*vertices.T, '.', markersize=10, color='red') + + for label, label_xy in zip(labels, label_xys): + ax.text(*label_xy, label, ha='center', va='center') + + ax.set_axis_off() + ax.set_title(f'fill_rule={fill_rule}') + +plt.show() + +# %% +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.patches` +# - `matplotlib.patches.Polygon` +# - `matplotlib.axes.Axes.add_patch` +# - `matplotlib.patches.Patch.set_fill_rule` diff --git a/galleries/tutorials/artists.py b/galleries/tutorials/artists.py index 21ba6ee0a9de..08f65079fd89 100644 --- a/galleries/tutorials/artists.py +++ b/galleries/tutorials/artists.py @@ -205,6 +205,7 @@ class in the Matplotlib API, and the one you will be working with most # animated = False # antialiased or aa = False # bbox = Bbox(x0=0.0, y0=0.0, x1=1.0, y1=1.0) +# blend_mode = normal # capstyle = butt # children = [] # clip_box = None @@ -217,6 +218,7 @@ class in the Matplotlib API, and the one you will be working with most # facecolor or fc = (1.0, 1.0, 1.0, 1.0) # figure = Figure(640x480) # fill = True +# fill_rule = nonzero # gid = None # hatch = None # height = 1 diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 5123817507e0..7b7947b2cf90 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -245,6 +245,7 @@ def __init__(self): self._animated = False self._alpha = None self._blend_mode = "normal" + self._fill_rule = "nonzero" self.clipbox = None self._clippath = None self._clipon = True @@ -1278,6 +1279,7 @@ def update_from(self, other): self._visible = other._visible self._alpha = other._alpha self._blend_mode = other._blend_mode + self._fill_rule = other._fill_rule self.clipbox = other.clipbox self._clipon = other._clipon self._clippath = other._clippath diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d73dd81c32fc..ddb1425b02cf 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -757,6 +757,7 @@ def __init__(self): self._alpha = 1.0 self._forced_alpha = False # if True, _alpha overrides A from RGBA self._blend_mode = "normal" + self._fill_rule = "nonzero" self._antialiased = 1 # use 0, 1 not True, False for extension code self._capstyle = CapStyle('butt') self._cliprect = None @@ -779,6 +780,7 @@ def copy_properties(self, gc): self._alpha = gc._alpha self._forced_alpha = gc._forced_alpha self._blend_mode = gc._blend_mode + self._fill_rule = gc._fill_rule self._antialiased = gc._antialiased self._capstyle = gc._capstyle self._cliprect = gc._cliprect @@ -813,6 +815,9 @@ def get_blend_mode(self): """Return the blend mode for compositing - not supported on all backends.""" return self._blend_mode + def get_fill_rule(self): + return self._fill_rule + def get_antialiased(self): """Return whether the object should try to do antialiased rendering.""" return self._antialiased @@ -924,6 +929,9 @@ def set_blend_mode(self, blend_mode): # Backend-independent input validation is done in Artist.set_blend_mode() self._blend_mode = blend_mode + def set_fill_rule(self, fill_rule): + self._fill_rule = fill_rule + def set_antialiased(self, b): """Set whether object should be drawn with antialiased rendering.""" # Use ints to make life easier on extension code trying to read the gc. diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 399607063f15..64cea7f8da7f 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -6,7 +6,7 @@ from matplotlib import ( widgets, _api, ) -from matplotlib.artist import Artist, BlendMode +from matplotlib.artist import Artist from matplotlib.axes import Axes from matplotlib.backend_managers import ToolManager from matplotlib.backend_tools import Cursors, ToolBase @@ -27,6 +27,7 @@ from .typing import ( CloseEventType, ColorType, DrawEventType, + FillRuleType, JoinStyleType, KeyEventType, LineStyleType, @@ -159,6 +160,7 @@ class GraphicsContextBase: def restore(self) -> None: ... def get_alpha(self) -> float: ... def get_blend_mode(self) -> str: ... + def get_fill_rule(self) -> FillRuleType: ... def get_antialiased(self) -> int: ... def get_capstyle(self) -> Literal["butt", "projecting", "round"]: ... def get_clip_rectangle(self) -> Bbox | None: ... @@ -174,7 +176,8 @@ class GraphicsContextBase: def get_gid(self) -> int | None: ... def get_snap(self) -> bool | None: ... def set_alpha(self, alpha: float) -> None: ... - def set_blend_mode(self, blend_mode: str | BlendMode) -> None: ... + def set_blend_mode(self, blend_mode: BlendModeType) -> None: ... + def set_fill_rule(self, fill_rule: FillRuleType) -> None: ... def set_antialiased(self, b: bool) -> None: ... def set_capstyle(self, cs: CapStyleType) -> None: ... def set_clip_rectangle(self, rectangle: Bbox | None) -> None: ... diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index c034949c6a07..be17fa693ae3 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -423,6 +423,11 @@ class GraphicsContextCairo(GraphicsContextBase): 'luminosity': cairo.OPERATOR_HSL_LUMINOSITY, } + _filld = { + 'nonzero': cairo.FILL_RULE_WINDING, + 'evenodd': cairo.FILL_RULE_EVEN_ODD, + } + def __init__(self, renderer): super().__init__() self.renderer = renderer @@ -501,6 +506,11 @@ def set_blend_mode(self, blend_mode): self.ctx.set_operator(_api.getitem_checked(self._operatord, blend_mode=self._blend_mode)) + def set_fill_rule(self, fill_rule): + super().set_fill_rule(fill_rule) + self.ctx.set_fill_rule(_api.getitem_checked(self._filld, + fill_rule=self._fill_rule)) + class _CairoRegion: def __init__(self, slices, data): diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 7e480f84fc67..67ff38fb325f 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -440,7 +440,9 @@ class Op(Enum): close_fill_stroke = b'b' fill_stroke = b'B' + fill_evenodd_stroke = b'B*' fill = b'f' + fill_evenodd = b'f*' closepath = b'h' close_stroke = b's' stroke = b'S' @@ -481,7 +483,7 @@ def pdfRepr(self): return self.value @classmethod - def paint_path(cls, fill, stroke): + def paint_path(cls, fill, stroke, *, fill_rule="nonzero"): """ Return the PDF operator to paint a path. @@ -494,11 +496,15 @@ def paint_path(cls, fill, stroke): """ if stroke: if fill: + if fill_rule == "evenodd": + return cls.fill_evenodd_stroke return cls.fill_stroke else: return cls.stroke else: if fill: + if fill_rule == "evenodd": + return cls.fill_evenodd return cls.fill else: return cls.endpath @@ -1983,7 +1989,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): path, transform, rgbFace is None and gc.get_hatch_path() is None, gc.get_sketch_params()) - self.file.output(self.gc.paint()) + self.file.output(self.gc.paint(fill_rule=gc._fill_rule)) def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, @@ -2489,12 +2495,12 @@ def fill(self, *args): (_fillcolor is not None and (len(_fillcolor) <= 3 or _fillcolor[3] != 0.0))) - def paint(self): + def paint(self, *, fill_rule="nonzero"): """ Return the appropriate pdf operator to cause the path to be stroked, filled, or both. """ - return Op.paint_path(self.fill(), self.stroke()) + return Op.paint_path(self.fill(), self.stroke(), fill_rule=fill_rule) capstyles = {'butt': 0, 'round': 1, 'projecting': 2} joinstyles = {'miter': 0, 'round': 1, 'bevel': 2} diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 06853b13dba7..7ea6eb09b557 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -556,8 +556,10 @@ def _print_pgf_path_styles(self, gc, rgbFace): r"\definecolor{currentfill}{rgb}{%f,%f,%f}" % tuple(rgbFace[:3])) _writeln(self.fh, r"\pgfsetfillcolor{currentfill}") - if has_fill and fillopacity != 1.0: - _writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity) + if fillopacity != 1.0: + _writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity) + if gc.get_fill_rule() == "evenodd": + _writeln(self.fh, r"\pgfseteorule") # linewidth and color lw = gc.get_linewidth() * mpl_pt_to_in * latex_in_to_pt diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 67a02b3fb064..7061ee6e2758 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -924,6 +924,7 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True): if self._is_transparent(rgbFace): fill = False hatch = gc.get_hatch() + fill_op = "eofill" if gc.get_fill_rule() == "evenodd" else "fill" if mightstroke: self.set_linewidth(gc.get_linewidth()) @@ -943,7 +944,7 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True): if stroke or hatch: write("gsave\n") self.set_color(*rgbFace[:3], store=False) - write("fill\n") + write(f"{fill_op}\n") if stroke or hatch: write("grestore\n") @@ -951,7 +952,7 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True): hatch_name = self.create_hatch(hatch, gc.get_hatch_linewidth()) write("gsave\n") write(_nums_to_str(*gc.get_hatch_color()[:3])) - write(f" {hatch_name} setpattern fill grestore\n") + write(f" {hatch_name} setpattern {fill_op} grestore\n") if stroke: write("stroke\n") diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 8373ffecb5b9..00790ab698de 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -620,6 +620,8 @@ def _get_style_dict(self, gc, rgbFace): attrib['opacity'] = _short_float_fmt(gc.get_alpha()) if (blend_mode := _svg_blend_mode(gc.get_blend_mode())) != "normal": attrib["mix-blend-mode"] = blend_mode + if (fill_rule := gc.get_fill_rule()) != "nonzero": + attrib["fill-rule"] = fill_rule offset, seq = gc.get_dashes() if seq is not None: diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index cae9e43a752a..c88d98f94a77 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -634,6 +634,29 @@ def get_hatch_linewidth(self): """Return the hatch linewidth.""" return self._hatch_linewidth + def set_fill_rule(self, fill_rule): + """ + Set the rule for filling a shape. + + See :doc:`/gallery/shapes_and_collections/fill_rule_demo`. + + Parameters + ---------- + fill_rule : {'nonzero', 'evenodd'} + 'nonzero' for the non-zero winding rule (the default), or + 'evenodd' for the even-odd rule + + References + ---------- + * `Wikipedia: Non-zero winding rule `__ + * `Wikipedia: Even-odd rule `__ + """ + _api.check_in_list(["nonzero", "evenodd"], fill_rule=fill_rule) + self._fill_rule = fill_rule + + def get_fill_rule(self): + return self._fill_rule + def _has_dashed_edge(self): """ Return whether the patch edge has a dashed linestyle. @@ -673,6 +696,7 @@ def _draw_paths_with_artist_properties( gc.set_alpha(self._alpha) gc.set_blend_mode(self.get_blend_mode()) + gc.set_fill_rule(self.get_fill_rule()) if self._hatch: gc.set_hatch(self._hatch) diff --git a/lib/matplotlib/patches.pyi b/lib/matplotlib/patches.pyi index 60b03b0f1e0c..8dd128ec7462 100644 --- a/lib/matplotlib/patches.pyi +++ b/lib/matplotlib/patches.pyi @@ -8,7 +8,7 @@ from typing import Any, Literal, overload import numpy as np from numpy.typing import ArrayLike -from .typing import ColorType, LineStyleType, CapStyleType, JoinStyleType +from .typing import ColorType, FillRuleType, LineStyleType, CapStyleType, JoinStyleType class Patch(artist.Artist): zorder: float @@ -68,6 +68,8 @@ class Patch(artist.Artist): def set_hatch_linewidth(self, lw: float) -> None: ... def get_hatch_linewidth(self) -> float: ... def get_hatch(self) -> str: ... + def set_fill_rule(self, fill_rule: FillRuleType) -> None: ... + def get_fill_rule(self) -> FillRuleType: ... def get_path(self) -> Path: ... class Shadow(Patch): diff --git a/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.pdf b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.pdf new file mode 100644 index 000000000000..3882344bb9fb Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.png b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.png new file mode 100644 index 000000000000..746b706225f8 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.svg b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.svg new file mode 100644 index 000000000000..188ac12f3fd8 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules.svg @@ -0,0 +1,240 @@ + + + + + + + + 2026-09-03T23:38:18.563086 + image/svg+xml + + + Matplotlib v3.12.0.dev540+gbdec8821e.d20260904, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_cairo.png b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_cairo.png new file mode 100644 index 000000000000..0b0d1658518c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_cairo.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_pgf.pdf b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_pgf.pdf new file mode 100644 index 000000000000..d7ad8aa5b72b Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backends_rendering/fill_rules_pgf.pdf differ diff --git a/lib/matplotlib/tests/test_backends_rendering.py b/lib/matplotlib/tests/test_backends_rendering.py index 48d20054c92b..72de3238706c 100644 --- a/lib/matplotlib/tests/test_backends_rendering.py +++ b/lib/matplotlib/tests/test_backends_rendering.py @@ -11,7 +11,8 @@ from matplotlib.backends.backend_pgf import RendererPgf from matplotlib.backends.backend_svg import RendererSVG from matplotlib.figure import Figure -from matplotlib.patches import Circle, Rectangle +from matplotlib.patches import Circle, PathPatch, Polygon, Rectangle +from matplotlib.path import Path from matplotlib.testing._markers import needs_pgf_pdflatex from matplotlib.testing.decorators import image_comparison @@ -277,3 +278,56 @@ def test_group_invalid_blend_mode(renderer): with pytest.raises(ValueError, match="not a valid value for blend_mode"): renderer_instance.open_blend_group("invalid_blend_mode") + + +def plot_fill_rule_comparison(): + deg = np.arange(6) * 144 + x = np.sin(deg * np.pi / 180) + y = np.cos(deg * np.pi / 180) + star = np.stack([x, y], axis=1) + + square_vertices = np.array([[-1, -1], [-1, 1], [1, 1], [1, -1], [-1, -1]]) + square_codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO] + + fig, axs = plt.subplots(1, 2, figsize=(4, 5)) + + for ax, fill_rule in zip(axs, ['nonzero', 'evenodd']): + stroked_star = Polygon(star + [0, 4], closed=False, + ec='b', lw=5, ls=(0, (5, 1)), + fc='r', hatch='xx', fill_rule=fill_rule) + ax.add_patch(stroked_star) + + nonstroked_star = Polygon(star + [0, 2], closed=False, ec='none', + fc='r', hatch='xx', fill_rule=fill_rule) + ax.add_patch(nonstroked_star) + + squares = Path(np.vstack([square_vertices * 0.9, + square_vertices / 3 + [0, 0.5], + square_vertices / 3 + [0.3, 0], + (square_vertices / 3)[::-1, :] + [0, -0.5]]), + square_codes * 4) + + ax.add_patch(PathPatch(squares, fc='g', ec='m', fill_rule=fill_rule)) + + ax.set_xlim(-1, 1) + ax.set_ylim(-1, 5.1) + ax.set_aspect('equal') + ax.set_axis_off() + + +@image_comparison(['fill_rules'], extensions=['png', 'svg', 'pdf'], style='mpl20') +def test_fill_rules(): + plot_fill_rule_comparison() + + +@pytest.mark.backend('cairo') +@image_comparison(['fill_rules_cairo.png'], style='mpl20') +def test_fill_rules_cairo(): + plot_fill_rule_comparison() + + +@needs_pgf_pdflatex +@pytest.mark.backend('pgf') +@image_comparison(['fill_rules_pgf.pdf'], style='mpl20') +def test_fill_rules_pgf(): + plot_fill_rule_comparison() diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index 4b87915ce303..1e2bec055395 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -61,6 +61,9 @@ ) """Blend modes. See :ref:`blend-modes`.""" +type FillRuleType = Literal["nonzero", "evenodd"] +"""Fill rule options.""" + type LineStyleType = ( Literal["-", "solid", "--", "dashed", "-.", "dashdot", ":", "dotted", "", "none", " ", "None"] | diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 36b0dc49c20a..78b4b2fdfc2e 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -295,6 +295,8 @@ template inline void RendererAgg::_draw_path(path_t &path, bool has_clippath, const std::optional &face, GCAgg &gc) { + theRasterizer.filling_rule(gc.filling_rule); + // Render face if (face) { theRasterizer.add_path(path); @@ -380,6 +382,8 @@ RendererAgg::_draw_path(path_t &path, bool has_clippath, const std::optional struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::filling_rule_e, const_name("filling_rule_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"nonzero", agg::fill_non_zero}, + {"evenodd", agg::fill_even_odd}, + }; + value = enum_values.at(src.cast()); + return true; + } + }; + template <> struct type_caster { public: PYBIND11_TYPE_CASTER(agg::line_cap_e, const_name("line_cap_e")); @@ -278,6 +293,7 @@ namespace PYBIND11_NAMESPACE { namespace detail { value.alpha = src.attr("_alpha").cast(); value.forced_alpha = src.attr("_forced_alpha").cast(); value.comp_op = src.attr("_blend_mode").cast(); + value.filling_rule = src.attr("_fill_rule").cast(); value.color = src.attr("_rgb").cast(); value.isaa = src.attr("_antialiased").cast(); value.cap = src.attr("_capstyle").cast();