diff --git a/.coveragerc b/.coveragerc index 87e3e968f..ea79190ae 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,4 +11,4 @@ exclude_lines = if __name__ == .__main__.: # Don't complain about debug code if Image.DEBUG: - if DEBUG: \ No newline at end of file + if DEBUG: diff --git a/.gitignore b/.gitignore index a0ba1b4c1..95ed4bac5 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,9 @@ docs/_build/ \#*# .#* +#Komodo +*.komodoproject + +#OS +.DS_Store + diff --git a/.travis.yml b/.travis.yml index b4b98e784..6226e5470 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,17 +7,23 @@ env: MAX_CONCURRENCY=4 python: - "pypy" + - "pypy3" - 2.6 - 2.7 + - "2.7_with_system_site_packages" # For PyQt4 - 3.2 - 3.3 - 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 coveralls nose pyroma" - - if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then pip install unittest2; fi + - "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 travis_retry pip install unittest2; fi # webp - pushd depends && ./install_webp.sh && popd @@ -28,22 +34,27 @@ install: script: - coverage erase - python setup.py clean - - python setup.py build_ext --inplace + - CFLAGS="-coverage" python setup.py build_ext --inplace - # Don't cover PyPy: it fails intermittently and is x5.8 slower (#640) - - if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then time python selftest.py; fi - - if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then time nosetests Tests/test_*.py; fi - - # Cover the others - - if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then time coverage run --append --include=PIL/* selftest.py; fi - - if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then time coverage run --append --include=PIL/* -m nose Tests/test_*.py; fi + - coverage run --append --include=PIL/* selftest.py + - coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py after_success: - - coverage report - # No need to send empty coverage to Coveralls for PyPy - - if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then coveralls; fi + # gather the coverage data + - 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 + - travis_retry gem install coveralls-lcov + - coveralls-lcov -v -n coverage.filtered.info > coverage.c.json - - pip install pep8 pyflakes + - coverage report + - travis_retry pip install coveralls-merge + - coveralls-merge coverage.c.json + + + - travis_retry pip install pep8 pyflakes - pep8 --statistics --count PIL/*.py - pep8 --statistics --count Tests/*.py - pyflakes PIL/*.py | tee >(wc -l) @@ -54,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 0bf1a7ee5..cc92eba8d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,9 +4,115 @@ Changelog (Pillow) 2.6.0 (unreleased) ------------------ -- Test PalmImagePlugin and method to skip known bad tests +- Jpeg2k Decode/Encode Memory Leak Fix #898 + [joshware, wiredfool] + +- EpsFilePlugin Speed improvements #886 + [wiredfool, karstenw] + +- Don't resize if already the right size. + [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 + [hugovk] + +- Windows fixes #871 + [wiredfool] + +- Fix TGA files with image ID field #856 + [megabuz] + +- Fixed wrong P-mode of small, unoptimized L-mode GIF #843 + [uvNikita] + +- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin + [Andrew Drake] + +- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin + [Andrew Drake] + +- setup.py: Close open file handle before deleting #844 + [divergentdave] + +- Return Profile with Transformed Images #837 + [wiredfool] + +- Changed docstring to refer to the correct function #836 + [MatMoore] + +- Adding coverage support for C code tests #833 + [wiredfool] + +- PyPy performance improvements #821 + [wiredfool] + +- Added support for reading MPO files + [Feneric] + +- Added support for encoding and decoding iTXt chunks #818 + [dolda2000] + +- HSV Support #816 + [wiredfool] + +- Removed unusable ImagePalette.new() + [hugovk] + +- Fix Scrambled XPM #808 + [wiredfool] + +- Doc cleanup + [wiredfool] + +- Fix `ImageStat` docs + [akx] + +- Added docs for ExifTags + [Wintermute3] + +- More tests for CurImagePlugin, DcxImagePlugin, Effects.c, GimpGradientFile, ImageFont, ImageMath, ImagePalette, IptcImagePlugin, SpiderImagePlugin, SgiImagePlugin, XpmImagePlugin and _util + [hugovk] + +- Fix return value of FreeTypeFont.textsize() does not include font offsets + [tk0miya] + +- Fix dispose calculations for animated GIFs #765 + [larsjsol] + +- Added class checking to Image __eq__ function #775 + [radarhere, hugovk] + +- Test PalmImagePlugin and method to skip known bad tests #776 [hugovk, wiredfool] +2.5.3 (2014-08-18) +------------------ + +- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin (backport) + [Andrew Drake] + + +2.5.2 (2014-08-13) +------------------ + +- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) + [Andrew Drake] + + +2.5.1 (2014-07-10) +------------------ + +- Fixed install issue if Multiprocessing.Pool is not available + [wiredfool] + +- 32bit mult overflow fix #782 + [wiredfool] 2.5.0 (2014-07-01) ------------------ @@ -227,6 +333,12 @@ Changelog (Pillow) - Prefer homebrew freetype over X11 freetype (but still allow both) [dmckeone] +2.3.2 (2014-08-13) +------------------ + +- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) + [Andrew Drake] + 2.3.1 (2014-03-14) ------------------ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..a31c9ee09 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing + +## Fixes, Features and Changes + +Send a pull request. We'll generally want documentation and [tests](Tests/README.rst) for new features. Tests or documentation on their own are also welcomed. Feel free to ask questions as an [issue](https://github.com/python-pillow/Pillow/issues/new) or on IRC (irc.freenode.net, #pil) + +- 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. +- Push to your fork, and make a pull request. + +A few guidelines: +- Try to keep any code commits clean and separate from reformatting commits. +- All new code is going to need tests. +- Try to follow PEP8. + +## Bugs + +When reporting bugs, please include example code that reproduces the issue, and if possible a problem image. The best reproductions are self-contained scripts that pull in as few dependencies as possible. An entire Django stack is harder to handle. + +Let us know: +- What did you do? +- What did you expect to happen? +- What actually happened? +- What versions of Pillow and Python are you using? diff --git a/MANIFEST.in b/MANIFEST.in index 00c132d47..643e8056c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,7 @@ + include *.c include *.h +include *.md include *.py include *.rst include *.txt @@ -25,14 +27,20 @@ recursive-include Images *.xpm recursive-include PIL *.md recursive-include Sane *.c recursive-include Sane *.py +recursive-include Sane *.rst recursive-include Sane *.txt recursive-include Sane CHANGES recursive-include Sane README recursive-include Scripts *.py +recursive-include Scripts *.rst +recursive-include Scripts *.sh recursive-include Scripts README recursive-include Tests *.bdf recursive-include Tests *.bin recursive-include Tests *.bmp +recursive-include Tests *.bw +recursive-include Tests *.cur +recursive-include Tests *.dcx recursive-include Tests *.doc recursive-include Tests *.eps recursive-include Tests *.fli @@ -46,6 +54,7 @@ recursive-include Tests *.j2k recursive-include Tests *.jp2 recursive-include Tests *.jpg recursive-include Tests *.lut +recursive-include Tests *.mpo recursive-include Tests *.pbm recursive-include Tests *.pcf recursive-include Tests *.pcx @@ -55,7 +64,10 @@ recursive-include Tests *.png recursive-include Tests *.ppm recursive-include Tests *.psd recursive-include Tests *.py +recursive-include Tests *.ras +recursive-include Tests *.rgb recursive-include Tests *.rst +recursive-include Tests *.sgi recursive-include Tests *.spider recursive-include Tests *.tar recursive-include Tests *.tif @@ -65,22 +77,20 @@ 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 Tk *.txt recursive-include depends *.rst +recursive-include depends *.sh recursive-include docs *.bat recursive-include docs *.gitignore recursive-include docs *.html recursive-include docs *.py recursive-include docs *.rst recursive-include docs *.txt -recursive-include docs Guardfile -recursive-include docs Makefile recursive-include docs BUILDME recursive-include docs COPYING +recursive-include docs Guardfile recursive-include docs LICENSE +recursive-include docs Makefile recursive-include libImaging *.c recursive-include libImaging *.h -recursive-include Sane *.rst -recursive-include Scripts *.rst diff --git a/Makefile b/Makefile index 0637e901f..98e0c647a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,16 @@ +.PHONY: pre clean install test inplace coverage test-dep help docs livedocs +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " clean remove build products" + @echo " install make and install" + @echo " test run tests on installed pillow" + @echo " inplace make inplace extension" + @echo " coverage run coverage test (in progress)" + @echo " docs make html docs" + @echo " docserver run an http server on the docs directory" + @echo " test-dep install coveraget and test dependencies" pre: virtualenv . @@ -15,13 +27,14 @@ pre: clean: python setup.py clean rm PIL/*.so || true - find . -name __pycache__ | xargs rm -r + rm -r build || true + find . -name __pycache__ | xargs rm -r || true install: python setup.py install python selftest.py --installed -test: install +test: python test-installed.py inplace: clean @@ -40,3 +53,9 @@ coverage: test-dep: pip install coveralls nose nose-cov pep8 pyflakes + +docs: + $(MAKE) -C docs html + +docserver: + cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null& \ No newline at end of file diff --git a/PIL/CurImagePlugin.py b/PIL/CurImagePlugin.py index 4cf2882e2..0178957ee 100644 --- a/PIL/CurImagePlugin.py +++ b/PIL/CurImagePlugin.py @@ -33,6 +33,7 @@ i32 = _binary.i32le def _accept(prefix): return prefix[:4] == b"\0\0\2\0" + ## # Image plugin for Windows Cursor files. @@ -48,7 +49,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): # check magic s = self.fp.read(6) if not _accept(s): - raise SyntaxError("not an CUR file") + raise SyntaxError("not a CUR file") # pick the largest cursor in the file m = b"" @@ -58,14 +59,14 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): m = s elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]): m = s - #print "width", i8(s[0]) - #print "height", i8(s[1]) - #print "colors", i8(s[2]) - #print "reserved", i8(s[3]) - #print "hotspot x", i16(s[4:]) - #print "hotspot y", i16(s[6:]) - #print "bytes", i32(s[8:]) - #print "offset", i32(s[12:]) + # print "width", i8(s[0]) + # print "height", i8(s[1]) + # print "colors", i8(s[2]) + # print "reserved", i8(s[3]) + # print "hotspot x", i16(s[4:]) + # print "hotspot y", i16(s[6:]) + # print "bytes", i32(s[8:]) + # print "offset", i32(s[12:]) # load as bitmap self._bitmap(i32(m[12:]) + offset) @@ -73,7 +74,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): # patch up the bitmap height self.size = self.size[0], self.size[1]//2 d, e, o, a = self.tile[0] - self.tile[0] = d, (0,0)+self.size, o, a + self.tile[0] = d, (0, 0)+self.size, o, a return diff --git a/PIL/DcxImagePlugin.py b/PIL/DcxImagePlugin.py index 631875e68..0940b3935 100644 --- a/PIL/DcxImagePlugin.py +++ b/PIL/DcxImagePlugin.py @@ -27,13 +27,15 @@ from PIL import Image, _binary from PIL.PcxImagePlugin import PcxImageFile -MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? +MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? i32 = _binary.i32le + def _accept(prefix): return i32(prefix) == MAGIC + ## # Image plugin for the Intel DCX format. 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/ExifTags.py b/PIL/ExifTags.py index 25cd08068..52e145f62 100644 --- a/PIL/ExifTags.py +++ b/PIL/ExifTags.py @@ -67,8 +67,8 @@ TAGS = { 0x0213: "YCbCrPositioning", 0x0214: "ReferenceBlackWhite", 0x1000: "RelatedImageFileFormat", - 0x1001: "RelatedImageLength", # FIXME / Dictionary contains duplicate keys - 0x1001: "RelatedImageWidth", # FIXME \ Dictionary contains duplicate keys + 0x1001: "RelatedImageWidth", + 0x1002: "RelatedImageLength", 0x828d: "CFARepeatPatternDim", 0x828e: "CFAPattern", 0x828f: "BatteryLevel", diff --git a/PIL/FontFile.py b/PIL/FontFile.py index 7c5704c9d..26ddff0ac 100644 --- a/PIL/FontFile.py +++ b/PIL/FontFile.py @@ -17,8 +17,6 @@ import os from PIL import Image, _binary -import marshal - try: import zlib except ImportError: @@ -26,6 +24,7 @@ except ImportError: WIDTH = 800 + def puti16(fp, values): # write network order (big-endian) 16-bit sequence for v in values: @@ -33,6 +32,7 @@ def puti16(fp, values): v += 65536 fp.write(_binary.o16be(v)) + ## # Base class for raster font file handlers. @@ -95,9 +95,8 @@ class FontFile: # print chr(i), dst, s self.metrics[i] = d, dst, s - - def save1(self, filename): - "Save font in version 1 format" + def save(self, filename): + "Save font" self.compile() @@ -107,7 +106,7 @@ class FontFile: # font metrics fp = open(os.path.splitext(filename)[0] + ".pil", "wb") fp.write(b"PILfont\n") - fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!! + fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!! fp.write(b"DATA\n") for id in range(256): m = self.metrics[id] @@ -117,30 +116,4 @@ class FontFile: puti16(fp, m[0] + m[1] + m[2]) fp.close() - - def save2(self, filename): - "Save font in version 2 format" - - # THIS IS WORK IN PROGRESS - - self.compile() - - data = marshal.dumps((self.metrics, self.info)) - - if zlib: - data = b"z" + zlib.compress(data, 9) - else: - data = b"u" + data - - fp = open(os.path.splitext(filename)[0] + ".pil", "wb") - - fp.write(b"PILfont2\n" + self.name + "\n" + "DATA\n") - - fp.write(data) - - self.bitmap.save(fp, "PNG") - - fp.close() - - - save = save1 # for now +# End of file diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index ec8301973..640af9efc 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -96,8 +96,15 @@ class GifImageFile(ImageFile.ImageFile): # rewind self.__offset = 0 self.dispose = None + self.dispose_extent = [0, 0, 0, 0] #x0, y0, x1, y1 self.__frame = -1 self.__fp.seek(self.__rewind) + self._prev_im = None + self.disposal_method = 0 + else: + # ensure that the previous frame was loaded + if not self.im: + self.load() if frame != self.__frame + 1: raise ValueError("cannot seek to frame %d" % frame) @@ -114,8 +121,7 @@ class GifImageFile(ImageFile.ImageFile): self.__offset = 0 if self.dispose: - self.im = self.dispose - self.dispose = None + self.im.paste(self.dispose, self.dispose_extent) from copy import copy self.palette = copy(self.global_palette) @@ -140,17 +146,16 @@ class GifImageFile(ImageFile.ImageFile): if flags & 1: self.info["transparency"] = i8(block[3]) self.info["duration"] = i16(block[1:3]) * 10 - try: - # disposal methods - if flags & 8: - # replace with background colour - self.dispose = Image.core.fill("P", self.size, - self.info["background"]) - elif flags & 16: - # replace with previous contents - self.dispose = self.im.copy() - except (AttributeError, KeyError): - pass + + # disposal method - find the value of bits 4 - 6 + dispose_bits = 0b00011100 & flags + dispose_bits = dispose_bits >> 2 + if dispose_bits: + # only set the dispose if it is not + # unspecified. I'm not sure if this is + # correct, but it seems to prevent the last + # frame from looking odd for some animations + self.disposal_method = dispose_bits elif i8(s) == 255: # # application extension @@ -172,6 +177,7 @@ class GifImageFile(ImageFile.ImageFile): # extent x0, y0 = i16(s[0:]), i16(s[2:]) x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:]) + self.dispose_extent = x0, y0, x1, y1 flags = i8(s[8]) interlace = (flags & 64) != 0 @@ -194,6 +200,26 @@ class GifImageFile(ImageFile.ImageFile): pass # raise IOError, "illegal GIF tag `%x`" % i8(s) + try: + if self.disposal_method < 2: + # do not dispose or none specified + self.dispose = None + elif self.disposal_method == 2: + # replace with background colour + self.dispose = Image.core.fill("P", self.size, + self.info["background"]) + else: + # replace with previous contents + if self.im: + self.dispose = self.im.copy() + + # only dispose the extent in this frame + if self.dispose: + self.dispose = self.dispose.crop(self.dispose_extent) + except (AttributeError, KeyError): + pass + + if not self.tile: # self.__fp = None raise EOFError("no more images in GIF file") @@ -205,6 +231,18 @@ class GifImageFile(ImageFile.ImageFile): def tell(self): return self.__frame + def load_end(self): + ImageFile.ImageFile.load_end(self) + + # if the disposal method is 'do not dispose', transparent + # pixels should show the content of the previous frame + if self._prev_im and self.disposal_method == 1: + # we do this by pasting the updated area onto the previous + # frame which we then use as the current image content + updated = self.im.crop(self.dispose_extent) + self._prev_im.paste(updated, self.dispose_extent, updated.convert('RGBA')) + self.im = self._prev_im + self._prev_im = self.im.copy() # -------------------------------------------------------------------- # Write GIF files @@ -230,10 +268,9 @@ def _save(im, fp, filename): except IOError: pass # write uncompressed file - try: - rawmode = RAWMODE[im.mode] + if im.mode in RAWMODE: imOut = im - except KeyError: + else: # convert on the fly (EXPERIMENTAL -- I'm not sure PIL # should automatically convert images on save...) if Image.getmodebase(im.mode) == "RGB": @@ -241,10 +278,8 @@ def _save(im, fp, filename): if im.palette: palette_size = len(im.palette.getdata()[1]) // 3 imOut = im.convert("P", palette=1, colors=palette_size) - rawmode = "P" else: imOut = im.convert("L") - rawmode = "L" # header try: @@ -252,12 +287,6 @@ def _save(im, fp, filename): except KeyError: palette = None im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) - if im.encoderinfo["optimize"]: - # When the mode is L, and we optimize, we end up with - # im.mode == P and rawmode = L, which fails. - # If we're optimizing the palette, we're going to be - # in a rawmode of P anyway. - rawmode = 'P' header, usedPaletteColors = getheader(imOut, palette, im.encoderinfo) for s in header: @@ -314,7 +343,7 @@ def _save(im, fp, filename): o8(8)) # bits imOut.encoderconfig = (8, interlace) - ImageFile._save(imOut, fp, [("gif", (0,0)+im.size, 0, rawmode)]) + ImageFile._save(imOut, fp, [("gif", (0,0)+im.size, 0, RAWMODE[imOut.mode])]) fp.write(b"\0") # end of image data diff --git a/PIL/GimpGradientFile.py b/PIL/GimpGradientFile.py index 7c88addae..696f425f1 100644 --- a/PIL/GimpGradientFile.py +++ b/PIL/GimpGradientFile.py @@ -24,6 +24,7 @@ from PIL._binary import o8 EPSILON = 1e-10 + def linear(middle, pos): if pos <= middle: if middle < EPSILON: @@ -38,25 +39,30 @@ def linear(middle, pos): else: return 0.5 + 0.5 * pos / middle + def curved(middle, pos): return pos ** (log(0.5) / log(max(middle, EPSILON))) + def sine(middle, pos): return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 + def sphere_increasing(middle, pos): return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) + def sphere_decreasing(middle, pos): return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) -SEGMENTS = [ linear, curved, sine, sphere_increasing, sphere_decreasing ] +SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] + class GradientFile: gradient = None - def getpalette(self, entries = 256): + def getpalette(self, entries=256): palette = [] @@ -89,6 +95,7 @@ class GradientFile: return b"".join(palette), "RGBA" + ## # File handler for GIMP's gradient format. @@ -99,7 +106,13 @@ class GimpGradientFile(GradientFile): if fp.readline()[:13] != b"GIMP Gradient": raise SyntaxError("not a GIMP gradient file") - count = int(fp.readline()) + line = fp.readline() + + # GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do + if line.startswith(b"Name: "): + line = fp.readline().strip() + + count = int(line) gradient = [] @@ -108,13 +121,13 @@ class GimpGradientFile(GradientFile): s = fp.readline().split() w = [float(x) for x in s[:11]] - x0, x1 = w[0], w[2] - xm = w[1] - rgb0 = w[3:7] - rgb1 = w[7:11] + x0, x1 = w[0], w[2] + xm = w[1] + rgb0 = w[3:7] + rgb1 = w[7:11] segment = SEGMENTS[int(s[11])] - cspace = int(s[12]) + cspace = int(s[12]) if cspace != 0: raise IOError("cannot handle HSV colour space") diff --git a/PIL/IcnsImagePlugin.py b/PIL/IcnsImagePlugin.py index 6951c9325..ca7a14931 100644 --- a/PIL/IcnsImagePlugin.py +++ b/PIL/IcnsImagePlugin.py @@ -179,6 +179,8 @@ class IcnsFile: i = HEADERSIZE while i < filesize: sig, blocksize = nextheader(fobj) + if blocksize <= 0: + raise SyntaxError('invalid block header') i += HEADERSIZE blocksize -= HEADERSIZE dct[sig] = (i, blocksize) diff --git a/PIL/Image.py b/PIL/Image.py index 787e60270..cd1e450ee 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -220,6 +220,7 @@ _MODEINFO = { "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), "LAB": ("RGB", "L", ("L", "A", "B")), + "HSV": ("RGB", "L", ("H", "S", "V")), # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and # BGR;24. Use these modes only if you know exactly what you're @@ -554,7 +555,6 @@ class Image: self.readonly = 0 def _dump(self, file=None, format=None): - import os import tempfile suffix = '' if format: @@ -573,6 +573,8 @@ class Image: return file def __eq__(self, other): + if self.__class__.__name__ != other.__class__.__name__: + return False a = (self.mode == other.mode) b = (self.size == other.size) c = (self.getpalette() == other.getpalette()) @@ -1512,6 +1514,9 @@ class Image: self.load() + if self.size == size: + return self._new(self.im) + if self.mode in ("1", "P"): resample = NEAREST @@ -1908,6 +1913,16 @@ class Image: im = self.im.transpose(method) return self._new(im) + def effect_spread(self, distance): + """ + Randomly spread pixels in an image. + + :param distance: Distance to spread pixels. + """ + self.load() + im = self.im.effect_spread(distance) + return self._new(im) + # -------------------------------------------------------------------- # Lazy operations @@ -2417,3 +2432,32 @@ def _show(image, **options): def _showxv(image, title=None, **options): from PIL import ImageShow ImageShow.show(image, title, **options) + + +# -------------------------------------------------------------------- +# Effects + +def effect_mandelbrot(size, extent, quality): + """ + Generate a Mandelbrot set covering the given extent. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param extent: The extent to cover, as a 4-tuple: + (x0, y0, x1, y2). + :param quality: Quality. + """ + return Image()._new(core.effect_mandelbrot(size, extent, quality)) + + +def effect_noise(size, sigma): + """ + Generate Gaussian noise centered around 128. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param sigma: Standard deviation of noise. + """ + return Image()._new(core.effect_noise(size, sigma)) + +# End of file diff --git a/PIL/ImageCms.py b/PIL/ImageCms.py index fc176952e..04a3f7f61 100644 --- a/PIL/ImageCms.py +++ b/PIL/ImageCms.py @@ -1,19 +1,19 @@ -""" -The Python Imaging Library. -$Id$ +## The Python Imaging Library. +## $Id$ -Optional color managment support, based on Kevin Cazabon's PyCMS -library. +## Optional color managment support, based on Kevin Cazabon's PyCMS +## library. -History: -2009-03-08 fl Added to PIL. +## History: -Copyright (C) 2002-2003 Kevin Cazabon -Copyright (c) 2009 by Fredrik Lundh +## 2009-03-08 fl Added to PIL. -See the README file for information on usage and redistribution. See -below for the original description. -""" +## Copyright (C) 2002-2003 Kevin Cazabon +## Copyright (c) 2009 by Fredrik Lundh +## Copyright (c) 2013 by Eric Soroos + +## See the README file for information on usage and redistribution. See +## below for the original description. from __future__ import print_function @@ -150,8 +150,13 @@ for flag in FLAGS.values(): class ImageCmsProfile: def __init__(self, profile): - # accepts a string (filename), a file-like object, or a low-level - # profile object + """ + :param profile: Either a string representing a filename, + a file like object containing a profile or a + low-level profile object + + """ + if isStringType(profile): self._set(core.profile_open(profile), profile) elif hasattr(profile, "read"): @@ -169,12 +174,23 @@ class ImageCmsProfile: self.product_name = None self.product_info = None + def tobytes(self): + """ + Returns the profile in a format suitable for embedding in + saved images. + + :returns: a bytes object containing the ICC profile. + """ + + return core.profile_tobytes(self.profile) class ImageCmsTransform(Image.ImagePointHandler): - """Transform. This can be used with the procedural API, or with the - standard Image.point() method. - """ + # Transform. This can be used with the procedural API, or with the + # standard Image.point() method. + # + # Will return the output profile in the output.info['icc_profile']. + def __init__(self, input, output, input_mode, output_mode, intent=INTENT_PERCEPTUAL, proof=None, @@ -197,6 +213,8 @@ class ImageCmsTransform(Image.ImagePointHandler): self.input_mode = self.inputMode = input_mode self.output_mode = self.outputMode = output_mode + self.output_profile = output + def point(self, im): return self.apply(im) @@ -205,6 +223,7 @@ class ImageCmsTransform(Image.ImagePointHandler): if imOut is None: imOut = Image.new(self.output_mode, im.size, None) self.transform.apply(im.im.id, imOut.im.id) + imOut.info['icc_profile'] = self.output_profile.tobytes() return imOut def apply_in_place(self, im): @@ -212,6 +231,7 @@ class ImageCmsTransform(Image.ImagePointHandler): if im.mode != self.output_mode: raise ValueError("mode mismatch") # wrong output mode self.transform.apply(im.im.id, im.im.id) + im.info['icc_profile'] = self.output_profile.tobytes() return im @@ -570,7 +590,7 @@ def applyTransform(im, transform, inPlace=0): with the transform applied is returned (and im is not changed). The default is False. :returns: Either None, or a new PIL Image object, depending on the value of - inPlace + inPlace. The profile will be returned in the image's info['icc_profile']. :exception PyCMSError: """ @@ -637,7 +657,7 @@ def getProfileName(profile): (pyCMS) Gets the internal product name for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, + If profile isn't a valid CmsProfile object or filename to a profile, a PyCMSError is raised If an error occurs while trying to obtain the name tag, a PyCMSError is raised. @@ -876,7 +896,7 @@ def isIntentSupported(profile, intent, direction): input/output/proof profile as you desire. Some profiles are created specifically for one "direction", can cannot - be used for others. Some profiles can only be used for certain + be used for others. Some profiles can only be used for certain rendering intents... so it's best to either verify this before trying to create a transform with them (using this function), or catch the potential PyCMSError that will occur if they don't support the modes diff --git a/PIL/ImageFile.py b/PIL/ImageFile.py index adb27f2bd..bdcc769ec 100644 --- a/PIL/ImageFile.py +++ b/PIL/ImageFile.py @@ -133,11 +133,27 @@ class ImageFile(Image.Image): return pixel self.map = None - + use_mmap = self.filename and len(self.tile) == 1 + # As of pypy 2.1.0, memory mapping was failing here. + use_mmap = use_mmap and not hasattr(sys, 'pypy_version_info') + readonly = 0 - if self.filename and len(self.tile) == 1 and not hasattr(sys, 'pypy_version_info'): - # As of pypy 2.1.0, memory mapping was failing here. + # look for read/seek overrides + try: + read = self.load_read + # don't use mmap if there are custom read/seek functions + use_mmap = False + except AttributeError: + read = self.fp.read + + try: + seek = self.load_seek + use_mmap = False + except AttributeError: + seek = self.fp.seek + + if use_mmap: # try memory mapping d, e, o, a = self.tile[0] if d == "raw" and a[0] == self.mode and a[0] in Image._MAPMODES: @@ -165,19 +181,7 @@ class ImageFile(Image.Image): self.load_prepare() - # look for read/seek overrides - try: - read = self.load_read - except AttributeError: - read = self.fp.read - - try: - seek = self.load_seek - except AttributeError: - seek = self.fp.seek - if not self.map: - # sort tiles in file order self.tile.sort(key=_tilesort) @@ -223,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 @@ -467,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: @@ -477,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/ImageFont.py b/PIL/ImageFont.py index 18d09b871..bd52ea938 100644 --- a/PIL/ImageFont.py +++ b/PIL/ImageFont.py @@ -29,13 +29,15 @@ from __future__ import print_function from PIL import Image from PIL._util import isDirectory, isPath -import os, sys +import os +import sys try: import warnings except ImportError: warnings = None + class _imagingft_not_installed: # module placeholder def __getattr__(self, id): @@ -90,8 +92,8 @@ class ImageFont: # read PILfont header if file.readline() != b"PILfont\n": raise SyntaxError("Not a PILfont file") - d = file.readline().split(b";") - self.info = [] # FIXME: should be a dictionary + file.readline().split(b";") + self.info = [] # FIXME: should be a dictionary while True: s = file.readline() if not s or s == b"DATA\n": @@ -113,6 +115,7 @@ class ImageFont: self.getsize = self.font.getsize self.getmask = self.font.getmask + ## # Wrapper for FreeType fonts. Application code should use the # truetype factory function to create font objects. @@ -124,14 +127,18 @@ class FreeTypeFont: # FIXME: use service provider instead if file: if warnings: - warnings.warn('file parameter deprecated, please use font parameter instead.', DeprecationWarning) + warnings.warn( + 'file parameter deprecated, ' + 'please use font parameter instead.', + DeprecationWarning) font = file if isPath(font): self.font = core.getfont(font, size, index, encoding) else: self.font_bytes = font.read() - self.font = core.getfont("", size, index, encoding, self.font_bytes) + self.font = core.getfont( + "", size, index, encoding, self.font_bytes) def getname(self): return self.font.family, self.font.style @@ -140,7 +147,8 @@ class FreeTypeFont: return self.font.ascent, self.font.descent def getsize(self, text): - return self.font.getsize(text)[0] + size, offset = self.font.getsize(text) + return (size[0] + offset[0], size[1] + offset[1]) def getoffset(self, text): return self.font.getsize(text)[1] @@ -151,7 +159,7 @@ class FreeTypeFont: def getmask2(self, text, mode="", fill=Image.core.fill): size, offset = self.font.getsize(text) im = fill("L", size, 0) - self.font.render(text, im.id, mode=="1") + self.font.render(text, im.id, mode == "1") return im, offset ## @@ -163,12 +171,13 @@ class FreeTypeFont: # be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM, # Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270. + class TransposedFont: "Wrapper for writing rotated or mirrored text" def __init__(self, font, orientation=None): self.font = font - self.orientation = orientation # any 'transpose' argument, or None + self.orientation = orientation # any 'transpose' argument, or None def getsize(self, text): w, h = self.font.getsize(text) @@ -221,7 +230,10 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None): if filename: if warnings: - warnings.warn('filename parameter deprecated, please use font parameter instead.', DeprecationWarning) + warnings.warn( + 'filename parameter deprecated, ' + 'please use font parameter instead.', + DeprecationWarning) font = filename try: @@ -272,8 +284,8 @@ def load_default(): import base64 f = ImageFont() f._load_pilfont_data( - # courB08 - BytesIO(base64.decodestring(b''' + # courB08 + BytesIO(base64.decodestring(b''' UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA @@ -392,15 +404,4 @@ w7IkEbzhVQAAAABJRU5ErkJggg== ''')))) return f - -if __name__ == "__main__": - # create font data chunk for embedding - import base64, os, sys - font = "../Tests/images/courB08" - print(" f._load_pilfont_data(") - print(" # %s" % os.path.basename(font)) - print(" BytesIO(base64.decodestring(b'''") - base64.encode(open(font + ".pil", "rb"), sys.stdout) - print("''')), Image.open(BytesIO(base64.decodestring(b'''") - base64.encode(open(font + ".pbm", "rb"), sys.stdout) - print("'''))))") +# End of file diff --git a/PIL/ImageMath.py b/PIL/ImageMath.py index adfcc4f6f..4dcc5125c 100644 --- a/PIL/ImageMath.py +++ b/PIL/ImageMath.py @@ -26,9 +26,11 @@ except ImportError: VERBOSE = 0 + def _isconstant(v): return isinstance(v, int) or isinstance(v, float) + class _Operand: # wraps an image operand, providing standard operators @@ -68,20 +70,25 @@ class _Operand: im2 = self.__fixup(im2) if im1.mode != im2.mode: # convert both arguments to floating point - if im1.mode != "F": im1 = im1.convert("F") - if im2.mode != "F": im2 = im2.convert("F") + if im1.mode != "F": + im1 = im1.convert("F") + if im2.mode != "F": + im2 = im2.convert("F") if im1.mode != im2.mode: raise ValueError("mode mismatch") if im1.size != im2.size: # crop both arguments to a common size size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1])) - if im1.size != size: im1 = im1.crop((0, 0) + size) - if im2.size != size: im2 = im2.crop((0, 0) + size) + if im1.size != size: + im1 = im1.crop((0, 0) + size) + if im2.size != size: + im2 = im2.crop((0, 0) + size) out = Image.new(mode or im1.mode, size, None) else: out = Image.new(mode or im1.mode, im1.size, None) - im1.load(); im2.load() + im1.load() + im2.load() try: op = getattr(_imagingmath, op+"_"+im1.mode) except AttributeError: @@ -101,34 +108,47 @@ class _Operand: def __abs__(self): return self.apply("abs", self) + def __pos__(self): return self + def __neg__(self): return self.apply("neg", self) # binary operators def __add__(self, other): return self.apply("add", self, other) + def __radd__(self, other): return self.apply("add", other, self) + def __sub__(self, other): return self.apply("sub", self, other) + def __rsub__(self, other): return self.apply("sub", other, self) + def __mul__(self, other): return self.apply("mul", self, other) + def __rmul__(self, other): return self.apply("mul", other, self) + def __truediv__(self, other): return self.apply("div", self, other) + def __rtruediv__(self, other): return self.apply("div", other, self) + def __mod__(self, other): return self.apply("mod", self, other) + def __rmod__(self, other): return self.apply("mod", other, self) + def __pow__(self, other): return self.apply("pow", self, other) + def __rpow__(self, other): return self.apply("pow", other, self) @@ -142,54 +162,77 @@ class _Operand: # bitwise def __invert__(self): return self.apply("invert", self) + def __and__(self, other): return self.apply("and", self, other) + def __rand__(self, other): return self.apply("and", other, self) + def __or__(self, other): return self.apply("or", self, other) + def __ror__(self, other): return self.apply("or", other, self) + def __xor__(self, other): return self.apply("xor", self, other) + def __rxor__(self, other): return self.apply("xor", other, self) + def __lshift__(self, other): return self.apply("lshift", self, other) + def __rshift__(self, other): return self.apply("rshift", self, other) # logical def __eq__(self, other): return self.apply("eq", self, other) + def __ne__(self, other): return self.apply("ne", self, other) + def __lt__(self, other): return self.apply("lt", self, other) + def __le__(self, other): return self.apply("le", self, other) + def __gt__(self, other): return self.apply("gt", self, other) + def __ge__(self, other): return self.apply("ge", self, other) + # conversions def imagemath_int(self): return _Operand(self.im.convert("I")) + + def imagemath_float(self): return _Operand(self.im.convert("F")) + # logical def imagemath_equal(self, other): return self.apply("eq", self, other, mode="I") + + def imagemath_notequal(self, other): return self.apply("ne", self, other, mode="I") + def imagemath_min(self, other): return self.apply("min", self, other) + + def imagemath_max(self, other): return self.apply("max", self, other) + def imagemath_convert(self, mode): return _Operand(self.im.convert(mode)) diff --git a/PIL/ImageMorph.py b/PIL/ImageMorph.py index b24dd134f..3f15621a6 100644 --- a/PIL/ImageMorph.py +++ b/PIL/ImageMorph.py @@ -15,19 +15,19 @@ LUT_SIZE = 1 << 9 class LutBuilder: """A class for building a MorphLut from a descriptive language - The input patterns is a list of a strings sequences like these: + The input patterns is a list of a strings sequences like these:: - 4:(... - .1. - 111)->1 + 4:(... + .1. + 111)->1 (whitespaces including linebreaks are ignored). The option 4 describes a series of symmetry operations (in this case a 4-rotation), the pattern is described by: - . or X - Ignore - 1 - Pixel is on - 0 - Pixel is off + - . or X - Ignore + - 1 - Pixel is on + - 0 - Pixel is off The result of the operation is described after "->" string. @@ -35,15 +35,16 @@ class LutBuilder: returned if no other match is found. Operations: - 4 - 4 way rotation - N - Negate - 1 - Dummy op for no other operation (an op must always be given) - M - Mirroring + + - 4 - 4 way rotation + - N - Negate + - 1 - Dummy op for no other operation (an op must always be given) + - M - Mirroring - Example: - - lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) - lut = lb.build_lut() + Example:: + + lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) + lut = lb.build_lut() """ def __init__(self, patterns=None, op_name=None): diff --git a/PIL/ImageOps.py b/PIL/ImageOps.py index 64c35cca1..9f84eff86 100644 --- a/PIL/ImageOps.py +++ b/PIL/ImageOps.py @@ -392,7 +392,7 @@ def solarize(image, threshold=128): """ Invert all pixel values above a threshold. - :param image: The image to posterize. + :param image: The image to solarize. :param threshold: All pixels above this greyscale level are inverted. :return: An image. """ diff --git a/PIL/ImagePalette.py b/PIL/ImagePalette.py index 59886827a..62f8814f1 100644 --- a/PIL/ImagePalette.py +++ b/PIL/ImagePalette.py @@ -17,19 +17,20 @@ # import array -from PIL import Image, ImageColor +import warnings +from PIL import ImageColor class ImagePalette: "Color palette for palette mapped images" - def __init__(self, mode = "RGB", palette = None, size = 0): + def __init__(self, mode="RGB", palette=None, size=0): self.mode = mode - self.rawmode = None # if set, palette contains raw data + self.rawmode = None # if set, palette contains raw data self.palette = palette or list(range(256))*len(self.mode) self.colors = {} self.dirty = None - if ((size == 0 and len(self.mode)*256 != len(self.palette)) or + if ((size == 0 and len(self.mode)*256 != len(self.palette)) or (size != 0 and size != len(self.palette))): raise ValueError("wrong palette size") @@ -55,7 +56,7 @@ class ImagePalette: return self.palette arr = array.array("B", self.palette) if hasattr(arr, 'tobytes'): - #py3k has a tobytes, tostring is deprecated. + # py3k has a tobytes, tostring is deprecated. return arr.tobytes() return arr.tostring() @@ -109,6 +110,7 @@ class ImagePalette: fp.write("\n") fp.close() + # -------------------------------------------------------------------- # Internal @@ -119,32 +121,53 @@ def raw(rawmode, data): palette.dirty = 1 return palette + # -------------------------------------------------------------------- # Factories def _make_linear_lut(black, white): + warnings.warn( + '_make_linear_lut() is deprecated. ' + 'Please call make_linear_lut() instead.', + DeprecationWarning, + stacklevel=2 + ) + return make_linear_lut(black, white) + + +def _make_gamma_lut(exp): + warnings.warn( + '_make_gamma_lut() is deprecated. ' + 'Please call make_gamma_lut() instead.', + DeprecationWarning, + stacklevel=2 + ) + return make_gamma_lut(exp) + + +def make_linear_lut(black, white): lut = [] if black == 0: for i in range(256): lut.append(white*i//255) else: - raise NotImplementedError # FIXME + raise NotImplementedError # FIXME return lut -def _make_gamma_lut(exp, mode="RGB"): + +def make_gamma_lut(exp): lut = [] for i in range(256): lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5)) return lut -def new(mode, data): - return Image.core.new_palette(mode, data) def negative(mode="RGB"): palette = list(range(256)) palette.reverse() return ImagePalette(mode, palette * len(mode)) + def random(mode="RGB"): from random import randint palette = [] @@ -152,16 +175,19 @@ def random(mode="RGB"): palette.append(randint(0, 255)) return ImagePalette(mode, palette) + def sepia(white="#fff0c0"): r, g, b = ImageColor.getrgb(white) - r = _make_linear_lut(0, r) - g = _make_linear_lut(0, g) - b = _make_linear_lut(0, b) + r = make_linear_lut(0, r) + g = make_linear_lut(0, g) + b = make_linear_lut(0, b) return ImagePalette("RGB", r + g + b) + def wedge(mode="RGB"): return ImagePalette(mode, list(range(256)) * len(mode)) + def load(filename): # FIXME: supports GIMP gradients only @@ -177,8 +203,8 @@ def load(filename): p = GimpPaletteFile.GimpPaletteFile(fp) lut = p.getpalette() except (SyntaxError, ValueError): - #import traceback - #traceback.print_exc() + # import traceback + # traceback.print_exc() pass if not lut: @@ -188,8 +214,8 @@ def load(filename): p = GimpGradientFile.GimpGradientFile(fp) lut = p.getpalette() except (SyntaxError, ValueError): - #import traceback - #traceback.print_exc() + # import traceback + # traceback.print_exc() pass if not lut: @@ -206,4 +232,4 @@ def load(filename): if not lut: raise IOError("cannot load palette") - return lut # data, rawmode + return lut # data, rawmode diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py index 85575612c..dc8607591 100644 --- a/PIL/IptcImagePlugin.py +++ b/PIL/IptcImagePlugin.py @@ -21,7 +21,8 @@ __version__ = "0.3" from PIL import Image, ImageFile, _binary -import os, tempfile +import os +import tempfile i8 = _binary.i8 i16 = _binary.i16be @@ -35,17 +36,20 @@ COMPRESSION = { PAD = o8(0) * 4 + # # Helpers def i(c): return i32((PAD + c)[-4:]) + def dump(c): for i in c: print("%02x" % i8(i), end=' ') print() + ## # Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields # from TIFF and JPEG files, use the getiptcinfo function. @@ -84,35 +88,13 @@ class IptcImageFile(ImageFile.ImageFile): return tag, size - def _is_raw(self, offset, size): - # - # check if the file can be mapped - - # DISABLED: the following only slows things down... - return 0 - - self.fp.seek(offset) - t, sz = self.field() - if sz != size[0]: - return 0 - y = 1 - while True: - self.fp.seek(sz, 1) - t, s = self.field() - if t != (8, 10): - break - if s != sz: - return 0 - y += 1 - return y == size[1] - def _open(self): # load descriptive fields while True: offset = self.fp.tell() tag, size = self.field() - if not tag or tag == (8,10): + if not tag or tag == (8, 10): break if size: tagdata = self.fp.read(size) @@ -129,10 +111,10 @@ class IptcImageFile(ImageFile.ImageFile): # print tag, self.info[tag] # mode - layers = i8(self.info[(3,60)][0]) - component = i8(self.info[(3,60)][1]) - if (3,65) in self.info: - id = i8(self.info[(3,65)][0])-1 + layers = i8(self.info[(3, 60)][0]) + component = i8(self.info[(3, 60)][1]) + if (3, 65) in self.info: + id = i8(self.info[(3, 65)][0])-1 else: id = 0 if layers == 1 and not component: @@ -143,22 +125,18 @@ class IptcImageFile(ImageFile.ImageFile): self.mode = "CMYK"[id] # size - self.size = self.getint((3,20)), self.getint((3,30)) + self.size = self.getint((3, 20)), self.getint((3, 30)) # compression try: - compression = COMPRESSION[self.getint((3,120))] + compression = COMPRESSION[self.getint((3, 120))] except KeyError: raise IOError("Unknown IPTC image compression") # tile - if tag == (8,10): - if compression == "raw" and self._is_raw(offset, self.size): - self.tile = [(compression, (offset, size + 5, -1), - (0, 0, self.size[0], self.size[1]))] - else: - self.tile = [("iptc", (compression, offset), - (0, 0, self.size[0], self.size[1]))] + if tag == (8, 10): + self.tile = [("iptc", (compression, offset), + (0, 0, self.size[0], self.size[1]))] def load(self): @@ -200,14 +178,17 @@ class IptcImageFile(ImageFile.ImageFile): im.load() self.im = im.im finally: - try: os.unlink(outfile) - except: pass + try: + os.unlink(outfile) + except: + pass Image.register_open("IPTC", IptcImageFile) Image.register_extension("IPTC", ".iim") + ## # Get IPTC information from TIFF, JPEG, or IPTC file. # @@ -230,11 +211,11 @@ def getiptcinfo(im): # extract the IPTC/NAA resource try: app = im.app["APP13"] - if app[:14] == "Photoshop 3.0\x00": + if app[:14] == b"Photoshop 3.0\x00": app = app[14:] # parse the image resource block offset = 0 - while app[offset:offset+4] == "8BIM": + while app[offset:offset+4] == b"8BIM": offset += 4 # resource code code = JpegImagePlugin.i16(app, offset) @@ -267,7 +248,7 @@ def getiptcinfo(im): pass if data is None: - return None # no properties + return None # no properties # create an IptcImagePlugin object without initializing it class FakeImage: @@ -282,6 +263,6 @@ def getiptcinfo(im): try: im._open() except (IndexError, KeyError): - pass # expected failure + pass # expected failure return im.info diff --git a/PIL/Jpeg2KImagePlugin.py b/PIL/Jpeg2KImagePlugin.py index 0a7a6e297..53b10ca1a 100644 --- a/PIL/Jpeg2KImagePlugin.py +++ b/PIL/Jpeg2KImagePlugin.py @@ -70,6 +70,9 @@ def _parse_jp2_header(fp): else: hlen = 8 + if lbox < hlen: + raise SyntaxError('Invalid JP2 header length') + if tbox == b'jp2h': header = fp.read(lbox - hlen) break diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index a434c5581..dde7d93ca 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -36,7 +36,9 @@ __version__ = "0.6" import array import struct -from PIL import Image, ImageFile, _binary +import io +from struct import unpack +from PIL import Image, ImageFile, TiffImagePlugin, _binary from PIL.JpegPresets import presets from PIL._util import isStringType @@ -110,6 +112,11 @@ def APP(self, marker): pass else: self.info["adobe_transform"] = adobe_transform + elif marker == 0xFFE2 and s[:4] == b"MPF\0": + # extract MPO information + self.info["mp"] = s[4:] + # offset is current location minus buffer size plus constant header size + self.info["mpoffset"] = self.fp.tell() - n + 4 def COM(self, marker): @@ -380,18 +387,22 @@ class JpegImageFile(ImageFile.ImageFile): def _getexif(self): return _getexif(self) + def _getmp(self): + return _getmp(self) + + +def _fixup(value): + # Helper function for _getexif() and _getmp() + if len(value) == 1: + return value[0] + return value + def _getexif(self): # Extract EXIF information. This method is highly experimental, # and is likely to be replaced with something better in a future # version. - from PIL import TiffImagePlugin - import io - def fixup(value): - if len(value) == 1: - return value[0] - return value # The EXIF record consists of a TIFF file embedded in a JPEG # application marker (!). try: @@ -405,7 +416,7 @@ def _getexif(self): info = TiffImagePlugin.ImageFileDirectory(head) info.load(file) for key, value in info.items(): - exif[key] = fixup(value) + exif[key] = _fixup(value) # get exif extension try: file.seek(exif[0x8769]) @@ -415,7 +426,7 @@ def _getexif(self): info = TiffImagePlugin.ImageFileDirectory(head) info.load(file) for key, value in info.items(): - exif[key] = fixup(value) + exif[key] = _fixup(value) # get gpsinfo extension try: file.seek(exif[0x8825]) @@ -426,9 +437,77 @@ def _getexif(self): info.load(file) exif[0x8825] = gps = {} for key, value in info.items(): - gps[key] = fixup(value) + gps[key] = _fixup(value) return exif + +def _getmp(self): + # Extract MP information. This method was inspired by the "highly + # experimental" _getexif version that's been in use for years now, + # itself based on the ImageFileDirectory class in the TIFF plug-in. + + # The MP record essentially consists of a TIFF file embedded in a JPEG + # application marker. + try: + data = self.info["mp"] + except KeyError: + return None + file = io.BytesIO(data) + head = file.read(8) + endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<' + mp = {} + # process dictionary + info = TiffImagePlugin.ImageFileDirectory(head) + info.load(file) + for key, value in info.items(): + mp[key] = _fixup(value) + # it's an error not to have a number of images + try: + quant = mp[0xB001] + except KeyError: + raise SyntaxError("malformed MP Index (no number of images)") + # get MP entries + try: + mpentries = [] + for entrynum in range(0, quant): + rawmpentry = mp[0xB002][entrynum * 16:(entrynum + 1) * 16] + unpackedentry = unpack('{0}LLLHH'.format(endianness), rawmpentry) + labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', 'EntryNo2') + mpentry = dict(zip(labels, unpackedentry)) + mpentryattr = { + 'DependentParentImageFlag': bool(mpentry['Attribute'] & (1<<31)), + 'DependentChildImageFlag': bool(mpentry['Attribute'] & (1<<30)), + 'RepresentativeImageFlag': bool(mpentry['Attribute'] & (1<<29)), + 'Reserved': (mpentry['Attribute'] & (3<<27)) >> 27, + 'ImageDataFormat': (mpentry['Attribute'] & (7<<24)) >> 24, + 'MPType': mpentry['Attribute'] & 0x00FFFFFF + } + if mpentryattr['ImageDataFormat'] == 0: + mpentryattr['ImageDataFormat'] = 'JPEG' + else: + raise SyntaxError("unsupported picture format in MPO") + mptypemap = { + 0x000000: 'Undefined', + 0x010001: 'Large Thumbnail (VGA Equivalent)', + 0x010002: 'Large Thumbnail (Full HD Equivalent)', + 0x020001: 'Multi-Frame Image (Panorama)', + 0x020002: 'Multi-Frame Image: (Disparity)', + 0x020003: 'Multi-Frame Image: (Multi-Angle)', + 0x030000: 'Baseline MP Primary Image' + } + mpentryattr['MPType'] = mptypemap.get(mpentryattr['MPType'], + 'Unknown') + mpentry['Attribute'] = mpentryattr + mpentries.append(mpentry) + mp[0xB002] = mpentries + except KeyError: + raise SyntaxError("malformed MP Index (bad MP Entry)") + # Next we should try and parse the individual image unique ID list; + # we don't because I've never seen this actually used in a real MPO + # file and so can't test it. + return mp + + # -------------------------------------------------------------------- # stuff to save JPEG files @@ -466,6 +545,15 @@ def convert_dict_qtables(qtables): def get_sampling(im): + # There's no subsampling when image have only 1 layer + # (grayscale images) or when they are CMYK (4 layers), + # so set subsampling to default value. + # + # NOTE: currently Pillow can't encode JPEG to YCCK format. + # If YCCK support is added in the future, subsampling code will have + # to be updated (here and in JpegEncode.c) to deal with 4 layers. + if not hasattr(im, 'layers') or im.layers in (1, 4): + return -1 sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] return samplings.get(sampling, -1) @@ -611,10 +699,27 @@ def _save_cjpeg(im, fp, filename): except: pass + +## +# Factory for making JPEG and MPO instances +def jpeg_factory(fp=None, filename=None): + im = JpegImageFile(fp, filename) + mpheader = im._getmp() + try: + if mpheader[45057] > 1: + # It's actually an MPO + from .MpoImagePlugin import MpoImageFile + im = MpoImageFile(fp, filename) + except (TypeError, IndexError): + # It is really a JPEG + pass + return im + + # -------------------------------------------------------------------q- # Registry stuff -Image.register_open("JPEG", JpegImageFile, _accept) +Image.register_open("JPEG", jpeg_factory, _accept) Image.register_save("JPEG", _save) Image.register_extension("JPEG", ".jfif") diff --git a/PIL/MpoImagePlugin.py b/PIL/MpoImagePlugin.py new file mode 100644 index 000000000..d053d9026 --- /dev/null +++ b/PIL/MpoImagePlugin.py @@ -0,0 +1,87 @@ +# +# The Python Imaging Library. +# $Id$ +# +# MPO file handling +# +# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the +# Camera & Imaging Products Association) +# +# The multi-picture object combines multiple JPEG images (with a modified EXIF +# data format) into a single file. While it can theoretically be used much like +# a GIF animation, it is commonly used to represent 3D photographs and is (as +# of this writing) the most commonly used format by 3D cameras. +# +# History: +# 2014-03-13 Feneric Created +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.1" + +from PIL import Image, JpegImagePlugin + +def _accept(prefix): + return JpegImagePlugin._accept(prefix) + +def _save(im, fp, filename): + # Note that we can only save the current frame at present + return JpegImagePlugin._save(im, fp, filename) + +## +# Image plugin for MPO images. + +class MpoImageFile(JpegImagePlugin.JpegImageFile): + + format = "MPO" + format_description = "MPO (CIPA DC-007)" + + def _open(self): + self.fp.seek(0) # prep the fp in order to pass the JPEG test + JpegImagePlugin.JpegImageFile._open(self) + self.mpinfo = self._getmp() + self.__framecount = self.mpinfo[0xB001] + self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset'] \ + for mpent in self.mpinfo[0xB002]] + self.__mpoffsets[0] = 0 + # Note that the following assertion will only be invalid if something + # gets broken within JpegImagePlugin. + assert self.__framecount == len(self.__mpoffsets) + del self.info['mpoffset'] # no longer needed + self.__fp = self.fp # FIXME: hack + self.__fp.seek(self.__mpoffsets[0]) # get ready to read first frame + self.__frame = 0 + self.offset = 0 + # for now we can only handle reading and individual frame extraction + self.readonly = 1 + + def load_seek(self, pos): + self.__fp.seek(pos) + + def seek(self, frame): + if frame < 0 or frame >= self.__framecount: + raise EOFError("no more images in MPO file") + else: + self.fp = self.__fp + self.offset = self.__mpoffsets[frame] + self.tile = [ + ("jpeg", (0, 0) + self.size, self.offset, (self.mode, "")) + ] + self.__frame = frame + + def tell(self): + return self.__frame + + +# -------------------------------------------------------------------q- +# Registry stuff + +# Note that since MPO shares a factory with JPEG, we do not need to do a +# separate registration for it here. +#Image.register_open("MPO", JpegImagePlugin.jpeg_factory, _accept) +Image.register_save("MPO", _save) + +Image.register_extension("MPO", ".mpo") + +Image.register_mime("MPO", "image/mpo") diff --git a/PIL/OleFileIO.py b/PIL/OleFileIO.py index 8a3c77be4..e35bfa679 100644 --- a/PIL/OleFileIO.py +++ b/PIL/OleFileIO.py @@ -1,28 +1,29 @@ #!/usr/local/bin/python # -*- coding: latin-1 -*- -""" -OleFileIO_PL: -Module to read Microsoft OLE2 files (also called Structured Storage or -Microsoft Compound Document File Format), such as Microsoft Office -documents, Image Composer and FlashPix files, Outlook messages, ... -This version is compatible with Python 2.6+ and 3.x +## OleFileIO_PL: +## Module to read Microsoft OLE2 files (also called Structured Storage or +## Microsoft Compound Document File Format), such as Microsoft Office +## documents, Image Composer and FlashPix files, Outlook messages, ... +## This version is compatible with Python 2.6+ and 3.x -version 0.30 2014-02-04 Philippe Lagadec - http://www.decalage.info +## version 0.30 2014-02-04 Philippe Lagadec - http://www.decalage.info -Project website: http://www.decalage.info/python/olefileio +## Project website: http://www.decalage.info/python/olefileio -Improved version of the OleFileIO module from PIL library v1.1.6 -See: http://www.pythonware.com/products/pil/index.htm +## Improved version of the OleFileIO module from PIL library v1.1.6 +## See: http://www.pythonware.com/products/pil/index.htm -The Python Imaging Library (PIL) is - Copyright (c) 1997-2005 by Secret Labs AB - Copyright (c) 1995-2005 by Fredrik Lundh -OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec +## The Python Imaging Library (PIL) is -See source code and LICENSE.txt for information on usage and redistribution. +## Copyright (c) 1997-2005 by Secret Labs AB +## Copyright (c) 1995-2005 by Fredrik Lundh + +## OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec + +## See source code and LICENSE.txt for information on usage and redistribution. + +## WARNING: THIS IS (STILL) WORK IN PROGRESS. -WARNING: THIS IS (STILL) WORK IN PROGRESS. -""" # Starting with OleFileIO_PL v0.30, only Python 2.6+ and 3.x is supported # This import enables print() as a function rather than a keyword @@ -370,8 +371,9 @@ for key in list(vars().keys()): def isOleFile (filename): """ Test if file is an OLE container (according to its header). - filename: file name or path (str, unicode) - return: True if OLE, False otherwise. + + :param filename: file name or path (str, unicode) + :returns: True if OLE, False otherwise. """ f = open(filename, 'rb') header = f.read(len(MAGIC)) @@ -397,8 +399,8 @@ def i16(c, o = 0): """ Converts a 2-bytes (16 bits) string to an integer. - c: string containing bytes to convert - o: offset of bytes to convert in string + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string """ return i8(c[o]) | (i8(c[o+1])<<8) @@ -407,8 +409,8 @@ def i32(c, o = 0): """ Converts a 4-bytes (32 bits) string to an integer. - c: string containing bytes to convert - o: offset of bytes to convert in string + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string """ ## return int(ord(c[o])+(ord(c[o+1])<<8)+(ord(c[o+2])<<16)+(ord(c[o+3])<<24)) ## # [PL]: added int() because "<<" gives long int since Python 2.4 @@ -419,7 +421,8 @@ def i32(c, o = 0): def _clsid(clsid): """ Converts a CLSID to a human-readable string. - clsid: string of length 16. + + :param clsid: string of length 16. """ assert len(clsid) == 16 # if clsid is only made of null bytes, return an empty string: @@ -439,8 +442,8 @@ def _unicode(s, errors='replace'): """ Map unicode string to Latin 1. (Python with Unicode support) - s: UTF-16LE unicode string to convert to Latin-1 - errors: 'replace', 'ignore' or 'strict'. + :param s: UTF-16LE unicode string to convert to Latin-1 + :param errors: 'replace', 'ignore' or 'strict'. """ #TODO: test if it OleFileIO works with Unicode strings, instead of # converting to Latin-1. @@ -650,14 +653,14 @@ class _OleStream(io.BytesIO): """ Constructor for _OleStream class. - fp : file object, the OLE container or the MiniFAT stream - sect : sector index of first sector in the stream - size : total size of the stream - offset : offset in bytes for the first FAT or MiniFAT sector - sectorsize: size of one sector - fat : array/list of sector indexes (FAT or MiniFAT) - filesize : size of OLE file (for debugging) - return : a BytesIO instance containing the OLE stream + :param fp : file object, the OLE container or the MiniFAT stream + :param sect : sector index of first sector in the stream + :param size : total size of the stream + :param offset : offset in bytes for the first FAT or MiniFAT sector + :param sectorsize: size of one sector + :param fat : array/list of sector indexes (FAT or MiniFAT) + :param filesize : size of OLE file (for debugging) + :returns : a BytesIO instance containing the OLE stream """ debug('_OleStream.__init__:') debug(' sect=%d (%X), size=%d, offset=%d, sectorsize=%d, len(fat)=%d, fp=%s' @@ -793,9 +796,9 @@ class _OleDirectoryEntry: Constructor for an _OleDirectoryEntry object. Parses a 128-bytes entry from the OLE Directory stream. - entry : string (must be 128 bytes long) - sid : index of this directory entry in the OLE file directory - olefile: OleFileIO containing this directory entry + :param entry : string (must be 128 bytes long) + :param sid : index of this directory entry in the OLE file directory + :param olefile: OleFileIO containing this directory entry """ self.sid = sid # ref to olefile is stored for future use @@ -989,7 +992,7 @@ class _OleDirectoryEntry: """ Return modification time of a directory entry. - return: None if modification time is null, a python datetime object + :returns: None if modification time is null, a python datetime object otherwise (UTC timezone) new in version 0.26 @@ -1003,7 +1006,7 @@ class _OleDirectoryEntry: """ Return creation time of a directory entry. - return: None if modification time is null, a python datetime object + :returns: None if modification time is null, a python datetime object otherwise (UTC timezone) new in version 0.26 @@ -1020,7 +1023,8 @@ class OleFileIO: OLE container object This class encapsulates the interface to an OLE 2 structured - storage file. Use the {@link listdir} and {@link openstream} methods to + storage file. Use the :py:meth:`~PIL.OleFileIO.OleFileIO.listdir` and + :py:meth:`~PIL.OleFileIO.OleFileIO.openstream` methods to access the contents of this file. Object names are given as a list of strings, one for each subentry @@ -1048,8 +1052,8 @@ class OleFileIO: """ Constructor for OleFileIO class. - filename: file to open. - raise_defects: minimal level for defects to be raised as exceptions. + :param filename: file to open. + :param raise_defects: minimal level for defects to be raised as exceptions. (use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a security-oriented application, see source code for details) """ @@ -1068,13 +1072,13 @@ class OleFileIO: It may raise an IOError exception according to the minimal level chosen for the OleFileIO object. - defect_level: defect level, possible values are: + :param defect_level: defect level, possible values are: DEFECT_UNSURE : a case which looks weird, but not sure it's a defect DEFECT_POTENTIAL : a potential defect DEFECT_INCORRECT : an error according to specifications, but parsing can go on DEFECT_FATAL : an error which cannot be ignored, parsing is impossible - message: string describing the defect, used with raised exception. - exception_type: exception class to be raised, IOError by default + :param message: string describing the defect, used with raised exception. + :param exception_type: exception class to be raised, IOError by default """ # added by [PL] if defect_level >= self._raise_defects_level: @@ -1089,7 +1093,7 @@ class OleFileIO: Open an OLE2 file. Reads the header, FAT and directory. - filename: string-like or file-like object + :param filename: string-like or file-like object """ #[PL] check if filename is a string-like or file-like object: # (it is better to check for a read() method) @@ -1276,8 +1280,8 @@ class OleFileIO: Checks if a stream has not been already referenced elsewhere. This method should only be called once for each known stream, and only if stream size is not null. - first_sect: index of first sector of the stream in FAT - minifat: if True, stream is located in the MiniFAT, else in the FAT + :param first_sect: index of first sector of the stream in FAT + :param minifat: if True, stream is located in the MiniFAT, else in the FAT """ if minifat: debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect) @@ -1371,8 +1375,9 @@ class OleFileIO: def loadfat_sect(self, sect): """ Adds the indexes of the given sector to the FAT - sect: string containing the first FAT sector, or array of long integers - return: index of last FAT sector. + + :param sect: string containing the first FAT sector, or array of long integers + :returns: index of last FAT sector. """ # a FAT sector is an array of ulong integers. if isinstance(sect, array.array): @@ -1505,8 +1510,9 @@ class OleFileIO: def getsect(self, sect): """ Read given sector from file on disk. - sect: sector index - returns a string containing the sector data. + + :param sect: sector index + :returns: a string containing the sector data. """ # [PL] this original code was wrong when sectors are 4KB instead of # 512 bytes: @@ -1530,7 +1536,8 @@ class OleFileIO: def loaddirectory(self, sect): """ Load the directory. - sect: sector index of directory stream. + + :param sect: sector index of directory stream. """ # The directory is stored in a standard # substream, independent of its size. @@ -1567,9 +1574,10 @@ class OleFileIO: Load a directory entry from the directory. This method should only be called once for each storage/stream when loading the directory. - sid: index of storage/stream in the directory. - return: a _OleDirectoryEntry object - raise: IOError if the entry has always been referenced. + + :param sid: index of storage/stream in the directory. + :returns: a _OleDirectoryEntry object + :exception IOError: if the entry has always been referenced. """ # check if SID is OK: if sid<0 or sid>=len(self.direntries): @@ -1598,9 +1606,9 @@ class OleFileIO: Open a stream, either in FAT or MiniFAT according to its size. (openstream helper) - start: index of first sector - size: size of stream (or nothing if size is unknown) - force_FAT: if False (default), stream will be opened in FAT or MiniFAT + :param start: index of first sector + :param size: size of stream (or nothing if size is unknown) + :param force_FAT: if False (default), stream will be opened in FAT or MiniFAT according to size. If True, it will always be opened in FAT. """ debug('OleFileIO.open(): sect=%d, size=%d, force_FAT=%s' % @@ -1630,11 +1638,11 @@ class OleFileIO: def _list(self, files, prefix, node, streams=True, storages=False): """ (listdir helper) - files: list of files to fill in - prefix: current location in storage tree (list of names) - node: current node (_OleDirectoryEntry object) - streams: bool, include streams if True (True by default) - new in v0.26 - storages: bool, include storages if True (False by default) - new in v0.26 + :param files: list of files to fill in + :param prefix: current location in storage tree (list of names) + :param node: current node (_OleDirectoryEntry object) + :param streams: bool, include streams if True (True by default) - new in v0.26 + :param storages: bool, include storages if True (False by default) - new in v0.26 (note: the root storage is never included) """ prefix = prefix + [node.name] @@ -1657,9 +1665,9 @@ class OleFileIO: """ Return a list of streams stored in this file - streams: bool, include streams if True (True by default) - new in v0.26 - storages: bool, include storages if True (False by default) - new in v0.26 - (note: the root storage is never included) + :param streams: bool, include streams if True (True by default) - new in v0.26 + :param storages: bool, include storages if True (False by default) - new in v0.26 + (note: the root storage is never included) """ files = [] self._list(files, [], self.root, streams, storages) @@ -1671,12 +1679,13 @@ class OleFileIO: Returns directory entry of given filename. (openstream helper) Note: this method is case-insensitive. - filename: path of stream in storage tree (except root entry), either: + :param filename: path of stream in storage tree (except root entry), either: + - a string using Unix path syntax, for example: 'storage_1/storage_1.2/stream' - a list of storage filenames, path to the desired stream/storage. Example: ['storage_1', 'storage_1.2', 'stream'] - return: sid of requested filename + :returns: sid of requested filename raise IOError if file not found """ @@ -1700,13 +1709,15 @@ class OleFileIO: """ Open a stream as a read-only file object (BytesIO). - filename: path of stream in storage tree (except root entry), either: + :param filename: path of stream in storage tree (except root entry), either: + - a string using Unix path syntax, for example: 'storage_1/storage_1.2/stream' - a list of storage filenames, path to the desired stream/storage. Example: ['storage_1', 'storage_1.2', 'stream'] - return: file object (read-only) - raise IOError if filename not found, or if this is not a stream. + + :returns: file object (read-only) + :exception IOError: if filename not found, or if this is not a stream. """ sid = self._find(filename) entry = self.direntries[sid] @@ -1720,8 +1731,9 @@ class OleFileIO: Test if given filename exists as a stream or a storage in the OLE container, and return its type. - filename: path of stream in storage tree. (see openstream for syntax) - return: False if object does not exist, its entry type (>0) otherwise: + :param filename: path of stream in storage tree. (see openstream for syntax) + :returns: False if object does not exist, its entry type (>0) otherwise: + - STGTY_STREAM: a stream - STGTY_STORAGE: a storage - STGTY_ROOT: the root entry @@ -1738,10 +1750,10 @@ class OleFileIO: """ Return modification time of a stream/storage. - filename: path of stream/storage in storage tree. (see openstream for - syntax) - return: None if modification time is null, a python datetime object - otherwise (UTC timezone) + :param filename: path of stream/storage in storage tree. (see openstream for + syntax) + :returns: None if modification time is null, a python datetime object + otherwise (UTC timezone) new in version 0.26 """ @@ -1754,10 +1766,10 @@ class OleFileIO: """ Return creation time of a stream/storage. - filename: path of stream/storage in storage tree. (see openstream for - syntax) - return: None if creation time is null, a python datetime object - otherwise (UTC timezone) + :param filename: path of stream/storage in storage tree. (see openstream for + syntax) + :returns: None if creation time is null, a python datetime object + otherwise (UTC timezone) new in version 0.26 """ @@ -1771,8 +1783,8 @@ class OleFileIO: Test if given filename exists as a stream or a storage in the OLE container. - filename: path of stream in storage tree. (see openstream for syntax) - return: True if object exist, else False. + :param filename: path of stream in storage tree. (see openstream for syntax) + :returns: True if object exist, else False. """ try: sid = self._find(filename) @@ -1785,9 +1797,10 @@ class OleFileIO: """ Return size of a stream in the OLE container, in bytes. - filename: path of stream in storage tree (see openstream for syntax) - return: size in bytes (long integer) - raise: IOError if file not found, TypeError if this is not a stream. + :param filename: path of stream in storage tree (see openstream for syntax) + :returns: size in bytes (long integer) + :exception IOError: if file not found + :exception TypeError: if this is not a stream """ sid = self._find(filename) entry = self.direntries[sid] @@ -1809,11 +1822,11 @@ class OleFileIO: """ Return properties described in substream. - filename: path of stream in storage tree (see openstream for syntax) - convert_time: bool, if True timestamps will be converted to Python datetime - no_conversion: None or list of int, timestamps not to be converted - (for example total editing time is not a real timestamp) - return: a dictionary of values indexed by id (integer) + :param filename: path of stream in storage tree (see openstream for syntax) + :param convert_time: bool, if True timestamps will be converted to Python datetime + :param no_conversion: None or list of int, timestamps not to be converted + (for example total editing time is not a real timestamp) + :returns: a dictionary of values indexed by id (integer) """ # make sure no_conversion is a list, just to simplify code below: if no_conversion == None: diff --git a/PIL/PSDraw.py b/PIL/PSDraw.py index 88593bb9d..26fdb74ea 100644 --- a/PIL/PSDraw.py +++ b/PIL/PSDraw.py @@ -73,9 +73,8 @@ class PSDraw: def setink(self, ink): """ - .. warning:: + .. warning:: This has been in the PIL API for ages but was never implemented. - This has been in the PIL API for ages but was never implemented. """ print("*** NOT YET IMPLEMENTED ***") diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index e794ef702..4dbedb783 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -147,6 +147,17 @@ class ChunkStream: return cids +# -------------------------------------------------------------------- +# Subclass of string to allow iTXt chunks to look like strings while +# keeping their extra information + +class iTXt(str): + @staticmethod + def __new__(cls, text, lang, tkey): + self = str.__new__(cls, text) + self.lang = lang + self.tkey = tkey + return self # -------------------------------------------------------------------- # PNG chunk container (for use with save(pnginfo=)) @@ -159,14 +170,36 @@ class PngInfo: def add(self, cid, data): self.chunks.append((cid, data)) + def add_itxt(self, key, value, lang="", tkey="", zip=False): + if not isinstance(key, bytes): + key = key.encode("latin-1", "strict") + if not isinstance(value, bytes): + value = value.encode("utf-8", "strict") + if not isinstance(lang, bytes): + lang = lang.encode("utf-8", "strict") + if not isinstance(tkey, bytes): + tkey = tkey.encode("utf-8", "strict") + + if zip: + import zlib + self.add(b"iTXt", key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value)) + else: + self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) + def add_text(self, key, value, zip=0): + if isinstance(value, iTXt): + return self.add_itxt(key, value, value.lang, value.tkey, bool(zip)) + # The tEXt chunk stores latin-1 text + if not isinstance(value, bytes): + try: + value = value.encode('latin-1', 'strict') + except UnicodeError: + return self.add_itxt(key, value, zip=bool(zip)) + if not isinstance(key, bytes): key = key.encode('latin-1', 'strict') - if not isinstance(value, bytes): - value = value.encode('latin-1', 'replace') - if zip: import zlib self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) @@ -329,6 +362,43 @@ class PngStream(ChunkStream): self.im_info[k] = self.im_text[k] = v return s + def chunk_iTXt(self, pos, length): + + # international text + r = s = ImageFile._safe_read(self.fp, length) + try: + k, r = r.split(b"\0", 1) + except ValueError: + return s + if len(r) < 2: + return s + cf, cm, r = i8(r[0]), i8(r[1]), r[2:] + try: + lang, tk, v = r.split(b"\0", 2) + except ValueError: + return s + if cf != 0: + if cm == 0: + import zlib + try: + v = zlib.decompress(v) + except zlib.error: + return s + else: + return s + if bytes is not str: + try: + k = k.decode("latin-1", "strict") + lang = lang.decode("utf-8", "strict") + tk = tk.decode("utf-8", "strict") + v = v.decode("utf-8", "strict") + except UnicodeError: + return s + + self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) + + return s + # -------------------------------------------------------------------- # PNG reader diff --git a/PIL/PyAccess.py b/PIL/PyAccess.py index f76beb820..7ccc313eb 100644 --- a/PIL/PyAccess.py +++ b/PIL/PyAccess.py @@ -261,6 +261,7 @@ mode_map = {'1': _PyAccess8, 'PA': _PyAccess32_2, 'RGB': _PyAccess32_3, 'LAB': _PyAccess32_3, + 'HSV': _PyAccess32_3, 'YCbCr': _PyAccess32_3, 'RGBA': _PyAccess32_4, 'RGBa': _PyAccess32_4, diff --git a/PIL/SgiImagePlugin.py b/PIL/SgiImagePlugin.py index b60df473c..2b8fcd8e4 100644 --- a/PIL/SgiImagePlugin.py +++ b/PIL/SgiImagePlugin.py @@ -31,6 +31,7 @@ i32 = _binary.i32be def _accept(prefix): return i16(prefix) == 474 + ## # Image plugin for SGI images. @@ -44,7 +45,7 @@ class SgiImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(512) if i16(s) != 474: - raise SyntaxError("not an SGI image file") + raise ValueError("Not an SGI image file") # relevant header entries compression = i8(s[2]) @@ -60,22 +61,22 @@ class SgiImageFile(ImageFile.ImageFile): elif layout == (1, 3, 4): self.mode = "RGBA" else: - raise SyntaxError("unsupported SGI image mode") + raise ValueError("Unsupported SGI image mode") # size self.size = i16(s[6:]), i16(s[8:]) - # decoder info if compression == 0: offset = 512 pagesize = self.size[0]*self.size[1]*layout[0] self.tile = [] for layer in self.mode: - self.tile.append(("raw", (0,0)+self.size, offset, (layer,0,-1))) + self.tile.append( + ("raw", (0, 0)+self.size, offset, (layer, 0, -1))) offset = offset + pagesize elif compression == 1: - self.tile = [("sgi_rle", (0,0)+self.size, 512, (self.mode, 0, -1))] + raise ValueError("SGI RLE encoding not supported") # # registry @@ -85,5 +86,6 @@ Image.register_open("SGI", SgiImageFile, _accept) Image.register_extension("SGI", ".bw") Image.register_extension("SGI", ".rgb") Image.register_extension("SGI", ".rgba") +Image.register_extension("SGI", ".sgi") -Image.register_extension("SGI", ".sgi") # really? +# End of file diff --git a/PIL/SunImagePlugin.py b/PIL/SunImagePlugin.py index 0db02ad25..e0a7aa6ee 100644 --- a/PIL/SunImagePlugin.py +++ b/PIL/SunImagePlugin.py @@ -29,6 +29,7 @@ i32 = _binary.i32be def _accept(prefix): return i32(prefix) == 0x59a66a95 + ## # Image plugin for Sun raster files. @@ -70,9 +71,9 @@ class SunImageFile(ImageFile.ImageFile): stride = (((self.size[0] * depth + 7) // 8) + 3) & (~3) if compression == 1: - self.tile = [("raw", (0,0)+self.size, offset, (rawmode, stride))] + self.tile = [("raw", (0, 0)+self.size, offset, (rawmode, stride))] elif compression == 2: - self.tile = [("sun_rle", (0,0)+self.size, offset, rawmode)] + self.tile = [("sun_rle", (0, 0)+self.size, offset, rawmode)] # # registry diff --git a/PIL/TgaImagePlugin.py b/PIL/TgaImagePlugin.py index 55790db08..46eafe8d0 100644 --- a/PIL/TgaImagePlugin.py +++ b/PIL/TgaImagePlugin.py @@ -42,9 +42,6 @@ MODES = { } -def _accept(prefix): - return prefix[0:1] == b"\0" - ## # Image plugin for Targa files. @@ -58,7 +55,7 @@ class TgaImageFile(ImageFile.ImageFile): # process header s = self.fp.read(18) - id = i8(s[0]) + idlen = i8(s[0]) colormaptype = i8(s[1]) imagetype = i8(s[2]) @@ -70,7 +67,7 @@ class TgaImageFile(ImageFile.ImageFile): self.size = i16(s[12:]), i16(s[14:]) # validate header fields - if id != 0 or colormaptype not in (0, 1) or\ + if colormaptype not in (0, 1) or\ self.size[0] <= 0 or self.size[1] <= 0 or\ depth not in (1, 8, 16, 24, 32): raise SyntaxError("not a TGA file") @@ -79,7 +76,7 @@ class TgaImageFile(ImageFile.ImageFile): if imagetype in (3, 11): self.mode = "L" if depth == 1: - self.mode = "1" # ??? + self.mode = "1" # ??? elif imagetype in (1, 9): self.mode = "P" elif imagetype in (2, 10): @@ -103,22 +100,25 @@ class TgaImageFile(ImageFile.ImageFile): if imagetype & 8: self.info["compression"] = "tga_rle" + if idlen: + self.info["id_section"] = self.fp.read(idlen) + if colormaptype: # read palette start, size, mapdepth = i16(s[3:]), i16(s[5:]), i16(s[7:]) if mapdepth == 16: - self.palette = ImagePalette.raw("BGR;16", - b"\0"*2*start + self.fp.read(2*size)) + self.palette = ImagePalette.raw( + "BGR;16", b"\0"*2*start + self.fp.read(2*size)) elif mapdepth == 24: - self.palette = ImagePalette.raw("BGR", - b"\0"*3*start + self.fp.read(3*size)) + self.palette = ImagePalette.raw( + "BGR", b"\0"*3*start + self.fp.read(3*size)) elif mapdepth == 32: - self.palette = ImagePalette.raw("BGRA", - b"\0"*4*start + self.fp.read(4*size)) + self.palette = ImagePalette.raw( + "BGRA", b"\0"*4*start + self.fp.read(4*size)) # setup tile descriptor try: - rawmode = MODES[(imagetype&7, depth)] + rawmode = MODES[(imagetype & 7, depth)] if imagetype & 8: # compressed self.tile = [("tga_rle", (0, 0)+self.size, @@ -127,7 +127,7 @@ class TgaImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0)+self.size, self.fp.tell(), (rawmode, 0, orientation))] except KeyError: - pass # cannot decode + pass # cannot decode # # -------------------------------------------------------------------- @@ -145,6 +145,7 @@ SAVE = { "RGBA": ("BGRA", 32, 0, 2), } + def _save(im, fp, filename, check=0): try: @@ -185,13 +186,14 @@ def _save(im, fp, filename, check=0): if colormaptype: fp.write(im.im.getpalette("RGB", "BGR")) - ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, 0, orientation))]) + ImageFile._save( + im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))]) # # -------------------------------------------------------------------- # Registry -Image.register_open("TGA", TgaImageFile, _accept) +Image.register_open("TGA", TgaImageFile) Image.register_save("TGA", _save) Image.register_extension("TGA", ".tga") diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 2e49931f7..50648288e 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -49,17 +49,18 @@ from PIL import _binary from PIL._util import isStringType import warnings -import array, sys +import array +import sys import collections import itertools import os # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False -WRITE_LIBTIFF= False +WRITE_LIBTIFF = False -II = b"II" # little-endian (intel-style) -MM = b"MM" # big-endian (motorola-style) +II = b"II" # little-endian (Intel style) +MM = b"MM" # big-endian (Motorola style) i8 = _binary.i8 o8 = _binary.o8 @@ -109,8 +110,8 @@ EXTRASAMPLES = 338 SAMPLEFORMAT = 339 JPEGTABLES = 347 COPYRIGHT = 33432 -IPTC_NAA_CHUNK = 33723 # newsphoto properties -PHOTOSHOP_CHUNK = 34377 # photoshop properties +IPTC_NAA_CHUNK = 33723 # newsphoto properties +PHOTOSHOP_CHUNK = 34377 # photoshop properties ICCPROFILE = 34675 EXIFIFD = 34665 XMP = 700 @@ -126,10 +127,10 @@ COMPRESSION_INFO = { 3: "group3", 4: "group4", 5: "tiff_lzw", - 6: "tiff_jpeg", # obsolete + 6: "tiff_jpeg", # obsolete 7: "jpeg", 8: "tiff_adobe_deflate", - 32771: "tiff_raw_16", # 16-bit padding + 32771: "tiff_raw_16", # 16-bit padding 32773: "packbits", 32809: "tiff_thunderscan", 32946: "tiff_deflate", @@ -137,7 +138,7 @@ COMPRESSION_INFO = { 34677: "tiff_sgilog24", } -COMPRESSION_INFO_REV = dict([(v,k) for (k,v) in COMPRESSION_INFO.items()]) +COMPRESSION_INFO_REV = dict([(v, k) for (k, v) in COMPRESSION_INFO.items()]) OPEN_INFO = { # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, @@ -150,7 +151,7 @@ OPEN_INFO = { (II, 1, 1, 1, (1,), ()): ("1", "1"), (II, 1, 1, 2, (1,), ()): ("1", "1;R"), (II, 1, 1, 1, (8,), ()): ("L", "L"), - (II, 1, 1, 1, (8,8), (2,)): ("LA", "LA"), + (II, 1, 1, 1, (8, 8), (2,)): ("LA", "LA"), (II, 1, 1, 2, (8,), ()): ("L", "L;R"), (II, 1, 1, 1, (12,), ()): ("I;16", "I;12"), (II, 1, 1, 1, (16,), ()): ("I;16", "I;16"), @@ -158,13 +159,13 @@ OPEN_INFO = { (II, 1, 1, 1, (32,), ()): ("I", "I;32N"), (II, 1, 2, 1, (32,), ()): ("I", "I;32S"), (II, 1, 3, 1, (32,), ()): ("F", "F;32F"), - (II, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"), - (II, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"), - (II, 2, 1, 1, (8,8,8,8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (II, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"), - (II, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"), - (II, 2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"), - (II, 2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10 + (II, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"), + (II, 2, 1, 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (II, 2, 1, 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (II, 2, 1, 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (II, 2, 1, 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (II, 2, 1, 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, 1, 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (II, 3, 1, 1, (1,), ()): ("P", "P;1"), (II, 3, 1, 2, (1,), ()): ("P", "P;1R"), (II, 3, 1, 1, (2,), ()): ("P", "P;2"), @@ -172,11 +173,11 @@ OPEN_INFO = { (II, 3, 1, 1, (4,), ()): ("P", "P;4"), (II, 3, 1, 2, (4,), ()): ("P", "P;4R"), (II, 3, 1, 1, (8,), ()): ("P", "P"), - (II, 3, 1, 1, (8,8), (2,)): ("PA", "PA"), + (II, 3, 1, 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, 1, 2, (8,), ()): ("P", "P;R"), - (II, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"), - (II, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"), - (II, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"), + (II, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (II, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + (II, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"), (MM, 0, 1, 1, (1,), ()): ("1", "1;I"), (MM, 0, 1, 2, (1,), ()): ("1", "1;IR"), @@ -185,18 +186,18 @@ OPEN_INFO = { (MM, 1, 1, 1, (1,), ()): ("1", "1"), (MM, 1, 1, 2, (1,), ()): ("1", "1;R"), (MM, 1, 1, 1, (8,), ()): ("L", "L"), - (MM, 1, 1, 1, (8,8), (2,)): ("LA", "LA"), + (MM, 1, 1, 1, (8, 8), (2,)): ("LA", "LA"), (MM, 1, 1, 2, (8,), ()): ("L", "L;R"), (MM, 1, 1, 1, (16,), ()): ("I;16B", "I;16B"), (MM, 1, 2, 1, (16,), ()): ("I;16BS", "I;16BS"), (MM, 1, 2, 1, (32,), ()): ("I;32BS", "I;32BS"), (MM, 1, 3, 1, (32,), ()): ("F", "F;32BF"), - (MM, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"), - (MM, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"), - (MM, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"), - (MM, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"), - (MM, 2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"), - (MM, 2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10 + (MM, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"), + (MM, 2, 1, 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (MM, 2, 1, 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (MM, 2, 1, 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (MM, 2, 1, 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (MM, 2, 1, 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 3, 1, 1, (1,), ()): ("P", "P;1"), (MM, 3, 1, 2, (1,), ()): ("P", "P;1R"), (MM, 3, 1, 1, (2,), ()): ("P", "P;2"), @@ -204,19 +205,21 @@ OPEN_INFO = { (MM, 3, 1, 1, (4,), ()): ("P", "P;4"), (MM, 3, 1, 2, (4,), ()): ("P", "P;4R"), (MM, 3, 1, 1, (8,), ()): ("P", "P"), - (MM, 3, 1, 1, (8,8), (2,)): ("PA", "PA"), + (MM, 3, 1, 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, 1, 2, (8,), ()): ("P", "P;R"), - (MM, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"), - (MM, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"), - (MM, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"), + (MM, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (MM, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + (MM, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"), } PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"] + def _accept(prefix): return prefix[:4] in PREFIXES + ## # Wrapper for TIFF IFDs. @@ -276,8 +279,9 @@ class ImageFileDirectory(collections.MutableMapping): #: For a complete dictionary, use the as_dict method. self.tags = {} self.tagdata = {} - self.tagtype = {} # added 2008-06-05 by Florian Hoech + self.tagtype = {} # added 2008-06-05 by Florian Hoech self.next = None + self.offset = None def __str__(self): return str(self.as_dict()) @@ -287,7 +291,9 @@ class ImageFileDirectory(collections.MutableMapping): return dict(self.items()) def named(self): - """Returns the complete tag dictionary, with named tags where posible.""" + """ + Returns the complete tag dictionary, with named tags where posible. + """ from PIL import TiffTags result = {} for tag_code, value in self.items(): @@ -295,7 +301,6 @@ class ImageFileDirectory(collections.MutableMapping): result[tag_name] = value return result - # dictionary API def __len__(self): @@ -305,7 +310,7 @@ class ImageFileDirectory(collections.MutableMapping): try: return self.tags[tag] except KeyError: - data = self.tagdata[tag] # unpack on the fly + data = self.tagdata[tag] # unpack on the fly type = self.tagtype[tag] size, handler = self.load_dispatch[type] self.tags[tag] = data = handler(self, data) @@ -319,7 +324,7 @@ class ImageFileDirectory(collections.MutableMapping): if tag == SAMPLEFORMAT: # work around broken (?) matrox library # (from Ted Wright, via Bob Klimek) - raise KeyError # use default + raise KeyError # use default raise ValueError("not a scalar") return value[0] except KeyError: @@ -411,6 +416,7 @@ class ImageFileDirectory(collections.MutableMapping): # load tag dictionary self.reset() + self.offset = fp.tell() i16 = self.i16 i32 = self.i32 @@ -433,7 +439,7 @@ class ImageFileDirectory(collections.MutableMapping): except KeyError: if Image.DEBUG: print("- unsupported type", typ) - continue # ignore unsupported type + continue # ignore unsupported type size, handler = dispatch @@ -442,21 +448,28 @@ class ImageFileDirectory(collections.MutableMapping): # Get and expand tag value if size > 4: here = fp.tell() + if Image.DEBUG: + print ("Tag Location: %s" %here) fp.seek(i32(ifd, 8)) + if Image.DEBUG: + print ("Data Location: %s" %fp.tell()) data = ImageFile._safe_read(fp, size) fp.seek(here) else: data = ifd[8:8+size] if len(data) != size: - warnings.warn("Possibly corrupt EXIF data. Expecting to read %d bytes but only got %d. Skipping tag %s" % (size, len(data), tag)) + warnings.warn("Possibly corrupt EXIF data. " + "Expecting to read %d bytes but only got %d. " + "Skipping tag %s" % (size, len(data), tag)) continue self.tagdata[tag] = data self.tagtype[tag] = typ if Image.DEBUG: - if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, ICCPROFILE, XMP): + if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, + ICCPROFILE, XMP): print("- value: " % size) else: print("- value:", self[tag]) @@ -518,8 +531,8 @@ class ImageFileDirectory(collections.MutableMapping): # integer data if tag == STRIPOFFSETS: stripoffsets = len(directory) - typ = 4 # to avoid catch-22 - elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ==5: + typ = 4 # to avoid catch-22 + elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ == 5: # identify rational data fields typ = 5 if isinstance(value[0], tuple): @@ -541,7 +554,8 @@ class ImageFileDirectory(collections.MutableMapping): typname = TiffTags.TYPES.get(typ, "unknown") print("save: %s (%d)" % (tagname, tag), end=' ') print("- type: %s (%d)" % (typname, typ), end=' ') - if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, ICCPROFILE, XMP): + if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, + ICCPROFILE, XMP): size = len(data) print("- value: " % size) else: @@ -576,7 +590,7 @@ class ImageFileDirectory(collections.MutableMapping): fp.write(o16(tag) + o16(typ) + o32(count) + value) # -- overwrite here for multi-page -- - fp.write(b"\0\0\0\0") # end of directory + fp.write(b"\0\0\0\0") # end of directory # pass 3: write auxiliary data to file for tag, typ, count, value, data in directory: @@ -586,6 +600,7 @@ class ImageFileDirectory(collections.MutableMapping): return offset + ## # Image plugin for TIFF files. @@ -616,23 +631,25 @@ class TiffImageFile(ImageFile.ImageFile): print ("- __first:", self.__first) print ("- ifh: ", ifh) - # and load the first frame + # and load the first frame self._seek(0) def seek(self, frame): "Select a given frame as current image" - if frame < 0: frame = 0 self._seek(frame) + # Create a new core image object on second and + # subsequent frames in the image. Image may be + # different size/mode. + Image._decompression_bomb_check(self.size) + self.im = Image.core.new(self.mode, self.size) def tell(self): "Return the current frame number" - return self._tell() def _seek(self, frame): - self.fp = self.__fp if frame < self.__frame: # rewind file @@ -641,14 +658,21 @@ class TiffImageFile(ImageFile.ImageFile): while self.__frame < frame: if not self.__next: raise EOFError("no more images in TIFF file") + if Image.DEBUG: + print("Seeking to frame %s, on frame %s, __next %s, location: %s"% + (frame, self.__frame, self.__next, self.fp.tell())) + # reset python3 buffered io handle in case fp + # was passed to libtiff, invalidating the buffer + self.fp.tell() self.fp.seek(self.__next) + if Image.DEBUG: + print("Loading tags, location: %s"%self.fp.tell()) self.tag.load(self.fp) self.__next = self.tag.next self.__frame += 1 self._setup() - + def _tell(self): - return self.__frame def _decoder(self, rawmode, layer, tile=None): @@ -694,9 +718,12 @@ class TiffImageFile(ImageFile.ImageFile): if not len(self.tile) == 1: raise IOError("Not exactly one tile") - # (self._compression, (extents tuple), 0, (rawmode, self._compression, fp)) + # (self._compression, (extents tuple), + # 0, (rawmode, self._compression, fp)) ignored, extents, ignored_2, args = self.tile[0] - decoder = Image._getdecoder(self.mode, 'libtiff', args, self.decoderconfig) + args = args + (self.ifd.offset,) + decoder = Image._getdecoder(self.mode, 'libtiff', args, + self.decoderconfig) try: decoder.setimage(self.im, extents) except ValueError: @@ -706,35 +733,36 @@ class TiffImageFile(ImageFile.ImageFile): # We've got a stringio like thing passed in. Yay for all in memory. # The decoder needs the entire file in one shot, so there's not # a lot we can do here other than give it the entire file. - # unless we could do something like get the address of the underlying - # string for stringio. + # unless we could do something like get the address of the + # underlying string for stringio. # # Rearranging for supporting byteio items, since they have a fileno - # that returns an IOError if there's no underlying fp. Easier to deal - # with here by reordering. + # that returns an IOError if there's no underlying fp. Easier to + # dea. with here by reordering. if Image.DEBUG: print ("have getvalue. just sending in a string from getvalue") - n,err = decoder.decode(self.fp.getvalue()) + n, err = decoder.decode(self.fp.getvalue()) elif hasattr(self.fp, "fileno"): # we've got a actual file on disk, pass in the fp. if Image.DEBUG: print ("have fileno, calling fileno version of the decoder.") self.fp.seek(0) - n,err = decoder.decode(b"fpfp") # 4 bytes, otherwise the trace might error out + # 4 bytes, otherwise the trace might error out + n, err = decoder.decode(b"fpfp") else: # we have something else. if Image.DEBUG: print ("don't have fileno or getvalue. just reading") # UNDONE -- so much for that buffer size thing. - n,err = decoder.decode(self.fp.read()) - + n, err = decoder.decode(self.fp.read()) self.tile = [] self.readonly = 0 # libtiff closed the fp in a, we need to close self.fp, if possible if hasattr(self.fp, 'close'): - self.fp.close() - self.fp = None # might be shared + if not self.__next: + self.fp.close() + self.fp = None # might be shared if err < 0: raise IOError(err) @@ -810,11 +838,11 @@ class TiffImageFile(ImageFile.ImageFile): xres = xres[0] / (xres[1] or 1) yres = yres[0] / (yres[1] or 1) resunit = getscalar(RESOLUTION_UNIT, 1) - if resunit == 2: # dots per inch + if resunit == 2: # dots per inch self.info["dpi"] = xres, yres - elif resunit == 3: # dots per centimeter. convert to dpi + elif resunit == 3: # dots per centimeter. convert to dpi self.info["dpi"] = xres * 2.54, yres * 2.54 - else: # No absolute unit of measurement + else: # No absolute unit of measurement self.info["resolution"] = xres, yres # build tile descriptors @@ -825,13 +853,16 @@ class TiffImageFile(ImageFile.ImageFile): offsets = self.tag[STRIPOFFSETS] h = getscalar(ROWSPERSTRIP, ysize) w = self.size[0] - if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", "group4", - "tiff_jpeg", "tiff_adobe_deflate", - "tiff_thunderscan", "tiff_deflate", - "tiff_sgilog", "tiff_sgilog24", + if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", + "group4", "tiff_jpeg", + "tiff_adobe_deflate", + "tiff_thunderscan", + "tiff_deflate", + "tiff_sgilog", + "tiff_sgilog24", "tiff_raw_16"]: - ## if Image.DEBUG: - ## print "Activating g4 compression for whole file" + # if Image.DEBUG: + # print "Activating g4 compression for whole file" # Decoder expects entire file as one tile. # There's a buffer size limit in load (64k) @@ -850,7 +881,8 @@ class TiffImageFile(ImageFile.ImageFile): # libtiff closes the file descriptor, so pass in a dup. try: - fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) + fp = hasattr(self.fp, "fileno") and \ + os.dup(self.fp.fileno()) except IOError: # io.BytesIO have a fileno, but returns an IOError if # it doesn't use a file descriptor. @@ -881,7 +913,7 @@ class TiffImageFile(ImageFile.ImageFile): # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds - a = (rawmode, self._compression, fp ) + a = (rawmode, self._compression, fp) self.tile.append( (self._compression, (0, 0, w, ysize), @@ -893,8 +925,8 @@ class TiffImageFile(ImageFile.ImageFile): a = self._decoder(rawmode, l, i) self.tile.append( (self._compression, - (0, min(y, ysize), w, min(y+h, ysize)), - offsets[i], a)) + (0, min(y, ysize), w, min(y+h, ysize)), + offsets[i], a)) if Image.DEBUG: print ("tiles: ", self.tile) y = y + h @@ -914,8 +946,8 @@ class TiffImageFile(ImageFile.ImageFile): # is not a multiple of the tile size... self.tile.append( (self._compression, - (x, y, x+w, y+h), - o, a)) + (x, y, x+w, y+h), + o, a)) x = x + w if x >= self.size[0]: x, y = 0, y + h @@ -937,25 +969,27 @@ class TiffImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Write TIFF files -# little endian is default except for image modes with explict big endian byte-order +# little endian is default except for image modes with +# explict big endian byte-order SAVE_INFO = { - # mode => rawmode, byteorder, photometrics, sampleformat, bitspersample, extra + # mode => rawmode, byteorder, photometrics, + # sampleformat, bitspersample, extra "1": ("1", II, 1, 1, (1,), None), "L": ("L", II, 1, 1, (8,), None), - "LA": ("LA", II, 1, 1, (8,8), 2), + "LA": ("LA", II, 1, 1, (8, 8), 2), "P": ("P", II, 3, 1, (8,), None), - "PA": ("PA", II, 3, 1, (8,8), 2), + "PA": ("PA", II, 3, 1, (8, 8), 2), "I": ("I;32S", II, 1, 2, (32,), None), "I;16": ("I;16", II, 1, 1, (16,), None), "I;16S": ("I;16S", II, 1, 2, (16,), None), "F": ("F;32F", II, 1, 3, (32,), None), - "RGB": ("RGB", II, 2, 1, (8,8,8), None), - "RGBX": ("RGBX", II, 2, 1, (8,8,8,8), 0), - "RGBA": ("RGBA", II, 2, 1, (8,8,8,8), 2), - "CMYK": ("CMYK", II, 5, 1, (8,8,8,8), None), - "YCbCr": ("YCbCr", II, 6, 1, (8,8,8), None), - "LAB": ("LAB", II, 8, 1, (8,8,8), None), + "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), + "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), + "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), + "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), + "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), + "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), "I;16B": ("I;16B", MM, 1, 1, (16,), None), @@ -963,6 +997,7 @@ SAVE_INFO = { "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), } + def _cvt_res(value): # convert value to TIFF rational number -- (numerator, denominator) if isinstance(value, collections.Sequence): @@ -973,6 +1008,7 @@ def _cvt_res(value): value = float(value) return (int(value * 65536), 65536) + def _save(im, fp, filename): try: @@ -982,7 +1018,8 @@ def _save(im, fp, filename): ifd = ImageFileDirectory(prefix) - compression = im.encoderinfo.get('compression',im.info.get('compression','raw')) + compression = im.encoderinfo.get('compression', im.info.get('compression', + 'raw')) libtiff = WRITE_LIBTIFF or compression != 'raw' @@ -999,17 +1036,16 @@ def _save(im, fp, filename): ifd[IMAGELENGTH] = im.size[1] # write any arbitrary tags passed in as an ImageFileDirectory - info = im.encoderinfo.get("tiffinfo",{}) + info = im.encoderinfo.get("tiffinfo", {}) if Image.DEBUG: - print ("Tiffinfo Keys: %s"% info.keys) + print("Tiffinfo Keys: %s" % info.keys) keys = list(info.keys()) for key in keys: ifd[key] = info.get(key) try: ifd.tagtype[key] = info.tagtype[key] except: - pass # might not be an IFD, Might not have populated type - + pass # might not be an IFD, Might not have populated type # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com @@ -1030,7 +1066,7 @@ def _save(im, fp, filename): ifd[IMAGEDESCRIPTION] = im.encoderinfo["description"] if "resolution" in im.encoderinfo: ifd[X_RESOLUTION] = ifd[Y_RESOLUTION] \ - = _cvt_res(im.encoderinfo["resolution"]) + = _cvt_res(im.encoderinfo["resolution"]) if "x resolution" in im.encoderinfo: ifd[X_RESOLUTION] = _cvt_res(im.encoderinfo["x resolution"]) if "y resolution" in im.encoderinfo: @@ -1077,8 +1113,9 @@ def _save(im, fp, filename): stride = len(bits) * ((im.size[0]*bits[0]+7)//8) ifd[ROWSPERSTRIP] = im.size[1] ifd[STRIPBYTECOUNTS] = stride * im.size[1] - ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer - ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression,1) # no compression by default + ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer + # no compression by default: + ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if libtiff: if Image.DEBUG: @@ -1089,23 +1126,27 @@ def _save(im, fp, filename): fp.seek(0) _fp = os.dup(fp.fileno()) - blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] # ICC Profile crashes. - atts={} + # ICC Profile crashes. + blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] + atts = {} # bits per sample is a single short in the tiff directory, not a list. atts[BITSPERSAMPLE] = bits[0] # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. - for k,v in itertools.chain(ifd.items(), getattr(im, 'ifd', {}).items()): + for k, v in itertools.chain(ifd.items(), + getattr(im, 'ifd', {}).items()): if k not in atts and k not in blocklist: if type(v[0]) == tuple and len(v) > 1: # A tuple of more than one rational tuples - # flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL + # flatten to floats, + # following tiffcp.c->cpTag->TIFF_RATIONAL atts[k] = [float(elt[0])/float(elt[1]) for elt in v] continue if type(v[0]) == tuple and len(v) == 1: # A tuple of one rational tuples - # flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL + # flatten to floats, + # following tiffcp.c->cpTag->TIFF_RATIONAL atts[k] = float(v[0][0])/float(v[0][1]) continue if type(v) == tuple and len(v) > 2: @@ -1115,7 +1156,8 @@ def _save(im, fp, filename): continue if type(v) == tuple and len(v) == 2: # one rational tuple - # flatten to float, following tiffcp.c->cpTag->TIFF_RATIONAL + # flatten to float, + # following tiffcp.c->cpTag->TIFF_RATIONAL atts[k] = float(v[0])/float(v[1]) continue if type(v) == tuple and len(v) == 1: @@ -1141,9 +1183,10 @@ def _save(im, fp, filename): a = (rawmode, compression, _fp, filename, atts) # print (im.mode, compression, a, im.encoderconfig) e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig) - e.setimage(im.im, (0,0)+im.size) + e.setimage(im.im, (0, 0)+im.size) while True: - l, s, d = e.encode(16*1024) # undone, change to self.decodermaxblock + # undone, change to self.decodermaxblock: + l, s, d = e.encode(16*1024) if not _fp: fp.write(d) if s: @@ -1155,13 +1198,12 @@ def _save(im, fp, filename): offset = ifd.save(fp) ImageFile._save(im, fp, [ - ("raw", (0,0)+im.size, offset, (rawmode, stride, 1)) + ("raw", (0, 0)+im.size, offset, (rawmode, stride, 1)) ]) - # -- helper for multi-page save -- if "_debug_multipage" in im.encoderinfo: - #just to access o32 and o16 (using correct byte order) + # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd # diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 92a4b5afc..d15aa7ebe 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -46,8 +46,8 @@ TAGS = { (262, 5): "CMYK", (262, 6): "YCbCr", (262, 8): "CieLAB", - (262, 32803): "CFA", # TIFF/EP, Adobe DNG - (262, 32892): "LinearRaw", # Adobe DNG + (262, 32803): "CFA", # TIFF/EP, Adobe DNG + (262, 32892): "LinearRaw", # Adobe DNG 263: "Thresholding", 264: "CellWidth", @@ -147,6 +147,100 @@ TAGS = { # ICC Profile 34675: "ICCProfile", + # Additional Exif Info + 33434: "ExposureTime", + 33437: "FNumber", + 34850: "ExposureProgram", + 34852: "SpectralSensitivity", + 34853: "GPSInfoIFD", + 34855: "ISOSpeedRatings", + 34856: "OECF", + 34864: "SensitivityType", + 34865: "StandardOutputSensitivity", + 34866: "RecommendedExposureIndex", + 34867: "ISOSpeed", + 34868: "ISOSpeedLatitudeyyy", + 34869: "ISOSpeedLatitudezzz", + 36864: "ExifVersion", + 36867: "DateTimeOriginal", + 36868: "DateTImeDigitized", + 37121: "ComponentsConfiguration", + 37122: "CompressedBitsPerPixel", + 37377: "ShutterSpeedValue", + 37378: "ApertureValue", + 37379: "BrightnessValue", + 37380: "ExposureBiasValue", + 37381: "MaxApertureValue", + 37382: "SubjectDistance", + 37383: "MeteringMode", + 37384: "LightSource", + 37385: "Flash", + 37386: "FocalLength", + 37396: "SubjectArea", + 37500: "MakerNote", + 37510: "UserComment", + 37520: "SubSec", + 37521: "SubSecTimeOriginal", + 37522: "SubsecTimeDigitized", + 40960: "FlashPixVersion", + 40961: "ColorSpace", + 40962: "PixelXDimension", + 40963: "PixelYDimension", + 40964: "RelatedSoundFile", + 40965: "InteroperabilityIFD", + 41483: "FlashEnergy", + 41484: "SpatialFrequencyResponse", + 41486: "FocalPlaneXResolution", + 41487: "FocalPlaneYResolution", + 41488: "FocalPlaneResolutionUnit", + 41492: "SubjectLocation", + 41493: "ExposureIndex", + 41495: "SensingMethod", + 41728: "FileSource", + 41729: "SceneType", + 41730: "CFAPattern", + 41985: "CustomRendered", + 41986: "ExposureMode", + 41987: "WhiteBalance", + 41988: "DigitalZoomRatio", + 41989: "FocalLengthIn35mmFilm", + 41990: "SceneCaptureType", + 41991: "GainControl", + 41992: "Contrast", + 41993: "Saturation", + 41994: "Sharpness", + 41995: "DeviceSettingDescription", + 41996: "SubjectDistanceRange", + 42016: "ImageUniqueID", + 42032: "CameraOwnerName", + 42033: "BodySerialNumber", + 42034: "LensSpecification", + 42035: "LensMake", + 42036: "LensModel", + 42037: "LensSerialNumber", + 42240: "Gamma", + + # MP Info + 45056: "MPFVersion", + 45057: "NumberOfImages", + 45058: "MPEntry", + 45059: "ImageUIDList", + 45060: "TotalFrames", + 45313: "MPIndividualNum", + 45569: "PanOrientation", + 45570: "PanOverlap_H", + 45571: "PanOverlap_V", + 45572: "BaseViewpointNum", + 45573: "ConvergenceAngle", + 45574: "BaselineLength", + 45575: "VerticalDivergence", + 45576: "AxisDistance_X", + 45577: "AxisDistance_Y", + 45578: "AxisDistance_Z", + 45579: "YawAngle", + 45580: "PitchAngle", + 45581: "RollAngle", + # Adobe DNG 50706: "DNGVersion", 50707: "DNGBackwardVersion", @@ -161,7 +255,6 @@ TAGS = { 50716: "BlackLevelDeltaV", 50717: "WhiteLevel", 50718: "DefaultScale", - 50741: "BestQualityScale", # FIXME! Dictionary contains duplicate keys 50741 50719: "DefaultCropOrigin", 50720: "DefaultCropSize", 50778: "CalibrationIlluminant1", @@ -185,11 +278,12 @@ TAGS = { 50737: "ChromaBlurRadius", 50738: "AntiAliasStrength", 50740: "DNGPrivateData", - 50741: "MakerNoteSafety", # FIXME! Dictionary contains duplicate keys 50741 + 50741: "MakerNoteSafety", + 50780: "BestQualityScale", - #ImageJ - 50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe - 50839: "ImageJMetaData", # private tag registered with Adobe + # ImageJ + 50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe + 50839: "ImageJMetaData", # private tag registered with Adobe } ## diff --git a/PIL/XpmImagePlugin.py b/PIL/XpmImagePlugin.py index 701a23b64..517580895 100644 --- a/PIL/XpmImagePlugin.py +++ b/PIL/XpmImagePlugin.py @@ -29,6 +29,7 @@ xpm_head = re.compile(b"\"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)") def _accept(prefix): return prefix[:9] == b"/* XPM */" + ## # Image plugin for X11 pixel maps. @@ -86,9 +87,9 @@ class XpmImageFile(ImageFile.ImageFile): elif rgb[0:1] == b"#": # FIXME: handle colour names (see ImagePalette.py) rgb = int(rgb[1:], 16) - palette[c] = o8((rgb >> 16) & 255) +\ - o8((rgb >> 8) & 255) +\ - o8(rgb & 255) + palette[c] = (o8((rgb >> 16) & 255) + + o8((rgb >> 8) & 255) + + o8(rgb & 255)) else: # unknown colour raise ValueError("cannot read this XPM file") diff --git a/PIL/__init__.py b/PIL/__init__.py index d446aa19b..7b4b8abfa 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -12,7 +12,7 @@ # ;-) VERSION = '1.1.7' # PIL version -PILLOW_VERSION = '2.5.0' # Pillow +PILLOW_VERSION = '2.5.3' # Pillow _plugins = ['BmpImagePlugin', 'BufrStubImagePlugin', @@ -36,6 +36,7 @@ _plugins = ['BmpImagePlugin', 'McIdasImagePlugin', 'MicImagePlugin', 'MpegImagePlugin', + 'MpoImagePlugin', 'MspImagePlugin', 'PalmImagePlugin', 'PcdImagePlugin', diff --git a/PIL/_util.py b/PIL/_util.py index eb5c2c242..51c6f6887 100644 --- a/PIL/_util.py +++ b/PIL/_util.py @@ -3,20 +3,25 @@ import os if bytes is str: def isStringType(t): return isinstance(t, basestring) + def isPath(f): return isinstance(f, basestring) else: def isStringType(t): return isinstance(t, str) + def isPath(f): return isinstance(f, (bytes, str)) + # Checks if an object is a string, and that it points to a directory. def isDirectory(f): return isPath(f) and os.path.isdir(f) + class deferred_error(object): def __init__(self, ex): self.ex = ex + def __getattr__(self, elt): raise self.ex diff --git a/README.rst b/README.rst index 224f98f03..d58666bc6 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Pillow *Python Imaging Library (Fork)* -Pillow is the "friendly" PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. For more information, please `read the documentation `_. +Pillow is the "friendly" PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. For more information, please `read the documentation `_, `check the changelog `_ and `find out how to contribute `_. .. image:: https://travis-ci.org/python-pillow/Pillow.svg?branch=master :target: https://travis-ci.org/python-pillow/Pillow @@ -20,3 +20,6 @@ Pillow is the "friendly" PIL fork by Alex Clark and Contributors. PIL is the Pyt .. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.png?branch=master :target: https://coveralls.io/r/python-pillow/Pillow?branch=master +.. image:: https://landscape.io/github/python-pillow/Pillow/master/landscape.png + :target: https://landscape.io/github/python-pillow/Pillow/master + :alt: Code Health diff --git a/Scripts/createfontdatachunk.py b/Scripts/createfontdatachunk.py new file mode 100644 index 000000000..0c860701a --- /dev/null +++ b/Scripts/createfontdatachunk.py @@ -0,0 +1,16 @@ +import base64 +import os +import sys + +if __name__ == "__main__": + # create font data chunk for embedding + font = "Tests/images/courB08" + print(" f._load_pilfont_data(") + print(" # %s" % os.path.basename(font)) + print(" BytesIO(base64.decodestring(b'''") + base64.encode(open(font + ".pil", "rb"), sys.stdout) + print("''')), Image.open(BytesIO(base64.decodestring(b'''") + base64.encode(open(font + ".pbm", "rb"), sys.stdout) + print("'''))))") + +# End of file diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py new file mode 100644 index 000000000..822d524fd --- /dev/null +++ b/Tests/32bit_segfault_check.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +from PIL import Image +import sys + + +if sys.maxsize < 2**32: + im = Image.new('L', (999999, 999999), 0) + + diff --git a/Tests/README.rst b/Tests/README.rst index 1eaa4f3a2..6fd770f55 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -3,22 +3,46 @@ 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``. +Depedencies +----------- + +Install:: + + pip install coverage nose + +If you're using Python 2.6, there's one additional dependency:: + + pip install unittest2 + 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 +**If Pillow has been built in-place** To run an individual test:: python Tests/test_image.py + +Run all the tests from the root of the Pillow source distribution:: + + nosetests -vx Tests/test_*.py + +Or with coverage:: + + coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py + coverage report + coverage html + open htmlcov/index.html + +**If Pillow has been installed** + +To run an individual test:: + + ./test-installed.py Tests/test_image.py + +Run all the tests from the root of the Pillow source distribution:: + + ./test-installed.py + + + diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py new file mode 100644 index 000000000..ce6338a71 --- /dev/null +++ b/Tests/check_icns_dos.py @@ -0,0 +1,10 @@ +# Tests potential DOS of IcnsImagePlugin with 0 length block. +# Run from anywhere that PIL is importable. + +from PIL import Image +from io import BytesIO + +if bytes is str: + Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00'))) +else: + Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00', 'latin-1'))) diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py new file mode 100644 index 000000000..68f065bbc --- /dev/null +++ b/Tests/check_j2k_dos.py @@ -0,0 +1,11 @@ +# Tests potential DOS of Jpeg2kImagePlugin with 0 length block. +# Run from anywhere that PIL is importable. + +from PIL import Image +from io import BytesIO + +if bytes is str: + Image.open(BytesIO(bytes('\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang'))) +else: + Image.open(BytesIO(bytes('\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang', 'latin-1'))) + diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py new file mode 100755 index 000000000..9dbb8c1f4 --- /dev/null +++ b/Tests/check_j2k_leaks.py @@ -0,0 +1,42 @@ +from helper import unittest, PillowTestCase +import sys +from PIL import Image +from io import BytesIO + +# Limits for testing the leak +mem_limit = 1024*1048576 +stack_size = 8*1048576 +iterations = int((mem_limit/stack_size)*2) +codecs = dir(Image.core) +test_file = "Tests/images/rgb_trns_ycbc.jp2" + + +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +class TestJpegLeaks(PillowTestCase): + def setUp(self): + if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: + self.skipTest('JPEG 2000 support not available') + + def test_leak_load(self): + from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for count in range(iterations): + with Image.open(test_file) as im: + im.load() + + def test_leak_save(self): + from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for count in range(iterations): + with Image.open(test_file) as im: + im.load() + test_output = BytesIO() + im.save(test_output, "JPEG2000") + test_output.seek(0) + output = test_output.read() + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/helper.py b/Tests/helper.py index 7f0aaa73c..637e77f9c 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -5,22 +5,23 @@ from __future__ import print_function import sys import tempfile import os -import glob if sys.version_info[:2] <= (2, 6): import unittest2 as unittest else: import unittest -def tearDownModule(): - #remove me later - pass class PillowTestCase(unittest.TestCase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) - self.currentResult = None # holds last result object passed to run method + # holds last result object passed to run method: + self.currentResult = None + + # Nicer output for --verbose + def __str__(self): + return self.__class__.__name__ + "." + self._testMethodName def run(self, result=None): self.currentResult = result # remember result for use later @@ -40,7 +41,7 @@ class PillowTestCase(unittest.TestCase): except OSError: pass # report? else: - print("=== orphaned temp file: %s" %path) + print("=== orphaned temp file: %s" % path) def assert_almost_equal(self, a, b, msg=None, eps=1e-6): self.assertLess( @@ -99,7 +100,7 @@ class PillowTestCase(unittest.TestCase): ave_diff = float(diff)/(a.size[0]*a.size[1]) self.assertGreaterEqual( epsilon, ave_diff, - msg or "average pixel value difference %.4f > epsilon %.4f" % ( + (msg or '') + " average pixel value difference %.4f > epsilon %.4f" % ( ave_diff, epsilon)) def assert_warning(self, warn_class, func): @@ -123,7 +124,8 @@ class PillowTestCase(unittest.TestCase): self.assertTrue(found) return result - def skipKnownBadTest(self, msg=None, platform=None, travis=None): + def skipKnownBadTest(self, msg=None, platform=None, + travis=None, interpreter=None): # Skip if platform/travis matches, and # PILLOW_RUN_KNOWN_BAD is not true in the environment. if bool(os.environ.get('PILLOW_RUN_KNOWN_BAD', False)): @@ -134,7 +136,9 @@ class PillowTestCase(unittest.TestCase): if platform is not None: skip = sys.platform.startswith(platform) if travis is not None: - skip = skip and (travis == bool(os.environ.get('TRAVIS',False))) + skip = skip and (travis == bool(os.environ.get('TRAVIS', False))) + if interpreter is not None: + skip = skip and (interpreter == 'pypy' and hasattr(sys, 'pypy_version_info')) if skip: self.skipTest(msg or "Known Bad Test") @@ -142,8 +146,8 @@ class PillowTestCase(unittest.TestCase): assert template[:5] in ("temp.", "temp_") (fd, path) = tempfile.mkstemp(template[4:], template[:4]) os.close(fd) - - self.addCleanup(self.delete_tempfile, path) + + self.addCleanup(self.delete_tempfile, path) return path def open_withImagemagick(self, f): @@ -155,8 +159,8 @@ class PillowTestCase(unittest.TestCase): from PIL import Image return Image.open(outfile) raise IOError() - - + + # helpers import sys @@ -176,6 +180,26 @@ def tostring(im, format, **options): return out.getvalue() +def hopper(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": + im = Image.open("Tests/images/hopper.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 lena(mode="RGB", cache={}): from PIL import Image im = None @@ -210,17 +234,21 @@ def command_succeeds(cmd): 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"]) + return (command_succeeds(["ppmquant", "--help"]) and + command_succeeds(["ppmtogif", "--help"])) + def imagemagick_available(): return command_succeeds(['convert', '-version']) - + # End of file diff --git a/Tests/images/deerstalker.cur b/Tests/images/deerstalker.cur new file mode 100644 index 000000000..16e4bfa52 Binary files /dev/null and b/Tests/images/deerstalker.cur differ diff --git a/Tests/images/default_font.png b/Tests/images/default_font.png new file mode 100644 index 000000000..bb4862b99 Binary files /dev/null and b/Tests/images/default_font.png differ diff --git a/Tests/images/dispose_bgnd.gif b/Tests/images/dispose_bgnd.gif new file mode 100644 index 000000000..d68504010 Binary files /dev/null and b/Tests/images/dispose_bgnd.gif differ diff --git a/Tests/images/dispose_none.gif b/Tests/images/dispose_none.gif new file mode 100644 index 000000000..beeb13fd9 Binary files /dev/null and b/Tests/images/dispose_none.gif differ diff --git a/Tests/images/dispose_prev.gif b/Tests/images/dispose_prev.gif new file mode 100644 index 000000000..0de7760e9 Binary files /dev/null and b/Tests/images/dispose_prev.gif differ diff --git a/Tests/images/effect_mandelbrot.png b/Tests/images/effect_mandelbrot.png new file mode 100644 index 000000000..d8167e721 Binary files /dev/null and b/Tests/images/effect_mandelbrot.png differ diff --git a/Tests/images/effect_spread.png b/Tests/images/effect_spread.png new file mode 100644 index 000000000..01016355a Binary files /dev/null and b/Tests/images/effect_spread.png differ diff --git a/Tests/images/frozenpond.mpo b/Tests/images/frozenpond.mpo new file mode 100644 index 000000000..2dfe44ac1 Binary files /dev/null and b/Tests/images/frozenpond.mpo differ diff --git a/Tests/images/gimp_gradient.ggr b/Tests/images/gimp_gradient.ggr new file mode 100644 index 000000000..4644e5537 --- /dev/null +++ b/Tests/images/gimp_gradient.ggr @@ -0,0 +1,6 @@ +GIMP Gradient +4 +0.000000 0.351923 0.534893 1.000000 1.000000 1.000000 0.910000 0.730303 0.510606 1.000000 0.480000 1 0 +0.501504 0.611002 0.637730 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1 0 +0.931891 0.951264 1.000000 0.531255 0.316078 1.031173 1.000000 0.000000 0.000000 0.000000 1.000000 0 0 +0.810551 0.881217 0.921883 0.717576 0.441331 0.081217 1.000000 0.751576 0.410331 0.081217 1.000000 0 0 \ No newline at end of file diff --git a/Tests/images/gimp_gradient_with_name.ggr b/Tests/images/gimp_gradient_with_name.ggr new file mode 100644 index 000000000..ab1c1d734 --- /dev/null +++ b/Tests/images/gimp_gradient_with_name.ggr @@ -0,0 +1,7 @@ +GIMP Gradient +Name: A GIMP 1.3 gradient file +4 +0.000000 0.351923 0.534893 1.000000 1.000000 1.000000 0.910000 0.730303 0.510606 1.000000 0.480000 1 0 +0.501504 0.611002 0.637730 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1 0 +0.931891 0.951264 1.000000 0.531255 0.316078 1.031173 1.000000 0.000000 0.000000 0.000000 1.000000 0 0 +0.810551 0.881217 0.921883 0.717576 0.441331 0.081217 1.000000 0.751576 0.410331 0.081217 1.000000 0 0 \ No newline at end of file diff --git a/Tests/images/hopper.bw b/Tests/images/hopper.bw new file mode 100644 index 000000000..c9dabf64a Binary files /dev/null and b/Tests/images/hopper.bw differ diff --git a/Tests/images/hopper.dcx b/Tests/images/hopper.dcx new file mode 100644 index 000000000..ba6bd062e Binary files /dev/null and b/Tests/images/hopper.dcx differ diff --git a/Tests/images/hopper.gif b/Tests/images/hopper.gif new file mode 100644 index 000000000..2e7f5ade8 Binary files /dev/null and b/Tests/images/hopper.gif differ diff --git a/Tests/images/hopper.ico b/Tests/images/hopper.ico new file mode 100644 index 000000000..0c6fd8c70 Binary files /dev/null and b/Tests/images/hopper.ico differ diff --git a/Tests/images/hopper.jpg b/Tests/images/hopper.jpg new file mode 100644 index 000000000..82ab5b967 Binary files /dev/null and b/Tests/images/hopper.jpg differ diff --git a/Tests/images/hopper.png b/Tests/images/hopper.png new file mode 100644 index 000000000..60ac671a2 Binary files /dev/null and b/Tests/images/hopper.png differ diff --git a/Tests/images/hopper.ppm b/Tests/images/hopper.ppm new file mode 100644 index 000000000..346e42392 Binary files /dev/null and b/Tests/images/hopper.ppm differ diff --git a/Tests/images/hopper.ras b/Tests/images/hopper.ras new file mode 100644 index 000000000..70f3c80d0 Binary files /dev/null and b/Tests/images/hopper.ras differ diff --git a/Tests/images/hopper.rgb b/Tests/images/hopper.rgb new file mode 100644 index 000000000..a72fc5b15 Binary files /dev/null and b/Tests/images/hopper.rgb differ diff --git a/Tests/images/hopper.spider b/Tests/images/hopper.spider new file mode 100644 index 000000000..70e31b07c Binary files /dev/null and b/Tests/images/hopper.spider differ diff --git a/Tests/images/hopper.tar b/Tests/images/hopper.tar new file mode 100644 index 000000000..593429c9a Binary files /dev/null and b/Tests/images/hopper.tar differ diff --git a/Tests/images/iptc.jpg b/Tests/images/iptc.jpg new file mode 100644 index 000000000..b4d49caa1 Binary files /dev/null and b/Tests/images/iptc.jpg differ diff --git a/Tests/images/iss634.gif b/Tests/images/iss634.gif new file mode 100644 index 000000000..ba4e4566f Binary files /dev/null and b/Tests/images/iss634.gif differ diff --git a/Tests/images/lena.gif b/Tests/images/lena.gif deleted file mode 100644 index 64a9b93c8..000000000 Binary files a/Tests/images/lena.gif and /dev/null differ diff --git a/Tests/images/lena.ico b/Tests/images/lena.ico deleted file mode 100644 index 6fadbe29e..000000000 Binary files a/Tests/images/lena.ico and /dev/null differ diff --git a/Tests/images/lena.png b/Tests/images/lena.png deleted file mode 100644 index 192875931..000000000 Binary files a/Tests/images/lena.png and /dev/null differ diff --git a/Tests/images/lena.spider b/Tests/images/lena.spider deleted file mode 100644 index df963c1c3..000000000 Binary files a/Tests/images/lena.spider and /dev/null differ diff --git a/Tests/images/lena.tar b/Tests/images/lena.tar deleted file mode 100644 index 3afb1f542..000000000 Binary files a/Tests/images/lena.tar and /dev/null differ diff --git a/Tests/images/lena_gray.jpg b/Tests/images/lena_gray.jpg new file mode 100644 index 000000000..72e9a4a77 Binary files /dev/null and b/Tests/images/lena_gray.jpg differ diff --git a/Tests/images/multipage-lastframe.tif b/Tests/images/multipage-lastframe.tif new file mode 100644 index 000000000..aeba534e2 Binary files /dev/null and b/Tests/images/multipage-lastframe.tif differ diff --git a/Tests/images/multipage.tiff b/Tests/images/multipage.tiff new file mode 100644 index 000000000..7a7f5f89a Binary files /dev/null and b/Tests/images/multipage.tiff differ diff --git a/Tests/images/rectangle_surrounding_text.png b/Tests/images/rectangle_surrounding_text.png new file mode 100644 index 000000000..2b75a5e9c Binary files /dev/null and b/Tests/images/rectangle_surrounding_text.png differ diff --git a/Tests/images/sugarshack.mpo b/Tests/images/sugarshack.mpo new file mode 100644 index 000000000..85346fcc9 Binary files /dev/null and b/Tests/images/sugarshack.mpo differ diff --git a/Tests/images/tga_id_field.tga b/Tests/images/tga_id_field.tga new file mode 100644 index 000000000..a3d666848 Binary files /dev/null and b/Tests/images/tga_id_field.tga differ diff --git a/Tests/images/transparent.sgi b/Tests/images/transparent.sgi new file mode 100644 index 000000000..482572df5 Binary files /dev/null and b/Tests/images/transparent.sgi differ diff --git a/Tests/make_hash.py b/Tests/make_hash.py index 32196e9f8..88bb2994b 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -9,6 +9,7 @@ modes = [ "RGB", "RGBA", "RGBa", "RGBX", "CMYK", "YCbCr", + "LAB", "HSV", ] diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index 1ad76cc50..22e582ec3 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase import PIL import PIL.Image diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index c8d93983b..b45ea76f6 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image import os diff --git a/Tests/test_cffi.py b/Tests/test_cffi.py index 8ff4e817f..b9f99976d 100644 --- a/Tests/test_cffi.py +++ b/Tests/test_cffi.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena try: import cffi diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 4c09bd9e3..0803732ce 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 2870aba04..e04f3642c 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image import io diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py new file mode 100644 index 000000000..54bfe84fe --- /dev/null +++ b/Tests/test_file_cur.py @@ -0,0 +1,27 @@ +from helper import unittest, PillowTestCase + +from PIL import Image, CurImagePlugin + + +class TestFileCur(PillowTestCase): + + def test_sanity(self): + # Arrange + test_file = "Tests/images/deerstalker.cur" + + # Act + im = Image.open(test_file) + + # Assert + self.assertEqual(im.size, (32, 32)) + self.assertIsInstance(im, CurImagePlugin.CurImageFile) + # Check some pixel colors to ensure image is loaded properly + self.assertEqual(im.getpixel((10, 1)), (0, 0, 0)) + self.assertEqual(im.getpixel((11, 1)), (253, 254, 254)) + self.assertEqual(im.getpixel((16, 16)), (84, 87, 86)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py new file mode 100644 index 000000000..2acf7b459 --- /dev/null +++ b/Tests/test_file_dcx.py @@ -0,0 +1,45 @@ +from helper import unittest, PillowTestCase, hopper + +from PIL import Image, DcxImagePlugin + +# Created with ImageMagick: convert hopper.ppm hopper.dcx +TEST_FILE = "Tests/images/hopper.dcx" + + +class TestFileDcx(PillowTestCase): + + def test_sanity(self): + # Arrange + + # Act + im = Image.open(TEST_FILE) + + # Assert + self.assertEqual(im.size, (128, 128)) + self.assertIsInstance(im, DcxImagePlugin.DcxImageFile) + orig = hopper() + self.assert_image_equal(im, orig) + + def test_tell(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act + frame = im.tell() + + # Assert + self.assertEqual(frame, 0) + + def test_seek_too_far(self): + # Arrange + im = Image.open(TEST_FILE) + frame = 999 # too big on purpose + + # Act / Assert + self.assertRaises(EOFError, lambda: im.seek(frame)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 6a1a1b5e2..a17ce274f 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image, EpsImagePlugin import io @@ -63,6 +63,17 @@ class TestFileEps(PillowTestCase): with io.open(self.tempfile('temp_iobase.eps'), 'wb') as fh: image1.save(fh, 'EPS') + def test_bytesio_object(self): + with open(file1, 'rb') as f: + img_bytes = io.BytesIO(f.read()) + + img = Image.open(img_bytes) + img.load() + + image1_scale1_compare = Image.open(file1_compare).convert("RGB") + image1_scale1_compare.load() + self.assert_image_similar(img, image1_scale1_compare, 5) + def test_render_scale1(self): # We need png support for these render test codecs = dir(Image.core) @@ -137,6 +148,83 @@ class TestFileEps(PillowTestCase): # open image with binary preview Image.open(file3) + def _test_readline(self,t, ending): + ending = "Failure with line ending: %s" %("".join("%s" %ord(s) for s in ending)) + self.assertEqual(t.readline().strip('\r\n'), 'something', ending) + self.assertEqual(t.readline().strip('\r\n'), 'else', ending) + self.assertEqual(t.readline().strip('\r\n'), 'baz', ending) + self.assertEqual(t.readline().strip('\r\n'), 'bif', ending) + + def _test_readline_stringio(self, test_string, ending): + # check all the freaking line endings possible + try: + import StringIO + except: + # don't skip, it skips everything in the parent test + return + t = StringIO.StringIO(test_string) + self._test_readline(t, ending) + + def _test_readline_io(self, test_string, ending): + import io + if str is bytes: + t = io.StringIO(unicode(test_string)) + else: + t = io.StringIO(test_string) + self._test_readline(t, ending) + + def _test_readline_file_universal(self, test_string, ending): + f = self.tempfile('temp.txt') + with open(f,'wb') as w: + if str is bytes: + w.write(test_string) + else: + w.write(test_string.encode('UTF-8')) + + with open(f,'rU') as t: + self._test_readline(t, ending) + + def _test_readline_file_psfile(self, test_string, ending): + f = self.tempfile('temp.txt') + with open(f,'wb') as w: + if str is bytes: + w.write(test_string) + else: + w.write(test_string.encode('UTF-8')) + + with open(f,'rb') as r: + t = EpsImagePlugin.PSFile(r) + self._test_readline(t, ending) + + def test_readline(self): + # check all the freaking line endings possible from the spec + #test_string = u'something\r\nelse\n\rbaz\rbif\n' + line_endings = ['\r\n', '\n'] + not_working_endings = ['\n\r', '\r'] + strings = ['something', 'else', 'baz', 'bif'] + + for ending in line_endings: + s = ending.join(strings) + # Native python versions will pass these endings. + #self._test_readline_stringio(s, ending) + #self._test_readline_io(s, ending) + #self._test_readline_file_universal(s, ending) + + self._test_readline_file_psfile(s, ending) + + for ending in not_working_endings: + # these only work with the PSFile, while they're in spec, + # they're not likely to be used + s = ending.join(strings) + + # Native python versions may fail on these endings. + #self._test_readline_stringio(s, ending) + #self._test_readline_io(s, ending) + #self._test_readline_file_universal(s, ending) + + self._test_readline_file_psfile(s, ending) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index a98a80b78..0c1d6e36a 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index e31779df0..72c9d9db4 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena, netpbm_available +from helper import unittest, PillowTestCase, hopper, netpbm_available from PIL import Image from PIL import GifImagePlugin @@ -6,8 +6,9 @@ from PIL import GifImagePlugin codecs = dir(Image.core) # sample gif stream -file = "Tests/images/lena.gif" -with open(file, "rb") as f: +TEST_GIF = "Tests/images/hopper.gif" + +with open(TEST_GIF, "rb") as f: data = f.read() @@ -18,7 +19,7 @@ class TestFileGif(PillowTestCase): self.skipTest("gif support not available") # can this happen? def test_sanity(self): - im = Image.open(file) + im = Image.open(TEST_GIF) im.load() self.assertEqual(im.mode, "P") self.assertEqual(im.size, (128, 128)) @@ -35,9 +36,17 @@ class TestFileGif(PillowTestCase): self.assertEqual(test(0), 800) self.assertEqual(test(1), 38) + def test_optimize_full_l(self): + from io import BytesIO + + im = Image.frombytes("L", (16, 16), bytes(bytearray(range(256)))) + file = BytesIO() + im.save(file, "GIF", optimize=True) + self.assertEqual(im.mode, "L") + def test_roundtrip(self): out = self.tempfile('temp.gif') - im = lena() + im = hopper() im.save(out) reread = Image.open(out) @@ -46,17 +55,17 @@ class TestFileGif(PillowTestCase): def test_roundtrip2(self): # see https://github.com/python-pillow/Pillow/issues/403 out = self.tempfile('temp.gif') - im = Image.open('Tests/images/lena.gif') + im = Image.open(TEST_GIF) im2 = im.copy() im2.save(out) reread = Image.open(out) - self.assert_image_similar(reread.convert('RGB'), lena(), 50) + self.assert_image_similar(reread.convert('RGB'), hopper(), 50) def test_palette_handling(self): # see https://github.com/python-pillow/Pillow/issues/513 - im = Image.open('Tests/images/lena.gif') + im = Image.open(TEST_GIF) im = im.convert('RGB') im = im.resize((100, 100), Image.ANTIALIAS) @@ -92,7 +101,7 @@ class TestFileGif(PillowTestCase): @unittest.skipUnless(netpbm_available(), "netpbm not available") def test_save_netpbm_bmp_mode(self): - img = Image.open(file).convert("RGB") + img = Image.open(TEST_GIF).convert("RGB") tempfile = self.tempfile("temp.gif") GifImagePlugin._save_netpbm(img, 0, tempfile) @@ -100,12 +109,56 @@ class TestFileGif(PillowTestCase): @unittest.skipUnless(netpbm_available(), "netpbm not available") def test_save_netpbm_l_mode(self): - img = Image.open(file).convert("L") + img = Image.open(TEST_GIF).convert("L") tempfile = self.tempfile("temp.gif") GifImagePlugin._save_netpbm(img, 0, tempfile) self.assert_image_similar(img, Image.open(tempfile).convert("L"), 0) + def test_seek(self): + img = Image.open("Tests/images/dispose_none.gif") + framecount = 0 + try: + while True: + framecount += 1 + img.seek(img.tell() +1) + except EOFError: + self.assertEqual(framecount, 5) + + def test_dispose_none(self): + img = Image.open("Tests/images/dispose_none.gif") + try: + while True: + img.seek(img.tell() +1) + self.assertEqual(img.disposal_method, 1) + except EOFError: + pass + + def test_dispose_background(self): + img = Image.open("Tests/images/dispose_bgnd.gif") + try: + while True: + img.seek(img.tell() +1) + self.assertEqual(img.disposal_method, 2) + except EOFError: + pass + + def test_dispose_previous(self): + img = Image.open("Tests/images/dispose_prev.gif") + try: + while True: + img.seek(img.tell() +1) + self.assertEqual(img.disposal_method, 3) + except EOFError: + pass + + def test_iss634(self): + img = Image.open("Tests/images/iss634.gif") + # seek to the second frame + img.seek(img.tell() +1) + # all transparent pixels should be replaced with the color from the first frame + self.assertEqual(img.histogram()[img.info['transparency']], 0) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py new file mode 100644 index 000000000..c64deb79d --- /dev/null +++ b/Tests/test_file_gimpgradient.py @@ -0,0 +1,127 @@ +from helper import unittest, PillowTestCase + +from PIL import GimpGradientFile + + +class TestImage(PillowTestCase): + + def test_linear_pos_le_middle(self): + # Arrange + middle = 0.5 + pos = 0.25 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + self.assertEqual(ret, 0.25) + + def test_linear_pos_le_small_middle(self): + # Arrange + middle = 1e-11 + pos = 1e-12 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + self.assertEqual(ret, 0.0) + + def test_linear_pos_gt_middle(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + self.assertEqual(ret, 0.75) + + def test_linear_pos_gt_small_middle(self): + # Arrange + middle = 1 - 1e-11 + pos = 1 - 1e-12 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + self.assertEqual(ret, 1.0) + + def test_curved(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.curved(middle, pos) + + # Assert + self.assertEqual(ret, 0.75) + + def test_sine(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.sine(middle, pos) + + # Assert + self.assertEqual(ret, 0.8535533905932737) + + def test_sphere_increasing(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.sphere_increasing(middle, pos) + + # Assert + self.assertEqual(ret, 0.9682458365518543) + + def test_sphere_decreasing(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.sphere_decreasing(middle, pos) + + # Assert + self.assertEqual(ret, 0.3385621722338523) + + def test_load_via_imagepalette(self): + # Arrange + from PIL import ImagePalette + test_file = "Tests/images/gimp_gradient.ggr" + + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + self.assertEqual(len(palette[0]), 1024) + self.assertEqual(palette[1], "RGBA") + + def test_load_1_3_via_imagepalette(self): + # Arrange + from PIL import ImagePalette + # GIMP 1.3 gradient files contain a name field + test_file = "Tests/images/gimp_gradient_with_name.ggr" + + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + self.assertEqual(len(palette[0]), 1024) + self.assertEqual(palette[1], "RGBA") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index f19eb16b7..99f6da9e3 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 165d10225..12f4ed3f3 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,16 +1,16 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image # sample ppm stream -file = "Tests/images/lena.ico" -data = open(file, "rb").read() +TEST_ICO_FILE = "Tests/images/hopper.ico" +TEST_DATA = open(TEST_ICO_FILE, "rb").read() class TestFileIco(PillowTestCase): def test_sanity(self): - im = Image.open(file) + im = Image.open(TEST_ICO_FILE) im.load() self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (16, 16)) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py new file mode 100644 index 000000000..bd331e5b2 --- /dev/null +++ b/Tests/test_file_iptc.py @@ -0,0 +1,79 @@ +from helper import unittest, PillowTestCase, lena + +from PIL import Image, IptcImagePlugin + +TEST_FILE = "Tests/images/iptc.jpg" + + +class TestFileIptc(PillowTestCase): + + # Helpers + + def dummy_IptcImagePlugin(self): + # Create an IptcImagePlugin object without initializing it + class FakeImage: + pass + im = FakeImage() + im.__class__ = IptcImagePlugin.IptcImageFile + return im + + # Tests + + def test_getiptcinfo_jpg_none(self): + # Arrange + im = lena() + + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + self.assertIsNone(iptc) + + def test_getiptcinfo_jpg_found(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + self.assertIsInstance(iptc, dict) + self.assertEqual(iptc[(2, 90)], b"Budapest") + self.assertEqual(iptc[(2, 101)], b"Hungary") + + def test_i(self): + # Arrange + c = b"a" + + # Act + ret = IptcImagePlugin.i(c) + + # Assert + self.assertEqual(ret, 97) + + def test_dump(self): + # Arrange + c = b"abc" + # Temporarily redirect stdout + try: + from cStringIO import StringIO + except ImportError: + from io import StringIO + import sys + old_stdout = sys.stdout + sys.stdout = mystdout = StringIO() + + # Act + IptcImagePlugin.dump(c) + + # Reset stdout + sys.stdout = old_stdout + + # Assert + self.assertEqual(mystdout.getvalue(), "61 62 63 \n") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 283c48eb7..98aa5be39 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena, py3 +from helper import unittest, PillowTestCase, lena, py3 from helper import djpeg_available, cjpeg_available import random @@ -60,7 +60,8 @@ class TestFileJpeg(PillowTestCase): self.assertGreater(y, 0.8) self.assertEqual(k, 0.0) # the opposite corner is black - c, m, y, k = [x / 255.0 for x in im.getpixel((im.size[0]-1, im.size[1]-1))] + c, m, y, k = [x / 255.0 for x in im.getpixel(( + im.size[0]-1, im.size[1]-1))] self.assertGreater(k, 0.9) # roundtrip, and check again im = self.roundtrip(im) @@ -69,7 +70,8 @@ class TestFileJpeg(PillowTestCase): self.assertGreater(m, 0.8) self.assertGreater(y, 0.8) self.assertEqual(k, 0.0) - c, m, y, k = [x / 255.0 for x in im.getpixel((im.size[0]-1, im.size[1]-1))] + c, m, y, k = [x / 255.0 for x in im.getpixel(( + im.size[0]-1, im.size[1]-1))] self.assertGreater(k, 0.9) def test_dpi(self): @@ -150,7 +152,8 @@ class TestFileJpeg(PillowTestCase): if py3: a = bytes(random.randint(0, 255) for _ in range(256 * 256 * 3)) else: - a = b''.join(chr(random.randint(0, 255)) for _ in range(256 * 256 * 3)) + a = b''.join(chr(random.randint(0, 255)) for _ in range( + 256 * 256 * 3)) im = Image.frombuffer("RGB", (256, 256), a, "raw", "RGB", 0, 1) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) @@ -216,10 +219,23 @@ class TestFileJpeg(PillowTestCase): info = im._getexif() self.assertEqual(info[305], 'Adobe Photoshop CS Macintosh') + def test_mp(self): + im = Image.open("Tests/images/pil_sample_rgb.jpg") + self.assertIsNone(im._getmp()) + def test_quality_keep(self): + # RGB im = Image.open("Tests/images/lena.jpg") f = self.tempfile('temp.jpg') im.save(f, quality='keep') + # Grayscale + im = Image.open("Tests/images/lena_gray.jpg") + f = self.tempfile('temp.jpg') + im.save(f, quality='keep') + # CMYK + im = Image.open("Tests/images/pil_sample_cmyk.jpg") + f = self.tempfile('temp.jpg') + im.save(f, quality='keep') def test_junk_jpeg_header(self): # https://github.com/python-pillow/Pillow/issues/630 @@ -231,11 +247,13 @@ class TestFileJpeg(PillowTestCase): qtables = im.quantization reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) self.assertEqual(im.quantization, reloaded.quantization) - self.assert_image_similar(im, self.roundtrip(im, qtables='web_low'), 30) - self.assert_image_similar(im, self.roundtrip(im, qtables='web_high'), 30) + self.assert_image_similar(im, self.roundtrip(im, qtables='web_low'), + 30) + self.assert_image_similar(im, self.roundtrip(im, qtables='web_high'), + 30) self.assert_image_similar(im, self.roundtrip(im, qtables='keep'), 30) - #values from wizard.txt in jpeg9-a src package. + # values from wizard.txt in jpeg9-a src package. standard_l_qtable = [int(s) for s in """ 16 11 10 16 24 40 51 61 12 12 14 19 26 58 60 55 @@ -247,7 +265,7 @@ class TestFileJpeg(PillowTestCase): 72 92 95 98 112 100 103 99 """.split(None)] - standard_chrominance_qtable= [int(s) for s in """ + standard_chrominance_qtable = [int(s) for s in """ 17 18 24 47 99 99 99 99 18 21 26 66 99 99 99 99 24 26 56 99 99 99 99 99 @@ -272,8 +290,8 @@ class TestFileJpeg(PillowTestCase): # dict of qtable lists self.assert_image_similar(im, self.roundtrip(im, - qtables={0:standard_l_qtable, - 1:standard_chrominance_qtable}), + qtables={0: standard_l_qtable, + 1: standard_chrominance_qtable}), 30) @unittest.skipUnless(djpeg_available(), "djpeg not available") @@ -291,6 +309,15 @@ class TestFileJpeg(PillowTestCase): # Default save quality is 75%, so a tiny bit of difference is alright self.assert_image_similar(img, Image.open(tempfile), 1) + def test_no_duplicate_0x1001_tag(self): + # Arrange + from PIL import ExifTags + tag_ids = dict(zip(ExifTags.TAGS.values(), ExifTags.TAGS.keys())) + + # Assert + self.assertEqual(tag_ids['RelatedImageWidth'], 0x1001) + self.assertEqual(tag_ids['RelatedImageLength'], 0x1002) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 23564c434..db67e9551 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image from io import BytesIO @@ -52,7 +52,8 @@ class TestFileJpeg2k(PillowTestCase): def test_lossless(self): im = Image.open('Tests/images/test-card-lossless.jp2') im.load() - im.save('/tmp/test-card.png') + outfile = self.tempfile('temp_test-card.png') + im.save(outfile) self.assert_image_similar(im, test_card, 1.0e-3) def test_lossy_tiled(self): diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1afeee488..bdac5f300 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,11 +1,10 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena, py3 +from helper import unittest, PillowTestCase, lena, py3 import os from PIL import Image, TiffImagePlugin - -class TestFileLibTiff(PillowTestCase): +class LibTiffTestCase(PillowTestCase): def setUp(self): codecs = dir(Image.core) @@ -32,6 +31,8 @@ class TestFileLibTiff(PillowTestCase): out = self.tempfile("temp.png") im.save(out) +class TestFileLibTiff(LibTiffTestCase): + def test_g4_tiff(self): """Test the ordinary file path load path""" @@ -311,6 +312,35 @@ class TestFileLibTiff(PillowTestCase): self.assertRaises(OSError, lambda: os.fstat(fn)) self.assertRaises(OSError, lambda: os.close(fn)) + def test_multipage(self): + # issue #862 + TiffImagePlugin.READ_LIBTIFF = True + im = Image.open('Tests/images/multipage.tiff') + # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + + im.seek(0) + self.assertEqual(im.size, (10,10)) + self.assertEqual(im.convert('RGB').getpixel((0,0)), (0,128,0)) + self.assertTrue(im.tag.next) + + im.seek(1) + self.assertEqual(im.size, (10,10)) + self.assertEqual(im.convert('RGB').getpixel((0,0)), (255,0,0)) + self.assertTrue(im.tag.next) + + im.seek(2) + self.assertFalse(im.tag.next) + self.assertEqual(im.size, (20,20)) + self.assertEqual(im.convert('RGB').getpixel((0,0)), (0,0,255)) + + TiffImagePlugin.READ_LIBTIFF = False + + def test__next(self): + TiffImagePlugin.READ_LIBTIFF = True + im = Image.open('Tests/images/lena.tif') + self.assertFalse(im.tag.next) + im.load() + self.assertFalse(im.tag.next) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index acc2390c9..9c73ee5c2 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,13 +1,11 @@ -from helper import unittest, tearDownModule +from helper import unittest from PIL import Image -from test_file_libtiff import TestFileLibTiff +from test_file_libtiff import LibTiffTestCase -class TestFileLibTiffSmall(TestFileLibTiff): - - # Inherits TestFileLibTiff's setUp() and self._assert_noerr() +class TestFileLibTiffSmall(LibTiffTestCase): """ The small lena image was failing on open in the libtiff decoder because the file pointer was set to the wrong place diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py new file mode 100644 index 000000000..a221eec15 --- /dev/null +++ b/Tests/test_file_mpo.py @@ -0,0 +1,127 @@ +from helper import unittest, PillowTestCase +from io import BytesIO +from PIL import Image + + +test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] + + +class TestFileMpo(PillowTestCase): + + def setUp(self): + codecs = dir(Image.core) + if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: + self.skipTest("jpeg support not available") + + def frame_roundtrip(self, im, **options): + # Note that for now, there is no MPO saving functionality + out = BytesIO() + im.save(out, "MPO", **options) + bytes = out.tell() + out.seek(0) + im = Image.open(out) + im.bytes = bytes # for testing only + return im + + def test_sanity(self): + for test_file in test_files: + im = Image.open(test_file) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (640, 480)) + self.assertEqual(im.format, "MPO") + + def test_app(self): + for test_file in test_files: + # Test APP/COM reader (@PIL135) + im = Image.open(test_file) + self.assertEqual(im.applist[0][0], 'APP1') + self.assertEqual(im.applist[1][0], 'APP2') + self.assertEqual(im.applist[1][1][:16], + b'MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00') + self.assertEqual(len(im.applist), 2) + + def test_exif(self): + for test_file in test_files: + im = Image.open(test_file) + info = im._getexif() + self.assertEqual(info[272], 'Nintendo 3DS') + self.assertEqual(info[296], 2) + self.assertEqual(info[34665], 188) + + def test_mp(self): + for test_file in test_files: + im = Image.open(test_file) + mpinfo = im._getmp() + self.assertEqual(mpinfo[45056], b'0100') + self.assertEqual(mpinfo[45057], 2) + + def test_mp_attribute(self): + for test_file in test_files: + im = Image.open(test_file) + mpinfo = im._getmp() + frameNumber = 0 + for mpentry in mpinfo[45058]: + mpattr = mpentry['Attribute'] + if frameNumber: + self.assertFalse(mpattr['RepresentativeImageFlag']) + else: + self.assertTrue(mpattr['RepresentativeImageFlag']) + self.assertFalse(mpattr['DependentParentImageFlag']) + self.assertFalse(mpattr['DependentChildImageFlag']) + self.assertEqual(mpattr['ImageDataFormat'], 'JPEG') + self.assertEqual(mpattr['MPType'], + 'Multi-Frame Image: (Disparity)') + self.assertEqual(mpattr['Reserved'], 0) + frameNumber += 1 + + def test_seek(self): + for test_file in test_files: + im = Image.open(test_file) + self.assertEqual(im.tell(), 0) + # prior to first image raises an error, both blatant and borderline + self.assertRaises(EOFError, im.seek, -1) + self.assertRaises(EOFError, im.seek, -523) + # after the final image raises an error, both blatant and borderline + self.assertRaises(EOFError, im.seek, 2) + self.assertRaises(EOFError, im.seek, 523) + # bad calls shouldn't change the frame + self.assertEqual(im.tell(), 0) + # this one will work + im.seek(1) + self.assertEqual(im.tell(), 1) + # and this one, too + im.seek(0) + self.assertEqual(im.tell(), 0) + + def test_image_grab(self): + for test_file in test_files: + im = Image.open(test_file) + self.assertEqual(im.tell(), 0) + im0 = im.tobytes() + im.seek(1) + self.assertEqual(im.tell(), 1) + im1 = im.tobytes() + im.seek(0) + self.assertEqual(im.tell(), 0) + im02 = im.tobytes() + self.assertEqual(im0, im02) + self.assertNotEqual(im0, im1) + + def test_save(self): + # Note that only individual frames can be saved at present + for test_file in test_files: + im = Image.open(test_file) + self.assertEqual(im.tell(), 0) + jpg0 = self.frame_roundtrip(im) + self.assert_image_similar(im, jpg0, 30) + im.seek(1) + self.assertEqual(im.tell(), 1) + jpg1 = self.frame_roundtrip(im) + self.assert_image_similar(im, jpg1, 30) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 2444879d1..a64faad10 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index c1947ff37..388df0237 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena, imagemagick_available +from helper import unittest, PillowTestCase, lena, imagemagick_available import os.path diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index d0800e203..f278bd91d 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 089168393..689302bb5 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena import os.path diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 145eff327..32dc10cd1 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, hopper from io import BytesIO @@ -10,8 +10,8 @@ codecs = dir(Image.core) # sample png stream -file = "Tests/images/lena.png" -data = open(file, "rb").read() +TEST_PNG_FILE = "Tests/images/hopper.png" +TEST_DATA = open(TEST_PNG_FILE, "rb").read() # stuff to create inline PNG images @@ -58,7 +58,7 @@ class TestFilePng(PillowTestCase): file = self.tempfile("temp.png") - lena("RGB").save(file) + hopper("RGB").save(file) im = Image.open(file) im.load() @@ -66,19 +66,19 @@ class TestFilePng(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "PNG") - lena("1").save(file) + hopper("1").save(file) im = Image.open(file) - lena("L").save(file) + hopper("L").save(file) im = Image.open(file) - lena("P").save(file) + hopper("P").save(file) im = Image.open(file) - lena("RGB").save(file) + hopper("RGB").save(file) im = Image.open(file) - lena("I").save(file) + hopper("I").save(file) im = Image.open(file) def test_broken(self): @@ -129,6 +129,39 @@ class TestFilePng(PillowTestCase): HEAD + chunk(b'zTXt', b'spam\0\0' + zlib.compress(b'egg')) + TAIL) self.assertEqual(im.info, {'spam': 'egg'}) + def test_bad_itxt(self): + + im = load(HEAD + chunk(b'iTXt') + TAIL) + self.assertEqual(im.info, {}) + + im = load(HEAD + chunk(b'iTXt', b'spam') + TAIL) + self.assertEqual(im.info, {}) + + im = load(HEAD + chunk(b'iTXt', b'spam\0') + TAIL) + self.assertEqual(im.info, {}) + + im = load(HEAD + chunk(b'iTXt', b'spam\0\x02') + TAIL) + self.assertEqual(im.info, {}) + + im = load(HEAD + chunk(b'iTXt', b'spam\0\0\0foo\0') + TAIL) + self.assertEqual(im.info, {}) + + im = load(HEAD + chunk(b'iTXt', b'spam\0\0\0en\0Spam\0egg') + TAIL) + self.assertEqual(im.info, {"spam": "egg"}) + self.assertEqual(im.info["spam"].lang, "en") + self.assertEqual(im.info["spam"].tkey, "Spam") + + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + zlib.compress(b"egg")[:1]) + TAIL) + self.assertEqual(im.info, {}) + + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\1en\0Spam\0' + zlib.compress(b"egg")) + TAIL) + self.assertEqual(im.info, {}) + + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + zlib.compress(b"egg")) + TAIL) + self.assertEqual(im.info, {"spam": "egg"}) + self.assertEqual(im.info["spam"].lang, "en") + self.assertEqual(im.info["spam"].tkey, "Spam") + def test_interlace(self): file = "Tests/images/pil123p.png" @@ -204,17 +237,17 @@ class TestFilePng(PillowTestCase): def test_load_verify(self): # Check open/load/verify exception (@PIL150) - im = Image.open("Tests/images/lena.png") + im = Image.open(TEST_PNG_FILE) im.verify() - im = Image.open("Tests/images/lena.png") + im = Image.open(TEST_PNG_FILE) im.load() self.assertRaises(RuntimeError, lambda: im.verify()) def test_roundtrip_dpi(self): # Check dpi roundtripping - im = Image.open(file) + im = Image.open(TEST_PNG_FILE) im = roundtrip(im, dpi=(100, 100)) self.assertEqual(im.info["dpi"], (100, 100)) @@ -222,7 +255,7 @@ class TestFilePng(PillowTestCase): def test_roundtrip_text(self): # Check text roundtripping - im = Image.open(file) + im = Image.open(TEST_PNG_FILE) info = PngImagePlugin.PngInfo() info.add_text("TXT", "VALUE") @@ -232,6 +265,50 @@ class TestFilePng(PillowTestCase): self.assertEqual(im.info, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) self.assertEqual(im.text, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) + def test_roundtrip_itxt(self): + # Check iTXt roundtripping + + im = Image.new("RGB", (32, 32)) + info = PngImagePlugin.PngInfo() + info.add_itxt("spam", "Eggs", "en", "Spam") + info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), zip=True) + + im = roundtrip(im, pnginfo=info) + self.assertEqual(im.info, {"spam": "Eggs", "eggs": "Spam"}) + self.assertEqual(im.text, {"spam": "Eggs", "eggs": "Spam"}) + self.assertEqual(im.text["spam"].lang, "en") + self.assertEqual(im.text["spam"].tkey, "Spam") + self.assertEqual(im.text["eggs"].lang, "en") + self.assertEqual(im.text["eggs"].tkey, "Eggs") + + def test_nonunicode_text(self): + # Check so that non-Unicode text is saved as a tEXt rather than iTXt + + im = Image.new("RGB", (32, 32)) + info = PngImagePlugin.PngInfo() + info.add_text("Text", "Ascii") + im = roundtrip(im, pnginfo=info) + self.assertEqual(type(im.info["Text"]), str) + + def test_unicode_text(self): + # Check preservation of non-ASCII characters on Python3 + # This cannot really be meaningfully tested on Python2, + # since it didn't preserve charsets to begin with. + + def rt_text(value): + im = Image.new("RGB", (32, 32)) + info = PngImagePlugin.PngInfo() + info.add_text("Text", value) + im = roundtrip(im, pnginfo=info) + self.assertEqual(im.info, {"Text": value}) + + if str is not bytes: + rt_text(" Aa" + chr(0xa0) + chr(0xc4) + chr(0xff)) # Latin1 + rt_text(chr(0x400) + chr(0x472) + chr(0x4ff)) # Cyrillic + rt_text(chr(0x4e00) + chr(0x66f0) + # CJK + chr(0x9fba) + chr(0x3042) + chr(0xac00)) + rt_text("A" + chr(0xc4) + chr(0x472) + chr(0x3042)) # Combined + def test_scary(self): # Check reading of evil PNG file. For information, see: # http://scary.beasts.org/security/CESA-2004-001.txt @@ -261,7 +338,7 @@ class TestFilePng(PillowTestCase): def test_trns_p(self): # Check writing a transparency of 0, issue #528 - im = lena('P') + im = hopper('P') im.info['transparency'] = 0 f = self.tempfile("temp.png") @@ -283,7 +360,7 @@ class TestFilePng(PillowTestCase): def test_roundtrip_icc_profile(self): # check that we can roundtrip the icc profile - im = lena('RGB') + im = hopper('RGB') jpeg_image = Image.open('Tests/images/flower2.jpg') expected_icc = jpeg_image.info['icc_profile'] diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index d9e4e0674..e1f1537d2 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 007646901..ee903ce5c 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py new file mode 100644 index 000000000..d49086c51 --- /dev/null +++ b/Tests/test_file_sgi.py @@ -0,0 +1,39 @@ +from helper import unittest, PillowTestCase + +from PIL import Image + + +class TestFileSgi(PillowTestCase): + + def test_rgb(self): + # Arrange + # Created with ImageMagick then renamed: + # convert hopper.ppm hopper.sgi + test_file = "Tests/images/hopper.rgb" + + # Act / Assert + self.assertRaises(ValueError, lambda: Image.open(test_file)) + + def test_l(self): + # Arrange + # Created with ImageMagick then renamed: + # convert hopper.ppm -monochrome hopper.sgi + test_file = "Tests/images/hopper.bw" + + # Act / Assert + self.assertRaises(ValueError, lambda: Image.open(test_file)) + + def test_rgba(self): + # Arrange + # Created with ImageMagick: + # convert transparent.png transparent.sgi + test_file = "Tests/images/transparent.sgi" + + # Act / Assert + self.assertRaises(ValueError, lambda: Image.open(test_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index e0731ca8c..7bfedad1a 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,15 +1,15 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image from PIL import SpiderImagePlugin -test_file = "Tests/images/lena.spider" +TEST_FILE = "Tests/images/hopper.spider" class TestImageSpider(PillowTestCase): def test_sanity(self): - im = Image.open(test_file) + im = Image.open(TEST_FILE) im.load() self.assertEqual(im.mode, "F") self.assertEqual(im.size, (128, 128)) @@ -18,7 +18,7 @@ class TestImageSpider(PillowTestCase): def test_save(self): # Arrange temp = self.tempfile('temp.spider') - im = lena() + im = hopper() # Act im.save(temp, "SPIDER") @@ -30,7 +30,50 @@ class TestImageSpider(PillowTestCase): self.assertEqual(im2.format, "SPIDER") def test_isSpiderImage(self): - self.assertTrue(SpiderImagePlugin.isSpiderImage(test_file)) + self.assertTrue(SpiderImagePlugin.isSpiderImage(TEST_FILE)) + + def test_tell(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act + index = im.tell() + + # Assert + self.assertEqual(index, 0) + + def test_loadImageSeries(self): + # Arrange + not_spider_file = "Tests/images/hopper.ppm" + file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] + + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) + + # Assert + self.assertEqual(len(img_list), 1) + self.assertIsInstance(img_list[0], Image.Image) + self.assertEqual(img_list[0].size, (128, 128)) + + def test_loadImageSeries_no_input(self): + # Arrange + file_list = None + + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) + + # Assert + self.assertEqual(img_list, None) + + def test_isInt_not_a_number(self): + # Arrange + not_a_number = "a" + + # Act + ret = SpiderImagePlugin.isInt(not_a_number) + + # Assert + self.assertEqual(ret, 0) if __name__ == '__main__': diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py new file mode 100644 index 000000000..332104062 --- /dev/null +++ b/Tests/test_file_sun.py @@ -0,0 +1,23 @@ +from helper import unittest, PillowTestCase + +from PIL import Image + + +class TestFileSun(PillowTestCase): + + def test_sanity(self): + # Arrange + # Created with ImageMagick: convert hopper.jpg hopper.ras + test_file = "Tests/images/hopper.ras" + + # Act + im = Image.open(test_file) + + # Assert + self.assertEqual(im.size, (128, 128)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 7e36f35fc..958b2f4df 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,11 +1,11 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image, TarIO codecs = dir(Image.core) -# sample ppm stream -tarfile = "Tests/images/lena.tar" +# Sample tar archive +TEST_TAR_FILE = "Tests/images/hopper.tar" class TestFileTar(PillowTestCase): @@ -16,7 +16,7 @@ class TestFileTar(PillowTestCase): def test_sanity(self): if "zip_decoder" in codecs: - tar = TarIO.TarIO(tarfile, 'lena.png') + tar = TarIO.TarIO(TEST_TAR_FILE, 'hopper.png') im = Image.open(tar) im.load() self.assertEqual(im.mode, "RGB") @@ -24,7 +24,7 @@ class TestFileTar(PillowTestCase): self.assertEqual(im.format, "PNG") if "jpeg_decoder" in codecs: - tar = TarIO.TarIO(tarfile, 'lena.jpg') + tar = TarIO.TarIO(TEST_TAR_FILE, 'hopper.jpg') im = Image.open(tar) im.load() self.assertEqual(im.mode, "RGB") diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py new file mode 100644 index 000000000..ea94dee64 --- /dev/null +++ b/Tests/test_file_tga.py @@ -0,0 +1,20 @@ +from helper import unittest, PillowTestCase + +from PIL import Image + + +class TestFileTga(PillowTestCase): + + def test_id_field(self): + # tga file with id field + test_file = "Tests/images/tga_id_field.tga" + + # Act + im = Image.open(test_file) + + # Assert + self.assertEqual(im.size, (100, 100)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index ed3d1e9cd..464f1c314 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,6 +1,6 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena, py3 +from helper import unittest, PillowTestCase, lena, py3 -from PIL import Image +from PIL import Image, TiffImagePlugin class TestFileTiff(PillowTestCase): @@ -141,6 +141,163 @@ class TestFileTiff(PillowTestCase): self.assertEqual( im.getextrema(), (-3.140936851501465, 3.140684127807617)) + def test_multipage(self): + # issue #862 + im = Image.open('Tests/images/multipage.tiff') + # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + + im.seek(0) + self.assertEqual(im.size, (10,10)) + self.assertEqual(im.convert('RGB').getpixel((0,0)), (0,128,0)) + + im.seek(1) + im.load() + self.assertEqual(im.size, (10,10)) + self.assertEqual(im.convert('RGB').getpixel((0,0)), (255,0,0)) + + im.seek(2) + im.load() + self.assertEqual(im.size, (20,20)) + self.assertEqual(im.convert('RGB').getpixel((0,0)), (0,0,255)) + + def test_multipage_last_frame(self): + im = Image.open('Tests/images/multipage-lastframe.tif') + im.load() + self.assertEqual(im.size, (20,20)) + self.assertEqual(im.convert('RGB').getpixel((0,0)), (0,0,255)) + + + def test___str__(self): + # Arrange + file = "Tests/images/pil136.tiff" + im = Image.open(file) + + # Act + ret = str(im.ifd) + + # Assert + self.assertIsInstance(ret, str) + self.assertEqual( + ret, + '{256: (55,), 257: (43,), 258: (8, 8, 8, 8), 259: (1,), ' + '262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,), ' + '279: (9460,), 282: ((720000, 10000),), ' + '283: ((720000, 10000),), 284: (1,)}') + + def test__delitem__(self): + # Arrange + file = "Tests/images/pil136.tiff" + im = Image.open(file) + len_before = len(im.ifd.as_dict()) + + # Act + del im.ifd[256] + + # Assert + len_after = len(im.ifd.as_dict()) + self.assertEqual(len_before, len_after + 1) + + def test_load_byte(self): + # Arrange + from PIL import TiffImagePlugin + ifd = TiffImagePlugin.ImageFileDirectory() + data = b"abc" + + # Act + ret = ifd.load_byte(data) + + # Assert + self.assertEqual(ret, b"abc") + + def test_load_string(self): + # Arrange + from PIL import TiffImagePlugin + ifd = TiffImagePlugin.ImageFileDirectory() + data = b"abc\0" + + # Act + ret = ifd.load_string(data) + + # Assert + self.assertEqual(ret, "abc") + + def test_load_float(self): + # Arrange + from PIL import TiffImagePlugin + ifd = TiffImagePlugin.ImageFileDirectory() + data = b"abcdabcd" + + # Act + ret = ifd.load_float(data) + + # Assert + self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) + + def test_load_double(self): + # Arrange + from PIL import TiffImagePlugin + ifd = TiffImagePlugin.ImageFileDirectory() + data = b"abcdefghabcdefgh" + + # Act + ret = ifd.load_double(data) + + # Assert + self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) + + def test_seek(self): + # Arrange + file = "Tests/images/pil136.tiff" + im = Image.open(file) + + # Act + im.seek(-1) + + # Assert + self.assertEqual(im.tell(), 0) + + def test_seek_eof(self): + # Arrange + file = "Tests/images/pil136.tiff" + im = Image.open(file) + self.assertEqual(im.tell(), 0) + + # Act / Assert + self.assertRaises(EOFError, lambda: im.seek(1)) + + def test__cvt_res_int(self): + # Arrange + from PIL.TiffImagePlugin import _cvt_res + value = 34 + + # Act + ret = _cvt_res(value) + + # Assert + self.assertEqual(ret, (34, 1)) + + def test__cvt_res_float(self): + # Arrange + from PIL.TiffImagePlugin import _cvt_res + value = 22.3 + + # Act + ret = _cvt_res(value) + + # Assert + self.assertEqual(ret, (1461452, 65536)) + + def test__cvt_res_sequence(self): + # Arrange + from PIL.TiffImagePlugin import _cvt_res + value = [0, 1] + + # Act + ret = _cvt_res(value) + + # Assert + self.assertEqual(ret, [0, 1]) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 2019f3455..4cebb56e2 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image, TiffImagePlugin, TiffTags @@ -8,7 +8,7 @@ tag_ids = dict(zip(TiffTags.TAGS.values(), TiffTags.TAGS.keys())) class TestFileTiffMetadata(PillowTestCase): def test_rt_metadata(self): - """ Test writing arbitray metadata into the tiff image directory + """ Test writing arbitrary metadata into the tiff image directory Use case is ImageJ private tags, one numeric, one arbitrary data. https://github.com/python-pillow/Pillow/issues/291 """ @@ -86,6 +86,10 @@ class TestFileTiffMetadata(PillowTestCase): self.assertEqual( value, reloaded[tag], "%s didn't roundtrip" % tag) + def test_no_duplicate_50741_tag(self): + self.assertEqual(tag_ids['MakerNoteSafety'], 50741) + self.assertEqual(tag_ids['BestQualityScale'], 50780) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 1eeea57d3..ffaf7c673 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 0df3143bb..5f8f653cf 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 9f8e339de..662ad1117 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 2470f4c49..6aadf9c7e 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index d520ef460..02aec70b1 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index e6e750298..4fc393808 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,21 +1,34 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase, lena from PIL import Image # sample ppm stream -file = "Tests/images/lena.xpm" -data = open(file, "rb").read() +TEST_FILE = "Tests/images/lena.xpm" class TestFileXpm(PillowTestCase): def test_sanity(self): - im = Image.open(file) + im = Image.open(TEST_FILE) im.load() self.assertEqual(im.mode, "P") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "XPM") + #large error due to quantization->44 colors. + self.assert_image_similar(im.convert('RGB'), lena('RGB'), 60) + + def test_load_read(self): + # Arrange + im = Image.open(TEST_FILE) + dummy_bytes = 1 + + # Act + data = im.load_read(dummy_bytes) + + # Assert + self.assertEqual(len(data), 16384) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index ce5a371e0..0df8e866b 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import FontFile, BdfFontFile diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 8c4c04cd4..5e9e02c8c 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image, FontFile, PcfFontFile from PIL import ImageFont, ImageDraw diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py new file mode 100644 index 000000000..03603aa9b --- /dev/null +++ b/Tests/test_format_hsv.py @@ -0,0 +1,169 @@ +from helper import unittest, PillowTestCase, lena + +from PIL import Image + +import colorsys, itertools + +class TestFormatHSV(PillowTestCase): + + def int_to_float(self, i): + return float(i)/255.0 + def str_to_float(self, i): + return float(ord(i))/255.0 + def to_int(self, f): + return int(f*255.0) + def tuple_to_ints(self, tp): + x,y,z = tp + return (int(x*255.0), int(y*255.0), int(z*255.0)) + + def test_sanity(self): + im = Image.new('HSV', (100,100)) + + def wedge(self): + w =Image._wedge() + w90 = w.rotate(90) + + (px, h) = w.size + + r = Image.new('L', (px*3,h)) + g = r.copy() + b = r.copy() + + r.paste(w, (0,0)) + r.paste(w90, (px,0)) + + g.paste(w90, (0,0)) + g.paste(w, (2*px,0)) + + b.paste(w, (px,0)) + b.paste(w90, (2*px,0)) + + img = Image.merge('RGB',(r,g,b)) + + #print (("%d, %d -> "% (int(1.75*px),int(.25*px))) + \ + # "(%s, %s, %s)"%img.getpixel((1.75*px, .25*px))) + #print (("%d, %d -> "% (int(.75*px),int(.25*px))) + \ + # "(%s, %s, %s)"%img.getpixel((.75*px, .25*px))) + return img + + def to_xxx_colorsys(self, im, func, mode): + # convert the hard way using the library colorsys routines. + + (r,g,b) = im.split() + + if bytes is str: + conv_func = self.str_to_float + else: + conv_func = self.int_to_float + + if hasattr(itertools, 'izip'): + iter_helper = itertools.izip + else: + iter_helper = itertools.zip_longest + + + converted = [self.tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) + for (_r, _g, _b) in iter_helper(r.tobytes(), g.tobytes(), b.tobytes())] + + if str is bytes: + new_bytes = b''.join(chr(h)+chr(s)+chr(v) for (h,s,v) in converted) + else: + new_bytes = b''.join(bytes(chr(h)+chr(s)+chr(v), 'latin-1') for (h,s,v) in converted) + + hsv = Image.frombytes(mode,r.size, new_bytes) + + return hsv + + def to_hsv_colorsys(self, im): + return self.to_xxx_colorsys(im, colorsys.rgb_to_hsv, 'HSV') + + def to_rgb_colorsys(self, im): + return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, 'RGB') + + def test_wedge(self): + src = self.wedge().resize((3*32,32),Image.BILINEAR) + im = src.convert('HSV') + comparable = self.to_hsv_colorsys(src) + + #print (im.getpixel((448, 64))) + #print (comparable.getpixel((448, 64))) + + #print(im.split()[0].histogram()) + #print(comparable.split()[0].histogram()) + + #im.split()[0].show() + #comparable.split()[0].show() + + self.assert_image_similar(im.split()[0], comparable.split()[0], + 1, "Hue conversion is wrong") + self.assert_image_similar(im.split()[1], comparable.split()[1], + 1, "Saturation conversion is wrong") + self.assert_image_similar(im.split()[2], comparable.split()[2], + 1, "Value conversion is wrong") + + #print (im.getpixel((192, 64))) + + comparable = src + im = im.convert('RGB') + + #im.split()[0].show() + #comparable.split()[0].show() + #print (im.getpixel((192, 64))) + #print (comparable.getpixel((192, 64))) + + self.assert_image_similar(im.split()[0], comparable.split()[0], + 3, "R conversion is wrong") + self.assert_image_similar(im.split()[1], comparable.split()[1], + 3, "G conversion is wrong") + self.assert_image_similar(im.split()[2], comparable.split()[2], + 3, "B conversion is wrong") + + + def test_convert(self): + im = lena('RGB').convert('HSV') + comparable = self.to_hsv_colorsys(lena('RGB')) + +# print ([ord(x) for x in im.split()[0].tobytes()[:80]]) +# print ([ord(x) for x in comparable.split()[0].tobytes()[:80]]) + +# print(im.split()[0].histogram()) +# print(comparable.split()[0].histogram()) + + self.assert_image_similar(im.split()[0], comparable.split()[0], + 1, "Hue conversion is wrong") + self.assert_image_similar(im.split()[1], comparable.split()[1], + 1, "Saturation conversion is wrong") + self.assert_image_similar(im.split()[2], comparable.split()[2], + 1, "Value conversion is wrong") + + + def test_hsv_to_rgb(self): + comparable = self.to_hsv_colorsys(lena('RGB')) + converted = comparable.convert('RGB') + comparable = self.to_rgb_colorsys(comparable) + + # print(converted.split()[1].histogram()) + # print(target.split()[1].histogram()) + + # print ([ord(x) for x in target.split()[1].tobytes()[:80]]) + # print ([ord(x) for x in converted.split()[1].tobytes()[:80]]) + + + self.assert_image_similar(converted.split()[0], comparable.split()[0], + 3, "R conversion is wrong") + self.assert_image_similar(converted.split()[1], comparable.split()[1], + 3, "G conversion is wrong") + self.assert_image_similar(converted.split()[2], comparable.split()[2], + 3, "B conversion is wrong") + + + + + + + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index 188b0d1fa..53468db5f 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_image.py b/Tests/test_image.py index ec82eb419..26e45b10a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,6 +1,7 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase, hopper from PIL import Image +import sys class TestImage(PillowTestCase): @@ -44,6 +45,156 @@ class TestImage(PillowTestCase): file = self.tempfile("temp.ppm") im._dump(file) + def test_comparison_with_other_type(self): + # Arrange + item = Image.new('RGB', (25, 25), '#000') + num = 12 + + # Act/Assert + # Shouldn't cause AttributeError (#774) + self.assertFalse(item is None) + self.assertFalse(item == None) + self.assertFalse(item == num) + + def test_expand_x(self): + # Arrange + im = hopper() + orig_size = im.size + xmargin = 5 + + # Act + im = im._expand(xmargin) + + # Assert + self.assertEqual(im.size[0], orig_size[0] + 2*xmargin) + self.assertEqual(im.size[1], orig_size[1] + 2*xmargin) + + def test_expand_xy(self): + # Arrange + im = hopper() + orig_size = im.size + xmargin = 5 + ymargin = 3 + + # Act + im = im._expand(xmargin, ymargin) + + # Assert + self.assertEqual(im.size[0], orig_size[0] + 2*xmargin) + self.assertEqual(im.size[1], orig_size[1] + 2*ymargin) + + def test_getbands(self): + # Arrange + im = hopper() + + # Act + bands = im.getbands() + + # Assert + self.assertEqual(bands, ('R', 'G', 'B')) + + def test_getbbox(self): + # Arrange + im = hopper() + + # Act + bbox = im.getbbox() + + # Assert + self.assertEqual(bbox, (0, 0, 128, 128)) + + def test_ne(self): + # Arrange + im1 = Image.new('RGB', (25, 25), 'black') + im2 = Image.new('RGB', (25, 25), 'white') + + # Act / Assert + self.assertTrue(im1 != im2) + + def test_alpha_composite(self): + # http://stackoverflow.com/questions/3374878 + # Arrange + from PIL import ImageDraw + + expected_colors = sorted([ + (1122, (128, 127, 0, 255)), + (1089, (0, 255, 0, 255)), + (3300, (255, 0, 0, 255)), + (1156, (170, 85, 0, 192)), + (1122, (0, 255, 0, 128)), + (1122, (255, 0, 0, 128)), + (1089, (0, 255, 0, 0))]) + + dst = Image.new('RGBA', size=(100, 100), color=(0, 255, 0, 255)) + draw = ImageDraw.Draw(dst) + draw.rectangle((0, 33, 100, 66), fill=(0, 255, 0, 128)) + draw.rectangle((0, 67, 100, 100), fill=(0, 255, 0, 0)) + src = Image.new('RGBA', size=(100, 100), color=(255, 0, 0, 255)) + draw = ImageDraw.Draw(src) + draw.rectangle((33, 0, 66, 100), fill=(255, 0, 0, 128)) + draw.rectangle((67, 0, 100, 100), fill=(255, 0, 0, 0)) + + # Act + img = Image.alpha_composite(dst, src) + + # Assert + img_colors = sorted(img.getcolors()) + self.assertEqual(img_colors, expected_colors) + + def test_effect_mandelbrot(self): + # Arrange + size = (512, 512) + extent = (-3, -2.5, 2, 2.5) + quality = 100 + + # Act + im = Image.effect_mandelbrot(size, extent, quality) + + # Assert + self.assertEqual(im.size, (512, 512)) + im2 = Image.open('Tests/images/effect_mandelbrot.png') + self.assert_image_equal(im, im2) + + def test_effect_mandelbrot_bad_arguments(self): + # Arrange + size = (512, 512) + # Get coordinates the wrong way round: + extent = (+3, +2.5, -2, -2.5) + # Quality < 2: + quality = 1 + + # Act/Assert + self.assertRaises( + ValueError, + lambda: Image.effect_mandelbrot(size, extent, quality)) + + @unittest.skipUnless(sys.platform.startswith('win32'), + "Stalls on Travis CI, passes on Windows") + def test_effect_noise(self): + # Arrange + size = (100, 100) + sigma = 128 + + # Act + im = Image.effect_noise(size, sigma) + + # Assert + self.assertEqual(im.size, (100, 100)) + self.assertEqual(im.getpixel((0, 0)), 60) + self.assertEqual(im.getpixel((0, 1)), 28) + + def test_effect_spread(self): + # Arrange + im = hopper() + distance = 10 + + # Act + im2 = im.effect_spread(distance) + + # Assert + self.assertEqual(im.size, (128, 128)) + im3 = Image.open('Tests/images/effect_spread.png') + self.assert_image_similar(im2, im3, 110) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index dce2fa106..c5e49fc39 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,11 +1,11 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image im = lena().resize((128, 100)) -class TestImageCrop(PillowTestCase): +class TestImageArray(PillowTestCase): def test_toarray(self): def test(mode): diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 1415bae3a..01a80732b 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 205118e47..a7882db94 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index f7ea48c95..da93fe7c8 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 252e60376..a76b8d266 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, fromstring, tostring +from helper import unittest, PillowTestCase, fromstring, tostring from PIL import Image diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 8c04ebb1d..2452479cc 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import ImageFilter @@ -29,6 +29,10 @@ class TestImageFilter(PillowTestCase): filter(ImageFilter.MinFilter) filter(ImageFilter.ModeFilter) filter(ImageFilter.Kernel((3, 3), list(range(9)))) + filter(ImageFilter.GaussianBlur) + filter(ImageFilter.GaussianBlur(5)) + filter(ImageFilter.UnsharpMask) + filter(ImageFilter.UnsharpMask(10)) self.assertRaises(TypeError, lambda: filter("hello")) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index abba18852..aad8046a1 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index 6aadaa502..e803abb02 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index f89dcf7ca..8d78195bd 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index 7011c3443..d3e5a4989 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena class TestImageGetColors(PillowTestCase): diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index 71416c4b9..ff6659595 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena class TestImageGetData(PillowTestCase): diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 7d896c821..af7f7698a 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena class TestImageGetExtrema(PillowTestCase): diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 6141877cd..d498d3923 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena, py3 +from helper import unittest, PillowTestCase, lena, py3 class TestImageGetIm(PillowTestCase): diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 8b6804a5a..0c399c432 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena class TestImageGetPalette(PillowTestCase): diff --git a/Tests/test_image_getpixel.py b/Tests/test_image_getpixel.py index 20be3bdec..965233f94 100644 --- a/Tests/test_image_getpixel.py +++ b/Tests/test_image_getpixel.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index 8c340847c..262a21d4b 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 6fd203758..70f78a1fb 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena class TestImageHistogram(PillowTestCase): diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 14cb76fdb..b1fc73182 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -9,21 +9,21 @@ class TestImageLoad(PillowTestCase): def test_sanity(self): - im = lena() + im = hopper() pix = im.load() - self.assertEqual(pix[0, 0], (223, 162, 133)) + self.assertEqual(pix[0, 0], (20, 20, 70)) def test_close(self): - im = Image.open("Tests/images/lena.gif") + im = Image.open("Tests/images/hopper.gif") im.close() self.assertRaises(ValueError, lambda: im.load()) self.assertRaises(ValueError, lambda: im.getpixel((0, 0))) def test_contextmanager(self): fn = None - with Image.open("Tests/images/lena.gif") as im: + with Image.open("Tests/images/hopper.gif") as im: fn = im.fp.fileno() os.fstat(fn) diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index ba5e0810a..74ed9b7aa 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image @@ -10,6 +10,27 @@ class TestImageMode(PillowTestCase): im = lena() im.mode + from PIL import ImageMode + + ImageMode.getmode("1") + ImageMode.getmode("L") + ImageMode.getmode("P") + ImageMode.getmode("RGB") + ImageMode.getmode("I") + ImageMode.getmode("F") + + m = ImageMode.getmode("1") + self.assertEqual(m.mode, "1") + self.assertEqual(m.bands, ("1",)) + self.assertEqual(m.basemode, "L") + self.assertEqual(m.basetype, "L") + + m = ImageMode.getmode("RGB") + self.assertEqual(m.mode, "RGB") + self.assertEqual(m.bands, ("R", "G", "B")) + self.assertEqual(m.basemode, "RGB") + self.assertEqual(m.basetype, "L") + def test_properties(self): def check(mode, *result): signature = ( diff --git a/Tests/test_image_offset.py b/Tests/test_image_offset.py index 1b45fec4a..09f12266f 100644 --- a/Tests/test_image_offset.py +++ b/Tests/test_image_offset.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena class TestImageOffset(PillowTestCase): diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 1f3aaf446..04054fa84 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,16 +1,10 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena import sys class TestImagePoint(PillowTestCase): - def setUp(self): - if hasattr(sys, 'pypy_version_info'): - # This takes _forever_ on PyPy. Open Bug, - # see https://github.com/python-pillow/Pillow/issues/484 - self.skipTest("Too slow on PyPy") - def test_sanity(self): im = lena() @@ -29,11 +23,24 @@ class TestImagePoint(PillowTestCase): def test_16bit_lut(self): """ Tests for 16 bit -> 8 bit lut for converting I->L images see https://github.com/python-pillow/Pillow/issues/440 - """ + """ + # This takes _forever_ on PyPy. Open Bug, + # see https://github.com/python-pillow/Pillow/issues/484 + #self.skipKnownBadTest(msg="Too Slow on pypy", interpreter='pypy') im = lena("I") im.point(list(range(256))*256, 'L') + def test_f_lut(self): + """ Tests for floating point lut of 8bit gray image """ + im = lena('L') + lut = [0.5 * float(x) for x in range(256)] + + out = im.point(lut, 'F') + + int_lut = [x//2 for x in range(256)] + self.assert_image_equal(out.convert('L'), im.point(int_lut, 'L')) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index bb36b335e..85c7ac262 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index d792adfe6..acea0d62a 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena import sys @@ -42,6 +42,30 @@ class TestImagePutData(PillowTestCase): self.assertEqual(put(sys.maxsize), (255, 255, 255, 127)) + def test_pypy_performance(self): + im = Image.new('L', (256,256)) + im.putdata(list(range(256))*256) + + def test_mode_i(self): + src = lena('L') + data = list(src.getdata()) + im = Image.new('I', src.size, 0) + im.putdata(data, 2, 256) + + target = [2* elt + 256 for elt in data] + self.assertEqual(list(im.getdata()), target) + + def test_mode_F(self): + src = lena('L') + data = list(src.getdata()) + im = Image.new('F', src.size, 0) + im.putdata(data, 2.0, 256.0) + + target = [2.0* float(elt) + 256.0 for elt in data] + self.assertEqual(list(im.getdata()), target) + + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 26ad09800..a77c1e565 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import ImagePalette diff --git a/Tests/test_image_putpixel.py b/Tests/test_image_putpixel.py index 1afc013c0..a7f5dc2bb 100644 --- a/Tests/test_image_putpixel.py +++ b/Tests/test_image_putpixel.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 63fe0cb21..2cbdac225 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index a200b17b4..6c9932e45 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena class TestImageResize(PillowTestCase): diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index bb24ddf4f..531fdd63f 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena class TestImageRotate(PillowTestCase): diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 284acd87c..343f4bf8e 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6b33da318..ee49be43e 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena class TestImageThumbnail(PillowTestCase): diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 93f01c9de..56b5ef001 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena, fromstring +from helper import unittest, PillowTestCase, lena, fromstring class TestImageToBitmap(PillowTestCase): diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index 3be9128c1..6dbf7d6f2 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,7 +1,7 @@ -from helper import unittest, lena +from helper import unittest, PillowTestCase, lena -class TestImageToBytes(unittest.TestCase): +class TestImageToBytes(PillowTestCase): def test_sanity(self): data = lena().tobytes() diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index b36344416..42a3d78f3 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,10 +1,26 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image class TestImageTransform(PillowTestCase): + def test_sanity(self): + from PIL import ImageTransform + + im = Image.new("L", (100, 100)) + + seq = tuple(range(10)) + + transform = ImageTransform.AffineTransform(seq[:6]) + im.transform((100, 100), transform) + transform = ImageTransform.ExtentTransform(seq[:4]) + im.transform((100, 100), transform) + transform = ImageTransform.QuadTransform(seq[:8]) + im.transform((100, 100), transform) + transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) + im.transform((100, 100), transform) + def test_extent(self): im = lena('RGB') (w, h) = im.size diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index ec83aa3a6..f13e54ee7 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index fe377f864..552314fd1 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import ImageChops diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index f3f0791e5..e731c8945 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,9 +1,12 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image +from io import BytesIO + try: from PIL import ImageCms + from PIL.ImageCms import ImageCmsProfile ImageCms.core.profile_open except ImportError as v: # Skipped via setUp() @@ -118,7 +121,7 @@ class TestImageCms(PillowTestCase): def test_extensions(self): # extensions - from io import BytesIO + i = Image.open("Tests/images/rgb.jpg") p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) self.assertEqual( @@ -196,6 +199,11 @@ class TestImageCms(PillowTestCase): # img_srgb.save('temp.srgb.tif') # visually verified vs ps. self.assert_image_similar(lena(), img_srgb, 30) + self.assertTrue(img_srgb.info['icc_profile']) + + profile = ImageCmsProfile(BytesIO(img_srgb.info['icc_profile'])) + self.assertTrue('sRGB' in ImageCms.getProfileDescription(profile)) + def test_lab_roundtrip(self): # check to see if we're at least internally consistent. @@ -205,11 +213,32 @@ class TestImageCms(PillowTestCase): t2 = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB") i = ImageCms.applyTransform(lena(), t) + + self.assertEqual(i.info['icc_profile'], + ImageCmsProfile(pLab).tobytes()) + out = ImageCms.applyTransform(i, t2) self.assert_image_similar(lena(), out, 2) + def test_profile_tobytes(self): + from io import BytesIO + i = Image.open("Tests/images/rgb.jpg") + p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) + + p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) + + # not the same bytes as the original icc_profile, + # but it does roundtrip + self.assertEqual(p.tobytes(),p2.tobytes()) + self.assertEqual(ImageCms.getProfileName(p), + ImageCms.getProfileName(p2)) + self.assertEqual(ImageCms.getProfileDescription(p), + ImageCms.getProfileDescription(p2)) + + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index fce64876b..5d8944852 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageColor diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4610e2b0b..b632da73b 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import ImageColor diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index eec26d768..433c49cf6 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import ImageEnhance diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 849767195..78fb3427f 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena, fromstring, tostring +from helper import unittest, PillowTestCase, lena, fromstring, tostring from io import BytesIO @@ -14,7 +14,7 @@ MAXBLOCK = ImageFile.MAXBLOCK SAFEBLOCK = ImageFile.SAFEBLOCK -class TestImagePutData(PillowTestCase): +class TestImageFile(PillowTestCase): def test_parser(self): diff --git a/Tests/test_imagefileio.py b/Tests/test_imagefileio.py index 791207dca..32ee0bc5e 100644 --- a/Tests/test_imagefileio.py +++ b/Tests/test_imagefileio.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena, tostring +from helper import unittest, PillowTestCase, lena, tostring from PIL import Image from PIL import ImageFileIO diff --git a/Tests/test_imagefilter.py b/Tests/test_imagefilter.py deleted file mode 100644 index 3dcb1d14f..000000000 --- a/Tests/test_imagefilter.py +++ /dev/null @@ -1,37 +0,0 @@ -from helper import unittest, PillowTestCase, tearDownModule - -from PIL import ImageFilter - - -class TestImageFilter(PillowTestCase): - - def test_sanity(self): - # see test_image_filter for more tests - - # Check these run. Exceptions cause failures. - ImageFilter.MaxFilter - ImageFilter.MedianFilter - ImageFilter.MinFilter - ImageFilter.ModeFilter - ImageFilter.Kernel((3, 3), list(range(9))) - ImageFilter.GaussianBlur - ImageFilter.GaussianBlur(5) - ImageFilter.UnsharpMask - ImageFilter.UnsharpMask(10) - - ImageFilter.BLUR - ImageFilter.CONTOUR - ImageFilter.DETAIL - ImageFilter.EDGE_ENHANCE - ImageFilter.EDGE_ENHANCE_MORE - ImageFilter.EMBOSS - ImageFilter.FIND_EDGES - ImageFilter.SMOOTH - ImageFilter.SMOOTH_MORE - ImageFilter.SHARPEN - - -if __name__ == '__main__': - unittest.main() - -# End of file diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 927c80bee..ed2439e7c 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,12 +1,12 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageDraw from io import BytesIO import os -font_path = "Tests/fonts/FreeMono.ttf" -font_size = 20 +FONT_PATH = "Tests/fonts/FreeMono.ttf" +FONT_SIZE = 20 try: @@ -20,17 +20,17 @@ try: ImageFont.core.freetype2_version, "\d+\.\d+\.\d+$") def test_font_with_name(self): - ImageFont.truetype(font_path, font_size) - self._render(font_path) + ImageFont.truetype(FONT_PATH, FONT_SIZE) + self._render(FONT_PATH) self._clean() def _font_as_bytes(self): - with open(font_path, 'rb') as f: + with open(FONT_PATH, 'rb') as f: font_bytes = BytesIO(f.read()) return font_bytes def test_font_with_filelike(self): - ImageFont.truetype(self._font_as_bytes(), font_size) + ImageFont.truetype(self._font_as_bytes(), FONT_SIZE) self._render(self._font_as_bytes()) # Usage note: making two fonts from the same buffer fails. # shared_bytes = self._font_as_bytes() @@ -39,18 +39,18 @@ try: self._clean() def test_font_with_open_file(self): - with open(font_path, 'rb') as f: + with open(FONT_PATH, 'rb') as f: self._render(f) self._clean() def test_font_old_parameters(self): self.assert_warning( DeprecationWarning, - lambda: ImageFont.truetype(filename=font_path, size=font_size)) + lambda: ImageFont.truetype(filename=FONT_PATH, size=FONT_SIZE)) def _render(self, font): txt = "Hello World!" - ttf = ImageFont.truetype(font, font_size) + ttf = ImageFont.truetype(font, FONT_SIZE) w, h = ttf.getsize(txt) img = Image.new("RGB", (256, 64), "white") d = ImageDraw.Draw(img) @@ -63,19 +63,33 @@ try: os.unlink('font.png') def test_render_equal(self): - img_path = self._render(font_path) - with open(font_path, 'rb') as f: + img_path = self._render(FONT_PATH) + with open(FONT_PATH, 'rb') as f: font_filelike = BytesIO(f.read()) img_filelike = self._render(font_filelike) self.assert_image_equal(img_path, img_filelike) self._clean() + def test_textsize_equal(self): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + txt = "Hello World!" + size = draw.textsize(txt, ttf) + draw.text((10, 10), txt, font=ttf) + draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) + + target = 'Tests/images/rectangle_surrounding_text.png' + target_img = Image.open(target) + self.assert_image_equal(im, target_img) + def test_render_multiline(self): im = Image.new(mode='RGB', size=(300, 100)) draw = ImageDraw.Draw(im) - ttf = ImageFont.truetype(font_path, font_size) - line_spacing = draw.textsize('A', font=ttf)[1] + 8 + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + line_spacing = draw.textsize('A', font=ttf)[1] + 4 lines = ['hey you', 'you are awesome', 'this looks awkward'] y = 0 for line in lines: @@ -94,7 +108,7 @@ try: img_grey = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_grey) word = "testing" - font = ImageFont.truetype(font_path, font_size) + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) orientation = Image.ROTATE_90 transposed_font = ImageFont.TransposedFont( @@ -116,7 +130,7 @@ try: img_grey = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_grey) word = "testing" - font = ImageFont.truetype(font_path, font_size) + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) orientation = None transposed_font = ImageFont.TransposedFont( @@ -133,6 +147,52 @@ try: # Check boxes a and b are same size self.assertEqual(box_size_a, box_size_b) + def test_free_type_font_get_name(self): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act + name = font.getname() + + # Assert + self.assertEqual(('FreeMono', 'Regular'), name) + + def test_free_type_font_get_metrics(self): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act + ascent, descent = font.getmetrics() + + # Assert + self.assertIsInstance(ascent, int) + self.assertIsInstance(descent, int) + self.assertEqual((ascent, descent), (16, 4)) # too exact check? + + def test_load_path_not_found(self): + # Arrange + filename = "somefilenamethatdoesntexist.ttf" + + # Act/Assert + self.assertRaises(IOError, lambda: ImageFont.load_path(filename)) + + def test_default_font(self): + # Arrange + txt = 'This is a "better than nothing" default font.' + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + + target = 'Tests/images/default_font.png' + target_img = Image.open(target) + + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) + + # Assert + self.assert_image_equal(im, target_img) + + except ImportError: class TestImageFont(PillowTestCase): def test_skip(self): diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index a6c50fb31..13affe6b9 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,9 +1,9 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase try: from PIL import ImageGrab - class TestImageCopy(PillowTestCase): + class TestImageGrab(PillowTestCase): def test_grab(self): im = ImageGrab.grab() @@ -14,7 +14,7 @@ try: self.assert_image(im, im.mode, im.size) except ImportError: - class TestImageCopy(PillowTestCase): + class TestImageGrab(PillowTestCase): def test_skip(self): self.skipTest("ImportError") diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 35d75dbbd..5d87c0229 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageMath @@ -14,9 +14,13 @@ def pixel(im): A = Image.new("L", (1, 1), 1) B = Image.new("L", (1, 1), 2) +Z = Image.new("L", (1, 1), 0) # Z for zero F = Image.new("F", (1, 1), 3) I = Image.new("I", (1, 1), 4) +A2 = A.resize((2, 2)) +B2 = B.resize((2, 2)) + images = {"A": A, "B": B, "F": F, "I": I} @@ -71,6 +75,112 @@ class TestImageMath(PillowTestCase): self.assertEqual(pixel(ImageMath.eval("A == 1", images)), "I 1") self.assertEqual(pixel(ImageMath.eval("A == 2", images)), "I 0") + def test_one_image_larger(self): + self.assertEqual(pixel(ImageMath.eval("A+B", A=A2, B=B)), "I 3") + self.assertEqual(pixel(ImageMath.eval("A+B", A=A, B=B2)), "I 3") + + def test_abs(self): + self.assertEqual(pixel(ImageMath.eval("abs(A)", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("abs(B)", B=B)), "I 2") + + def test_binary_mod(self): + self.assertEqual(pixel(ImageMath.eval("A%A", A=A)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B%B", B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A%B", A=A, B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("B%A", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z%A", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z%B", B=B, Z=Z)), "I 0") + + def test_bitwise_invert(self): + self.assertEqual(pixel(ImageMath.eval("~Z", Z=Z)), "I -1") + self.assertEqual(pixel(ImageMath.eval("~A", A=A)), "I -2") + self.assertEqual(pixel(ImageMath.eval("~B", B=B)), "I -3") + + def test_bitwise_and(self): + self.assertEqual(pixel(ImageMath.eval("Z&Z", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z&A", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A&Z", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A&A", A=A, Z=Z)), "I 1") + + def test_bitwise_or(self): + self.assertEqual(pixel(ImageMath.eval("Z|Z", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z|A", A=A, Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A|Z", A=A, Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A|A", A=A, Z=Z)), "I 1") + + def test_bitwise_xor(self): + self.assertEqual(pixel(ImageMath.eval("Z^Z", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z^A", A=A, Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A^Z", A=A, Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A^A", A=A, Z=Z)), "I 0") + + def test_bitwise_leftshift(self): + self.assertEqual(pixel(ImageMath.eval("Z<<0", Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z<<1", Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A<<0", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A<<1", A=A)), "I 2") + + def test_bitwise_rightshift(self): + self.assertEqual(pixel(ImageMath.eval("Z>>0", Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z>>1", Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A>>0", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A>>1", A=A)), "I 0") + + def test_logical_eq(self): + self.assertEqual(pixel(ImageMath.eval("A==A", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("B==B", B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A==B", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B==A", A=A, B=B)), "I 0") + + def test_logical_ne(self): + self.assertEqual(pixel(ImageMath.eval("A!=A", A=A)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B!=B", B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A!=B", A=A, B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("B!=A", A=A, B=B)), "I 1") + + def test_logical_lt(self): + self.assertEqual(pixel(ImageMath.eval("AA", A=A)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B>B", B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A>B", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B>A", A=A, B=B)), "I 1") + + def test_logical_ge(self): + self.assertEqual(pixel(ImageMath.eval("A>=A", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("B>=B", B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A>=B", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B>=A", A=A, B=B)), "I 1") + + def test_logical_equal(self): + self.assertEqual(pixel(ImageMath.eval("equal(A, A)", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("equal(B, B)", B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("equal(Z, Z)", Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("equal(A, B)", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("equal(B, A)", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)), "I 0") + + def test_logical_not_equal(self): + self.assertEqual(pixel(ImageMath.eval("notequal(A, A)", A=A)), "I 0") + self.assertEqual(pixel(ImageMath.eval("notequal(B, B)", B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)), "I 0") + self.assertEqual( + pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)), "I 1") + self.assertEqual( + pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)), "I 1") + self.assertEqual( + pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)), "I 1") + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagemode.py b/Tests/test_imagemode.py deleted file mode 100644 index 7febc697e..000000000 --- a/Tests/test_imagemode.py +++ /dev/null @@ -1,32 +0,0 @@ -from helper import unittest, PillowTestCase, tearDownModule - -from PIL import ImageMode - - -class TestImageMode(PillowTestCase): - - def test_sanity(self): - ImageMode.getmode("1") - ImageMode.getmode("L") - ImageMode.getmode("P") - ImageMode.getmode("RGB") - ImageMode.getmode("I") - ImageMode.getmode("F") - - m = ImageMode.getmode("1") - self.assertEqual(m.mode, "1") - self.assertEqual(m.bands, ("1",)) - self.assertEqual(m.basemode, "L") - self.assertEqual(m.basetype, "L") - - m = ImageMode.getmode("RGB") - self.assertEqual(m.mode, "RGB") - self.assertEqual(m.bands, ("R", "G", "B")) - self.assertEqual(m.basemode, "RGB") - self.assertEqual(m.basetype, "L") - - -if __name__ == '__main__': - unittest.main() - -# End of file diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 299a7c618..a4a94ca4d 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import ImageOps diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 486b201ab..be7a669da 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageOps diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 3ee7ee869..e56c61390 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import ImagePalette @@ -44,6 +44,109 @@ class TestImagePalette(PillowTestCase): self.assertIsInstance(p, ImagePalette) self.assertEqual(p.palette, palette.tobytes()) + def test_make_linear_lut(self): + # Arrange + from PIL.ImagePalette import make_linear_lut + black = 0 + white = 255 + + # Act + lut = make_linear_lut(black, white) + + # Assert + self.assertIsInstance(lut, list) + self.assertEqual(len(lut), 256) + # Check values + for i in range(0, len(lut)): + self.assertEqual(lut[i], i) + + def test_make_linear_lut_not_yet_implemented(self): + # Update after FIXME + # Arrange + from PIL.ImagePalette import make_linear_lut + black = 1 + white = 255 + + # Act + self.assertRaises( + NotImplementedError, + lambda: make_linear_lut(black, white)) + + def test_make_gamma_lut(self): + # Arrange + from PIL.ImagePalette import make_gamma_lut + exp = 5 + + # Act + lut = make_gamma_lut(exp) + + # Assert + self.assertIsInstance(lut, list) + self.assertEqual(len(lut), 256) + # Check a few values + self.assertEqual(lut[0], 0) + self.assertEqual(lut[63], 0) + self.assertEqual(lut[127], 8) + self.assertEqual(lut[191], 60) + self.assertEqual(lut[255], 255) + + def test_private_make_linear_lut_warning(self): + # Arrange + from PIL.ImagePalette import _make_linear_lut + black = 0 + white = 255 + + # Act / Assert + self.assert_warning( + DeprecationWarning, + lambda: _make_linear_lut(black, white)) + + def test_private_make_gamma_lut_warning(self): + # Arrange + from PIL.ImagePalette import _make_gamma_lut + exp = 5 + + # Act / Assert + self.assert_warning( + DeprecationWarning, + lambda: _make_gamma_lut(exp)) + + def test_rawmode_valueerrors(self): + # Arrange + from PIL.ImagePalette import raw + palette = raw("RGB", list(range(256))*3) + + # Act / Assert + self.assertRaises(ValueError, lambda: palette.tobytes()) + self.assertRaises(ValueError, lambda: palette.getcolor((1, 2, 3))) + f = self.tempfile("temp.lut") + self.assertRaises(ValueError, lambda: palette.save(f)) + + def test_getdata(self): + # Arrange + data_in = list(range(256))*3 + palette = ImagePalette("RGB", data_in) + + # Act + mode, data_out = palette.getdata() + + # Assert + self.assertEqual(mode, "RGB;L") + + def test_rawmode_getdata(self): + # Arrange + from PIL.ImagePalette import raw + data_in = list(range(256))*3 + palette = raw("RGB", data_in) + + # Act + rawmode, data_out = palette.getdata() + + # Assert + self.assertEqual(rawmode, "RGB") + self.assertEqual(data_in, data_out) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index c293e4225..cd221b5ca 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import ImagePath diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 549fc7fd6..fd50bf320 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena try: from PIL import ImageQt diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 7f8838207..068290451 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,6 +1,6 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena -from PIL import ImageSequence +from PIL import Image, ImageSequence, TiffImagePlugin class TestImageSequence(PillowTestCase): @@ -22,7 +22,31 @@ class TestImageSequence(PillowTestCase): self.assertEqual(index, 1) + def _test_multipage_tiff(self, dbg=False): + # debug had side effect of calling fp.tell. + Image.DEBUG=dbg + im = Image.open('Tests/images/multipage.tiff') + for index, frame in enumerate(ImageSequence.Iterator(im)): + frame.load() + self.assertEqual(index, im.tell()) + frame.convert('RGB') + Image.DEBUG=False + + def test_tiff(self): + #self._test_multipage_tiff(True) + self._test_multipage_tiff(False) + def test_libtiff(self): + codecs = dir(Image.core) + + if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs: + self.skipTest("tiff support not available") + + TiffImagePlugin.READ_LIBTIFF = True + #self._test_multipage_tiff(True) + self._test_multipage_tiff(False) + TiffImagePlugin.READ_LIBTIFF = False + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 08b3ff183..e94ae2d0a 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageShow diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 7eded56cf..4d30ff023 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import ImageStat diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index b868096b2..87a07e288 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase class TestImageTk(PillowTestCase): diff --git a/Tests/test_imagetransform.py b/Tests/test_imagetransform.py deleted file mode 100644 index dfffafe54..000000000 --- a/Tests/test_imagetransform.py +++ /dev/null @@ -1,27 +0,0 @@ -from helper import unittest, PillowTestCase, tearDownModule - -from PIL import Image -from PIL import ImageTransform - - -class TestImageTransform(PillowTestCase): - - def test_sanity(self): - im = Image.new("L", (100, 100)) - - seq = tuple(range(10)) - - transform = ImageTransform.AffineTransform(seq[:6]) - im.transform((100, 100), transform) - transform = ImageTransform.ExtentTransform(seq[:4]) - im.transform((100, 100), transform) - transform = ImageTransform.QuadTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) - im.transform((100, 100), transform) - - -if __name__ == '__main__': - unittest.main() - -# End of file diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 89e131113..0ff22601e 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import ImageWin diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index c7ea4c701..1a16deff2 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,9 +1,9 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image -class TestSanity(PillowTestCase): +class TestLibImage(PillowTestCase): def test_setmode(self): diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index c8ed39c40..102835b58 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, py3 +from helper import unittest, PillowTestCase, py3 from PIL import Image diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 0465fb207..9ef136bf9 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index d8e205b66..0cd5dba0f 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,22 +1,26 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image class TestModeI16(PillowTestCase): + original = lena().resize((32,32)).convert('I') + def verify(self, im1): - im2 = lena("I") + im2 = self.original.copy() self.assertEqual(im1.size, im2.size) pix1 = im1.load() pix2 = im2.load() for y in range(im1.size[1]): for x in range(im1.size[0]): xy = x, y + p1 = pix1[xy] + p2 = pix2[xy] self.assertEqual( - pix1[xy], pix2[xy], + p1, p2, ("got %r from mode %s at %s, expected %r" % - (pix1[xy], im1.mode, xy, pix2[xy]))) + (p1, im1.mode, xy, p2))) def test_basic(self): # PIL 1.1 has limited support for 16-bit image data. Check that @@ -24,7 +28,7 @@ class TestModeI16(PillowTestCase): def basic(mode): - imIn = lena("I").convert(mode) + imIn = self.original.convert(mode) self.verify(imIn) w, h = imIn.size @@ -92,7 +96,7 @@ class TestModeI16(PillowTestCase): def test_convert(self): - im = lena("I") + im = self.original.copy() self.verify(im.convert("I;16")) self.verify(im.convert("I;16").convert("L")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index c3c0f7e90..07c3e0c21 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule, lena +from helper import unittest, PillowTestCase, lena from PIL import Image diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py index f31302db1..1cff273a1 100644 --- a/Tests/test_olefileio.py +++ b/Tests/test_olefileio.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase import datetime diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 304baf964..eae5eb671 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c10156cc0..295ef1057 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -7,7 +7,7 @@ except ImportError: pass -class TestPyroma(unittest.TestCase): +class TestPyroma(PillowTestCase): def setUp(self): try: diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index eff03fd59..d6d08c2d4 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, tearDownModule +from helper import unittest, PillowTestCase from helper import djpeg_available, cjpeg_available, netpbm_available import sys @@ -6,8 +6,8 @@ import shutil from PIL import Image, JpegImagePlugin, GifImagePlugin -test_jpg = "Tests/images/lena.jpg" -test_gif = "Tests/images/lena.gif" +TEST_JPG = "Tests/images/hopper.jpg" +TEST_GIF = "Tests/images/hopper.gif" test_filenames = ( "temp_';", @@ -31,24 +31,24 @@ class TestShellInjection(PillowTestCase): def test_load_djpeg_filename(self): for filename in test_filenames: src_file = self.tempfile(filename) - shutil.copy(test_jpg, src_file) + 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) + 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") + 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") + im = Image.open(TEST_GIF).convert("L") self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_util.py b/Tests/test_util.py new file mode 100644 index 000000000..a547c6bd6 --- /dev/null +++ b/Tests/test_util.py @@ -0,0 +1,81 @@ +from helper import unittest, PillowTestCase + +from PIL import _util + + +class TestUtil(PillowTestCase): + + def test_is_string_type(self): + # Arrange + color = "red" + + # Act + it_is = _util.isStringType(color) + + # Assert + self.assertTrue(it_is) + + def test_is_not_string_type(self): + # Arrange + color = (255, 0, 0) + + # Act + it_is_not = _util.isStringType(color) + + # Assert + self.assertFalse(it_is_not) + + def test_is_path(self): + # Arrange + fp = "filename.ext" + + # Act + it_is = _util.isStringType(fp) + + # Assert + self.assertTrue(it_is) + + def test_is_not_path(self): + # Arrange + filename = self.tempfile("temp.ext") + fp = open(filename, 'w').close() + + # Act + it_is_not = _util.isPath(fp) + + # Assert + self.assertFalse(it_is_not) + + def test_is_directory(self): + # Arrange + directory = "Tests" + + # Act + it_is = _util.isDirectory(directory) + + # Assert + self.assertTrue(it_is) + + def test_is_not_directory(self): + # Arrange + text = "abc" + + # Act + it_is_not = _util.isDirectory(text) + + # Assert + self.assertFalse(it_is_not) + + def test_deferred_error(self): + # Arrange + + # Act + thing = _util.deferred_error(ValueError("Some error text")) + + # Assert + self.assertRaises(ValueError, lambda: thing.some_attr) + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/_imaging.c b/_imaging.c index 92258032f..ec8205dd4 100644 --- a/_imaging.c +++ b/_imaging.c @@ -71,7 +71,7 @@ * See the README file for information on usage and redistribution. */ -#define PILLOW_VERSION "2.5.0" +#define PILLOW_VERSION "2.5.3" #include "Python.h" @@ -365,9 +365,12 @@ getbands(const char* mode) static void* getlist(PyObject* arg, int* length, const char* wrong_length, int type) { - int i, n; + int i, n, itemp; + double dtemp; void* list; - + PyObject* seq; + PyObject* op; + if (!PySequence_Check(arg)) { PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; @@ -383,71 +386,35 @@ getlist(PyObject* arg, int* length, const char* wrong_length, int type) if (!list) return PyErr_NoMemory(); - switch (type) { - case TYPE_UINT8: - if (PyList_Check(arg)) { - for (i = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(arg, i); - int temp = PyInt_AsLong(op); - ((UINT8*)list)[i] = CLIP(temp); - } - } else { - for (i = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(arg, i); - int temp = PyInt_AsLong(op); - Py_XDECREF(op); - ((UINT8*)list)[i] = CLIP(temp); - } + seq = PySequence_Fast(arg, must_be_sequence); + if (!seq) { + free(list); + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + + for (i = 0; i < n; i++) { + op = PySequence_Fast_GET_ITEM(seq, i); + // DRY, branch prediction is going to work _really_ well + // on this switch. And 3 fewer loops to copy/paste. + switch (type) { + case TYPE_UINT8: + itemp = PyInt_AsLong(op); + ((UINT8*)list)[i] = CLIP(itemp); + break; + case TYPE_INT32: + itemp = PyInt_AsLong(op); + ((INT32*)list)[i] = itemp; + break; + case TYPE_FLOAT32: + dtemp = PyFloat_AsDouble(op); + ((FLOAT32*)list)[i] = (FLOAT32) dtemp; + break; + case TYPE_DOUBLE: + dtemp = PyFloat_AsDouble(op); + ((double*)list)[i] = (double) dtemp; + break; } - break; - case TYPE_INT32: - if (PyList_Check(arg)) { - for (i = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(arg, i); - int temp = PyInt_AsLong(op); - ((INT32*)list)[i] = temp; - } - } else { - for (i = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(arg, i); - int temp = PyInt_AsLong(op); - Py_XDECREF(op); - ((INT32*)list)[i] = temp; - } - } - break; - case TYPE_FLOAT32: - if (PyList_Check(arg)) { - for (i = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(arg, i); - double temp = PyFloat_AsDouble(op); - ((FLOAT32*)list)[i] = (FLOAT32) temp; - } - } else { - for (i = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(arg, i); - double temp = PyFloat_AsDouble(op); - Py_XDECREF(op); - ((FLOAT32*)list)[i] = (FLOAT32) temp; - } - } - break; - case TYPE_DOUBLE: - if (PyList_Check(arg)) { - for (i = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(arg, i); - double temp = PyFloat_AsDouble(op); - ((double*)list)[i] = temp; - } - } else { - for (i = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(arg, i); - double temp = PyFloat_AsDouble(op); - Py_XDECREF(op); - ((double*)list)[i] = temp; - } - } - break; } if (length) @@ -1253,6 +1220,8 @@ _putdata(ImagingObject* self, PyObject* args) Py_ssize_t n, i, x, y; PyObject* data; + PyObject* seq; + PyObject* op; double scale = 1.0; double offset = 0.0; @@ -1292,69 +1261,61 @@ _putdata(ImagingObject* self, PyObject* args) x = 0, y++; } } else { - if (scale == 1.0 && offset == 0.0) { - /* Clipped data */ - if (PyList_Check(data)) { - for (i = x = y = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(data, i); - image->image8[y][x] = (UINT8) CLIP(PyInt_AsLong(op)); - if (++x >= (int) image->xsize) - x = 0, y++; - } - } else { - for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(data, i); - image->image8[y][x] = (UINT8) CLIP(PyInt_AsLong(op)); - Py_XDECREF(op); - if (++x >= (int) image->xsize) - x = 0, y++; - } - } + seq = PySequence_Fast(data, must_be_sequence); + if (!seq) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + if (scale == 1.0 && offset == 0.0) { + /* Clipped data */ + for (i = x = y = 0; i < n; i++) { + op = PySequence_Fast_GET_ITEM(data, i); + image->image8[y][x] = (UINT8) CLIP(PyInt_AsLong(op)); + if (++x >= (int) image->xsize){ + x = 0, y++; + } + } + } else { - if (PyList_Check(data)) { - /* Scaled and clipped data */ - for (i = x = y = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(data, i); - image->image8[y][x] = CLIP( - (int) (PyFloat_AsDouble(op) * scale + offset)); - if (++x >= (int) image->xsize) - x = 0, y++; - } - } else { - for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(data, i); - image->image8[y][x] = CLIP( - (int) (PyFloat_AsDouble(op) * scale + offset)); - Py_XDECREF(op); - if (++x >= (int) image->xsize) - x = 0, y++; - } - } - } - PyErr_Clear(); /* Avoid weird exceptions */ + /* Scaled and clipped data */ + for (i = x = y = 0; i < n; i++) { + PyObject *op = PySequence_Fast_GET_ITEM(data, i); + image->image8[y][x] = CLIP( + (int) (PyFloat_AsDouble(op) * scale + offset)); + if (++x >= (int) image->xsize){ + x = 0, y++; + } + } + } + PyErr_Clear(); /* Avoid weird exceptions */ } } else { /* 32-bit images */ + seq = PySequence_Fast(data, must_be_sequence); + if (!seq) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } switch (image->type) { case IMAGING_TYPE_INT32: for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(data, i); + op = PySequence_Fast_GET_ITEM(data, i); IMAGING_PIXEL_INT32(image, x, y) = (INT32) (PyFloat_AsDouble(op) * scale + offset); - Py_XDECREF(op); - if (++x >= (int) image->xsize) + if (++x >= (int) image->xsize){ x = 0, y++; + } } PyErr_Clear(); /* Avoid weird exceptions */ break; case IMAGING_TYPE_FLOAT32: for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(data, i); + op = PySequence_Fast_GET_ITEM(data, i); IMAGING_PIXEL_FLOAT32(image, x, y) = (FLOAT32) (PyFloat_AsDouble(op) * scale + offset); - Py_XDECREF(op); - if (++x >= (int) image->xsize) + if (++x >= (int) image->xsize){ x = 0, y++; + } } PyErr_Clear(); /* Avoid weird exceptions */ break; @@ -1365,16 +1326,15 @@ _putdata(ImagingObject* self, PyObject* args) INT32 inkint; } u; - PyObject *op = PySequence_GetItem(data, i); + op = PySequence_Fast_GET_ITEM(data, i); if (!op || !getink(op, image, u.ink)) { - Py_DECREF(op); return NULL; } /* FIXME: what about scale and offset? */ image->image32[y][x] = u.inkint; - Py_XDECREF(op); - if (++x >= (int) image->xsize) + if (++x >= (int) image->xsize){ x = 0, y++; + } } PyErr_Clear(); /* Avoid weird exceptions */ break; diff --git a/_imagingcms.c b/_imagingcms.c index df26e1a2d..3b822006a 100644 --- a/_imagingcms.c +++ b/_imagingcms.c @@ -6,6 +6,8 @@ * http://www.cazabon.com * Adapted/reworked for PIL by Fredrik Lundh * Copyright (c) 2009 Fredrik Lundh + * Updated to LCMS2 + * Copyright (c) 2013 Eric Soroos * * pyCMS home page: http://www.cazabon.com/pyCMS * littleCMS home page: http://www.littlecms.com @@ -47,7 +49,8 @@ http://www.cazabon.com\n\ /* known to-do list with current version: - Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all PIL modes (it probably isn't for the more obscure ones) + Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all + PIL modes (it probably isn't for the more obscure ones) Add support for creating custom RGB profiles on the fly Add support for checking presence of a specific tag in a profile @@ -132,6 +135,49 @@ cms_profile_fromstring(PyObject* self, PyObject* args) return cms_profile_new(hProfile); } +static PyObject* +cms_profile_tobytes(PyObject* self, PyObject* args) +{ + char *pProfile =NULL; + cmsUInt32Number nProfile; + PyObject* CmsProfile; + + cmsHPROFILE *profile; + + PyObject* ret; + if (!PyArg_ParseTuple(args, "O", &CmsProfile)){ + return NULL; + } + + profile = ((CmsProfileObject*)CmsProfile)->profile; + + if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { + PyErr_SetString(PyExc_IOError, "Could not determine profile size"); + return NULL; + } + + pProfile = (char*)malloc(nProfile); + if (!pProfile) { + PyErr_SetString(PyExc_IOError, "Out of Memory"); + return NULL; + } + + if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { + PyErr_SetString(PyExc_IOError, "Could not get profile"); + free(pProfile); + return NULL; + } + +#if PY_VERSION_HEX >= 0x03000000 + ret = PyBytes_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); +#else + ret = PyString_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); +#endif + + free(pProfile); + return ret; +} + static void cms_profile_dealloc(CmsProfileObject* self) { @@ -483,6 +529,7 @@ static PyMethodDef pyCMSdll_methods[] = { {"profile_open", cms_profile_open, 1}, {"profile_frombytes", cms_profile_fromstring, 1}, {"profile_fromstring", cms_profile_fromstring, 1}, + {"profile_tobytes", cms_profile_tobytes, 1}, /* profile and transform functions */ {"buildTransform", buildTransform, 1}, diff --git a/decode.c b/decode.c index d5e329384..c56f42592 100644 --- a/decode.c +++ b/decode.c @@ -103,6 +103,8 @@ PyImaging_DecoderNew(int contextsize) static void _dealloc(ImagingDecoderObject* decoder) { + if (decoder->cleanup) + decoder->cleanup(&decoder->state); free(decoder->state.buffer); free(decoder->state.context); Py_XDECREF(decoder->lock); @@ -442,8 +444,9 @@ PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args) char* rawmode; char* compname; int fp; + int ifdoffset; - if (! PyArg_ParseTuple(args, "sssi", &mode, &rawmode, &compname, &fp)) + if (! PyArg_ParseTuple(args, "sssii", &mode, &rawmode, &compname, &fp, &ifdoffset)) return NULL; TRACE(("new tiff decoder %s\n", compname)); @@ -455,7 +458,7 @@ PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args) if (get_unpacker(decoder, mode, rawmode) < 0) return NULL; - if (! ImagingLibTiffInit(&decoder->state, fp)) { + if (! ImagingLibTiffInit(&decoder->state, fp, ifdoffset)) { Py_DECREF(decoder); PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); return NULL; diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index b3a6ccc4d..83620f4b8 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -11,9 +11,9 @@ rm -r openjpeg-2.1.0 tar -xvzf openjpeg-2.1.0.tar.gz -pushd openjpeg-2.1.0 +pushd openjpeg-2.1.0 -cmake -DCMAKE_INSTALL_PREFIX=/usr . && make && sudo make install +cmake -DCMAKE_INSTALL_PREFIX=/usr . && make -j4 && sudo make -j4 install popd diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 5f5963712..7810159cc 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,18 +1,15 @@ #!/bin/bash # install webp - -if [ ! -f libwebp-0.4.0.tar.gz ]; then - wget 'https://webp.googlecode.com/files/libwebp-0.4.0.tar.gz' +if [ ! -f libwebp-0.4.1.tar.gz ]; then + wget 'http://downloads.webmproject.org/releases/webp/libwebp-0.4.1.tar.gz' fi -rm -r libwebp-0.4.0 -tar -xvzf libwebp-0.4.0.tar.gz +rm -r libwebp-0.4.1 +tar -xvzf libwebp-0.4.1.tar.gz +pushd libwebp-0.4.1 -pushd libwebp-0.4.0 - -./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make && sudo make install +./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install popd - diff --git a/docs/PIL.rst b/docs/PIL.rst index 6726f661f..8bf89c685 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -20,14 +20,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`ExifTags` Module ----------------------- - -.. automodule:: PIL.ExifTags - :members: - :undoc-members: - :show-inheritance: - :mod:`FontFile` Module ---------------------- @@ -60,15 +52,8 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`ImageCms` Module ----------------------- - -.. automodule:: PIL.ImageCms - :members: - :undoc-members: - :show-inheritance: - .. intentionally skipped documenting this because it's not documented anywhere + :mod:`ImageDraw2` Module ------------------------ @@ -78,6 +63,7 @@ can be found here. :show-inheritance: .. intentionally skipped documenting this because it's deprecated + :mod:`ImageFileIO` Module ------------------------- @@ -86,6 +72,7 @@ can be found here. :undoc-members: :show-inheritance: + :mod:`ImageShow` Module ----------------------- @@ -110,14 +97,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`OleFileIO` Module ------------------------ - -.. automodule:: PIL.OleFileIO - :members: - :undoc-members: - :show-inheritance: - :mod:`PaletteFile` Module ------------------------- diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 93f964e41..b5e5e44c1 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -27,6 +27,8 @@ image. The current release supports the following standard modes: * ``RGBA`` (4x8-bit pixels, true color with transparency mask) * ``CMYK`` (4x8-bit pixels, color separation) * ``YCbCr`` (3x8-bit pixels, color video format) + * ``LAB`` (3x8-bit pixels, the L*a*b color space) + * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) * ``I`` (32-bit signed integer pixels) * ``F`` (32-bit floating point pixels) @@ -34,7 +36,7 @@ PIL also provides limited support for a few special modes, including ``LA`` (L with alpha), ``RGBX`` (true color with padding) and ``RGBa`` (true color with premultiplied alpha). However, PIL doesn’t support user-defined modes; if you to handle band combinations that are not listed above, use a sequence of Image -objects. +objects. You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` attribute. This is a string containing one of the above values. diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 03e55f35a..cd187a4a2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -136,7 +136,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **quality** The image quality, on a scale from 1 (worst) to 95 (best). The default is 75. Values above 95 should be avoided; 100 disables portions of the JPEG - compression algorithm, and results in large files with hardly any gain in = + compression algorithm, and results in large files with hardly any gain in image quality. **optimize** @@ -588,6 +588,20 @@ PIL identifies and reads Microsoft Image Composer (MIC) files. When opened, the first sprite in the file is loaded. You can use :py:meth:`~file.seek` and :py:meth:`~file.tell` to read other sprites from the file. +MPO +^^^ + +Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary +image when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell` +methods may be used to read other pictures from the file. The pictures are +zero-indexed and random access is supported. + +MIC (read only) + +Pillow identifies and reads Microsoft Image Composer (MIC) files. When opened, the +first sprite in the file is loaded. You can use :py:meth:`~file.seek` and +:py:meth:`~file.tell` to read other sprites from the file. + PCD ^^^ @@ -656,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/docs/installation.rst b/docs/installation.rst index 8abbd6679..a61213e15 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -79,15 +79,21 @@ Many of Pillow's features require external libraries: * Pillow has been tested with openjpeg **2.0.0** and **2.1.0**. -If the prerequisites are installed in the standard library locations for your -machine (e.g. :file:`/usr` or :file:`/usr/local`), no additional configuration -should be required. If they are installed in a non-standard location, you may -need to configure setuptools to use those locations (i.e. by editing -:file:`setup.py` and/or :file:`setup.cfg`). Once you have installed the -prerequisites, run:: +Once you have installed the prerequisites,run:: $ pip install Pillow +If the prerequisites are installed in the standard library locations +for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no +additional configuration should be required. If they are installed in +a non-standard location, you may need to configure setuptools to use +those locations by editing :file:`setup.py` or +:file:`setup.cfg`, or by adding environment variables on the command +line:: + + $ CFLAGS="-I/usr/pkg/include" pip install pillow + + Linux installation ------------------ diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst new file mode 100644 index 000000000..9fc7cd13b --- /dev/null +++ b/docs/reference/ExifTags.rst @@ -0,0 +1,26 @@ +.. py:module:: PIL.ExifTags +.. py:currentmodule:: PIL.ExifTags + +:py:mod:`ExifTags` Module +========================== + +The :py:mod:`ExifTags` module exposes two dictionaries which +provide constants and clear-text names for various well-known EXIF tags. + +.. py:class:: PIL.ExifTags.TAGS + + The TAG dictionary maps 16-bit integer EXIF tag enumerations to + descriptive string names. For instance: + + >>> from PIL.ExifTags import TAGS + >>> TAGS[0x010e] + 'ImageDescription' + +.. py:class:: PIL.ExifTags.GPSTAGS + + The GPSTAGS dictionary maps 8-bit integer EXIF gps enumerations to + descriptive string names. For instance: + + >>> from PIL.ExifTags import GPSTAGS + >>> GPSTAGS[20] + 'GPSDestLatitude' diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index bf64c835d..11666dd0b 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -49,7 +49,10 @@ Functions .. autofunction:: open - .. warning:: > To protect against potential DOS attacks caused by "`decompression bombs`_" (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`_ to have warnings output to the logging facility instead of stderr. + .. warning:: To protect against potential DOS attacks caused by "`decompression bombs`_" (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`_ to have warnings output to the logging facility instead of stderr. + + .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb + .. _the logging documentation: https://docs.python.org/2/library/logging.html?highlight=logging#integration-with-the-warnings-module Image processing ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst new file mode 100644 index 000000000..2d5bb1388 --- /dev/null +++ b/docs/reference/ImageCms.rst @@ -0,0 +1,13 @@ +.. py:module:: PIL.ImageCms +.. py:currentmodule:: PIL.ImageCms + +:py:mod:`ImageCms` Module +========================= + +The :py:mod:`ImageCms` module provides color profile management +support using the LittleCMS2 color management engine, based on Kevin +Cazabon's PyCMS library. + +.. automodule:: PIL.ImageCms + :members: + :noindex: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 30aa15a9b..c24a9dd99 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -74,6 +74,34 @@ To load a OpenType/TrueType font, use the truetype function in the :py:mod:`~PIL.ImageFont` module. Note that this function depends on third-party libraries, and may not available in all PIL builds. +Example: Draw Partial Opacity Text +---------------------------------- + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + # get an image + base = Image.open('Pillow/Tests/images/lena.png').convert('RGBA') + + # make a blank image for the text, initialized to transparent text color + txt = Image.new('RGBA', base.size, (255,255,255,0)) + + # get a font + fnt = ImageFont.truetype('Pillow/Tests/fonts/FreeMono.ttf', 40) + # get a drawing context + d = ImageDraw.Draw(txt) + + # draw text, half opacity + d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128)) + # draw text, full opacity + d.text((10,60), "World", font=fnt, fill=(255,255,255,255)) + + out = Image.alpha_composite(base, txt) + + out.show() + + + Functions --------- @@ -83,6 +111,13 @@ Functions Note that the image will be modified in place. + :param im: The image to draw in. + :param mode: Optional mode to use for color values. For RGB + images, this argument can be RGB or RGBA (to blend the + drawing into the image). For all other modes, this argument + must be the same as the image mode. If omitted, the mode + defaults to the mode of the image. + Methods ------- diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst new file mode 100644 index 000000000..eaf3b1c5e --- /dev/null +++ b/docs/reference/ImageMorph.rst @@ -0,0 +1,13 @@ +.. py:module:: PIL.ImageMorph +.. py:currentmodule:: PIL.ImageMorph + +:py:mod:`ImageMorph` Module +=========================== + +The :py:mod:`ImageMorph` module provides morphology operations on images. + +.. automodule:: PIL.ImageMorph + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index c8dfe3062..b8925bf8c 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -22,32 +22,32 @@ for a region of an image. .. py:attribute:: count - Total number of pixels. + Total number of pixels for each band in the image. .. py:attribute:: sum - Sum of all pixels. + Sum of all pixels for each band in the image. .. py:attribute:: sum2 - Squared sum of all pixels. + Squared sum of all pixels for each band in the image. - .. py:attribute:: pixel + .. py:attribute:: mean - Average pixel level. + Average (arithmetic mean) pixel level for each band in the image. .. py:attribute:: median - Median pixel level. + Median pixel level for each band in the image. .. py:attribute:: rms - RMS (root-mean-square). + RMS (root-mean-square) for each band in the image. .. py:attribute:: var - Variance. + Variance for each band in the image. .. py:attribute:: stddev - Standard deviation. + Standard deviation for each band in the image. diff --git a/docs/reference/OleFileIO.rst b/docs/reference/OleFileIO.rst new file mode 100644 index 000000000..74c4b7b36 --- /dev/null +++ b/docs/reference/OleFileIO.rst @@ -0,0 +1,364 @@ +.. py:module:: PIL.OleFileIO +.. py:currentmodule:: PIL.OleFileIO + +:py:mod:`OleFileIO` Module +=========================== + +The :py:mod:`OleFileIO` module reads Microsoft OLE2 files (also called +Structured Storage or Microsoft Compound Document File Format), such +as Microsoft Office documents, Image Composer and FlashPix files, and +Outlook messages. + +This module is the `OleFileIO\_PL`_ project by Philippe Lagadec, v0.30, +merged back into Pillow. + +.. _OleFileIO\_PL: http://www.decalage.info/python/olefileio + +How to use this module +---------------------- + +For more information, see also the file **PIL/OleFileIO.py**, sample +code at the end of the module itself, and docstrings within the code. + +About the structure of OLE files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An OLE file can be seen as a mini file system or a Zip archive: It +contains **streams** of data that look like files embedded within the +OLE file. Each stream has a name. For example, the main stream of a MS +Word document containing its text is named "WordDocument". + +An OLE file can also contain **storages**. A storage is a folder that +contains streams or other storages. For example, a MS Word document with +VBA macros has a storage called "Macros". + +Special streams can contain **properties**. A property is a specific +value that can be used to store information such as the metadata of a +document (title, author, creation date, etc). Property stream names +usually start with the character '05'. + +For example, a typical MS Word document may look like this: + +:: + + \x05DocumentSummaryInformation (stream) + \x05SummaryInformation (stream) + WordDocument (stream) + Macros (storage) + PROJECT (stream) + PROJECTwm (stream) + VBA (storage) + Module1 (stream) + ThisDocument (stream) + _VBA_PROJECT (stream) + dir (stream) + ObjectPool (storage) + +Test if a file is an OLE container +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use isOleFile to check if the first bytes of the file contain the Magic +for OLE files, before opening it. isOleFile returns True if it is an OLE +file, False otherwise. + +.. code-block:: python + + assert OleFileIO.isOleFile('myfile.doc') + +Open an OLE file from disk +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create an OleFileIO object with the file path as parameter: + +.. code-block:: python + + ole = OleFileIO.OleFileIO('myfile.doc') + +Open an OLE file from a file-like object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is useful if the file is not on disk, e.g. already stored in a +string or as a file-like object. + +.. code-block:: python + + ole = OleFileIO.OleFileIO(f) + +For example the code below reads a file into a string, then uses BytesIO +to turn it into a file-like object. + +.. code-block:: python + + data = open('myfile.doc', 'rb').read() + f = io.BytesIO(data) # or StringIO.StringIO for Python 2.x + ole = OleFileIO.OleFileIO(f) + +How to handle malformed OLE files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the parser is configured to be as robust and permissive as +possible, allowing to parse most malformed OLE files. Only fatal errors +will raise an exception. It is possible to tell the parser to be more +strict in order to raise exceptions for files that do not fully conform +to the OLE specifications, using the raise\_defect option: + +.. code-block:: python + + ole = OleFileIO.OleFileIO('myfile.doc', raise_defects=DEFECT_INCORRECT) + +When the parsing is done, the list of non-fatal issues detected is +available as a list in the parsing\_issues attribute of the OleFileIO +object: + +.. code-block:: python + + print('Non-fatal issues raised during parsing:') + if ole.parsing_issues: + for exctype, msg in ole.parsing_issues: + print('- %s: %s' % (exctype.__name__, msg)) + else: + print('None') + +Syntax for stream and storage path +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two different syntaxes are allowed for methods that need or return the +path of streams and storages: + +1) Either a **list of strings** including all the storages from the root + up to the stream/storage name. For example a stream called + "WordDocument" at the root will have ['WordDocument'] as full path. A + stream called "ThisDocument" located in the storage "Macros/VBA" will + be ['Macros', 'VBA', 'ThisDocument']. This is the original syntax + from PIL. While hard to read and not very convenient, this syntax + works in all cases. + +2) Or a **single string with slashes** to separate storage and stream + names (similar to the Unix path syntax). The previous examples would + be 'WordDocument' and 'Macros/VBA/ThisDocument'. This syntax is + easier, but may fail if a stream or storage name contains a slash. + +Both are case-insensitive. + +Switching between the two is easy: + +.. code-block:: python + + slash_path = '/'.join(list_path) + list_path = slash_path.split('/') + +Get the list of streams +~~~~~~~~~~~~~~~~~~~~~~~ + +listdir() returns a list of all the streams contained in the OLE file, +including those stored in storages. Each stream is listed itself as a +list, as described above. + +.. code-block:: python + + print(ole.listdir()) + +Sample result: + +.. code-block:: python + + [['\x01CompObj'], ['\x05DocumentSummaryInformation'], ['\x05SummaryInformation'] + , ['1Table'], ['Macros', 'PROJECT'], ['Macros', 'PROJECTwm'], ['Macros', 'VBA', + 'Module1'], ['Macros', 'VBA', 'ThisDocument'], ['Macros', 'VBA', '_VBA_PROJECT'] + , ['Macros', 'VBA', 'dir'], ['ObjectPool'], ['WordDocument']] + +As an option it is possible to choose if storages should also be listed, +with or without streams: + +.. code-block:: python + + ole.listdir (streams=False, storages=True) + +Test if known streams/storages exist: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +exists(path) checks if a given stream or storage exists in the OLE file. + +.. code-block:: python + + if ole.exists('worddocument'): + print("This is a Word document.") + if ole.exists('macros/vba'): + print("This document seems to contain VBA macros.") + +Read data from a stream +~~~~~~~~~~~~~~~~~~~~~~~ + +openstream(path) opens a stream as a file-like object. + +The following example extracts the "Pictures" stream from a PPT file: + +.. code-block:: python + + pics = ole.openstream('Pictures') + data = pics.read() + + +Get information about a stream/storage +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several methods can provide the size, type and timestamps of a given +stream/storage: + +get\_size(path) returns the size of a stream in bytes: + +.. code-block:: python + + s = ole.get_size('WordDocument') + +get\_type(path) returns the type of a stream/storage, as one of the +following constants: STGTY\_STREAM for a stream, STGTY\_STORAGE for a +storage, STGTY\_ROOT for the root entry, and False for a non existing +path. + +.. code-block:: python + + t = ole.get_type('WordDocument') + +get\_ctime(path) and get\_mtime(path) return the creation and +modification timestamps of a stream/storage, as a Python datetime object +with UTC timezone. Please note that these timestamps are only present if +the application that created the OLE file explicitly stored them, which +is rarely the case. When not present, these methods return None. + +.. code-block:: python + + c = ole.get_ctime('WordDocument') + m = ole.get_mtime('WordDocument') + +The root storage is a special case: You can get its creation and +modification timestamps using the OleFileIO.root attribute: + +.. code-block:: python + + c = ole.root.getctime() + m = ole.root.getmtime() + +Extract metadata +~~~~~~~~~~~~~~~~ + +get\_metadata() will check if standard property streams exist, parse all +the properties they contain, and return an OleMetadata object with the +found properties as attributes. + +.. code-block:: python + + meta = ole.get_metadata() + print('Author:', meta.author) + print('Title:', meta.title) + print('Creation date:', meta.create_time) + # print all metadata: + meta.dump() + +Available attributes include: + +:: + + codepage, title, subject, author, keywords, comments, template, + last_saved_by, revision_number, total_edit_time, last_printed, create_time, + last_saved_time, num_pages, num_words, num_chars, thumbnail, + creating_application, security, codepage_doc, category, presentation_target, + bytes, lines, paragraphs, slides, notes, hidden_slides, mm_clips, + scale_crop, heading_pairs, titles_of_parts, manager, company, links_dirty, + chars_with_spaces, unused, shared_doc, link_base, hlinks, hlinks_changed, + version, dig_sig, content_type, content_status, language, doc_version + +See the source code of the OleMetadata class for more information. + +Parse a property stream +~~~~~~~~~~~~~~~~~~~~~~~ + +get\_properties(path) can be used to parse any property stream that is +not handled by get\_metadata. It returns a dictionary indexed by +integers. Each integer is the index of the property, pointing to its +value. For example in the standard property stream +'05SummaryInformation', the document title is property #2, and the +subject is #3. + +.. code-block:: python + + p = ole.getproperties('specialprops') + +By default as in the original PIL version, timestamp properties are +converted into a number of seconds since Jan 1,1601. With the option +convert\_time, you can obtain more convenient Python datetime objects +(UTC timezone). If some time properties should not be converted (such as +total editing time in '05SummaryInformation'), the list of indexes can +be passed as no\_conversion: + +.. code-block:: python + + p = ole.getproperties('specialprops', convert_time=True, no_conversion=[10]) + +Close the OLE file +~~~~~~~~~~~~~~~~~~ + +Unless your application is a simple script that terminates after +processing an OLE file, do not forget to close each OleFileIO object +after parsing to close the file on disk. + +.. code-block:: python + + ole.close() + +Use OleFileIO as a script +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +OleFileIO can also be used as a script from the command-line to +display the structure of an OLE file and its metadata, for example: + +:: + + PIL/OleFileIO.py myfile.doc + +You can use the option -c to check that all streams can be read fully, +and -d to generate very verbose debugging information. + +How to contribute +----------------- + +The code is available in `a Mercurial repository on +bitbucket `_. You may use +it to submit enhancements or to report any issue. + +If you would like to help us improve this module, or simply provide +feedback, please `contact me `_. You can +help in many ways: + +- test this module on different platforms / Python versions +- find and report bugs +- improve documentation, code samples, docstrings +- write unittest test cases +- provide tricky malformed files + +How to report bugs +------------------ + +To report a bug, for example a normal file which is not parsed +correctly, please use the `issue reporting +page `_, +or if you prefer to do it privately, use this `contact +form `_. Please provide all the +information about the context and how to reproduce the bug. + +If possible please join the debugging output of OleFileIO. For this, +launch the following command : + +:: + + PIL/OleFileIO.py -d -c file >debug.txt + + +Classes and Methods +------------------- + +.. automodule:: PIL.OleFileIO + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 2d57e37be..2f10b861d 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -4,9 +4,11 @@ Reference .. toctree:: :maxdepth: 2 + Image ImageChops ImageColor + ImageCms ImageDraw ImageEnhance ImageFile @@ -14,6 +16,7 @@ Reference ImageFont ImageGrab ImageMath + ImageMorph ImageOps ImagePalette ImagePath @@ -22,5 +25,7 @@ Reference ImageStat ImageTk ImageWin + ExifTags + OleFileIO PSDraw ../PIL diff --git a/encode.c b/encode.c index b14942376..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 */ @@ -540,8 +553,8 @@ static unsigned int** get_qtables_arrays(PyObject* qtables) { tables = PySequence_Fast(qtables, "expected a sequence"); num_tables = PySequence_Size(qtables); - if (num_tables < 2 || num_tables > NUM_QUANT_TBLS) { - PyErr_SetString(PyExc_ValueError, "Not a valid numbers of quantization tables. Should be between 2 and 4."); + if (num_tables < 1 || num_tables > NUM_QUANT_TBLS) { + PyErr_SetString(PyExc_ValueError, "Not a valid numbers of quantization tables. Should be between 1 and 4."); return NULL; } qarrays = (unsigned int**) PyMem_Malloc(num_tables * sizeof(unsigned int*)); @@ -760,7 +773,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) (ttag_t) PyInt_AsLong(key), intav); free(intav); - } + } } else { TRACE((" %d elements, setting as floats \n", len)); floatav = malloc(sizeof(float)*len); @@ -903,7 +916,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) j2k_decode_coord_tuple(tile_offset, &context->tile_offset_x, &context->tile_offset_y); - j2k_decode_coord_tuple(tile_size, + j2k_decode_coord_tuple(tile_size, &context->tile_size_x, &context->tile_size_y); @@ -918,7 +931,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) if (context->tile_offset_x > context->offset_x || context->tile_offset_y > context->offset_y) { - PyErr_SetString(PyExc_ValueError, + PyErr_SetString(PyExc_ValueError, "JPEG 2000 tile offset too large to cover image area"); Py_DECREF(encoder); return NULL; diff --git a/libImaging/Access.c b/libImaging/Access.c index 62c97f3a3..97474a0b8 100644 --- a/libImaging/Access.c +++ b/libImaging/Access.c @@ -13,8 +13,8 @@ #include "Imaging.h" /* use Tests/make_hash.py to calculate these values */ -#define ACCESS_TABLE_SIZE 21 -#define ACCESS_TABLE_HASH 30197 +#define ACCESS_TABLE_SIZE 27 +#define ACCESS_TABLE_HASH 3078 static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; @@ -238,6 +238,7 @@ ImagingAccessInit() ADD("CMYK", line_32, get_pixel_32, put_pixel_32); ADD("YCbCr", line_32, get_pixel_32, put_pixel_32); ADD("LAB", line_32, get_pixel_32, put_pixel_32); + ADD("HSV", line_32, get_pixel_32, put_pixel_32); } ImagingAccess diff --git a/libImaging/Convert.c b/libImaging/Convert.c index 631263b31..3d9119c7f 100644 --- a/libImaging/Convert.c +++ b/libImaging/Convert.c @@ -35,6 +35,9 @@ #include "Imaging.h" +#define MAX(a, b) (a)>(b) ? (a) : (b) +#define MIN(a, b) (a)<(b) ? (a) : (b) + #define CLIP(v) ((v) <= 0 ? 0 : (v) >= 255 ? 255 : (v)) #define CLIP16(v) ((v) <= -32768 ? -32768 : (v) >= 32767 ? 32767 : (v)) @@ -46,6 +49,12 @@ #define L(rgb)\ ((INT32) (rgb)[0]*299 + (INT32) (rgb)[1]*587 + (INT32) (rgb)[2]*114) +#ifndef round +double round(double x) { + return floor(x+0.5); +} +#endif + /* ------------------- */ /* 1 (bit) conversions */ /* ------------------- */ @@ -236,6 +245,126 @@ rgb2bgr24(UINT8* out, const UINT8* in, int xsize) } } +static void +rgb2hsv(UINT8* out, const UINT8* in, int xsize) +{ // following colorsys.py + float h,s,rc,gc,bc,cr; + UINT8 maxc,minc; + UINT8 r, g, b; + UINT8 uh,us,uv; + int x; + + for (x = 0; x < xsize; x++, in += 4) { + r = in[0]; + g = in[1]; + b = in[2]; + + maxc = MAX(r,MAX(g,b)); + minc = MIN(r,MIN(g,b)); + uv = maxc; + if (minc == maxc){ + *out++ = 0; + *out++ = 0; + *out++ = uv; + } else { + cr = (float)(maxc-minc); + s = cr/(float)maxc; + rc = ((float)(maxc-r))/cr; + gc = ((float)(maxc-g))/cr; + bc = ((float)(maxc-b))/cr; + if (r == maxc) { + h = bc-gc; + } else if (g == maxc) { + h = 2.0 + rc-bc; + } else { + h = 4.0 + gc-rc; + } + // incorrect hue happens if h/6 is negative. + h = fmod((h/6.0 + 1.0), 1.0); + + uh = (UINT8)CLIP((int)(h*255.0)); + us = (UINT8)CLIP((int)(s*255.0)); + + *out++ = uh; + *out++ = us; + *out++ = uv; + + } + *out++ = in[3]; + } +} + +static void +hsv2rgb(UINT8* out, const UINT8* in, int xsize) +{ // following colorsys.py + + int p,q,t; + UINT8 up,uq,ut; + int i, x; + float f, fs; + UINT8 h,s,v; + + for (x = 0; x < xsize; x++, in += 4) { + h = in[0]; + s = in[1]; + v = in[2]; + + if (s==0){ + *out++ = v; + *out++ = v; + *out++ = v; + } else { + i = floor((float)h * 6.0 / 255.0); // 0 - 6 + f = (float)h * 6.0 / 255.0 - (float)i; // 0-1 : remainder. + fs = ((float)s)/255.0; + + p = round((float)v * (1.0-fs)); + q = round((float)v * (1.0-fs*f)); + t = round((float)v * (1.0-fs*(1.0-f))); + up = (UINT8)CLIP(p); + uq = (UINT8)CLIP(q); + ut = (UINT8)CLIP(t); + + switch (i%6) { + case 0: + *out++ = v; + *out++ = ut; + *out++ = up; + break; + case 1: + *out++ = uq; + *out++ = v; + *out++ = up; + break; + case 2: + *out++ = up; + *out++ = v; + *out++ = ut; + break; + case 3: + *out++ = up; + *out++ = uq; + *out++ = v; + break; + case 4: + *out++ = ut; + *out++ = up; + *out++ = v; + break; + case 5: + *out++ = v; + *out++ = up; + *out++ = uq; + break; + + } + } + *out++ = in[3]; + } +} + + + /* ---------------- */ /* RGBA conversions */ /* ---------------- */ @@ -658,6 +787,7 @@ static struct { { "RGB", "RGBX", rgb2rgba }, { "RGB", "CMYK", rgb2cmyk }, { "RGB", "YCbCr", ImagingConvertRGB2YCbCr }, + { "RGB", "HSV", rgb2hsv }, { "RGBA", "1", rgb2bit }, { "RGBA", "L", rgb2l }, @@ -687,6 +817,8 @@ static struct { { "YCbCr", "L", ycbcr2l }, { "YCbCr", "RGB", ImagingConvertYCbCr2RGB }, + { "HSV", "RGB", hsv2rgb }, + { "I", "I;16", I_I16L }, { "I;16", "I", I16L_I }, { "L", "I;16", L_I16L }, diff --git a/libImaging/Effects.c b/libImaging/Effects.c index db6e72989..eb598d968 100644 --- a/libImaging/Effects.c +++ b/libImaging/Effects.c @@ -48,25 +48,25 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) for (y = 0; y < ysize; y++) { UINT8* buf = im->image8[y]; - for (x = 0; x < xsize; x++) { - x1 = y1 = xi2 = yi2 = 0.0; - cr = x*dr + extent[0]; - ci = y*di + extent[1]; - for (k = 1;; k++) { - y1 = 2*x1*y1 + ci; - x1 = xi2 - yi2 + cr; - xi2 = x1*x1; - yi2 = y1*y1; - if ((xi2 + yi2) > radius) { - buf[x] = k*255/quality; - break; - } - if (k > quality) { - buf[x] = 0; - break; - } - } - } + for (x = 0; x < xsize; x++) { + x1 = y1 = xi2 = yi2 = 0.0; + cr = x*dr + extent[0]; + ci = y*di + extent[1]; + for (k = 1;; k++) { + y1 = 2*x1*y1 + ci; + x1 = xi2 - yi2 + cr; + xi2 = x1*x1; + yi2 = y1*y1; + if ((xi2 + yi2) > radius) { + buf[x] = k*255/quality; + break; + } + if (k > quality) { + buf[x] = 0; + break; + } + } + } } return im; } @@ -74,7 +74,7 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) Imaging ImagingEffectNoise(int xsize, int ysize, float sigma) { - /* Generate gaussian noise centered around 128 */ + /* Generate Gaussian noise centered around 128 */ Imaging imOut; int x, y; @@ -83,19 +83,19 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) imOut = ImagingNew("L", xsize, ysize); if (!imOut) - return NULL; + return NULL; next = 0.0; nextok = 0; for (y = 0; y < imOut->ysize; y++) { UINT8* out = imOut->image8[y]; - for (x = 0; x < imOut->xsize; x++) { + for (x = 0; x < imOut->xsize; x++) { if (nextok) { this = next; nextok = 0; } else { - /* after numerical recepies */ + /* after numerical recipes */ double v1, v2, radius, factor; do { v1 = rand()*(2.0/32767.0) - 1.0; @@ -113,14 +113,6 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) return imOut; } -Imaging -ImagingEffectPerlinTurbulence(int xsize, int ysize) -{ - /* Perlin turbulence (In progress) */ - - return NULL; -} - Imaging ImagingEffectSpread(Imaging imIn, int distance) { @@ -132,11 +124,11 @@ ImagingEffectSpread(Imaging imIn, int distance) imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); if (!imOut) - return NULL; + return NULL; -#define SPREAD(type, image)\ +#define SPREAD(type, image)\ for (y = 0; y < imIn->ysize; y++)\ - for (x = 0; x < imIn->xsize; x++) {\ + for (x = 0; x < imIn->xsize; x++) {\ int xx = x + (rand() % distance) - distance/2;\ int yy = y + (rand() % distance) - distance/2;\ if (xx >= 0 && xx < imIn->xsize && yy >= 0 && yy < imIn->ysize) {\ @@ -147,9 +139,9 @@ ImagingEffectSpread(Imaging imIn, int distance) } if (imIn->image8) { - SPREAD(UINT8, image8); + SPREAD(UINT8, image8); } else { - SPREAD(INT32, image32); + SPREAD(INT32, image32); } ImagingCopyInfo(imOut, imIn); @@ -157,217 +149,4 @@ ImagingEffectSpread(Imaging imIn, int distance) return imOut; } -/* -------------------------------------------------------------------- */ -/* Taken from the "C" code in the W3C SVG specification. Translated - to C89 by Fredrik Lundh */ - -#if 0 - -/* Produces results in the range [1, 2**31 - 2]. -Algorithm is: r = (a * r) mod m -where a = 16807 and m = 2**31 - 1 = 2147483647 -See [Park & Miller], CACM vol. 31 no. 10 p. 1195, Oct. 1988 -To test: the algorithm should produce the result 1043618065 -as the 10,000th generated number if the original seed is 1. -*/ -#define RAND_m 2147483647 /* 2**31 - 1 */ -#define RAND_a 16807 /* 7**5; primitive root of m */ -#define RAND_q 127773 /* m / a */ -#define RAND_r 2836 /* m % a */ - -static long -perlin_setup_seed(long lSeed) -{ - if (lSeed <= 0) lSeed = -(lSeed % (RAND_m - 1)) + 1; - if (lSeed > RAND_m - 1) lSeed = RAND_m - 1; - return lSeed; -} - -static long -perlin_random(long lSeed) -{ - long result; - result = RAND_a * (lSeed % RAND_q) - RAND_r * (lSeed / RAND_q); - if (result <= 0) result += RAND_m; - return result; -} - -#define BSize 0x100 -#define BM 0xff -#define PerlinN 0x1000 -#define NP 12 /* 2^PerlinN */ -#define NM 0xfff -static int perlin_uLatticeSelector[BSize + BSize + 2]; -static double perlin_fGradient[4][BSize + BSize + 2][2]; -typedef struct -{ - int nWidth; /* How much to subtract to wrap for stitching. */ - int nHeight; - int nWrapX; /* Minimum value to wrap. */ - int nWrapY; -} StitchInfo; - -static void -perlin_init(long lSeed) -{ - double s; - int i, j, k; - lSeed = perlin_setup_seed(lSeed); - for(k = 0; k < 4; k++) - { - for(i = 0; i < BSize; i++) - { - perlin_uLatticeSelector[i] = i; - for (j = 0; j < 2; j++) - perlin_fGradient[k][i][j] = (double)(((lSeed = perlin_random(lSeed)) % (BSize + BSize)) - BSize) / BSize; - s = (double) (sqrt(perlin_fGradient[k][i][0] * perlin_fGradient[k][i][0] + perlin_fGradient[k][i][1] * perlin_fGradient[k][i][1])); - perlin_fGradient[k][i][0] /= s; - perlin_fGradient[k][i][1] /= s; - } - } - while(--i) - { - k = perlin_uLatticeSelector[i]; - perlin_uLatticeSelector[i] = perlin_uLatticeSelector[j = (lSeed = perlin_random(lSeed)) % BSize]; - perlin_uLatticeSelector[j] = k; - } - for(i = 0; i < BSize + 2; i++) - { - perlin_uLatticeSelector[BSize + i] = perlin_uLatticeSelector[i]; - for(k = 0; k < 4; k++) - for(j = 0; j < 2; j++) - perlin_fGradient[k][BSize + i][j] = perlin_fGradient[k][i][j]; - } -} - -#define s_curve(t) ( t * t * (3. - 2. * t) ) -#define lerp(t, a, b) ( a + t * (b - a) ) -static double -perlin_noise2(int nColorChannel, double vec[2], StitchInfo *pStitchInfo) -{ - int bx0, bx1, by0, by1, b00, b10, b01, b11; - double rx0, rx1, ry0, ry1, *q, sx, sy, a, b, t, u, v; - register int i, j; - - t = vec[0] + (double) PerlinN; - bx0 = (int)t; - bx1 = bx0+1; - rx0 = t - (int)t; - rx1 = rx0 - 1.0f; - t = vec[1] + (double) PerlinN; - by0 = (int)t; - by1 = by0+1; - ry0 = t - (int)t; - ry1 = ry0 - 1.0f; - - /* If stitching, adjust lattice points accordingly. */ - if(pStitchInfo != NULL) - { - if(bx0 >= pStitchInfo->nWrapX) - bx0 -= pStitchInfo->nWidth; - if(bx1 >= pStitchInfo->nWrapX) - bx1 -= pStitchInfo->nWidth; - if(by0 >= pStitchInfo->nWrapY) - by0 -= pStitchInfo->nHeight; - if(by1 >= pStitchInfo->nWrapY) - by1 -= pStitchInfo->nHeight; - } - - bx0 &= BM; - bx1 &= BM; - by0 &= BM; - by1 &= BM; - - i = perlin_uLatticeSelector[bx0]; - j = perlin_uLatticeSelector[bx1]; - b00 = perlin_uLatticeSelector[i + by0]; - b10 = perlin_uLatticeSelector[j + by0]; - b01 = perlin_uLatticeSelector[i + by1]; - b11 = perlin_uLatticeSelector[j + by1]; - sx = (double) (s_curve(rx0)); - sy = (double) (s_curve(ry0)); - q = perlin_fGradient[nColorChannel][b00]; u = rx0 * q[0] + ry0 * q[1]; - q = perlin_fGradient[nColorChannel][b10]; v = rx1 * q[0] + ry0 * q[1]; - a = lerp(sx, u, v); - q = perlin_fGradient[nColorChannel][b01]; u = rx0 * q[0] + ry1 * q[1]; - q = perlin_fGradient[nColorChannel][b11]; v = rx1 * q[0] + ry1 * q[1]; - b = lerp(sx, u, v); - return lerp(sy, a, b); -} - -double -perlin_turbulence( - int nColorChannel, double *point, double fBaseFreqX, double fBaseFreqY, - int nNumOctaves, int bFractalSum, int bDoStitching, - double fTileX, double fTileY, double fTileWidth, double fTileHeight) -{ - StitchInfo stitch; - StitchInfo *pStitchInfo = NULL; /* Not stitching when NULL. */ - - double fSum = 0.0f; - double vec[2]; - double ratio = 1; - - int nOctave; - - vec[0] = point[0] * fBaseFreqX; - vec[1] = point[1] * fBaseFreqY; - - /* Adjust the base frequencies if necessary for stitching. */ - if(bDoStitching) - { - /* When stitching tiled turbulence, the frequencies must be adjusted */ - /* so that the tile borders will be continuous. */ - if(fBaseFreqX != 0.0) - { - double fLoFreq = (double) (floor(fTileWidth * fBaseFreqX)) / fTileWidth; - double fHiFreq = (double) (ceil(fTileWidth * fBaseFreqX)) / fTileWidth; - if(fBaseFreqX / fLoFreq < fHiFreq / fBaseFreqX) - fBaseFreqX = fLoFreq; - else - fBaseFreqX = fHiFreq; - } - - if(fBaseFreqY != 0.0) - { - double fLoFreq = (double) (floor(fTileHeight * fBaseFreqY)) / fTileHeight; - double fHiFreq = (double) (ceil(fTileHeight * fBaseFreqY)) / fTileHeight; - if(fBaseFreqY / fLoFreq < fHiFreq / fBaseFreqY) - fBaseFreqY = fLoFreq; - else - fBaseFreqY = fHiFreq; - } - - /* Set up initial stitch values. */ - pStitchInfo = &stitch; - stitch.nWidth = (int) (fTileWidth * fBaseFreqX + 0.5f); - stitch.nWrapX = (int) (fTileX * fBaseFreqX + PerlinN + stitch.nWidth); - stitch.nHeight = (int) (fTileHeight * fBaseFreqY + 0.5f); - stitch.nWrapY = (int) (fTileY * fBaseFreqY + PerlinN + stitch.nHeight); - } - - for(nOctave = 0; nOctave < nNumOctaves; nOctave++) - { - if(bFractalSum) - fSum += (double) (perlin_noise2(nColorChannel, vec, pStitchInfo) / ratio); - else - fSum += (double) (fabs(perlin_noise2(nColorChannel, vec, pStitchInfo)) / ratio); - - vec[0] *= 2; - vec[1] *= 2; - ratio *= 2; - - if(pStitchInfo != NULL) - { - /* Update stitch values. Subtracting PerlinN before the multiplication and */ - /* adding it afterward simplifies to subtracting it once. */ - stitch.nWidth *= 2; - stitch.nWrapX = 2 * stitch.nWrapX - PerlinN; - stitch.nHeight *= 2; - stitch.nWrapY = 2 * stitch.nWrapY - PerlinN; - } - } - return fSum; -} - -#endif +// End of file diff --git a/libImaging/ImPlatform.h b/libImaging/ImPlatform.h index be1f20f3f..70ee63119 100644 --- a/libImaging/ImPlatform.h +++ b/libImaging/ImPlatform.h @@ -69,4 +69,6 @@ #define FLOAT32 float #define FLOAT64 double - +#ifdef _MSC_VER +typedef signed __int64 int64_t; +#endif 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/Pack.c b/libImaging/Pack.c index 1cc1f3a94..fecafbde4 100644 --- a/libImaging/Pack.c +++ b/libImaging/Pack.c @@ -566,6 +566,12 @@ static struct { {"LAB", "A", 8, band1}, {"LAB", "B", 8, band2}, + /* HSV */ + {"HSV", "HSV", 24, ImagingPackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, + /* integer */ {"I", "I", 32, copy4}, {"I", "I;16B", 16, packI16B}, diff --git a/libImaging/Storage.c b/libImaging/Storage.c index d31db5cb2..d65de1c0a 100644 --- a/libImaging/Storage.c +++ b/libImaging/Storage.c @@ -186,6 +186,13 @@ ImagingNewPrologueSubtype(const char *mode, unsigned xsize, unsigned ysize, im->pixelsize = 4; im->linesize = xsize * 4; + } else if (strcmp(mode, "HSV") == 0) { + /* 24-bit color, luminance, + 2 color channels */ + /* L is uint8, a,b are int8 */ + im->bands = 3; + im->pixelsize = 4; + im->linesize = xsize * 4; + } else { free(im); return (Imaging) ImagingError_ValueError("unrecognized mode"); @@ -379,7 +386,7 @@ ImagingNew(const char* mode, int xsize, int ysize) } else bytes = strlen(mode); /* close enough */ - if ((Py_ssize_t) xsize * ysize * bytes <= THRESHOLD) { + if ((int64_t) xsize * (int64_t) ysize * bytes <= THRESHOLD) { im = ImagingNewBlock(mode, xsize, ysize); if (im) return im; diff --git a/libImaging/TiffDecode.c b/libImaging/TiffDecode.c index 787cd4506..1d320e9bd 100644 --- a/libImaging/TiffDecode.c +++ b/libImaging/TiffDecode.c @@ -21,8 +21,8 @@ #include "TiffDecode.h" void dump_state(const TIFFSTATE *state){ - TRACE(("State: Location %u size %d eof %d data: %p \n", (uint)state->loc, - (int)state->size, (uint)state->eof, state->data)); + TRACE(("State: Location %u size %d eof %d data: %p ifd: %d\n", (uint)state->loc, + (int)state->size, (uint)state->eof, state->data, state->ifd)); } /* @@ -142,7 +142,7 @@ void _tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) { (void) hdata; (void) base; (void) size; } -int ImagingLibTiffInit(ImagingCodecState state, int fp) { +int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; TRACE(("initing libtiff\n")); @@ -158,6 +158,7 @@ int ImagingLibTiffInit(ImagingCodecState state, int fp) { clientstate->size = 0; clientstate->data = 0; clientstate->fp = fp; + clientstate->ifd = offset; clientstate->eof = 0; return 1; @@ -195,7 +196,6 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int clientstate->loc = 0; clientstate->data = (tdata_t)buffer; clientstate->flrealloc = 0; - dump_state(clientstate); TIFFSetWarningHandler(NULL); @@ -220,6 +220,16 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int return -1; } + if (clientstate->ifd){ + unsigned int ifdoffset = clientstate->ifd; + TRACE(("reading tiff ifd %d\n", ifdoffset)); + int rv = TIFFSetSubDirectory(tiff, ifdoffset); + if (!rv){ + TRACE(("error in TIFFSetSubDirectory")); + return -1; + } + } + size = TIFFScanlineSize(tiff); TRACE(("ScanlineSize: %d \n", size)); if (size > state->bytes) { diff --git a/libImaging/TiffDecode.h b/libImaging/TiffDecode.h index 46c940d1b..9875309e3 100644 --- a/libImaging/TiffDecode.h +++ b/libImaging/TiffDecode.h @@ -26,6 +26,7 @@ typedef struct { toff_t loc; /* toff_t == uint32 */ tsize_t size; /* tsize_t == int32 */ int fp; + int ifd; /* offset of the ifd, used for multipage */ TIFF *tiff; /* Used in write */ toff_t eof; int flrealloc; /* may we realloc */ @@ -33,7 +34,7 @@ typedef struct { -extern int ImagingLibTiffInit(ImagingCodecState state, int fp); +extern int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); @@ -50,5 +51,4 @@ extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); #define TRACE(args) - #endif diff --git a/libImaging/Unpack.c b/libImaging/Unpack.c index 552c759b9..7c453dbfd 100644 --- a/libImaging/Unpack.c +++ b/libImaging/Unpack.c @@ -1159,6 +1159,12 @@ static struct { {"LAB", "A", 8, band1}, {"LAB", "B", 8, band2}, + /* HSV Color */ + {"HSV", "HSV", 24, ImagingUnpackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, + /* integer variations */ {"I", "I", 32, copy4}, {"I", "I;8", 8, unpackI8}, diff --git a/mp_compile.py b/mp_compile.py index 71b4089e1..adbac7c19 100644 --- a/mp_compile.py +++ b/mp_compile.py @@ -3,8 +3,13 @@ from multiprocessing import Pool, cpu_count from distutils.ccompiler import CCompiler -import os +import os, sys +try: + MAX_PROCS = int(os.environ.get('MAX_CONCURRENCY', cpu_count())) +except: + MAX_PROCS = None + # hideous monkeypatching. but. but. but. def _mp_compile_one(tp): @@ -31,22 +36,27 @@ def _mp_compile(self, sources, output_dir=None, macros=None, 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) + 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 - ] + 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 +# explicitly don't enable if environment says 1 processor +if MAX_PROCS != 1 and not sys.platform.startswith('win'): + try: + # bug, only enable if we can make a Pool. see issue #790 and + # http://stackoverflow.com/questions/6033599/oserror-38-errno-38-with-multiprocessing + pool = Pool(2) + CCompiler.compile = _mp_compile + except Exception as msg: + print("Exception installing mp_compile, proceeding without: %s" %msg) +else: + print("Single threaded build, not installing mp_compile: %s processes" %MAX_PROCS) diff --git a/profile-installed.py b/profile-installed.py new file mode 100755 index 000000000..be9a960d2 --- /dev/null +++ b/profile-installed.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +import nose +import os +import sys +import glob + +import profile + +# 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')) + +# if there's no test selected (mostly) choose a working default. +# Something is required, because if we import the tests from the local +# directory, once again, we've got the non-installed PIL in the way +if len(sys.argv) == 1: + sys.argv.extend(glob.glob('Tests/test*.py')) + +# 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') + +if __name__ == '__main__': + profile.run("nose.main()", sort=2) diff --git a/selftest.py b/selftest.py index 29af34ad2..b13dfec28 100644 --- a/selftest.py +++ b/selftest.py @@ -49,13 +49,13 @@ def testimage(): Or open existing files: - >>> im = Image.open(os.path.join(ROOT, "Tests/images/lena.gif")) + >>> im = Image.open(os.path.join(ROOT, "Tests/images/hopper.gif")) >>> _info(im) ('GIF', 'P', (128, 128)) - >>> _info(Image.open(os.path.join(ROOT, "Tests/images/lena.ppm"))) + >>> _info(Image.open(os.path.join(ROOT, "Tests/images/hopper.ppm"))) ('PPM', 'RGB', (128, 128)) >>> try: - ... _info(Image.open(os.path.join(ROOT, "Tests/images/lena.jpg"))) + ... _info(Image.open(os.path.join(ROOT, "Tests/images/hopper.jpg"))) ... except IOError as v: ... print(v) ('JPEG', 'RGB', (128, 128)) @@ -63,7 +63,7 @@ def testimage(): PIL doesn't actually load the image data until it's needed, or you call the "load" method: - >>> im = Image.open(os.path.join(ROOT, "Tests/images/lena.ppm")) + >>> im = Image.open(os.path.join(ROOT, "Tests/images/hopper.ppm")) >>> print(im.im) # internal image attribute None >>> a = im.load() @@ -73,7 +73,7 @@ def testimage(): You can apply many different operations on images. Most operations return a new image: - >>> im = Image.open(os.path.join(ROOT, "Tests/images/lena.ppm")) + >>> im = Image.open(os.path.join(ROOT, "Tests/images/hopper.ppm")) >>> _info(im.convert("L")) (None, 'L', (128, 128)) >>> _info(im.copy()) @@ -89,9 +89,9 @@ def testimage(): >>> len(im.getdata()) 16384 >>> im.getextrema() - ((61, 255), (26, 234), (44, 223)) + ((0, 255), (0, 255), (0, 255)) >>> im.getpixel((0, 0)) - (223, 162, 133) + (20, 20, 70) >>> len(im.getprojection()) 2 >>> len(im.histogram()) diff --git a/setup.py b/setup.py index e94e34d28..8c46d7a0e 100644 --- a/setup.py +++ b/setup.py @@ -90,7 +90,7 @@ except (ImportError, OSError): NAME = 'Pillow' -PILLOW_VERSION = '2.5.0' +PILLOW_VERSION = '2.5.3' TCL_ROOT = None JPEG_ROOT = None JPEG2K_ROOT = None @@ -487,6 +487,8 @@ class pil_build_ext(build_ext): # In Google's precompiled zip it is call "libwebp": if _find_library_file(self, "webp"): feature.webp = "webp" + elif _find_library_file(self, "libwebp"): + feature.webp = "libwebp" if feature.want('webpmux'): if (_find_include_file(self, "webp/mux.h") and @@ -494,6 +496,9 @@ class pil_build_ext(build_ext): if (_find_library_file(self, "webpmux") and _find_library_file(self, "webpdemux")): feature.webpmux = "webpmux" + if (_find_library_file(self, "libwebpmux") and + _find_library_file(self, "libwebpdemux")): + feature.webpmux = "libwebpmux" for f in feature: if not getattr(feature, f) and feature.require(f): @@ -559,13 +564,13 @@ class pil_build_ext(build_ext): libraries=["lcms2"] + extra)) if os.path.isfile("_webp.c") and feature.webp: - libs = ["webp"] + libs = [feature.webp] defs = [] if feature.webpmux: defs.append(("HAVE_WEBPMUX", None)) - libs.append("webpmux") - libs.append("webpdemux") + libs.append(feature.webpmux) + libs.append(feature.webpmux.replace('pmux','pdemux')) exts.append(Extension( "PIL._webp", ["_webp.c"], libraries=libs, define_macros=defs)) @@ -706,6 +711,7 @@ class pil_build_ext(build_ext): if ret >> 8 == 0: fp = open(tmpfile, 'r') multiarch_path_component = fp.readline().strip() + fp.close() _add_directory( self.compiler.library_dirs, '/usr/lib/' + multiarch_path_component) @@ -720,9 +726,7 @@ setup( name=NAME, version=PILLOW_VERSION, description='Python Imaging Library (Fork)', - long_description=( - _read('README.rst') + b'\n' + - _read('CHANGES.rst')).decode('utf-8'), + long_description=_read('README.rst').decode('utf-8'), author='Alex Clark (fork author)', author_email='aclark@aclark.net', url='http://python-pillow.github.io/',