Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/users/next_whats_new/text_antialias_artist.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Per-artist text antialiasing control
------------------------------------

`.Text` instances now expose :meth:`~matplotlib.text.Text.set_antialiased`
and :meth:`~matplotlib.text.Text.get_antialiased`, allowing text artists to
override the global :rc:`text.antialiased` setting on a per-artist basis. The
new preference is propagated through `GraphicsContextBase` so backends such as
Agg and Cairo honor the per-text value.
10 changes: 10 additions & 0 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,7 @@ def __init__(self):
self._alpha = 1.0
self._forced_alpha = False # if True, _alpha overrides A from RGBA
self._antialiased = 1 # use 0, 1 not True, False for extension code
self._text_antialiased = None
self._capstyle = CapStyle('butt')
self._cliprect = None
self._clippath = None
Expand All @@ -784,6 +785,7 @@ def copy_properties(self, gc):
self._alpha = gc._alpha
self._forced_alpha = gc._forced_alpha
self._antialiased = gc._antialiased
self._text_antialiased = gc._text_antialiased
self._capstyle = gc._capstyle
self._cliprect = gc._cliprect
self._clippath = gc._clippath
Expand Down Expand Up @@ -817,6 +819,10 @@ def get_antialiased(self):
"""Return whether the object should try to do antialiased rendering."""
return self._antialiased

def get_text_antialiased(self):
"""Return whether text should be rendered with antialiasing."""
return self._text_antialiased

def get_capstyle(self):
"""Return the `.CapStyle`."""
return self._capstyle.name
Expand Down Expand Up @@ -912,6 +918,10 @@ def set_antialiased(self, b):
# Use ints to make life easier on extension code trying to read the gc.
self._antialiased = int(bool(b))

def set_text_antialiased(self, b):
"""Set whether text should be drawn with antialiased rendering."""
self._text_antialiased = None if b is None else int(bool(b))

@_docstring.interpd
def set_capstyle(self, cs):
"""
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/backend_bases.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class GraphicsContextBase:
def restore(self) -> None: ...
def get_alpha(self) -> float: ...
def get_antialiased(self) -> int: ...
def get_text_antialiased(self) -> int | None: ...
def get_capstyle(self) -> Literal["butt", "projecting", "round"]: ...
def get_clip_rectangle(self) -> Bbox | None: ...
def get_clip_path(
Expand All @@ -158,6 +159,7 @@ class GraphicsContextBase:
def get_snap(self) -> bool | None: ...
def set_alpha(self, alpha: float) -> None: ...
def set_antialiased(self, b: bool) -> None: ...
def set_text_antialiased(self, b: bool | None) -> None: ...
def set_capstyle(self, cs: CapStyleType) -> None: ...
def set_clip_rectangle(self, rectangle: Bbox | None) -> None: ...
def set_clip_path(self, path: TransformedPath | None) -> None: ...
Expand Down
9 changes: 7 additions & 2 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,13 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
# We pass '0' for angle here, since it will be rotated (in raster
# space) in the following call to draw_text_image).
font.set_text(s, 0, flags=get_hinting_flag())
font.draw_glyphs_to_bitmap(
antialiased=mpl.rcParams['text.antialiased'])
aa_gc = gc.get_text_antialiased()
text_aa = mtext.get_antialiased() if mtext is not None else None
antialiased = (
aa_gc if aa_gc is not None
else text_aa if text_aa is not None
else mpl.rcParams['text.antialiased'])
font.draw_glyphs_to_bitmap(antialiased=antialiased)
d = font.get_descent() / 64.0
# The descent needs to be adjusted for the angle.
xo, yo = font.get_bitmap_offset()
Expand Down
11 changes: 9 additions & 2 deletions lib/matplotlib/backends/backend_cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,16 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
ctx.select_font_face(*_cairo_font_args_from_font_prop(prop))
ctx.set_font_size(self.points_to_pixels(prop.get_size_in_points()))
opts = cairo.FontOptions()
gc_aa = gc.get_text_antialiased()
if gc_aa is not None:
aa = bool(gc_aa)
else:
text_aa = (mtext.get_antialiased()
if mtext is not None else None)
aa = (text_aa if text_aa is not None
else mpl.rcParams['text.antialiased'])
opts.set_antialias(
cairo.ANTIALIAS_DEFAULT if mpl.rcParams["text.antialiased"]
else cairo.ANTIALIAS_NONE)
cairo.ANTIALIAS_DEFAULT if aa else cairo.ANTIALIAS_NONE)
ctx.set_font_options(opts)
if angle:
ctx.rotate(np.deg2rad(-angle))
Expand Down
73 changes: 71 additions & 2 deletions lib/matplotlib/tests/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from matplotlib.testing.decorators import check_figures_equal, image_comparison
from matplotlib.testing._markers import needs_usetex
from matplotlib.text import Text
from matplotlib.backends.backend_agg import RendererAgg


@image_comparison(['font_styles'])
Expand Down Expand Up @@ -197,6 +198,74 @@ def test_antialiasing():
# rendered.


def _draw_with_renderer(renderer, text_obj, *, x=0, y=0):
gc = renderer.new_gc()
try:
renderer.draw_text(
gc,
x,
y,
text_obj.get_text(),
text_obj.get_fontproperties(),
0,
mtext=text_obj,
)
finally:
gc.restore()


def test_text_antialias_override_rendereragg():
def render_with(text_config, rc_value):
renderer = RendererAgg(200, 200, 72)
text_obj = Text(50, 100, "override", fontsize=48)
text_config(text_obj)
with mpl.rc_context({'text.antialiased': rc_value}):
_draw_with_renderer(renderer, text_obj, x=50, y=100)
alpha = np.unique(np.asarray(renderer.buffer_rgba())[..., 3])
return [int(v) for v in alpha]

aa_false = render_with(lambda text: text.set_antialiased(False), True)
assert aa_false == [0, 255]

aa_true = render_with(lambda text: text.set_antialiased(True), False)
assert len(aa_true) > 2


def test_text_antialias_none_falls_back():
def render_with(text_config):
renderer = RendererAgg(200, 200, 72)
text_obj = Text(50, 100, "fallback", fontsize=48)
text_config(text_obj)
with mpl.rc_context({'text.antialiased': False}):
_draw_with_renderer(renderer, text_obj, x=50, y=100)
alpha = np.unique(np.asarray(renderer.buffer_rgba())[..., 3])
return [int(v) for v in alpha]

fallback_false = render_with(lambda text: text.set_antialiased(False))

def configure_none(text):
text.set_antialiased(True)
text.set_antialiased(None)

fallback_none = render_with(configure_none)
assert fallback_none == fallback_false


def test_rendereragg_text_antialias_respects_gc():
renderer = RendererAgg(200, 200, 72)
gc = renderer.new_gc()
try:
gc.set_text_antialiased(False)
font_props = FontProperties(size=48)
with mpl.rc_context({'text.antialiased': True}):
renderer.draw_text(gc, 50, 100, "gc", font_props, 0, mtext=None)
finally:
gc.restore()

alpha = np.unique(np.asarray(renderer.buffer_rgba())[..., 3])
assert alpha.tolist() == [0, 255]


def test_afm_kerning():
fn = mpl.font_manager.findfont("Helvetica", fontext="afm")
with open(fn, 'rb') as fh:
Expand Down Expand Up @@ -824,15 +893,15 @@ def test_parse_math():
fig.canvas.draw()

ax.text(0, 0, r"$ \wrong{math} $", parse_math=True)
with pytest.raises(ValueError, match='Unknown symbol'):
with pytest.raises(ValueError, match=r'(Unknown symbol|Expected token)'):
fig.canvas.draw()


def test_parse_math_rcparams():
# Default is True
fig, ax = plt.subplots()
ax.text(0, 0, r"$ \wrong{math} $")
with pytest.raises(ValueError, match='Unknown symbol'):
with pytest.raises(ValueError, match=r'(Unknown symbol|Expected token)'):
fig.canvas.draw()

# Setting rcParams to False
Expand Down
31 changes: 30 additions & 1 deletion lib/matplotlib/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ def _get_text_metrics_with_cache_impl(
"fontweight": ["weight"],
})
class Text(Artist):
"""Handle storing and drawing of text in window or data coordinates."""
"""
Handle storing and drawing of text in window or data coordinates.

The ``antialiased`` property controls per-artist text antialiasing via
:meth:`set_antialiased`.
"""

zorder = 3
_charsize_cache = dict()
Expand Down Expand Up @@ -183,6 +188,7 @@ def _reset_visual_defaults(
self._transform_rotates_text = transform_rotates_text
self._bbox_patch = None # a FancyBboxPatch instance
self._renderer = None
self._text_antialiased = None
if linespacing is None:
linespacing = 1.2 # Maybe use rcParam later.
self.set_linespacing(linespacing)
Expand Down Expand Up @@ -322,6 +328,7 @@ def update_from(self, other):
self._transform_rotates_text = other._transform_rotates_text
self._picker = other._picker
self._linespacing = other._linespacing
self._text_antialiased = other._text_antialiased
self.stale = True

def _get_layout(self, renderer):
Expand Down Expand Up @@ -739,6 +746,12 @@ def draw(self, renderer):
gc.set_url(self._url)
self._set_gc_clip(gc)

text_aa = self.get_antialiased()
resolved_text_aa = (
text_aa if text_aa is not None
else mpl.rcParams['text.antialiased'])
gc.set_text_antialiased(resolved_text_aa)

angle = self.get_rotation()

for line, wh, x, y in info:
Expand Down Expand Up @@ -770,6 +783,22 @@ def draw(self, renderer):
renderer.close_group('text')
self.stale = False

def set_antialiased(self, aa):
"""Set whether the text should use antialiased rendering.

Parameters
----------
aa : bool or None
If ``None``, defer to :rc:`text.antialiased`.
"""
_api.check_isinstance((bool, type(None)), antialiased=aa)
self._text_antialiased = None if aa is None else bool(aa)
self.stale = True

def get_antialiased(self):
"""Return the per-artist antialiasing setting."""
return self._text_antialiased

def get_color(self):
"""Return the color of the text."""
return self._color
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/text.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Text(Artist):
def get_wrap(self) -> bool: ...
def set_wrap(self, wrap: bool) -> None: ...
def get_color(self) -> ColorType: ...
def get_antialiased(self) -> bool | None: ...
def get_fontproperties(self) -> FontProperties: ...
def get_fontfamily(self) -> list[str]: ...
def get_fontname(self) -> str: ...
Expand All @@ -69,6 +70,7 @@ class Text(Artist):
) -> Bbox: ...
def set_backgroundcolor(self, color: ColorType) -> None: ...
def set_color(self, color: ColorType) -> None: ...
def set_antialiased(self, aa: bool | None) -> None: ...
def set_horizontalalignment(
self, align: Literal["left", "center", "right"]
) -> None: ...
Expand Down