Skip to content

Commit e545bb2

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 def8fa4 commit e545bb2

File tree

7 files changed

+129
-13
lines changed

7 files changed

+129
-13
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"""
2323

2424
from contextlib import nullcontext
25+
import math
2526
from math import radians, cos, sin
2627

2728
import numpy as np
@@ -30,8 +31,9 @@
3031
from matplotlib import _api, cbook
3132
from matplotlib.backend_bases import (
3233
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
34+
from matplotlib.dviread import Dvi
3335
from matplotlib.font_manager import fontManager as _fontManager, get_font
34-
from matplotlib.ft2font import LoadFlags
36+
from matplotlib.ft2font import FT2Image, LoadFlags
3537
from matplotlib.mathtext import MathTextParser
3638
from matplotlib.path import Path
3739
from matplotlib.transforms import Bbox, BboxBase
@@ -208,7 +210,8 @@ def get_text_width_height_descent(self, s, prop, ismath):
208210

209211
_api.check_in_list(["TeX", True, False], ismath=ismath)
210212
if ismath == "TeX":
211-
return super().get_text_width_height_descent(s, prop, ismath)
213+
return [*map(
214+
math.ceil, super().get_text_width_height_descent(s, prop, ismath))]
212215

213216
if ismath:
214217
ox, oy, width, height, descent, font_image = \
@@ -227,19 +230,45 @@ def get_text_width_height_descent(self, s, prop, ismath):
227230
def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
228231
# docstring inherited
229232
# todo, handle props, angle, origins
230-
size = prop.get_size_in_points()
231-
232-
texmanager = self.get_texmanager()
233233

234-
Z = texmanager.get_grey(s, size, self.dpi)
235-
Z = np.array(Z * 255.0, np.uint8)
234+
size = prop.get_size_in_points()
235+
dvifile = self.get_texmanager().make_dvi(s, size)
236+
with Dvi(dvifile, self.dpi) as dvi:
237+
page, = dvi
238+
w = math.ceil(page.width)
239+
h = math.ceil(page.height)
240+
d = math.ceil(page.descent)
241+
242+
image = np.zeros((h + d, w), np.uint8)
243+
244+
for text in page.text:
245+
hf = mpl.rcParams["text.hinting_factor"]
246+
font = get_font(text.font_path)
247+
font.set_size(text.font_size, self.dpi)
248+
slant = text.font_effects.get("slant", 0)
249+
extend = text.font_effects.get("extend", 1)
250+
matrix = [
251+
[round(65536 * extend / hf), round(65536 * extend * slant)],
252+
[0, 65536],
253+
]
254+
font._set_transform(matrix, [0, 0])
255+
glyph = font.load_glyph(text.index)
256+
# text.y is upwards from baseline, _draw_glyph_at wants upwards from bottom.
257+
font._draw_glyph_at(image, text.x, d + text.y, glyph,
258+
antialiased=gc.get_antialiased())
259+
260+
for box in page.boxes:
261+
x0 = round(box.x)
262+
x1 = x0 + max(round(box.width), 1)
263+
y1 = round(h - box.y)
264+
y0 = y1 - max(round(box.height), 1)
265+
image[y0:y1, x0:x1] = 0xff
236266

237-
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
238267
xd = d * sin(radians(angle))
239268
yd = d * cos(radians(angle))
240269
x = round(x + xd)
241270
y = round(y + yd)
242-
self._renderer.draw_text_image(Z, x, y, angle, gc)
271+
self._renderer.draw_text_image(image, x, y, angle, gc)
243272

244273
def get_canvas_width_height(self):
245274
# docstring inherited
Loading

src/ft2font.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,17 @@ void FT2Font::set_size(double ptsize, double dpi)
306306
}
307307
}
308308

309+
void FT2Font::set_transform(
310+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta)
311+
{
312+
FT_Matrix m = {matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]};
313+
FT_Vector d = {delta[0], delta[1]};
314+
FT_Set_Transform(face, &m, &d);
315+
for (auto & fallback : fallbacks) {
316+
fallback->set_transform(matrix, delta);
317+
}
318+
}
319+
309320
void FT2Font::set_charmap(int i)
310321
{
311322
if (i >= face->num_charmaps) {
@@ -696,6 +707,27 @@ void FT2Font::draw_glyph_to_bitmap(
696707
draw_bitmap(im, &bitmap->bitmap, x + bitmap->left, y);
697708
}
698709

710+
void FT2Font::draw_glyph_at(
711+
py::array_t<uint8_t, py::array::c_style> im,
712+
double x, double y, size_t glyphInd, bool antialiased)
713+
{
714+
if (glyphInd >= glyphs.size()) {
715+
throw std::runtime_error("glyph num is out of range");
716+
}
717+
FT_Vector sub_offset = {FT_Fixed(x * 64 + .5), FT_Fixed(y * 64 + .5)};
718+
FT_Error error = FT_Glyph_To_Bitmap(
719+
&glyphs[glyphInd],
720+
antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO,
721+
&sub_offset, // additional translation
722+
1 // destroy image
723+
);
724+
if (error) {
725+
throw_ft_error("Could not convert glyph to bitmap", error);
726+
}
727+
FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[glyphInd];
728+
draw_bitmap(im, &bitmap->bitmap, bitmap->left, im.shape(0) - bitmap->top);
729+
}
730+
699731
void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer,
700732
bool fallback = false)
701733
{

src/ft2font.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ class FT2Font
7979
virtual ~FT2Font();
8080
void clear();
8181
void set_size(double ptsize, double dpi);
82+
void set_transform(
83+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta);
8284
void set_charmap(int i);
8385
void select_charmap(unsigned long i);
8486
void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
@@ -107,6 +109,9 @@ class FT2Font
107109
void draw_glyph_to_bitmap(
108110
py::array_t<uint8_t, py::array::c_style> im,
109111
int x, int y, size_t glyphInd, bool antialiased);
112+
void draw_glyph_at(
113+
py::array_t<uint8_t, py::array::c_style> im,
114+
double x, double y, size_t glyphInd, bool antialiased);
110115
void get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback);
111116
long get_name_index(char *name);
112117
FT_UInt get_char_index(FT_ULong charcode, bool fallback);

src/ft2font_wrapper.cpp

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,25 @@ PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi)
541541
self->x->set_size(ptsize, dpi);
542542
}
543543

544+
const char *PyFT2Font__set_transform__doc__ = R"""(
545+
Set the transform of the text.
546+
547+
This is a low-level function directly taking inputs in 26.6 format. Refer
548+
to the FreeType docs of FT_Set_Transform for further description.
549+
550+
Parameters
551+
----------
552+
matrix : (2, 2) array of int
553+
delta : (2,) array of int
554+
)""";
555+
556+
static void
557+
PyFT2Font__set_transform(
558+
PyFT2Font *self, std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta)
559+
{
560+
self->x->set_transform(matrix, delta);
561+
}
562+
544563
const char *PyFT2Font_set_charmap__doc__ = R"""(
545564
Make the i-th charmap current.
546565
@@ -971,7 +990,7 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""(
971990
image : 2d array of uint8
972991
The image buffer on which to draw the glyph.
973992
x, y : int
974-
The pixel location at which to draw the glyph.
993+
The position of the glyph's top left corner.
975994
glyph : Glyph
976995
The glyph to draw.
977996
antialiased : bool, default: True
@@ -990,9 +1009,36 @@ PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, py::buffer &image,
9901009
auto xd = _double_to_<int>("x", vxd);
9911010
auto yd = _double_to_<int>("y", vyd);
9921011

993-
self->x->draw_glyph_to_bitmap(
994-
py::array_t<uint8_t, py::array::c_style>{image},
995-
xd, yd, glyph->glyphInd, antialiased);
1012+
self->x->draw_glyph_to_bitmap(image, xd, yd, glyph->glyphInd, antialiased);
1013+
}
1014+
1015+
const char *PyFT2Font__draw_glyph_at__doc__ = R"""(
1016+
Draw a single glyph to the bitmap at pixel locations x, y.
1017+
1018+
Parameters
1019+
----------
1020+
image : FT2Image
1021+
The image buffer on which to draw the glyph. If the buffer is too
1022+
small, the glyph will be cropped.
1023+
x, y : float
1024+
The position of the glyph's origin.
1025+
glyph : Glyph
1026+
The glyph to draw.
1027+
antialiased : bool, default: True
1028+
Whether to render glyphs 8-bit antialiased or in pure black-and-white.
1029+
1030+
See Also
1031+
--------
1032+
.draw_glyphs_to_bitmap
1033+
)""";
1034+
1035+
static void
1036+
PyFT2Font__draw_glyph_at(
1037+
PyFT2Font *self,
1038+
py::buffer &image, double x, double y, PyGlyph *glyph,
1039+
bool antialiased = true)
1040+
{
1041+
self->x->draw_glyph_at(image, x, y, glyph->glyphInd, antialiased);
9961042
}
9971043

9981044
const char *PyFT2Font_get_glyph_name__doc__ = R"""(
@@ -1615,6 +1661,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16151661
.def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__)
16161662
.def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a,
16171663
PyFT2Font_set_size__doc__)
1664+
.def("_set_transform", &PyFT2Font__set_transform, "matrix"_a, "delta"_a)
16181665
.def("set_charmap", &PyFT2Font_set_charmap, "i"_a,
16191666
PyFT2Font_set_charmap__doc__)
16201667
.def("select_charmap", &PyFT2Font_select_charmap, "i"_a,
@@ -1654,6 +1701,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16541701
PyFT2Font_draw_glyph_to_bitmap__doc__);
16551702
}
16561703
cls
1704+
.def("_draw_glyph_at", &PyFT2Font__draw_glyph_at,
1705+
"image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true,
1706+
PyFT2Font__draw_glyph_at__doc__)
16571707
.def("get_glyph_name", &PyFT2Font_get_glyph_name, "index"_a,
16581708
PyFT2Font_get_glyph_name__doc__)
16591709
.def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__)

0 commit comments

Comments
 (0)