Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit cb3c6f3

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

File tree

Expand file treeCollapse file tree

2 files changed

+134
-23
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+134
-23
lines changed

‎lib/matplotlib/backends/backend_webagg_core.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_webagg_core.py
+4-14Lines 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

Copy file name to clipboardExpand all lines: src/_png.cpp
+130-9Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.