Skip to content

Commit b241ca4

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 de00c49 commit b241ca4

File tree

7 files changed

+125
-10
lines changed

7 files changed

+125
-10
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 40 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,47 @@ 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 = FT2Image(w, h + d)
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+
image = np.asarray(image)
261+
262+
for box in page.boxes:
263+
x0 = round(box.x)
264+
x1 = x0 + max(round(box.width), 1)
265+
y1 = round(h - box.y)
266+
y0 = y1 - max(round(box.height), 1)
267+
image[y0:y1, x0:x1] = 0xff
236268

237-
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
238269
xd = d * sin(radians(angle))
239270
yd = d * cos(radians(angle))
240271
x = round(x + xd)
241272
y = round(y + yd)
242-
self._renderer.draw_text_image(Z, x, y, angle, gc)
273+
self._renderer.draw_text_image(image, x, y, angle, gc)
243274

244275
def get_canvas_width_height(self):
245276
# docstring inherited
Loading

src/ft2font.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,17 @@ void FT2Font::set_size(double ptsize, double dpi)
334334
}
335335
}
336336

337+
void FT2Font::set_transform(
338+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta)
339+
{
340+
FT_Matrix m = {matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]};
341+
FT_Vector d = {delta[0], delta[1]};
342+
FT_Set_Transform(face, &m, &d);
343+
for (auto & fallback : fallbacks) {
344+
fallback->set_transform(matrix, delta);
345+
}
346+
}
347+
337348
void FT2Font::set_charmap(int i)
338349
{
339350
if (i >= face->num_charmaps) {
@@ -721,6 +732,25 @@ void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd,
721732
im.draw_bitmap(&bitmap->bitmap, x + bitmap->left, y);
722733
}
723734

735+
void FT2Font::draw_glyph_at(FT2Image &im, double x, double y, size_t glyphInd, bool antialiased)
736+
{
737+
if (glyphInd >= glyphs.size()) {
738+
throw std::runtime_error("glyph num is out of range");
739+
}
740+
FT_Vector sub_offset = {FT_Fixed(x * 64 + .5), FT_Fixed(y * 64 + .5)};
741+
FT_Error error = FT_Glyph_To_Bitmap(
742+
&glyphs[glyphInd],
743+
antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO,
744+
&sub_offset, // additional translation
745+
1 // destroy image
746+
);
747+
if (error) {
748+
throw_ft_error("Could not convert glyph to bitmap", error);
749+
}
750+
FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[glyphInd];
751+
im.draw_bitmap(&bitmap->bitmap, bitmap->left, im.get_height() - bitmap->top);
752+
}
753+
724754
void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer,
725755
bool fallback = false)
726756
{

src/ft2font.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ class FT2Font
7676
virtual ~FT2Font();
7777
void clear();
7878
void set_size(double ptsize, double dpi);
79+
void set_transform(
80+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta);
7981
void set_charmap(int i);
8082
void select_charmap(unsigned long i);
8183
void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
@@ -102,6 +104,7 @@ class FT2Font
102104
long get_descent();
103105
void draw_glyphs_to_bitmap(bool antialiased);
104106
void draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased);
107+
void draw_glyph_at(FT2Image &im, double x, double y, size_t glyphInd, bool antialiased);
105108
void get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback);
106109
long get_name_index(char *name);
107110
FT_UInt get_char_index(FT_ULong charcode, bool fallback);

src/ft2font_wrapper.cpp

Lines changed: 52 additions & 1 deletion
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 FT_Fixed
553+
delta : (2,) array of FT_Fixed
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 : FT2Image
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
@@ -993,6 +1012,34 @@ PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, FT2Image &image,
9931012
self->x->draw_glyph_to_bitmap(image, xd, yd, glyph->glyphInd, antialiased);
9941013
}
9951014

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(PyFT2Font *self, FT2Image &image,
1037+
double x, double y,
1038+
PyGlyph *glyph, bool antialiased = true)
1039+
{
1040+
self->x->draw_glyph_at(image, x, y, glyph->glyphInd, antialiased);
1041+
}
1042+
9961043
const char *PyFT2Font_get_glyph_name__doc__ = R"""(
9971044
Retrieve the ASCII name of a given glyph *index* in a face.
9981045
@@ -1614,6 +1661,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16141661
.def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__)
16151662
.def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a,
16161663
PyFT2Font_set_size__doc__)
1664+
.def("set_transform", &PyFT2Font_set_transform, "matrix"_a, "delta"_a)
16171665
.def("set_charmap", &PyFT2Font_set_charmap, "i"_a,
16181666
PyFT2Font_set_charmap__doc__)
16191667
.def("select_charmap", &PyFT2Font_select_charmap, "i"_a,
@@ -1643,6 +1691,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16431691
.def("draw_glyph_to_bitmap", &PyFT2Font_draw_glyph_to_bitmap,
16441692
"image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true,
16451693
PyFT2Font_draw_glyph_to_bitmap__doc__)
1694+
.def("_draw_glyph_at", &PyFT2Font__draw_glyph_at,
1695+
"image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true,
1696+
PyFT2Font__draw_glyph_at__doc__)
16461697
.def("get_glyph_name", &PyFT2Font_get_glyph_name, "index"_a,
16471698
PyFT2Font_get_glyph_name__doc__)
16481699
.def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__)

0 commit comments

Comments
 (0)