Skip to content

Commit a0f6560

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 04c8eef commit a0f6560

File tree

7 files changed

+118
-9
lines changed

7 files changed

+118
-9
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 37 additions & 8 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
@@ -31,6 +32,7 @@
3132
from matplotlib import _api, cbook
3233
from matplotlib.backend_bases import (
3334
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
35+
from matplotlib.dviread import Dvi
3436
from matplotlib.font_manager import fontManager as _fontManager, get_font
3537
from matplotlib.ft2font import LoadFlags
3638
from matplotlib.mathtext import MathTextParser
@@ -219,7 +221,8 @@ def get_text_width_height_descent(self, s, prop, ismath):
219221

220222
_api.check_in_list(["TeX", True, False], ismath=ismath)
221223
if ismath == "TeX":
222-
return super().get_text_width_height_descent(s, prop, ismath)
224+
return [*map(
225+
math.ceil, super().get_text_width_height_descent(s, prop, ismath))]
223226

224227
if ismath:
225228
ox, oy, width, height, descent, font_image = \
@@ -238,19 +241,45 @@ def get_text_width_height_descent(self, s, prop, ismath):
238241
def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
239242
# docstring inherited
240243
# todo, handle props, angle, origins
241-
size = prop.get_size_in_points()
242-
243-
texmanager = self.get_texmanager()
244244

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

248-
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
249278
xd = d * sin(radians(angle))
250279
yd = d * cos(radians(angle))
251280
x = round(x + xd)
252281
y = round(y + yd)
253-
self._renderer.draw_text_image(Z, x, y, angle, gc)
282+
self._renderer.draw_text_image(image, x, y, angle, gc)
254283

255284
def get_canvas_width_height(self):
256285
# docstring inherited
603 Bytes
Loading
3.59 KB
Loading
2.19 KB
Loading

src/ft2font.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,17 @@ void FT2Font::set_size(double ptsize, double dpi)
283283
}
284284
}
285285

286+
void FT2Font::_set_transform(
287+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta)
288+
{
289+
FT_Matrix m = {matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]};
290+
FT_Vector d = {delta[0], delta[1]};
291+
FT_Set_Transform(face, &m, &d);
292+
for (auto & fallback : fallbacks) {
293+
fallback->_set_transform(matrix, delta);
294+
}
295+
}
296+
286297
void FT2Font::set_charmap(int i)
287298
{
288299
if (i >= face->num_charmaps) {
@@ -696,6 +707,24 @@ 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_CHECK(
719+
FT_Glyph_To_Bitmap,
720+
&glyphs[glyphInd],
721+
antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO,
722+
&sub_offset, // additional translation
723+
1); // destroy image
724+
FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[glyphInd];
725+
draw_bitmap(im, &bitmap->bitmap, bitmap->left, im.shape(0) - bitmap->top);
726+
}
727+
699728
std::string FT2Font::get_glyph_name(unsigned int glyph_number)
700729
{
701730
std::string buffer;

src/ft2font.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ class FT2Font
107107
void close();
108108
void clear();
109109
void set_size(double ptsize, double dpi);
110+
void _set_transform(
111+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta);
110112
void set_charmap(int i);
111113
void select_charmap(unsigned long i);
112114
void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
@@ -132,6 +134,9 @@ class FT2Font
132134
void draw_glyph_to_bitmap(
133135
py::array_t<uint8_t, py::array::c_style> im,
134136
int x, int y, size_t glyphInd, bool antialiased);
137+
void _draw_glyph_at(
138+
py::array_t<uint8_t, py::array::c_style> im,
139+
double x, double y, size_t glyphInd, bool antialiased);
135140
std::string get_glyph_name(unsigned int glyph_number);
136141
long get_name_index(char *name);
137142
FT_UInt get_char_index(FT_ULong charcode, bool fallback);

src/ft2font_wrapper.cpp

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,18 @@ const char *PyFT2Font_set_size__doc__ = R"""(
535535
The DPI used for rendering the text.
536536
)""";
537537

538+
const char *PyFT2Font__set_transform__doc__ = R"""(
539+
Set the transform of the text.
540+
541+
This is a low-level function directly taking inputs in 26.6 format. Refer
542+
to the FreeType docs of FT_Set_Transform for further description.
543+
544+
Parameters
545+
----------
546+
matrix : (2, 2) array of int
547+
delta : (2,) array of int
548+
)""";
549+
538550
const char *PyFT2Font_set_charmap__doc__ = R"""(
539551
Make the i-th charmap current.
540552
@@ -912,7 +924,7 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""(
912924
image : 2d array of uint8
913925
The image buffer on which to draw the glyph.
914926
x, y : int
915-
The pixel location at which to draw the glyph.
927+
The position of the glyph's top left corner.
916928
glyph : Glyph
917929
The glyph to draw.
918930
antialiased : bool, default: True
@@ -936,6 +948,36 @@ PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, py::buffer &image,
936948
xd, yd, glyph->glyphInd, antialiased);
937949
}
938950

951+
const char *PyFT2Font__draw_glyph_at__doc__ = R"""(
952+
Draw a single glyph to the bitmap at pixel locations x, y.
953+
954+
Parameters
955+
----------
956+
image : FT2Image
957+
The image buffer on which to draw the glyph. If the buffer is too
958+
small, the glyph will be cropped.
959+
x, y : float
960+
The position of the glyph's origin.
961+
glyph : Glyph
962+
The glyph to draw.
963+
antialiased : bool, default: True
964+
Whether to render glyphs 8-bit antialiased or in pure black-and-white.
965+
966+
See Also
967+
--------
968+
.draw_glyphs_to_bitmap
969+
)""";
970+
971+
static void
972+
PyFT2Font__draw_glyph_at(PyFT2Font *self, py::buffer &image,
973+
double x, double y,
974+
PyGlyph *glyph, bool antialiased = true)
975+
{
976+
self->_draw_glyph_at(
977+
py::array_t<uint8_t, py::array::c_style>{image},
978+
x, y, glyph->glyphInd, antialiased);
979+
}
980+
939981
const char *PyFT2Font_get_glyph_name__doc__ = R"""(
940982
Retrieve the ASCII name of a given glyph *index* in a face.
941983
@@ -1527,6 +1569,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
15271569
.def("clear", &PyFT2Font::clear, PyFT2Font_clear__doc__)
15281570
.def("set_size", &PyFT2Font::set_size, "ptsize"_a, "dpi"_a,
15291571
PyFT2Font_set_size__doc__)
1572+
.def("_set_transform", &PyFT2Font::_set_transform, "matrix"_a, "delta"_a)
15301573
.def("set_charmap", &PyFT2Font::set_charmap, "i"_a,
15311574
PyFT2Font_set_charmap__doc__)
15321575
.def("select_charmap", &PyFT2Font::select_charmap, "i"_a,
@@ -1567,6 +1610,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
15671610
PyFT2Font_draw_glyph_to_bitmap__doc__);
15681611
}
15691612
cls
1613+
.def("_draw_glyph_at", &PyFT2Font__draw_glyph_at,
1614+
"image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true,
1615+
PyFT2Font__draw_glyph_at__doc__)
15701616
.def("get_glyph_name", &PyFT2Font::get_glyph_name, "index"_a,
15711617
PyFT2Font_get_glyph_name__doc__)
15721618
.def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__)

0 commit comments

Comments
 (0)