diff --git a/.travis.yml b/.travis.yml index b89406fc0..6226e5470 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,14 +16,14 @@ python: - 3.4 install: - - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick" - - "pip install cffi" - - "pip install coverage nose" + - "travis_retry sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick" + - "travis_retry pip install cffi" + - "travis_retry pip install coverage nose" # Pyroma installation is slow on Py3, so just do it for Py2. - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then travis_retry pip install pyroma; fi - - if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then pip install unittest2; fi + - if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then travis_retry pip install unittest2; fi # webp - pushd depends && ./install_webp.sh && popd @@ -37,26 +37,24 @@ script: - CFLAGS="-coverage" python setup.py build_ext --inplace - coverage run --append --include=PIL/* selftest.py - # FIXME: re-add -x option to fail fast - # - coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py - - coverage run --append --include=PIL/* -m nose -v Tests/test_*.py + - coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py after_success: # gather the coverage data - - sudo apt-get -qq install lcov + - travis_retry sudo apt-get -qq install lcov - lcov --capture --directory . -b . --output-file coverage.info # filter to remove system headers - lcov --remove coverage.info '/usr/*' -o coverage.filtered.info # convert to json - - gem install coveralls-lcov + - travis_retry gem install coveralls-lcov - coveralls-lcov -v -n coverage.filtered.info > coverage.c.json - coverage report - - pip install coveralls-merge + - travis_retry pip install coveralls-merge - coveralls-merge coverage.c.json - - pip install pep8 pyflakes + - travis_retry pip install pep8 pyflakes - pep8 --statistics --count PIL/*.py - pep8 --statistics --count Tests/*.py - pyflakes PIL/*.py | tee >(wc -l) @@ -67,3 +65,5 @@ after_success: # (Installation is very slow on Py3, so just do it for Py2.) - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then Scripts/diffcover-install.sh; fi - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then Scripts/diffcover-run.sh; fi +matrix: + fast_finish: true diff --git a/CHANGES.rst b/CHANGES.rst index b8dbab630..cd00ce285 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,10 +4,31 @@ Changelog (Pillow) 2.6.0 (unreleased) ------------------ -- Fix for reading multipage TIFFs #885 - [kostrom, wiredfool] +- On Windows, do not execute convert.exe without specifying path #912 + [cgohlke] -- Correctly handle saving gray and CMYK JPEGs with quality-keep #857 +- Fix msvc build error #911 + [cgohlke] + +- Fix for handling P + transparency -> RGBA conversions #904 + [wiredfool] + +- Retain alpha in ImageEnhance operations #909 + [wiredfool] + +- Jpeg2k Decode/encode memory leak fix #898 + [joshware, wiredfool] + +- EpsFilePlugin Speed improvements #886 + [wiredfool, karstenw] + +- Don't resize if already the right size #892 + [radarhere] + +- Fix for reading multipage TIFFs #885 + [kostrom, wiredfool] + +- Correctly handle saving gray and CMYK JPEGs with quality=keep #857 [etienned] - Correct duplicate Tiff Metadata and Exif tag values diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a31c9ee09..30c375a17 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ Send a pull request. We'll generally want documentation and [tests](Tests/README - Fork the repo - Make a branch - Add your changes + Tests -- Run the test suite. Try to run on both Python 2.x and 3.x, or you'll get tripped up. You can enable [Travis CI on your repo](https://travis-ci.org/profile/) to catch test failures prior to the pull request. +- Run the test suite. Try to run on both Python 2.x and 3.x, or you'll get tripped up. You can enable [Travis CI on your repo](https://travis-ci.org/profile/) to catch test failures prior to the pull request, and [Coveralls](https://coveralls.io/repos/new) to see if the changed code is covered by tests. - Push to your fork, and make a pull request. A few guidelines: diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index 9f963f7e6..58a4b7220 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -86,26 +86,32 @@ def Ghostscript(tile, size, fp, scale=1): out_fd, outfile = tempfile.mkstemp() os.close(out_fd) - in_fd, infile = tempfile.mkstemp() - os.close(in_fd) - - # ignore length and offset! - # ghostscript can read it - # copy whole file to read in ghostscript - with open(infile, 'wb') as f: - # fetch length of fp - fp.seek(0, 2) - fsize = fp.tell() - # ensure start position - # go back - fp.seek(0) - lengthfile = fsize - while lengthfile > 0: - s = fp.read(min(lengthfile, 100*1024)) - if not s: - break - length -= len(s) - f.write(s) + + infile_temp = None + if hasattr(fp, 'name') and os.path.exists(fp.name): + infile = fp.name + else: + in_fd, infile_temp = tempfile.mkstemp() + os.close(in_fd) + infile = infile_temp + + # ignore length and offset! + # ghostscript can read it + # copy whole file to read in ghostscript + with open(infile_temp, 'wb') as f: + # fetch length of fp + fp.seek(0, 2) + fsize = fp.tell() + # ensure start position + # go back + fp.seek(0) + lengthfile = fsize + while lengthfile > 0: + s = fp.read(min(lengthfile, 100*1024)) + if not s: + break + lengthfile -= len(s) + f.write(s) # Build ghostscript command command = ["gs", @@ -136,49 +142,36 @@ def Ghostscript(tile, size, fp, scale=1): finally: try: os.unlink(outfile) - os.unlink(infile) + if infile_temo: + os.unlink(infile_temp) except: pass return im class PSFile: - """Wrapper that treats either CR or LF as end of line.""" + """Wrapper for bytesio object that treats either CR or LF as end of line.""" def __init__(self, fp): self.fp = fp self.char = None - def __getattr__(self, id): - v = getattr(self.fp, id) - setattr(self, id, v) - return v def seek(self, offset, whence=0): self.char = None self.fp.seek(offset, whence) - def read(self, count): - return self.fp.read(count).decode('latin-1') - def readbinary(self, count): - return self.fp.read(count) - def tell(self): - pos = self.fp.tell() - if self.char: - pos -= 1 - return pos def readline(self): - s = b"" - if self.char: - c = self.char - self.char = None - else: - c = self.fp.read(1) + s = self.char or b"" + self.char = None + + c = self.fp.read(1) while c not in b"\r\n": s = s + c c = self.fp.read(1) - if c == b"\r": - self.char = self.fp.read(1) - if self.char == b"\n": - self.char = None - return s.decode('latin-1') + "\n" + self.char = self.fp.read(1) + # line endings can be 1 or 2 of \r \n, in either order + if self.char in b"\r\n": + self.char = None + + return s.decode('latin-1') def _accept(prefix): return prefix[:4] == b"%!PS" or i32(prefix) == 0xC6D3D0C5 @@ -193,36 +186,27 @@ class EpsImageFile(ImageFile.ImageFile): format = "EPS" format_description = "Encapsulated Postscript" + mode_map = { 1:"L", 2:"LAB", 3:"RGB" } + def _open(self): + (length, offset) = self._find_offset(self.fp) - fp = PSFile(self.fp) + # Rewrap the open file pointer in something that will + # convert line endings and decode to latin-1. + try: + if bytes is str: + # Python2, no encoding conversion necessary + fp = open(self.fp.name, "Ur") + else: + # Python3, can use bare open command. + fp = open(self.fp.name, "Ur", encoding='latin-1') + except Exception as msg: + # Expect this for bytesio/stringio + fp = PSFile(self.fp) - # FIX for: Some EPS file not handled correctly / issue #302 - # EPS can contain binary data - # or start directly with latin coding - # read header in both ways to handle both - # file types - # more info see http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - - # for HEAD without binary preview - s = fp.read(4) - # for HEAD with binary preview - fp.seek(0) - sb = fp.readbinary(160) - - if s[:4] == "%!PS": - fp.seek(0, 2) - length = fp.tell() - offset = 0 - elif i32(sb[0:4]) == 0xC6D3D0C5: - offset = i32(sb[4:8]) - length = i32(sb[8:12]) - else: - raise SyntaxError("not an EPS file") - - # go to offset - start of "%!PS" + # go to offset - start of "%!PS" fp.seek(offset) - + box = None self.mode = "RGB" @@ -231,18 +215,12 @@ class EpsImageFile(ImageFile.ImageFile): # # Load EPS header - s = fp.readline() + s = fp.readline().strip('\r\n') while s: - if len(s) > 255: raise SyntaxError("not an EPS file") - if s[-2:] == '\r\n': - s = s[:-2] - elif s[-1:] == '\n': - s = s[:-1] - try: m = split.match(s) except re.error as v: @@ -264,9 +242,7 @@ class EpsImageFile(ImageFile.ImageFile): pass else: - m = field.match(s) - if m: k = m.group(1) @@ -276,16 +252,16 @@ class EpsImageFile(ImageFile.ImageFile): self.info[k[:8]] = k[9:] else: self.info[k] = "" - elif s[0:1] == '%': + elif s[0] == '%': # handle non-DSC Postscript comments that some # tools mistakenly put in the Comments section pass else: raise IOError("bad EPS header") - s = fp.readline() + s = fp.readline().strip('\r\n') - if s[:1] != "%": + if s[0] != "%": break @@ -297,63 +273,48 @@ class EpsImageFile(ImageFile.ImageFile): if len(s) > 255: raise SyntaxError("not an EPS file") - if s[-2:] == '\r\n': - s = s[:-2] - elif s[-1:] == '\n': - s = s[:-1] - if s[:11] == "%ImageData:": + # Encoded bitmapped image. + [x, y, bi, mo, z3, z4, en, id] = s[11:].split(None, 7) - [x, y, bi, mo, z3, z4, en, id] =\ - s[11:].split(None, 7) - - x = int(x); y = int(y) - - bi = int(bi) - mo = int(mo) - - en = int(en) - - if en == 1: - decoder = "eps_binary" - elif en == 2: - decoder = "eps_hex" - else: + if int(bi) != 8: break - if bi != 8: + try: + self.mode = self.mode_map[int(mo)] + except: break - if mo == 1: - self.mode = "L" - elif mo == 2: - self.mode = "LAB" - elif mo == 3: - self.mode = "RGB" - else: - break - - if id[:1] == id[-1:] == '"': - id = id[1:-1] - - # Scan forward to the actual image data - while True: - s = fp.readline() - if not s: - break - if s[:len(id)] == id: - self.size = x, y - self.tile2 = [(decoder, - (0, 0, x, y), - fp.tell(), - 0)] - return - - s = fp.readline() + + self.size = int(x), int(y) + return + + s = fp.readline().strip('\r\n') if not s: break if not box: raise IOError("cannot determine EPS bounding box") + def _find_offset(self, fp): + + s = fp.read(160) + + if s[:4] == b"%!PS": + # for HEAD without binary preview + fp.seek(0, 2) + length = fp.tell() + offset = 0 + elif i32(s[0:4]) == 0xC6D3D0C5: + # FIX for: Some EPS file not handled correctly / issue #302 + # EPS can contain binary data + # or start directly with latin coding + # more info see http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf + offset = i32(s[4:8]) + length = i32(s[8:12]) + else: + raise SyntaxError("not an EPS file") + + return (length, offset) + def load(self, scale=1): # Load EPS via Ghostscript if not self.tile: diff --git a/PIL/Image.py b/PIL/Image.py index 5e1416a33..54291fbdd 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -530,7 +530,7 @@ class Image: """ Closes the file pointer, if possible. - This operation will destroy the image core and release it's memory. + This operation will destroy the image core and release its memory. The image data will be unusable afterward. This function is only required to close images that have not @@ -867,7 +867,17 @@ class Image: trns = trns_im.getpixel((0,0)) elif self.mode == 'P' and mode == 'RGBA': + t = self.info['transparency'] delete_trns = True + + if isinstance(t, bytes): + self.im.putpalettealphas(t) + elif isinstance(t, int): + self.im.putpalettealpha(t,0) + else: + raise ValueError("Transparency for P mode should" + + " be bytes or int") + if mode == "P" and palette == ADAPTIVE: im = self.im.quantize(colors) @@ -1514,6 +1524,9 @@ class Image: self.load() + if self.size == size: + return self._new(self.im) + if self.mode in ("1", "P"): resample = NEAREST diff --git a/PIL/ImageEnhance.py b/PIL/ImageEnhance.py index f802dc1d3..a196d5b09 100644 --- a/PIL/ImageEnhance.py +++ b/PIL/ImageEnhance.py @@ -47,8 +47,11 @@ class Color(_Enhance): """ def __init__(self, image): self.image = image - self.degenerate = image.convert("L").convert(image.mode) + self.intermediate_mode = 'L' + if 'A' in image.getbands(): + self.intermediate_mode = 'LA' + self.degenerate = image.convert(self.intermediate_mode).convert(image.mode) class Contrast(_Enhance): """Adjust image contrast. @@ -62,6 +65,9 @@ class Contrast(_Enhance): mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) self.degenerate = Image.new("L", image.size, mean).convert(image.mode) + if 'A' in image.getbands(): + self.degenerate.putalpha(image.split()[-1]) + class Brightness(_Enhance): """Adjust image brightness. @@ -74,6 +80,9 @@ class Brightness(_Enhance): self.image = image self.degenerate = Image.new(image.mode, image.size, 0) + if 'A' in image.getbands(): + self.degenerate.putalpha(image.split()[-1]) + class Sharpness(_Enhance): """Adjust image sharpness. @@ -85,3 +94,6 @@ class Sharpness(_Enhance): def __init__(self, image): self.image = image self.degenerate = image.filter(ImageFilter.SMOOTH) + + if 'A' in image.getbands(): + self.degenerate.putalpha(image.split()[-1]) diff --git a/PIL/ImageFile.py b/PIL/ImageFile.py index 5e4745d76..bdcc769ec 100644 --- a/PIL/ImageFile.py +++ b/PIL/ImageFile.py @@ -227,6 +227,8 @@ class ImageFile(Image.Image): break b = b[n:] t = t + n + # Need to cleanup here to prevent leaks in PyPy + d.cleanup() self.tile = [] self.readonly = readonly @@ -471,6 +473,7 @@ def _save(im, fp, tile, bufsize=0): break if s < 0: raise IOError("encoder error %d when writing image file" % s) + e.cleanup() else: # slight speedup: compress to real file object for e, b, o, a in tile: @@ -481,6 +484,7 @@ def _save(im, fp, tile, bufsize=0): s = e.encode_to_file(fh, bufsize) if s < 0: raise IOError("encoder error %d when writing image file" % s) + e.cleanup() try: fp.flush() except: pass diff --git a/PIL/ImageGrab.py b/PIL/ImageGrab.py index 9bb190934..ef0135334 100644 --- a/PIL/ImageGrab.py +++ b/PIL/ImageGrab.py @@ -17,6 +17,9 @@ from PIL import Image +import sys +if sys.platform != "win32": + raise ImportError("ImageGrab is Windows only") try: # built-in driver (1.1.3 and later) @@ -40,7 +43,7 @@ def grab(bbox=None): def grabclipboard(): - debug = 0 # temporary interface + debug = 0 # temporary interface data = Image.core.grabclipboard(debug) if isinstance(data, bytes): from PIL import BmpImagePlugin diff --git a/README.rst b/README.rst index 482a1e2ec..a5cb77785 100644 --- a/README.rst +++ b/README.rst @@ -19,4 +19,3 @@ Pillow is the "friendly" PIL fork by `Alex Clark and Contributors cleanup) + decoder->cleanup(&decoder->state); free(decoder->state.buffer); free(decoder->state.context); Py_XDECREF(decoder->lock); diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 97caea722..cd187a4a2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -670,6 +670,7 @@ files, using either JPEG or HEX encoding depending on the image mode (and whether JPEG support is available or not). PIXAR (read only) +^^^^ PIL provides limited support for PIXAR raster files. The library can identify and read “dumped” RGB files. diff --git a/encode.c b/encode.c index 3fa900b1d..34e3b8933 100644 --- a/encode.c +++ b/encode.c @@ -99,6 +99,18 @@ _dealloc(ImagingEncoderObject* encoder) PyObject_Del(encoder); } +static PyObject* +_encode_cleanup(ImagingEncoderObject* encoder, PyObject* args) +{ + int status = 0; + + if (encoder->cleanup){ + status = encoder->cleanup(&encoder->state); + } + + return Py_BuildValue("i", status); +} + static PyObject* _encode(ImagingEncoderObject* encoder, PyObject* args) { @@ -239,6 +251,7 @@ _setimage(ImagingEncoderObject* encoder, PyObject* args) static struct PyMethodDef methods[] = { {"encode", (PyCFunction)_encode, 1}, + {"cleanup", (PyCFunction)_encode_cleanup, 1}, {"encode_to_file", (PyCFunction)_encode_to_file, 1}, {"setimage", (PyCFunction)_setimage, 1}, {NULL, NULL} /* sentinel */ diff --git a/libImaging/Jpeg2KDecode.c b/libImaging/Jpeg2KDecode.c index 97ec81003..abf8cebbe 100644 --- a/libImaging/Jpeg2KDecode.c +++ b/libImaging/Jpeg2KDecode.c @@ -800,6 +800,11 @@ ImagingJpeg2KDecodeCleanup(ImagingCodecState state) { if (context->decoder) ImagingIncrementalCodecDestroy(context->decoder); + context->error_msg = NULL; + + /* Prevent multiple calls to ImagingIncrementalCodecDestroy */ + context->decoder = NULL; + return -1; } diff --git a/libImaging/Jpeg2KEncode.c b/libImaging/Jpeg2KEncode.c old mode 100644 new mode 100755 index e8eef08c1..ea4bca2f2 --- a/libImaging/Jpeg2KEncode.c +++ b/libImaging/Jpeg2KEncode.c @@ -576,15 +576,20 @@ int ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { JPEG2KENCODESTATE *context = (JPEG2KENCODESTATE *)state->context; - if (context->quality_layers) + if (context->quality_layers && context->encoder) Py_DECREF(context->quality_layers); if (context->error_msg) free ((void *)context->error_msg); + context->error_msg = NULL; + if (context->encoder) ImagingIncrementalCodecDestroy(context->encoder); + /* Prevent multiple calls to ImagingIncrementalCodecDestroy */ + context->encoder = NULL; + return -1; } diff --git a/libImaging/TiffDecode.c b/libImaging/TiffDecode.c index 1d320e9bd..76bd887a7 100644 --- a/libImaging/TiffDecode.c +++ b/libImaging/TiffDecode.c @@ -221,9 +221,10 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int } if (clientstate->ifd){ - unsigned int ifdoffset = clientstate->ifd; + int rv; + unsigned int ifdoffset = clientstate->ifd; TRACE(("reading tiff ifd %d\n", ifdoffset)); - int rv = TIFFSetSubDirectory(tiff, ifdoffset); + rv = TIFFSetSubDirectory(tiff, ifdoffset); if (!rv){ TRACE(("error in TIFFSetSubDirectory")); return -1;