Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 42b9388

Browse files
committed
Rasterize dvi files without dvipng.
This patch drops the reliance on dvipng to rasterize dvi files prior to inclusion by agg, instead performing the rasterization ourselves (as a consequence, the rasterization output also becomes dependent of the freetype version used). Note that this approach will be needed anyways to support xetex and luatex, as dvipng doesn't support dvi files generated by these engines. Baseline images change slightly, for the better or the worse. The top-left blue cross text in test_rotation.py ("Myrt0") seems to be better top-aligned against the blue line (the old version overshot a bit); the bounding box of the formulas in test_usetex.py seems a bit worse.
1 parent d0df260 commit 42b9388

4 files changed

Lines changed: 91 additions & 10 deletions

File tree

lib/matplotlib/backends/backend_agg.py

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from matplotlib import _api, cbook
3232
from matplotlib.backend_bases import (
3333
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
34+
from matplotlib.dviread import Dvi
3435
from matplotlib.font_manager import fontManager as _fontManager, get_font
3536
from matplotlib.ft2font import LoadFlags, RenderMode
3637
from matplotlib.mathtext import MathTextParser
@@ -266,19 +267,74 @@ def get_text_width_height_descent(self, s, prop, ismath):
266267
def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
267268
# docstring inherited
268269
# todo, handle props, angle, origins
270+
269271
size = prop.get_size_in_points()
270272

271-
texmanager = self.get_texmanager()
273+
if mpl.rcParams["text.latex.engine"] == "latex+dvipng":
274+
Z = self.get_texmanager().get_grey(s, size, self.dpi)
275+
Z = (Z * 0xff).astype(np.uint8)
276+
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
277+
xd = d * math.sin(math.radians(angle))
278+
yd = d * math.cos(math.radians(angle))
279+
x = round(x + xd)
280+
y = round(y + yd)
281+
self._renderer.draw_text_image(Z, x, y, angle, gc)
282+
return
283+
284+
dvifile = self.get_texmanager().make_dvi(s, size)
285+
with Dvi(dvifile, self.dpi) as dvi:
286+
page, = dvi
287+
w = math.ceil(page.width)
288+
h = math.ceil(page.height)
289+
d = math.ceil(page.descent)
272290

273-
Z = texmanager.get_grey(s, size, self.dpi)
274-
Z = np.array(Z * 255.0, np.uint8)
291+
cos = math.cos(math.radians(angle))
292+
sin = math.sin(math.radians(angle))
275293

276-
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
277-
xd = d * math.sin(math.radians(angle))
278-
yd = d * math.cos(math.radians(angle))
279-
x = round(x + xd)
280-
y = round(y + yd)
281-
self._renderer.draw_text_image(Z, x, y, angle, gc)
294+
for text in page.text:
295+
hf = mpl.rcParams["text.hinting_factor"]
296+
font = get_font(text.font_path)
297+
font.set_size(text.font_size, self.dpi)
298+
slant = text.font_effects.get("slant", 0)
299+
extend = text.font_effects.get("extend", 1)
300+
font._set_transform(
301+
(0x10000 * np.array([[cos, -sin], [sin, cos]])
302+
@ [[extend, extend * slant], [0, 1]]
303+
@ [[1 / hf, 0], [0, 1]]).round().astype(int),
304+
[round(0x40 * (x + text.x * cos - text.y * sin)),
305+
# FreeType's y is upwards.
306+
round(0x40 * (self.height - y + text.x * sin + text.y * cos))]
307+
)
308+
bitmap = font._render_glyph(
309+
text.index, get_hinting_flag(),
310+
RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO)
311+
buffer = np.asarray(bitmap.buffer)
312+
if not gc.get_antialiased():
313+
buffer *= 0xff
314+
# draw_text_image's y is downwards & the bitmap bottom side.
315+
self._renderer.draw_text_image(
316+
buffer,
317+
bitmap.left, int(self.height) - bitmap.top + buffer.shape[0],
318+
0, gc)
319+
320+
rgba = gc.get_rgb()
321+
if len(rgba) == 3 or gc.get_forced_alpha():
322+
rgba = rgba[:3] + (gc.get_alpha(),)
323+
gc1 = self.new_gc()
324+
gc1.set_linewidth(0)
325+
gc1.set_snap(gc.get_snap())
326+
for box in page.boxes:
327+
path = Path._create_closed([
328+
(box.x, box.y),
329+
(box.x + box.width, box.y),
330+
(box.x + box.width, box.y + box.height),
331+
(box.x, box.y + box.height)])
332+
self._renderer.draw_path(
333+
gc1, path,
334+
mpl.transforms.Affine2D()
335+
.rotate_deg(angle).translate(x, self.height - y),
336+
rgba)
337+
gc1.restore()
282338

283339
def get_canvas_width_height(self):
284340
# docstring inherited

lib/matplotlib/mpl-data/matplotlibrc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,16 @@
331331
# zapf chancery, charter, serif, sans-serif, helvetica,
332332
# avant garde, courier, monospace, computer modern roman,
333333
# computer modern sans serif, computer modern typewriter
334+
335+
## The TeX engine/format to use. The following values are supported:
336+
## - "latex": The classic TeX engine (the current default). All backends render
337+
## TeX's output by parsing the DVI output into glyphs and boxes and emitting
338+
## those one by one.
339+
## - "latex+dvipng": The same as "tex", with the exception that Agg-based backends
340+
## rely on dvipng to rasterize TeX's output. This value was the default up to
341+
## Matplotlib 3.10.
342+
#text.latex.engine: latex
343+
334344
#text.latex.preamble: # IMPROPER USE OF THIS FEATURE WILL LEAD TO LATEX FAILURES
335345
# AND IS THEREFORE UNSUPPORTED. PLEASE DO NOT ASK FOR HELP
336346
# IF THIS FEATURE DOES NOT DO WHAT YOU EXPECT IT TO.

lib/matplotlib/rcsetup.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,7 @@ def _convert_validator_spec(key, conv):
10741074
# text props
10751075
"text.color": validate_color,
10761076
"text.usetex": validate_bool,
1077+
"text.latex.engine": ["latex", "latex+dvipng"],
10771078
"text.latex.preamble": validate_string,
10781079
"text.hinting": ["default", "no_autohint", "force_autohint",
10791080
"no_hinting", "auto", "native", "either", "none"],
@@ -1832,6 +1833,20 @@ class _Param:
18321833
"monospace, computer modern roman, computer modern sans serif, "
18331834
"computer modern typewriter"
18341835
),
1836+
_Param(
1837+
"text.latex.engine",
1838+
default="latex",
1839+
validator=["latex", "latex+dvipng"],
1840+
description=(
1841+
"The TeX engine/format to use. The following values are ""supported:\n"
1842+
"- 'latex': The classic TeX engine (the current default). All backends "
1843+
"render TeX's output by parsing the DVI output into glyphs and boxes and "
1844+
"emitting those one by one.\n"
1845+
"- 'latex+dvipng': The same as 'tex', with the exception that Agg-based "
1846+
"backends rely on dvipng to rasterize TeX's output. This value was the "
1847+
"default up to Matplotlib 3.10."
1848+
)
1849+
),
18351850
_Param(
18361851
"text.latex.preamble",
18371852
default="",

src/ft2font_wrapper.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,7 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""(
971971
image : 2d array of uint8
972972
The image buffer on which to draw the glyph.
973973
x, y : int
974-
The pixel location at which to draw the glyph.
974+
The position of the glyph's top left corner.
975975
glyph : Glyph
976976
The glyph to draw.
977977
antialiased : bool, default: True

0 commit comments

Comments
 (0)