Skip to content

Commit f3966bf

Browse files
committed
fix texts and rotation using freetype and correct rotation methods
1 parent 6d3098b commit f3966bf

File tree

3 files changed

+259
-30
lines changed

3 files changed

+259
-30
lines changed

examples/rotation.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Script taken from matplotlib examples to test rotation modes in text rendering."""
2+
import matplotlib.pyplot as plt
3+
4+
import matplotlib
5+
import logging
6+
7+
logging.basicConfig()
8+
logger = logging.getLogger("pygame_matplotlib")
9+
# logger.setLevel(logging.DEBUG)
10+
matplotlib.use("pygame")
11+
12+
13+
def test_rotation_mode(fig, mode):
14+
ha_list = ["left", "center", "right"]
15+
va_list = ["top", "center", "baseline", "bottom"]
16+
axs = fig.subplots(
17+
len(va_list),
18+
len(ha_list),
19+
sharex=True,
20+
sharey=True,
21+
subplot_kw=dict(aspect=1),
22+
gridspec_kw=dict(hspace=0, wspace=0),
23+
)
24+
25+
# labels and title
26+
for ha, ax in zip(ha_list, axs[-1, :]):
27+
ax.set_xlabel(ha)
28+
for va, ax in zip(va_list, axs[:, 0]):
29+
ax.set_ylabel(va)
30+
axs[0, 1].set_title(f"rotation_mode='{mode}'", size="large")
31+
32+
kw = (
33+
{}
34+
if mode == "default"
35+
else {"bbox": dict(boxstyle="square,pad=0.", ec="none", fc="C1", alpha=0.3)}
36+
)
37+
38+
texts = {}
39+
40+
# use a different text alignment in each Axes
41+
for i, va in enumerate(va_list):
42+
for j, ha in enumerate(ha_list):
43+
ax = axs[i, j]
44+
# prepare Axes layout
45+
ax.set(xticks=[], yticks=[])
46+
ax.axvline(0.5, color="skyblue", zorder=0)
47+
ax.axhline(0.5, color="skyblue", zorder=0)
48+
ax.plot(0.5, 0.5, color="C0", marker="o", zorder=1)
49+
# add text with rotation and alignment settings
50+
tx = ax.text(
51+
0.5,
52+
0.5,
53+
"Tpg",
54+
size="x-large",
55+
rotation=90,
56+
horizontalalignment=ha,
57+
verticalalignment=va,
58+
rotation_mode=mode,
59+
**kw,
60+
)
61+
texts[ax] = tx
62+
63+
if mode == "default":
64+
# highlight bbox
65+
fig.canvas.draw()
66+
for ax, text in texts.items():
67+
bb = text.get_window_extent().transformed(ax.transData.inverted())
68+
rect = plt.Rectangle(
69+
(bb.x0, bb.y0), bb.width, bb.height, facecolor="C1", alpha=0.3, zorder=2
70+
)
71+
ax.add_patch(rect)
72+
73+
74+
fig = plt.figure(figsize=(8, 5))
75+
subfigs = fig.subfigures(1, 2)
76+
test_rotation_mode(subfigs[0], "default")
77+
test_rotation_mode(subfigs[1], "anchor")
78+
plt.show()

examples/text.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Taken from the matplotlib text example."""
2+
3+
import matplotlib.pyplot as plt
4+
import logging
5+
6+
# Set up logging
7+
logging.basicConfig()
8+
logger = logging.getLogger("pygame_matplotlib")
9+
logger.setLevel(logging.DEBUG)
10+
11+
import matplotlib
12+
# matplotlib.use("pygame")
13+
14+
fig = plt.figure()
15+
ax = fig.add_subplot()
16+
fig.subplots_adjust(top=0.85)
17+
18+
# Set titles for the figure and the subplot respectively
19+
fig.suptitle("bold figure suptitle", fontsize=14, fontweight="bold")
20+
ax.set_title("axes title")
21+
22+
ax.set_xlabel("xlabel")
23+
ax.set_ylabel("ylabel")
24+
25+
# Set both x- and y-axis limits to [0, 10] instead of default [0, 1]
26+
ax.axis([0, 10, 0, 10])
27+
28+
ax.text(
29+
3,
30+
8,
31+
"boxed italics text in data coords",
32+
style="italic",
33+
bbox={"facecolor": "red", "alpha": 0.5, "pad": 10},
34+
)
35+
36+
ax.text(2, 6, r"an equation: $E=mc^2$", fontsize=15)
37+
38+
ax.text(3, 2, "Unicode: Institut für Festkörperphysik")
39+
40+
41+
ax.text(3, 5, "aaa", rotation=90)
42+
ax.text(3, 5, "bbb", rotation=135)
43+
44+
ax.text(
45+
0.95,
46+
0.01,
47+
"colored text in axes coords",
48+
verticalalignment="bottom",
49+
horizontalalignment="right",
50+
transform=ax.transAxes,
51+
color="green",
52+
fontsize=15,
53+
)
54+
55+
ax.plot([2], [1], "o")
56+
ax.annotate(
57+
"annotate",
58+
xy=(2, 1),
59+
xytext=(3, 4),
60+
arrowprops=dict(facecolor="black", shrink=0.05),
61+
)
62+
63+
plt.show()

pygame_matplotlib/backend_pygame.py

Lines changed: 118 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from matplotlib.transforms import Affine2D
1919
import pygame
2020
import pygame.image
21+
import pygame.freetype
2122
from pygame import gfxdraw
2223
from matplotlib._pylab_helpers import Gcf
2324
from matplotlib.backend_bases import (
@@ -239,55 +240,142 @@ def draw_image(self, gc, x, y, im):
239240
)
240241

241242
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
242-
243-
logger.info(
243+
logger.debug(
244244
f"Drawing text: {s=} at ({x=}, {y=}) with {angle=} and ismath={ismath} "
245245
f"{mtext=} {prop=} {gc=}"
246246
)
247+
247248
# make sure font module is initialized
248-
if not pygame.font.get_init():
249-
pygame.font.init()
249+
if not pygame.freetype.get_init():
250+
pygame.freetype.init()
251+
250252
# prop is the font properties
251-
font_size = prop.get_size_in_points() * 2
252-
myfont = pygame.font.Font(prop.get_file(), int(font_size))
253+
# Size must be adjusted
254+
font_size = prop.get_size() * 1.42
255+
font_file = prop.get_file()
256+
logger.debug(f"Font file: {font_file}, size: {font_size}")
257+
258+
# Set bold
259+
# 'light', 'normal', 'regular', 'book',
260+
#' medium', 'roman', 'semibold', 'demibold', 'demi', 'bold',
261+
# 'heavy', 'extra bold', 'black'
262+
font_weight = prop.get_weight()
263+
if isinstance(font_weight, int):
264+
# If the weight is an int, we assume it is a font weight
265+
# in the range 0-1000
266+
bold = font_weight >= 600
267+
else:
268+
bold = font_weight in ["bold", "heavy", "black", "extra bold"]
269+
logger.debug(f"Font weight: {font_weight}, bold: {bold}")
270+
271+
# Set italic
272+
# style can be 'normal', 'italic' or 'oblique'
273+
font_style = prop.get_style()
274+
italic = font_style in ["italic", "oblique"]
275+
276+
if font_file is None:
277+
# Use default matplotlib font
278+
font_file = "DejaVuSans"
279+
280+
pgfont = pygame.freetype.SysFont(
281+
font_file, int(font_size), bold=bold, italic=italic
282+
)
283+
284+
logger.debug(f"Font: {pgfont}")
285+
253286
# apply it to text on a label
254-
font_surface = myfont.render(
255-
s, gc.get_antialiased(), [val * 255 for val in gc.get_rgb()]
287+
fg_color = [val * 255 for val in gc.get_rgb()]
288+
289+
# Use freetype to render the text
290+
font_surface, rotated_rect = pgfont.render(
291+
s,
292+
fg_color,
293+
size=font_size,
294+
rotation=int(angle),
256295
)
257-
if angle:
258-
font_surface = pygame.transform.rotate(font_surface, angle)
259-
296+
no_rotation_rect = pgfont.get_rect(s, size=font_size)
297+
260298
# Get the expected size of the font
261-
width, height = myfont.size(s)
299+
# width, height = (pgfont.get_rect(s).size if USE_FREETYPE else pgfont.size(s))
300+
width, height = rotated_rect.size
301+
262302
# Tuple for the position of the font
263303
font_surf_position = (x, self.surface.get_height() - y)
264304
if mtext is not None:
265305
# Use the alignement from mtext or default
266306
h_alignment = mtext.get_horizontalalignment()
267307
v_alignment = mtext.get_verticalalignment()
308+
rotation_mode = mtext.get_rotation_mode()
268309
else:
269310
h_alignment = "center"
270311
v_alignment = "center"
312+
313+
logger.debug(
314+
f"{h_alignment=}, {v_alignment=}, {rotation_mode=}, {width=}, {height=}"
315+
)
271316
# Use the alignement to know where the font should go
272-
if h_alignment == "left":
273-
h_offset = 0
274-
elif h_alignment == "center":
275-
h_offset = -width / 2
276-
elif h_alignment == "right":
277-
h_offset = -width
278-
else:
279-
h_offset = 0
280-
281-
if v_alignment == "top":
282-
v_offset = 0
283-
elif v_alignment == "center" or v_alignment == "center_baseline":
284-
v_offset = -height / 2
285-
elif v_alignment == "bottom" or v_alignment == "baseline":
286-
v_offset = -height
317+
if rotation_mode == "anchor":
318+
# Anchor the text to the position
319+
sin_a = np.sin(np.radians(angle))
320+
cos_a = np.cos(np.radians(angle))
321+
322+
sub_width, sub_height = no_rotation_rect.size
323+
324+
if h_alignment == "left":
325+
if v_alignment == "top":
326+
h_offset = 0
327+
v_offset = sub_width * sin_a
328+
elif v_alignment == "center":
329+
h_offset = sub_height * sin_a / 2
330+
v_offset = height - sub_height * cos_a / 2
331+
else: # "bottom" or "baseline"
332+
h_offset = sub_height * sin_a
333+
v_offset = height
334+
elif h_alignment == "center":
335+
if v_alignment == "top":
336+
h_offset = sub_width / 2 * cos_a
337+
v_offset = sub_width / 2 * sin_a
338+
elif v_alignment == "center":
339+
h_offset = width / 2
340+
v_offset = height / 2
341+
else:
342+
h_offset = width - (sub_width / 2 * cos_a)
343+
v_offset = height - sub_width / 2 * sin_a
344+
elif h_alignment == "right":
345+
if v_alignment == "top":
346+
h_offset = width - sub_height * sin_a
347+
v_offset = 0
348+
elif v_alignment == "center":
349+
h_offset = width - (sub_height / 2 * sin_a)
350+
v_offset = sub_height / 2 * cos_a
351+
else:
352+
h_offset = width
353+
v_offset = cos_a * sub_height
354+
else:
355+
raise ValueError(f"Unknown {h_alignment=}")
356+
h_offset, v_offset = -h_offset, -v_offset
357+
287358
else:
288-
v_offset = 0
289-
# pygame.draw.circle(self.surface, (255, 0, 0), (x, self.surface.get_height() - y), 3)
290-
# pygame.draw.lines(self.surface, (0, 255, 0), True, ((x, self.surface.get_height() - y), (x + width, self.surface.get_height() - y), (x + width, self.surface.get_height() - y + height)))
359+
# The text box is aligned to the position
360+
361+
if h_alignment == "left":
362+
h_offset = 0
363+
elif h_alignment == "center":
364+
h_offset = -width / 2
365+
elif h_alignment == "right":
366+
h_offset = -width
367+
else:
368+
h_offset = 0
369+
370+
if v_alignment == "top":
371+
v_offset = 0
372+
elif v_alignment == "center" or v_alignment == "center_baseline":
373+
v_offset = -height / 2
374+
elif v_alignment == "bottom" or v_alignment == "baseline":
375+
v_offset = -height
376+
else:
377+
v_offset = 0
378+
291379
# Tuple for the position of the font
292380
font_surf_position = (
293381
x + h_offset,

0 commit comments

Comments
 (0)