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

Skip to content

Commit 658fc65

Browse files
committed
Handle some simpler colour fonts
This adds support for fonts with colour glyphs supported by FreeType. Specifically, this should mean COLRv0 fonts. There also exist some other colour font types, which are not supported: * CBDT is a non-scalable bitmap format and we don't support those, but it may be possible if we do a scaling ourselves. #31207 * COLRv1 essentially requires a full renderer setup. #31210 * SBIX is another non-scalable bitmap format likee CBDT. #31208 * SVG requires a parser (though it's some font-specific subset of the whole SVG spec). #31211 Fixes #31209
1 parent 90f98c7 commit 658fc65

4 files changed

Lines changed: 132 additions & 21 deletions

File tree

lib/matplotlib/backends/backend_agg.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ def get_hinting_flag():
5656
return mapping[mpl.rcParams['text.hinting']]
5757

5858

59+
def _get_load_flags():
60+
return get_hinting_flag() | LoadFlags.COLOR | LoadFlags.NO_SVG
61+
62+
5963
class RendererAgg(RendererBase):
6064
"""
6165
The renderer handles all the drawing primitives using a graphics
@@ -176,7 +180,7 @@ def _draw_text_glyphs_and_boxes(self, gc, x, y, angle, glyphs, boxes):
176180
# y is downwards.
177181
cos = math.cos(math.radians(angle))
178182
sin = math.sin(math.radians(angle))
179-
load_flags = get_hinting_flag()
183+
load_flags = _get_load_flags()
180184
for font, size, glyph_index, slant, extend, dx, dy in glyphs: # dy is upwards.
181185
font.set_size(size, self.dpi)
182186
hf = font._hinting_factor
@@ -192,13 +196,19 @@ def _draw_text_glyphs_and_boxes(self, gc, x, y, angle, glyphs, boxes):
192196
glyph_index, load_flags,
193197
RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO)
194198
buffer = bitmap.buffer
195-
if not gc.get_antialiased():
196-
buffer *= 0xff
197-
# draw_text_image's y is downwards & the bitmap bottom side.
198-
self._renderer.draw_text_image(
199-
buffer,
200-
bitmap.left, int(self.height) - bitmap.top + buffer.shape[0],
201-
0, gc)
199+
if buffer.ndim == 3:
200+
self._renderer.draw_text_bgra_image(
201+
gc,
202+
bitmap.left, bitmap.top - buffer.shape[0],
203+
buffer)
204+
else:
205+
if not gc.get_antialiased():
206+
buffer *= 0xff
207+
# draw_text_image's y is downwards & the bitmap bottom side.
208+
self._renderer.draw_text_image(
209+
buffer,
210+
bitmap.left, int(self.height) - bitmap.top + buffer.shape[0],
211+
0, gc)
202212

203213
rgba = gc.get_rgb()
204214
if len(rgba) == 3 or gc.get_forced_alpha():
@@ -240,7 +250,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
240250
return self.draw_mathtext(gc, x, y, s, prop, angle)
241251
font = self._prepare_font(prop)
242252
items = font._layout(
243-
s, flags=get_hinting_flag(),
253+
s, flags=_get_load_flags(),
244254
features=mtext.get_fontfeatures() if mtext is not None else None,
245255
language=mtext.get_language() if mtext is not None else None)
246256
size = prop.get_size_in_points()
@@ -262,7 +272,7 @@ def get_text_width_height_descent(self, s, prop, ismath):
262272
return parse.width, parse.height, parse.depth
263273

264274
font = self._prepare_font(prop)
265-
font.set_text(s, 0.0, flags=get_hinting_flag())
275+
font.set_text(s, 0.0, flags=_get_load_flags())
266276
w, h = font.get_width_height() # width and height of unrotated string
267277
d = font.get_descent()
268278
w /= 64.0 # convert from subpixels

src/_backend_agg.h

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <algorithm>
1313
#include <functional>
1414
#include <optional>
15+
#include <type_traits>
1516
#include <vector>
1617

1718
#include "agg_alpha_mask_u8.h"
@@ -152,11 +153,18 @@ class RendererAgg
152153
template <class ImageArray>
153154
void draw_text_image(GCAgg &gc, ImageArray &image, int x, int y, double angle);
154155

155-
template <class ImageArray>
156+
template <class ImageArray,
157+
// This defines the input pixel order only, but is a full blender because
158+
// Agg requires it internally.
159+
class ImagePixelFormat=pixfmt,
160+
// This defines the actual blender, but must use RGBA order, to correspond
161+
// with the rendering buffer.
162+
class Blender=pixfmt>
156163
void draw_image(GCAgg &gc,
157164
double x,
158165
double y,
159-
ImageArray &image);
166+
ImageArray &image,
167+
bool flip_image = false);
160168

161169
template <class PathGenerator,
162170
class TransformArray,
@@ -771,11 +779,12 @@ class span_conv_alpha
771779
}
772780
};
773781

774-
template <class ImageArray>
782+
template <class ImageArray, class ImagePixelFormat, class Blender>
775783
inline void RendererAgg::draw_image(GCAgg &gc,
776784
double x,
777785
double y,
778-
ImageArray &image)
786+
ImageArray &image,
787+
bool flip_image)
779788
{
780789
double alpha = gc.alpha;
781790

@@ -787,8 +796,9 @@ inline void RendererAgg::draw_image(GCAgg &gc,
787796
agg::rendering_buffer buffer;
788797
buffer.attach(image.mutable_data(0, 0, 0),
789798
(unsigned)image.shape(1), (unsigned)image.shape(0),
790-
-(int)image.shape(1) * 4);
791-
pixfmt pixf(buffer);
799+
(flip_image ? 4 : -4) * image.shape(1));
800+
// NOTE: this pixel format only describes the pixel order, not the colour state.
801+
ImagePixelFormat pixf(buffer);
792802

793803
if (has_clippath) {
794804
agg::trans_affine mtx;
@@ -823,10 +833,21 @@ inline void RendererAgg::draw_image(GCAgg &gc,
823833

824834
theRasterizer.add_path(rect2);
825835
agg::render_scanlines(theRasterizer, scanlineAlphaMask, ri);
836+
} else if constexpr(!std::is_same_v<Blender, pixfmt>) {
837+
static_assert(std::is_same_v<typename Blender::order_type, agg::order_rgba>,
838+
"Blender order must be RGBA");
839+
auto imgFmt = Blender{renderingBuffer};
840+
auto rendererImage = agg::renderer_base{imgFmt};
841+
rendererImage.reset_clipping(true);
842+
set_clipbox(gc.cliprect, rendererImage);
843+
rendererImage.blend_from(
844+
pixf, nullptr, (int)x, (int)(height - (y + image.shape(0))),
845+
(agg::int8u)(alpha * 255));
826846
} else {
827847
set_clipbox(gc.cliprect, rendererBase);
828848
rendererBase.blend_from(
829-
pixf, nullptr, (int)x, (int)(height - (y + image.shape(0))), (agg::int8u)(alpha * 255));
849+
pixf, nullptr, (int)x, (int)(height - (y + image.shape(0))),
850+
(agg::int8u)(alpha * 255));
830851
}
831852

832853
rendererBase.reset_clipping(true);

src/_backend_agg_wrapper.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,66 @@ PyRendererAgg_draw_image(RendererAgg *self,
131131
self->draw_image(gc, x, y, image);
132132
}
133133

134+
//========================================================blender_rgba_pre_plain
135+
// Blends premultiplied colors into a plain (non-premultiplied) buffer.
136+
template<class ColorT, class Order>
137+
struct blender_rgba_pre_plain : agg::conv_rgba_pre<ColorT, Order>
138+
{
139+
typedef ColorT color_type;
140+
typedef Order order_type;
141+
typedef typename color_type::value_type value_type;
142+
typedef typename color_type::calc_type calc_type;
143+
typedef typename color_type::long_type long_type;
144+
145+
// First premultiply the destination buffer, blend pixels using the premultiplied
146+
// form of Alvy-Ray Smith's compositing function, then demultiply the destination.
147+
148+
//--------------------------------------------------------------------
149+
static AGG_INLINE void blend_pix(value_type* p,
150+
value_type cr, value_type cg, value_type cb,
151+
value_type alpha, agg::cover_type cover)
152+
{
153+
blend_pix(p,
154+
color_type::mult_cover(cr, cover),
155+
color_type::mult_cover(cg, cover),
156+
color_type::mult_cover(cb, cover),
157+
color_type::mult_cover(alpha, cover));
158+
}
159+
160+
//--------------------------------------------------------------------
161+
static AGG_INLINE void blend_pix(value_type* p,
162+
value_type cr, value_type cg, value_type cb,
163+
value_type alpha)
164+
{
165+
agg::multiplier_rgba<ColorT, Order>::premultiply(p);
166+
p[Order::R] = color_type::prelerp(p[Order::R], cr, alpha);
167+
p[Order::G] = color_type::prelerp(p[Order::G], cg, alpha);
168+
p[Order::B] = color_type::prelerp(p[Order::B], cb, alpha);
169+
p[Order::A] = color_type::prelerp(p[Order::A], alpha, alpha);
170+
agg::multiplier_rgba<ColorT, Order>::demultiply(p);
171+
}
172+
};
173+
174+
static void
175+
PyRendererAgg_draw_text_bgra_image(RendererAgg *self,
176+
GCAgg &gc,
177+
int x,
178+
int y,
179+
py::array_t<agg::int8u, py::array::c_style | py::array::forcecast> image_obj)
180+
{
181+
// TODO: This really shouldn't be mutable, but Agg's renderer buffers aren't const.
182+
auto image = image_obj.mutable_unchecked<3>();
183+
184+
gc.alpha = 1.0;
185+
186+
// FreeType uses premultiplied BGRA, so use a different pixel format than draw_image.
187+
using blender_rgba32_pre_plain = blender_rgba_pre_plain<agg::rgba8, agg::order_rgba>;
188+
using pixfmt_rgba32_pre_plain = agg::pixfmt_alpha_blend_rgba<blender_rgba32_pre_plain,
189+
agg::rendering_buffer>;
190+
self->draw_image<decltype(image), agg::pixfmt_bgra32, pixfmt_rgba32_pre_plain>(
191+
gc, x, y, image, true);
192+
}
193+
134194
static void
135195
PyRendererAgg_draw_path_collection(RendererAgg *self,
136196
GCAgg &gc,
@@ -227,6 +287,8 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used())
227287
"face"_a = nullptr)
228288
.def("draw_text_image", &PyRendererAgg_draw_text_image,
229289
"image"_a, "x"_a, "y"_a, "angle"_a, "gc"_a)
290+
.def("draw_text_bgra_image", &PyRendererAgg_draw_text_bgra_image,
291+
"gc"_a, "x"_a, "y"_a, "image"_a.noconvert(true))
230292
.def("draw_image", &PyRendererAgg_draw_image,
231293
"gc"_a, "x"_a, "y"_a, "image"_a)
232294
.def("draw_path_collection", &PyRendererAgg_draw_path_collection,

src/ft2font_wrapper.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,22 @@ struct PyPositionedBitmap {
297297
left{slot->bitmap_left}, top{slot->bitmap_top}, owning{true}
298298
{
299299
FT_Bitmap_Init(&bitmap);
300-
FT_CHECK(FT_Bitmap_Convert, _ft2Library, &slot->bitmap, &bitmap, 1);
300+
if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
301+
FT_CHECK(FT_Bitmap_Convert, _ft2Library, &slot->bitmap, &bitmap, 1);
302+
} else {
303+
FT_CHECK(FT_Bitmap_Copy, _ft2Library, &slot->bitmap, &bitmap);
304+
}
301305
}
302306

303307
PyPositionedBitmap(FT_BitmapGlyph bg) :
304308
left{bg->left}, top{bg->top}, owning{true}
305309
{
306310
FT_Bitmap_Init(&bitmap);
307-
FT_CHECK(FT_Bitmap_Convert, _ft2Library, &bg->bitmap, &bitmap, 1);
311+
if (bg->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
312+
FT_CHECK(FT_Bitmap_Convert, _ft2Library, &bg->bitmap, &bitmap, 1);
313+
} else {
314+
FT_CHECK(FT_Bitmap_Copy, _ft2Library, &bg->bitmap, &bitmap);
315+
}
308316
}
309317

310318
PyPositionedBitmap(PyPositionedBitmap& other) = delete; // Non-copyable.
@@ -1693,9 +1701,19 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16931701
.def_readonly("top", &PyPositionedBitmap::top)
16941702
.def_property_readonly(
16951703
"buffer", [](PyPositionedBitmap &self) -> py::array {
1696-
return {{self.bitmap.rows, self.bitmap.width},
1704+
if (self.bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
1705+
return {
1706+
{self.bitmap.rows, self.bitmap.width, 4u},
1707+
{self.bitmap.pitch, 4, 1},
1708+
self.bitmap.buffer
1709+
};
1710+
} else {
1711+
return {
1712+
{self.bitmap.rows, self.bitmap.width},
16971713
{self.bitmap.pitch, 1},
1698-
self.bitmap.buffer};
1714+
self.bitmap.buffer
1715+
};
1716+
}
16991717
})
17001718
;
17011719

0 commit comments

Comments
 (0)