Merge pull request #2634 from wiredfool/issue_2629

Fix for memory leaks in font handling
This commit is contained in:
wiredfool 2017-09-04 11:23:57 +01:00 committed by GitHub
commit e71757aa6f
5 changed files with 114 additions and 30 deletions

View File

@ -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

View File

@ -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

34
Tests/test_font_leaks.py Normal file
View File

@ -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()

View File

@ -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[] = {

View File

@ -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 */