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

Skip to content

Commit f97e16b

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 4efa3c0 commit f97e16b

4 files changed

Lines changed: 133 additions & 22 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: 29 additions & 8 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,
@@ -807,11 +815,12 @@ class span_conv_alpha
807815
}
808816
};
809817

810-
template <class ImageArray>
818+
template <class ImageArray, class ImagePixelFormat, class Blender>
811819
inline void RendererAgg::draw_image(GCAgg &gc,
812820
double x,
813821
double y,
814-
ImageArray &image)
822+
ImageArray &image,
823+
bool flip_image)
815824
{
816825
double alpha = gc.alpha;
817826

@@ -823,8 +832,9 @@ inline void RendererAgg::draw_image(GCAgg &gc,
823832
agg::rendering_buffer buffer;
824833
buffer.attach(image.mutable_data(0, 0, 0),
825834
(unsigned)image.shape(1), (unsigned)image.shape(0),
826-
-(int)image.shape(1) * 4);
827-
pixfmt pixf(buffer);
835+
(flip_image ? 4 : -4) * image.shape(1));
836+
// NOTE: this pixel format only describes the pixel order, not the colour state.
837+
ImagePixelFormat pixf(buffer);
828838

829839
if (has_clippath) {
830840
agg::trans_affine mtx;
@@ -847,7 +857,7 @@ inline void RendererAgg::draw_image(GCAgg &gc,
847857
inv_mtx.invert();
848858

849859
typedef agg::span_allocator<agg::rgba8> color_span_alloc_type;
850-
typedef agg::image_accessor_clip<pixfmt> image_accessor_type;
860+
typedef agg::image_accessor_clip<ImagePixelFormat> image_accessor_type;
851861
typedef agg::span_interpolator_linear<> interpolator_type;
852862
typedef agg::span_image_filter_rgba_nn<image_accessor_type, interpolator_type>
853863
image_span_gen_type;
@@ -871,10 +881,21 @@ inline void RendererAgg::draw_image(GCAgg &gc,
871881

872882
theRasterizer.add_path(rect2);
873883
agg::render_scanlines(theRasterizer, scanlineAlphaMask, ri);
884+
} else if constexpr(!std::is_same_v<Blender, pixfmt>) {
885+
static_assert(std::is_same_v<typename Blender::order_type, agg::order_rgba>,
886+
"Blender order must be RGBA");
887+
auto imgFmt = Blender{renderingBuffer};
888+
auto rendererImage = agg::renderer_base{imgFmt};
889+
rendererImage.reset_clipping(true);
890+
set_clipbox(gc.cliprect, rendererImage);
891+
rendererImage.blend_from(
892+
pixf, nullptr, (int)x, (int)(height - (y + image.shape(0))),
893+
(agg::int8u)(alpha * 255));
874894
} else {
875895
set_clipbox(gc.cliprect, rendererBase);
876896
rendererBase.blend_from(
877-
pixf, nullptr, (int)x, (int)(height - (y + image.shape(0))), (agg::int8u)(alpha * 255));
897+
pixf, nullptr, (int)x, (int)(height - (y + image.shape(0))),
898+
(agg::int8u)(alpha * 255));
878899
}
879900

880901
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)