From 196a48b4fd5cd388d5412d7aa0115e99b6e8c792 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 28 Feb 2014 15:57:53 -0800 Subject: [PATCH 01/56] added context manager support --- PIL/Image.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/PIL/Image.py b/PIL/Image.py index 75e7efc75..d7435d1ef 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -497,6 +497,25 @@ class Image: _makeself = _new # compatibility + # with compatibility + def __enter__(self): + return self + def __exit__(self, *args): + self.close() + + def close(self): + """ Close the file pointer, if possible. Destroy the image core. + This releases memory, and the image will be unusable afterward + """ + try: + self.fp.close() + except Exception as msg: + if Image.DEBUG: + print ("Error closing: %s" %msg) + + self.im = None + + def _copy(self): self.load() self.im = self.im.copy() From d514c1fb647a42b3c5d3649540678b6c59a48d04 Mon Sep 17 00:00:00 2001 From: hugovk Date: Wed, 2 Apr 2014 11:00:43 +0300 Subject: [PATCH 02/56] Add coverage report --- .travis.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index d68de0b32..1c6f8385e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ language: python virtualenv: system_site_packages: true -notifications: - irc: "chat.freenode.net#pil" +# notifications: + # irc: "chat.freenode.net#pil" python: - 2.6 @@ -14,21 +14,26 @@ python: - 3.3 - "pypy" -install: +install: - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev cmake" - "pip install cffi" + - "pip install coveralls" - # webp + # webp - pushd depends && ./install_webp.sh && popd # openjpeg - pushd depends && ./install_openjpeg.sh && popd script: + - coverage erase - python setup.py clean - python setup.py build_ext --inplace - - python selftest.py - - python Tests/run.py + - coverage run --append selftest.py + - coverage run --append Tests/run.py + +after_success: + - coveralls matrix: allow_failures: From cd987294365764b9a1c3d747775bc7c530b0c6d7 Mon Sep 17 00:00:00 2001 From: hugovk Date: Wed, 2 Apr 2014 11:40:40 +0300 Subject: [PATCH 03/56] Omit library modules from coverage --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1c6f8385e..9a618fc2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,10 +29,11 @@ script: - coverage erase - python setup.py clean - python setup.py build_ext --inplace - - coverage run --append selftest.py - - coverage run --append Tests/run.py + - coverage run --append --omit=/usr/* selftest.py + - coverage run --append --omit=/usr/* Tests/run.py after_success: + - coverage report - coveralls matrix: From b6d44fe31c7a3176f1a203cebed490cc078a8ab1 Mon Sep 17 00:00:00 2001 From: hugovk Date: Wed, 2 Apr 2014 11:57:02 +0300 Subject: [PATCH 04/56] Only measure coverage of code in the PIL directory, but includes all those files, even those unexecuted --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9a618fc2b..c4319bfcb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,8 +29,8 @@ script: - coverage erase - python setup.py clean - python setup.py build_ext --inplace - - coverage run --append --omit=/usr/* selftest.py - - coverage run --append --omit=/usr/* Tests/run.py + - coverage run --append --source=PIL selftest.py + - coverage run --append --source=PIL Tests/run.py after_success: - coverage report From ec0c933a3ccd0bb32ff55f6d57d7c26a84cba065 Mon Sep 17 00:00:00 2001 From: hugovk Date: Wed, 2 Apr 2014 12:12:47 +0300 Subject: [PATCH 05/56] Include PIL/*.py for coverage to see if it includes unexecuted code --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c4319bfcb..0b232ec07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,8 +29,8 @@ script: - coverage erase - python setup.py clean - python setup.py build_ext --inplace - - coverage run --append --source=PIL selftest.py - - coverage run --append --source=PIL Tests/run.py + - coverage run --append --include=PIL/* selftest.py + - coverage run --append --include=PIL/* Tests/run.py after_success: - coverage report From 4474917fc292d283a0402f2fedad2ef3c4a820fb Mon Sep 17 00:00:00 2001 From: hugovk Date: Wed, 2 Apr 2014 13:53:34 +0300 Subject: [PATCH 06/56] Re-enable notifications --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0b232ec07..0cabcc35d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ language: python virtualenv: system_site_packages: true -# notifications: - # irc: "chat.freenode.net#pil" +notifications: + irc: "chat.freenode.net#pil" python: - 2.6 From 5e0d2a30044861fa93a55c4788e28143002eed96 Mon Sep 17 00:00:00 2001 From: hugovk Date: Sat, 5 Apr 2014 01:25:55 +0300 Subject: [PATCH 07/56] Add pep8 and pyflakes reports to Travis CI --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0cabcc35d..b104b65e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,9 @@ script: after_success: - coverage report - coveralls + - pip install pep8 pyflakes + - pep8 PIL/*.py + - pyflakes PIL/*.py matrix: allow_failures: From 07650be8320c85883392e90cfea63c2e7da5d52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 7 Apr 2014 17:36:36 +0200 Subject: [PATCH 08/56] Fix variable name Wrong variable name was used for transparency manipulations. --- PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PIL/Image.py b/PIL/Image.py index 18d2c8267..54c68e2dc 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -807,7 +807,7 @@ class Image: new_im = self._new(im) if delete_trns: #crash fail if we leave a bytes transparency in an rgb/l mode. - del(new.info['transparency']) + del(new_im.info['transparency']) if trns is not None: if new_im.mode == 'P': try: From e0b7f86cf65700753029869fae26bb71cf90df7b Mon Sep 17 00:00:00 2001 From: Alex Clark Date: Mon, 7 Apr 2014 18:37:40 -0400 Subject: [PATCH 09/56] Add history --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 49946c36c..346ba7101 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,12 @@ Changelog (Pillow) ================== +2.5.0 (unreleased) +------------------ + +- Fix variable name used for transparency manipulations + [nijel] + 2.4.0 (2014-04-01) ------------------ From 844ed441deb6b75d3048fa111977188ed47f0b76 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 7 Apr 2014 15:59:33 -0700 Subject: [PATCH 10/56] Add the suffix if it's not there, not if it is --- PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PIL/Image.py b/PIL/Image.py index 54c68e2dc..40a467d12 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -513,7 +513,7 @@ class Image: if not format or format == "PPM": self.im.save_ppm(file) else: - if file.endswith(format): + if not file.endswith(format): file = file + "." + format self.save(file, format) return file From 86d5c5c3894f58895f31287081cdd146f5fe00f7 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 7 Apr 2014 16:01:49 -0700 Subject: [PATCH 11/56] Have the tempfile use a suffix with a dot --- PIL/Image.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PIL/Image.py b/PIL/Image.py index 40a467d12..359aae716 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -505,8 +505,11 @@ class Image: def _dump(self, file=None, format=None): import tempfile, os + suffix = '' + if format: + suffix = '.'+format if not file: - f, file = tempfile.mkstemp(format or '') + f, file = tempfile.mkstemp(suffix) os.close(f) self.load() From 57d4efbeb8967b36d9dd0d1b49748bc6cfae229a Mon Sep 17 00:00:00 2001 From: Alex Clark Date: Mon, 7 Apr 2014 19:18:32 -0400 Subject: [PATCH 12/56] Add history --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 346ba7101..f83c6f339 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changelog (Pillow) 2.5.0 (unreleased) ------------------ +- Have the tempfile use a suffix with a dot + [wiredfool] + - Fix variable name used for transparency manipulations [nijel] From 864fb95cb1c1c2fb45bc35292ddf9e76783a11d6 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 7 Apr 2014 22:10:45 -0700 Subject: [PATCH 13/56] Read test for 16 bit binary pgm file, #602 --- Tests/images/16_bit_binary.pgm | Bin 0 -> 4016 bytes Tests/images/16_bit_binary_pgm.png | Bin 0 -> 578 bytes Tests/test_file_ppm.py | 10 ++++++++++ 3 files changed, 10 insertions(+) create mode 100644 Tests/images/16_bit_binary.pgm create mode 100644 Tests/images/16_bit_binary_pgm.png diff --git a/Tests/images/16_bit_binary.pgm b/Tests/images/16_bit_binary.pgm new file mode 100644 index 0000000000000000000000000000000000000000..fd41a5c3d1b04b92408c7344cc67e52541a295b6 GIT binary patch literal 4016 zcma*U3r9l%0D#eqlxw7nlu=Vg%51GWkrF90rWq+?dKgp2lo=_JGG@w{GNy+yC8ms- zGNz355L3pKj45ME%#@M(3*R{=QRr2-Yt(8%CyH88_=A4u+3F+vGssqs*m5KLwZeY9 zv88dgc+S3eu!Su)|INP6vd?#HPGqwO?32J|T}i&b4znlFv5bwZvEd?nXlD14Y)Hul{p?Pa4cJ)!1H0YN`ogSN%6i=F zMxI?auxoL4wT)f&vhFhLva*ZUtW(1}LhM2i-kpXr%!VWdC1IuiGfz=sVZGzP(Se1|MuCQG*tn!xa>|+)CY+F4m jUt(Kc*yb@-cE&cfvJIQ8^ph2)*t#pWwuhDMvNg~@LpIA; literal 0 HcmV?d00001 diff --git a/Tests/images/16_bit_binary_pgm.png b/Tests/images/16_bit_binary_pgm.png new file mode 100644 index 0000000000000000000000000000000000000000..918be1ad41d738db5afe429469538df9e580d7d9 GIT binary patch literal 578 zcmeAS@N?(olHy`uVBq!ia0vp^B0!uX03;Yb2TTqIQY`6?zK#rxZ3_%vOp6EdnUcKS zUH<82DMc#HK4B-bTr#q6iAL1q1 z{qwxDzj){Gw?)6NJgvI-RO`H|?TezqM@9b*J9v-^+F?loI?shG!vX~#}WkDbP(*O@n6{{LwJ@>sh&JI}bpEGv&)mKCFxtS4-#_c(C+#VMyf z7N7nw>2yHn=>sj^UrsIEe|Bm4<)!LRF4;c6H1*-7n~#Mo3tp}$uH5|Kl2}QmY++^X zyH8$=D$hPm-RXU~$NTYKZ|CKwKif{iz#K_Rf*wV__0?4*9 kFqpgI_iPjmx%nxXX_dG&y!q;^4%EQl>FVdQ&MBb@0M*s{ZvX%Q literal 0 HcmV?d00001 diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fccb94905..5e0aa84ac 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -12,3 +12,13 @@ def test_sanity(): assert_equal(im.mode, "RGB") assert_equal(im.size, (128, 128)) assert_equal(im.format, "PPM") + +def test_16bit_pgm(): + im = Image.open('Tests/images/16_bit_binary.pgm') + im.load() + assert_equal(im.mode, 'I') + assert_equal(im.size, (20,100)) + + tgt = Image.open('Tests/images/16_bit_binary_pgm.png') + assert_image_equal(im, tgt) + From f5ba642b5e49253f7256e08aa5e53226aac1194e Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 7 Apr 2014 22:12:33 -0700 Subject: [PATCH 14/56] Read support for 16 bit pgm file --- PIL/PpmImagePlugin.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/PIL/PpmImagePlugin.py b/PIL/PpmImagePlugin.py index 9aa5b1135..d7af308c2 100644 --- a/PIL/PpmImagePlugin.py +++ b/PIL/PpmImagePlugin.py @@ -96,7 +96,18 @@ class PpmImageFile(ImageFile.ImageFile): ysize = s if mode == "1": break - + elif ix == 2: + # maxgrey + if s > 255: + if not mode == 'L': + raise ValueError("Too many colors for band: %s" %s) + if s <= 2**16 - 1: + self.mode = 'I' + rawmode = 'I;16B' + else: + self.mode = 'I'; + rawmode = 'I;32B' + self.size = xsize, ysize self.tile = [("raw", (0, 0, xsize, ysize), From 2daac27713dfaf75ac7af872e891d357734b6706 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 7 Apr 2014 22:22:42 -0700 Subject: [PATCH 15/56] Tests for writing hi bit pgm --- Tests/test_file_ppm.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 5e0aa84ac..34136a83c 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -22,3 +22,15 @@ def test_16bit_pgm(): tgt = Image.open('Tests/images/16_bit_binary_pgm.png') assert_image_equal(im, tgt) + +def test_16bit_pgm_write(): + im = Image.open('Tests/images/16_bit_binary.pgm') + im.load() + + f = tempfile('temp.pgm') + assert_no_exception(lambda: im.save(f, 'PPM')) + + reloaded = Image.open(f) + assert_image_equal(im, reloaded) + + From 327ea209b8dd91b60945d74e3c32edbebee63810 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 7 Apr 2014 22:23:04 -0700 Subject: [PATCH 16/56] Write support, fixes #602 --- PIL/PpmImagePlugin.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/PIL/PpmImagePlugin.py b/PIL/PpmImagePlugin.py index d7af308c2..a5f01891f 100644 --- a/PIL/PpmImagePlugin.py +++ b/PIL/PpmImagePlugin.py @@ -127,6 +127,11 @@ def _save(im, fp, filename): rawmode, head = "1;I", b"P4" elif im.mode == "L": rawmode, head = "L", b"P5" + elif im.mode == "I": + if im.getextrema()[1] < 2**16: + rawmode, head = "I;16B", b"P5" + else: + rawmode, head = "I;32B", b"P5" elif im.mode == "RGB": rawmode, head = "RGB", b"P6" elif im.mode == "RGBA": @@ -134,8 +139,15 @@ def _save(im, fp, filename): else: raise IOError("cannot write mode %s as PPM" % im.mode) fp.write(head + ("\n%d %d\n" % im.size).encode('ascii')) - if head != b"P4": + if head == b"P6": fp.write(b"255\n") + if head == b"P5": + if rawmode == "I": + fp.write(b"255\n") + elif rawmode == "I;16B": + fp.write(b"65535\n") + elif rawmode == "I;32B": + fp.write(b"2147483648\n") ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, 0, 1))]) # ALTERNATIVE: save via builtin debug function From 213cec00cb5556c1be735326280c7e649999531e Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 7 Apr 2014 22:24:48 -0700 Subject: [PATCH 17/56] consistency --- PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PIL/PpmImagePlugin.py b/PIL/PpmImagePlugin.py index a5f01891f..328892323 100644 --- a/PIL/PpmImagePlugin.py +++ b/PIL/PpmImagePlugin.py @@ -101,7 +101,7 @@ class PpmImageFile(ImageFile.ImageFile): if s > 255: if not mode == 'L': raise ValueError("Too many colors for band: %s" %s) - if s <= 2**16 - 1: + if s < 2**16: self.mode = 'I' rawmode = 'I;16B' else: From e4b8cdac4c5146c48f5ed2634b5d005df37ad7ad Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 8 Apr 2014 12:22:39 +0300 Subject: [PATCH 18/56] Update .gitignore from https://github.com/github/gitignore keeping Vim and emacs cruft --- .gitignore | 59 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f16a1f9a8..a0ba1b4c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,56 @@ -*.pyc -*.egg-info -build -dist -.tox +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions *.so -docs/_build + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ # Vim cruft .*.swp @@ -13,3 +59,4 @@ docs/_build *~ \#*# .#* + From 3e9cde4412bf86a646139f627a71ecf6e8d3796b Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 8 Apr 2014 12:28:00 +0300 Subject: [PATCH 19/56] Use tester.py's built-in coverage and coverage.py's built-in cleanup --- .travis.yml | 2 +- Tests/tester.py | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index b104b65e2..34ffcfe1a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ script: - python setup.py clean - python setup.py build_ext --inplace - coverage run --append --include=PIL/* selftest.py - - coverage run --append --include=PIL/* Tests/run.py + - python Tests/run.py --coverage after_success: - coverage report diff --git a/Tests/tester.py b/Tests/tester.py index f7e2c26c6..8543a0d28 100644 --- a/Tests/tester.py +++ b/Tests/tester.py @@ -83,7 +83,7 @@ def assert_deep_equal(a, b, msg=None): else: failure(msg or "got %s, expected %s" % (a,b)) else: - failure(msg or "got length %s, expected %s" % (len(a), len(b))) + failure(msg or "got length %s, expected %s" % (len(a), len(b))) except: assert_equal(a,b,msg) @@ -285,13 +285,6 @@ def _setup(): except OSError: pass - if "--coverage" in sys.argv: - import coverage - coverage.stop() - # The coverage module messes up when used from inside an - # atexit handler. Do an explicit save to make sure that - # we actually flush the coverage cache. - coverage.the_coverage.save() import atexit, sys atexit.register(report) if "--coverage" in sys.argv: From ed00b2e6d29e18aef4a6419fae9d06b091fa65d0 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 8 Apr 2014 12:38:30 +0300 Subject: [PATCH 20/56] Read existing data coverage on measurement start, save on stop. Only measure PIL code. --- Tests/tester.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/tester.py b/Tests/tester.py index 8543a0d28..f2d11ed14 100644 --- a/Tests/tester.py +++ b/Tests/tester.py @@ -289,7 +289,8 @@ def _setup(): atexit.register(report) if "--coverage" in sys.argv: import coverage - coverage.start() + cov = coverage.coverage(auto_data=True, include="PIL/*") + cov .start() if "--log" in sys.argv: _logfile = open("test.log", "a") From 5afdd6cb541e89bcf21816a76a6934576c4d0f3e Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 8 Apr 2014 17:17:10 +0300 Subject: [PATCH 21/56] pep8 Tests/tester.py --- Tests/tester.py | 83 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/Tests/tester.py b/Tests/tester.py index f2d11ed14..510486b1f 100644 --- a/Tests/tester.py +++ b/Tests/tester.py @@ -6,15 +6,13 @@ warnings.simplefilter('default') # temporarily turn off resource warnings that warn about unclosed # files in the test scripts. try: - warnings.filterwarnings("ignore", category=ResourceWarning) + warnings.filterwarnings("ignore", category=ResourceWarning) except NameError: - # we expect a NameError on py2.x, since it doesn't have ResourceWarnings. - pass - - + # we expect a NameError on py2.x, since it doesn't have ResourceWarnings. + pass import sys -py3 = (sys.version_info >= (3,0)) +py3 = (sys.version_info >= (3, 0)) # some test helpers @@ -22,6 +20,7 @@ _target = None _tempfiles = [] _logfile = None + def success(): import sys success.count += 1 @@ -29,8 +28,10 @@ def success(): print(sys.argv[0], success.count, failure.count, file=_logfile) return True + def failure(msg=None, frame=None): - import sys, linecache + import sys + import linecache failure.count += 1 if _target: if frame is None: @@ -49,6 +50,7 @@ def failure(msg=None, frame=None): success.count = failure.count = 0 + # predicates def assert_true(v, msg=None): @@ -57,35 +59,39 @@ def assert_true(v, msg=None): else: failure(msg or "got %r, expected true value" % v) + def assert_false(v, msg=None): if v: failure(msg or "got %r, expected false value" % v) else: success() + def assert_equal(a, b, msg=None): if a == b: success() else: failure(msg or "got %r, expected %r" % (a, b)) + def assert_almost_equal(a, b, msg=None, eps=1e-6): if abs(a-b) < eps: success() else: failure(msg or "got %r, expected %r" % (a, b)) + def assert_deep_equal(a, b, msg=None): try: if len(a) == len(b): - if all([x==y for x,y in zip(a,b)]): + if all([x == y for x, y in zip(a, b)]): success() else: - failure(msg or "got %s, expected %s" % (a,b)) + failure(msg or "got %s, expected %s" % (a, b)) else: failure(msg or "got length %s, expected %s" % (len(a), len(b))) except: - assert_equal(a,b,msg) + assert_equal(a, b, msg) def assert_match(v, pattern, msg=None): @@ -95,8 +101,10 @@ def assert_match(v, pattern, msg=None): else: failure(msg or "got %r, doesn't match pattern %r" % (v, pattern)) + def assert_exception(exc_class, func): - import sys, traceback + import sys + import traceback try: func() except exc_class: @@ -108,8 +116,10 @@ def assert_exception(exc_class, func): else: failure("expected %r exception, got no exception" % exc_class.__name__) + def assert_no_exception(func): - import sys, traceback + import sys + import traceback try: func() except: @@ -118,11 +128,14 @@ def assert_no_exception(func): else: success() + def assert_warning(warn_class, func): # note: this assert calls func three times! import warnings + def warn_error(message, category=UserWarning, **options): raise category(message) + def warn_ignore(message, category=UserWarning, **options): pass warn = warnings.warn @@ -134,22 +147,25 @@ def assert_warning(warn_class, func): warnings.warn = warn_error assert_exception(warn_class, func) finally: - warnings.warn = warn # restore + warnings.warn = warn # restore return result # helpers from io import BytesIO + def fromstring(data): from PIL import Image return Image.open(BytesIO(data)) + def tostring(im, format, **options): out = BytesIO() im.save(out, format, **options) return out.getvalue() + def lena(mode="RGB", cache={}): from PIL import Image im = cache.get(mode) @@ -165,6 +181,7 @@ def lena(mode="RGB", cache={}): cache[mode] = im return im + def assert_image(im, mode, size, msg=None): if mode is not None and im.mode != mode: failure(msg or "got mode %r, expected %r" % (im.mode, mode)) @@ -173,6 +190,7 @@ def assert_image(im, mode, size, msg=None): else: success() + def assert_image_equal(a, b, msg=None): if a.mode != b.mode: failure(msg or "got mode %r, expected %r" % (a.mode, b.mode)) @@ -184,6 +202,7 @@ def assert_image_equal(a, b, msg=None): else: success() + def assert_image_similar(a, b, epsilon, msg=None): epsilon = float(epsilon) if a.mode != b.mode: @@ -193,19 +212,25 @@ def assert_image_similar(a, b, epsilon, msg=None): diff = 0 try: ord(b'0') - for abyte,bbyte in zip(a.tobytes(),b.tobytes()): + for abyte, bbyte in zip(a.tobytes(), b.tobytes()): diff += abs(ord(abyte)-ord(bbyte)) except: - for abyte,bbyte in zip(a.tobytes(),b.tobytes()): + for abyte, bbyte in zip(a.tobytes(), b.tobytes()): diff += abs(abyte-bbyte) ave_diff = float(diff)/(a.size[0]*a.size[1]) if epsilon < ave_diff: - return failure(msg or "average pixel value difference %.4f > epsilon %.4f" %(ave_diff, epsilon)) + return failure( + msg or "average pixel value difference %.4f > epsilon %.4f" % ( + ave_diff, epsilon)) else: return success() + def tempfile(template, *extra): - import os, os.path, sys, tempfile + import os + import os.path + import sys + import tempfile files = [] root = os.path.join(tempfile.gettempdir(), 'pillow-tests') try: @@ -222,18 +247,20 @@ def tempfile(template, *extra): _tempfiles.extend(files) return files[0] + # test runner def run(): global _target, _tests, run - import sys, traceback + import sys + import traceback _target = sys.modules["__main__"] - run = None # no need to run twice + run = None # no need to run twice tests = [] for name, value in list(vars(_target).items()): if name[:5] == "test_" and type(value) is type(success): tests.append((value.__code__.co_firstlineno, name, value)) - tests.sort() # sort by line + tests.sort() # sort by line for lineno, name, func in tests: try: _tests = [] @@ -251,41 +278,49 @@ def run(): sys.argv[0], lineno, v)) failure.count += 1 + def yield_test(function, *args): # collect delayed/generated tests _tests.append((function, args)) + def skip(msg=None): import os print("skip") - os._exit(0) # don't run exit handlers + os._exit(0) # don't run exit handlers + def ignore(pattern): """Tells the driver to ignore messages matching the pattern, for the duration of the current test.""" print('ignore: %s' % pattern) + def _setup(): global _logfile + def report(): if run: run() if success.count and not failure.count: print("ok") # only clean out tempfiles if test passed - import os, os.path, tempfile + import os + import os.path + import tempfile for file in _tempfiles: try: os.remove(file) except OSError: - pass # report? + pass # report? temp_root = os.path.join(tempfile.gettempdir(), 'pillow-tests') try: os.rmdir(temp_root) except OSError: pass - import atexit, sys + import atexit + import sys atexit.register(report) if "--coverage" in sys.argv: import coverage From 9210f88102000fa207dfbbe93516c6f1951d4d80 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 8 Apr 2014 17:20:52 +0300 Subject: [PATCH 22/56] Start coverage measurement earlier so tests are properly covered --- Tests/tester.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/tester.py b/Tests/tester.py index 510486b1f..5900a7f3a 100644 --- a/Tests/tester.py +++ b/Tests/tester.py @@ -299,6 +299,12 @@ def ignore(pattern): def _setup(): global _logfile + import sys + if "--coverage" in sys.argv: + import coverage + cov = coverage.coverage(auto_data=True, include="PIL/*") + cov.start() + def report(): if run: run() @@ -320,12 +326,8 @@ def _setup(): pass import atexit - import sys atexit.register(report) - if "--coverage" in sys.argv: - import coverage - cov = coverage.coverage(auto_data=True, include="PIL/*") - cov .start() + if "--log" in sys.argv: _logfile = open("test.log", "a") From 88e235f3f540ea7629b4db3dff7d64437bdd596b Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 8 Apr 2014 22:43:57 -0700 Subject: [PATCH 23/56] sanity check on ascii integers --- PIL/PpmImagePlugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PIL/PpmImagePlugin.py b/PIL/PpmImagePlugin.py index 328892323..a7cb08e75 100644 --- a/PIL/PpmImagePlugin.py +++ b/PIL/PpmImagePlugin.py @@ -63,7 +63,11 @@ class PpmImageFile(ImageFile.ImageFile): c = self.fp.read(1) if not c or c in b_whitespace: break + if c > b'\x79': + raise ValueError("Expected ASCII value, found binary") s = s + c + if (len(s) > 9): + raise ValueError("Expected int, got > 9 digits") return s def _open(self): From 398450a5e49b73c5b6209c15a1fd8b316a650844 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 8 Apr 2014 22:44:24 -0700 Subject: [PATCH 24/56] Fix failing tests, turns out I is different than L --- PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PIL/PpmImagePlugin.py b/PIL/PpmImagePlugin.py index a7cb08e75..070efd185 100644 --- a/PIL/PpmImagePlugin.py +++ b/PIL/PpmImagePlugin.py @@ -146,7 +146,7 @@ def _save(im, fp, filename): if head == b"P6": fp.write(b"255\n") if head == b"P5": - if rawmode == "I": + if rawmode == "L": fp.write(b"255\n") elif rawmode == "I;16B": fp.write(b"65535\n") From b27ef7646855d4e27d8e3dca528fd2e847756ac8 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 8 Apr 2014 23:42:34 -0700 Subject: [PATCH 25/56] Rename import_err to something more general --- PIL/ImageCms.py | 4 ++-- PIL/_util.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PIL/ImageCms.py b/PIL/ImageCms.py index c875712c1..363650250 100644 --- a/PIL/ImageCms.py +++ b/PIL/ImageCms.py @@ -89,8 +89,8 @@ try: except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. - from _util import import_err - _imagingcms = import_err(ex) + from _util import deferred_error + _imagingcms = deferred_error(ex) from PIL._util import isStringType core = _imagingcms diff --git a/PIL/_util.py b/PIL/_util.py index 761c258f1..eb5c2c242 100644 --- a/PIL/_util.py +++ b/PIL/_util.py @@ -15,7 +15,7 @@ else: def isDirectory(f): return isPath(f) and os.path.isdir(f) -class import_err(object): +class deferred_error(object): def __init__(self, ex): self.ex = ex def __getattr__(self, elt): From 3d352329f45139458bdf3b11a2352cc24b4c6c47 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 8 Apr 2014 23:43:13 -0700 Subject: [PATCH 26/56] Use the deferred error to provide a logical exception on access to a closed image --- PIL/Image.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index 36060c759..99acb78fa 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -92,7 +92,7 @@ except ImportError: from PIL import ImageMode from PIL._binary import i8, o8 -from PIL._util import isPath, isStringType +from PIL._util import isPath, isStringType, deferred_error import os, sys @@ -513,7 +513,10 @@ class Image: if Image.DEBUG: print ("Error closing: %s" %msg) - self.im = None + # Instead of simply setting to None, we're setting up a + # deferred error that will better explain that the core image + # object is gone. + self.im = deferred_error(ValueError("Operation on closed image")) def _copy(self): From dd6067dbf4f6e9c9691ec6fcb9e06f8d5122065f Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 9 Apr 2014 10:17:20 +0300 Subject: [PATCH 27/56] Coverage badge in README [ci skip] --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 78a256c0d..cf9039b70 100644 --- a/README.rst +++ b/README.rst @@ -16,4 +16,7 @@ Pillow is the "friendly" PIL fork by Alex Clark and Contributors. PIL is the Pyt :target: https://pypi.python.org/pypi/Pillow/ :alt: Number of PyPI downloads +.. image:: https://coveralls.io/repos/python-imaging/Pillow/badge.png?branch=master + :target: https://coveralls.io/r/python-imaging/Pillow?branch=master + The documentation is hosted at http://pillow.readthedocs.org/. It contains installation instructions, tutorials, reference, compatibility details, and more. From 1cb4bf7c1514c866c28c4b79ec22e885499aaab0 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 9 Apr 2014 10:33:13 +0300 Subject: [PATCH 28/56] SVG Travis, and Coverage badge in docs [ci skip] The SVG badge uses the Shields project for consistency with other projects' badges. More info: http://blog.travis-ci.com/2014-03-20-build-status-badges-support-svg/ --- docs/index.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index a59cda115..520addd93 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,8 +4,9 @@ Pillow Pillow is the 'friendly' PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. -.. image:: https://travis-ci.org/python-imaging/Pillow.png +.. image:: https://travis-ci.org/python-imaging/Pillow.svg?branch=master :target: https://travis-ci.org/python-imaging/Pillow + :alt: GitHub build status .. image:: https://pypip.in/v/Pillow/badge.png :target: https://pypi.python.org/pypi/Pillow/ @@ -15,6 +16,10 @@ Python Imaging Library by Fredrik Lundh and Contributors. :target: https://pypi.python.org/pypi/Pillow/ :alt: Number of PyPI downloads +.. image:: https://coveralls.io/repos/python-imaging/Pillow/badge.png?branch=master + :target: https://coveralls.io/r/python-imaging/Pillow?branch=master + :alt: Test coverage + To start using Pillow, please read the :doc:`installation instructions `. From 0378a89c6455bc67b1bea5f163e9fca649a1f650 Mon Sep 17 00:00:00 2001 From: hugovk Date: Wed, 9 Apr 2014 10:37:03 +0300 Subject: [PATCH 29/56] Use SVG Travis CI badge [ci skip] --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cf9039b70..277c5d01f 100644 --- a/README.rst +++ b/README.rst @@ -5,8 +5,9 @@ Pillow Pillow is the "friendly" PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. -.. image:: https://travis-ci.org/python-imaging/Pillow.png +.. image:: https://travis-ci.org/python-imaging/Pillow.svg?branch=master :target: https://travis-ci.org/python-imaging/Pillow + :alt: Travis CI build status .. image:: https://pypip.in/v/Pillow/badge.png :target: https://pypi.python.org/pypi/Pillow/ From 5a63e77097eaa9b8c331b24c5ae2978a14ce065d Mon Sep 17 00:00:00 2001 From: hugovk Date: Wed, 9 Apr 2014 10:40:16 +0300 Subject: [PATCH 30/56] Fix alt text [ci skip] --- docs/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 520addd93..25e9f6b73 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,4 @@ -Pillow +Pillow ====== Pillow is the 'friendly' PIL fork by Alex Clark and Contributors. PIL is the @@ -6,7 +6,7 @@ Python Imaging Library by Fredrik Lundh and Contributors. .. image:: https://travis-ci.org/python-imaging/Pillow.svg?branch=master :target: https://travis-ci.org/python-imaging/Pillow - :alt: GitHub build status + :alt: Travis CI build status .. image:: https://pypip.in/v/Pillow/badge.png :target: https://pypi.python.org/pypi/Pillow/ @@ -53,7 +53,7 @@ Pillow is a volunteer effort led by Alex Clark. If you can't help with development please consider helping us financially. Your assistance would be very much appreciated! -.. note:: Contributors please add your name and donation preference here. +.. note:: Contributors please add your name and donation preference here. ======================================= ======================================= **Developer** **Preference** From 029a4a5079a6d33edad9c7cf073bf9f11fe0d1a9 Mon Sep 17 00:00:00 2001 From: Hijackal Date: Thu, 10 Apr 2014 15:52:53 +0200 Subject: [PATCH 31/56] Add specific 32-bit float tiff format We regularly use this format to store 32bit floats and I would like to see it handled by clean Pillow installations without having to add it on every system I use. --- PIL/TiffImagePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 18d5909dc..fe658d22c 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -146,6 +146,7 @@ OPEN_INFO = { (II, 0, 1, 2, (1,), ()): ("1", "1;IR"), (II, 0, 1, 1, (8,), ()): ("L", "L;I"), (II, 0, 1, 2, (8,), ()): ("L", "L;IR"), + (II, 0, 3, 1, (32,), ()): ("F", "F;32F"), (II, 1, 1, 1, (1,), ()): ("1", "1"), (II, 1, 1, 2, (1,), ()): ("1", "1;R"), (II, 1, 1, 1, (8,), ()): ("L", "L"), From 267cdf523eb1280f247e95dfb48949d870b7f18c Mon Sep 17 00:00:00 2001 From: hugovk Date: Fri, 11 Apr 2014 10:28:06 +0300 Subject: [PATCH 32/56] Print out lists of failed tests (and temporarily force a failure for testing) --- Tests/run.py | 31 ++++++++++++++++++++----------- Tests/test_mode_i16.py | 8 ++++++-- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Tests/run.py b/Tests/run.py index 01a3f3603..57fc5245d 100644 --- a/Tests/run.py +++ b/Tests/run.py @@ -2,7 +2,12 @@ from __future__ import print_function # minimal test runner -import glob, os, os.path, sys, tempfile, re +import glob +import os +import os.path +import re +import sys +import tempfile try: root = os.path.dirname(__file__) @@ -34,6 +39,7 @@ files.sort() success = failure = 0 include = [x for x in sys.argv[1:] if x[:2] != "--"] skipped = [] +failed = [] python_options = " ".join(python_options) tester_options = " ".join(tester_options) @@ -48,8 +54,8 @@ for file in files: # 2>&1 works on unix and on modern windowses. we might care about # very old Python versions, but not ancient microsoft products :-) out = os.popen("%s %s -u %s %s 2>&1" % ( - sys.executable, python_options, file, tester_options - )) + sys.executable, python_options, file, tester_options + )) result = out.read() # Extract any ignore patterns @@ -63,7 +69,7 @@ for file in files: if not p.endswith('$'): p = p + '$' return p - + ignore_res = [re.compile(fix_re(p), re.MULTILINE) for p in ignore_pats] except: print('(bad ignore patterns %r)' % ignore_pats) @@ -73,11 +79,11 @@ for file in files: result = r.sub('', result) result = result.strip() - + if result == "ok": result = None elif result == "skip": - print("---", "skipped") # FIXME: driver should include a reason + print("---", "skipped") # FIXME: driver should include a reason skipped.append(test) continue elif not result: @@ -91,7 +97,7 @@ for file in files: # if there's an ok at the end, it's not really ok result = result[:-3] print(result) - failure = failure + 1 + failed.append[test] else: success = success + 1 @@ -105,6 +111,7 @@ if tempfiles: print(file) print("-"*68) + def tests(n): if n == 1: return "1 test" @@ -112,10 +119,12 @@ def tests(n): return "%d tests" % n if skipped: - print("---", tests(len(skipped)), "skipped.") - print(skipped) -if failure: - print("***", tests(failure), "of", (success + failure), "failed.") + print("---", tests(len(skipped)), "skipped:") + print(", ".join(skipped)) +if failed: + failure = len(failed) + print("***", tests(failure), "of", (success + failure), "failed:") + print(", ".join(failed)) sys.exit(1) else: print(tests(success), "passed.") diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 4c1798509..2130ee3f6 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -2,6 +2,7 @@ from tester import * from PIL import Image + def verify(im1): im2 = lena("I") assert_equal(im1.size, im2.size) @@ -18,6 +19,7 @@ def verify(im1): return success() + def test_basic(): # PIL 1.1 has limited support for 16-bit image data. Check that # create/copy/transform and save works as expected. @@ -30,10 +32,10 @@ def test_basic(): w, h = imIn.size imOut = imIn.copy() - verify(imOut) # copy + verify(imOut) # copy imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) - verify(imOut) # transform + verify(imOut) # transform filename = tempfile("temp.im") imIn.save(filename) @@ -103,3 +105,5 @@ def test_convert(): verify(im.convert("I;16B")) verify(im.convert("I;16B").convert("L")) verify(im.convert("I;16B").convert("I")) + + assert(False) # TEMP FOR TESTING! From cf6daf03fedaa5a63172733394d8d023230c844d Mon Sep 17 00:00:00 2001 From: hugovk Date: Fri, 11 Apr 2014 10:47:37 +0300 Subject: [PATCH 33/56] Use correct brackets --- Tests/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/run.py b/Tests/run.py index 57fc5245d..4dccc005a 100644 --- a/Tests/run.py +++ b/Tests/run.py @@ -97,7 +97,7 @@ for file in files: # if there's an ok at the end, it's not really ok result = result[:-3] print(result) - failed.append[test] + failed.append(test) else: success = success + 1 From 9c6b07d21b84c66df7cc168a95e93da2721b9873 Mon Sep 17 00:00:00 2001 From: hugovk Date: Fri, 11 Apr 2014 10:59:58 +0300 Subject: [PATCH 34/56] Remove temporary forced failure --- Tests/test_mode_i16.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 2130ee3f6..782a26623 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -105,5 +105,3 @@ def test_convert(): verify(im.convert("I;16B")) verify(im.convert("I;16B").convert("L")) verify(im.convert("I;16B").convert("I")) - - assert(False) # TEMP FOR TESTING! From 7f164d641fffd02c5bc9be5079d1afd2bba0b19b Mon Sep 17 00:00:00 2001 From: hijackal Date: Fri, 11 Apr 2014 11:39:05 +0200 Subject: [PATCH 35/56] Test image: TIFF for OPEN_INFO = {(II, 0, 3, 1, (32,), ()): ("F", "F;32F")} --- Tests/images/10ct_32bit_128.tiff | Bin 0 -> 65634 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Tests/images/10ct_32bit_128.tiff diff --git a/Tests/images/10ct_32bit_128.tiff b/Tests/images/10ct_32bit_128.tiff new file mode 100644 index 0000000000000000000000000000000000000000..aa2546fae8a61b7e8f1b3e52eb589dc4a73270d6 GIT binary patch literal 65634 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 gjNF@lN^tw;DH3A8 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 Date: Fri, 11 Apr 2014 14:03:32 +0300 Subject: [PATCH 36/56] version 0.30 2014-02-04 --- PIL/OleFileIO-README.md | 322 +++++++++++++++++++++++++++++++++------- PIL/OleFileIO.py | 103 ++++++++++--- 2 files changed, 345 insertions(+), 80 deletions(-) diff --git a/PIL/OleFileIO-README.md b/PIL/OleFileIO-README.md index f02a548d6..4a4fdcbca 100644 --- a/PIL/OleFileIO-README.md +++ b/PIL/OleFileIO-README.md @@ -1,27 +1,22 @@ OleFileIO_PL ============ -[OleFileIO_PL](http://www.decalage.info/python/olefileio) is a Python module to read [Microsoft OLE2 files (also called Structured Storage, Compound File Binary Format or Compound Document File Format)](http://en.wikipedia.org/wiki/Compound_File_Binary_Format), such as Microsoft Office documents, Image Composer and FlashPix files, Outlook messages, ... +[OleFileIO_PL](http://www.decalage.info/python/olefileio) is a Python module to parse and read [Microsoft OLE2 files (also called Structured Storage, Compound File Binary Format or Compound Document File Format)](http://en.wikipedia.org/wiki/Compound_File_Binary_Format), such as Microsoft Office documents, Image Composer and FlashPix files, Outlook messages, StickyNotes, several Microscopy file formats ... -This is an improved version of the OleFileIO module from [PIL](http://www.pythonware.com/products/pil/index.htm), the excellent Python Imaging Library, created and maintained by Fredrik Lundh. The API is still compatible with PIL, but I have improved the internal implementation significantly, with new features, bugfixes and a more robust design. +This is an improved version of the OleFileIO module from [PIL](http://www.pythonware.com/products/pil/index.htm), the excellent Python Imaging Library, created and maintained by Fredrik Lundh. The API is still compatible with PIL, but since 2005 I have improved the internal implementation significantly, with new features, bugfixes and a more robust design. As far as I know, this module is now the most complete and robust Python implementation to read MS OLE2 files, portable on several operating systems. (please tell me if you know other similar Python modules) -WARNING: THIS IS (STILL) WORK IN PROGRESS. +OleFileIO_PL can be used as an independent module or with PIL. The goal is to have it integrated into [Pillow](http://python-imaging.github.io/), the friendly fork of PIL. -Main improvements over PIL version of OleFileIO: ------------------------------------------------- - -- Better compatibility with Python 2.6 (also compatible with Python 3.0+) -- Support for files larger than 6.8MB -- Robust: many checks to detect malformed files -- Improved API -- New features: metadata extraction, stream/storage timestamps -- Added setup.py and install.bat to ease installation +OleFileIO\_PL is mostly meant for developers. If you are looking for tools to analyze OLE files or to extract data, then please also check [python-oletools](http://www.decalage.info/python/oletools), which are built upon OleFileIO_PL. News ---- +Follow all updates and news on Twitter: + +- **2014-02-04 v0.30**: now compatible with Python 3.x, thanks to Martin Panter who did most of the hard work. - 2013-07-24 v0.26: added methods to parse stream/storage timestamps, improved listdir to include storages, fixed parsing of direntry timestamps - 2013-05-27 v0.25: improved metadata extraction, properties parsing and exception handling, fixed [issue #12](https://bitbucket.org/decalage/olefileio_pl/issue/12/error-when-converting-timestamps-in-ole) - 2013-05-07 v0.24: new features to extract metadata (get\_metadata method and OleMetadata class), improved getproperties to convert timestamps to Python datetime @@ -34,47 +29,224 @@ News - 2009-12-10 v0.19: fixed support for 64 bits platforms (thanks to Ben G. and Martijn for reporting the bug) - see changelog in source code for more info. -Download: ---------- +Download +-------- The archive is available on [the project page](https://bitbucket.org/decalage/olefileio_pl/downloads). +Features +-------- -How to use this module: ------------------------ +- Parse and read any OLE file such as Microsoft Office 97-2003 legacy document formats (Word .doc, Excel .xls, PowerPoint .ppt, Visio .vsd, Project .mpp), Image Composer and FlashPix files, Outlook messages, StickyNotes, Zeiss AxioVision ZVI files, Olympus FluoView OIB files, ... +- List all the streams and storages contained in an OLE file +- Open streams as files +- Parse and read property streams, containing metadata of the file +- Portable, pure Python module, no dependency -See sample code at the end of the module, and also docstrings. -Here are a few examples: +Main improvements over the original version of OleFileIO in PIL: +---------------------------------------------------------------- + +- Compatible with Python 3.x and 2.6+ +- Many bug fixes +- Support for files larger than 6.8MB +- Support for 64 bits platforms and big-endian CPUs +- Robust: many checks to detect malformed files +- Runtime option to choose if malformed files should be parsed or raise exceptions +- Improved API +- Metadata extraction, stream/storage timestamps (e.g. for document forensics) +- Can open file-like objects +- Added setup.py and install.bat to ease installation +- More convenient slash-based syntax for stream paths + + + +How to use this module +---------------------- + +OleFileIO_PL can be used as an independent module or with PIL. The main functions and methods are explained below. + +For more information, see also the file **OleFileIO_PL.html**, 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 '\x05'. + +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) + + + +### Import OleFileIO_PL ### :::python import OleFileIO_PL - - # Test if a file is an OLE container: + +As of version 0.30, the code has been changed to be compatible with Python 3.x. As a consequence, compatibility with Python 2.5 or older is not provided anymore. However, a copy of v0.26 is available as OleFileIO_PL2.py. If your application needs to be compatible with Python 2.5 or older, you may use the following code to load the old version when needed: + + :::python + try: + import OleFileIO_PL + except: + import OleFileIO_PL2 as OleFileIO_PL + +If you think OleFileIO_PL should stay compatible with Python 2.5 or older, please [contact me](http://decalage.info/contact). + + +### 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 (new in v0.16). + + :::python assert OleFileIO_PL.isOleFile('myfile.doc') - - # Open an OLE file from disk: + + +### Open an OLE file from disk ### + +Create an OleFileIO object with the file path as parameter: + + :::python ole = OleFileIO_PL.OleFileIO('myfile.doc') - - # Get list of streams: + +### 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. + + :::python + ole = OleFileIO_PL.OleFileIO(f) + +For example the code below reads a file into a string, then uses BytesIO to turn it into a file-like object. + + :::python + data = open('myfile.doc', 'rb').read() + f = io.BytesIO(data) # or StringIO.StringIO for Python 2.x + ole = OleFileIO_PL.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 (new in v0.14): + + :::python + ole = OleFileIO_PL.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 (new in 0.25): + + :::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. (new in v0.15) + +Both are case-insensitive. + +Switching between the two is easy: + + :::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. + + :::python print(ole.listdir()) - - # Test if known streams/storages exist: + +Sample result: + + :::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 (new in v0.26): + + :::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 (new in v0.16). + + :::python if ole.exists('worddocument'): print("This is a Word document.") - print("size :", ole.get_size('worddocument')) if ole.exists('macros/vba'): print("This document seems to contain VBA macros.") - # Extract the "Pictures" stream from a PPT file: - if ole.exists('Pictures'): - pics = ole.openstream('Pictures') - data = pics.read() - f = open('Pictures.bin', 'wb') - f.write(data) - f.close() - # Extract metadata (new in v0.24) - see source code for all attributes: +### 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: + + :::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 (new in v0.16): + + :::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 (new in v0.15). + + :::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 (new in v0.26). + + :::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 (new in v0.26): + + :::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 (new in v0.24). + + :::python meta = ole.get_metadata() print('Author:', meta.author) print('Title:', meta.title) @@ -82,29 +254,67 @@ Here are a few examples: # print all metadata: meta.dump() - # Close the OLE file: +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 '\x05SummaryInformation', the document title is property #2, and the subject is #3. + + :::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 '\x05SummaryInformation'), the list of indexes can be passed as no_conversion (new in v0.25): + + :::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. (new in v0.22) + + :::python ole.close() - - # Work with a file-like object (e.g. StringIO) instead of a file on disk: - data = open('myfile.doc', 'rb').read() - f = io.BytesIO(data) - ole = OleFileIO_PL.OleFileIO(f) - print(ole.listdir()) - ole.close() - - -It can also be used as a script from the command-line to display the structure of an OLE file, for example: + +### Use OleFileIO_PL as a script ### + +OleFileIO_PL can also be used as a script from the command-line to display the structure of an OLE file and its metadata, for example: OleFileIO_PL.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. + +## Real-life examples ## + A real-life example: [using OleFileIO_PL for malware analysis and forensics](http://blog.gregback.net/2011/03/using-remnux-for-forensic-puzzle-6/). -How to contribute: ------------------- +See also [this paper](https://computer-forensics.sans.org/community/papers/gcfa/grow-forensic-tools-taxonomy-python-libraries-helpful-forensic-analysis_6879) about python tools for forensics, which features OleFileIO_PL. + +About Python 2 and 3 +-------------------- + +OleFileIO\_PL used to support only Python 2.x. As of version 0.30, the code has been changed to be compatible with Python 3.x. As a consequence, compatibility with Python 2.5 or older is not provided anymore. However, a copy of v0.26 is available as OleFileIO_PL2.py. See above the "import" section for a workaround. + +If you think OleFileIO_PL should stay compatible with Python 2.5 or older, please [contact me](http://decalage.info/contact). + +How to contribute +----------------- The code is available in [a Mercurial repository on bitbucket](https://bitbucket.org/decalage/olefileio_pl). 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, you may also send an e-mail to decalage(at)laposte.net. You can help in many ways: +If you would like to help us improve this module, or simply provide feedback, please [contact me](http://decalage.info/contact). You can help in many ways: - test this module on different platforms / Python versions - find and report bugs @@ -112,21 +322,21 @@ If you would like to help us improve this module, or simply provide feedback, yo - write unittest test cases - provide tricky malformed files -How to report bugs: -------------------- +How to report bugs +------------------ -To report a bug, for example a normal file which is not parsed correctly, please use the [issue reporting page](https://bitbucket.org/decalage/olefileio_pl/issues?status=new&status=open), or send an e-mail with an attachment containing the debugging output of OleFileIO_PL. +To report a bug, for example a normal file which is not parsed correctly, please use the [issue reporting page](https://bitbucket.org/decalage/olefileio_pl/issues?status=new&status=open), or if you prefer to do it privately, use this [contact form](http://decalage.info/contact). Please provide all the information about the context and how to reproduce the bug. -For this, launch the following command : +If possible please join the debugging output of OleFileIO_PL. For this, launch the following command : - OleFileIO_PL.py -d -c file >debug.txt + OleFileIO_PL.py -d -c file >debug.txt License ------- OleFileIO_PL is open-source. -OleFileIO_PL changes are Copyright (c) 2005-2013 by Philippe Lagadec. +OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec. The Python Imaging Library (PIL) is @@ -138,4 +348,4 @@ By obtaining, using, and/or copying this software and/or its associated document Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/PIL/OleFileIO.py b/PIL/OleFileIO.py index 91074be6b..c99300dc1 100644 --- a/PIL/OleFileIO.py +++ b/PIL/OleFileIO.py @@ -2,11 +2,12 @@ # -*- 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, ... +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.26 2013-07-24 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 @@ -16,25 +17,30 @@ 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-2013 by Philippe Lagadec +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. """ -from __future__ import print_function +# 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 +# (main requirement to be compatible with Python 3.x) +# The comment on the line below should be printed on Python 2.5 or older: +from __future__ import print_function # This version of OleFileIO_PL requires Python 2.6+ or 3.x. + __author__ = "Philippe Lagadec, Fredrik Lundh (Secret Labs AB)" -__date__ = "2013-07-24" -__version__ = '0.26' +__date__ = "2014-02-04" +__version__ = '0.30' #--- LICENSE ------------------------------------------------------------------ # OleFileIO_PL is an improved version of the OleFileIO module from the # Python Imaging Library (PIL). -# OleFileIO_PL changes are Copyright (c) 2005-2013 by Philippe Lagadec +# OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec # # The Python Imaging Library (PIL) is # Copyright (c) 1997-2005 by Secret Labs AB @@ -133,9 +139,14 @@ __version__ = '0.26' # of a directory entry or a storage/stream # - fixed parsing of direntry timestamps # 2013-07-24 PL: - new options in listdir to list storages and/or streams +# 2014-02-04 v0.30 PL: - upgraded code to support Python 3.x by Martin Panter +# - several fixes for Python 2.6 (xrange, MAGIC) +# - reused i32 from Pillow's _binary #----------------------------------------------------------------------------- # TODO (for version 1.0): +# + isOleFile should accept file-like objects like open +# + fix how all the methods handle unicode str and/or bytes as arguments # + add path attrib to _OleDirEntry, set it once and for all in init or # append_kids (then listdir/_list can be simplified) # - TESTS with Linux, MacOSX, Python 1.5.2, various files, PIL, ... @@ -220,17 +231,26 @@ __version__ = '0.26' #------------------------------------------------------------------------------ + import io import sys -from PIL import _binary import struct, array, os.path, datetime #[PL] Define explicitly the public API to avoid private objects in pydoc: __all__ = ['OleFileIO', 'isOleFile', 'MAGIC'] +# For Python 3.x, need to redefine long as int: if str is not bytes: long = int +# Need to make sure we use xrange both on Python 2 and 3.x: +try: + # on Python 2 we need xrange: + iterrange = xrange +except: + # no xrange, for Python 3 it was renamed as range: + iterrange = range + #[PL] workaround to fix an issue with array item size on 64 bits systems: if array.array('L').itemsize == 4: # on 32 bits platforms, long integers in an array are 32 bits: @@ -281,8 +301,7 @@ def set_debug_mode(debug_mode): else: debug = debug_pass -#TODO: convert this to hex -MAGIC = b'\320\317\021\340\241\261\032\341' +MAGIC = b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1' #[PL]: added constants for Sector IDs (from AAF specifications) MAXREGSECT = 0xFFFFFFFA; # maximum SECT @@ -362,9 +381,39 @@ def isOleFile (filename): return False -i8 = _binary.i8 -i16 = _binary.i16le -i32 = _binary.i32le +if bytes is str: + # version for Python 2.x + def i8(c): + return ord(c) +else: + # version for Python 3.x + def i8(c): + return c if c.__class__ is int else c[0] + + +#TODO: replace i16 and i32 with more readable struct.unpack equivalent? + +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 + """ + return i8(c[o]) | (i8(c[o+1])<<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 + """ +## 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 + # copied from Pillow's _binary: + return i8(c[o]) | (i8(c[o+1])<<8) | (i8(c[o+2])<<16) | (i8(c[o+3])<<24) def _clsid(clsid): @@ -373,7 +422,9 @@ def _clsid(clsid): clsid: string of length 16. """ assert len(clsid) == 16 - if clsid == bytearray(16): + # if clsid is only made of null bytes, return an empty string: + # (PL: why not simply return the string with zeroes?) + if not clsid.strip(b"\0"): return "" return (("%08X-%04X-%04X-%02X%02X-" + "%02X" * 6) % ((i32(clsid, 0), i16(clsid, 4), i16(clsid, 6)) + @@ -902,18 +953,22 @@ class _OleDirectoryEntry: def __eq__(self, other): "Compare entries by name" return self.name == other.name + def __lt__(self, other): "Compare entries by name" return self.name < other.name - #TODO: replace by the same function as MS implementation ? - # (order by name length first, then case-insensitive order) - + def __ne__(self, other): return not self.__eq__(other) + def __le__(self, other): return self.__eq__(other) or self.__lt__(other) + # Reflected __lt__() and __le__() will be used for __gt__() and __ge__() + #TODO: replace by the same function as MS implementation ? + # (order by name length first, then case-insensitive order) + def dump(self, tab = 0): "Dump this entry, and all its subentries (for debug purposes only)" @@ -978,7 +1033,7 @@ class OleFileIO: if entry[1:2] == "Image": fin = ole.openstream(entry) fout = open(entry[0:1], "wb") - while True: + while 1: s = fin.read(8192) if not s: break @@ -1046,7 +1101,7 @@ class OleFileIO: #TODO: if larger than 1024 bytes, this could be the actual data => BytesIO self.fp = open(filename, "rb") # old code fails if filename is not a plain string: - #if isPath(filename): + #if isinstance(filename, (bytes, basestring)): # self.fp = open(filename, "rb") #else: # self.fp = filename @@ -1133,7 +1188,7 @@ class OleFileIO: ) = struct.unpack(fmt_header, header1) debug( struct.unpack(fmt_header, header1)) - if self.Sig != b'\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1': + if self.Sig != MAGIC: # OLE signature should always be present self._raise_defect(DEFECT_FATAL, "incorrect OLE signature") if self.clsid != bytearray(16): @@ -1385,7 +1440,7 @@ class OleFileIO: if self.csectDif != nb_difat: raise IOError('incorrect DIFAT') isect_difat = self.sectDifStart - for i in range(nb_difat): + for i in iterrange(nb_difat): debug( "DIFAT block %d, sector %X" % (i, isect_difat) ) #TODO: check if corresponding FAT SID = DIFSECT sector_difat = self.getsect(isect_difat) @@ -1494,7 +1549,7 @@ class OleFileIO: #self.direntries = [] # We start with a list of "None" object self.direntries = [None] * max_entries -## for sid in range(max_entries): +## for sid in iterrange(max_entries): ## entry = fp.read(128) ## if not entry: ## break From 2271ee6f65ca559ca630407cf9459915371ecb52 Mon Sep 17 00:00:00 2001 From: hugovk Date: Fri, 11 Apr 2014 14:53:33 +0300 Subject: [PATCH 37/56] Add some simple tests for OleFileIO.py --- Tests/images/test-ole-file.doc | Bin 0 -> 22016 bytes Tests/test_olefileio.py | 101 +++++++++++++++++++++++++++++++++ Tests/tester.py | 28 +++++++++ 3 files changed, 129 insertions(+) create mode 100644 Tests/images/test-ole-file.doc create mode 100644 Tests/test_olefileio.py diff --git a/Tests/images/test-ole-file.doc b/Tests/images/test-ole-file.doc new file mode 100644 index 0000000000000000000000000000000000000000..07a1ff4e4dd41f919c482cecdb455526b4354418 GIT binary patch literal 22016 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 E0U#u=;Q#;t literal 0 HcmV?d00001 diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py new file mode 100644 index 000000000..3cd581b2e --- /dev/null +++ b/Tests/test_olefileio.py @@ -0,0 +1,101 @@ +from __future__ import print_function +from tester import * + +import PIL.OleFileIO as OleFileIO + + +def test_isOleFile_false(): + # Arrange + non_ole_file = "Tests/images/flower.jpg" + + # Act + is_ole = OleFileIO.isOleFile(non_ole_file) + + # Assert + assert_false(is_ole) + + +def test_isOleFile_true(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + + # Act + is_ole = OleFileIO.isOleFile(ole_file) + + # Assert + assert_true(is_ole) + + +def test_exists_worddocument(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + + # Act + exists = ole.exists('worddocument') + + # Assert + assert_true(exists) + + +def test_exists_no_vba_macros(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + + # Act + exists = ole.exists('macros/vba') + + # Assert + assert_false(exists) + + +def test_get_type(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + + # Act + type = ole.get_type('worddocument') + + # Assert + assert_equal(type, OleFileIO.STGTY_STREAM) + + +def test_get_size(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + + # Act + size = ole.get_size('worddocument') + + # Assert + assert_greater(size, 0) + + +def test_get_rootentry_name(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + + # Act + root = ole.get_rootentry_name() + + # Assert + assert_equal(root, "Root Entry") + + +def test_meta(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + + # Act + meta = ole.get_metadata() + + # Assert + assert_equal(meta.author, "Laurence Ipsum") + assert_equal(meta.num_pages, 1) + +# End of file diff --git a/Tests/tester.py b/Tests/tester.py index 5900a7f3a..41f27e4ff 100644 --- a/Tests/tester.py +++ b/Tests/tester.py @@ -94,6 +94,34 @@ def assert_deep_equal(a, b, msg=None): assert_equal(a, b, msg) +def assert_greater(a, b, msg=None): + if a > b: + success() + else: + failure(msg or "got %r, expected %r" % (a, b)) + + +def assert_greater_equal(a, b, msg=None): + if a >= b: + success() + else: + failure(msg or "got %r, expected %r" % (a, b)) + + +def assert_less(a, b, msg=None): + if a < b: + success() + else: + failure(msg or "got %r, expected %r" % (a, b)) + + +def assert_less_equal(a, b, msg=None): + if a <= b: + success() + else: + failure(msg or "got %r, expected %r" % (a, b)) + + def assert_match(v, pattern, msg=None): import re if re.match(pattern, v): From bfc05b7a8cbbd678cbd837791450c5d67d19922e Mon Sep 17 00:00:00 2001 From: hugovk Date: Fri, 11 Apr 2014 15:02:42 +0300 Subject: [PATCH 38/56] Handle bytes in test for Py2 and 3 --- Tests/test_olefileio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py index 3cd581b2e..3d65d9018 100644 --- a/Tests/test_olefileio.py +++ b/Tests/test_olefileio.py @@ -95,7 +95,7 @@ def test_meta(): meta = ole.get_metadata() # Assert - assert_equal(meta.author, "Laurence Ipsum") + assert_equal(meta.author, b"Laurence Ipsum") assert_equal(meta.num_pages, 1) # End of file From be201bf4f3f377b64588876f3e7914a7e56faba6 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 12 Apr 2014 12:43:24 +0300 Subject: [PATCH 39/56] Undo cleanup reversal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Just pointing out that this bit is actually reversing a recent cleanup in Pillow. See commit 923018d and PR #474. I guess that cleanup was merged into Pillow after I did my merge from Pillow to PL’s fork." https://github.com/python-imaging/Pillow/pull/618#discussion_r11559186 --- PIL/OleFileIO.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PIL/OleFileIO.py b/PIL/OleFileIO.py index c99300dc1..8a3c77be4 100644 --- a/PIL/OleFileIO.py +++ b/PIL/OleFileIO.py @@ -1033,7 +1033,7 @@ class OleFileIO: if entry[1:2] == "Image": fin = ole.openstream(entry) fout = open(entry[0:1], "wb") - while 1: + while True: s = fin.read(8192) if not s: break From d5dee90f412def249a888f46e426464e8532c86f Mon Sep 17 00:00:00 2001 From: Steven Myint Date: Sat, 12 Apr 2014 07:08:50 -0700 Subject: [PATCH 40/56] Fix spelling errors in documentation https://pillow.readthedocs.org/en/latest/about.html --- docs/about.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/about.rst b/docs/about.rst index b6ae2eaf9..e8c9356dc 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -39,7 +39,7 @@ Why a fork? PIL is not setuptools compatible. Please see `this Image-SIG post`_ for a more detailed explanation. Also, PIL's current bi-yearly (or greater) release -schedule is too infrequent to accomodate the large number and frequency of +schedule is too infrequent to accommodate the large number and frequency of issues reported. .. _this Image-SIG post: https://mail.python.org/pipermail/image-sig/2010-August/006480.html @@ -52,7 +52,7 @@ What about PIL? Prior to Pillow 2.0.0, very few image code changes were made. Pillow 2.0.0 added Python 3 support and includes many bug fixes from many contributors. -As more time passes since the last PIL release, the likelyhood of a new PIL +As more time passes since the last PIL release, the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement. So if you still want to support PIL, please `report issues here first`_, then From deecfdabcd3748c7aec6fe2d9c8f168bd4bb4480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Cuenca=20Abela?= Date: Sun, 13 Apr 2014 00:28:26 +0200 Subject: [PATCH 41/56] Update installation instructions of little cms2 in Mac --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 715a6ac30..94054df82 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -143,7 +143,7 @@ distribution. Otherwise, use whatever XCode you used to compile Python.) The easiest way to install the prerequisites is via `Homebrew `_. After you install Homebrew, run:: - $ brew install libtiff libjpeg webp littlecms + $ brew install libtiff libjpeg webp little-cms2 If you've built your own Python, then you should be able to install Pillow using:: From f40cf7870f7c030bafe49d6a8ec536010d8cdf87 Mon Sep 17 00:00:00 2001 From: hugovk Date: Sun, 13 Apr 2014 23:16:01 +0300 Subject: [PATCH 42/56] More testing --- Tests/test_olefileio.py | 57 +++++++++++++++++++++++++++++++++++++++++ Tests/tester.py | 24 ++++++++++++++--- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py index 3d65d9018..d21b704e3 100644 --- a/Tests/test_olefileio.py +++ b/Tests/test_olefileio.py @@ -1,5 +1,6 @@ from __future__ import print_function from tester import * +import datetime import PIL.OleFileIO as OleFileIO @@ -36,6 +37,7 @@ def test_exists_worddocument(): # Assert assert_true(exists) + ole.close() def test_exists_no_vba_macros(): @@ -48,6 +50,7 @@ def test_exists_no_vba_macros(): # Assert assert_false(exists) + ole.close() def test_get_type(): @@ -60,6 +63,7 @@ def test_get_type(): # Assert assert_equal(type, OleFileIO.STGTY_STREAM) + ole.close() def test_get_size(): @@ -72,6 +76,7 @@ def test_get_size(): # Assert assert_greater(size, 0) + ole.close() def test_get_rootentry_name(): @@ -84,6 +89,7 @@ def test_get_rootentry_name(): # Assert assert_equal(root, "Root Entry") + ole.close() def test_meta(): @@ -97,5 +103,56 @@ def test_meta(): # Assert assert_equal(meta.author, b"Laurence Ipsum") assert_equal(meta.num_pages, 1) + ole.close() + +def test_gettimes(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + root_entry = ole.direntries[0] + + # Act + ctime = root_entry.getmtime() + mtime = root_entry.getmtime() + + # Assert + assert_is_instance(ctime, datetime.datetime) + assert_is_instance(mtime, datetime.datetime) + assert_equal(ctime.year, 2014) + ole.close() + + +def test_listdir(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + + # Act + dirlist = ole.listdir() + + # Assert + assert_in(['WordDocument'], dirlist) + ole.close() + + +def test_debug(): + # Arrange + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + meta = ole.get_metadata() + + # Act + OleFileIO.set_debug_mode(True) + ole.dumpdirectory() + meta.dump() + + OleFileIO.set_debug_mode(False) + ole.dumpdirectory() + meta.dump() + + # Assert + # No assert, just check they run ok + ole.close() + # End of file diff --git a/Tests/tester.py b/Tests/tester.py index 41f27e4ff..32da48e98 100644 --- a/Tests/tester.py +++ b/Tests/tester.py @@ -98,28 +98,44 @@ def assert_greater(a, b, msg=None): if a > b: success() else: - failure(msg or "got %r, expected %r" % (a, b)) + failure(msg or "%r unexpectedly not greater than %r" % (a, b)) def assert_greater_equal(a, b, msg=None): if a >= b: success() else: - failure(msg or "got %r, expected %r" % (a, b)) + failure( + msg or "%r unexpectedly not greater than or equal to %r" % (a, b)) def assert_less(a, b, msg=None): if a < b: success() else: - failure(msg or "got %r, expected %r" % (a, b)) + failure(msg or "%r unexpectedly not less than %r" % (a, b)) def assert_less_equal(a, b, msg=None): if a <= b: success() else: - failure(msg or "got %r, expected %r" % (a, b)) + failure( + msg or "%r unexpectedly not less than or equal to %r" % (a, b)) + + +def assert_is_instance(a, b, msg=None): + if isinstance(a, b): + success() + else: + failure(msg or "got %r, expected %r" % (type(a), b)) + + +def assert_in(a, b, msg=None): + if a in b: + success() + else: + failure(msg or "%r unexpectedly not in %r" % (a, b)) def assert_match(v, pattern, msg=None): From 949e87e55d3c17e043ca6d88724c346a0b027608 Mon Sep 17 00:00:00 2001 From: hugovk Date: Sun, 13 Apr 2014 23:28:41 +0300 Subject: [PATCH 43/56] Remove 'failing' test_debug() --- Tests/test_olefileio.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py index d21b704e3..b4531e641 100644 --- a/Tests/test_olefileio.py +++ b/Tests/test_olefileio.py @@ -135,24 +135,4 @@ def test_listdir(): ole.close() -def test_debug(): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - meta = ole.get_metadata() - - # Act - OleFileIO.set_debug_mode(True) - ole.dumpdirectory() - meta.dump() - - OleFileIO.set_debug_mode(False) - ole.dumpdirectory() - meta.dump() - - # Assert - # No assert, just check they run ok - ole.close() - - # End of file From 6ff77414b3fd55fb243f4e27c4df9de0598e9098 Mon Sep 17 00:00:00 2001 From: hugovk Date: Mon, 14 Apr 2014 00:05:31 +0300 Subject: [PATCH 44/56] Test some debug functions. No asserts, just check they run ok. --- Tests/run.py | 5 +++++ Tests/test_olefileio.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Tests/run.py b/Tests/run.py index 4dccc005a..758923f0f 100644 --- a/Tests/run.py +++ b/Tests/run.py @@ -58,6 +58,11 @@ for file in files: )) result = out.read() + result_lines = result.splitlines() + if len(result_lines): + if result_lines[0] == "ignore_all_except_last_line": + result = result_lines[-1] + # Extract any ignore patterns ignore_pats = ignore_re.findall(result) result = ignore_re.sub('', result) diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py index b4531e641..f31845556 100644 --- a/Tests/test_olefileio.py +++ b/Tests/test_olefileio.py @@ -135,4 +135,26 @@ def test_listdir(): ole.close() +def test_debug(): + # Arrange + print("ignore_all_except_last_line") + ole_file = "Tests/images/test-ole-file.doc" + ole = OleFileIO.OleFileIO(ole_file) + meta = ole.get_metadata() + + # Act + OleFileIO.set_debug_mode(True) + ole.dumpdirectory() + meta.dump() + + OleFileIO.set_debug_mode(False) + ole.dumpdirectory() + meta.dump() + + # Assert + # No assert, just check they run ok + print("ok") + ole.close() + + # End of file From 7ba5962512cad6abaa5c6c8313d3cdb877a0daea Mon Sep 17 00:00:00 2001 From: hugovk Date: Mon, 14 Apr 2014 00:27:08 +0300 Subject: [PATCH 45/56] Fix typo and update test --- Tests/test_olefileio.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py index f31845556..98374c4ca 100644 --- a/Tests/test_olefileio.py +++ b/Tests/test_olefileio.py @@ -105,6 +105,7 @@ def test_meta(): assert_equal(meta.num_pages, 1) ole.close() + def test_gettimes(): # Arrange ole_file = "Tests/images/test-ole-file.doc" @@ -112,13 +113,14 @@ def test_gettimes(): root_entry = ole.direntries[0] # Act - ctime = root_entry.getmtime() + ctime = root_entry.getctime() mtime = root_entry.getmtime() # Assert - assert_is_instance(ctime, datetime.datetime) + assert_is_instance(ctime, type(None)) assert_is_instance(mtime, datetime.datetime) - assert_equal(ctime.year, 2014) + assert_equal(ctime, None) + assert_equal(mtime.year, 2014) ole.close() From aba195d35eb343f1142a8a4433c7305c28ea3921 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 14 Apr 2014 06:11:39 -0700 Subject: [PATCH 46/56] Tests for #614 --- Tests/test_file_tiff.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 9041b2046..804ae04e4 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -128,3 +128,14 @@ def test_12bit_rawmode(): print (im2.getpixel((0,2))) assert_image_equal(im, im2) + +def test_32bit_float(): + # Issue 614, specific 32 bit float format + path = 'Tests/images/10ct_32bit_128.tiff' + im = Image.open(path) + im.load() + + assert_equal(im.getpixel((0,0)), -0.4526388943195343) + assert_equal(im.getextrema(), (-3.140936851501465, 3.140684127807617)) + + From 6bf95f003bad4a3b1cf162c3f708505f8f168f1f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2014 12:12:29 -0700 Subject: [PATCH 47/56] Update CHANGES.rst --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f83c6f339..961e85619 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changelog (Pillow) 2.5.0 (unreleased) ------------------ +- Added support for additional TIFF floating point format + [Hijackal] + - Have the tempfile use a suffix with a dot [wiredfool] From a54754db1f304e0b7b416822b68f455519dcf5eb Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2014 12:20:20 -0700 Subject: [PATCH 48/56] Update CHANGES.rst --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 961e85619..19ec55fbc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changelog (Pillow) 2.5.0 (unreleased) ------------------ +- Updated OleFileIO to version 0.30 from upstream + [hugovk] + - Added support for additional TIFF floating point format [Hijackal] From b182e239bbe77d50d7b60d734b4c421688b2d862 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2014 12:23:19 -0700 Subject: [PATCH 49/56] Updated Changes.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 19ec55fbc..5298efb22 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changelog (Pillow) 2.5.0 (unreleased) ------------------ +- Added support for 16 bit PGM files. + [wiredfool] + - Updated OleFileIO to version 0.30 from upstream [hugovk] From 8c6a4c0299f33aa05ddba19041d15b4c10684095 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2014 21:53:49 -0700 Subject: [PATCH 50/56] Docs changes for close/context manager --- PIL/Image.py | 27 ++++++++++++++++++--------- docs/handbook/tutorial.rst | 4 ++-- docs/reference/Image.rst | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index 99acb78fa..333397701 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -497,16 +497,23 @@ class Image: _makeself = _new # compatibility - # with compatibility + # Context Manager Support def __enter__(self): return self def __exit__(self, *args): self.close() def close(self): - """ Close the file pointer, if possible. Destroy the image core. - This releases memory, and the image will be unusable afterward - """ + """ + Closes the file pointer, if possible. + + This operation will destroy the image core and release it's memory. + The image data will be unusable afterward. + + This function is only required to close images that have not + had their file read and closed by the + :py:meth:`~PIL.Image.Image.load` method. + """ try: self.fp.close() except Exception as msg: @@ -664,7 +671,8 @@ class Image: Allocates storage for the image and loads the pixel data. In normal cases, you don't need to call this method, since the Image class automatically loads an opened image when it is - accessed for the first time. + accessed for the first time. This method will close the file + associated with the image. :returns: An image access object. """ @@ -2096,10 +2104,11 @@ def open(fp, mode="r"): """ Opens and identifies the given image file. - This is a lazy operation; this function identifies the file, but the - actual image data is not read from the file until you try to process - the data (or call the :py:meth:`~PIL.Image.Image.load` method). - See :py:func:`~PIL.Image.new`. + This is a lazy operation; this function identifies the file, but + the file remains open and the actual image data is not read from + the file until you try to process the data (or call the + :py:meth:`~PIL.Image.Image.load` method). See + :py:func:`~PIL.Image.new`. :param file: A filename (string) or a file object. The file object must implement :py:meth:`~file.read`, :py:meth:`~file.seek`, and diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 9ce50da7d..05d619f40 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -126,8 +126,8 @@ Identify Image Files for infile in sys.argv[1:]: try: - im = Image.open(infile) - print(infile, im.format, "%dx%d" % im.size, im.mode) + with Image.open(infile) as im: + print(infile, im.format, "%dx%d" % im.size, im.mode) except IOError: pass diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index fe13c882b..7125fcad4 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -136,9 +136,9 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.verify .. automethod:: PIL.Image.Image.fromstring -.. deprecated:: 2.0 .. automethod:: PIL.Image.Image.load +.. automethod:: PIL.Image.Image.close Attributes ---------- From 471cecb5237b8481d0ce9469bd4974e6ab299836 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2014 22:01:55 -0700 Subject: [PATCH 51/56] tests for close and context manager --- Tests/test_image_load.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index d28452c41..b385b9686 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -2,6 +2,8 @@ from tester import * from PIL import Image +import os + def test_sanity(): im = lena() @@ -9,3 +11,17 @@ def test_sanity(): pix = im.load() assert_equal(pix[0, 0], (223, 162, 133)) + +def test_close(): + im = Image.open("Images/lena.gif") + assert_no_exception(lambda: im.close()) + assert_exception(ValueError, lambda: im.load()) + assert_exception(ValueError, lambda: im.getpixel((0,0))) + +def test_contextmanager(): + fn = None + with Image.open("Images/lena.gif") as im: + fn = im.fp.fileno() + assert_no_exception(lambda: os.fstat(fn)) + + assert_exception(OSError, lambda: os.fstat(fn)) From 8c5ed8a8733483b5d72c72c0e5a13c3e79453b3b Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2014 22:22:16 -0700 Subject: [PATCH 52/56] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5298efb22..c774a6ddf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changelog (Pillow) 2.5.0 (unreleased) ------------------ +- Added Image.close, context manager support. + [wiredfool] + - Added support for 16 bit PGM files. [wiredfool] From b917623513466b0067b36964d664228bff34e527 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 18 Apr 2014 15:11:05 +0300 Subject: [PATCH 53/56] Run slow pypy first It's always the slowest and we should give it a head start so we're not waiting for it to finish at the end. It means we're making the most use of our parallel job-runners for the quicker jobs. See also #632. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 34ffcfe1a..07862a6c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,11 +8,11 @@ notifications: irc: "chat.freenode.net#pil" python: + - "pypy" - 2.6 - 2.7 - 3.2 - 3.3 - - "pypy" install: - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev cmake" From 522e0ff31cdb4e0ad40f26526530d3d265744757 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 30 Apr 2014 10:06:35 +0300 Subject: [PATCH 54/56] Python 3.4 is live on Travis CI http://blog.travis-ci.com/2014-04-28-upcoming-build-environment-updates/ https://twitter.com/travisci/status/461365365587456000 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 07862a6c1..937127492 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ python: - 2.7 - 3.2 - 3.3 + - 3.4 install: - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev cmake" From d8e4ed11986d410dab77c0c5ff5d9f94d335aafb Mon Sep 17 00:00:00 2001 From: hugovk Date: Wed, 30 Apr 2014 11:05:15 +0300 Subject: [PATCH 55/56] system_site_packages was causing build errors. Allow 3.4 as a failure for now, as a PendingDeprecationWarning is causing failures; this is a problem with the test runner not liking any print output. --- .travis.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 937127492..cc039d91e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,5 @@ language: python -# for python-qt4 -virtualenv: - system_site_packages: true - notifications: irc: "chat.freenode.net#pil" @@ -13,7 +9,6 @@ python: - 2.7 - 3.2 - 3.3 - - 3.4 install: - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev cmake" @@ -43,3 +38,4 @@ after_success: matrix: allow_failures: - python: "pypy" + - python: 3.4 From 0de2212653a25190a8e8be61e63cd8607bfb9194 Mon Sep 17 00:00:00 2001 From: hugovk Date: Wed, 30 Apr 2014 11:07:43 +0300 Subject: [PATCH 56/56] Add 3.4 back --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index cc039d91e..ccc6e7abf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ python: - 2.7 - 3.2 - 3.3 + - 3.4 install: - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev cmake"