@@ -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+
3758static 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
67123static 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;
@@ -160,6 +228,11 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
160228 goto exit;
161229 }
162230
231+ png_set_compression_level (png_ptr, compression);
232+ if (filter >= 0 ) {
233+ png_set_filter (png_ptr, 0 , filter);
234+ }
235+
163236 info_ptr = png_create_info_struct (png_ptr);
164237 if (info_ptr == NULL ) {
165238 PyErr_SetString (PyExc_RuntimeError, " Could not create info struct" );
@@ -171,7 +244,9 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
171244 goto exit;
172245 }
173246
174- if (fp) {
247+ if (buff.str ) {
248+ png_set_write_fn (png_ptr, (void *)&buff, &write_png_data_buffer, &flush_png_data_buffer);
249+ } else if (fp) {
175250 png_init_io (png_ptr, fp);
176251 } else {
177252 png_set_write_fn (png_ptr, (void *)py_file, &write_png_data, &flush_png_data);
@@ -235,8 +310,13 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
235310 }
236311
237312 if (PyErr_Occurred ()) {
313+ Py_XDECREF (buff.str );
238314 return NULL ;
239315 } else {
316+ if (buff.str ) {
317+ _PyBytes_Resize (&buff.str , buff.cursor );
318+ return buff.str ;
319+ }
240320 Py_RETURN_NONE;
241321 }
242322}
@@ -509,21 +589,46 @@ static PyObject *_read_png(PyObject *filein, bool float_result)
509589 }
510590}
511591
512- const char *Py_read_png_float__doc__ = " read_png_float(file)" ;
592+ const char *Py_read_png_float__doc__ =
593+ " read_png_float(file)\n "
594+ " \n "
595+ " Read in a PNG file, converting values to floating-point doubles\n "
596+ " in the range (0, 1)\n "
597+ " \n "
598+ " Parameters\n "
599+ " ----------\n "
600+ " file : str path or file-like object\n " ;
513601
514602static PyObject *Py_read_png_float (PyObject *self, PyObject *args, PyObject *kwds)
515603{
516604 return _read_png (args, true );
517605}
518606
519- const char *Py_read_png_int__doc__ = " read_png_int(file)" ;
607+ const char *Py_read_png_int__doc__ =
608+ " read_png_int(file)\n "
609+ " \n "
610+ " Read in a PNG file with original integer values.\n "
611+ " \n "
612+ " Parameters\n "
613+ " ----------\n "
614+ " file : str path or file-like object\n " ;
520615
521616static PyObject *Py_read_png_int (PyObject *self, PyObject *args, PyObject *kwds)
522617{
523618 return _read_png (args, false );
524619}
525620
526- const char *Py_read_png__doc__ = " read_png(file)" ;
621+ const char *Py_read_png__doc__ =
622+ " read_png(file)\n "
623+ " \n "
624+ " Read in a PNG file, converting values to floating-point doubles\n "
625+ " in the range (0, 1)\n "
626+ " \n "
627+ " Alias for read_png_float()\n "
628+ " \n "
629+ " Parameters\n "
630+ " ----------\n "
631+ " file : str path or file-like object\n " ;
527632
528633static PyMethodDef module_methods[] = {
529634 {" write_png" , (PyCFunction)Py_write_png, METH_VARARGS|METH_KEYWORDS, Py_write_png__doc__},
@@ -573,6 +678,15 @@ extern "C" {
573678
574679 import_array ();
575680
681+ if (PyModule_AddIntConstant (m, " PNG_FILTER_NONE" , PNG_FILTER_NONE) ||
682+ PyModule_AddIntConstant (m, " PNG_FILTER_SUB" , PNG_FILTER_SUB) ||
683+ PyModule_AddIntConstant (m, " PNG_FILTER_UP" , PNG_FILTER_UP) ||
684+ PyModule_AddIntConstant (m, " PNG_FILTER_AVG" , PNG_FILTER_AVG) ||
685+ PyModule_AddIntConstant (m, " PNG_FILTER_PAETH" , PNG_FILTER_PAETH)) {
686+ INITERROR;
687+ }
688+
689+
576690#if PY3K
577691 return m;
578692#endif
0 commit comments