Skip to content

Rasterize dvi files without dvipng. #30039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 38 additions & 9 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"""

from contextlib import nullcontext
import math
from math import radians, cos, sin

import numpy as np
Expand All @@ -30,8 +31,9 @@
from matplotlib import _api, cbook
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
from matplotlib.dviread import Dvi
from matplotlib.font_manager import fontManager as _fontManager, get_font
from matplotlib.ft2font import LoadFlags
from matplotlib.ft2font import FT2Image, LoadFlags

Check failure on line 36 in lib/matplotlib/backends/backend_agg.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 `matplotlib.ft2font.FT2Image` imported but unused Raw Output: message:"`matplotlib.ft2font.FT2Image` imported but unused" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/backends/backend_agg.py" range:{start:{line:36 column:32} end:{line:36 column:40}}} source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"F401" url:"https://docs.astral.sh/ruff/rules/unused-import"} suggestions:{range:{start:{line:36 column:1} end:{line:36 column:51}} text:"from matplotlib.ft2font import LoadFlags"}
from matplotlib.mathtext import MathTextParser
from matplotlib.path import Path
from matplotlib.transforms import Bbox, BboxBase
Expand Down Expand Up @@ -208,7 +210,8 @@

_api.check_in_list(["TeX", True, False], ismath=ismath)
if ismath == "TeX":
return super().get_text_width_height_descent(s, prop, ismath)
return [*map(
math.ceil, super().get_text_width_height_descent(s, prop, ismath))]

if ismath:
ox, oy, width, height, descent, font_image = \
Expand All @@ -227,19 +230,45 @@
def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
# docstring inherited
# todo, handle props, angle, origins
size = prop.get_size_in_points()

texmanager = self.get_texmanager()

Z = texmanager.get_grey(s, size, self.dpi)
Z = np.array(Z * 255.0, np.uint8)
size = prop.get_size_in_points()
dvifile = self.get_texmanager().make_dvi(s, size)
with Dvi(dvifile, self.dpi) as dvi:
page, = dvi
w = math.ceil(page.width)
h = math.ceil(page.height)
d = math.ceil(page.descent)

image = np.zeros((h + d, w), np.uint8)

for text in page.text:
hf = mpl.rcParams["text.hinting_factor"]
font = get_font(text.font_path)
font.set_size(text.font_size, self.dpi)
slant = text.font_effects.get("slant", 0)
extend = text.font_effects.get("extend", 1)
matrix = [
[round(65536 * extend / hf), round(65536 * extend * slant)],
[0, 65536],
]
font._set_transform(matrix, [0, 0])
glyph = font.load_glyph(text.index)
# text.y is upwards from baseline, _draw_glyph_at wants upwards from bottom.
font._draw_glyph_at(image, text.x, d + text.y, glyph,
antialiased=gc.get_antialiased())

for box in page.boxes:
x0 = round(box.x)
x1 = x0 + max(round(box.width), 1)
y1 = round(h - box.y)
y0 = y1 - max(round(box.height), 1)
image[y0:y1, x0:x1] = 0xff

w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
xd = d * sin(radians(angle))
yd = d * cos(radians(angle))
x = round(x + xd)
y = round(y + yd)
self._renderer.draw_text_image(Z, x, y, angle, gc)
self._renderer.draw_text_image(image, x, y, angle, gc)

def get_canvas_width_height(self):
# docstring inherited
Expand Down
Binary file modified lib/matplotlib/tests/baseline_images/test_usetex/eqnarray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified lib/matplotlib/tests/baseline_images/test_usetex/rotation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions src/ft2font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,17 @@
}
}

void FT2Font::set_transform(
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta)
{
FT_Matrix m = {matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]};
FT_Vector d = {delta[0], delta[1]};
FT_Set_Transform(face, &m, &d);
for (auto & fallback : fallbacks) {
fallback->set_transform(matrix, delta);
}
}

void FT2Font::set_charmap(int i)
{
if (i >= face->num_charmaps) {
Expand Down Expand Up @@ -696,6 +707,27 @@
draw_bitmap(im, &bitmap->bitmap, x + bitmap->left, y);
}

void FT2Font::draw_glyph_at(
py::array_t<uint8_t, py::array::c_style> im,
double x, double y, size_t glyphInd, bool antialiased)
{
if (glyphInd >= glyphs.size()) {
throw std::runtime_error("glyph num is out of range");
}
FT_Vector sub_offset = {FT_Fixed(x * 64 + .5), FT_Fixed(y * 64 + .5)};
FT_Error error = FT_Glyph_To_Bitmap(
&glyphs[glyphInd],
antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO,
&sub_offset, // additional translation
1 // destroy image
);
if (error) {
throw_ft_error("Could not convert glyph to bitmap", error);

Check warning on line 725 in src/ft2font.cpp

View check run for this annotation

Codecov / codecov/patch

src/ft2font.cpp#L725

Added line #L725 was not covered by tests
}
FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[glyphInd];
draw_bitmap(im, &bitmap->bitmap, bitmap->left, im.shape(0) - bitmap->top);
}

void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer,
bool fallback = false)
{
Expand Down
5 changes: 5 additions & 0 deletions src/ft2font.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class FT2Font
virtual ~FT2Font();
void clear();
void set_size(double ptsize, double dpi);
void set_transform(
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta);
void set_charmap(int i);
void select_charmap(unsigned long i);
void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
Expand Down Expand Up @@ -107,6 +109,9 @@ class FT2Font
void draw_glyph_to_bitmap(
py::array_t<uint8_t, py::array::c_style> im,
int x, int y, size_t glyphInd, bool antialiased);
void draw_glyph_at(
py::array_t<uint8_t, py::array::c_style> im,
double x, double y, size_t glyphInd, bool antialiased);
void get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback);
long get_name_index(char *name);
FT_UInt get_char_index(FT_ULong charcode, bool fallback);
Expand Down
58 changes: 54 additions & 4 deletions src/ft2font_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,25 @@ PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi)
self->x->set_size(ptsize, dpi);
}

const char *PyFT2Font__set_transform__doc__ = R"""(
Set the transform of the text.

This is a low-level function directly taking inputs in 26.6 format. Refer
to the FreeType docs of FT_Set_Transform for further description.

Parameters
----------
matrix : (2, 2) array of int
delta : (2,) array of int
)""";

static void
PyFT2Font__set_transform(
PyFT2Font *self, std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta)
{
self->x->set_transform(matrix, delta);
}

const char *PyFT2Font_set_charmap__doc__ = R"""(
Make the i-th charmap current.

Expand Down Expand Up @@ -971,7 +990,7 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""(
image : 2d array of uint8
The image buffer on which to draw the glyph.
x, y : int
The pixel location at which to draw the glyph.
The position of the glyph's top left corner.
glyph : Glyph
The glyph to draw.
antialiased : bool, default: True
Expand All @@ -990,9 +1009,36 @@ PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, py::buffer &image,
auto xd = _double_to_<int>("x", vxd);
auto yd = _double_to_<int>("y", vyd);

self->x->draw_glyph_to_bitmap(
py::array_t<uint8_t, py::array::c_style>{image},
xd, yd, glyph->glyphInd, antialiased);
self->x->draw_glyph_to_bitmap(image, xd, yd, glyph->glyphInd, antialiased);
}

const char *PyFT2Font__draw_glyph_at__doc__ = R"""(
Draw a single glyph to the bitmap at pixel locations x, y.

Parameters
----------
image : FT2Image
The image buffer on which to draw the glyph. If the buffer is too
small, the glyph will be cropped.
x, y : float
The position of the glyph's origin.
glyph : Glyph
The glyph to draw.
antialiased : bool, default: True
Whether to render glyphs 8-bit antialiased or in pure black-and-white.

See Also
--------
.draw_glyphs_to_bitmap
)""";

static void
PyFT2Font__draw_glyph_at(
PyFT2Font *self,
py::buffer &image, double x, double y, PyGlyph *glyph,
bool antialiased = true)
{
self->x->draw_glyph_at(image, x, y, glyph->glyphInd, antialiased);
}

const char *PyFT2Font_get_glyph_name__doc__ = R"""(
Expand Down Expand Up @@ -1615,6 +1661,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
.def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__)
.def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a,
PyFT2Font_set_size__doc__)
.def("_set_transform", &PyFT2Font__set_transform, "matrix"_a, "delta"_a)
.def("set_charmap", &PyFT2Font_set_charmap, "i"_a,
PyFT2Font_set_charmap__doc__)
.def("select_charmap", &PyFT2Font_select_charmap, "i"_a,
Expand Down Expand Up @@ -1654,6 +1701,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
PyFT2Font_draw_glyph_to_bitmap__doc__);
}
cls
.def("_draw_glyph_at", &PyFT2Font__draw_glyph_at,
"image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true,
PyFT2Font__draw_glyph_at__doc__)
.def("get_glyph_name", &PyFT2Font_get_glyph_name, "index"_a,
PyFT2Font_get_glyph_name__doc__)
.def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__)
Expand Down
Loading