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

Skip to content

Commit cb3c6f3

Browse files
committed
Merge pull request #5389 from mdboom/webagg-speed
Faster image generation in WebAgg/NbAgg backends
2 parents 2f3b2ca + bf1dca7 commit cb3c6f3

File tree

2 files changed

+134
-23
lines changed

2 files changed

+134
-23
lines changed

lib/matplotlib/backends/backend_webagg_core.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,6 @@ class FigureCanvasWebAggCore(backend_agg.FigureCanvasAgg):
143143
def __init__(self, *args, **kwargs):
144144
backend_agg.FigureCanvasAgg.__init__(self, *args, **kwargs)
145145

146-
# A buffer to hold the PNG data for the last frame. This is
147-
# retained so it can be resent to each client without
148-
# regenerating it.
149-
self._png_buffer = io.BytesIO()
150-
151146
# Set to True when the renderer contains data that is newer
152147
# than the PNG buffer.
153148
self._png_is_old = True
@@ -225,24 +220,19 @@ def get_diff_image(self):
225220
diff = buff != last_buffer
226221
output = np.where(diff, buff, 0)
227222

228-
# Clear out the PNG data buffer rather than recreating it
229-
# each time. This reduces the number of memory
230-
# (de)allocations.
231-
self._png_buffer.truncate()
232-
self._png_buffer.seek(0)
233-
234223
# TODO: We should write a new version of write_png that
235224
# handles the differencing inline
236-
_png.write_png(
225+
buff = _png.write_png(
237226
output.view(dtype=np.uint8).reshape(output.shape + (4,)),
238-
self._png_buffer)
227+
None, compression=6, filter=_png.PNG_FILTER_NONE)
239228

240229
# Swap the renderer frames
241230
self._renderer, self._last_renderer = (
242231
self._last_renderer, renderer)
243232
self._force_full = False
244233
self._png_is_old = False
245-
return self._png_buffer.getvalue()
234+
235+
return buff
246236

247237
def get_renderer(self, cleared=None):
248238
# Mirrors super.get_renderer, but caches the old one

src/_png.cpp

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,27 @@ extern "C" {
3434
#undef jmpbuf
3535
#endif
3636

37+
struct buffer_t {
38+
PyObject *str;
39+
size_t cursor;
40+
size_t size;
41+
};
42+
43+
44+
static void write_png_data_buffer(png_structp png_ptr, png_bytep data, png_size_t length)
45+
{
46+
buffer_t *buff = (buffer_t *)png_get_io_ptr(png_ptr);
47+
if (buff->cursor + length < buff->size) {
48+
memcpy(PyBytes_AS_STRING(buff->str) + buff->cursor, data, length);
49+
buff->cursor += length;
50+
}
51+
}
52+
53+
static void flush_png_data_buffer(png_structp png_ptr)
54+
{
55+
56+
}
57+
3758
static void write_png_data(png_structp png_ptr, png_bytep data, png_size_t length)
3859
{
3960
PyObject *py_file_obj = (PyObject *)png_get_io_ptr(png_ptr);
@@ -62,27 +83,66 @@ static void flush_png_data(png_structp png_ptr)
6283
Py_XDECREF(result);
6384
}
6485

65-
const char *Py_write_png__doc__ = "write_png(buffer, file, dpi=0)";
86+
const char *Py_write_png__doc__ =
87+
"write_png(buffer, file, dpi=0, compression=6, filter=auto)\n"
88+
"\n"
89+
"Parameters\n"
90+
"----------\n"
91+
"buffer : numpy array of image data\n"
92+
" Must be an MxNxD array of dtype uint8.\n"
93+
" - if D is 1, the image is greyscale\n"
94+
" - if D is 3, the image is RGB\n"
95+
" - if D is 4, the image is RGBA\n"
96+
"\n"
97+
"file : str path, file-like object or None\n"
98+
" - If a str, must be a file path\n"
99+
" - If a file-like object, must write bytes\n"
100+
" - If None, a byte string containing the PNG data will be returned\n"
101+
"\n"
102+
"dpi : float\n"
103+
" The dpi to store in the file metadata.\n"
104+
"\n"
105+
"compression : int\n"
106+
" The level of lossless zlib compression to apply. 0 indicates no\n"
107+
" compression. Values 1-9 indicate low/fast through high/slow\n"
108+
" compression. Default is 6.\n"
109+
"\n"
110+
"filter : int\n"
111+
" Filter to apply. Must be one of the constants: PNG_FILTER_NONE,\n"
112+
" PNG_FILTER_SUB, PNG_FILTER_UP, PNG_FILTER_AVG, PNG_FILTER_PAETH.\n"
113+
" See the PNG standard for more information.\n"
114+
" If not provided, libpng will try to automatically determine the\n"
115+
" best filter on a line-by-line basis.\n"
116+
"\n"
117+
"Returns\n"
118+
"-------\n"
119+
"buffer : bytes or None\n"
120+
" Byte string containing the PNG content if None was passed in for\n"
121+
" file, otherwise None is returned.\n";
66122

67123
static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
68124
{
69125
numpy::array_view<unsigned char, 3> buffer;
70126
PyObject *filein;
71127
double dpi = 0;
72-
const char *names[] = { "buffer", "file", "dpi", NULL };
128+
int compression = 6;
129+
int filter = -1;
130+
const char *names[] = { "buffer", "file", "dpi", "compression", "filter", NULL };
73131

74132
// We don't need strict contiguity, just for each row to be
75133
// contiguous, and libpng has special handling for getting RGB out
76134
// of RGBA, ARGB or BGR. But the simplest thing to do is to
77135
// enforce contiguity using array_view::converter_contiguous.
78136
if (!PyArg_ParseTupleAndKeywords(args,
79137
kwds,
80-
"O&O|d:write_png",
138+
"O&O|dii:write_png",
81139
(char **)names,
82140
&buffer.converter_contiguous,
83141
&buffer,
84142
&filein,
85-
&dpi)) {
143+
&dpi,
144+
&compression,
145+
&filter)) {
86146
return NULL;
87147
}
88148

@@ -104,6 +164,8 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
104164
png_infop info_ptr = NULL;
105165
struct png_color_8_struct sig_bit;
106166
int png_color_type;
167+
buffer_t buff;
168+
buff.str = NULL;
107169

108170
switch (channels) {
109171
case 1:
@@ -122,6 +184,12 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
122184
goto exit;
123185
}
124186

187+
if (compression < 0 || compression > 9) {
188+
PyErr_Format(PyExc_ValueError,
189+
"compression must be in range 0-9, got %d", compression);
190+
goto exit;
191+
}
192+
125193
if (PyBytes_Check(filein) || PyUnicode_Check(filein)) {
126194
if ((py_file = mpl_PyFile_OpenFile(filein, (char *)"wb")) == NULL) {
127195
goto exit;
@@ -131,7 +199,14 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
131199
py_file = filein;
132200
}
133201

134-
if ((fp = mpl_PyFile_Dup(py_file, (char *)"wb", &offset))) {
202+
if (filein == Py_None) {
203+
buff.size = width * height * 4 + 1024;
204+
buff.str = PyBytes_FromStringAndSize(NULL, buff.size);
205+
if (buff.str == NULL) {
206+
goto exit;
207+
}
208+
buff.cursor = 0;
209+
} else if ((fp = mpl_PyFile_Dup(py_file, (char *)"wb", &offset))) {
135210
close_dup_file = true;
136211
} else {
137212
PyErr_Clear();
@@ -152,6 +227,11 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
152227
goto exit;
153228
}
154229

230+
png_set_compression_level(png_ptr, compression);
231+
if (filter >= 0) {
232+
png_set_filter(png_ptr, 0, filter);
233+
}
234+
155235
info_ptr = png_create_info_struct(png_ptr);
156236
if (info_ptr == NULL) {
157237
PyErr_SetString(PyExc_RuntimeError, "Could not create info struct");
@@ -163,7 +243,9 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
163243
goto exit;
164244
}
165245

166-
if (fp) {
246+
if (buff.str) {
247+
png_set_write_fn(png_ptr, (void *)&buff, &write_png_data_buffer, &flush_png_data_buffer);
248+
} else if (fp) {
167249
png_init_io(png_ptr, fp);
168250
} else {
169251
png_set_write_fn(png_ptr, (void *)py_file, &write_png_data, &flush_png_data);
@@ -227,8 +309,13 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
227309
}
228310

229311
if (PyErr_Occurred()) {
312+
Py_XDECREF(buff.str);
230313
return NULL;
231314
} else {
315+
if (buff.str) {
316+
_PyBytes_Resize(&buff.str, buff.cursor);
317+
return buff.str;
318+
}
232319
Py_RETURN_NONE;
233320
}
234321
}
@@ -494,21 +581,46 @@ static PyObject *_read_png(PyObject *filein, bool float_result)
494581
}
495582
}
496583

497-
const char *Py_read_png_float__doc__ = "read_png_float(file)";
584+
const char *Py_read_png_float__doc__ =
585+
"read_png_float(file)\n"
586+
"\n"
587+
"Read in a PNG file, converting values to floating-point doubles\n"
588+
"in the range (0, 1)\n"
589+
"\n"
590+
"Parameters\n"
591+
"----------\n"
592+
"file : str path or file-like object\n";
498593

499594
static PyObject *Py_read_png_float(PyObject *self, PyObject *args, PyObject *kwds)
500595
{
501596
return _read_png(args, true);
502597
}
503598

504-
const char *Py_read_png_int__doc__ = "read_png_int(file)";
599+
const char *Py_read_png_int__doc__ =
600+
"read_png_int(file)\n"
601+
"\n"
602+
"Read in a PNG file with original integer values.\n"
603+
"\n"
604+
"Parameters\n"
605+
"----------\n"
606+
"file : str path or file-like object\n";
505607

506608
static PyObject *Py_read_png_int(PyObject *self, PyObject *args, PyObject *kwds)
507609
{
508610
return _read_png(args, false);
509611
}
510612

511-
const char *Py_read_png__doc__ = "read_png(file)";
613+
const char *Py_read_png__doc__ =
614+
"read_png(file)\n"
615+
"\n"
616+
"Read in a PNG file, converting values to floating-point doubles\n"
617+
"in the range (0, 1)\n"
618+
"\n"
619+
"Alias for read_png_float()\n"
620+
"\n"
621+
"Parameters\n"
622+
"----------\n"
623+
"file : str path or file-like object\n";
512624

513625
static PyMethodDef module_methods[] = {
514626
{"write_png", (PyCFunction)Py_write_png, METH_VARARGS|METH_KEYWORDS, Py_write_png__doc__},
@@ -558,6 +670,15 @@ extern "C" {
558670

559671
import_array();
560672

673+
if (PyModule_AddIntConstant(m, "PNG_FILTER_NONE", PNG_FILTER_NONE) ||
674+
PyModule_AddIntConstant(m, "PNG_FILTER_SUB", PNG_FILTER_SUB) ||
675+
PyModule_AddIntConstant(m, "PNG_FILTER_UP", PNG_FILTER_UP) ||
676+
PyModule_AddIntConstant(m, "PNG_FILTER_AVG", PNG_FILTER_AVG) ||
677+
PyModule_AddIntConstant(m, "PNG_FILTER_PAETH", PNG_FILTER_PAETH)) {
678+
INITERROR;
679+
}
680+
681+
561682
#if PY3K
562683
return m;
563684
#endif

0 commit comments

Comments
 (0)