diff --git a/.travis.yml b/.travis.yml index 389051358..d4880847c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ language: python notifications: irc: "chat.freenode.net#pil" +env: MAX_CONCURRENCY=4 + python: - "pypy" - 2.6 @@ -12,9 +14,9 @@ python: - 3.4 install: - - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev cmake" + - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake" - "pip install cffi" - - "pip install coveralls nose" + - "pip install coveralls nose pyroma" - if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then pip install unittest2; fi # webp diff --git a/CHANGES.rst b/CHANGES.rst index 06f6e7090..ea5f56b95 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,39 @@ Changelog (Pillow) 2.5.0 (unreleased) ------------------ +- Prevent shell injection #748 + [mbrown1413, wiredfool] + +- Support for Resolution in BMP files #734 + [gcq] + +- Fix error in setup.py for Python 3 + [matthew-brett] + +- Pyroma fix and add Python 3.4 to setup metadata #742 + [wirefool] + +- Top level flake8 fixes #741 + [aclark] + +- Remove obsolete Animated Raster Graphics (ARG) support + [hugovk] + +- Fix test_imagedraw failures #727 + [cgohlke] + +- Fix AttributeError: class Image has no attribute 'DEBUG' #726 + [cgohlke] + +- Fix msvc warning: 'inline' : macro redefinition #725 + [cgohlke] + +- Cleanup #654 + [dvska, hugovk, wiredfool] + +- 16-bit monochrome support for JPEG2000 + [videan42] + - Fixed ImagePalette.save [brightpisces] diff --git a/MANIFEST.in b/MANIFEST.in index c2358f76f..00c132d47 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,8 @@ include *.c include *.h include *.py include *.rst +include *.txt +include .coveragerc include .gitattributes include .travis.yml include Makefile @@ -28,29 +30,45 @@ recursive-include Sane CHANGES recursive-include Sane README recursive-include Scripts *.py recursive-include Scripts README +recursive-include Tests *.bdf recursive-include Tests *.bin recursive-include Tests *.bmp +recursive-include Tests *.doc recursive-include Tests *.eps +recursive-include Tests *.fli recursive-include Tests *.gif recursive-include Tests *.gnuplot recursive-include Tests *.html recursive-include Tests *.icm recursive-include Tests *.icns recursive-include Tests *.ico +recursive-include Tests *.j2k recursive-include Tests *.jp2 recursive-include Tests *.jpg +recursive-include Tests *.lut +recursive-include Tests *.pbm recursive-include Tests *.pcf recursive-include Tests *.pcx +recursive-include Tests *.pgm +recursive-include Tests *.pil recursive-include Tests *.png recursive-include Tests *.ppm +recursive-include Tests *.psd recursive-include Tests *.py +recursive-include Tests *.rst +recursive-include Tests *.spider +recursive-include Tests *.tar recursive-include Tests *.tif recursive-include Tests *.tiff recursive-include Tests *.ttf recursive-include Tests *.txt +recursive-include Tests *.webp +recursive-include Tests *.xpm recursive-include Tk *.c recursive-include Tk *.txt +recursive-include Tk *.rst recursive-include depends *.sh +recursive-include depends *.rst recursive-include docs *.bat recursive-include docs *.gitignore recursive-include docs *.html @@ -64,3 +82,5 @@ recursive-include docs COPYING recursive-include docs LICENSE recursive-include libImaging *.c recursive-include libImaging *.h +recursive-include Sane *.rst +recursive-include Scripts *.rst diff --git a/Makefile b/Makefile index dca36d4ed..4f6a00711 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,11 @@ pre: + virtualenv . + bin/pip install -r requirements.txt bin/python setup.py develop bin/python selftest.py - bin/python Tests/run.py + bin/nosetests Tests/test_*.py + bin/python setup.py install + bin/python test-installed.py check-manifest pyroma . viewdoc diff --git a/PIL/ArgImagePlugin.py b/PIL/ArgImagePlugin.py deleted file mode 100644 index 7fc167c60..000000000 --- a/PIL/ArgImagePlugin.py +++ /dev/null @@ -1,506 +0,0 @@ -# -# THIS IS WORK IN PROGRESS -# -# The Python Imaging Library. -# $Id$ -# -# ARG animation support code -# -# history: -# 1996-12-30 fl Created -# 1996-01-06 fl Added safe scripting environment -# 1996-01-10 fl Added JHDR, UHDR and sYNC support -# 2005-03-02 fl Removed AAPP and ARUN support -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996-97. -# -# See the README file for information on usage and redistribution. -# - -from __future__ import print_function - -__version__ = "0.4" - -from PIL import Image, ImageFile, ImagePalette - -from PIL.PngImagePlugin import i8, i16, i32, ChunkStream, _MODES - -MAGIC = b"\212ARG\r\n\032\n" - -# -------------------------------------------------------------------- -# ARG parser - -class ArgStream(ChunkStream): - "Parser callbacks for ARG data" - - def __init__(self, fp): - - ChunkStream.__init__(self, fp) - - self.eof = 0 - - self.im = None - self.palette = None - - self.__reset() - - def __reset(self): - - # reset decoder state (called on init and sync) - - self.count = 0 - self.id = None - self.action = ("NONE",) - - self.images = {} - self.names = {} - - - def chunk_AHDR(self, offset, bytes): - "AHDR -- animation header" - - # assertions - if self.count != 0: - raise SyntaxError("misplaced AHDR chunk") - - s = self.fp.read(bytes) - self.size = i32(s), i32(s[4:]) - try: - self.mode, self.rawmode = _MODES[(i8(s[8]), i8(s[9]))] - except: - raise SyntaxError("unknown ARG mode") - - if Image.DEBUG: - print("AHDR size", self.size) - print("AHDR mode", self.mode, self.rawmode) - - return s - - def chunk_AFRM(self, offset, bytes): - "AFRM -- next frame follows" - - # assertions - if self.count != 0: - raise SyntaxError("misplaced AFRM chunk") - - self.show = 1 - self.id = 0 - self.count = 1 - self.repair = None - - s = self.fp.read(bytes) - if len(s) >= 2: - self.id = i16(s) - if len(s) >= 4: - self.count = i16(s[2:4]) - if len(s) >= 6: - self.repair = i16(s[4:6]) - else: - self.repair = None - - if Image.DEBUG: - print("AFRM", self.id, self.count) - - return s - - def chunk_ADEF(self, offset, bytes): - "ADEF -- store image" - - # assertions - if self.count != 0: - raise SyntaxError("misplaced ADEF chunk") - - self.show = 0 - self.id = 0 - self.count = 1 - self.repair = None - - s = self.fp.read(bytes) - if len(s) >= 2: - self.id = i16(s) - if len(s) >= 4: - self.count = i16(s[2:4]) - - if Image.DEBUG: - print("ADEF", self.id, self.count) - - return s - - def chunk_NAME(self, offset, bytes): - "NAME -- name the current image" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced NAME chunk") - - name = self.fp.read(bytes) - self.names[self.id] = name - - return name - - def chunk_AEND(self, offset, bytes): - "AEND -- end of animation" - - if Image.DEBUG: - print("AEND") - - self.eof = 1 - - raise EOFError("end of ARG file") - - def __getmodesize(self, s, full=1): - - size = i32(s), i32(s[4:]) - - try: - mode, rawmode = _MODES[(i8(s[8]), i8(s[9]))] - except: - raise SyntaxError("unknown image mode") - - if full: - if i8(s[12]): - pass # interlace not yet supported - if i8(s[11]): - raise SyntaxError("unknown filter category") - - return size, mode, rawmode - - def chunk_PAST(self, offset, bytes): - "PAST -- paste one image into another" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced PAST chunk") - - if self.repair is not None: - # we must repair the target image before we - # start pasting - - # brute force; a better solution would be to - # update only the dirty rectangles in images[id]. - # note that if images[id] doesn't exist, it must - # be created - - self.images[self.id] = self.images[self.repair].copy() - self.repair = None - - s = self.fp.read(bytes) - im = self.images[i16(s)] - x, y = i32(s[2:6]), i32(s[6:10]) - bbox = x, y, im.size[0]+x, im.size[1]+y - - if im.mode in ["RGBA"]: - # paste with transparency - # FIXME: should handle P+transparency as well - self.images[self.id].paste(im, bbox, im) - else: - # paste without transparency - self.images[self.id].paste(im, bbox) - - self.action = ("PAST",) - self.__store() - - return s - - def chunk_BLNK(self, offset, bytes): - "BLNK -- create blank image" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced BLNK chunk") - - s = self.fp.read(bytes) - size, mode, rawmode = self.__getmodesize(s, 0) - - # store image (FIXME: handle colour) - self.action = ("BLNK",) - self.im = Image.core.fill(mode, size, 0) - self.__store() - - return s - - def chunk_IHDR(self, offset, bytes): - "IHDR -- full image follows" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced IHDR chunk") - - # image header - s = self.fp.read(bytes) - size, mode, rawmode = self.__getmodesize(s) - - # decode and store image - self.action = ("IHDR",) - self.im = Image.core.new(mode, size) - self.decoder = Image.core.zip_decoder(rawmode) - self.decoder.setimage(self.im, (0,0) + size) - self.data = b"" - - return s - - def chunk_DHDR(self, offset, bytes): - "DHDR -- delta image follows" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced DHDR chunk") - - s = self.fp.read(bytes) - - size, mode, rawmode = self.__getmodesize(s) - - # delta header - diff = i8(s[13]) - offs = i32(s[14:18]), i32(s[18:22]) - - bbox = offs + (offs[0]+size[0], offs[1]+size[1]) - - if Image.DEBUG: - print("DHDR", diff, bbox) - - # FIXME: decode and apply image - self.action = ("DHDR", diff, bbox) - - # setup decoder - self.im = Image.core.new(mode, size) - - self.decoder = Image.core.zip_decoder(rawmode) - self.decoder.setimage(self.im, (0,0) + size) - - self.data = b"" - - return s - - def chunk_JHDR(self, offset, bytes): - "JHDR -- JPEG image follows" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced JHDR chunk") - - # image header - s = self.fp.read(bytes) - size, mode, rawmode = self.__getmodesize(s, 0) - - # decode and store image - self.action = ("JHDR",) - self.im = Image.core.new(mode, size) - self.decoder = Image.core.jpeg_decoder(rawmode) - self.decoder.setimage(self.im, (0,0) + size) - self.data = b"" - - return s - - def chunk_UHDR(self, offset, bytes): - "UHDR -- uncompressed image data follows (EXPERIMENTAL)" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced UHDR chunk") - - # image header - s = self.fp.read(bytes) - size, mode, rawmode = self.__getmodesize(s, 0) - - # decode and store image - self.action = ("UHDR",) - self.im = Image.core.new(mode, size) - self.decoder = Image.core.raw_decoder(rawmode) - self.decoder.setimage(self.im, (0,0) + size) - self.data = b"" - - return s - - def chunk_IDAT(self, offset, bytes): - "IDAT -- image data block" - - # pass compressed chunks through the decoder - s = self.fp.read(bytes) - self.data = self.data + s - n, e = self.decoder.decode(self.data) - if n < 0: - # end of image - if e < 0: - raise IOError("decoder error %d" % e) - else: - self.data = self.data[n:] - - return s - - def chunk_DEND(self, offset, bytes): - return self.chunk_IEND(offset, bytes) - - def chunk_JEND(self, offset, bytes): - return self.chunk_IEND(offset, bytes) - - def chunk_UEND(self, offset, bytes): - return self.chunk_IEND(offset, bytes) - - def chunk_IEND(self, offset, bytes): - "IEND -- end of image" - - # we now have a new image. carry out the operation - # defined by the image header. - - # won't need these anymore - del self.decoder - del self.data - - self.__store() - - return self.fp.read(bytes) - - def __store(self): - - # apply operation - cid = self.action[0] - - if cid in ["BLNK", "IHDR", "JHDR", "UHDR"]: - # store - self.images[self.id] = self.im - - elif cid == "DHDR": - # paste - cid, mode, bbox = self.action - im0 = self.images[self.id] - im1 = self.im - if mode == 0: - im1 = im1.chop_add_modulo(im0.crop(bbox)) - im0.paste(im1, bbox) - - self.count -= 1 - - if self.count == 0 and self.show: - self.im = self.images[self.id] - raise EOFError # end of this frame - - def chunk_PLTE(self, offset, bytes): - "PLTE -- palette data" - - s = self.fp.read(bytes) - if self.mode == "P": - self.palette = ImagePalette.raw("RGB", s) - return s - - def chunk_sYNC(self, offset, bytes): - "SYNC -- reset decoder" - - if self.count != 0: - raise SyntaxError("misplaced sYNC chunk") - - s = self.fp.read(bytes) - self.__reset() - return s - - -# -------------------------------------------------------------------- -# ARG reader - -def _accept(prefix): - return prefix[:8] == MAGIC - -## -# Image plugin for the experimental Animated Raster Graphics format. - -class ArgImageFile(ImageFile.ImageFile): - - format = "ARG" - format_description = "Animated raster graphics" - - def _open(self): - - if Image.warnings: - Image.warnings.warn( - "The ArgImagePlugin driver is obsolete, and will be removed " - "from a future release of PIL. If you rely on this module, " - "please contact the PIL authors.", - RuntimeWarning - ) - - if self.fp.read(8) != MAGIC: - raise SyntaxError("not an ARG file") - - self.arg = ArgStream(self.fp) - - # read and process the first chunk (AHDR) - - cid, offset, bytes = self.arg.read() - - if cid != "AHDR": - raise SyntaxError("expected an AHDR chunk") - - s = self.arg.call(cid, offset, bytes) - - self.arg.crc(cid, s) - - # image characteristics - self.mode = self.arg.mode - self.size = self.arg.size - - def load(self): - - if self.arg.im is None: - self.seek(0) - - # image data - self.im = self.arg.im - self.palette = self.arg.palette - - # set things up for further processing - Image.Image.load(self) - - def seek(self, frame): - - if self.arg.eof: - raise EOFError("end of animation") - - self.fp = self.arg.fp - - while True: - - # - # process chunks - - cid, offset, bytes = self.arg.read() - - if self.arg.eof: - raise EOFError("end of animation") - - try: - s = self.arg.call(cid, offset, bytes) - except EOFError: - break - - except "glurk": # AttributeError - if Image.DEBUG: - print(cid, bytes, "(unknown)") - s = self.fp.read(bytes) - - self.arg.crc(cid, s) - - self.fp.read(4) # ship extra CRC - - def tell(self): - return 0 - - def verify(self): - "Verify ARG file" - - # back up to first chunk - self.fp.seek(8) - - self.arg.verify(self) - self.arg.close() - - self.fp = None - -# -# -------------------------------------------------------------------- - -Image.register_open("ARG", ArgImageFile, _accept) - -Image.register_extension("ARG", ".arg") - -Image.register_mime("ARG", "video/x-arg") diff --git a/PIL/BmpImagePlugin.py b/PIL/BmpImagePlugin.py index 436ca5dce..fae6bd391 100644 --- a/PIL/BmpImagePlugin.py +++ b/PIL/BmpImagePlugin.py @@ -28,6 +28,7 @@ __version__ = "0.7" from PIL import Image, ImageFile, ImagePalette, _binary +import math i8 = _binary.i8 i16 = _binary.i16le @@ -88,6 +89,7 @@ class BmpImageFile(ImageFile.ImageFile): bits = i16(s[14:]) self.size = i32(s[4:]), i32(s[8:]) compression = i32(s[16:]) + pxperm = (i32(s[24:]), i32(s[28:])) # Pixels per meter lutsize = 4 colors = i32(s[32:]) direction = -1 @@ -95,6 +97,8 @@ class BmpImageFile(ImageFile.ImageFile): # upside-down storage self.size = self.size[0], 2**32 - self.size[1] direction = 0 + + self.info["dpi"] = tuple(map(lambda x: math.ceil(x / 39.3701), pxperm)) else: raise IOError("Unsupported BMP header type (%d)" % len(s)) @@ -203,30 +207,37 @@ def _save(im, fp, filename, check=0): if check: return check + info = im.encoderinfo + + dpi = info.get("dpi", (96, 96)) + + # 1 meter == 39.3701 inches + ppm = tuple(map(lambda x: int(x * 39.3701), dpi)) + stride = ((im.size[0]*bits+7)//8+3)&(~3) header = 40 # or 64 for OS/2 version 2 offset = 14 + header + colors * 4 image = stride * im.size[1] # bitmap header - fp.write(b"BM" + # file type (magic) - o32(offset+image) + # file size - o32(0) + # reserved - o32(offset)) # image data offset + fp.write(b"BM" + # file type (magic) + o32(offset+image) + # file size + o32(0) + # reserved + o32(offset)) # image data offset # bitmap info header - fp.write(o32(header) + # info header size - o32(im.size[0]) + # width - o32(im.size[1]) + # height - o16(1) + # planes - o16(bits) + # depth - o32(0) + # compression (0=uncompressed) - o32(image) + # size of bitmap - o32(1) + o32(1) + # resolution - o32(colors) + # colors used - o32(colors)) # colors important + fp.write(o32(header) + # info header size + o32(im.size[0]) + # width + o32(im.size[1]) + # height + o16(1) + # planes + o16(bits) + # depth + o32(0) + # compression (0=uncompressed) + o32(image) + # size of bitmap + o32(ppm[0]) + o32(ppm[1]) + # resolution + o32(colors) + # colors used + o32(colors)) # colors important - fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) + fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) if im.mode == "1": for i in (0, 255): diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index c6d449425..ec8301973 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -333,13 +333,41 @@ def _save_netpbm(im, fp, filename): # below for information on how to enable this. import os + from subprocess import Popen, check_call, PIPE, CalledProcessError + import tempfile file = im._dump() + if im.mode != "RGB": - os.system("ppmtogif %s >%s" % (file, filename)) + with open(filename, 'wb') as f: + stderr = tempfile.TemporaryFile() + check_call(["ppmtogif", file], stdout=f, stderr=stderr) else: - os.system("ppmquant 256 %s | ppmtogif >%s" % (file, filename)) - try: os.unlink(file) - except: pass + with open(filename, 'wb') as f: + + # Pipe ppmquant output into ppmtogif + # "ppmquant 256 %s | ppmtogif > %s" % (file, filename) + quant_cmd = ["ppmquant", "256", file] + togif_cmd = ["ppmtogif"] + stderr = tempfile.TemporaryFile() + quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=stderr) + stderr = tempfile.TemporaryFile() + togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=stderr) + + # Allow ppmquant to receive SIGPIPE if ppmtogif exits + quant_proc.stdout.close() + + retcode = quant_proc.wait() + if retcode: + raise CalledProcessError(retcode, quant_cmd) + + retcode = togif_proc.wait() + if retcode: + raise CalledProcessError(retcode, togif_cmd) + + try: + os.unlink(file) + except: + pass # -------------------------------------------------------------------- diff --git a/PIL/Image.py b/PIL/Image.py index 0d97e9dc7..787e60270 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -539,7 +539,7 @@ class Image: try: self.fp.close() except Exception as msg: - if Image.DEBUG: + if DEBUG: print ("Error closing: %s" % msg) # Instead of simply setting to None, we're setting up a diff --git a/PIL/Jpeg2KImagePlugin.py b/PIL/Jpeg2KImagePlugin.py index c4c980f6e..66069802e 100644 --- a/PIL/Jpeg2KImagePlugin.py +++ b/PIL/Jpeg2KImagePlugin.py @@ -40,7 +40,10 @@ def _parse_codestream(fp): size = (xsiz - xosiz, ysiz - yosiz) if csiz == 1: - mode = 'L' + if (yrsiz[0] & 0x7f) > 8: + mode = 'I;16' + else: + mode = 'L' elif csiz == 2: mode = 'LA' elif csiz == 3: @@ -78,6 +81,7 @@ def _parse_jp2_header(fp): size = None mode = None + bpc = None hio = io.BytesIO(header) while True: @@ -95,7 +99,9 @@ def _parse_jp2_header(fp): = struct.unpack('>IIHBBBB', content) size = (width, height) if unkc: - if nc == 1: + if nc == 1 and (bpc & 0x7f) > 8: + mode = 'I;16' + elif nc == 1: mode = 'L' elif nc == 2: mode = 'LA' @@ -109,13 +115,19 @@ def _parse_jp2_header(fp): if meth == 1: cs = struct.unpack('>I', content[3:7])[0] if cs == 16: # sRGB - if nc == 3: + if nc == 1 and (bpc & 0x7f) > 8: + mode = 'I;16' + elif nc == 1: + mode = 'L' + elif nc == 3: mode = 'RGB' elif nc == 4: mode = 'RGBA' break elif cs == 17: # grayscale - if nc == 1: + if nc == 1 and (bpc & 0x7f) > 8: + mode = 'I;16' + elif nc == 1: mode = 'L' elif nc == 2: mode = 'LA' @@ -129,10 +141,10 @@ def _parse_jp2_header(fp): return (size, mode) - ## # Image plugin for JPEG2000 images. + class Jpeg2KImageFile(ImageFile.ImageFile): format = "JPEG2000" format_description = "JPEG 2000 (ISO 15444)" @@ -174,7 +186,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): f.seek(pos, 0) except: length = -1 - + self.tile = [('jpeg2k', (0, 0) + self.size, 0, (self.codec, self.reduce, self.layers, fd, length))] diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index 1d300fc04..1e5d57e42 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -354,12 +354,14 @@ class JpegImageFile(ImageFile.ImageFile): # ALTERNATIVE: handle JPEGs via the IJG command line utilities + import subprocess import tempfile import os f, path = tempfile.mkstemp() os.close(f) if os.path.exists(self.filename): - os.system("djpeg '%s' >'%s'" % (self.filename, path)) + with open(path, 'wb') as f: + subprocess.check_call(["djpeg", self.filename], stdout=f) else: raise ValueError("Invalid Filename") @@ -602,8 +604,10 @@ def _save(im, fp, filename): def _save_cjpeg(im, fp, filename): # ALTERNATIVE: handle JPEGs via the IJG command line utilities. import os - file = im._dump() - os.system("cjpeg %s >%s" % (file, filename)) + import subprocess + tempfile = im._dump() + with open(filename, 'wb') as f: + subprocess.check_call(["cjpeg", tempfile], stdout=f) try: os.unlink(file) except: diff --git a/PIL/__init__.py b/PIL/__init__.py index c35f6207e..14daee5ca 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -14,8 +14,7 @@ VERSION = '1.1.7' # PIL version PILLOW_VERSION = '2.4.0' # Pillow -_plugins = ['ArgImagePlugin', - 'BmpImagePlugin', +_plugins = ['BmpImagePlugin', 'BufrStubImagePlugin', 'CurImagePlugin', 'DcxImagePlugin', diff --git a/PIL/tests.py b/PIL/tests.py deleted file mode 100644 index eb4a8342d..000000000 --- a/PIL/tests.py +++ /dev/null @@ -1,17 +0,0 @@ -import unittest - - -class PillowTests(unittest.TestCase): - """ - Can we start moving the test suite here? - """ - - def test_suite_should_move_here(self): - """ - Great idea! - """ - assert True is True - - -if __name__ == '__main__': - unittest.main() diff --git a/Scripts/player.py b/Scripts/player.py index 84b636668..0c90286c5 100644 --- a/Scripts/player.py +++ b/Scripts/player.py @@ -18,25 +18,6 @@ import sys Image.DEBUG = 0 -# -------------------------------------------------------------------- -# experimental: support ARG animation scripts - -import ArgImagePlugin - -def applet_hook(animation, images): - app = animation(animation_display, images) - app.run() - -ArgImagePlugin.APPLET_HOOK = applet_hook - -class AppletDisplay: - def __init__(self, ui): - self.__ui = ui - def paste(self, im, bbox): - self.__ui.image.paste(im, bbox) - def update(self): - self.__ui.update_idletasks() - # -------------------------------------------------------------------- # an image animation player @@ -56,10 +37,6 @@ class UI(Label): else: self.image = ImageTk.PhotoImage(im) - # APPLET SUPPORT (very crude, and not 100% safe) - global animation_display - animation_display = AppletDisplay(self) - Label.__init__(self, master, image=self.image, bg="black", bd=0) self.update() diff --git a/Tests/README.rst b/Tests/README.rst new file mode 100644 index 000000000..1eaa4f3a2 --- /dev/null +++ b/Tests/README.rst @@ -0,0 +1,24 @@ +Pillow Tests +============ + +Test scripts are named ``test_xxx.py`` and use the ``unittest`` module. A base class and helper functions can be found in ``helper.py``. + +Execution +--------- + +Run the tests from the root of the Pillow source distribution:: + + python selftest.py + nosetests Tests/test_*.py + +Or with coverage:: + + coverage run --append --include=PIL/* selftest.py + coverage run --append --include=PIL/* -m nose Tests/test_*.py + coverage report + coverage html + open htmlcov/index.html + +To run an individual test:: + + python Tests/test_image.py diff --git a/Tests/README.txt b/Tests/README.txt deleted file mode 100644 index 169bc4da5..000000000 --- a/Tests/README.txt +++ /dev/null @@ -1,14 +0,0 @@ -Minimalistic PIL test framework. - -Test scripts are named "test_xxx" and are supposed to output "ok". That's it. To run the tests:: - - python setup.py develop - -Run the tests from the root of the Pillow source distribution: - - python selftest.py - python Tests/run.py --installed - -To run an individual test: - - python Tests/test_image.py diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 8f8ef937a..8aa322aff 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,23 +1,25 @@ -from tester import * +from helper import * -# not running this test by default. No DOS against travis. +# Not running this test by default. No DOS against Travis CI. from PIL import PyAccess -from PIL import Image import time + def iterate_get(size, access): - (w,h) = size + (w, h) = size for x in range(w): for y in range(h): - access[(x,y)] + access[(x, y)] + def iterate_set(size, access): - (w,h) = size + (w, h) = size for x in range(w): for y in range(h): - access[(x,y)] = (x %256,y%256,0) + access[(x, y)] = (x % 256, y % 256, 0) + def timer(func, label, *args): iterations = 5000 @@ -25,27 +27,34 @@ def timer(func, label, *args): for x in range(iterations): func(*args) if time.time()-starttime > 10: - print ("%s: breaking at %s iterations, %.6f per iteration"%(label, x+1, (time.time()-starttime)/(x+1.0))) + print("%s: breaking at %s iterations, %.6f per iteration" % ( + label, x+1, (time.time()-starttime)/(x+1.0))) break if x == iterations-1: endtime = time.time() - print ("%s: %.4f s %.6f per iteration" %(label, endtime-starttime, (endtime-starttime)/(x+1.0))) + print("%s: %.4f s %.6f per iteration" % ( + label, endtime-starttime, (endtime-starttime)/(x+1.0))) -def test_direct(): - im = lena() - im.load() - #im = Image.new( "RGB", (2000,2000), (1,3,2)) - caccess = im.im.pixel_access(False) - access = PyAccess.new(im, False) - assert_equal(caccess[(0,0)], access[(0,0)]) +class BenchCffiAccess(PillowTestCase): - print ("Size: %sx%s" % im.size) - timer(iterate_get, 'PyAccess - get', im.size, access) - timer(iterate_set, 'PyAccess - set', im.size, access) - timer(iterate_get, 'C-api - get', im.size, caccess) - timer(iterate_set, 'C-api - set', im.size, caccess) - - + def test_direct(self): + im = lena() + im.load() + # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) + caccess = im.im.pixel_access(False) + access = PyAccess.new(im, False) - + self.assertEqual(caccess[(0, 0)], access[(0, 0)]) + + print ("Size: %sx%s" % im.size) + timer(iterate_get, 'PyAccess - get', im.size, access) + timer(iterate_set, 'PyAccess - set', im.size, access) + timer(iterate_get, 'C-api - get', im.size, caccess) + timer(iterate_set, 'C-api - set', im.size, caccess) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/bench_get.py b/Tests/bench_get.py index eca491600..8a1331d39 100644 --- a/Tests/bench_get.py +++ b/Tests/bench_get.py @@ -1,13 +1,14 @@ import sys sys.path.insert(0, ".") -import tester +import helper import timeit + def bench(mode): - im = tester.lena(mode) + im = helper.lena(mode) get = im.im.getpixel - xy = 50, 50 # position shouldn't really matter + xy = 50, 50 # position shouldn't really matter t0 = timeit.default_timer() for i in range(1000000): get(xy) diff --git a/Tests/cms_test.py b/Tests/cms_test.py deleted file mode 100644 index 464c9c7ef..000000000 --- a/Tests/cms_test.py +++ /dev/null @@ -1,222 +0,0 @@ -# PyCMSTests.py -# Examples of how to use pyCMS, as well as tests to verify it works properly -# By Kevin Cazabon (kevin@cazabon.com) - -# Imports -import os -from PIL import Image -from PIL import ImageCms - -# import PyCMSError separately so we can catch it -PyCMSError = ImageCms.PyCMSError - -####################################################################### -# Configuration: -####################################################################### -# set this to the image you want to test with -IMAGE = "c:\\temp\\test.tif" - -# set this to where you want to save the output images -OUTPUTDIR = "c:\\temp\\" - -# set these to two different ICC profiles, one for input, one for output -# set the corresponding mode to the proper PIL mode for that profile -INPUT_PROFILE = "c:\\temp\\profiles\\sRGB.icm" -INMODE = "RGB" - -OUTPUT_PROFILE = "c:\\temp\\profiles\\genericRGB.icm" -OUTMODE = "RGB" - -PROOF_PROFILE = "c:\\temp\\profiles\\monitor.icm" - -# set to True to show() images, False to save them into OUTPUT_DIRECTORY -SHOW = False - -# Tests you can enable/disable -TEST_error_catching = True -TEST_profileToProfile = True -TEST_profileToProfile_inPlace = True -TEST_buildTransform = True -TEST_buildTransformFromOpenProfiles = True -TEST_buildProofTransform = True -TEST_getProfileInfo = True -TEST_misc = False - -####################################################################### -# helper functions -####################################################################### -def outputImage(im, funcName = None): - # save or display the image, depending on value of SHOW_IMAGES - if SHOW: - im.show() - else: - im.save(os.path.join(OUTPUTDIR, "%s.tif" %funcName)) - - -####################################################################### -# The tests themselves -####################################################################### - -if TEST_error_catching: - im = Image.open(IMAGE) - try: - #neither of these proifles exists (unless you make them), so we should - # get an error - imOut = ImageCms.profileToProfile(im, "missingProfile.icm", "cmyk.icm") - - except PyCMSError as reason: - print("We caught a PyCMSError: %s\n\n" %reason) - - print("error catching test completed successfully (if you see the message \ - above that we caught the error).") - -if TEST_profileToProfile: - # open the image file using the standard PIL function Image.open() - im = Image.open(IMAGE) - - # send the image, input/output profiles, and rendering intent to - # ImageCms.profileToProfile() - imOut = ImageCms.profileToProfile(im, INPUT_PROFILE, OUTPUT_PROFILE, \ - outputMode = OUTMODE) - - # now that the image is converted, save or display it - outputImage(imOut, "profileToProfile") - - print("profileToProfile test completed successfully.") - -if TEST_profileToProfile_inPlace: - # we'll do the same test as profileToProfile, but modify im in place - # instead of getting a new image returned to us - im = Image.open(IMAGE) - - # send the image to ImageCms.profileToProfile(), specifying inPlace = True - result = ImageCms.profileToProfile(im, INPUT_PROFILE, OUTPUT_PROFILE, \ - outputMode = OUTMODE, inPlace = True) - - # now that the image is converted, save or display it - if result is None: - # this is the normal result when modifying in-place - outputImage(im, "profileToProfile_inPlace") - else: - # something failed... - print("profileToProfile in-place failed: %s" %result) - - print("profileToProfile in-place test completed successfully.") - -if TEST_buildTransform: - # make a transform using the input and output profile path strings - transform = ImageCms.buildTransform(INPUT_PROFILE, OUTPUT_PROFILE, INMODE, \ - OUTMODE) - - # now, use the trnsform to convert a couple images - im = Image.open(IMAGE) - - # transform im normally - im2 = ImageCms.applyTransform(im, transform) - outputImage(im2, "buildTransform") - - # then transform it again using the same transform, this time in-place. - result = ImageCms.applyTransform(im, transform, inPlace = True) - outputImage(im, "buildTransform_inPlace") - - print("buildTransform test completed successfully.") - - # and, to clean up a bit, delete the transform - # this should call the C destructor for the transform structure. - # Python should also do this automatically when it goes out of scope. - del(transform) - -if TEST_buildTransformFromOpenProfiles: - # we'll actually test a couple profile open/creation functions here too - - # first, get a handle to an input profile, in this case we'll create - # an sRGB profile on the fly: - inputProfile = ImageCms.createProfile("sRGB") - - # then, get a handle to the output profile - outputProfile = ImageCms.getOpenProfile(OUTPUT_PROFILE) - - # make a transform from these - transform = ImageCms.buildTransformFromOpenProfiles(inputProfile, \ - outputProfile, INMODE, OUTMODE) - - # now, use the trnsform to convert a couple images - im = Image.open(IMAGE) - - # transform im normally - im2 = ImageCms.applyTransform(im, transform) - outputImage(im2, "buildTransformFromOpenProfiles") - - # then do it again using the same transform, this time in-place. - result = ImageCms.applyTransform(im, transform, inPlace = True) - outputImage(im, "buildTransformFromOpenProfiles_inPlace") - - print("buildTransformFromOpenProfiles test completed successfully.") - - # and, to clean up a bit, delete the transform - # this should call the C destructor for the each item. - # Python should also do this automatically when it goes out of scope. - del(inputProfile) - del(outputProfile) - del(transform) - -if TEST_buildProofTransform: - # make a transform using the input and output and proof profile path - # strings - # images converted with this transform will simulate the appearance - # of the output device while actually being displayed/proofed on the - # proof device. This usually means a monitor, but can also mean - # other proof-printers like dye-sub, etc. - transform = ImageCms.buildProofTransform(INPUT_PROFILE, OUTPUT_PROFILE, \ - PROOF_PROFILE, INMODE, OUTMODE) - - # now, use the trnsform to convert a couple images - im = Image.open(IMAGE) - - # transform im normally - im2 = ImageCms.applyTransform(im, transform) - outputImage(im2, "buildProofTransform") - - # then transform it again using the same transform, this time in-place. - result = ImageCms.applyTransform(im, transform, inPlace = True) - outputImage(im, "buildProofTransform_inPlace") - - print("buildProofTransform test completed successfully.") - - # and, to clean up a bit, delete the transform - # this should call the C destructor for the transform structure. - # Python should also do this automatically when it goes out of scope. - del(transform) - -if TEST_getProfileInfo: - # get a profile handle - profile = ImageCms.getOpenProfile(INPUT_PROFILE) - - # lets print some info about our input profile: - print("Profile name (retrieved from profile string path name): %s" %ImageCms.getProfileName(INPUT_PROFILE)) - - # or, you could do the same thing using a profile handle as the arg - print("Profile name (retrieved from profile handle): %s" %ImageCms.getProfileName(profile)) - - # now lets get the embedded "info" tag contents - # once again, you can use a path to a profile, or a profile handle - print("Profile info (retrieved from profile handle): %s" %ImageCms.getProfileInfo(profile)) - - # and what's the default intent of this profile? - print("The default intent is (this will be an integer): %d" %(ImageCms.getDefaultIntent(profile))) - - # Hmmmm... but does this profile support INTENT_ABSOLUTE_COLORIMETRIC? - print("Does it support INTENT_ABSOLUTE_COLORIMETRIC?: (1 is yes, -1 is no): %s" \ - %ImageCms.isIntentSupported(profile, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, \ - ImageCms.DIRECTION_INPUT)) - - print("getProfileInfo test completed successfully.") - -if TEST_misc: - # test the versions, about, and copyright functions - print("Versions: %s" %str(ImageCms.versions())) - print("About:\n\n%s" %ImageCms.about()) - print("Copyright:\n\n%s" %ImageCms.copyright()) - - print("misc test completed successfully.") - diff --git a/Tests/helper.py b/Tests/helper.py index aacbfc009..416586c78 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -10,6 +10,8 @@ else: import unittest +# This should be imported into every test_XXX.py file to report +# any remaining temp files at the end of the run. def tearDownModule(): import glob import os @@ -90,9 +92,8 @@ class PillowTestCase(unittest.TestCase): self.assertEqual( a.size, b.size, msg or "got size %r, expected %r" % (a.size, b.size)) - self.assertEqual( - a.tobytes(), b.tobytes(), - msg or "got different content") + if a.tobytes() != b.tobytes(): + self.fail(msg or "got different content") def assert_image_similar(self, a, b, epsilon, msg=None): epsilon = float(epsilon) @@ -160,59 +161,11 @@ class PillowTestCase(unittest.TestCase): return files[0] -# # require that deprecation warnings are triggered -# import warnings -# warnings.simplefilter('default') -# # temporarily turn off resource warnings that warn about unclosed -# # files in the test scripts. -# try: -# warnings.filterwarnings("ignore", category=ResourceWarning) -# except NameError: -# # we expect a NameError on py2.x, since it doesn't have ResourceWarnings. -# pass +# helpers import sys py3 = (sys.version_info >= (3, 0)) -# # some test helpers -# -# _target = None -# _tempfiles = [] -# _logfile = None -# -# -# def success(): -# import sys -# success.count += 1 -# if _logfile: -# print(sys.argv[0], success.count, failure.count, file=_logfile) -# return True -# -# -# def failure(msg=None, frame=None): -# import sys -# import linecache -# failure.count += 1 -# if _target: -# if frame is None: -# frame = sys._getframe() -# while frame.f_globals.get("__name__") != _target.__name__: -# frame = frame.f_back -# location = (frame.f_code.co_filename, frame.f_lineno) -# prefix = "%s:%d: " % location -# line = linecache.getline(*location) -# print(prefix + line.strip() + " failed:") -# if msg: -# print("- " + msg) -# if _logfile: -# print(sys.argv[0], success.count, failure.count, file=_logfile) -# return False -# -# success.count = failure.count = 0 -# - - -# helpers def fromstring(data): from io import BytesIO @@ -230,6 +183,9 @@ def tostring(im, format, **options): def lena(mode="RGB", cache={}): from PIL import Image im = None + # FIXME: Implement caching to reduce reading from disk but so an original + # copy is returned each time and the cached image isn't modified by tests + # (for fast, isolated, repeatable tests). # im = cache.get(mode) if im is None: if mode == "RGB": @@ -244,98 +200,28 @@ def lena(mode="RGB", cache={}): return im -# def assert_image_completely_equal(a, b, msg=None): -# if a != b: -# failure(msg or "images different") -# else: -# success() -# -# -# # test runner -# -# def run(): -# global _target, _tests, run -# import sys -# import traceback -# _target = sys.modules["__main__"] -# run = None # no need to run twice -# tests = [] -# for name, value in list(vars(_target).items()): -# if name[:5] == "test_" and type(value) is type(success): -# tests.append((value.__code__.co_firstlineno, name, value)) -# tests.sort() # sort by line -# for lineno, name, func in tests: -# try: -# _tests = [] -# func() -# for func, args in _tests: -# func(*args) -# except: -# t, v, tb = sys.exc_info() -# tb = tb.tb_next -# if tb: -# failure(frame=tb.tb_frame) -# traceback.print_exception(t, v, tb) -# else: -# print("%s:%d: cannot call test function: %s" % ( -# sys.argv[0], lineno, v)) -# failure.count += 1 -# -# -# def yield_test(function, *args): -# # collect delayed/generated tests -# _tests.append((function, args)) -# -# -# def skip(msg=None): -# import os -# print("skip") -# os._exit(0) # don't run exit handlers -# -# -# def ignore(pattern): -# """Tells the driver to ignore messages matching the pattern, for the -# duration of the current test.""" -# print('ignore: %s' % pattern) -# -# -# def _setup(): -# global _logfile -# -# import sys -# if "--coverage" in sys.argv: -# # Temporary: ignore PendingDeprecationWarning from Coverage (Py3.4) -# with warnings.catch_warnings(): -# warnings.simplefilter("ignore") -# import coverage -# cov = coverage.coverage(auto_data=True, include="PIL/*") -# cov.start() -# -# def report(): -# if run: -# run() -# if success.count and not failure.count: -# print("ok") -# # only clean out tempfiles if test passed -# import os -# import os.path -# import tempfile -# for file in _tempfiles: -# try: -# os.remove(file) -# except OSError: -# pass # report? -# temp_root = os.path.join(tempfile.gettempdir(), 'pillow-tests') -# try: -# os.rmdir(temp_root) -# except OSError: -# pass -# -# import atexit -# atexit.register(report) -# -# if "--log" in sys.argv: -# _logfile = open("test.log", "a") -# -# -# _setup() +def command_succeeds(cmd): + """ + Runs the command, which must be a list of strings. Returns True if the + command succeeds, or False if an OSError was raised by subprocess.Popen. + """ + import os + import subprocess + with open(os.devnull, 'w') as f: + try: + subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT).wait() + except OSError: + return False + return True + +def djpeg_available(): + return command_succeeds(['djpeg', '--help']) + +def cjpeg_available(): + return command_succeeds(['cjpeg', '--help']) + +def netpbm_available(): + return command_succeeds(["ppmquant", "--help"]) and \ + command_succeeds(["ppmtogif", "--help"]) + +# End of file diff --git a/Tests/images/16bit.cropped.j2k b/Tests/images/16bit.cropped.j2k new file mode 100644 index 000000000..c12df0cd7 Binary files /dev/null and b/Tests/images/16bit.cropped.j2k differ diff --git a/Tests/images/16bit.cropped.jp2 b/Tests/images/16bit.cropped.jp2 new file mode 100644 index 000000000..0f6685a46 Binary files /dev/null and b/Tests/images/16bit.cropped.jp2 differ diff --git a/Tests/large_memory_numpy_test.py b/Tests/large_memory_numpy_test.py index eb9b8aa01..deb2fc8c6 100644 --- a/Tests/large_memory_numpy_test.py +++ b/Tests/large_memory_numpy_test.py @@ -1,4 +1,4 @@ -from tester import * +from helper import * # This test is not run automatically. # @@ -6,32 +6,37 @@ from tester import * # second test. Running this automatically would amount to a denial of # service on our testing infrastructure. I expect this test to fail # on any 32 bit machine, as well as any smallish things (like -# raspberrypis). +# Raspberry Pis). from PIL import Image try: import numpy as np except: - skip() - -ydim = 32769 -xdim = 48000 -f = tempfile('temp.png') + sys.exit("Skipping: Numpy not installed") -def _write_png(xdim,ydim): - dtype = np.uint8 - a = np.zeros((xdim, ydim), dtype=dtype) - im = Image.fromarray(a, 'L') - im.save(f) - success() - -def test_large(): - """ succeeded prepatch""" - _write_png(xdim,ydim) -def test_2gpx(): - """failed prepatch""" - _write_png(xdim,xdim) +YDIM = 32769 +XDIM = 48000 +class LargeMemoryNumpyTest(PillowTestCase): + + def _write_png(self, xdim, ydim): + dtype = np.uint8 + a = np.zeros((xdim, ydim), dtype=dtype) + f = self.tempfile('temp.png') + im = Image.fromarray(a, 'L') + im.save(f) + + def test_large(self): + """ succeeded prepatch""" + self._write_png(XDIM, YDIM) + + def test_2gpx(self): + """failed prepatch""" + self._write_png(XDIM, XDIM) +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/large_memory_test.py b/Tests/large_memory_test.py index 148841ec2..8552ed4dd 100644 --- a/Tests/large_memory_test.py +++ b/Tests/large_memory_test.py @@ -1,4 +1,4 @@ -from tester import * +from helper import * # This test is not run automatically. # @@ -6,22 +6,31 @@ from tester import * # second test. Running this automatically would amount to a denial of # service on our testing infrastructure. I expect this test to fail # on any 32 bit machine, as well as any smallish things (like -# raspberrypis). It does succeed on a 3gb Ubuntu 12.04x64 VM on python -# 2.7 an 3.2 +# Raspberry Pis). It does succeed on a 3gb Ubuntu 12.04x64 VM on Python +# 2.7 an 3.2. from PIL import Image -ydim = 32769 -xdim = 48000 -f = tempfile('temp.png') +YDIM = 32769 +XDIM = 48000 -def _write_png(xdim,ydim): - im = Image.new('L',(xdim,ydim),(0)) - im.save(f) - success() -def test_large(): - """ succeeded prepatch""" - _write_png(xdim,ydim) -def test_2gpx(): - """failed prepatch""" - _write_png(xdim,xdim) +class LargeMemoryTest(PillowTestCase): + + def _write_png(self, xdim, ydim): + f = self.tempfile('temp.png') + im = Image.new('L', (xdim, ydim), (0)) + im.save(f) + + def test_large(self): + """ succeeded prepatch""" + self._write_png(XDIM, YDIM) + + def test_2gpx(self): + """failed prepatch""" + self._write_png(XDIM, XDIM) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/make_hash.py b/Tests/make_hash.py index 71e208cff..32196e9f8 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -1,7 +1,5 @@ # brute-force search for access descriptor hash table -import random - modes = [ "1", "L", "LA", @@ -13,12 +11,14 @@ modes = [ "YCbCr", ] + def hash(s, i): # djb2 hash: multiply by 33 and xor character for c in s: - i = (((i<<5) + i) ^ ord(c)) & 0xffffffff + i = (((i << 5) + i) ^ ord(c)) & 0xffffffff return i + def check(size, i0): h = [None] * size for m in modes: diff --git a/Tests/run.py b/Tests/run.py deleted file mode 100644 index 82e1b94dc..000000000 --- a/Tests/run.py +++ /dev/null @@ -1,135 +0,0 @@ -from __future__ import print_function - -# minimal test runner - -import glob -import os -import os.path -import re -import sys -import tempfile - -try: - root = os.path.dirname(__file__) -except NameError: - root = os.path.dirname(sys.argv[0]) - -if not os.path.isfile("PIL/Image.py"): - print("***", "please run this script from the PIL development directory as") - print("***", "$ python Tests/run.py") - sys.exit(1) - -print("-"*68) - -python_options = [] -tester_options = [] - -if "--installed" not in sys.argv: - os.environ["PYTHONPATH"] = "." - -if "--coverage" in sys.argv: - tester_options.append("--coverage") - -if "--log" in sys.argv: - tester_options.append("--log") - -files = glob.glob(os.path.join(root, "test_*.py")) -files.sort() - -success = failure = 0 -include = [x for x in sys.argv[1:] if x[:2] != "--"] -skipped = [] -failed = [] - -python_options = " ".join(python_options) -tester_options = " ".join(tester_options) - -ignore_re = re.compile('^ignore: (.*)$', re.MULTILINE) - -for file in files: - test, ext = os.path.splitext(os.path.basename(file)) - if include and test not in include: - continue - print("running", test, "...") - # 2>&1 works on unix and on modern windowses. we might care about - # very old Python versions, but not ancient microsoft products :-) - out = os.popen("%s %s -u %s %s 2>&1" % ( - sys.executable, python_options, file, tester_options - )) - result = out.read() - - result_lines = result.splitlines() - if len(result_lines): - if result_lines[0] == "ignore_all_except_last_line": - result = result_lines[-1] - - # Extract any ignore patterns - ignore_pats = ignore_re.findall(result) - result = ignore_re.sub('', result) - - try: - def fix_re(p): - if not p.startswith('^'): - p = '^' + p - if not p.endswith('$'): - p += '$' - return p - - ignore_res = [re.compile(fix_re(p), re.MULTILINE) for p in ignore_pats] - except: - print('(bad ignore patterns %r)' % ignore_pats) - ignore_res = [] - - for r in ignore_res: - result = r.sub('', result) - - result = result.strip() - - if result == "ok": - result = None - elif result == "skip": - print("---", "skipped") # FIXME: driver should include a reason - skipped.append(test) - continue - elif not result: - result = "(no output)" - status = out.close() - if status or result: - if status: - print("=== error", status) - if result: - if result[-3:] == "\nok": - # if there's an ok at the end, it's not really ok - result = result[:-3] - print(result) - failed.append(test) - else: - success += 1 - -print("-"*68) - -temp_root = os.path.join(tempfile.gettempdir(), 'pillow-tests') -tempfiles = glob.glob(os.path.join(temp_root, "temp_*")) -if tempfiles: - print("===", "remaining temporary files") - for file in tempfiles: - print(file) - print("-"*68) - - -def tests(n): - if n == 1: - return "1 test" - else: - return "%d tests" % n - -if skipped: - print("---", tests(len(skipped)), "skipped:") - print(", ".join(skipped)) -if failed: - failure = len(failed) - print("***", tests(failure), "of", (success + failure), "failed:") - print(", ".join(failed)) - sys.exit(1) -else: - print(tests(success), "passed.") diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 29ddb9824..2870aba04 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -37,6 +37,18 @@ class TestFileBmp(PillowTestCase): self.assertEqual(im.size, reloaded.size) self.assertEqual(reloaded.format, "BMP") + def test_dpi(self): + dpi = (72, 72) + + output = io.BytesIO() + im = lena() + im.save(output, "BMP", dpi=dpi) + + output.seek(0) + reloaded = Image.open(output) + + self.assertEqual(reloaded.info["dpi"], dpi) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 01ac1d95c..e31779df0 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,6 +1,7 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, tearDownModule, lena, netpbm_available from PIL import Image +from PIL import GifImagePlugin codecs = dir(Image.core) @@ -89,6 +90,22 @@ class TestFileGif(PillowTestCase): reloaded = roundtrip(im)[1].convert('RGB') self.assert_image_equal(im, reloaded) + @unittest.skipUnless(netpbm_available(), "netpbm not available") + def test_save_netpbm_bmp_mode(self): + img = Image.open(file).convert("RGB") + + tempfile = self.tempfile("temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + self.assert_image_similar(img, Image.open(tempfile).convert("RGB"), 0) + + @unittest.skipUnless(netpbm_available(), "netpbm not available") + def test_save_netpbm_l_mode(self): + img = Image.open(file).convert("L") + + tempfile = self.tempfile("temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + self.assert_image_similar(img, Image.open(tempfile).convert("L"), 0) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index dae1d0019..283c48eb7 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,10 +1,12 @@ from helper import unittest, PillowTestCase, tearDownModule, lena, py3 +from helper import djpeg_available, cjpeg_available import random from io import BytesIO from PIL import Image from PIL import ImageFile +from PIL import JpegImagePlugin codecs = dir(Image.core) @@ -273,8 +275,23 @@ class TestFileJpeg(PillowTestCase): qtables={0:standard_l_qtable, 1:standard_chrominance_qtable}), 30) - - + + @unittest.skipUnless(djpeg_available(), "djpeg not available") + def test_load_djpeg(self): + img = Image.open(test_file) + img.load_djpeg() + self.assert_image_similar(img, Image.open(test_file), 0) + + @unittest.skipUnless(cjpeg_available(), "cjpeg not available") + def test_save_cjpeg(self): + img = Image.open(test_file) + + tempfile = self.tempfile("temp.jpg") + JpegImagePlugin._save_cjpeg(img, 0, tempfile) + # Default save quality is 75%, so a tiny bit of difference is alright + self.assert_image_similar(img, Image.open(tempfile), 1) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b662124b4..b763687f7 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -120,6 +120,42 @@ class TestFileJpeg2k(PillowTestCase): self.assertEqual(j2k.mode, 'RGBA') self.assertEqual(jp2.mode, 'RGBA') + def test_16bit_monochrome_has_correct_mode(self): + + j2k = Image.open('Tests/images/16bit.cropped.j2k') + jp2 = Image.open('Tests/images/16bit.cropped.jp2') + + j2k.load() + jp2.load() + + self.assertEqual(j2k.mode, 'I;16') + self.assertEqual(jp2.mode, 'I;16') + + def test_16bit_monchrome_jp2_like_tiff(self): + + tiff_16bit = Image.open('Tests/images/16bit.cropped.tif') + jp2 = Image.open('Tests/images/16bit.cropped.jp2') + self.assert_image_similar(jp2, tiff_16bit, 1e-3) + + def test_16bit_monchrome_j2k_like_tiff(self): + + tiff_16bit = Image.open('Tests/images/16bit.cropped.tif') + j2k = Image.open('Tests/images/16bit.cropped.j2k') + self.assert_image_similar(j2k, tiff_16bit, 1e-3) + + def test_16bit_j2k_roundtrips(self): + + j2k = Image.open('Tests/images/16bit.cropped.j2k') + im = self.roundtrip(j2k) + self.assert_image_equal(im, j2k) + + def test_16bit_jp2_roundtrips(self): + + jp2 = Image.open('Tests/images/16bit.cropped.jp2') + im = self.roundtrip(jp2) + self.assert_image_equal(im, jp2) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py new file mode 100644 index 000000000..2c2aeae96 --- /dev/null +++ b/Tests/test_pyroma.py @@ -0,0 +1,33 @@ +import unittest + +try: + import pyroma +except ImportError: + # Skip via setUp() + pass + + +class TestPyroma(unittest.TestCase): + + def setUp(self): + try: + import pyroma + except ImportError: + self.skipTest("ImportError") + + def test_pyroma(self): + # Arrange + data = pyroma.projectdata.get_data(".") + + # Act + rating = pyroma.ratings.rate(data) + + # Assert + # Should have a perfect score + self.assertEqual(rating, (10, [])) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py new file mode 100644 index 000000000..fd14e5f44 --- /dev/null +++ b/Tests/test_shell_injection.py @@ -0,0 +1,56 @@ +from helper import unittest, PillowTestCase, tearDownModule +from helper import djpeg_available, cjpeg_available, netpbm_available + +import shutil + +from PIL import Image, JpegImagePlugin, GifImagePlugin + +test_jpg = "Tests/images/lena.jpg" +test_gif = "Tests/images/lena.gif" + +test_filenames = ( + "temp_';", + "temp_\";", + "temp_'\"|", + "temp_'\"||", + "temp_'\"&&", +) + +class TestShellInjection(PillowTestCase): + + def assert_save_filename_check(self, src_img, save_func): + for filename in test_filenames: + dest_file = self.tempfile(filename) + save_func(src_img, 0, dest_file) + # If file can't be opened, shell injection probably occurred + Image.open(dest_file).load() + + @unittest.skipUnless(djpeg_available(), "djpeg not available") + def test_load_djpeg_filename(self): + for filename in test_filenames: + src_file = self.tempfile(filename) + shutil.copy(test_jpg, src_file) + + im = Image.open(src_file) + im.load_djpeg() + + @unittest.skipUnless(cjpeg_available(), "cjpeg not available") + def test_save_cjpeg_filename(self): + im = Image.open(test_jpg) + self.assert_save_filename_check(im, JpegImagePlugin._save_cjpeg) + + @unittest.skipUnless(netpbm_available(), "netpbm not available") + def test_save_netpbm_filename_bmp_mode(self): + im = Image.open(test_gif).convert("RGB") + self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) + + @unittest.skipUnless(netpbm_available(), "netpbm not available") + def test_save_netpbm_filename_l_mode(self): + im = Image.open(test_gif).convert("L") + self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/tester.py b/Tests/tester.py deleted file mode 100644 index 866009251..000000000 --- a/Tests/tester.py +++ /dev/null @@ -1,388 +0,0 @@ -from __future__ import print_function - -# require that deprecation warnings are triggered -import warnings -warnings.simplefilter('default') -# temporarily turn off resource warnings that warn about unclosed -# files in the test scripts. -try: - warnings.filterwarnings("ignore", category=ResourceWarning) -except NameError: - # we expect a NameError on py2.x, since it doesn't have ResourceWarnings. - pass - -import sys -py3 = (sys.version_info >= (3, 0)) - -# some test helpers - -_target = None -_tempfiles = [] -_logfile = None - - -def success(): - import sys - success.count += 1 - if _logfile: - print(sys.argv[0], success.count, failure.count, file=_logfile) - return True - - -def failure(msg=None, frame=None): - import sys - import linecache - failure.count += 1 - if _target: - if frame is None: - frame = sys._getframe() - while frame.f_globals.get("__name__") != _target.__name__: - frame = frame.f_back - location = (frame.f_code.co_filename, frame.f_lineno) - prefix = "%s:%d: " % location - line = linecache.getline(*location) - print(prefix + line.strip() + " failed:") - if msg: - print("- " + msg) - if _logfile: - print(sys.argv[0], success.count, failure.count, file=_logfile) - return False - -success.count = failure.count = 0 - - -# predicates - -def assert_true(v, msg=None): - if v: - success() - else: - failure(msg or "got %r, expected true value" % v) - - -def assert_false(v, msg=None): - if v: - failure(msg or "got %r, expected false value" % v) - else: - success() - - -def assert_equal(a, b, msg=None): - if a == b: - success() - else: - failure(msg or "got %r, expected %r" % (a, b)) - - -def assert_almost_equal(a, b, msg=None, eps=1e-6): - if abs(a-b) < eps: - success() - else: - failure(msg or "got %r, expected %r" % (a, b)) - - -def assert_deep_equal(a, b, msg=None): - try: - if len(a) == len(b): - if all([x == y for x, y in zip(a, b)]): - success() - else: - failure(msg or "got %s, expected %s" % (a, b)) - else: - failure(msg or "got length %s, expected %s" % (len(a), len(b))) - except: - assert_equal(a, b, msg) - - -def assert_greater(a, b, msg=None): - if a > b: - success() - else: - failure(msg or "%r unexpectedly not greater than %r" % (a, b)) - - -def assert_greater_equal(a, b, msg=None): - if a >= b: - success() - else: - failure( - msg or "%r unexpectedly not greater than or equal to %r" % (a, b)) - - -def assert_less(a, b, msg=None): - if a < b: - success() - else: - failure(msg or "%r unexpectedly not less than %r" % (a, b)) - - -def assert_less_equal(a, b, msg=None): - if a <= b: - success() - else: - failure( - msg or "%r unexpectedly not less than or equal to %r" % (a, b)) - - -def assert_is_instance(a, b, msg=None): - if isinstance(a, b): - success() - else: - failure(msg or "got %r, expected %r" % (type(a), b)) - - -def assert_in(a, b, msg=None): - if a in b: - success() - else: - failure(msg or "%r unexpectedly not in %r" % (a, b)) - - -def assert_match(v, pattern, msg=None): - import re - if re.match(pattern, v): - success() - else: - failure(msg or "got %r, doesn't match pattern %r" % (v, pattern)) - - -def assert_exception(exc_class, func): - import sys - import traceback - try: - func() - except exc_class: - success() - except: - failure("expected %r exception, got %r" % ( - exc_class.__name__, sys.exc_info()[0].__name__)) - traceback.print_exc() - else: - failure("expected %r exception, got no exception" % exc_class.__name__) - - -def assert_no_exception(func): - import sys - import traceback - try: - func() - except: - failure("expected no exception, got %r" % sys.exc_info()[0].__name__) - traceback.print_exc() - else: - success() - - -def assert_warning(warn_class, func): - # note: this assert calls func three times! - import warnings - - def warn_error(message, category=UserWarning, **options): - raise category(message) - - def warn_ignore(message, category=UserWarning, **options): - pass - warn = warnings.warn - result = None - try: - warnings.warn = warn_ignore - assert_no_exception(func) - result = func() - warnings.warn = warn_error - assert_exception(warn_class, func) - finally: - warnings.warn = warn # restore - return result - -# helpers - -from io import BytesIO - - -def fromstring(data): - from PIL import Image - return Image.open(BytesIO(data)) - - -def tostring(im, format, **options): - out = BytesIO() - im.save(out, format, **options) - return out.getvalue() - - -def lena(mode="RGB", cache={}): - from PIL import Image - im = cache.get(mode) - if im is None: - if mode == "RGB": - im = Image.open("Tests/images/lena.ppm") - elif mode == "F": - im = lena("L").convert(mode) - elif mode[:4] == "I;16": - im = lena("I").convert(mode) - else: - im = lena("RGB").convert(mode) - cache[mode] = im - return im - - -def assert_image(im, mode, size, msg=None): - if mode is not None and im.mode != mode: - failure(msg or "got mode %r, expected %r" % (im.mode, mode)) - elif size is not None and im.size != size: - failure(msg or "got size %r, expected %r" % (im.size, size)) - else: - success() - - -def assert_image_equal(a, b, msg=None): - if a.mode != b.mode: - failure(msg or "got mode %r, expected %r" % (a.mode, b.mode)) - elif a.size != b.size: - failure(msg or "got size %r, expected %r" % (a.size, b.size)) - elif a.tobytes() != b.tobytes(): - failure(msg or "got different content") - else: - success() - - -def assert_image_completely_equal(a, b, msg=None): - if a != b: - failure(msg or "images different") - else: - success() - - -def assert_image_similar(a, b, epsilon, msg=None): - epsilon = float(epsilon) - if a.mode != b.mode: - return failure(msg or "got mode %r, expected %r" % (a.mode, b.mode)) - elif a.size != b.size: - return failure(msg or "got size %r, expected %r" % (a.size, b.size)) - diff = 0 - try: - ord(b'0') - for abyte, bbyte in zip(a.tobytes(), b.tobytes()): - diff += abs(ord(abyte)-ord(bbyte)) - except: - for abyte, bbyte in zip(a.tobytes(), b.tobytes()): - diff += abs(abyte-bbyte) - ave_diff = float(diff)/(a.size[0]*a.size[1]) - if epsilon < ave_diff: - return failure( - msg or "average pixel value difference %.4f > epsilon %.4f" % ( - ave_diff, epsilon)) - else: - return success() - - -def tempfile(template, *extra): - import os - import os.path - import sys - import tempfile - files = [] - root = os.path.join(tempfile.gettempdir(), 'pillow-tests') - try: - os.mkdir(root) - except OSError: - pass - for temp in (template,) + extra: - assert temp[:5] in ("temp.", "temp_") - name = os.path.basename(sys.argv[0]) - name = temp[:4] + os.path.splitext(name)[0][4:] - name = name + "_%d" % len(_tempfiles) + temp[4:] - name = os.path.join(root, name) - files.append(name) - _tempfiles.extend(files) - return files[0] - - -# test runner - -def run(): - global _target, _tests, run - import sys - import traceback - _target = sys.modules["__main__"] - run = None # no need to run twice - tests = [] - for name, value in list(vars(_target).items()): - if name[:5] == "test_" and type(value) is type(success): - tests.append((value.__code__.co_firstlineno, name, value)) - tests.sort() # sort by line - for lineno, name, func in tests: - try: - _tests = [] - func() - for func, args in _tests: - func(*args) - except: - t, v, tb = sys.exc_info() - tb = tb.tb_next - if tb: - failure(frame=tb.tb_frame) - traceback.print_exception(t, v, tb) - else: - print("%s:%d: cannot call test function: %s" % ( - sys.argv[0], lineno, v)) - failure.count += 1 - - -def yield_test(function, *args): - # collect delayed/generated tests - _tests.append((function, args)) - - -def skip(msg=None): - import os - print("skip") - os._exit(0) # don't run exit handlers - - -def ignore(pattern): - """Tells the driver to ignore messages matching the pattern, for the - duration of the current test.""" - print('ignore: %s' % pattern) - - -def _setup(): - global _logfile - - import sys - if "--coverage" in sys.argv: - # Temporary: ignore PendingDeprecationWarning from Coverage (Py3.4) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - import coverage - cov = coverage.coverage(auto_data=True, include="PIL/*") - cov.start() - - def report(): - if run: - run() - if success.count and not failure.count: - print("ok") - # only clean out tempfiles if test passed - import os - import os.path - import tempfile - for file in _tempfiles: - try: - os.remove(file) - except OSError: - pass # report? - temp_root = os.path.join(tempfile.gettempdir(), 'pillow-tests') - try: - os.rmdir(temp_root) - except OSError: - pass - - import atexit - atexit.register(report) - - if "--log" in sys.argv: - _logfile = open("test.log", "a") - - -_setup() diff --git a/_imagingmorph.c b/_imagingmorph.c index 7e7fdd879..1dee7eb64 100644 --- a/_imagingmorph.c +++ b/_imagingmorph.c @@ -267,9 +267,10 @@ setup_module(PyObject* m) static PyMethodDef functions[] = { /* Functions */ - {"apply", (PyCFunction)apply, 1}, - {"get_on_pixels", (PyCFunction)get_on_pixels, 1}, - {"match", (PyCFunction)match, 1}, + {"apply", (PyCFunction)apply, METH_VARARGS, NULL}, + {"get_on_pixels", (PyCFunction)get_on_pixels, METH_VARARGS, NULL}, + {"match", (PyCFunction)match, METH_VARARGS, NULL}, + {NULL, NULL, 0, NULL} }; #if PY_VERSION_HEX >= 0x03000000 diff --git a/depends/README.rst b/depends/README.rst new file mode 100644 index 000000000..62c101ecf --- /dev/null +++ b/depends/README.rst @@ -0,0 +1,4 @@ +Depends +======= + +Scripts in this directory can be used to download, build & install non-packaged dependencies; useful for testing with Travis CI. diff --git a/docs/plugins.rst b/docs/plugins.rst index 001cee949..a069f80df 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -1,14 +1,6 @@ Plugin reference ================ -:mod:`ArgImagePlugin` Module ----------------------------- - -.. automodule:: PIL.ArgImagePlugin - :members: - :undoc-members: - :show-inheritance: - :mod:`BmpImagePlugin` Module ---------------------------- diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 7125fcad4..6dcb73638 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -49,6 +49,8 @@ Functions .. autofunction:: open + .. warning:: > To protect against potential DOS attacks caused by "[decompression bombs](https://en.wikipedia.org/wiki/Zip_bomb)" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up a lot of memory), Pillow will issue a `DecompressionBombWarning` if the image is over a certain limit. If desired, the warning can be turned into an error with `warnings.simplefilter('error', Image.DecompressionBombWarning)` or suppressed entirely with `warnings.simplefilter('ignore', Image.DecompressionBombWarning)`. See also [the logging documentation](https://docs.python.org/2/library/logging.html?highlight=logging#integration-with-the-warnings-module) to have warnings output to the logging facility instead of stderr. + Image processing ^^^^^^^^^^^^^^^^ diff --git a/libImaging/ImPlatform.h b/libImaging/ImPlatform.h index 92c126a10..8e85e627c 100644 --- a/libImaging/ImPlatform.h +++ b/libImaging/ImPlatform.h @@ -17,12 +17,12 @@ #error Sorry, this library requires ANSI header files. #endif +#if !defined(PIL_USE_INLINE) +#define inline +#else #if defined(_MSC_VER) && !defined(__GNUC__) #define inline __inline #endif - -#if !defined(PIL_USE_INLINE) -#define inline #endif #ifdef _WIN32 diff --git a/libImaging/Imaging.h b/libImaging/Imaging.h index 26207d121..d958387c9 100644 --- a/libImaging/Imaging.h +++ b/libImaging/Imaging.h @@ -20,7 +20,7 @@ extern "C" { #ifndef M_PI -#define M_PI 3.14159265359 +#define M_PI 3.1415926535897932384626433832795 #endif diff --git a/libImaging/Jpeg2KDecode.c b/libImaging/Jpeg2KDecode.c index 1b61b4f7d..97ec81003 100644 --- a/libImaging/Jpeg2KDecode.c +++ b/libImaging/Jpeg2KDecode.c @@ -135,6 +135,56 @@ j2ku_gray_l(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, } } + +static void +j2ku_gray_i(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, Imaging im) +{ + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shift = 16 - in->comps[0].prec; + int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; + int csiz = (in->comps[0].prec + 7) >> 3; + + unsigned x, y; + + if (csiz == 3) + csiz = 4; + + if (shift < 0) + offset += 1 << (-shift - 1); + + switch (csiz) { + case 1: + for (y = 0; y < h; ++y) { + const UINT8 *data = &tiledata[y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) + *row++ = j2ku_shift(offset + *data++, shift); + } + break; + case 2: + for (y = 0; y < h; ++y) { + const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) + *row++ = j2ku_shift(offset + *data++, shift); + } + break; + case 4: + for (y = 0; y < h; ++y) { + const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) + *row++ = j2ku_shift(offset + *data++, shift); + } + break; + } +} + + static void j2ku_gray_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im) @@ -466,6 +516,8 @@ j2ku_sycca_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, static const struct j2k_decode_unpacker j2k_unpackers[] = { { "L", OPJ_CLRSPC_GRAY, 1, j2ku_gray_l }, + { "I;16", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i }, + { "I;16B", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i }, { "LA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la }, { "RGB", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb }, { "RGB", OPJ_CLRSPC_GRAY, 2, j2ku_gray_rgb }, diff --git a/libImaging/Jpeg2KEncode.c b/libImaging/Jpeg2KEncode.c index 8e7d0d1f2..e8eef08c1 100644 --- a/libImaging/Jpeg2KEncode.c +++ b/libImaging/Jpeg2KEncode.c @@ -88,6 +88,22 @@ j2k_pack_l(Imaging im, UINT8 *buf, } } +static void +j2k_pack_i16(Imaging im, UINT8 *buf, + unsigned x0, unsigned y0, unsigned w, unsigned h) +{ + UINT8 *ptr = buf; + unsigned x,y; + for (y = 0; y < h; ++y) { + UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); + for (x = 0; x < w; ++x) { + *ptr++ = *data++; + *ptr++ = *data++; + } + } +} + + static void j2k_pack_la(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) @@ -247,6 +263,9 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, j2k_pack_tile_t pack; int ret = -1; + unsigned prec = 8; + unsigned bpp = 8; + stream = opj_stream_default_create(OPJ_FALSE); if (!stream) { @@ -271,6 +290,18 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_l; + } else if (strcmp (im->mode, "I;16") == 0){ + components = 1; + color_space = OPJ_CLRSPC_GRAY; + pack = j2k_pack_i16; + prec = 16; + bpp = 12; + } else if (strcmp (im->mode, "I;16B") == 0){ + components = 1; + color_space = OPJ_CLRSPC_GRAY; + pack = j2k_pack_i16; + prec = 16; + bpp = 12; } else if (strcmp (im->mode, "LA") == 0) { components = 2; color_space = OPJ_CLRSPC_GRAY; @@ -298,8 +329,8 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, image_params[n].w = im->xsize; image_params[n].h = im->ysize; image_params[n].x0 = image_params[n].y0 = 0; - image_params[n].prec = 8; - image_params[n].bpp = 8; + image_params[n].prec = prec; + image_params[n].bpp = bpp; image_params[n].sgnd = 0; } @@ -442,7 +473,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, num_tiles = tiles_x * tiles_y; - state->buffer = malloc (tile_width * tile_height * components); + state->buffer = malloc (tile_width * tile_height * components * prec / 8); tile_ndx = 0; for (y = 0; y < tiles_y; ++y) { @@ -474,7 +505,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, pack(im, state->buffer, pixx, pixy, pixw, pixh); - data_size = pixw * pixh * components; + data_size = pixw * pixh * components * prec / 8; if (!opj_write_tile(codec, tile_ndx++, state->buffer, data_size, stream)) { diff --git a/mp_compile.py b/mp_compile.py new file mode 100644 index 000000000..71b4089e1 --- /dev/null +++ b/mp_compile.py @@ -0,0 +1,52 @@ +# A monkey patch of the base distutils.ccompiler to use parallel builds +# Tested on 2.7, looks to be identical to 3.3. + +from multiprocessing import Pool, cpu_count +from distutils.ccompiler import CCompiler +import os + + +# hideous monkeypatching. but. but. but. +def _mp_compile_one(tp): + (self, obj, build, cc_args, extra_postargs, pp_opts) = tp + try: + src, ext = build[obj] + except KeyError: + return + self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) + return + + +def _mp_compile(self, sources, output_dir=None, macros=None, + include_dirs=None, debug=0, extra_preargs=None, + extra_postargs=None, depends=None): + """Compile one or more source files. + + see distutils.ccompiler.CCompiler.compile for comments. + """ + # A concrete compiler class can either override this method + # entirely or implement _compile(). + + macros, objects, extra_postargs, pp_opts, build = self._setup_compile( + output_dir, macros, include_dirs, sources, depends, extra_postargs) + cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) + + try: + max_procs = int(os.environ.get('MAX_CONCURRENCY', cpu_count())) + except: + max_procs = None + pool = Pool(max_procs) + try: + print ("Building using %d processes" % pool._processes) + except: + pass + arr = [ + (self, obj, build, cc_args, extra_postargs, pp_opts) for obj in objects + ] + pool.map_async(_mp_compile_one, arr) + pool.close() + pool.join() + # Return *all* object filenames, not just the ones we just built. + return objects + +CCompiler.compile = _mp_compile diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..86ddbd771 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# Testing reqs +-e . +nose diff --git a/setup.py b/setup.py index 2fbcc2959..97ff17d97 100644 --- a/setup.py +++ b/setup.py @@ -210,7 +210,9 @@ class pil_build_ext(build_ext): # if Homebrew is installed, use its lib and include directories import subprocess try: - prefix = subprocess.check_output(['brew', '--prefix']).strip() + prefix = subprocess.check_output( + ['brew', '--prefix'] + ).strip().decode('latin1') except: # Homebrew not installed prefix = None @@ -403,7 +405,12 @@ class pil_build_ext(build_ext): # Find the best version for directory in self.compiler.include_dirs: - for name in os.listdir(directory): + try: + listdir = os.listdir(directory) + except Exception: + # WindowsError, FileNotFoundError + continue + for name in listdir: if name.startswith('openjpeg-') and \ os.path.isfile(os.path.join(directory, name, 'openjpeg.h')): @@ -705,6 +712,7 @@ class pil_build_ext(build_ext): finally: os.unlink(tmpfile) + setup( name=NAME, version=VERSION, @@ -728,7 +736,9 @@ setup( "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", ], + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + ], cmdclass={"build_ext": pil_build_ext}, ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], include_package_data=True, @@ -738,6 +748,5 @@ setup( keywords=["Imaging", ], license='Standard PIL License', zip_safe=True, - ) - +) # End of file diff --git a/test-installed.py b/test-installed.py index 7f58f4966..0248590a5 100755 --- a/test-installed.py +++ b/test-installed.py @@ -4,8 +4,8 @@ import os import sys import glob -# monkey with the path, removing the local directory but adding the Tests/ directory -# for helper.py and the other local imports there. +# monkey with the path, removing the local directory but adding the Tests/ +# directory for helper.py and the other local imports there. del(sys.path[0]) sys.path.insert(0, os.path.abspath('./Tests')) @@ -16,7 +16,7 @@ sys.path.insert(0, os.path.abspath('./Tests')) if len(sys.argv) == 1: sys.argv.extend(glob.glob('Tests/test*.py')) -# Make sure that nose doesn't muck with our paths. +# Make sure that nose doesn't muck with our paths. if ('--no-path-adjustment' not in sys.argv) and ('-P' not in sys.argv): sys.argv.insert(1, '--no-path-adjustment')