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 b3e9443

Browse filesBrowse files
committed
Faster image generation in WebAgg/NbAgg backends
- Write the PNG data to string buffer, rather than an io.BytesIO to eliminate the cost of memory reallocation - Add compression and filter arguments to `_png.write_png` - Use compression=3 and filter=NONE, which seems to be a sweet spot for processing time vs. file size
1 parent 2f3b2ca commit b3e9443
Copy full SHA for b3e9443

File tree

Expand file treeCollapse file tree

2 files changed

+70
-19
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+70
-19
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
+66-5Lines changed: 66 additions & 5 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);
@@ -69,20 +90,24 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
6990
numpy::array_view<unsigned char, 3> buffer;
7091
PyObject *filein;
7192
double dpi = 0;
72-
const char *names[] = { "buffer", "file", "dpi", NULL };
93+
int compression = 6;
94+
int filter = -1;
95+
const char *names[] = { "buffer", "file", "dpi", "compression", "filter", NULL };
7396

7497
// We don't need strict contiguity, just for each row to be
7598
// contiguous, and libpng has special handling for getting RGB out
7699
// of RGBA, ARGB or BGR. But the simplest thing to do is to
77100
// enforce contiguity using array_view::converter_contiguous.
78101
if (!PyArg_ParseTupleAndKeywords(args,
79102
kwds,
80-
"O&O|d:write_png",
103+
"O&O|dii:write_png",
81104
(char **)names,
82105
&buffer.converter_contiguous,
83106
&buffer,
84107
&filein,
85-
&dpi)) {
108+
&dpi,
109+
&compression,
110+
&filter)) {
86111
return NULL;
87112
}
88113

@@ -104,6 +129,8 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
104129
png_infop info_ptr = NULL;
105130
struct png_color_8_struct sig_bit;
106131
int png_color_type;
132+
buffer_t buff;
133+
buff.str = NULL;
107134

108135
switch (channels) {
109136
case 1:
@@ -122,6 +149,12 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
122149
goto exit;
123150
}
124151

152+
if (compression < 0 || compression > 9) {
153+
PyErr_Format(PyExc_ValueError,
154+
"compression must be in range 0-9, got %d", compression);
155+
goto exit;
156+
}
157+
125158
if (PyBytes_Check(filein) || PyUnicode_Check(filein)) {
126159
if ((py_file = mpl_PyFile_OpenFile(filein, (char *)"wb")) == NULL) {
127160
goto exit;
@@ -131,7 +164,14 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
131164
py_file = filein;
132165
}
133166

134-
if ((fp = mpl_PyFile_Dup(py_file, (char *)"wb", &offset))) {
167+
if (filein == Py_None) {
168+
buff.size = width * height * 4 + 1024;
169+
buff.str = PyBytes_FromStringAndSize(NULL, buff.size);
170+
if (buff.str == NULL) {
171+
goto exit;
172+
}
173+
buff.cursor = 0;
174+
} else if ((fp = mpl_PyFile_Dup(py_file, (char *)"wb", &offset))) {
135175
close_dup_file = true;
136176
} else {
137177
PyErr_Clear();
@@ -152,6 +192,11 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
152192
goto exit;
153193
}
154194

195+
png_set_compression_level(png_ptr, compression);
196+
if (filter >= 0) {
197+
png_set_filter(png_ptr, 0, filter);
198+
}
199+
155200
info_ptr = png_create_info_struct(png_ptr);
156201
if (info_ptr == NULL) {
157202
PyErr_SetString(PyExc_RuntimeError, "Could not create info struct");
@@ -163,7 +208,9 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
163208
goto exit;
164209
}
165210

166-
if (fp) {
211+
if (buff.str) {
212+
png_set_write_fn(png_ptr, (void *)&buff, &write_png_data_buffer, &flush_png_data_buffer);
213+
} else if (fp) {
167214
png_init_io(png_ptr, fp);
168215
} else {
169216
png_set_write_fn(png_ptr, (void *)py_file, &write_png_data, &flush_png_data);
@@ -227,8 +274,13 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
227274
}
228275

229276
if (PyErr_Occurred()) {
277+
Py_XDECREF(buff.str);
230278
return NULL;
231279
} else {
280+
if (buff.str) {
281+
_PyBytes_Resize(&buff.str, buff.cursor);
282+
return buff.str;
283+
}
232284
Py_RETURN_NONE;
233285
}
234286
}
@@ -558,6 +610,15 @@ extern "C" {
558610

559611
import_array();
560612

613+
if (PyModule_AddIntConstant(m, "PNG_FILTER_NONE", PNG_FILTER_NONE) ||
614+
PyModule_AddIntConstant(m, "PNG_FILTER_SUB", PNG_FILTER_SUB) ||
615+
PyModule_AddIntConstant(m, "PNG_FILTER_UP", PNG_FILTER_UP) ||
616+
PyModule_AddIntConstant(m, "PNG_FILTER_AVG", PNG_FILTER_AVG) ||
617+
PyModule_AddIntConstant(m, "PNG_FILTER_PAETH", PNG_FILTER_PAETH)) {
618+
INITERROR;
619+
}
620+
621+
561622
#if PY3K
562623
return m;
563624
#endif

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.