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

Skip to content

Commit 7627118

Browse files
anntzerQuLogic
authored andcommitted
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 da4d754 commit 7627118

5 files changed

Lines changed: 102 additions & 10 deletions

File tree

lib/matplotlib/backends/backend_agg.py

Lines changed: 75 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,84 @@ 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
272287

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

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)
291+
for text in page.text:
292+
hf = mpl.rcParams["text.hinting_factor"]
293+
# Resolving text.index will implicitly call get_font(), which
294+
# resets the font transform, so it has to be done before explicitly
295+
# setting the font transform below.
296+
index = text.index
297+
font = get_font(text.font_path)
298+
font.set_size(text.font_size, self.dpi)
299+
slant = text.font_effects.get("slant", 0)
300+
extend = text.font_effects.get("extend", 1)
301+
font._set_transform(
302+
(0x10000 * np.array([[cos, -sin], [sin, cos]])
303+
@ [[extend, extend * slant], [0, 1]]
304+
@ [[1 / hf, 0], [0, 1]]).round().astype(int),
305+
[round(0x40 * (x + text.x * cos - text.y * sin)),
306+
# FreeType's y is upwards.
307+
round(0x40 * (self.height - y + text.x * sin + text.y * cos))]
308+
)
309+
bitmap = font._render_glyph(
310+
index, get_hinting_flag(),
311+
RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO)
312+
buffer = np.asarray(bitmap.buffer)
313+
if not gc.get_antialiased():
314+
buffer *= 0xff
315+
# draw_text_image's y is downwards & the bitmap bottom side.
316+
self._renderer.draw_text_image(
317+
buffer,
318+
bitmap.left, int(self.height) - bitmap.top + buffer.shape[0],
319+
0, gc)
320+
321+
rgba = gc.get_rgb()
322+
if len(rgba) == 3 or gc.get_forced_alpha():
323+
rgba = rgba[:3] + (gc.get_alpha(),)
324+
gc1 = self.new_gc()
325+
gc1.set_linewidth(0)
326+
gc1.set_snap(gc.get_snap())
327+
for box in page.boxes:
328+
bx = box.x
329+
by = box.y
330+
bw = box.width
331+
bh = box.height
332+
if gc1.get_snap() in [None, True]:
333+
# Prevent thin bars from disappearing by growing symmetrically.
334+
if bw < 1:
335+
bx -= (1 - bw) / 2
336+
bw = 1
337+
if bh < 1:
338+
by -= (1 - bh) / 2
339+
bh = 1
340+
path = Path._create_closed([
341+
(bx, by), (bx + bw, by), (bx + bw, by + bh), (bx, by + bh)])
342+
self._renderer.draw_path(
343+
gc1, path,
344+
mpl.transforms.Affine2D()
345+
.rotate_deg(angle).translate(x, self.height - y),
346+
rgba)
347+
gc1.restore()
282348

283349
def get_canvas_width_height(self):
284350
# 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 "latex", with the exception that Agg-based
340+
## backends rely on dvipng to rasterize TeX's output. This value was the
341+
## default up to 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
@@ -1073,6 +1073,7 @@ def _convert_validator_spec(key, conv):
10731073
# text props
10741074
"text.color": validate_color,
10751075
"text.usetex": validate_bool,
1076+
"text.latex.engine": ["latex", "latex+dvipng"],
10761077
"text.latex.preamble": validate_string,
10771078
"text.hinting": ["default", "no_autohint", "force_autohint",
10781079
"no_hinting", "auto", "native", "either", "none"],
@@ -1831,6 +1832,20 @@ class _Param:
18311832
"monospace, computer modern roman, computer modern sans serif, "
18321833
"computer modern typewriter"
18331834
),
1835+
_Param(
1836+
"text.latex.engine",
1837+
default="latex",
1838+
validator=["latex", "latex+dvipng"],
1839+
description=(
1840+
"The TeX engine/format to use. The following values are supported:\n"
1841+
"- 'latex': The classic TeX engine (the current default). All backends "
1842+
"render TeX's output by parsing the DVI output into glyphs and boxes and "
1843+
"emitting those one by one.\n"
1844+
"- 'latex+dvipng': The same as 'latex', with the exception that Agg-based "
1845+
"backends rely on dvipng to rasterize TeX's output. This value was the "
1846+
"default up to Matplotlib 3.10."
1847+
)
1848+
),
18341849
_Param(
18351850
"text.latex.preamble",
18361851
default="",

lib/matplotlib/typing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@
452452
"text.hinting_factor",
453453
"text.kerning_factor",
454454
"text.language",
455+
"text.latex.engine",
455456
"text.latex.preamble",
456457
"text.parse_math",
457458
"text.usetex",

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)