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) return Image.open(outfile)
raise IOError() 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 # 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 PIL import Image, ImageFile, PngImagePlugin
from io import BytesIO from io import BytesIO
@ -7,9 +7,6 @@ import sys
codecs = dir(Image.core) 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 # sample png stream
@ -539,26 +536,14 @@ class TestFilePng(PillowTestCase):
@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") @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): def setUp(self):
if "zip_encoder" not in codecs or "zip_decoder" not in codecs: if "zip_encoder" not in codecs or "zip_decoder" not in codecs:
self.skipTest("zip/deflate support not available") 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): def test_leak_load(self):
with open('Tests/images/hopper.png', 'rb') as f: with open('Tests/images/hopper.png', 'rb') as f:
DATA = BytesIO(f.read(16 * 1024)) DATA = BytesIO(f.read(16 * 1024))
@ -566,13 +551,13 @@ class TestTruncatedPngPLeaks(PillowTestCase):
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
with Image.open(DATA) as im: with Image.open(DATA) as im:
im.load() im.load()
start_mem = self._get_mem_usage()
def core():
with Image.open(DATA) as im:
im.load()
try: try:
for _ in range(ITERATIONS): self._test_leak(core)
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')
finally: finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False 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){ void _font_text_asBytes(PyObject* encoded_string, unsigned char** text){
/* Allocates *text, returns a 'new reference'. Caller is required to free */
PyObject* bytes = NULL; PyObject* bytes = NULL;
Py_ssize_t len = 0;
char *buffer;
*text = NULL; *text = NULL;
if (PyUnicode_CheckExact(encoded_string)){ if (PyUnicode_CheckExact(encoded_string)){
bytes = PyUnicode_AsLatin1String(encoded_string); bytes = PyUnicode_AsLatin1String(encoded_string);
PyBytes_AsStringAndSize(bytes, &buffer, &len);
} else if (PyBytes_Check(encoded_string)) { } 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; return;
} }
#if PY_VERSION_HEX < 0x03000000 #if PY_VERSION_HEX < 0x03000000
/* likely case here is py2.x with an ordinary string. /* likely case here is py2.x with an ordinary string.
but this isn't defined in Py3.x */ but this isn't defined in Py3.x */
if (PyString_Check(encoded_string)) { 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 #endif
} }
@ -2248,6 +2267,7 @@ _font_getmask(ImagingFontObject* self, PyObject* args)
im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize); im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize);
if (!im) { if (!im) {
free(text);
return NULL; return NULL;
} }
@ -2273,9 +2293,11 @@ _font_getmask(ImagingFontObject* self, PyObject* args)
x = x + glyph->dx; x = x + glyph->dx;
b = b + glyph->dy; b = b + glyph->dy;
} }
free(text);
return PyImagingNew(im); return PyImagingNew(im);
failed: failed:
free(text);
ImagingDelete(im); ImagingDelete(im);
return NULL; return NULL;
} }
@ -2285,6 +2307,7 @@ _font_getsize(ImagingFontObject* self, PyObject* args)
{ {
unsigned char* text; unsigned char* text;
PyObject* encoded_string; PyObject* encoded_string;
PyObject* val;
if (!PyArg_ParseTuple(args, "O:getsize", &encoded_string)) if (!PyArg_ParseTuple(args, "O:getsize", &encoded_string))
return NULL; return NULL;
@ -2294,7 +2317,9 @@ _font_getsize(ImagingFontObject* self, PyObject* args)
return NULL; 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[] = { static struct PyMethodDef _font_methods[] = {

View File

@ -497,6 +497,11 @@ font_getsize(FontObject* self, PyObject* args)
FT_Done_Glyph(glyph); FT_Done_Glyph(glyph);
} }
if (glyph_info) {
PyMem_Free(glyph_info);
glyph_info = NULL;
}
if (face) { if (face) {
/* left bearing */ /* left bearing */