diff --git a/Tests/helper.py b/Tests/helper.py index 65b1ca8bd..989215ca4 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -170,6 +170,41 @@ class PillowTestCase(unittest.TestCase): return Image.open(outfile) raise IOError() +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +class PillowLeakTestCase(PillowTestCase): + # requires unix/osx + iterations = 100 # count + mem_limit = 512 # k + + def _get_mem_usage(self): + """ + Gets the RUSAGE memory usage, returns in K. Encapsulates the difference + between OSX and Linux rss reporting + + :returns; memory usage in kilobytes + """ + + from resource import getpagesize, getrusage, RUSAGE_SELF + mem = getrusage(RUSAGE_SELF).ru_maxrss + if sys.platform == 'darwin': + # man 2 getrusage: + # ru_maxrss the maximum resident set size utilized (in bytes). + return mem / 1024 # Kb + else: + # linux + # man 2 getrusage + # ru_maxrss (since Linux 2.6.32) + # This is the maximum resident set size used (in kilobytes). + return mem # Kb + + def _test_leak(self, core): + start_mem = self._get_mem_usage() + for cycle in range(self.iterations): + core() + mem = (self._get_mem_usage() - start_mem) + self.assertLess(mem, self.mem_limit, + msg='memory usage limit exceeded in iteration %d' % cycle) + # helpers diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index e7b747622..200c93b74 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from helper import unittest, PillowTestCase, PillowLeakTestCase, hopper from PIL import Image, ImageFile, PngImagePlugin from io import BytesIO @@ -7,9 +7,6 @@ import sys codecs = dir(Image.core) -# For Truncated phng memory leak -MEM_LIMIT = 2 # max increase in MB -ITERATIONS = 100 # Leak is 56k/iteration, this will leak 5.6megs # sample png stream @@ -539,26 +536,14 @@ class TestFilePng(PillowTestCase): @unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") -class TestTruncatedPngPLeaks(PillowTestCase): +class TestTruncatedPngPLeaks(PillowLeakTestCase): + mem_limit = 2*1024 # max increase in K + iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs def setUp(self): if "zip_encoder" not in codecs or "zip_decoder" not in codecs: self.skipTest("zip/deflate support not available") - def _get_mem_usage(self): - from resource import getpagesize, getrusage, RUSAGE_SELF - mem = getrusage(RUSAGE_SELF).ru_maxrss - if sys.platform == 'darwin': - # man 2 getrusage: - # ru_maxrss the maximum resident set size utilized (in bytes). - return mem / 1024 / 1024 # megs - else: - # linux - # man 2 getrusage - # ru_maxrss (since Linux 2.6.32) - # This is the maximum resident set size used (in kilobytes). - return mem / 1024 # megs - def test_leak_load(self): with open('Tests/images/hopper.png', 'rb') as f: DATA = BytesIO(f.read(16 * 1024)) @@ -566,13 +551,13 @@ class TestTruncatedPngPLeaks(PillowTestCase): ImageFile.LOAD_TRUNCATED_IMAGES = True with Image.open(DATA) as im: im.load() - start_mem = self._get_mem_usage() + + def core(): + with Image.open(DATA) as im: + im.load() + try: - for _ in range(ITERATIONS): - with Image.open(DATA) as im: - im.load() - mem = (self._get_mem_usage() - start_mem) - self.assertLess(mem, MEM_LIMIT, msg='memory usage limit exceeded') + self._test_leak(core) finally: ImageFile.LOAD_TRUNCATED_IMAGES = False diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py new file mode 100644 index 000000000..709339233 --- /dev/null +++ b/Tests/test_font_leaks.py @@ -0,0 +1,34 @@ +from __future__ import division +from helper import unittest, PillowLeakTestCase +import sys +from PIL import Image, features, ImageDraw, ImageFont + +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +class TestTTypeFontLeak(PillowLeakTestCase): + # fails at iteration 3 in master + iterations = 10 + mem_limit = 4096 #k + + def _test_font(self, font): + im = Image.new('RGB', (255,255), 'white') + draw = ImageDraw.ImageDraw(im) + self._test_leak(lambda: draw.text((0, 0), "some text "*1024, #~10k + font=font, fill="black")) + + @unittest.skipIf(not features.check('freetype2'), "Test requires freetype2") + def test_leak(self): + ttype = ImageFont.truetype('Tests/fonts/FreeMono.ttf', 20) + self._test_font(ttype) + +class TestDefaultFontLeak(TestTTypeFontLeak): + # fails at iteration 37 in master + iterations = 100 + mem_limit = 1024 #k + + def test_leak(self): + default_font = ImageFont.load_default() + self._test_font(default_font) + + +if __name__ == '__main__': + unittest.main() diff --git a/_imaging.c b/_imaging.c index b7726a2cc..60a97aa19 100644 --- a/_imaging.c +++ b/_imaging.c @@ -2198,26 +2198,45 @@ textwidth(ImagingFontObject* self, const unsigned char* text) } void _font_text_asBytes(PyObject* encoded_string, unsigned char** text){ + /* Allocates *text, returns a 'new reference'. Caller is required to free */ + PyObject* bytes = NULL; + Py_ssize_t len = 0; + char *buffer; *text = NULL; if (PyUnicode_CheckExact(encoded_string)){ bytes = PyUnicode_AsLatin1String(encoded_string); + PyBytes_AsStringAndSize(bytes, &buffer, &len); } else if (PyBytes_Check(encoded_string)) { - bytes = encoded_string; + PyBytes_AsStringAndSize(encoded_string, &buffer, &len); } - if (bytes) { - *text = (unsigned char*)PyBytes_AsString(bytes); + + if (len) { + *text = calloc(len,1); + if (*text) { + memcpy(*text, buffer, len); + } + if(bytes) { + Py_DECREF(bytes); + } return; } + #if PY_VERSION_HEX < 0x03000000 /* likely case here is py2.x with an ordinary string. but this isn't defined in Py3.x */ if (PyString_Check(encoded_string)) { - *text = (unsigned char *)PyString_AsString(encoded_string); + PyString_AsStringAndSize(encoded_string, &buffer, &len); + *text = calloc(len,1); + if (*text) { + memcpy(*text, buffer, len); + } + return; } + #endif } @@ -2248,6 +2267,7 @@ _font_getmask(ImagingFontObject* self, PyObject* args) im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize); if (!im) { + free(text); return NULL; } @@ -2273,9 +2293,11 @@ _font_getmask(ImagingFontObject* self, PyObject* args) x = x + glyph->dx; b = b + glyph->dy; } + free(text); return PyImagingNew(im); failed: + free(text); ImagingDelete(im); return NULL; } @@ -2285,6 +2307,7 @@ _font_getsize(ImagingFontObject* self, PyObject* args) { unsigned char* text; PyObject* encoded_string; + PyObject* val; if (!PyArg_ParseTuple(args, "O:getsize", &encoded_string)) return NULL; @@ -2294,7 +2317,9 @@ _font_getsize(ImagingFontObject* self, PyObject* args) return NULL; } - return Py_BuildValue("ii", textwidth(self, text), self->ysize); + val = Py_BuildValue("ii", textwidth(self, text), self->ysize); + free(text); + return val; } static struct PyMethodDef _font_methods[] = { diff --git a/_imagingft.c b/_imagingft.c index 21188996d..af9fb3dfb 100644 --- a/_imagingft.c +++ b/_imagingft.c @@ -497,6 +497,11 @@ font_getsize(FontObject* self, PyObject* args) FT_Done_Glyph(glyph); } + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + if (face) { /* left bearing */