Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d2e72db

Browse files
committedMar 17, 2025·
Support (mostly) all image surface formats, and dithering.
1 parent 247583d commit d2e72db

File tree

9 files changed

+248
-85
lines changed

9 files changed

+248
-85
lines changed
 

‎CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
next
2+
====
3+
4+
- Changed image format selection to ``set_options(image_format=...)``.
5+
- Added support for dithering control.
6+
17
v0.6.1 (2024-11-07)
28
===================
39

‎README.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ path <add_dll_directory_>`_).
110110
is detected at runtime.
111111
112112
cairo 1.17.2 added support for floating point surfaces, usable with
113-
``mplcairo.set_options(float_surface=True)``; the presence of this feature
114-
is detected at runtime. However, cairo 1.17.2 (and only that version) also
115-
has a bug that causes (in particular) polar gridlines to be incorrectly
116-
cropped. This bug was fixed in 2d1a137.
113+
``mplcairo.set_options(image_format=mplcairo.format_t.RGBA128F)``; the
114+
presence of this feature is detected at runtime. However, cairo 1.17.2
115+
(and only that version) also has a bug that causes (in particular) polar
116+
gridlines to be incorrectly cropped. This bug was fixed in 2d1a137.
117117
118118
cairo 1.17.4 fixed a rare crash in rasterization (in dfe3aa6).
119119

‎dither.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Similarly to support for blending operators, mplcairo also exposes support for
3+
dithering_ control.
4+
5+
The example below uses 1-bit rendering (A1, monochrome) to demonstrate the
6+
effect of the dithering algorithms. Note that A1 buffers are currently only
7+
supported with the ``mplcairo.qt`` backend.
8+
9+
.. _dithering: https://www.cairographics.org/manual/cairo-cairo-pattern-t.html#cairo-dither-t
10+
"""
11+
12+
import matplotlib as mpl
13+
from matplotlib import pyplot as plt
14+
import numpy as np
15+
16+
import mplcairo
17+
from mplcairo import dither_t
18+
19+
20+
mplcairo.set_options(image_format="A1")
21+
22+
rgba = mpl.image.imread(mpl.cbook.get_sample_data("grace_hopper.jpg"))
23+
alpha = rgba[:, :, :3].mean(-1).round().astype("u1") # To transparency mask.
24+
rgba = np.dstack([np.full(alpha.shape + (3,), 0xff), alpha])
25+
26+
# Figure and axes are made transparent, otherwise their patches would cover
27+
# everything else.
28+
axs = (plt.figure(figsize=(12, 4), facecolor="none")
29+
.subplots(1, len(dither_t),
30+
subplot_kw=dict(xticks=[], yticks=[], facecolor="none")))
31+
for dither, ax in zip(dither_t, axs):
32+
im = ax.imshow(rgba)
33+
ax.set(title=dither.name)
34+
dither.patch_artist(im)
35+
plt.show()

‎ext/_mplcairo.cpp

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ P11X_DECLARE_ENUM(
2626
{"GOOD", CAIRO_ANTIALIAS_GOOD},
2727
{"BEST", CAIRO_ANTIALIAS_BEST}
2828
)
29+
P11X_DECLARE_ENUM(
30+
"dither_t", "enum.Enum",
31+
{"NONE", mplcairo::detail::CAIRO_DITHER_NONE},
32+
{"DEFAULT", mplcairo::detail::CAIRO_DITHER_DEFAULT},
33+
{"FAST", mplcairo::detail::CAIRO_DITHER_FAST},
34+
{"GOOD", mplcairo::detail::CAIRO_DITHER_GOOD},
35+
{"BEST", mplcairo::detail::CAIRO_DITHER_BEST},
36+
)
2937
P11X_DECLARE_ENUM(
3038
"operator_t", "enum.Enum",
3139
{"CLEAR", CAIRO_OPERATOR_CLEAR},
@@ -58,15 +66,17 @@ P11X_DECLARE_ENUM(
5866
{"HSL_COLOR", CAIRO_OPERATOR_HSL_COLOR},
5967
{"HSL_LUMINOSITY", CAIRO_OPERATOR_HSL_LUMINOSITY}
6068
)
61-
P11X_DECLARE_ENUM( // Only for error messages.
62-
"_format_t", "enum.Enum",
69+
P11X_DECLARE_ENUM(
70+
"format_t", "enum.Enum",
6371
{"INVALID", CAIRO_FORMAT_INVALID},
6472
{"ARGB32", CAIRO_FORMAT_ARGB32},
6573
{"RGB24", CAIRO_FORMAT_RGB24},
6674
{"A8", CAIRO_FORMAT_A8},
6775
{"A1", CAIRO_FORMAT_A1},
6876
{"RGB16_565", CAIRO_FORMAT_RGB16_565},
69-
{"RGB30", CAIRO_FORMAT_RGB30}
77+
{"RGB30", CAIRO_FORMAT_RGB30},
78+
{"RGB96F", static_cast<cairo_format_t>(6)},
79+
{"RGBA128F", static_cast<cairo_format_t>(7)}
7080
)
7181
P11X_DECLARE_ENUM( // Only for error messages.
7282
"_surface_type_t", "enum.Enum",
@@ -263,7 +273,8 @@ GraphicsContextRenderer::GraphicsContextRenderer(
263273
/* hatch_linewidth */ {}, // Lazily loaded by get_hatch_linewidth.
264274
/* sketch */ {},
265275
/* snap */ true, // Defaults to None, i.e. True for us.
266-
/* url */ {}
276+
/* url */ {},
277+
/* dither */ detail::CAIRO_DITHER_DEFAULT
267278
}}}));
268279
}
269280

@@ -303,7 +314,7 @@ GraphicsContextRenderer::~GraphicsContextRenderer()
303314
cairo_t* GraphicsContextRenderer::cr_from_image_args(int width, int height)
304315
{
305316
auto const& surface =
306-
cairo_image_surface_create(get_cairo_format(), width, height);
317+
cairo_image_surface_create(detail::IMAGE_FORMAT, width, height);
307318
auto const& cr = cairo_create(surface);
308319
cairo_surface_destroy(surface);
309320
return cr;
@@ -953,6 +964,9 @@ void GraphicsContextRenderer::draw_image(
953964
auto const& mtx =
954965
cairo_matrix_t{1, 0, 0, -1, -x, -y + height_};
955966
cairo_pattern_set_matrix(pattern, &mtx);
967+
if (detail::cairo_pattern_set_dither) {
968+
detail::cairo_pattern_set_dither(pattern, get_additional_state().dither);
969+
}
956970
cairo_set_source(cr_, pattern);
957971
cairo_pattern_destroy(pattern);
958972
cairo_paint(cr_);
@@ -1049,7 +1063,7 @@ void maybe_multithread(
10491063
for (auto i = 0; i < detail::COLLECTION_THREADS; ++i) {
10501064
auto const& surface =
10511065
cairo_surface_create_similar_image(
1052-
cairo_get_target(cr), get_cairo_format(), width, height);
1066+
cairo_get_target(cr), detail::IMAGE_FORMAT, width, height);
10531067
auto const& ctx = cairo_create(surface);
10541068
cairo_surface_destroy(surface);
10551069
ctxs.push_back(ctx);
@@ -1164,7 +1178,7 @@ void GraphicsContextRenderer::draw_markers(
11641178
auto const& raster_gcr =
11651179
make_pattern_gcr(
11661180
cairo_surface_create_similar_image(
1167-
cairo_get_target(cr_), get_cairo_format(),
1181+
cairo_get_target(cr_), detail::IMAGE_FORMAT,
11681182
std::ceil(x1 - x0 + 1), std::ceil(y1 - y0 + 1)));
11691183
auto const& raster_cr = raster_gcr.cr_;
11701184
cairo_set_antialias(raster_cr, cairo_get_antialias(cr_));
@@ -1669,7 +1683,8 @@ py::array GraphicsContextRenderer::_stop_filter_get_buffer()
16691683
restore();
16701684
auto const& pattern = cairo_pop_group(cr_);
16711685
auto const& raster_surface =
1672-
cairo_image_surface_create(get_cairo_format(), int(width_), int(height_));
1686+
cairo_image_surface_create(
1687+
detail::IMAGE_FORMAT, int(width_), int(height_));
16731688
auto const& raster_cr = cairo_create(raster_surface);
16741689
cairo_set_source(raster_cr, pattern);
16751690
cairo_pattern_destroy(pattern);
@@ -2012,8 +2027,8 @@ Note that the defaults below refer to the initial values of the options;
20122027
options not passed to `set_options` are left unchanged.
20132028
20142029
At import time, mplcairo will set the initial values of the options from the
2015-
``MPLCAIRO_<OPTION_NAME>`` environment variables (loading them as Python
2016-
literals), if any such variables are set.
2030+
``MPLCAIRO_<OPTION_NAME>`` environment variables, if any such variables are
2031+
set. (They are loaded as Python literals, e.g. strings must be quoted.)
20172032
20182033
This function can also be used as a context manager
20192034
(``with set_options(...): ...``). In that case, the original values of the
@@ -2028,9 +2043,11 @@ cairo_circles : bool, default: True
20282043
collection_threads : int, default: 0
20292044
Number of threads to use to render markers and collections, if nonzero.
20302045
2031-
float_surface : bool, default: False
2032-
Whether to use a floating point surface (more accurate, but uses more
2033-
memory).
2046+
image_format : format_t, default: ARGB32
2047+
The internal image format (either a `format_t`, or the corresponding name).
2048+
All backends can render ARGB32 and RGBA128F images. Qt can additionally
2049+
render RGB24, A8, RGB16_565, RGB30, and (with an extra conversion) A1. No
2050+
backend currently supports RGBA96F.
20342051
20352052
miter_limit : float, default: 10
20362053
Setting for cairo_set_miter_limit__. If negative, use Matplotlib's (bad)
@@ -2048,7 +2065,7 @@ _debug: bool, default: False
20482065
Notes
20492066
-----
20502067
An additional format-specific control knob is the ``MaxVersion`` entry in the
2051-
*metadata* dict passed to ``savefig``. It can take values ``"1.4"``/``"1.5``
2068+
*metadata* dict passed to ``savefig``. It can take values ``"1.4"``/``"1.5"``
20522069
(to restrict to PDF 1.4 or 1.5 -- default: 1.5), ``"2"``/``"3"`` (to restrict
20532070
to PostScript levels 2 or 3 -- default: 3), or ``"1.1"``/``"1.2"`` (to restrict
20542071
to SVG 1.1 or 1.2 -- default: 1.1).
@@ -2186,12 +2203,21 @@ Only intended for debugging purposes.
21862203
.def("set_snap", &GraphicsContextRenderer::set_snap)
21872204
.def("set_url", &GraphicsContextRenderer::set_url)
21882205

2189-
// This one function is specific to mplcairo.
2206+
// mplcairo-specific methods.
21902207
.def(
21912208
"set_mplcairo_operator",
21922209
[](GraphicsContextRenderer& gcr, cairo_operator_t op) -> void {
21932210
cairo_set_operator(gcr.cr_, op);
21942211
})
2212+
.def(
2213+
"set_mplcairo_dither",
2214+
[](GraphicsContextRenderer& gcr, detail::cairo_dither_t dither) -> void {
2215+
if (!detail::cairo_pattern_set_dither) {
2216+
py::module::import("warnings").attr("warn")(
2217+
"cairo_pattern_set_dither requires cairo>=1.18.0");
2218+
}
2219+
gcr.get_additional_state().dither = dither;
2220+
})
21952221

21962222
.def(
21972223
"get_clip_rectangle",

‎ext/_util.cpp

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ py::object RC_PARAMS{},
6666
PIXEL_MARKER{},
6767
UNIT_CIRCLE{};
6868
int COLLECTION_THREADS{};
69-
bool FLOAT_SURFACE{};
69+
cairo_format_t IMAGE_FORMAT{CAIRO_FORMAT_ARGB32};
7070
double MITER_LIMIT{10.};
7171
bool DEBUG{};
7272
MplcairoScriptSurface MPLCAIRO_SCRIPT_SURFACE{[] {
@@ -114,7 +114,7 @@ py::dict get_options()
114114
return py::dict(
115115
"cairo_circles"_a=bool(detail::UNIT_CIRCLE),
116116
"collection_threads"_a=detail::COLLECTION_THREADS,
117-
"float_surface"_a=detail::FLOAT_SURFACE,
117+
"image_format"_a=detail::IMAGE_FORMAT,
118118
"miter_limit"_a=detail::MITER_LIMIT,
119119
"raqm"_a=has_raqm(),
120120
"_debug"_a=detail::DEBUG);
@@ -139,11 +139,24 @@ py::object set_options(py::kwargs kwargs)
139139
Py_XDECREF(detail::UNIT_CIRCLE.release().ptr());
140140
}
141141
}
142-
if (auto const& float_surface = pop_option("float_surface", bool{})) {
143-
if (*float_surface && cairo_version() < CAIRO_VERSION_ENCODE(1, 17, 2)) {
142+
if (auto const& image_format =
143+
pop_option("image_format",
144+
std::variant<cairo_format_t, std::string>{})) {
145+
auto fmt = std::visit(overloaded {
146+
[](cairo_format_t fmt) {
147+
return fmt;
148+
},
149+
[](std::string fmt) { // operator[]() doesn't support std::string
150+
return
151+
py::module::import("mplcairo").attr("format_t")[fmt.c_str()]
152+
.cast<cairo_format_t>();
153+
}
154+
}, *image_format);
155+
if (fmt > CAIRO_FORMAT_RGB30
156+
&& cairo_version() < CAIRO_VERSION_ENCODE(1, 17, 2)) {
144157
throw std::invalid_argument{"float surfaces require cairo>=1.17.2"};
145158
}
146-
detail::FLOAT_SURFACE = *float_surface;
159+
detail::IMAGE_FORMAT = fmt;
147160
}
148161
if (auto const& threads = pop_option("collection_threads", int{})) {
149162
detail::COLLECTION_THREADS = *threads;
@@ -175,12 +188,6 @@ py::object rc_param(std::string key)
175188
PyDict_GetItemString(detail::RC_PARAMS.ptr(), key.data()));
176189
}
177190

178-
cairo_format_t get_cairo_format() {
179-
return
180-
detail::FLOAT_SURFACE
181-
? static_cast<cairo_format_t>(7) : CAIRO_FORMAT_ARGB32;
182-
}
183-
184191
rgba_t to_rgba(py::object color, std::optional<double> alpha)
185192
{
186193
return
@@ -679,6 +686,15 @@ void fill_and_stroke_exact(
679686
}
680687

681688
py::array image_surface_to_buffer(cairo_surface_t* surface) {
689+
// Possible outputs:
690+
// ARGB32 -> uint8, (h, w, 4)
691+
// RGB24 -> uint8, (h, w, 3) (non-contiguous)
692+
// A8 -> uint8, (h, w)
693+
// A1 -> ("V{w}", void(ceil(w/8))), (h,)
694+
// RGB16_565 -> uint16, (h, w)
695+
// RGB30 -> uint32, (h, w)
696+
// RGB96F -> float, (h, w, 3)
697+
// RGB128F -> float, (h, w, 4)
682698
if (auto const& type = cairo_surface_get_type(surface);
683699
type != CAIRO_SURFACE_TYPE_IMAGE) {
684700
throw std::runtime_error{
@@ -687,37 +703,47 @@ py::array image_surface_to_buffer(cairo_surface_t* surface) {
687703
}
688704
cairo_surface_reference(surface);
689705
cairo_surface_flush(surface);
706+
auto const& h = cairo_image_surface_get_height(surface),
707+
& w = cairo_image_surface_get_width(surface),
708+
& stride = cairo_image_surface_get_stride(surface);
709+
auto const& data = cairo_image_surface_get_data(surface);
710+
auto const& capsule = py::capsule(
711+
surface,
712+
[](void* surface) -> void {
713+
cairo_surface_destroy(static_cast<cairo_surface_t*>(surface));
714+
});
690715
switch (auto const& fmt = cairo_image_surface_get_format(surface);
691716
// Avoid "not in enumerated type" warning with CAIRO_FORMAT_RGBA_128F.
692717
static_cast<int>(fmt)) {
693718
case static_cast<int>(CAIRO_FORMAT_ARGB32):
694-
return py::array_t<uint8_t>{
695-
{cairo_image_surface_get_height(surface),
696-
cairo_image_surface_get_width(surface),
697-
4},
698-
{cairo_image_surface_get_stride(surface), 4, 1},
699-
cairo_image_surface_get_data(surface),
700-
py::capsule(
701-
surface,
702-
[](void* surface) -> void {
703-
cairo_surface_destroy(static_cast<cairo_surface_t*>(surface));
704-
})};
705-
case 7: // CAIRO_FORMAT_RGBA_128F.
719+
return py::array_t<uint8_t>{{h, w, 4}, {stride, 4, 1}, data, capsule};
720+
case static_cast<int>(CAIRO_FORMAT_RGB24):
721+
return py::array_t<uint8_t>{{h, w, 3}, {stride, 4, 1}, data, capsule};
722+
case static_cast<int>(CAIRO_FORMAT_A8):
723+
return py::array_t<uint8_t>{{h, w}, {stride, 1}, data, capsule};
724+
case static_cast<int>(CAIRO_FORMAT_A1): {
725+
auto dtype_args = py::list{};
726+
dtype_args.append(py::make_tuple(
727+
"V" + std::to_string(w), "V" + std::to_string((w + 7) / 8)));
728+
return py::array{
729+
py::dtype::from_args(dtype_args), {h}, {stride}, data, capsule};
730+
}
731+
case static_cast<int>(CAIRO_FORMAT_RGB16_565):
732+
return py::array_t<uint16_t>{
733+
{h, w}, {stride, 2}, reinterpret_cast<uint16_t*>(data), capsule};
734+
case static_cast<int>(CAIRO_FORMAT_RGB30):
735+
return py::array_t<uint32_t>{
736+
{h, w}, {stride, 4}, reinterpret_cast<uint32_t*>(data), capsule};
737+
case 6: // CAIRO_FORMAT_RGB96F.
738+
return py::array_t<float>{
739+
{h, w, 3}, {stride, 12, 4}, reinterpret_cast<float*>(data), capsule};
740+
case 7: // CAIRO_FORMAT_RGBA128F.
706741
return py::array_t<float>{
707-
{cairo_image_surface_get_height(surface),
708-
cairo_image_surface_get_width(surface),
709-
4},
710-
{cairo_image_surface_get_stride(surface), 16, 4},
711-
reinterpret_cast<float*>(cairo_image_surface_get_data(surface)),
712-
py::capsule(
713-
surface,
714-
[](void* surface) -> void {
715-
cairo_surface_destroy(static_cast<cairo_surface_t*>(surface));
716-
})};
742+
{h, w, 4}, {stride, 16, 4}, reinterpret_cast<float*>(data), capsule};
717743
default:
718744
throw std::invalid_argument{
719-
"_get_buffer only supports images surfaces with ARGB32 and RGBA128F "
720-
"formats, not {}"_format(fmt).cast<std::string>()};
745+
"_get_buffer does not support image surfaces with format "
746+
"{}"_format(fmt).cast<std::string>()};
721747
}
722748
}
723749

‎ext/_util.h

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,24 @@ extern std::array<uint8_t, 0x10000>
2626

2727
// Optional parts of cairo.
2828

29-
// Copy-pasted from cairo.h, backported from 1.15.
29+
// From cairo.h, backported from 1.15.
3030
#define CAIRO_TAG_DEST "cairo.dest"
3131
#define CAIRO_TAG_LINK "Link"
3232
extern void (*cairo_tag_begin)(cairo_t*, char const*, char const*);
3333
extern void (*cairo_tag_end)(cairo_t*, char const*);
34-
// Copy-pasted from cairo.h, backported from 1.16.
34+
// From cairo.h, backported from 1.16.
3535
extern void (*cairo_font_options_set_variations)(cairo_font_options_t *, const char *);
36-
37-
// Modified from cairo-pdf.h.
36+
// From cairo.h, backported from 1.18.
37+
typedef enum _cairo_dither {
38+
CAIRO_DITHER_NONE,
39+
CAIRO_DITHER_DEFAULT,
40+
CAIRO_DITHER_FAST,
41+
CAIRO_DITHER_GOOD,
42+
CAIRO_DITHER_BEST
43+
} cairo_dither_t;
44+
extern void (*cairo_pattern_set_dither)(cairo_pattern_t *pattern, cairo_dither_t dither);
45+
46+
// From cairo-pdf.h.
3847
enum cairo_pdf_version_t {};
3948
typedef enum _cairo_pdf_metadata {
4049
CAIRO_PDF_METADATA_TITLE,
@@ -57,7 +66,7 @@ extern void (*cairo_pdf_surface_set_metadata)(
5766
cairo_surface_t*, cairo_pdf_metadata_t, char const*);
5867
extern void (*cairo_pdf_surface_set_size)(cairo_surface_t*, double, double);
5968

60-
// Modified from cairo-ps.h.
69+
// From cairo-ps.h.
6170
enum cairo_ps_level_t {};
6271
extern void (*cairo_ps_get_levels)(cairo_ps_level_t const**, int*);
6372
extern cairo_surface_t* (*cairo_ps_surface_create_for_stream)(
@@ -68,7 +77,7 @@ extern void (*cairo_ps_surface_restrict_to_level)(
6877
extern void (*cairo_ps_surface_set_eps)(cairo_surface_t*, cairo_bool_t);
6978
extern void (*cairo_ps_surface_set_size)(cairo_surface_t*, double, double);
7079

71-
// Modified from cairo-svg.h.
80+
// From cairo-svg.h.
7281
enum cairo_svg_version_t {};
7382
extern void (*cairo_svg_get_versions)(cairo_svg_version_t const**, int*);
7483
extern cairo_surface_t* (*cairo_svg_surface_create_for_stream)(
@@ -80,6 +89,7 @@ extern void (*cairo_svg_surface_restrict_to_version)(
8089
_(cairo_tag_begin) \
8190
_(cairo_tag_end) \
8291
_(cairo_font_options_set_variations) \
92+
_(cairo_pattern_set_dither) \
8393
_(cairo_pdf_get_versions) \
8494
_(cairo_pdf_surface_create_for_stream) \
8595
_(cairo_pdf_surface_restrict_to_version) \
@@ -111,7 +121,7 @@ extern py::object RC_PARAMS;
111121
extern py::object PIXEL_MARKER;
112122
extern py::object UNIT_CIRCLE;
113123
extern int COLLECTION_THREADS;
114-
extern bool FLOAT_SURFACE;
124+
extern cairo_format_t IMAGE_FORMAT;
115125
extern double MITER_LIMIT;
116126
extern bool DEBUG;
117127
enum class MplcairoScriptSurface {
@@ -144,6 +154,8 @@ struct AdditionalState {
144154
bool snap;
145155
std::optional<std::string> url;
146156

157+
detail::cairo_dither_t dither;
158+
147159
rgba_t get_hatch_color();
148160
double get_hatch_linewidth();
149161
};
@@ -163,7 +175,6 @@ bool py_eq(py::object obj1, py::object obj2);
163175
py::dict get_options();
164176
py::object set_options(py::kwargs kwargs);
165177
py::object rc_param(std::string key);
166-
cairo_format_t get_cairo_format();
167178
rgba_t to_rgba(py::object color, std::optional<double> alpha = {});
168179
cairo_matrix_t matrix_from_transform(py::object transform, double y0 = 0);
169180
cairo_matrix_t matrix_from_transform(

‎src/mplcairo/__init__.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ def _load_symbols():
2626
import matplotlib as mpl
2727

2828
from . import _mplcairo
29-
from ._mplcairo import antialias_t, operator_t, get_options, set_options
29+
from ._mplcairo import (
30+
antialias_t, dither_t, format_t, operator_t, get_options, set_options)
3031

3132
__all__ = [
32-
"antialias_t", "operator_t",
33+
"antialias_t", "dither_t", "operator_t", "format_t",
3334
"get_options", "set_options",
3435
"get_context", "get_raw_buffer",
3536
]
@@ -97,25 +98,40 @@ def get_raw_buffer(canvas):
9798
"""
9899
Get the canvas' raw internal buffer.
99100
100-
This is normally a uint8 buffer of shape ``(m, n, 4)`` in
101-
ARGB32 order, unless the canvas was created after calling
102-
``set_options(float_surface=True)`` in which case this is
103-
a float32 buffer of shape ``(m, n, 4)`` in RGBA128F order.
101+
The buffer's shape and dtype depend on the ``image_format`` passed to
102+
`set_options`:
103+
- ``ARGB32``: uint8 (h, w, 4), in ARGB32 order (the default);
104+
- ``RGB24``: uint8 (h, w, 3), in RGB24 order;
105+
- ``A8``: uint8 (h, w);
106+
- ``A1``: [("V{w}", void)] (h), where w is the actual buffer width in
107+
pixels (e.g. "V640"), and the void field is wide enough to contain the
108+
data but is padded to a byte boundary;
109+
- ``RGB16_565``: uint16 (h, w);
110+
- ``RGB30``: uint32 (h, w);
111+
- ``RGB96F``: float (h, w, 3);
112+
- ``RGBA128F``: float (h, w, 4).
104113
"""
105114
return canvas.renderer._get_buffer()
106115

107116

108-
def _operator_patch_artist(op, artist):
109-
"""Patch an artist to make it use this compositing operator for drawing."""
117+
def _patch_artist(op, artist):
118+
"""
119+
Patch an artist so that it is drawn with this compositing operator or
120+
dithering algorithm.
121+
"""
110122

111123
def draw(renderer):
112124
gc = renderer.new_gc()
113-
gc.set_mplcairo_operator(op)
125+
if isinstance(op, operator_t):
126+
gc.set_mplcairo_operator(op)
127+
elif isinstance(op, dither_t):
128+
gc.set_mplcairo_dither(op)
114129
_base_draw(renderer)
115130
gc.restore()
116131

117132
_base_draw = artist.draw
118133
artist.draw = draw
119134

120135

121-
operator_t.patch_artist = _operator_patch_artist
136+
dither_t.patch_artist = _patch_artist
137+
operator_t.patch_artist = _patch_artist

‎src/mplcairo/_util.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,24 @@
44
import sys
55

66
import matplotlib as mpl
7+
import numpy as np
78

89
from . import _backports
910

1011

12+
def detect_buffer_format(buf):
13+
return {
14+
(np.uint8, (4,)): "argb32",
15+
(np.uint8, (3,)): "rgb24",
16+
(np.uint8, ()): "a8",
17+
(np.void, ()): "a1",
18+
(np.uint16, ()): "rgb16_565",
19+
(np.uint32, ()): "rgb30",
20+
(np.float32, (3,)): "rgb96f",
21+
(np.float32, (4,)): "rgba128f",
22+
}[buf.dtype.type, buf.shape[2:]]
23+
24+
1125
@functools.lru_cache(1)
1226
def get_tex_font_map():
1327
return mpl.dviread.PsfontsMap(mpl.dviread.find_tex_file("pdftex.map"))

‎src/mplcairo/qt.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ctypes
2+
import sys
23

34
# This does support QT_API=pyqt6 on Matplotlib versions that can handle it.
45
try:
@@ -8,31 +9,59 @@
89
_BackendQT5 as _BackendQT, FigureCanvasQT)
910
from matplotlib.backends.qt_compat import QtCore, QtGui
1011

11-
from . import _mplcairo
12+
from . import _mplcairo, _util
1213
from .base import FigureCanvasCairo
1314

1415

16+
_QT_VERSION = tuple(QtCore.QLibraryInfo.version().segments())
17+
18+
1519
class FigureCanvasQTCairo(FigureCanvasCairo, FigureCanvasQT):
1620
def paintEvent(self, event):
1721
if hasattr(self, "_update_dpi") and self._update_dpi():
1822
return # matplotlib#19123 (<3.4).
1923
# We always repaint the full canvas (doing otherwise would require an
2024
# additional copy of the buffer into a contiguous block, so it's not
2125
# clear it would be faster).
22-
buf = _mplcairo.cairo_to_premultiplied_argb32(
23-
self.get_renderer()._get_buffer())
24-
height, width, _ = buf.shape
25-
# The image buffer is not necessarily contiguous, but the padding
26-
# in the ARGB32 case (each scanline is 32-bit aligned) happens to
27-
# match what QImage requires; in the RGBA128F case the documented Qt
28-
# requirement does not seem necessary?
29-
if QtGui.__name__.startswith("PyQt6"):
30-
from PyQt6 import sip
31-
ptr = sip.voidptr(buf)
26+
buf = self.get_renderer()._get_buffer()
27+
fmt = _util.detect_buffer_format(buf)
28+
if fmt == "argb32":
29+
qfmt = 6 # ARGB32_Premultiplied
30+
elif fmt == "rgb24":
31+
assert buf.strides[1] == 4 # alpha channel as padding.
32+
qfmt = 4 # RGB32
33+
elif fmt == "a8":
34+
qfmt = 24 # Grayscale8
35+
elif fmt == "a1":
36+
qfmt = {"big": 1, "little": 2}[sys.byteorder] # Mono, Mono_LSB
37+
elif fmt == "rgb16_565":
38+
qfmt = 7 # RGB16
39+
elif fmt == "rgb30":
40+
qfmt = 21 # RGB30
41+
elif fmt == "rgb96f":
42+
raise ValueError(f"{fmt} is not supported by Qt")
43+
elif fmt == "rgba128f":
44+
if _QT_VERSION >= (6, 2):
45+
qfmt = 35 # RGBA32FPx4_Premultiplied
46+
else:
47+
qfmt = 6
48+
buf = _mplcairo.cairo_to_premultiplied_argb32(buf)
49+
if fmt == "a1":
50+
height, = buf.shape
51+
fieldname, = buf.dtype.names
52+
assert fieldname[0] == "V"
53+
width = int(fieldname[1:])
3254
else:
33-
ptr = buf
55+
height, width = buf.shape[:2]
56+
ptr = (
57+
# Also supports non-contiguous (RGB24) data.
58+
buf.ctypes.data if QtCore.__name__.startswith("PyQt") else
59+
# Required by PySide, but fails for non-contiguous data.
60+
buf)
3461
qimage = QtGui.QImage(
35-
ptr, width, height, QtGui.QImage.Format(6)) # ARGB32_Premultiplied
62+
ptr, width, height, buf.strides[0], QtGui.QImage.Format(qfmt))
63+
if fmt == "a1": # FIXME directly drawing Format_Mono segfaults?
64+
qimage = qimage.convertedTo(QtGui.QImage.Format(6))
3665
getattr(qimage, "setDevicePixelRatio", lambda _: None)(
3766
self.device_pixel_ratio)
3867
# https://bugreports.qt.io/browse/PYSIDE-140

0 commit comments

Comments
 (0)
Please sign in to comment.